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:
erio
2026-04-21 14:14:49 +08:00
parent 0c48f08f5c
commit a296425994
53 changed files with 8318 additions and 394 deletions

View File

@@ -14,6 +14,7 @@ import (
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitordailyrollup"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
)
// ChannelMonitorCreate is the builder for creating a ChannelMonitor entity.
@@ -142,6 +143,46 @@ func (_c *ChannelMonitorCreate) SetCreatedBy(v int64) *ChannelMonitorCreate {
return _c
}
// 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
}
// AddHistoryIDs adds the "history" edge to the ChannelMonitorHistory entity by IDs.
func (_c *ChannelMonitorCreate) AddHistoryIDs(ids ...int64) *ChannelMonitorCreate {
_c.mutation.AddHistoryIDs(ids...)
@@ -172,6 +213,25 @@ func (_c *ChannelMonitorCreate) AddDailyRollups(v ...*ChannelMonitorDailyRollup)
return _c.AddDailyRollupIDs(ids...)
}
// 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)
}
// Mutation returns the ChannelMonitorMutation object of the builder.
func (_c *ChannelMonitorCreate) Mutation() *ChannelMonitorMutation {
return _c.mutation
@@ -227,6 +287,14 @@ func (_c *ChannelMonitorCreate) defaults() {
v := channelmonitor.DefaultEnabled
_c.mutation.SetEnabled(v)
}
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)
}
}
// check runs all checks and user-defined validators on the builder.
@@ -299,6 +367,17 @@ func (_c *ChannelMonitorCreate) check() error {
if _, ok := _c.mutation.CreatedBy(); !ok {
return &ValidationError{Name: "created_by", err: errors.New(`ent: missing required field "ChannelMonitor.created_by"`)}
}
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)}
}
}
return nil
}
@@ -378,6 +457,18 @@ func (_c *ChannelMonitorCreate) createSpec() (*ChannelMonitor, *sqlgraph.CreateS
_spec.SetField(channelmonitor.FieldCreatedBy, field.TypeInt64, value)
_node.CreatedBy = value
}
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
}
if nodes := _c.mutation.HistoryIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
@@ -410,6 +501,23 @@ func (_c *ChannelMonitorCreate) createSpec() (*ChannelMonitor, *sqlgraph.CreateS
}
_spec.Edges = append(_spec.Edges, edge)
}
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)
}
return _node, _spec
}
@@ -630,6 +738,66 @@ func (u *ChannelMonitorUpsert) AddCreatedBy(v int64) *ChannelMonitorUpsert {
return u
}
// 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
}
// UpdateNewValues updates the mutable fields using the new values that were set on create.
// Using this option is equivalent to using:
//
@@ -871,6 +1039,76 @@ func (u *ChannelMonitorUpsertOne) UpdateCreatedBy() *ChannelMonitorUpsertOne {
})
}
// 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()
})
}
// Exec executes the query.
func (u *ChannelMonitorUpsertOne) Exec(ctx context.Context) error {
if len(u.create.conflict) == 0 {
@@ -1278,6 +1516,76 @@ func (u *ChannelMonitorUpsertBulk) UpdateCreatedBy() *ChannelMonitorUpsertBulk {
})
}
// 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()
})
}
// Exec executes the query.
func (u *ChannelMonitorUpsertBulk) Exec(ctx context.Context) error {
if u.create.err != nil {