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
This commit is contained in:
80
backend/ent/schema/channel_monitor_request_template.go
Normal file
80
backend/ent/schema/channel_monitor_request_template.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect/entsql"
|
||||
"entgo.io/ent/schema"
|
||||
"entgo.io/ent/schema/edge"
|
||||
"entgo.io/ent/schema/field"
|
||||
"entgo.io/ent/schema/index"
|
||||
)
|
||||
|
||||
// ChannelMonitorRequestTemplate 请求模板:一组可复用的 headers + 可选 body 覆盖配置。
|
||||
//
|
||||
// 语义为快照:模板被"应用"到监控时,extra_headers / body_override_mode / body_override
|
||||
// 会被**拷贝**到 channel_monitors 同名字段;后续模板变动不会自动影响已应用的监控——
|
||||
// 必须用户主动在模板编辑 Dialog 里点「应用到关联监控」才会覆盖快照。
|
||||
// 这样模板改错不会瞬间打挂所有已经跑起来的监控。
|
||||
type ChannelMonitorRequestTemplate struct {
|
||||
ent.Schema
|
||||
}
|
||||
|
||||
func (ChannelMonitorRequestTemplate) Annotations() []schema.Annotation {
|
||||
return []schema.Annotation{
|
||||
entsql.Annotation{Table: "channel_monitor_request_templates"},
|
||||
}
|
||||
}
|
||||
|
||||
func (ChannelMonitorRequestTemplate) Mixin() []ent.Mixin {
|
||||
return []ent.Mixin{
|
||||
mixins.TimeMixin{},
|
||||
}
|
||||
}
|
||||
|
||||
func (ChannelMonitorRequestTemplate) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.String("name").
|
||||
NotEmpty().
|
||||
MaxLen(100),
|
||||
field.Enum("provider").
|
||||
Values("openai", "anthropic", "gemini"),
|
||||
field.String("description").
|
||||
Optional().
|
||||
Default("").
|
||||
MaxLen(500),
|
||||
// extra_headers: 用户自定义 HTTP 头(如 User-Agent 伪装)。
|
||||
// 运行时 merge 进 adapter 默认 headers,用户值优先;
|
||||
// hop-by-hop 黑名单(Host/Content-Length/...)由 checker 过滤。
|
||||
field.JSON("extra_headers", map[string]string{}).
|
||||
Default(map[string]string{}),
|
||||
// body_override_mode: 'off' | 'merge' | 'replace'
|
||||
// off - 用 adapter 默认 body(忽略 body_override)
|
||||
// merge - adapter 默认 body 与 body_override 浅合并(body_override 优先,
|
||||
// model/messages/contents 等关键字段在 checker 里走黑名单跳过)
|
||||
// replace - 直接用 body_override 作为完整 body;此时跳过 challenge 校验,
|
||||
// 改为 HTTP 2xx + 响应文本非空即视为可用
|
||||
field.String("body_override_mode").
|
||||
Default("off").
|
||||
MaxLen(10),
|
||||
// body_override: JSON 对象,根据 body_override_mode 使用。
|
||||
// 用 map[string]any 以便前端传任意结构(含嵌套)。
|
||||
field.JSON("body_override", map[string]any{}).
|
||||
Optional(),
|
||||
}
|
||||
}
|
||||
|
||||
func (ChannelMonitorRequestTemplate) Edges() []ent.Edge {
|
||||
return []ent.Edge{
|
||||
edge.From("monitors", ChannelMonitor.Type).
|
||||
Ref("request_template"),
|
||||
}
|
||||
}
|
||||
|
||||
func (ChannelMonitorRequestTemplate) Indexes() []ent.Index {
|
||||
return []ent.Index{
|
||||
// 同一 provider 内 name 唯一:允许 Anthropic + OpenAI 重名 "伪装官方客户端"。
|
||||
index.Fields("provider", "name").Unique(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user