Files
sub2api/backend/internal/service/channel_monitor_const.go
erio 8cf83c984e feat(channel-monitor): aggregate history to daily rollups + soft delete
明细只保留 1 天,超过 1 天聚合到新表 channel_monitor_daily_rollups(按
monitor_id/model/bucket_date 维度),聚合保留 30 天。两张表都用 SoftDeleteMixin
软删除(DELETE 自动改为 UPDATE deleted_at = NOW())。

聚合 + 清理任务由 OpsCleanupService 的 cron 统一调度,与运维监控的清理共享
schedule(默认 0 2 * * *)和 leader lock。ChannelMonitorRunner 的 cleanupLoop
被移除,只保留 dueCheckLoop。

读取路径 ComputeAvailability* 改为 UNION 明细(今天 deleted_at IS NULL)+
聚合(过去 windowDays 天 deleted_at IS NULL),SUM(ok)/SUM(total) 自然加权
计算可用率,AVG latency 用 SUM(sum_latency_ms)/SUM(count_latency)。

watermark 表 channel_monitor_aggregation_watermark 单行(id=1),记录
last_aggregated_date,重启后从该日期 +1 继续聚合,首次为 nil 则从
today - 30d 开始回填,单次最多 35 天上限避免长事务。

raw SQL 的 ListLatestPerModel / ListLatestForMonitorIDs / ListRecentHistoryForMonitors
都补上 deleted_at IS NULL 过滤(SoftDeleteMixin interceptor 只对 ent query 生效)。

bump version to 0.1.114.28

GroupBadge 在 MonitorKeyPickerDialog 中复用平台主题色 + 倍率/专属倍率
(顺手优化)。
2026-04-21 10:10:56 +08:00

