Files
sub2api/backend/ent/channelmonitor_create.go

1611 lines
52 KiB
Go
Raw Normal View History

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
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"errors"
"fmt"
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitordailyrollup"
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
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
feat(channel-monitor): request templates with snapshot apply + headers/body override Problem: Upstream channels can reject monitor probes based on client fingerprint (e.g. "only Claude Code clients allowed"). The monitor had no way to customize the outgoing request to bypass such restrictions. Solution: Introduce reusable request templates that carry extra_headers plus an optional body override; monitors reference a template and receive a snapshot copy on apply. Template edits do NOT auto-propagate — users must click "apply to associated monitors" to refresh snapshots, so a bad template edit cannot instantly break all production monitors. Data model (migration 112): - channel_monitor_request_templates: id, name, provider, description, extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'), body_override jsonb. Unique (provider, name). - channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers, +body_override_mode, +body_override (the three runtime snapshot fields). Checker (channel_monitor_checker.go): - callProvider + runCheckForModel accept a CheckOptions carrying the snapshot fields. mergeHeaders applies user headers on top of adapter defaults (forbidden list: Host / Content-Length / Transfer-Encoding / Connection / Content-Encoding). - buildRequestBody: off -> adapter default body merge -> shallow-merge over default; per-provider deny list (model/messages/contents) protects the challenge contract replace -> user body verbatim - Replace mode skips challenge validation; instead HTTP 2xx + non-empty extracted response text = operational, empty = failed. - 4 new unit tests cover all three modes + replace/empty-response case. Admin API: - /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot on all template_id=id monitors, returns affected count). - channel_monitor request/response DTOs gain the 4 new fields. Frontend: - channelMonitorTemplate.ts API client. - MonitorAdvancedRequestConfig.vue shared component for headers textarea + body mode radio + body JSON editor; used by both template and monitor forms. - MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/ delete/apply, live "associated monitors" count per row. - MonitorFiltersBar: new 模板管理 button next to 新增监控. - MonitorFormDialog: collapsible 高级 section with template dropdown (filtered by form.provider, clears on provider change) + embedded AdvancedRequestConfig. Picking a template copies its fields into the form (snapshot semantics mirrored on the client). - i18n zh/en entries for all new copy. chore: bump version to 0.1.114.32
2026-04-21 14:14:49 +08:00
"github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
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
)
// ChannelMonitorCreate is the builder for creating a ChannelMonitor entity.
type ChannelMonitorCreate struct {
config
mutation *ChannelMonitorMutation
hooks []Hook
conflict []sql.ConflictOption
}
// SetCreatedAt sets the "created_at" field.
func (_c *ChannelMonitorCreate) SetCreatedAt(v time.Time) *ChannelMonitorCreate {
_c.mutation.SetCreatedAt(v)
return _c
}
// SetNillableCreatedAt sets the "created_at" field if the given value is not nil.
func (_c *ChannelMonitorCreate) SetNillableCreatedAt(v *time.Time) *ChannelMonitorCreate {
if v != nil {
_c.SetCreatedAt(*v)
}
return _c
}
// SetUpdatedAt sets the "updated_at" field.
func (_c *ChannelMonitorCreate) SetUpdatedAt(v time.Time) *ChannelMonitorCreate {
_c.mutation.SetUpdatedAt(v)
return _c
}
// SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil.
func (_c *ChannelMonitorCreate) SetNillableUpdatedAt(v *time.Time) *ChannelMonitorCreate {
if v != nil {
_c.SetUpdatedAt(*v)
}
return _c
}
// SetName sets the "name" field.
func (_c *ChannelMonitorCreate) SetName(v string) *ChannelMonitorCreate {
_c.mutation.SetName(v)
return _c
}
// SetProvider sets the "provider" field.
func (_c *ChannelMonitorCreate) SetProvider(v channelmonitor.Provider) *ChannelMonitorCreate {
_c.mutation.SetProvider(v)
return _c
}
// SetEndpoint sets the "endpoint" field.
func (_c *ChannelMonitorCreate) SetEndpoint(v string) *ChannelMonitorCreate {
_c.mutation.SetEndpoint(v)
return _c
}
// SetAPIKeyEncrypted sets the "api_key_encrypted" field.
func (_c *ChannelMonitorCreate) SetAPIKeyEncrypted(v string) *ChannelMonitorCreate {
_c.mutation.SetAPIKeyEncrypted(v)
return _c
}
// SetPrimaryModel sets the "primary_model" field.
func (_c *ChannelMonitorCreate) SetPrimaryModel(v string) *ChannelMonitorCreate {
_c.mutation.SetPrimaryModel(v)
return _c
}
// SetExtraModels sets the "extra_models" field.
func (_c *ChannelMonitorCreate) SetExtraModels(v []string) *ChannelMonitorCreate {
_c.mutation.SetExtraModels(v)
return _c
}
// SetGroupName sets the "group_name" field.
func (_c *ChannelMonitorCreate) SetGroupName(v string) *ChannelMonitorCreate {
_c.mutation.SetGroupName(v)
return _c
}
// SetNillableGroupName sets the "group_name" field if the given value is not nil.
func (_c *ChannelMonitorCreate) SetNillableGroupName(v *string) *ChannelMonitorCreate {
if v != nil {
_c.SetGroupName(*v)
}
return _c
}
// SetEnabled sets the "enabled" field.
func (_c *ChannelMonitorCreate) SetEnabled(v bool) *ChannelMonitorCreate {
_c.mutation.SetEnabled(v)
return _c
}
// SetNillableEnabled sets the "enabled" field if the given value is not nil.
func (_c *ChannelMonitorCreate) SetNillableEnabled(v *bool) *ChannelMonitorCreate {
if v != nil {
_c.SetEnabled(*v)
}
return _c
}
// SetIntervalSeconds sets the "interval_seconds" field.
func (_c *ChannelMonitorCreate) SetIntervalSeconds(v int) *ChannelMonitorCreate {
_c.mutation.SetIntervalSeconds(v)
return _c
}
// SetLastCheckedAt sets the "last_checked_at" field.
func (_c *ChannelMonitorCreate) SetLastCheckedAt(v time.Time) *ChannelMonitorCreate {
_c.mutation.SetLastCheckedAt(v)
return _c
}
// SetNillableLastCheckedAt sets the "last_checked_at" field if the given value is not nil.
func (_c *ChannelMonitorCreate) SetNillableLastCheckedAt(v *time.Time) *ChannelMonitorCreate {
if v != nil {
_c.SetLastCheckedAt(*v)
}
return _c
}
// SetCreatedBy sets the "created_by" field.
func (_c *ChannelMonitorCreate) SetCreatedBy(v int64) *ChannelMonitorCreate {
_c.mutation.SetCreatedBy(v)
return _c
}
feat(channel-monitor): request templates with snapshot apply + headers/body override Problem: Upstream channels can reject monitor probes based on client fingerprint (e.g. "only Claude Code clients allowed"). The monitor had no way to customize the outgoing request to bypass such restrictions. Solution: Introduce reusable request templates that carry extra_headers plus an optional body override; monitors reference a template and receive a snapshot copy on apply. Template edits do NOT auto-propagate — users must click "apply to associated monitors" to refresh snapshots, so a bad template edit cannot instantly break all production monitors. Data model (migration 112): - channel_monitor_request_templates: id, name, provider, description, extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'), body_override jsonb. Unique (provider, name). - channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers, +body_override_mode, +body_override (the three runtime snapshot fields). Checker (channel_monitor_checker.go): - callProvider + runCheckForModel accept a CheckOptions carrying the snapshot fields. mergeHeaders applies user headers on top of adapter defaults (forbidden list: Host / Content-Length / Transfer-Encoding / Connection / Content-Encoding). - buildRequestBody: off -> adapter default body merge -> shallow-merge over default; per-provider deny list (model/messages/contents) protects the challenge contract replace -> user body verbatim - Replace mode skips challenge validation; instead HTTP 2xx + non-empty extracted response text = operational, empty = failed. - 4 new unit tests cover all three modes + replace/empty-response case. Admin API: - /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot on all template_id=id monitors, returns affected count). - channel_monitor request/response DTOs gain the 4 new fields. Frontend: - channelMonitorTemplate.ts API client. - MonitorAdvancedRequestConfig.vue shared component for headers textarea + body mode radio + body JSON editor; used by both template and monitor forms. - MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/ delete/apply, live "associated monitors" count per row. - MonitorFiltersBar: new 模板管理 button next to 新增监控. - MonitorFormDialog: collapsible 高级 section with template dropdown (filtered by form.provider, clears on provider change) + embedded AdvancedRequestConfig. Picking a template copies its fields into the form (snapshot semantics mirrored on the client). - i18n zh/en entries for all new copy. chore: bump version to 0.1.114.32
2026-04-21 14:14:49 +08:00
// SetTemplateID sets the "template_id" field.
func (_c *ChannelMonitorCreate) SetTemplateID(v int64) *ChannelMonitorCreate {
_c.mutation.SetTemplateID(v)
return _c
}
// SetNillableTemplateID sets the "template_id" field if the given value is not nil.
func (_c *ChannelMonitorCreate) SetNillableTemplateID(v *int64) *ChannelMonitorCreate {
if v != nil {
_c.SetTemplateID(*v)
}
return _c
}
// SetExtraHeaders sets the "extra_headers" field.
func (_c *ChannelMonitorCreate) SetExtraHeaders(v map[string]string) *ChannelMonitorCreate {
_c.mutation.SetExtraHeaders(v)
return _c
}
// SetBodyOverrideMode sets the "body_override_mode" field.
func (_c *ChannelMonitorCreate) SetBodyOverrideMode(v string) *ChannelMonitorCreate {
_c.mutation.SetBodyOverrideMode(v)
return _c
}
// SetNillableBodyOverrideMode sets the "body_override_mode" field if the given value is not nil.
func (_c *ChannelMonitorCreate) SetNillableBodyOverrideMode(v *string) *ChannelMonitorCreate {
if v != nil {
_c.SetBodyOverrideMode(*v)
}
return _c
}
// SetBodyOverride sets the "body_override" field.
func (_c *ChannelMonitorCreate) SetBodyOverride(v map[string]interface{}) *ChannelMonitorCreate {
_c.mutation.SetBodyOverride(v)
return _c
}
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
// AddHistoryIDs adds the "history" edge to the ChannelMonitorHistory entity by IDs.
func (_c *ChannelMonitorCreate) AddHistoryIDs(ids ...int64) *ChannelMonitorCreate {
_c.mutation.AddHistoryIDs(ids...)
return _c
}
// AddHistory adds the "history" edges to the ChannelMonitorHistory entity.
func (_c *ChannelMonitorCreate) AddHistory(v ...*ChannelMonitorHistory) *ChannelMonitorCreate {
ids := make([]int64, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _c.AddHistoryIDs(ids...)
}
// AddDailyRollupIDs adds the "daily_rollups" edge to the ChannelMonitorDailyRollup entity by IDs.
func (_c *ChannelMonitorCreate) AddDailyRollupIDs(ids ...int64) *ChannelMonitorCreate {
_c.mutation.AddDailyRollupIDs(ids...)
return _c
}
// AddDailyRollups adds the "daily_rollups" edges to the ChannelMonitorDailyRollup entity.
func (_c *ChannelMonitorCreate) AddDailyRollups(v ...*ChannelMonitorDailyRollup) *ChannelMonitorCreate {
ids := make([]int64, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _c.AddDailyRollupIDs(ids...)
}
feat(channel-monitor): request templates with snapshot apply + headers/body override Problem: Upstream channels can reject monitor probes based on client fingerprint (e.g. "only Claude Code clients allowed"). The monitor had no way to customize the outgoing request to bypass such restrictions. Solution: Introduce reusable request templates that carry extra_headers plus an optional body override; monitors reference a template and receive a snapshot copy on apply. Template edits do NOT auto-propagate — users must click "apply to associated monitors" to refresh snapshots, so a bad template edit cannot instantly break all production monitors. Data model (migration 112): - channel_monitor_request_templates: id, name, provider, description, extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'), body_override jsonb. Unique (provider, name). - channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers, +body_override_mode, +body_override (the three runtime snapshot fields). Checker (channel_monitor_checker.go): - callProvider + runCheckForModel accept a CheckOptions carrying the snapshot fields. mergeHeaders applies user headers on top of adapter defaults (forbidden list: Host / Content-Length / Transfer-Encoding / Connection / Content-Encoding). - buildRequestBody: off -> adapter default body merge -> shallow-merge over default; per-provider deny list (model/messages/contents) protects the challenge contract replace -> user body verbatim - Replace mode skips challenge validation; instead HTTP 2xx + non-empty extracted response text = operational, empty = failed. - 4 new unit tests cover all three modes + replace/empty-response case. Admin API: - /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot on all template_id=id monitors, returns affected count). - channel_monitor request/response DTOs gain the 4 new fields. Frontend: - channelMonitorTemplate.ts API client. - MonitorAdvancedRequestConfig.vue shared component for headers textarea + body mode radio + body JSON editor; used by both template and monitor forms. - MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/ delete/apply, live "associated monitors" count per row. - MonitorFiltersBar: new 模板管理 button next to 新增监控. - MonitorFormDialog: collapsible 高级 section with template dropdown (filtered by form.provider, clears on provider change) + embedded AdvancedRequestConfig. Picking a template copies its fields into the form (snapshot semantics mirrored on the client). - i18n zh/en entries for all new copy. chore: bump version to 0.1.114.32
2026-04-21 14:14:49 +08:00
// SetRequestTemplateID sets the "request_template" edge to the ChannelMonitorRequestTemplate entity by ID.
func (_c *ChannelMonitorCreate) SetRequestTemplateID(id int64) *ChannelMonitorCreate {
_c.mutation.SetRequestTemplateID(id)
return _c
}
// SetNillableRequestTemplateID sets the "request_template" edge to the ChannelMonitorRequestTemplate entity by ID if the given value is not nil.
func (_c *ChannelMonitorCreate) SetNillableRequestTemplateID(id *int64) *ChannelMonitorCreate {
if id != nil {
_c = _c.SetRequestTemplateID(*id)
}
return _c
}
// SetRequestTemplate sets the "request_template" edge to the ChannelMonitorRequestTemplate entity.
func (_c *ChannelMonitorCreate) SetRequestTemplate(v *ChannelMonitorRequestTemplate) *ChannelMonitorCreate {
return _c.SetRequestTemplateID(v.ID)
}
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
// Mutation returns the ChannelMonitorMutation object of the builder.
func (_c *ChannelMonitorCreate) Mutation() *ChannelMonitorMutation {
return _c.mutation
}
// Save creates the ChannelMonitor in the database.
func (_c *ChannelMonitorCreate) Save(ctx context.Context) (*ChannelMonitor, error) {
_c.defaults()
return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks)
}
// SaveX calls Save and panics if Save returns an error.
func (_c *ChannelMonitorCreate) SaveX(ctx context.Context) *ChannelMonitor {
v, err := _c.Save(ctx)
if err != nil {
panic(err)
}
return v
}
// Exec executes the query.
func (_c *ChannelMonitorCreate) Exec(ctx context.Context) error {
_, err := _c.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
func (_c *ChannelMonitorCreate) ExecX(ctx context.Context) {
if err := _c.Exec(ctx); err != nil {
panic(err)
}
}
// defaults sets the default values of the builder before save.
func (_c *ChannelMonitorCreate) defaults() {
if _, ok := _c.mutation.CreatedAt(); !ok {
v := channelmonitor.DefaultCreatedAt()
_c.mutation.SetCreatedAt(v)
}
if _, ok := _c.mutation.UpdatedAt(); !ok {
v := channelmonitor.DefaultUpdatedAt()
_c.mutation.SetUpdatedAt(v)
}
if _, ok := _c.mutation.ExtraModels(); !ok {
v := channelmonitor.DefaultExtraModels
_c.mutation.SetExtraModels(v)
}
if _, ok := _c.mutation.GroupName(); !ok {
v := channelmonitor.DefaultGroupName
_c.mutation.SetGroupName(v)
}
if _, ok := _c.mutation.Enabled(); !ok {
v := channelmonitor.DefaultEnabled
_c.mutation.SetEnabled(v)
}
feat(channel-monitor): request templates with snapshot apply + headers/body override Problem: Upstream channels can reject monitor probes based on client fingerprint (e.g. "only Claude Code clients allowed"). The monitor had no way to customize the outgoing request to bypass such restrictions. Solution: Introduce reusable request templates that carry extra_headers plus an optional body override; monitors reference a template and receive a snapshot copy on apply. Template edits do NOT auto-propagate — users must click "apply to associated monitors" to refresh snapshots, so a bad template edit cannot instantly break all production monitors. Data model (migration 112): - channel_monitor_request_templates: id, name, provider, description, extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'), body_override jsonb. Unique (provider, name). - channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers, +body_override_mode, +body_override (the three runtime snapshot fields). Checker (channel_monitor_checker.go): - callProvider + runCheckForModel accept a CheckOptions carrying the snapshot fields. mergeHeaders applies user headers on top of adapter defaults (forbidden list: Host / Content-Length / Transfer-Encoding / Connection / Content-Encoding). - buildRequestBody: off -> adapter default body merge -> shallow-merge over default; per-provider deny list (model/messages/contents) protects the challenge contract replace -> user body verbatim - Replace mode skips challenge validation; instead HTTP 2xx + non-empty extracted response text = operational, empty = failed. - 4 new unit tests cover all three modes + replace/empty-response case. Admin API: - /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot on all template_id=id monitors, returns affected count). - channel_monitor request/response DTOs gain the 4 new fields. Frontend: - channelMonitorTemplate.ts API client. - MonitorAdvancedRequestConfig.vue shared component for headers textarea + body mode radio + body JSON editor; used by both template and monitor forms. - MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/ delete/apply, live "associated monitors" count per row. - MonitorFiltersBar: new 模板管理 button next to 新增监控. - MonitorFormDialog: collapsible 高级 section with template dropdown (filtered by form.provider, clears on provider change) + embedded AdvancedRequestConfig. Picking a template copies its fields into the form (snapshot semantics mirrored on the client). - i18n zh/en entries for all new copy. chore: bump version to 0.1.114.32
2026-04-21 14:14:49 +08:00
if _, ok := _c.mutation.ExtraHeaders(); !ok {
v := channelmonitor.DefaultExtraHeaders
_c.mutation.SetExtraHeaders(v)
}
if _, ok := _c.mutation.BodyOverrideMode(); !ok {
v := channelmonitor.DefaultBodyOverrideMode
_c.mutation.SetBodyOverrideMode(v)
}
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
}
// check runs all checks and user-defined validators on the builder.
func (_c *ChannelMonitorCreate) check() error {
if _, ok := _c.mutation.CreatedAt(); !ok {
return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "ChannelMonitor.created_at"`)}
}
if _, ok := _c.mutation.UpdatedAt(); !ok {
return &ValidationError{Name: "updated_at", err: errors.New(`ent: missing required field "ChannelMonitor.updated_at"`)}
}
if _, ok := _c.mutation.Name(); !ok {
return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "ChannelMonitor.name"`)}
}
if v, ok := _c.mutation.Name(); ok {
if err := channelmonitor.NameValidator(v); err != nil {
return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.name": %w`, err)}
}
}
if _, ok := _c.mutation.Provider(); !ok {
return &ValidationError{Name: "provider", err: errors.New(`ent: missing required field "ChannelMonitor.provider"`)}
}
if v, ok := _c.mutation.Provider(); ok {
if err := channelmonitor.ProviderValidator(v); err != nil {
return &ValidationError{Name: "provider", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.provider": %w`, err)}
}
}
if _, ok := _c.mutation.Endpoint(); !ok {
return &ValidationError{Name: "endpoint", err: errors.New(`ent: missing required field "ChannelMonitor.endpoint"`)}
}
if v, ok := _c.mutation.Endpoint(); ok {
if err := channelmonitor.EndpointValidator(v); err != nil {
return &ValidationError{Name: "endpoint", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.endpoint": %w`, err)}
}
}
if _, ok := _c.mutation.APIKeyEncrypted(); !ok {
return &ValidationError{Name: "api_key_encrypted", err: errors.New(`ent: missing required field "ChannelMonitor.api_key_encrypted"`)}
}
if v, ok := _c.mutation.APIKeyEncrypted(); ok {
if err := channelmonitor.APIKeyEncryptedValidator(v); err != nil {
return &ValidationError{Name: "api_key_encrypted", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.api_key_encrypted": %w`, err)}
}
}
if _, ok := _c.mutation.PrimaryModel(); !ok {
return &ValidationError{Name: "primary_model", err: errors.New(`ent: missing required field "ChannelMonitor.primary_model"`)}
}
if v, ok := _c.mutation.PrimaryModel(); ok {
if err := channelmonitor.PrimaryModelValidator(v); err != nil {
return &ValidationError{Name: "primary_model", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.primary_model": %w`, err)}
}
}
if _, ok := _c.mutation.ExtraModels(); !ok {
return &ValidationError{Name: "extra_models", err: errors.New(`ent: missing required field "ChannelMonitor.extra_models"`)}
}
if v, ok := _c.mutation.GroupName(); ok {
if err := channelmonitor.GroupNameValidator(v); err != nil {
return &ValidationError{Name: "group_name", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.group_name": %w`, err)}
}
}
if _, ok := _c.mutation.Enabled(); !ok {
return &ValidationError{Name: "enabled", err: errors.New(`ent: missing required field "ChannelMonitor.enabled"`)}
}
if _, ok := _c.mutation.IntervalSeconds(); !ok {
return &ValidationError{Name: "interval_seconds", err: errors.New(`ent: missing required field "ChannelMonitor.interval_seconds"`)}
}
if v, ok := _c.mutation.IntervalSeconds(); ok {
if err := channelmonitor.IntervalSecondsValidator(v); err != nil {
return &ValidationError{Name: "interval_seconds", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.interval_seconds": %w`, err)}
}
}
if _, ok := _c.mutation.CreatedBy(); !ok {
return &ValidationError{Name: "created_by", err: errors.New(`ent: missing required field "ChannelMonitor.created_by"`)}
}
feat(channel-monitor): request templates with snapshot apply + headers/body override Problem: Upstream channels can reject monitor probes based on client fingerprint (e.g. "only Claude Code clients allowed"). The monitor had no way to customize the outgoing request to bypass such restrictions. Solution: Introduce reusable request templates that carry extra_headers plus an optional body override; monitors reference a template and receive a snapshot copy on apply. Template edits do NOT auto-propagate — users must click "apply to associated monitors" to refresh snapshots, so a bad template edit cannot instantly break all production monitors. Data model (migration 112): - channel_monitor_request_templates: id, name, provider, description, extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'), body_override jsonb. Unique (provider, name). - channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers, +body_override_mode, +body_override (the three runtime snapshot fields). Checker (channel_monitor_checker.go): - callProvider + runCheckForModel accept a CheckOptions carrying the snapshot fields. mergeHeaders applies user headers on top of adapter defaults (forbidden list: Host / Content-Length / Transfer-Encoding / Connection / Content-Encoding). - buildRequestBody: off -> adapter default body merge -> shallow-merge over default; per-provider deny list (model/messages/contents) protects the challenge contract replace -> user body verbatim - Replace mode skips challenge validation; instead HTTP 2xx + non-empty extracted response text = operational, empty = failed. - 4 new unit tests cover all three modes + replace/empty-response case. Admin API: - /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot on all template_id=id monitors, returns affected count). - channel_monitor request/response DTOs gain the 4 new fields. Frontend: - channelMonitorTemplate.ts API client. - MonitorAdvancedRequestConfig.vue shared component for headers textarea + body mode radio + body JSON editor; used by both template and monitor forms. - MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/ delete/apply, live "associated monitors" count per row. - MonitorFiltersBar: new 模板管理 button next to 新增监控. - MonitorFormDialog: collapsible 高级 section with template dropdown (filtered by form.provider, clears on provider change) + embedded AdvancedRequestConfig. Picking a template copies its fields into the form (snapshot semantics mirrored on the client). - i18n zh/en entries for all new copy. chore: bump version to 0.1.114.32
2026-04-21 14:14:49 +08:00
if _, ok := _c.mutation.ExtraHeaders(); !ok {
return &ValidationError{Name: "extra_headers", err: errors.New(`ent: missing required field "ChannelMonitor.extra_headers"`)}
}
if _, ok := _c.mutation.BodyOverrideMode(); !ok {
return &ValidationError{Name: "body_override_mode", err: errors.New(`ent: missing required field "ChannelMonitor.body_override_mode"`)}
}
if v, ok := _c.mutation.BodyOverrideMode(); ok {
if err := channelmonitor.BodyOverrideModeValidator(v); err != nil {
return &ValidationError{Name: "body_override_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.body_override_mode": %w`, err)}
}
}
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
return nil
}
func (_c *ChannelMonitorCreate) sqlSave(ctx context.Context) (*ChannelMonitor, error) {
if err := _c.check(); err != nil {
return nil, err
}
_node, _spec := _c.createSpec()
if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil {
if sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
return nil, err
}
id := _spec.ID.Value.(int64)
_node.ID = int64(id)
_c.mutation.id = &_node.ID
_c.mutation.done = true
return _node, nil
}
func (_c *ChannelMonitorCreate) createSpec() (*ChannelMonitor, *sqlgraph.CreateSpec) {
var (
_node = &ChannelMonitor{config: _c.config}
_spec = sqlgraph.NewCreateSpec(channelmonitor.Table, sqlgraph.NewFieldSpec(channelmonitor.FieldID, field.TypeInt64))
)
_spec.OnConflict = _c.conflict
if value, ok := _c.mutation.CreatedAt(); ok {
_spec.SetField(channelmonitor.FieldCreatedAt, field.TypeTime, value)
_node.CreatedAt = value
}
if value, ok := _c.mutation.UpdatedAt(); ok {
_spec.SetField(channelmonitor.FieldUpdatedAt, field.TypeTime, value)
_node.UpdatedAt = value
}
if value, ok := _c.mutation.Name(); ok {
_spec.SetField(channelmonitor.FieldName, field.TypeString, value)
_node.Name = value
}
if value, ok := _c.mutation.Provider(); ok {
_spec.SetField(channelmonitor.FieldProvider, field.TypeEnum, value)
_node.Provider = value
}
if value, ok := _c.mutation.Endpoint(); ok {
_spec.SetField(channelmonitor.FieldEndpoint, field.TypeString, value)
_node.Endpoint = value
}
if value, ok := _c.mutation.APIKeyEncrypted(); ok {
_spec.SetField(channelmonitor.FieldAPIKeyEncrypted, field.TypeString, value)
_node.APIKeyEncrypted = value
}
if value, ok := _c.mutation.PrimaryModel(); ok {
_spec.SetField(channelmonitor.FieldPrimaryModel, field.TypeString, value)
_node.PrimaryModel = value
}
if value, ok := _c.mutation.ExtraModels(); ok {
_spec.SetField(channelmonitor.FieldExtraModels, field.TypeJSON, value)
_node.ExtraModels = value
}
if value, ok := _c.mutation.GroupName(); ok {
_spec.SetField(channelmonitor.FieldGroupName, field.TypeString, value)
_node.GroupName = value
}
if value, ok := _c.mutation.Enabled(); ok {
_spec.SetField(channelmonitor.FieldEnabled, field.TypeBool, value)
_node.Enabled = value
}
if value, ok := _c.mutation.IntervalSeconds(); ok {
_spec.SetField(channelmonitor.FieldIntervalSeconds, field.TypeInt, value)
_node.IntervalSeconds = value
}
if value, ok := _c.mutation.LastCheckedAt(); ok {
_spec.SetField(channelmonitor.FieldLastCheckedAt, field.TypeTime, value)
_node.LastCheckedAt = &value
}
if value, ok := _c.mutation.CreatedBy(); ok {
_spec.SetField(channelmonitor.FieldCreatedBy, field.TypeInt64, value)
_node.CreatedBy = value
}
feat(channel-monitor): request templates with snapshot apply + headers/body override Problem: Upstream channels can reject monitor probes based on client fingerprint (e.g. "only Claude Code clients allowed"). The monitor had no way to customize the outgoing request to bypass such restrictions. Solution: Introduce reusable request templates that carry extra_headers plus an optional body override; monitors reference a template and receive a snapshot copy on apply. Template edits do NOT auto-propagate — users must click "apply to associated monitors" to refresh snapshots, so a bad template edit cannot instantly break all production monitors. Data model (migration 112): - channel_monitor_request_templates: id, name, provider, description, extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'), body_override jsonb. Unique (provider, name). - channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers, +body_override_mode, +body_override (the three runtime snapshot fields). Checker (channel_monitor_checker.go): - callProvider + runCheckForModel accept a CheckOptions carrying the snapshot fields. mergeHeaders applies user headers on top of adapter defaults (forbidden list: Host / Content-Length / Transfer-Encoding / Connection / Content-Encoding). - buildRequestBody: off -> adapter default body merge -> shallow-merge over default; per-provider deny list (model/messages/contents) protects the challenge contract replace -> user body verbatim - Replace mode skips challenge validation; instead HTTP 2xx + non-empty extracted response text = operational, empty = failed. - 4 new unit tests cover all three modes + replace/empty-response case. Admin API: - /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot on all template_id=id monitors, returns affected count). - channel_monitor request/response DTOs gain the 4 new fields. Frontend: - channelMonitorTemplate.ts API client. - MonitorAdvancedRequestConfig.vue shared component for headers textarea + body mode radio + body JSON editor; used by both template and monitor forms. - MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/ delete/apply, live "associated monitors" count per row. - MonitorFiltersBar: new 模板管理 button next to 新增监控. - MonitorFormDialog: collapsible 高级 section with template dropdown (filtered by form.provider, clears on provider change) + embedded AdvancedRequestConfig. Picking a template copies its fields into the form (snapshot semantics mirrored on the client). - i18n zh/en entries for all new copy. chore: bump version to 0.1.114.32
2026-04-21 14:14:49 +08:00
if value, ok := _c.mutation.ExtraHeaders(); ok {
_spec.SetField(channelmonitor.FieldExtraHeaders, field.TypeJSON, value)
_node.ExtraHeaders = value
}
if value, ok := _c.mutation.BodyOverrideMode(); ok {
_spec.SetField(channelmonitor.FieldBodyOverrideMode, field.TypeString, value)
_node.BodyOverrideMode = value
}
if value, ok := _c.mutation.BodyOverride(); ok {
_spec.SetField(channelmonitor.FieldBodyOverride, field.TypeJSON, value)
_node.BodyOverride = value
}
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
if nodes := _c.mutation.HistoryIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: channelmonitor.HistoryTable,
Columns: []string{channelmonitor.HistoryColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(channelmonitorhistory.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges = append(_spec.Edges, edge)
}
if nodes := _c.mutation.DailyRollupsIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: channelmonitor.DailyRollupsTable,
Columns: []string{channelmonitor.DailyRollupsColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(channelmonitordailyrollup.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
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
}
_spec.Edges = append(_spec.Edges, edge)
}
feat(channel-monitor): request templates with snapshot apply + headers/body override Problem: Upstream channels can reject monitor probes based on client fingerprint (e.g. "only Claude Code clients allowed"). The monitor had no way to customize the outgoing request to bypass such restrictions. Solution: Introduce reusable request templates that carry extra_headers plus an optional body override; monitors reference a template and receive a snapshot copy on apply. Template edits do NOT auto-propagate — users must click "apply to associated monitors" to refresh snapshots, so a bad template edit cannot instantly break all production monitors. Data model (migration 112): - channel_monitor_request_templates: id, name, provider, description, extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'), body_override jsonb. Unique (provider, name). - channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers, +body_override_mode, +body_override (the three runtime snapshot fields). Checker (channel_monitor_checker.go): - callProvider + runCheckForModel accept a CheckOptions carrying the snapshot fields. mergeHeaders applies user headers on top of adapter defaults (forbidden list: Host / Content-Length / Transfer-Encoding / Connection / Content-Encoding). - buildRequestBody: off -> adapter default body merge -> shallow-merge over default; per-provider deny list (model/messages/contents) protects the challenge contract replace -> user body verbatim - Replace mode skips challenge validation; instead HTTP 2xx + non-empty extracted response text = operational, empty = failed. - 4 new unit tests cover all three modes + replace/empty-response case. Admin API: - /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot on all template_id=id monitors, returns affected count). - channel_monitor request/response DTOs gain the 4 new fields. Frontend: - channelMonitorTemplate.ts API client. - MonitorAdvancedRequestConfig.vue shared component for headers textarea + body mode radio + body JSON editor; used by both template and monitor forms. - MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/ delete/apply, live "associated monitors" count per row. - MonitorFiltersBar: new 模板管理 button next to 新增监控. - MonitorFormDialog: collapsible 高级 section with template dropdown (filtered by form.provider, clears on provider change) + embedded AdvancedRequestConfig. Picking a template copies its fields into the form (snapshot semantics mirrored on the client). - i18n zh/en entries for all new copy. chore: bump version to 0.1.114.32
2026-04-21 14:14:49 +08:00
if nodes := _c.mutation.RequestTemplateIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: false,
Table: channelmonitor.RequestTemplateTable,
Columns: []string{channelmonitor.RequestTemplateColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(channelmonitorrequesttemplate.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_node.TemplateID = &nodes[0]
_spec.Edges = append(_spec.Edges, edge)
}
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
return _node, _spec
}
// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause
// of the `INSERT` statement. For example:
//
// client.ChannelMonitor.Create().
// SetCreatedAt(v).
// OnConflict(
// // Update the row with the new values
// // the was proposed for insertion.
// sql.ResolveWithNewValues(),
// ).
// // Override some of the fields with custom
// // update values.
// Update(func(u *ent.ChannelMonitorUpsert) {
// SetCreatedAt(v+v).
// }).
// Exec(ctx)
func (_c *ChannelMonitorCreate) OnConflict(opts ...sql.ConflictOption) *ChannelMonitorUpsertOne {
_c.conflict = opts
return &ChannelMonitorUpsertOne{
create: _c,
}
}
// OnConflictColumns calls `OnConflict` and configures the columns
// as conflict target. Using this option is equivalent to using:
//
// client.ChannelMonitor.Create().
// OnConflict(sql.ConflictColumns(columns...)).
// Exec(ctx)
func (_c *ChannelMonitorCreate) OnConflictColumns(columns ...string) *ChannelMonitorUpsertOne {
_c.conflict = append(_c.conflict, sql.ConflictColumns(columns...))
return &ChannelMonitorUpsertOne{
create: _c,
}
}
type (
// ChannelMonitorUpsertOne is the builder for "upsert"-ing
// one ChannelMonitor node.
ChannelMonitorUpsertOne struct {
create *ChannelMonitorCreate
}
// ChannelMonitorUpsert is the "OnConflict" setter.
ChannelMonitorUpsert struct {
*sql.UpdateSet
}
)
// SetUpdatedAt sets the "updated_at" field.
func (u *ChannelMonitorUpsert) SetUpdatedAt(v time.Time) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldUpdatedAt, v)
return u
}
// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateUpdatedAt() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldUpdatedAt)
return u
}
// SetName sets the "name" field.
func (u *ChannelMonitorUpsert) SetName(v string) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldName, v)
return u
}
// UpdateName sets the "name" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateName() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldName)
return u
}
// SetProvider sets the "provider" field.
func (u *ChannelMonitorUpsert) SetProvider(v channelmonitor.Provider) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldProvider, v)
return u
}
// UpdateProvider sets the "provider" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateProvider() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldProvider)
return u
}
// SetEndpoint sets the "endpoint" field.
func (u *ChannelMonitorUpsert) SetEndpoint(v string) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldEndpoint, v)
return u
}
// UpdateEndpoint sets the "endpoint" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateEndpoint() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldEndpoint)
return u
}
// SetAPIKeyEncrypted sets the "api_key_encrypted" field.
func (u *ChannelMonitorUpsert) SetAPIKeyEncrypted(v string) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldAPIKeyEncrypted, v)
return u
}
// UpdateAPIKeyEncrypted sets the "api_key_encrypted" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateAPIKeyEncrypted() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldAPIKeyEncrypted)
return u
}
// SetPrimaryModel sets the "primary_model" field.
func (u *ChannelMonitorUpsert) SetPrimaryModel(v string) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldPrimaryModel, v)
return u
}
// UpdatePrimaryModel sets the "primary_model" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdatePrimaryModel() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldPrimaryModel)
return u
}
// SetExtraModels sets the "extra_models" field.
func (u *ChannelMonitorUpsert) SetExtraModels(v []string) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldExtraModels, v)
return u
}
// UpdateExtraModels sets the "extra_models" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateExtraModels() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldExtraModels)
return u
}
// SetGroupName sets the "group_name" field.
func (u *ChannelMonitorUpsert) SetGroupName(v string) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldGroupName, v)
return u
}
// UpdateGroupName sets the "group_name" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateGroupName() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldGroupName)
return u
}
// ClearGroupName clears the value of the "group_name" field.
func (u *ChannelMonitorUpsert) ClearGroupName() *ChannelMonitorUpsert {
u.SetNull(channelmonitor.FieldGroupName)
return u
}
// SetEnabled sets the "enabled" field.
func (u *ChannelMonitorUpsert) SetEnabled(v bool) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldEnabled, v)
return u
}
// UpdateEnabled sets the "enabled" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateEnabled() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldEnabled)
return u
}
// SetIntervalSeconds sets the "interval_seconds" field.
func (u *ChannelMonitorUpsert) SetIntervalSeconds(v int) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldIntervalSeconds, v)
return u
}
// UpdateIntervalSeconds sets the "interval_seconds" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateIntervalSeconds() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldIntervalSeconds)
return u
}
// AddIntervalSeconds adds v to the "interval_seconds" field.
func (u *ChannelMonitorUpsert) AddIntervalSeconds(v int) *ChannelMonitorUpsert {
u.Add(channelmonitor.FieldIntervalSeconds, v)
return u
}
// SetLastCheckedAt sets the "last_checked_at" field.
func (u *ChannelMonitorUpsert) SetLastCheckedAt(v time.Time) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldLastCheckedAt, v)
return u
}
// UpdateLastCheckedAt sets the "last_checked_at" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateLastCheckedAt() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldLastCheckedAt)
return u
}
// ClearLastCheckedAt clears the value of the "last_checked_at" field.
func (u *ChannelMonitorUpsert) ClearLastCheckedAt() *ChannelMonitorUpsert {
u.SetNull(channelmonitor.FieldLastCheckedAt)
return u
}
// SetCreatedBy sets the "created_by" field.
func (u *ChannelMonitorUpsert) SetCreatedBy(v int64) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldCreatedBy, v)
return u
}
// UpdateCreatedBy sets the "created_by" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateCreatedBy() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldCreatedBy)
return u
}
// AddCreatedBy adds v to the "created_by" field.
func (u *ChannelMonitorUpsert) AddCreatedBy(v int64) *ChannelMonitorUpsert {
u.Add(channelmonitor.FieldCreatedBy, v)
return u
}
feat(channel-monitor): request templates with snapshot apply + headers/body override Problem: Upstream channels can reject monitor probes based on client fingerprint (e.g. "only Claude Code clients allowed"). The monitor had no way to customize the outgoing request to bypass such restrictions. Solution: Introduce reusable request templates that carry extra_headers plus an optional body override; monitors reference a template and receive a snapshot copy on apply. Template edits do NOT auto-propagate — users must click "apply to associated monitors" to refresh snapshots, so a bad template edit cannot instantly break all production monitors. Data model (migration 112): - channel_monitor_request_templates: id, name, provider, description, extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'), body_override jsonb. Unique (provider, name). - channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers, +body_override_mode, +body_override (the three runtime snapshot fields). Checker (channel_monitor_checker.go): - callProvider + runCheckForModel accept a CheckOptions carrying the snapshot fields. mergeHeaders applies user headers on top of adapter defaults (forbidden list: Host / Content-Length / Transfer-Encoding / Connection / Content-Encoding). - buildRequestBody: off -> adapter default body merge -> shallow-merge over default; per-provider deny list (model/messages/contents) protects the challenge contract replace -> user body verbatim - Replace mode skips challenge validation; instead HTTP 2xx + non-empty extracted response text = operational, empty = failed. - 4 new unit tests cover all three modes + replace/empty-response case. Admin API: - /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot on all template_id=id monitors, returns affected count). - channel_monitor request/response DTOs gain the 4 new fields. Frontend: - channelMonitorTemplate.ts API client. - MonitorAdvancedRequestConfig.vue shared component for headers textarea + body mode radio + body JSON editor; used by both template and monitor forms. - MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/ delete/apply, live "associated monitors" count per row. - MonitorFiltersBar: new 模板管理 button next to 新增监控. - MonitorFormDialog: collapsible 高级 section with template dropdown (filtered by form.provider, clears on provider change) + embedded AdvancedRequestConfig. Picking a template copies its fields into the form (snapshot semantics mirrored on the client). - i18n zh/en entries for all new copy. chore: bump version to 0.1.114.32
2026-04-21 14:14:49 +08:00
// SetTemplateID sets the "template_id" field.
func (u *ChannelMonitorUpsert) SetTemplateID(v int64) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldTemplateID, v)
return u
}
// UpdateTemplateID sets the "template_id" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateTemplateID() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldTemplateID)
return u
}
// ClearTemplateID clears the value of the "template_id" field.
func (u *ChannelMonitorUpsert) ClearTemplateID() *ChannelMonitorUpsert {
u.SetNull(channelmonitor.FieldTemplateID)
return u
}
// SetExtraHeaders sets the "extra_headers" field.
func (u *ChannelMonitorUpsert) SetExtraHeaders(v map[string]string) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldExtraHeaders, v)
return u
}
// UpdateExtraHeaders sets the "extra_headers" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateExtraHeaders() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldExtraHeaders)
return u
}
// SetBodyOverrideMode sets the "body_override_mode" field.
func (u *ChannelMonitorUpsert) SetBodyOverrideMode(v string) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldBodyOverrideMode, v)
return u
}
// UpdateBodyOverrideMode sets the "body_override_mode" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateBodyOverrideMode() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldBodyOverrideMode)
return u
}
// SetBodyOverride sets the "body_override" field.
func (u *ChannelMonitorUpsert) SetBodyOverride(v map[string]interface{}) *ChannelMonitorUpsert {
u.Set(channelmonitor.FieldBodyOverride, v)
return u
}
// UpdateBodyOverride sets the "body_override" field to the value that was provided on create.
func (u *ChannelMonitorUpsert) UpdateBodyOverride() *ChannelMonitorUpsert {
u.SetExcluded(channelmonitor.FieldBodyOverride)
return u
}
// ClearBodyOverride clears the value of the "body_override" field.
func (u *ChannelMonitorUpsert) ClearBodyOverride() *ChannelMonitorUpsert {
u.SetNull(channelmonitor.FieldBodyOverride)
return u
}
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
// UpdateNewValues updates the mutable fields using the new values that were set on create.
// Using this option is equivalent to using:
//
// client.ChannelMonitor.Create().
// OnConflict(
// sql.ResolveWithNewValues(),
// ).
// Exec(ctx)
func (u *ChannelMonitorUpsertOne) UpdateNewValues() *ChannelMonitorUpsertOne {
u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues())
u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) {
if _, exists := u.create.mutation.CreatedAt(); exists {
s.SetIgnore(channelmonitor.FieldCreatedAt)
}
}))
return u
}
// Ignore sets each column to itself in case of conflict.
// Using this option is equivalent to using:
//
// client.ChannelMonitor.Create().
// OnConflict(sql.ResolveWithIgnore()).
// Exec(ctx)
func (u *ChannelMonitorUpsertOne) Ignore() *ChannelMonitorUpsertOne {
u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore())
return u
}
// DoNothing configures the conflict_action to `DO NOTHING`.
// Supported only by SQLite and PostgreSQL.
func (u *ChannelMonitorUpsertOne) DoNothing() *ChannelMonitorUpsertOne {
u.create.conflict = append(u.create.conflict, sql.DoNothing())
return u
}
// Update allows overriding fields `UPDATE` values. See the ChannelMonitorCreate.OnConflict
// documentation for more info.
func (u *ChannelMonitorUpsertOne) Update(set func(*ChannelMonitorUpsert)) *ChannelMonitorUpsertOne {
u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) {
set(&ChannelMonitorUpsert{UpdateSet: update})
}))
return u
}
// SetUpdatedAt sets the "updated_at" field.
func (u *ChannelMonitorUpsertOne) SetUpdatedAt(v time.Time) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetUpdatedAt(v)
})
}
// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateUpdatedAt() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateUpdatedAt()
})
}
// SetName sets the "name" field.
func (u *ChannelMonitorUpsertOne) SetName(v string) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetName(v)
})
}
// UpdateName sets the "name" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateName() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateName()
})
}
// SetProvider sets the "provider" field.
func (u *ChannelMonitorUpsertOne) SetProvider(v channelmonitor.Provider) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetProvider(v)
})
}
// UpdateProvider sets the "provider" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateProvider() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateProvider()
})
}
// SetEndpoint sets the "endpoint" field.
func (u *ChannelMonitorUpsertOne) SetEndpoint(v string) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetEndpoint(v)
})
}
// UpdateEndpoint sets the "endpoint" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateEndpoint() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateEndpoint()
})
}
// SetAPIKeyEncrypted sets the "api_key_encrypted" field.
func (u *ChannelMonitorUpsertOne) SetAPIKeyEncrypted(v string) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetAPIKeyEncrypted(v)
})
}
// UpdateAPIKeyEncrypted sets the "api_key_encrypted" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateAPIKeyEncrypted() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateAPIKeyEncrypted()
})
}
// SetPrimaryModel sets the "primary_model" field.
func (u *ChannelMonitorUpsertOne) SetPrimaryModel(v string) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetPrimaryModel(v)
})
}
// UpdatePrimaryModel sets the "primary_model" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdatePrimaryModel() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdatePrimaryModel()
})
}
// SetExtraModels sets the "extra_models" field.
func (u *ChannelMonitorUpsertOne) SetExtraModels(v []string) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetExtraModels(v)
})
}
// UpdateExtraModels sets the "extra_models" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateExtraModels() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateExtraModels()
})
}
// SetGroupName sets the "group_name" field.
func (u *ChannelMonitorUpsertOne) SetGroupName(v string) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetGroupName(v)
})
}
// UpdateGroupName sets the "group_name" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateGroupName() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateGroupName()
})
}
// ClearGroupName clears the value of the "group_name" field.
func (u *ChannelMonitorUpsertOne) ClearGroupName() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.ClearGroupName()
})
}
// SetEnabled sets the "enabled" field.
func (u *ChannelMonitorUpsertOne) SetEnabled(v bool) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetEnabled(v)
})
}
// UpdateEnabled sets the "enabled" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateEnabled() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateEnabled()
})
}
// SetIntervalSeconds sets the "interval_seconds" field.
func (u *ChannelMonitorUpsertOne) SetIntervalSeconds(v int) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetIntervalSeconds(v)
})
}
// AddIntervalSeconds adds v to the "interval_seconds" field.
func (u *ChannelMonitorUpsertOne) AddIntervalSeconds(v int) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.AddIntervalSeconds(v)
})
}
// UpdateIntervalSeconds sets the "interval_seconds" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateIntervalSeconds() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateIntervalSeconds()
})
}
// SetLastCheckedAt sets the "last_checked_at" field.
func (u *ChannelMonitorUpsertOne) SetLastCheckedAt(v time.Time) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetLastCheckedAt(v)
})
}
// UpdateLastCheckedAt sets the "last_checked_at" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateLastCheckedAt() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateLastCheckedAt()
})
}
// ClearLastCheckedAt clears the value of the "last_checked_at" field.
func (u *ChannelMonitorUpsertOne) ClearLastCheckedAt() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.ClearLastCheckedAt()
})
}
// SetCreatedBy sets the "created_by" field.
func (u *ChannelMonitorUpsertOne) SetCreatedBy(v int64) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetCreatedBy(v)
})
}
// AddCreatedBy adds v to the "created_by" field.
func (u *ChannelMonitorUpsertOne) AddCreatedBy(v int64) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.AddCreatedBy(v)
})
}
// UpdateCreatedBy sets the "created_by" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateCreatedBy() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateCreatedBy()
})
}
feat(channel-monitor): request templates with snapshot apply + headers/body override Problem: Upstream channels can reject monitor probes based on client fingerprint (e.g. "only Claude Code clients allowed"). The monitor had no way to customize the outgoing request to bypass such restrictions. Solution: Introduce reusable request templates that carry extra_headers plus an optional body override; monitors reference a template and receive a snapshot copy on apply. Template edits do NOT auto-propagate — users must click "apply to associated monitors" to refresh snapshots, so a bad template edit cannot instantly break all production monitors. Data model (migration 112): - channel_monitor_request_templates: id, name, provider, description, extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'), body_override jsonb. Unique (provider, name). - channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers, +body_override_mode, +body_override (the three runtime snapshot fields). Checker (channel_monitor_checker.go): - callProvider + runCheckForModel accept a CheckOptions carrying the snapshot fields. mergeHeaders applies user headers on top of adapter defaults (forbidden list: Host / Content-Length / Transfer-Encoding / Connection / Content-Encoding). - buildRequestBody: off -> adapter default body merge -> shallow-merge over default; per-provider deny list (model/messages/contents) protects the challenge contract replace -> user body verbatim - Replace mode skips challenge validation; instead HTTP 2xx + non-empty extracted response text = operational, empty = failed. - 4 new unit tests cover all three modes + replace/empty-response case. Admin API: - /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot on all template_id=id monitors, returns affected count). - channel_monitor request/response DTOs gain the 4 new fields. Frontend: - channelMonitorTemplate.ts API client. - MonitorAdvancedRequestConfig.vue shared component for headers textarea + body mode radio + body JSON editor; used by both template and monitor forms. - MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/ delete/apply, live "associated monitors" count per row. - MonitorFiltersBar: new 模板管理 button next to 新增监控. - MonitorFormDialog: collapsible 高级 section with template dropdown (filtered by form.provider, clears on provider change) + embedded AdvancedRequestConfig. Picking a template copies its fields into the form (snapshot semantics mirrored on the client). - i18n zh/en entries for all new copy. chore: bump version to 0.1.114.32
2026-04-21 14:14:49 +08:00
// SetTemplateID sets the "template_id" field.
func (u *ChannelMonitorUpsertOne) SetTemplateID(v int64) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetTemplateID(v)
})
}
// UpdateTemplateID sets the "template_id" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateTemplateID() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateTemplateID()
})
}
// ClearTemplateID clears the value of the "template_id" field.
func (u *ChannelMonitorUpsertOne) ClearTemplateID() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.ClearTemplateID()
})
}
// SetExtraHeaders sets the "extra_headers" field.
func (u *ChannelMonitorUpsertOne) SetExtraHeaders(v map[string]string) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetExtraHeaders(v)
})
}
// UpdateExtraHeaders sets the "extra_headers" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateExtraHeaders() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateExtraHeaders()
})
}
// SetBodyOverrideMode sets the "body_override_mode" field.
func (u *ChannelMonitorUpsertOne) SetBodyOverrideMode(v string) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetBodyOverrideMode(v)
})
}
// UpdateBodyOverrideMode sets the "body_override_mode" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateBodyOverrideMode() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateBodyOverrideMode()
})
}
// SetBodyOverride sets the "body_override" field.
func (u *ChannelMonitorUpsertOne) SetBodyOverride(v map[string]interface{}) *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetBodyOverride(v)
})
}
// UpdateBodyOverride sets the "body_override" field to the value that was provided on create.
func (u *ChannelMonitorUpsertOne) UpdateBodyOverride() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateBodyOverride()
})
}
// ClearBodyOverride clears the value of the "body_override" field.
func (u *ChannelMonitorUpsertOne) ClearBodyOverride() *ChannelMonitorUpsertOne {
return u.Update(func(s *ChannelMonitorUpsert) {
s.ClearBodyOverride()
})
}
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
// Exec executes the query.
func (u *ChannelMonitorUpsertOne) Exec(ctx context.Context) error {
if len(u.create.conflict) == 0 {
return errors.New("ent: missing options for ChannelMonitorCreate.OnConflict")
}
return u.create.Exec(ctx)
}
// ExecX is like Exec, but panics if an error occurs.
func (u *ChannelMonitorUpsertOne) ExecX(ctx context.Context) {
if err := u.create.Exec(ctx); err != nil {
panic(err)
}
}
// Exec executes the UPSERT query and returns the inserted/updated ID.
func (u *ChannelMonitorUpsertOne) ID(ctx context.Context) (id int64, err error) {
node, err := u.create.Save(ctx)
if err != nil {
return id, err
}
return node.ID, nil
}
// IDX is like ID, but panics if an error occurs.
func (u *ChannelMonitorUpsertOne) IDX(ctx context.Context) int64 {
id, err := u.ID(ctx)
if err != nil {
panic(err)
}
return id
}
// ChannelMonitorCreateBulk is the builder for creating many ChannelMonitor entities in bulk.
type ChannelMonitorCreateBulk struct {
config
err error
builders []*ChannelMonitorCreate
conflict []sql.ConflictOption
}
// Save creates the ChannelMonitor entities in the database.
func (_c *ChannelMonitorCreateBulk) Save(ctx context.Context) ([]*ChannelMonitor, error) {
if _c.err != nil {
return nil, _c.err
}
specs := make([]*sqlgraph.CreateSpec, len(_c.builders))
nodes := make([]*ChannelMonitor, len(_c.builders))
mutators := make([]Mutator, len(_c.builders))
for i := range _c.builders {
func(i int, root context.Context) {
builder := _c.builders[i]
builder.defaults()
var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
mutation, ok := m.(*ChannelMonitorMutation)
if !ok {
return nil, fmt.Errorf("unexpected mutation type %T", m)
}
if err := builder.check(); err != nil {
return nil, err
}
builder.mutation = mutation
var err error
nodes[i], specs[i] = builder.createSpec()
if i < len(mutators)-1 {
_, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation)
} else {
spec := &sqlgraph.BatchCreateSpec{Nodes: specs}
spec.OnConflict = _c.conflict
// Invoke the actual operation on the latest mutation in the chain.
if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil {
if sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
}
}
if err != nil {
return nil, err
}
mutation.id = &nodes[i].ID
if specs[i].ID.Value != nil {
id := specs[i].ID.Value.(int64)
nodes[i].ID = int64(id)
}
mutation.done = true
return nodes[i], nil
})
for i := len(builder.hooks) - 1; i >= 0; i-- {
mut = builder.hooks[i](mut)
}
mutators[i] = mut
}(i, ctx)
}
if len(mutators) > 0 {
if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil {
return nil, err
}
}
return nodes, nil
}
// SaveX is like Save, but panics if an error occurs.
func (_c *ChannelMonitorCreateBulk) SaveX(ctx context.Context) []*ChannelMonitor {
v, err := _c.Save(ctx)
if err != nil {
panic(err)
}
return v
}
// Exec executes the query.
func (_c *ChannelMonitorCreateBulk) Exec(ctx context.Context) error {
_, err := _c.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
func (_c *ChannelMonitorCreateBulk) ExecX(ctx context.Context) {
if err := _c.Exec(ctx); err != nil {
panic(err)
}
}
// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause
// of the `INSERT` statement. For example:
//
// client.ChannelMonitor.CreateBulk(builders...).
// OnConflict(
// // Update the row with the new values
// // the was proposed for insertion.
// sql.ResolveWithNewValues(),
// ).
// // Override some of the fields with custom
// // update values.
// Update(func(u *ent.ChannelMonitorUpsert) {
// SetCreatedAt(v+v).
// }).
// Exec(ctx)
func (_c *ChannelMonitorCreateBulk) OnConflict(opts ...sql.ConflictOption) *ChannelMonitorUpsertBulk {
_c.conflict = opts
return &ChannelMonitorUpsertBulk{
create: _c,
}
}
// OnConflictColumns calls `OnConflict` and configures the columns
// as conflict target. Using this option is equivalent to using:
//
// client.ChannelMonitor.Create().
// OnConflict(sql.ConflictColumns(columns...)).
// Exec(ctx)
func (_c *ChannelMonitorCreateBulk) OnConflictColumns(columns ...string) *ChannelMonitorUpsertBulk {
_c.conflict = append(_c.conflict, sql.ConflictColumns(columns...))
return &ChannelMonitorUpsertBulk{
create: _c,
}
}
// ChannelMonitorUpsertBulk is the builder for "upsert"-ing
// a bulk of ChannelMonitor nodes.
type ChannelMonitorUpsertBulk struct {
create *ChannelMonitorCreateBulk
}
// UpdateNewValues updates the mutable fields using the new values that
// were set on create. Using this option is equivalent to using:
//
// client.ChannelMonitor.Create().
// OnConflict(
// sql.ResolveWithNewValues(),
// ).
// Exec(ctx)
func (u *ChannelMonitorUpsertBulk) UpdateNewValues() *ChannelMonitorUpsertBulk {
u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues())
u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) {
for _, b := range u.create.builders {
if _, exists := b.mutation.CreatedAt(); exists {
s.SetIgnore(channelmonitor.FieldCreatedAt)
}
}
}))
return u
}
// Ignore sets each column to itself in case of conflict.
// Using this option is equivalent to using:
//
// client.ChannelMonitor.Create().
// OnConflict(sql.ResolveWithIgnore()).
// Exec(ctx)
func (u *ChannelMonitorUpsertBulk) Ignore() *ChannelMonitorUpsertBulk {
u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore())
return u
}
// DoNothing configures the conflict_action to `DO NOTHING`.
// Supported only by SQLite and PostgreSQL.
func (u *ChannelMonitorUpsertBulk) DoNothing() *ChannelMonitorUpsertBulk {
u.create.conflict = append(u.create.conflict, sql.DoNothing())
return u
}
// Update allows overriding fields `UPDATE` values. See the ChannelMonitorCreateBulk.OnConflict
// documentation for more info.
func (u *ChannelMonitorUpsertBulk) Update(set func(*ChannelMonitorUpsert)) *ChannelMonitorUpsertBulk {
u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) {
set(&ChannelMonitorUpsert{UpdateSet: update})
}))
return u
}
// SetUpdatedAt sets the "updated_at" field.
func (u *ChannelMonitorUpsertBulk) SetUpdatedAt(v time.Time) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetUpdatedAt(v)
})
}
// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateUpdatedAt() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateUpdatedAt()
})
}
// SetName sets the "name" field.
func (u *ChannelMonitorUpsertBulk) SetName(v string) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetName(v)
})
}
// UpdateName sets the "name" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateName() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateName()
})
}
// SetProvider sets the "provider" field.
func (u *ChannelMonitorUpsertBulk) SetProvider(v channelmonitor.Provider) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetProvider(v)
})
}
// UpdateProvider sets the "provider" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateProvider() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateProvider()
})
}
// SetEndpoint sets the "endpoint" field.
func (u *ChannelMonitorUpsertBulk) SetEndpoint(v string) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetEndpoint(v)
})
}
// UpdateEndpoint sets the "endpoint" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateEndpoint() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateEndpoint()
})
}
// SetAPIKeyEncrypted sets the "api_key_encrypted" field.
func (u *ChannelMonitorUpsertBulk) SetAPIKeyEncrypted(v string) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetAPIKeyEncrypted(v)
})
}
// UpdateAPIKeyEncrypted sets the "api_key_encrypted" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateAPIKeyEncrypted() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateAPIKeyEncrypted()
})
}
// SetPrimaryModel sets the "primary_model" field.
func (u *ChannelMonitorUpsertBulk) SetPrimaryModel(v string) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetPrimaryModel(v)
})
}
// UpdatePrimaryModel sets the "primary_model" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdatePrimaryModel() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdatePrimaryModel()
})
}
// SetExtraModels sets the "extra_models" field.
func (u *ChannelMonitorUpsertBulk) SetExtraModels(v []string) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetExtraModels(v)
})
}
// UpdateExtraModels sets the "extra_models" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateExtraModels() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateExtraModels()
})
}
// SetGroupName sets the "group_name" field.
func (u *ChannelMonitorUpsertBulk) SetGroupName(v string) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetGroupName(v)
})
}
// UpdateGroupName sets the "group_name" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateGroupName() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateGroupName()
})
}
// ClearGroupName clears the value of the "group_name" field.
func (u *ChannelMonitorUpsertBulk) ClearGroupName() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.ClearGroupName()
})
}
// SetEnabled sets the "enabled" field.
func (u *ChannelMonitorUpsertBulk) SetEnabled(v bool) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetEnabled(v)
})
}
// UpdateEnabled sets the "enabled" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateEnabled() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateEnabled()
})
}
// SetIntervalSeconds sets the "interval_seconds" field.
func (u *ChannelMonitorUpsertBulk) SetIntervalSeconds(v int) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetIntervalSeconds(v)
})
}
// AddIntervalSeconds adds v to the "interval_seconds" field.
func (u *ChannelMonitorUpsertBulk) AddIntervalSeconds(v int) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.AddIntervalSeconds(v)
})
}
// UpdateIntervalSeconds sets the "interval_seconds" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateIntervalSeconds() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateIntervalSeconds()
})
}
// SetLastCheckedAt sets the "last_checked_at" field.
func (u *ChannelMonitorUpsertBulk) SetLastCheckedAt(v time.Time) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetLastCheckedAt(v)
})
}
// UpdateLastCheckedAt sets the "last_checked_at" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateLastCheckedAt() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateLastCheckedAt()
})
}
// ClearLastCheckedAt clears the value of the "last_checked_at" field.
func (u *ChannelMonitorUpsertBulk) ClearLastCheckedAt() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.ClearLastCheckedAt()
})
}
// SetCreatedBy sets the "created_by" field.
func (u *ChannelMonitorUpsertBulk) SetCreatedBy(v int64) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetCreatedBy(v)
})
}
// AddCreatedBy adds v to the "created_by" field.
func (u *ChannelMonitorUpsertBulk) AddCreatedBy(v int64) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.AddCreatedBy(v)
})
}
// UpdateCreatedBy sets the "created_by" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateCreatedBy() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateCreatedBy()
})
}
feat(channel-monitor): request templates with snapshot apply + headers/body override Problem: Upstream channels can reject monitor probes based on client fingerprint (e.g. "only Claude Code clients allowed"). The monitor had no way to customize the outgoing request to bypass such restrictions. Solution: Introduce reusable request templates that carry extra_headers plus an optional body override; monitors reference a template and receive a snapshot copy on apply. Template edits do NOT auto-propagate — users must click "apply to associated monitors" to refresh snapshots, so a bad template edit cannot instantly break all production monitors. Data model (migration 112): - channel_monitor_request_templates: id, name, provider, description, extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'), body_override jsonb. Unique (provider, name). - channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers, +body_override_mode, +body_override (the three runtime snapshot fields). Checker (channel_monitor_checker.go): - callProvider + runCheckForModel accept a CheckOptions carrying the snapshot fields. mergeHeaders applies user headers on top of adapter defaults (forbidden list: Host / Content-Length / Transfer-Encoding / Connection / Content-Encoding). - buildRequestBody: off -> adapter default body merge -> shallow-merge over default; per-provider deny list (model/messages/contents) protects the challenge contract replace -> user body verbatim - Replace mode skips challenge validation; instead HTTP 2xx + non-empty extracted response text = operational, empty = failed. - 4 new unit tests cover all three modes + replace/empty-response case. Admin API: - /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot on all template_id=id monitors, returns affected count). - channel_monitor request/response DTOs gain the 4 new fields. Frontend: - channelMonitorTemplate.ts API client. - MonitorAdvancedRequestConfig.vue shared component for headers textarea + body mode radio + body JSON editor; used by both template and monitor forms. - MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/ delete/apply, live "associated monitors" count per row. - MonitorFiltersBar: new 模板管理 button next to 新增监控. - MonitorFormDialog: collapsible 高级 section with template dropdown (filtered by form.provider, clears on provider change) + embedded AdvancedRequestConfig. Picking a template copies its fields into the form (snapshot semantics mirrored on the client). - i18n zh/en entries for all new copy. chore: bump version to 0.1.114.32
2026-04-21 14:14:49 +08:00
// SetTemplateID sets the "template_id" field.
func (u *ChannelMonitorUpsertBulk) SetTemplateID(v int64) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetTemplateID(v)
})
}
// UpdateTemplateID sets the "template_id" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateTemplateID() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateTemplateID()
})
}
// ClearTemplateID clears the value of the "template_id" field.
func (u *ChannelMonitorUpsertBulk) ClearTemplateID() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.ClearTemplateID()
})
}
// SetExtraHeaders sets the "extra_headers" field.
func (u *ChannelMonitorUpsertBulk) SetExtraHeaders(v map[string]string) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetExtraHeaders(v)
})
}
// UpdateExtraHeaders sets the "extra_headers" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateExtraHeaders() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateExtraHeaders()
})
}
// SetBodyOverrideMode sets the "body_override_mode" field.
func (u *ChannelMonitorUpsertBulk) SetBodyOverrideMode(v string) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetBodyOverrideMode(v)
})
}
// UpdateBodyOverrideMode sets the "body_override_mode" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateBodyOverrideMode() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateBodyOverrideMode()
})
}
// SetBodyOverride sets the "body_override" field.
func (u *ChannelMonitorUpsertBulk) SetBodyOverride(v map[string]interface{}) *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.SetBodyOverride(v)
})
}
// UpdateBodyOverride sets the "body_override" field to the value that was provided on create.
func (u *ChannelMonitorUpsertBulk) UpdateBodyOverride() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.UpdateBodyOverride()
})
}
// ClearBodyOverride clears the value of the "body_override" field.
func (u *ChannelMonitorUpsertBulk) ClearBodyOverride() *ChannelMonitorUpsertBulk {
return u.Update(func(s *ChannelMonitorUpsert) {
s.ClearBodyOverride()
})
}
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
// Exec executes the query.
func (u *ChannelMonitorUpsertBulk) Exec(ctx context.Context) error {
if u.create.err != nil {
return u.create.err
}
for i, b := range u.create.builders {
if len(b.conflict) != 0 {
return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the ChannelMonitorCreateBulk instead", i)
}
}
if len(u.create.conflict) == 0 {
return errors.New("ent: missing options for ChannelMonitorCreateBulk.OnConflict")
}
return u.create.Exec(ctx)
}
// ExecX is like Exec, but panics if an error occurs.
func (u *ChannelMonitorUpsertBulk) ExecX(ctx context.Context) {
if err := u.create.Exec(ctx); err != nil {
panic(err)
}
}