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 全绿
This commit is contained in:
64
backend/ent/schema/channel_monitor_history.go
Normal file
64
backend/ent/schema/channel_monitor_history.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect/entsql"
|
||||
"entgo.io/ent/schema"
|
||||
"entgo.io/ent/schema/edge"
|
||||
"entgo.io/ent/schema/field"
|
||||
"entgo.io/ent/schema/index"
|
||||
)
|
||||
|
||||
// ChannelMonitorHistory holds the schema definition for the ChannelMonitorHistory entity.
|
||||
// 渠道监控历史:每次检测每个模型一行记录,由调度器写入,定期清理 30 天前的旧数据。
|
||||
type ChannelMonitorHistory struct {
|
||||
ent.Schema
|
||||
}
|
||||
|
||||
func (ChannelMonitorHistory) Annotations() []schema.Annotation {
|
||||
return []schema.Annotation{
|
||||
entsql.Annotation{Table: "channel_monitor_histories"},
|
||||
}
|
||||
}
|
||||
|
||||
func (ChannelMonitorHistory) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.Int64("monitor_id"),
|
||||
field.String("model").
|
||||
NotEmpty().
|
||||
MaxLen(200),
|
||||
field.Enum("status").
|
||||
Values("operational", "degraded", "failed", "error"),
|
||||
field.Int("latency_ms").
|
||||
Optional().
|
||||
Nillable(),
|
||||
field.Int("ping_latency_ms").
|
||||
Optional().
|
||||
Nillable(),
|
||||
field.String("message").
|
||||
Optional().
|
||||
Default("").
|
||||
MaxLen(500),
|
||||
field.Time("checked_at").
|
||||
Default(time.Now),
|
||||
}
|
||||
}
|
||||
|
||||
func (ChannelMonitorHistory) Edges() []ent.Edge {
|
||||
return []ent.Edge{
|
||||
edge.From("monitor", ChannelMonitor.Type).
|
||||
Ref("history").
|
||||
Field("monitor_id").
|
||||
Unique().
|
||||
Required(),
|
||||
}
|
||||
}
|
||||
|
||||
func (ChannelMonitorHistory) Indexes() []ent.Index {
|
||||
return []ent.Index{
|
||||
index.Fields("monitor_id", "model", "checked_at"),
|
||||
index.Fields("checked_at"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user