140 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"time"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
)
// ChannelMonitor 全局常量。
// 这些是 MVP 阶段的硬编码值,按需可以提到 config 中。
const (
// monitorRequestTimeout 单次模型请求总超时(含 Body 读取)。
monitorRequestTimeout = 45 * time.Second
// monitorPingTimeout HEAD 请求 endpoint origin 的超时。
monitorPingTimeout = 8 * time.Second
// monitorDegradedThreshold 主请求成功但耗时超过该阈值视为 degraded。
monitorDegradedThreshold = 6 * time.Second
// monitorHistoryRetentionDays 明细历史保留天数。
// 明细只保留 1 天,超出由 SoftDeleteMixin 软删;
// 维护任务每天凌晨跑(由 OpsCleanupService 统一调度)。
monitorHistoryRetentionDays = 1
// monitorRollupRetentionDays 日聚合保留天数。
// 日聚合行由 RunDailyMaintenance 在超过该窗口后软删。
monitorRollupRetentionDays = 30
// monitorMaintenanceMaxDaysPerRun 单次维护任务最多聚合的天数。
// 用于限制首次上线回填30 天)+ 少量余量,避免长事务。
monitorMaintenanceMaxDaysPerRun = 35
// monitorWorkerConcurrency 调度器并发执行的监控数pond 池容量)。
monitorWorkerConcurrency = 5
// monitorTickerInterval 调度器扫描"到期监控"的间隔。
monitorTickerInterval = 5 * time.Second
// monitorMinIntervalSeconds / monitorMaxIntervalSeconds 用户配置的检测间隔上下限。
monitorMinIntervalSeconds = 15
monitorMaxIntervalSeconds = 3600
// monitorMessageMaxBytes message 字段最大字节数(与 schema/migration 一致)。
monitorMessageMaxBytes = 500
// monitorResponseMaxBytes 单次模型响应最大读取字节,防止 OOM。
monitorResponseMaxBytes = 64 * 1024
// monitorChallengeMin / monitorChallengeMax challenge 操作数范围。
monitorChallengeMin = 1
monitorChallengeMax = 50
// providerOpenAIPath OpenAI Chat Completions 路径。
providerOpenAIPath = "/v1/chat/completions"
// providerAnthropicPath Anthropic Messages 路径。
providerAnthropicPath = "/v1/messages"
// providerGeminiPathTemplate Gemini generateContent 路径模板(含 model 占位)。
providerGeminiPathTemplate = "/v1beta/models/%s:generateContent"
// MonitorProviderOpenAI / Anthropic / Gemini provider 字符串常量(也是 ent enum 的实际值)。
MonitorProviderOpenAI = "openai"
MonitorProviderAnthropic = "anthropic"
MonitorProviderGemini = "gemini"
// MonitorStatusOperational 等监控状态字符串常量(与 ent enum 一致)。
MonitorStatusOperational = "operational"
MonitorStatusDegraded = "degraded"
MonitorStatusFailed = "failed"
MonitorStatusError = "error"
// monitorAvailability7Days / 15 / 30 用于聚合查询窗口。
monitorAvailability7Days = 7
monitorAvailability15Days = 15
monitorAvailability30Days = 30
// MonitorHistoryDefaultLimit 历史查询默认返回条数handler 层共享)。
MonitorHistoryDefaultLimit = 100
// MonitorHistoryMaxLimit 历史查询最大返回条数handler 层共享)。
MonitorHistoryMaxLimit = 1000
// monitorTimelineMaxPoints 用户视图 timeline 每个监控最多返回的历史点数。
monitorTimelineMaxPoints = 60
// monitorEndpointResolveTimeout validateEndpoint 解析 hostname 的最长耗时。
monitorEndpointResolveTimeout = 5 * time.Second
// ---- checker / runner 行为参数(消除 magic 值)----
// monitorAnthropicAPIVersion Anthropic Messages API 版本头。
monitorAnthropicAPIVersion = "2023-06-01"
// monitorChallengeMaxTokens 单次 challenge 请求的 max_tokens足够回答个位数算术
monitorChallengeMaxTokens = 50
// monitorListDueTimeout tickDueChecks 查询到期监控的总超时。
monitorListDueTimeout = 10 * time.Second
// monitorRunOneBuffer runOne 的总超时缓冲(除请求超时与 ping 超时外的额外裕量)。
monitorRunOneBuffer = 10 * time.Second
// monitorIdleConnTimeout HTTP transport 空闲连接关闭超时。
monitorIdleConnTimeout = 30 * time.Second
// monitorTLSHandshakeTimeout HTTP transport TLS 握手超时。
monitorTLSHandshakeTimeout = 10 * time.Second
// monitorResponseHeaderTimeout HTTP transport 等待响应头超时。
monitorResponseHeaderTimeout = 30 * time.Second
// monitorPingDiscardMaxBytes ping 时丢弃响应体的最大字节数。
monitorPingDiscardMaxBytes = 1024
// monitorDialTimeout 自定义 dialer 单次连接超时。
monitorDialTimeout = 10 * time.Second
// monitorDialKeepAlive 自定义 dialer keep-alive 间隔。
monitorDialKeepAlive = 30 * time.Second
)
// 业务错误(统一在此声明,避免散落)。
var (
ErrChannelMonitorNotFound = infraerrors.NotFound(
"CHANNEL_MONITOR_NOT_FOUND", "channel monitor not found",
)
ErrChannelMonitorInvalidProvider = infraerrors.BadRequest(
"CHANNEL_MONITOR_INVALID_PROVIDER", "provider must be one of openai/anthropic/gemini",
)
ErrChannelMonitorInvalidInterval = infraerrors.BadRequest(
"CHANNEL_MONITOR_INVALID_INTERVAL", "interval_seconds must be in [15, 3600]",
)
ErrChannelMonitorInvalidEndpoint = infraerrors.BadRequest(
"CHANNEL_MONITOR_INVALID_ENDPOINT", "endpoint must be a valid https URL",
)
ErrChannelMonitorEndpointScheme = infraerrors.BadRequest(
"CHANNEL_MONITOR_ENDPOINT_SCHEME", "endpoint must use https scheme",
)
ErrChannelMonitorEndpointPath = infraerrors.BadRequest(
"CHANNEL_MONITOR_ENDPOINT_PATH", "endpoint must be base origin only (no path/query/fragment)",
)
ErrChannelMonitorEndpointPrivate = infraerrors.BadRequest(
"CHANNEL_MONITOR_ENDPOINT_PRIVATE", "endpoint must be a public host",
)
ErrChannelMonitorEndpointUnreachable = infraerrors.BadRequest(
"CHANNEL_MONITOR_ENDPOINT_UNREACHABLE", "endpoint hostname could not be resolved",
)
ErrChannelMonitorMissingAPIKey = infraerrors.BadRequest(
"CHANNEL_MONITOR_MISSING_API_KEY", "api_key is required when creating a monitor",
)
ErrChannelMonitorMissingPrimaryModel = infraerrors.BadRequest(
"CHANNEL_MONITOR_MISSING_PRIMARY_MODEL", "primary_model is required",
)
ErrChannelMonitorAPIKeyDecryptFailed = infraerrors.InternalServer(
"CHANNEL_MONITOR_KEY_DECRYPT_FAILED", "api key decryption failed; please re-edit the monitor with a fresh key",
)
)