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:
erio
2026-04-20 20:21:02 +08:00
parent 0b85a8da88
commit 20a4e41872
67 changed files with 14997 additions and 32 deletions

View File

@@ -245,6 +245,7 @@ export default {
// Common
common: {
loading: 'Loading...',
submitting: 'Submitting...',
justNow: 'just now',
save: 'Save',
saved: 'Saved successfully',
@@ -363,7 +364,11 @@ export default {
orderManagement: 'Orders',
paymentDashboard: 'Payment Dashboard',
paymentConfig: 'Payment Config',
paymentPlans: 'Plans'
paymentPlans: 'Plans',
channelManagement: 'Channels',
channelPricing: 'Channel Pricing',
channelMonitor: 'Channel Monitor',
channelStatus: 'Channel Status',
},
// Auth
@@ -846,6 +851,58 @@ export default {
userAgent: 'User-Agent'
},
// Shared keys for channel monitor (admin + user views)
monitorCommon: {
status: {
operational: 'Operational',
degraded: 'Degraded',
failed: 'Failed',
error: 'Error',
unknown: '-'
},
providers: {
openai: 'OpenAI',
anthropic: 'Anthropic',
gemini: 'Gemini'
},
extraModelsHeader: 'Extra Models',
extraModelsEmpty: 'No extra models',
latencyEmpty: '-'
},
// Channel Status (user-facing read-only view)
channelStatus: {
title: 'Channel Status',
description: 'Inspect channel availability, latency and recent status',
searchPlaceholder: 'Search channels...',
allProviders: 'All Providers',
loadError: 'Failed to load channel status',
detailLoadError: 'Failed to load channel detail',
detailTitle: 'Channel Detail',
closeDetail: 'Close',
columns: {
name: 'Name',
provider: 'Provider',
groupName: 'Group',
primaryModel: 'Primary Model',
availability7d: '7d Availability',
latency: 'Latency (ms)'
},
detailColumns: {
model: 'Model',
latestStatus: 'Latest Status',
latestLatency: 'Latest Latency (ms)',
availability7d: '7d Availability',
availability15d: '15d Availability',
availability30d: '30d Availability',
avgLatency7d: '7d Avg Latency (ms)'
},
empty: {
title: 'No channels available',
description: 'No monitored channels have been configured yet.'
}
},
// Redeem
redeem: {
title: 'Redeem Code',
@@ -2014,6 +2071,69 @@ export default {
}
},
// Channel Monitor
channelMonitor: {
title: 'Channel Monitor',
description: 'Monitor channel availability, latency and status',
searchPlaceholder: 'Search monitor name...',
allProviders: 'All Providers',
allStatus: 'All Status',
enabledFilter: 'Enabled',
onlyEnabled: 'Enabled only',
onlyDisabled: 'Disabled only',
createButton: 'Create Monitor',
createTitle: 'Create Channel Monitor',
editTitle: 'Edit Channel Monitor',
runNow: 'Run Now',
runSuccess: 'Check completed',
runFailed: 'Check failed',
apiKeyDecryptFailed: 'API Key decryption failed. Please re-edit this monitor with a fresh key.',
createSuccess: 'Monitor created',
updateSuccess: 'Monitor updated',
deleteSuccess: 'Monitor deleted',
loadError: 'Failed to load monitors',
deleteConfirm: 'Are you sure you want to delete monitor "{name}"? This action cannot be undone.',
nameRequired: 'Please enter a monitor name',
primaryModelRequired: 'Please enter a primary model',
columns: {
name: 'Name',
provider: 'Provider',
primaryModel: 'Primary Model',
availability7d: '7d Availability',
latency: 'Latency (ms)',
enabled: 'Enabled',
actions: 'Actions'
},
form: {
name: 'Name',
namePlaceholder: 'Enter monitor name',
provider: 'Provider',
endpoint: 'Endpoint',
endpointPlaceholder: 'https://api.example.com',
useCurrentDomain: 'Use current service',
apiKey: 'API Key',
apiKeyPlaceholder: 'Enter API Key',
apiKeyEditPlaceholder: 'Leave blank to keep current key',
useMyKey: 'Use my key',
selectKeyTitle: 'Select my API Key',
selectKeyHint: 'Only your active, non-expired keys are listed.',
noActiveKey: 'No active API keys available',
primaryModel: 'Primary Model',
primaryModelPlaceholder: 'gpt-4o-mini',
extraModels: 'Extra Models',
extraModelsPlaceholder: 'Press Enter to add extra model',
groupName: 'Group Name',
groupNamePlaceholder: 'Optional, used to group rows in user view',
intervalSeconds: 'Interval (seconds)',
intervalSecondsHint: 'Range: 15 - 3600 seconds',
enabled: 'Enable monitor',
kindRequired: 'Please select a provider'
},
runResultTitle: 'Check Result',
noMonitorsYet: 'No monitors yet',
createFirstMonitor: 'Create your first monitor to track channel availability'
},
// Subscriptions
subscriptions: {
title: 'Subscription Management',