Files
sub2api/backend/migrations/125_add_channel_monitors.sql
erio 20a4e41872 feat(monitor): admin channel monitor MVP with SSRF protection and batch aggregation
新增 admin「渠道监控」模块(参考 BingZi-233/check-cx),独立于现有 Channel 体系。
admin 配置 + 后台定时调用上游 LLM chat completions 健康检查 + 所有登录用户只读可见。

后端:
- ent: channel_monitor + channel_monitor_history(AES-256-GCM 加密 api_key)
- service 按职责拆分:service/aggregator/validate/checker/runner/ssrf
- provider strategy map 替代 switch(openai/anthropic/gemini)
- repository batch 聚合(ListLatestForMonitorIDs + ComputeAvailabilityForMonitors)消除 N+1
- runner: ticker(5s) + pond worker pool(5) + inFlight 防并发 + TrySubmit 防雪崩
  + 凌晨 3 点 cron 清理 30 天历史
- SSRF 防护:强制 https + 私网/loopback/云元数据 IP 拒绝(127/8、10/8、172.16/12、
  192.168/16、169.254/16、100.64/10、::1、fc00::/7、fe80::/10)+ DialContext
  在 socket 层防 DNS rebinding
- API key sanitize:擦除 url.Error 与上游响应 body 中的 sk-/sk-ant-/AIza/JWT 模式
- APIKeyDecryptFailed 标志位 + 单 monitor 路径检测,避免空 key 调用上游

handler:
- admin: CRUD + 手动触发 + 历史接口(api_key 脱敏)
- user: 只读列表 + 状态详情(去除 api_key/endpoint)
- ParseChannelMonitorID 共用 + dto.ChannelMonitorExtraModelStatus 共用

前端:
- 路由 /admin/channels/{pricing,monitor} + /monitor(用户只读)
- AppSidebar 父项 expandOnly 支持
- ChannelMonitorView 拆为 8 个子组件 + ChannelStatusView 拆出 detail dialog
- composables/useChannelMonitorFormat + constants/channelMonitor 共享
- i18n monitorCommon namespace 消除 admin/user 两 view 重复

合规:所有文件符合 CLAUDE.md(Go ≤ 500 行 / Vue ≤ 300 行 / 函数 ≤ 30 行)
CI: go build / gofmt / golangci-lint(0 issues) / make test-unit / pnpm build 全绿
2026-04-20 20:21:02 +08:00

59 lines
3.0 KiB
SQL
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.

-- Migration: 125_add_channel_monitors
-- 渠道监控 MVP周期性对外部 provider/endpoint/api_key 做模型心跳测试。
--
-- 表结构说明:
-- - channel_monitors 渠道配置表(一行 = 一个监控对象)
-- - channel_monitor_histories 检测历史明细表(一次检测一个模型 = 一行)
--
-- 设计要点:
-- - api_key_encrypted 列存放 AES-256-GCM 密文base64由 service 层加密。
-- - extra_models 用 JSONB 存储字符串数组,便于扩展(后续可加权重等元数据)。
-- - history 表通过 ON DELETE CASCADE 自动清理已删除监控的历史。
-- - (enabled, last_checked_at) 索引服务于调度器扫描“到期需要检测”的监控。
-- - histories 上 (monitor_id, model, checked_at DESC) 服务用户视图聚合查询;
-- 单独的 (checked_at) 索引服务定期清理 30 天前数据的 DELETE。
CREATE TABLE IF NOT EXISTS channel_monitors (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
provider VARCHAR(20) NOT NULL, -- openai / anthropic / gemini
endpoint VARCHAR(500) NOT NULL, -- base origin
api_key_encrypted TEXT NOT NULL, -- AES-256-GCM (base64)
primary_model VARCHAR(200) NOT NULL,
extra_models JSONB NOT NULL DEFAULT '[]'::jsonb,
group_name VARCHAR(100) NOT NULL DEFAULT '',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
interval_seconds INT NOT NULL,
last_checked_at TIMESTAMPTZ,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT channel_monitors_provider_check CHECK (provider IN ('openai', 'anthropic', 'gemini')),
CONSTRAINT channel_monitors_interval_check CHECK (interval_seconds BETWEEN 15 AND 3600)
);
CREATE INDEX IF NOT EXISTS idx_channel_monitors_enabled_last_checked
ON channel_monitors (enabled, last_checked_at);
CREATE INDEX IF NOT EXISTS idx_channel_monitors_provider
ON channel_monitors (provider);
CREATE INDEX IF NOT EXISTS idx_channel_monitors_group_name
ON channel_monitors (group_name);
CREATE TABLE IF NOT EXISTS channel_monitor_histories (
id BIGSERIAL PRIMARY KEY,
monitor_id BIGINT NOT NULL REFERENCES channel_monitors(id) ON DELETE CASCADE,
model VARCHAR(200) NOT NULL,
status VARCHAR(20) NOT NULL,
latency_ms INT,
ping_latency_ms INT,
message VARCHAR(500) NOT NULL DEFAULT '',
checked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT channel_monitor_histories_status_check
CHECK (status IN ('operational', 'degraded', 'failed', 'error'))
);
CREATE INDEX IF NOT EXISTS idx_channel_monitor_histories_monitor_model_checked
ON channel_monitor_histories (monitor_id, model, checked_at DESC);
CREATE INDEX IF NOT EXISTS idx_channel_monitor_histories_checked_at
ON channel_monitor_histories (checked_at);