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
360 lines
14 KiB
Go
360 lines
14 KiB
Go
// Code generated by ent, DO NOT EDIT.
|
|
|
|
package ent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"entgo.io/ent"
|
|
"entgo.io/ent/dialect/sql"
|
|
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
|
|
"github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
|
|
)
|
|
|
|
// ChannelMonitor is the model entity for the ChannelMonitor schema.
|
|
type ChannelMonitor struct {
|
|
config `json:"-"`
|
|
// ID of the ent.
|
|
ID int64 `json:"id,omitempty"`
|
|
// CreatedAt holds the value of the "created_at" field.
|
|
CreatedAt time.Time `json:"created_at,omitempty"`
|
|
// UpdatedAt holds the value of the "updated_at" field.
|
|
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
|
// Name holds the value of the "name" field.
|
|
Name string `json:"name,omitempty"`
|
|
// Provider holds the value of the "provider" field.
|
|
Provider channelmonitor.Provider `json:"provider,omitempty"`
|
|
// Provider base origin, e.g. https://api.openai.com
|
|
Endpoint string `json:"endpoint,omitempty"`
|
|
// AES-256-GCM encrypted API key
|
|
APIKeyEncrypted string `json:"-"`
|
|
// PrimaryModel holds the value of the "primary_model" field.
|
|
PrimaryModel string `json:"primary_model,omitempty"`
|
|
// Additional model names to test alongside primary_model
|
|
ExtraModels []string `json:"extra_models,omitempty"`
|
|
// GroupName holds the value of the "group_name" field.
|
|
GroupName string `json:"group_name,omitempty"`
|
|
// Enabled holds the value of the "enabled" field.
|
|
Enabled bool `json:"enabled,omitempty"`
|
|
// IntervalSeconds holds the value of the "interval_seconds" field.
|
|
IntervalSeconds int `json:"interval_seconds,omitempty"`
|
|
// LastCheckedAt holds the value of the "last_checked_at" field.
|
|
LastCheckedAt *time.Time `json:"last_checked_at,omitempty"`
|
|
// CreatedBy holds the value of the "created_by" field.
|
|
CreatedBy int64 `json:"created_by,omitempty"`
|
|
// TemplateID holds the value of the "template_id" field.
|
|
TemplateID *int64 `json:"template_id,omitempty"`
|
|
// ExtraHeaders holds the value of the "extra_headers" field.
|
|
ExtraHeaders map[string]string `json:"extra_headers,omitempty"`
|
|
// BodyOverrideMode holds the value of the "body_override_mode" field.
|
|
BodyOverrideMode string `json:"body_override_mode,omitempty"`
|
|
// BodyOverride holds the value of the "body_override" field.
|
|
BodyOverride map[string]interface{} `json:"body_override,omitempty"`
|
|
// Edges holds the relations/edges for other nodes in the graph.
|
|
// The values are being populated by the ChannelMonitorQuery when eager-loading is set.
|
|
Edges ChannelMonitorEdges `json:"edges"`
|
|
selectValues sql.SelectValues
|
|
}
|
|
|
|
// ChannelMonitorEdges holds the relations/edges for other nodes in the graph.
|
|
type ChannelMonitorEdges struct {
|
|
// History holds the value of the history edge.
|
|
History []*ChannelMonitorHistory `json:"history,omitempty"`
|
|
// DailyRollups holds the value of the daily_rollups edge.
|
|
DailyRollups []*ChannelMonitorDailyRollup `json:"daily_rollups,omitempty"`
|
|
// RequestTemplate holds the value of the request_template edge.
|
|
RequestTemplate *ChannelMonitorRequestTemplate `json:"request_template,omitempty"`
|
|
// loadedTypes holds the information for reporting if a
|
|
// type was loaded (or requested) in eager-loading or not.
|
|
loadedTypes [3]bool
|
|
}
|
|
|
|
// HistoryOrErr returns the History value or an error if the edge
|
|
// was not loaded in eager-loading.
|
|
func (e ChannelMonitorEdges) HistoryOrErr() ([]*ChannelMonitorHistory, error) {
|
|
if e.loadedTypes[0] {
|
|
return e.History, nil
|
|
}
|
|
return nil, &NotLoadedError{edge: "history"}
|
|
}
|
|
|
|
// DailyRollupsOrErr returns the DailyRollups value or an error if the edge
|
|
// was not loaded in eager-loading.
|
|
func (e ChannelMonitorEdges) DailyRollupsOrErr() ([]*ChannelMonitorDailyRollup, error) {
|
|
if e.loadedTypes[1] {
|
|
return e.DailyRollups, nil
|
|
}
|
|
return nil, &NotLoadedError{edge: "daily_rollups"}
|
|
}
|
|
|
|
// RequestTemplateOrErr returns the RequestTemplate value or an error if the edge
|
|
// was not loaded in eager-loading, or loaded but was not found.
|
|
func (e ChannelMonitorEdges) RequestTemplateOrErr() (*ChannelMonitorRequestTemplate, error) {
|
|
if e.RequestTemplate != nil {
|
|
return e.RequestTemplate, nil
|
|
} else if e.loadedTypes[2] {
|
|
return nil, &NotFoundError{label: channelmonitorrequesttemplate.Label}
|
|
}
|
|
return nil, &NotLoadedError{edge: "request_template"}
|
|
}
|
|
|
|
// scanValues returns the types for scanning values from sql.Rows.
|
|
func (*ChannelMonitor) scanValues(columns []string) ([]any, error) {
|
|
values := make([]any, len(columns))
|
|
for i := range columns {
|
|
switch columns[i] {
|
|
case channelmonitor.FieldExtraModels, channelmonitor.FieldExtraHeaders, channelmonitor.FieldBodyOverride:
|
|
values[i] = new([]byte)
|
|
case channelmonitor.FieldEnabled:
|
|
values[i] = new(sql.NullBool)
|
|
case channelmonitor.FieldID, channelmonitor.FieldIntervalSeconds, channelmonitor.FieldCreatedBy, channelmonitor.FieldTemplateID:
|
|
values[i] = new(sql.NullInt64)
|
|
case channelmonitor.FieldName, channelmonitor.FieldProvider, channelmonitor.FieldEndpoint, channelmonitor.FieldAPIKeyEncrypted, channelmonitor.FieldPrimaryModel, channelmonitor.FieldGroupName, channelmonitor.FieldBodyOverrideMode:
|
|
values[i] = new(sql.NullString)
|
|
case channelmonitor.FieldCreatedAt, channelmonitor.FieldUpdatedAt, channelmonitor.FieldLastCheckedAt:
|
|
values[i] = new(sql.NullTime)
|
|
default:
|
|
values[i] = new(sql.UnknownType)
|
|
}
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
// assignValues assigns the values that were returned from sql.Rows (after scanning)
|
|
// to the ChannelMonitor fields.
|
|
func (_m *ChannelMonitor) assignValues(columns []string, values []any) error {
|
|
if m, n := len(values), len(columns); m < n {
|
|
return fmt.Errorf("mismatch number of scan values: %d != %d", m, n)
|
|
}
|
|
for i := range columns {
|
|
switch columns[i] {
|
|
case channelmonitor.FieldID:
|
|
value, ok := values[i].(*sql.NullInt64)
|
|
if !ok {
|
|
return fmt.Errorf("unexpected type %T for field id", value)
|
|
}
|
|
_m.ID = int64(value.Int64)
|
|
case channelmonitor.FieldCreatedAt:
|
|
if value, ok := values[i].(*sql.NullTime); !ok {
|
|
return fmt.Errorf("unexpected type %T for field created_at", values[i])
|
|
} else if value.Valid {
|
|
_m.CreatedAt = value.Time
|
|
}
|
|
case channelmonitor.FieldUpdatedAt:
|
|
if value, ok := values[i].(*sql.NullTime); !ok {
|
|
return fmt.Errorf("unexpected type %T for field updated_at", values[i])
|
|
} else if value.Valid {
|
|
_m.UpdatedAt = value.Time
|
|
}
|
|
case channelmonitor.FieldName:
|
|
if value, ok := values[i].(*sql.NullString); !ok {
|
|
return fmt.Errorf("unexpected type %T for field name", values[i])
|
|
} else if value.Valid {
|
|
_m.Name = value.String
|
|
}
|
|
case channelmonitor.FieldProvider:
|
|
if value, ok := values[i].(*sql.NullString); !ok {
|
|
return fmt.Errorf("unexpected type %T for field provider", values[i])
|
|
} else if value.Valid {
|
|
_m.Provider = channelmonitor.Provider(value.String)
|
|
}
|
|
case channelmonitor.FieldEndpoint:
|
|
if value, ok := values[i].(*sql.NullString); !ok {
|
|
return fmt.Errorf("unexpected type %T for field endpoint", values[i])
|
|
} else if value.Valid {
|
|
_m.Endpoint = value.String
|
|
}
|
|
case channelmonitor.FieldAPIKeyEncrypted:
|
|
if value, ok := values[i].(*sql.NullString); !ok {
|
|
return fmt.Errorf("unexpected type %T for field api_key_encrypted", values[i])
|
|
} else if value.Valid {
|
|
_m.APIKeyEncrypted = value.String
|
|
}
|
|
case channelmonitor.FieldPrimaryModel:
|
|
if value, ok := values[i].(*sql.NullString); !ok {
|
|
return fmt.Errorf("unexpected type %T for field primary_model", values[i])
|
|
} else if value.Valid {
|
|
_m.PrimaryModel = value.String
|
|
}
|
|
case channelmonitor.FieldExtraModels:
|
|
if value, ok := values[i].(*[]byte); !ok {
|
|
return fmt.Errorf("unexpected type %T for field extra_models", values[i])
|
|
} else if value != nil && len(*value) > 0 {
|
|
if err := json.Unmarshal(*value, &_m.ExtraModels); err != nil {
|
|
return fmt.Errorf("unmarshal field extra_models: %w", err)
|
|
}
|
|
}
|
|
case channelmonitor.FieldGroupName:
|
|
if value, ok := values[i].(*sql.NullString); !ok {
|
|
return fmt.Errorf("unexpected type %T for field group_name", values[i])
|
|
} else if value.Valid {
|
|
_m.GroupName = value.String
|
|
}
|
|
case channelmonitor.FieldEnabled:
|
|
if value, ok := values[i].(*sql.NullBool); !ok {
|
|
return fmt.Errorf("unexpected type %T for field enabled", values[i])
|
|
} else if value.Valid {
|
|
_m.Enabled = value.Bool
|
|
}
|
|
case channelmonitor.FieldIntervalSeconds:
|
|
if value, ok := values[i].(*sql.NullInt64); !ok {
|
|
return fmt.Errorf("unexpected type %T for field interval_seconds", values[i])
|
|
} else if value.Valid {
|
|
_m.IntervalSeconds = int(value.Int64)
|
|
}
|
|
case channelmonitor.FieldLastCheckedAt:
|
|
if value, ok := values[i].(*sql.NullTime); !ok {
|
|
return fmt.Errorf("unexpected type %T for field last_checked_at", values[i])
|
|
} else if value.Valid {
|
|
_m.LastCheckedAt = new(time.Time)
|
|
*_m.LastCheckedAt = value.Time
|
|
}
|
|
case channelmonitor.FieldCreatedBy:
|
|
if value, ok := values[i].(*sql.NullInt64); !ok {
|
|
return fmt.Errorf("unexpected type %T for field created_by", values[i])
|
|
} else if value.Valid {
|
|
_m.CreatedBy = value.Int64
|
|
}
|
|
case channelmonitor.FieldTemplateID:
|
|
if value, ok := values[i].(*sql.NullInt64); !ok {
|
|
return fmt.Errorf("unexpected type %T for field template_id", values[i])
|
|
} else if value.Valid {
|
|
_m.TemplateID = new(int64)
|
|
*_m.TemplateID = value.Int64
|
|
}
|
|
case channelmonitor.FieldExtraHeaders:
|
|
if value, ok := values[i].(*[]byte); !ok {
|
|
return fmt.Errorf("unexpected type %T for field extra_headers", values[i])
|
|
} else if value != nil && len(*value) > 0 {
|
|
if err := json.Unmarshal(*value, &_m.ExtraHeaders); err != nil {
|
|
return fmt.Errorf("unmarshal field extra_headers: %w", err)
|
|
}
|
|
}
|
|
case channelmonitor.FieldBodyOverrideMode:
|
|
if value, ok := values[i].(*sql.NullString); !ok {
|
|
return fmt.Errorf("unexpected type %T for field body_override_mode", values[i])
|
|
} else if value.Valid {
|
|
_m.BodyOverrideMode = value.String
|
|
}
|
|
case channelmonitor.FieldBodyOverride:
|
|
if value, ok := values[i].(*[]byte); !ok {
|
|
return fmt.Errorf("unexpected type %T for field body_override", values[i])
|
|
} else if value != nil && len(*value) > 0 {
|
|
if err := json.Unmarshal(*value, &_m.BodyOverride); err != nil {
|
|
return fmt.Errorf("unmarshal field body_override: %w", err)
|
|
}
|
|
}
|
|
default:
|
|
_m.selectValues.Set(columns[i], values[i])
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Value returns the ent.Value that was dynamically selected and assigned to the ChannelMonitor.
|
|
// This includes values selected through modifiers, order, etc.
|
|
func (_m *ChannelMonitor) Value(name string) (ent.Value, error) {
|
|
return _m.selectValues.Get(name)
|
|
}
|
|
|
|
// QueryHistory queries the "history" edge of the ChannelMonitor entity.
|
|
func (_m *ChannelMonitor) QueryHistory() *ChannelMonitorHistoryQuery {
|
|
return NewChannelMonitorClient(_m.config).QueryHistory(_m)
|
|
}
|
|
|
|
// QueryDailyRollups queries the "daily_rollups" edge of the ChannelMonitor entity.
|
|
func (_m *ChannelMonitor) QueryDailyRollups() *ChannelMonitorDailyRollupQuery {
|
|
return NewChannelMonitorClient(_m.config).QueryDailyRollups(_m)
|
|
}
|
|
|
|
// QueryRequestTemplate queries the "request_template" edge of the ChannelMonitor entity.
|
|
func (_m *ChannelMonitor) QueryRequestTemplate() *ChannelMonitorRequestTemplateQuery {
|
|
return NewChannelMonitorClient(_m.config).QueryRequestTemplate(_m)
|
|
}
|
|
|
|
// Update returns a builder for updating this ChannelMonitor.
|
|
// Note that you need to call ChannelMonitor.Unwrap() before calling this method if this ChannelMonitor
|
|
// was returned from a transaction, and the transaction was committed or rolled back.
|
|
func (_m *ChannelMonitor) Update() *ChannelMonitorUpdateOne {
|
|
return NewChannelMonitorClient(_m.config).UpdateOne(_m)
|
|
}
|
|
|
|
// Unwrap unwraps the ChannelMonitor entity that was returned from a transaction after it was closed,
|
|
// so that all future queries will be executed through the driver which created the transaction.
|
|
func (_m *ChannelMonitor) Unwrap() *ChannelMonitor {
|
|
_tx, ok := _m.config.driver.(*txDriver)
|
|
if !ok {
|
|
panic("ent: ChannelMonitor is not a transactional entity")
|
|
}
|
|
_m.config.driver = _tx.drv
|
|
return _m
|
|
}
|
|
|
|
// String implements the fmt.Stringer.
|
|
func (_m *ChannelMonitor) String() string {
|
|
var builder strings.Builder
|
|
builder.WriteString("ChannelMonitor(")
|
|
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
|
builder.WriteString("created_at=")
|
|
builder.WriteString(_m.CreatedAt.Format(time.ANSIC))
|
|
builder.WriteString(", ")
|
|
builder.WriteString("updated_at=")
|
|
builder.WriteString(_m.UpdatedAt.Format(time.ANSIC))
|
|
builder.WriteString(", ")
|
|
builder.WriteString("name=")
|
|
builder.WriteString(_m.Name)
|
|
builder.WriteString(", ")
|
|
builder.WriteString("provider=")
|
|
builder.WriteString(fmt.Sprintf("%v", _m.Provider))
|
|
builder.WriteString(", ")
|
|
builder.WriteString("endpoint=")
|
|
builder.WriteString(_m.Endpoint)
|
|
builder.WriteString(", ")
|
|
builder.WriteString("api_key_encrypted=<sensitive>")
|
|
builder.WriteString(", ")
|
|
builder.WriteString("primary_model=")
|
|
builder.WriteString(_m.PrimaryModel)
|
|
builder.WriteString(", ")
|
|
builder.WriteString("extra_models=")
|
|
builder.WriteString(fmt.Sprintf("%v", _m.ExtraModels))
|
|
builder.WriteString(", ")
|
|
builder.WriteString("group_name=")
|
|
builder.WriteString(_m.GroupName)
|
|
builder.WriteString(", ")
|
|
builder.WriteString("enabled=")
|
|
builder.WriteString(fmt.Sprintf("%v", _m.Enabled))
|
|
builder.WriteString(", ")
|
|
builder.WriteString("interval_seconds=")
|
|
builder.WriteString(fmt.Sprintf("%v", _m.IntervalSeconds))
|
|
builder.WriteString(", ")
|
|
if v := _m.LastCheckedAt; v != nil {
|
|
builder.WriteString("last_checked_at=")
|
|
builder.WriteString(v.Format(time.ANSIC))
|
|
}
|
|
builder.WriteString(", ")
|
|
builder.WriteString("created_by=")
|
|
builder.WriteString(fmt.Sprintf("%v", _m.CreatedBy))
|
|
builder.WriteString(", ")
|
|
if v := _m.TemplateID; v != nil {
|
|
builder.WriteString("template_id=")
|
|
builder.WriteString(fmt.Sprintf("%v", *v))
|
|
}
|
|
builder.WriteString(", ")
|
|
builder.WriteString("extra_headers=")
|
|
builder.WriteString(fmt.Sprintf("%v", _m.ExtraHeaders))
|
|
builder.WriteString(", ")
|
|
builder.WriteString("body_override_mode=")
|
|
builder.WriteString(_m.BodyOverrideMode)
|
|
builder.WriteString(", ")
|
|
builder.WriteString("body_override=")
|
|
builder.WriteString(fmt.Sprintf("%v", _m.BodyOverride))
|
|
builder.WriteByte(')')
|
|
return builder.String()
|
|
}
|
|
|
|
// ChannelMonitors is a parsable slice of ChannelMonitor.
|
|
type ChannelMonitors []*ChannelMonitor
|