Commit 20a4e418 authored by erio's avatar erio
Browse files

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 全绿
parent 0b85a8da
...@@ -19,6 +19,8 @@ import ( ...@@ -19,6 +19,8 @@ import (
"github.com/Wei-Shaw/sub2api/ent/apikey" "github.com/Wei-Shaw/sub2api/ent/apikey"
"github.com/Wei-Shaw/sub2api/ent/authidentity" "github.com/Wei-Shaw/sub2api/ent/authidentity"
"github.com/Wei-Shaw/sub2api/ent/authidentitychannel" "github.com/Wei-Shaw/sub2api/ent/authidentitychannel"
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
"github.com/Wei-Shaw/sub2api/ent/errorpassthroughrule" "github.com/Wei-Shaw/sub2api/ent/errorpassthroughrule"
"github.com/Wei-Shaw/sub2api/ent/group" "github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/idempotencyrecord" "github.com/Wei-Shaw/sub2api/ent/idempotencyrecord"
...@@ -62,6 +64,8 @@ const ( ...@@ -62,6 +64,8 @@ const (
TypeAnnouncementRead = "AnnouncementRead" TypeAnnouncementRead = "AnnouncementRead"
TypeAuthIdentity = "AuthIdentity" TypeAuthIdentity = "AuthIdentity"
TypeAuthIdentityChannel = "AuthIdentityChannel" TypeAuthIdentityChannel = "AuthIdentityChannel"
TypeChannelMonitor = "ChannelMonitor"
TypeChannelMonitorHistory = "ChannelMonitorHistory"
TypeErrorPassthroughRule = "ErrorPassthroughRule" TypeErrorPassthroughRule = "ErrorPassthroughRule"
TypeGroup = "Group" TypeGroup = "Group"
TypeIdempotencyRecord = "IdempotencyRecord" TypeIdempotencyRecord = "IdempotencyRecord"
...@@ -8734,6 +8738,2034 @@ func (m *AuthIdentityChannelMutation) ResetEdge(name string) error { ...@@ -8734,6 +8738,2034 @@ func (m *AuthIdentityChannelMutation) ResetEdge(name string) error {
return fmt.Errorf("unknown AuthIdentityChannel edge %s", name) return fmt.Errorf("unknown AuthIdentityChannel edge %s", name)
} }
   
// ChannelMonitorMutation represents an operation that mutates the ChannelMonitor nodes in the graph.
type ChannelMonitorMutation struct {
config
op Op
typ string
id *int64
created_at *time.Time
updated_at *time.Time
name *string
provider *channelmonitor.Provider
endpoint *string
api_key_encrypted *string
primary_model *string
extra_models *[]string
appendextra_models []string
group_name *string
enabled *bool
interval_seconds *int
addinterval_seconds *int
last_checked_at *time.Time
created_by *int64
addcreated_by *int64
clearedFields map[string]struct{}
history map[int64]struct{}
removedhistory map[int64]struct{}
clearedhistory bool
done bool
oldValue func(context.Context) (*ChannelMonitor, error)
predicates []predicate.ChannelMonitor
}
var _ ent.Mutation = (*ChannelMonitorMutation)(nil)
// channelmonitorOption allows management of the mutation configuration using functional options.
type channelmonitorOption func(*ChannelMonitorMutation)
// newChannelMonitorMutation creates new mutation for the ChannelMonitor entity.
func newChannelMonitorMutation(c config, op Op, opts ...channelmonitorOption) *ChannelMonitorMutation {
m := &ChannelMonitorMutation{
config: c,
op: op,
typ: TypeChannelMonitor,
clearedFields: make(map[string]struct{}),
}
for _, opt := range opts {
opt(m)
}
return m
}
// withChannelMonitorID sets the ID field of the mutation.
func withChannelMonitorID(id int64) channelmonitorOption {
return func(m *ChannelMonitorMutation) {
var (
err error
once sync.Once
value *ChannelMonitor
)
m.oldValue = func(ctx context.Context) (*ChannelMonitor, error) {
once.Do(func() {
if m.done {
err = errors.New("querying old values post mutation is not allowed")
} else {
value, err = m.Client().ChannelMonitor.Get(ctx, id)
}
})
return value, err
}
m.id = &id
}
}
// withChannelMonitor sets the old ChannelMonitor of the mutation.
func withChannelMonitor(node *ChannelMonitor) channelmonitorOption {
return func(m *ChannelMonitorMutation) {
m.oldValue = func(context.Context) (*ChannelMonitor, error) {
return node, nil
}
m.id = &node.ID
}
}
// Client returns a new `ent.Client` from the mutation. If the mutation was
// executed in a transaction (ent.Tx), a transactional client is returned.
func (m ChannelMonitorMutation) Client() *Client {
client := &Client{config: m.config}
client.init()
return client
}
// Tx returns an `ent.Tx` for mutations that were executed in transactions;
// it returns an error otherwise.
func (m ChannelMonitorMutation) Tx() (*Tx, error) {
if _, ok := m.driver.(*txDriver); !ok {
return nil, errors.New("ent: mutation is not running in a transaction")
}
tx := &Tx{config: m.config}
tx.init()
return tx, nil
}
// ID returns the ID value in the mutation. Note that the ID is only available
// if it was provided to the builder or after it was returned from the database.
func (m *ChannelMonitorMutation) ID() (id int64, exists bool) {
if m.id == nil {
return
}
return *m.id, true
}
// IDs queries the database and returns the entity ids that match the mutation's predicate.
// That means, if the mutation is applied within a transaction with an isolation level such
// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated
// or updated by the mutation.
func (m *ChannelMonitorMutation) IDs(ctx context.Context) ([]int64, error) {
switch {
case m.op.Is(OpUpdateOne | OpDeleteOne):
id, exists := m.ID()
if exists {
return []int64{id}, nil
}
fallthrough
case m.op.Is(OpUpdate | OpDelete):
return m.Client().ChannelMonitor.Query().Where(m.predicates...).IDs(ctx)
default:
return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op)
}
}
// SetCreatedAt sets the "created_at" field.
func (m *ChannelMonitorMutation) SetCreatedAt(t time.Time) {
m.created_at = &t
}
// CreatedAt returns the value of the "created_at" field in the mutation.
func (m *ChannelMonitorMutation) CreatedAt() (r time.Time, exists bool) {
v := m.created_at
if v == nil {
return
}
return *v, true
}
// OldCreatedAt returns the old "created_at" field's value of the ChannelMonitor entity.
// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldCreatedAt requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldCreatedAt: %w", err)
}
return oldValue.CreatedAt, nil
}
// ResetCreatedAt resets all changes to the "created_at" field.
func (m *ChannelMonitorMutation) ResetCreatedAt() {
m.created_at = nil
}
// SetUpdatedAt sets the "updated_at" field.
func (m *ChannelMonitorMutation) SetUpdatedAt(t time.Time) {
m.updated_at = &t
}
// UpdatedAt returns the value of the "updated_at" field in the mutation.
func (m *ChannelMonitorMutation) UpdatedAt() (r time.Time, exists bool) {
v := m.updated_at
if v == nil {
return
}
return *v, true
}
// OldUpdatedAt returns the old "updated_at" field's value of the ChannelMonitor entity.
// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorMutation) OldUpdatedAt(ctx context.Context) (v time.Time, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldUpdatedAt is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldUpdatedAt requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldUpdatedAt: %w", err)
}
return oldValue.UpdatedAt, nil
}
// ResetUpdatedAt resets all changes to the "updated_at" field.
func (m *ChannelMonitorMutation) ResetUpdatedAt() {
m.updated_at = nil
}
// SetName sets the "name" field.
func (m *ChannelMonitorMutation) SetName(s string) {
m.name = &s
}
// Name returns the value of the "name" field in the mutation.
func (m *ChannelMonitorMutation) Name() (r string, exists bool) {
v := m.name
if v == nil {
return
}
return *v, true
}
// OldName returns the old "name" field's value of the ChannelMonitor entity.
// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorMutation) OldName(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldName is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldName requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldName: %w", err)
}
return oldValue.Name, nil
}
// ResetName resets all changes to the "name" field.
func (m *ChannelMonitorMutation) ResetName() {
m.name = nil
}
// SetProvider sets the "provider" field.
func (m *ChannelMonitorMutation) SetProvider(c channelmonitor.Provider) {
m.provider = &c
}
// Provider returns the value of the "provider" field in the mutation.
func (m *ChannelMonitorMutation) Provider() (r channelmonitor.Provider, exists bool) {
v := m.provider
if v == nil {
return
}
return *v, true
}
// OldProvider returns the old "provider" field's value of the ChannelMonitor entity.
// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorMutation) OldProvider(ctx context.Context) (v channelmonitor.Provider, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldProvider is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldProvider requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldProvider: %w", err)
}
return oldValue.Provider, nil
}
// ResetProvider resets all changes to the "provider" field.
func (m *ChannelMonitorMutation) ResetProvider() {
m.provider = nil
}
// SetEndpoint sets the "endpoint" field.
func (m *ChannelMonitorMutation) SetEndpoint(s string) {
m.endpoint = &s
}
// Endpoint returns the value of the "endpoint" field in the mutation.
func (m *ChannelMonitorMutation) Endpoint() (r string, exists bool) {
v := m.endpoint
if v == nil {
return
}
return *v, true
}
// OldEndpoint returns the old "endpoint" field's value of the ChannelMonitor entity.
// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorMutation) OldEndpoint(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldEndpoint is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldEndpoint requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldEndpoint: %w", err)
}
return oldValue.Endpoint, nil
}
// ResetEndpoint resets all changes to the "endpoint" field.
func (m *ChannelMonitorMutation) ResetEndpoint() {
m.endpoint = nil
}
// SetAPIKeyEncrypted sets the "api_key_encrypted" field.
func (m *ChannelMonitorMutation) SetAPIKeyEncrypted(s string) {
m.api_key_encrypted = &s
}
// APIKeyEncrypted returns the value of the "api_key_encrypted" field in the mutation.
func (m *ChannelMonitorMutation) APIKeyEncrypted() (r string, exists bool) {
v := m.api_key_encrypted
if v == nil {
return
}
return *v, true
}
// OldAPIKeyEncrypted returns the old "api_key_encrypted" field's value of the ChannelMonitor entity.
// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorMutation) OldAPIKeyEncrypted(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldAPIKeyEncrypted is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldAPIKeyEncrypted requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldAPIKeyEncrypted: %w", err)
}
return oldValue.APIKeyEncrypted, nil
}
// ResetAPIKeyEncrypted resets all changes to the "api_key_encrypted" field.
func (m *ChannelMonitorMutation) ResetAPIKeyEncrypted() {
m.api_key_encrypted = nil
}
// SetPrimaryModel sets the "primary_model" field.
func (m *ChannelMonitorMutation) SetPrimaryModel(s string) {
m.primary_model = &s
}
// PrimaryModel returns the value of the "primary_model" field in the mutation.
func (m *ChannelMonitorMutation) PrimaryModel() (r string, exists bool) {
v := m.primary_model
if v == nil {
return
}
return *v, true
}
// OldPrimaryModel returns the old "primary_model" field's value of the ChannelMonitor entity.
// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorMutation) OldPrimaryModel(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldPrimaryModel is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldPrimaryModel requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldPrimaryModel: %w", err)
}
return oldValue.PrimaryModel, nil
}
// ResetPrimaryModel resets all changes to the "primary_model" field.
func (m *ChannelMonitorMutation) ResetPrimaryModel() {
m.primary_model = nil
}
// SetExtraModels sets the "extra_models" field.
func (m *ChannelMonitorMutation) SetExtraModels(s []string) {
m.extra_models = &s
m.appendextra_models = nil
}
// ExtraModels returns the value of the "extra_models" field in the mutation.
func (m *ChannelMonitorMutation) ExtraModels() (r []string, exists bool) {
v := m.extra_models
if v == nil {
return
}
return *v, true
}
// OldExtraModels returns the old "extra_models" field's value of the ChannelMonitor entity.
// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorMutation) OldExtraModels(ctx context.Context) (v []string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldExtraModels is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldExtraModels requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldExtraModels: %w", err)
}
return oldValue.ExtraModels, nil
}
// AppendExtraModels adds s to the "extra_models" field.
func (m *ChannelMonitorMutation) AppendExtraModels(s []string) {
m.appendextra_models = append(m.appendextra_models, s...)
}
// AppendedExtraModels returns the list of values that were appended to the "extra_models" field in this mutation.
func (m *ChannelMonitorMutation) AppendedExtraModels() ([]string, bool) {
if len(m.appendextra_models) == 0 {
return nil, false
}
return m.appendextra_models, true
}
// ResetExtraModels resets all changes to the "extra_models" field.
func (m *ChannelMonitorMutation) ResetExtraModels() {
m.extra_models = nil
m.appendextra_models = nil
}
// SetGroupName sets the "group_name" field.
func (m *ChannelMonitorMutation) SetGroupName(s string) {
m.group_name = &s
}
// GroupName returns the value of the "group_name" field in the mutation.
func (m *ChannelMonitorMutation) GroupName() (r string, exists bool) {
v := m.group_name
if v == nil {
return
}
return *v, true
}
// OldGroupName returns the old "group_name" field's value of the ChannelMonitor entity.
// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorMutation) OldGroupName(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldGroupName is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldGroupName requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldGroupName: %w", err)
}
return oldValue.GroupName, nil
}
// ClearGroupName clears the value of the "group_name" field.
func (m *ChannelMonitorMutation) ClearGroupName() {
m.group_name = nil
m.clearedFields[channelmonitor.FieldGroupName] = struct{}{}
}
// GroupNameCleared returns if the "group_name" field was cleared in this mutation.
func (m *ChannelMonitorMutation) GroupNameCleared() bool {
_, ok := m.clearedFields[channelmonitor.FieldGroupName]
return ok
}
// ResetGroupName resets all changes to the "group_name" field.
func (m *ChannelMonitorMutation) ResetGroupName() {
m.group_name = nil
delete(m.clearedFields, channelmonitor.FieldGroupName)
}
// SetEnabled sets the "enabled" field.
func (m *ChannelMonitorMutation) SetEnabled(b bool) {
m.enabled = &b
}
// Enabled returns the value of the "enabled" field in the mutation.
func (m *ChannelMonitorMutation) Enabled() (r bool, exists bool) {
v := m.enabled
if v == nil {
return
}
return *v, true
}
// OldEnabled returns the old "enabled" field's value of the ChannelMonitor entity.
// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorMutation) OldEnabled(ctx context.Context) (v bool, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldEnabled is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldEnabled requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldEnabled: %w", err)
}
return oldValue.Enabled, nil
}
// ResetEnabled resets all changes to the "enabled" field.
func (m *ChannelMonitorMutation) ResetEnabled() {
m.enabled = nil
}
// SetIntervalSeconds sets the "interval_seconds" field.
func (m *ChannelMonitorMutation) SetIntervalSeconds(i int) {
m.interval_seconds = &i
m.addinterval_seconds = nil
}
// IntervalSeconds returns the value of the "interval_seconds" field in the mutation.
func (m *ChannelMonitorMutation) IntervalSeconds() (r int, exists bool) {
v := m.interval_seconds
if v == nil {
return
}
return *v, true
}
// OldIntervalSeconds returns the old "interval_seconds" field's value of the ChannelMonitor entity.
// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorMutation) OldIntervalSeconds(ctx context.Context) (v int, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldIntervalSeconds is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldIntervalSeconds requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldIntervalSeconds: %w", err)
}
return oldValue.IntervalSeconds, nil
}
// AddIntervalSeconds adds i to the "interval_seconds" field.
func (m *ChannelMonitorMutation) AddIntervalSeconds(i int) {
if m.addinterval_seconds != nil {
*m.addinterval_seconds += i
} else {
m.addinterval_seconds = &i
}
}
// AddedIntervalSeconds returns the value that was added to the "interval_seconds" field in this mutation.
func (m *ChannelMonitorMutation) AddedIntervalSeconds() (r int, exists bool) {
v := m.addinterval_seconds
if v == nil {
return
}
return *v, true
}
// ResetIntervalSeconds resets all changes to the "interval_seconds" field.
func (m *ChannelMonitorMutation) ResetIntervalSeconds() {
m.interval_seconds = nil
m.addinterval_seconds = nil
}
// SetLastCheckedAt sets the "last_checked_at" field.
func (m *ChannelMonitorMutation) SetLastCheckedAt(t time.Time) {
m.last_checked_at = &t
}
// LastCheckedAt returns the value of the "last_checked_at" field in the mutation.
func (m *ChannelMonitorMutation) LastCheckedAt() (r time.Time, exists bool) {
v := m.last_checked_at
if v == nil {
return
}
return *v, true
}
// OldLastCheckedAt returns the old "last_checked_at" field's value of the ChannelMonitor entity.
// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorMutation) OldLastCheckedAt(ctx context.Context) (v *time.Time, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldLastCheckedAt is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldLastCheckedAt requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldLastCheckedAt: %w", err)
}
return oldValue.LastCheckedAt, nil
}
// ClearLastCheckedAt clears the value of the "last_checked_at" field.
func (m *ChannelMonitorMutation) ClearLastCheckedAt() {
m.last_checked_at = nil
m.clearedFields[channelmonitor.FieldLastCheckedAt] = struct{}{}
}
// LastCheckedAtCleared returns if the "last_checked_at" field was cleared in this mutation.
func (m *ChannelMonitorMutation) LastCheckedAtCleared() bool {
_, ok := m.clearedFields[channelmonitor.FieldLastCheckedAt]
return ok
}
// ResetLastCheckedAt resets all changes to the "last_checked_at" field.
func (m *ChannelMonitorMutation) ResetLastCheckedAt() {
m.last_checked_at = nil
delete(m.clearedFields, channelmonitor.FieldLastCheckedAt)
}
// SetCreatedBy sets the "created_by" field.
func (m *ChannelMonitorMutation) SetCreatedBy(i int64) {
m.created_by = &i
m.addcreated_by = nil
}
// CreatedBy returns the value of the "created_by" field in the mutation.
func (m *ChannelMonitorMutation) CreatedBy() (r int64, exists bool) {
v := m.created_by
if v == nil {
return
}
return *v, true
}
// OldCreatedBy returns the old "created_by" field's value of the ChannelMonitor entity.
// If the ChannelMonitor object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorMutation) OldCreatedBy(ctx context.Context) (v int64, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldCreatedBy is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldCreatedBy requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldCreatedBy: %w", err)
}
return oldValue.CreatedBy, nil
}
// AddCreatedBy adds i to the "created_by" field.
func (m *ChannelMonitorMutation) AddCreatedBy(i int64) {
if m.addcreated_by != nil {
*m.addcreated_by += i
} else {
m.addcreated_by = &i
}
}
// AddedCreatedBy returns the value that was added to the "created_by" field in this mutation.
func (m *ChannelMonitorMutation) AddedCreatedBy() (r int64, exists bool) {
v := m.addcreated_by
if v == nil {
return
}
return *v, true
}
// ResetCreatedBy resets all changes to the "created_by" field.
func (m *ChannelMonitorMutation) ResetCreatedBy() {
m.created_by = nil
m.addcreated_by = nil
}
// AddHistoryIDs adds the "history" edge to the ChannelMonitorHistory entity by ids.
func (m *ChannelMonitorMutation) AddHistoryIDs(ids ...int64) {
if m.history == nil {
m.history = make(map[int64]struct{})
}
for i := range ids {
m.history[ids[i]] = struct{}{}
}
}
// ClearHistory clears the "history" edge to the ChannelMonitorHistory entity.
func (m *ChannelMonitorMutation) ClearHistory() {
m.clearedhistory = true
}
// HistoryCleared reports if the "history" edge to the ChannelMonitorHistory entity was cleared.
func (m *ChannelMonitorMutation) HistoryCleared() bool {
return m.clearedhistory
}
// RemoveHistoryIDs removes the "history" edge to the ChannelMonitorHistory entity by IDs.
func (m *ChannelMonitorMutation) RemoveHistoryIDs(ids ...int64) {
if m.removedhistory == nil {
m.removedhistory = make(map[int64]struct{})
}
for i := range ids {
delete(m.history, ids[i])
m.removedhistory[ids[i]] = struct{}{}
}
}
// RemovedHistory returns the removed IDs of the "history" edge to the ChannelMonitorHistory entity.
func (m *ChannelMonitorMutation) RemovedHistoryIDs() (ids []int64) {
for id := range m.removedhistory {
ids = append(ids, id)
}
return
}
// HistoryIDs returns the "history" edge IDs in the mutation.
func (m *ChannelMonitorMutation) HistoryIDs() (ids []int64) {
for id := range m.history {
ids = append(ids, id)
}
return
}
// ResetHistory resets all changes to the "history" edge.
func (m *ChannelMonitorMutation) ResetHistory() {
m.history = nil
m.clearedhistory = false
m.removedhistory = nil
}
// Where appends a list predicates to the ChannelMonitorMutation builder.
func (m *ChannelMonitorMutation) Where(ps ...predicate.ChannelMonitor) {
m.predicates = append(m.predicates, ps...)
}
// WhereP appends storage-level predicates to the ChannelMonitorMutation builder. Using this method,
// users can use type-assertion to append predicates that do not depend on any generated package.
func (m *ChannelMonitorMutation) WhereP(ps ...func(*sql.Selector)) {
p := make([]predicate.ChannelMonitor, len(ps))
for i := range ps {
p[i] = ps[i]
}
m.Where(p...)
}
// Op returns the operation name.
func (m *ChannelMonitorMutation) Op() Op {
return m.op
}
// SetOp allows setting the mutation operation.
func (m *ChannelMonitorMutation) SetOp(op Op) {
m.op = op
}
// Type returns the node type of this mutation (ChannelMonitor).
func (m *ChannelMonitorMutation) Type() string {
return m.typ
}
// Fields returns all fields that were changed during this mutation. Note that in
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *ChannelMonitorMutation) Fields() []string {
fields := make([]string, 0, 13)
if m.created_at != nil {
fields = append(fields, channelmonitor.FieldCreatedAt)
}
if m.updated_at != nil {
fields = append(fields, channelmonitor.FieldUpdatedAt)
}
if m.name != nil {
fields = append(fields, channelmonitor.FieldName)
}
if m.provider != nil {
fields = append(fields, channelmonitor.FieldProvider)
}
if m.endpoint != nil {
fields = append(fields, channelmonitor.FieldEndpoint)
}
if m.api_key_encrypted != nil {
fields = append(fields, channelmonitor.FieldAPIKeyEncrypted)
}
if m.primary_model != nil {
fields = append(fields, channelmonitor.FieldPrimaryModel)
}
if m.extra_models != nil {
fields = append(fields, channelmonitor.FieldExtraModels)
}
if m.group_name != nil {
fields = append(fields, channelmonitor.FieldGroupName)
}
if m.enabled != nil {
fields = append(fields, channelmonitor.FieldEnabled)
}
if m.interval_seconds != nil {
fields = append(fields, channelmonitor.FieldIntervalSeconds)
}
if m.last_checked_at != nil {
fields = append(fields, channelmonitor.FieldLastCheckedAt)
}
if m.created_by != nil {
fields = append(fields, channelmonitor.FieldCreatedBy)
}
return fields
}
// Field returns the value of a field with the given name. The second boolean
// return value indicates that this field was not set, or was not defined in the
// schema.
func (m *ChannelMonitorMutation) Field(name string) (ent.Value, bool) {
switch name {
case channelmonitor.FieldCreatedAt:
return m.CreatedAt()
case channelmonitor.FieldUpdatedAt:
return m.UpdatedAt()
case channelmonitor.FieldName:
return m.Name()
case channelmonitor.FieldProvider:
return m.Provider()
case channelmonitor.FieldEndpoint:
return m.Endpoint()
case channelmonitor.FieldAPIKeyEncrypted:
return m.APIKeyEncrypted()
case channelmonitor.FieldPrimaryModel:
return m.PrimaryModel()
case channelmonitor.FieldExtraModels:
return m.ExtraModels()
case channelmonitor.FieldGroupName:
return m.GroupName()
case channelmonitor.FieldEnabled:
return m.Enabled()
case channelmonitor.FieldIntervalSeconds:
return m.IntervalSeconds()
case channelmonitor.FieldLastCheckedAt:
return m.LastCheckedAt()
case channelmonitor.FieldCreatedBy:
return m.CreatedBy()
}
return nil, false
}
// OldField returns the old value of the field from the database. An error is
// returned if the mutation operation is not UpdateOne, or the query to the
// database failed.
func (m *ChannelMonitorMutation) OldField(ctx context.Context, name string) (ent.Value, error) {
switch name {
case channelmonitor.FieldCreatedAt:
return m.OldCreatedAt(ctx)
case channelmonitor.FieldUpdatedAt:
return m.OldUpdatedAt(ctx)
case channelmonitor.FieldName:
return m.OldName(ctx)
case channelmonitor.FieldProvider:
return m.OldProvider(ctx)
case channelmonitor.FieldEndpoint:
return m.OldEndpoint(ctx)
case channelmonitor.FieldAPIKeyEncrypted:
return m.OldAPIKeyEncrypted(ctx)
case channelmonitor.FieldPrimaryModel:
return m.OldPrimaryModel(ctx)
case channelmonitor.FieldExtraModels:
return m.OldExtraModels(ctx)
case channelmonitor.FieldGroupName:
return m.OldGroupName(ctx)
case channelmonitor.FieldEnabled:
return m.OldEnabled(ctx)
case channelmonitor.FieldIntervalSeconds:
return m.OldIntervalSeconds(ctx)
case channelmonitor.FieldLastCheckedAt:
return m.OldLastCheckedAt(ctx)
case channelmonitor.FieldCreatedBy:
return m.OldCreatedBy(ctx)
}
return nil, fmt.Errorf("unknown ChannelMonitor field %s", name)
}
// SetField sets the value of a field with the given name. It returns an error if
// the field is not defined in the schema, or if the type mismatched the field
// type.
func (m *ChannelMonitorMutation) SetField(name string, value ent.Value) error {
switch name {
case channelmonitor.FieldCreatedAt:
v, ok := value.(time.Time)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetCreatedAt(v)
return nil
case channelmonitor.FieldUpdatedAt:
v, ok := value.(time.Time)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetUpdatedAt(v)
return nil
case channelmonitor.FieldName:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetName(v)
return nil
case channelmonitor.FieldProvider:
v, ok := value.(channelmonitor.Provider)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetProvider(v)
return nil
case channelmonitor.FieldEndpoint:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetEndpoint(v)
return nil
case channelmonitor.FieldAPIKeyEncrypted:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetAPIKeyEncrypted(v)
return nil
case channelmonitor.FieldPrimaryModel:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetPrimaryModel(v)
return nil
case channelmonitor.FieldExtraModels:
v, ok := value.([]string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetExtraModels(v)
return nil
case channelmonitor.FieldGroupName:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetGroupName(v)
return nil
case channelmonitor.FieldEnabled:
v, ok := value.(bool)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetEnabled(v)
return nil
case channelmonitor.FieldIntervalSeconds:
v, ok := value.(int)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetIntervalSeconds(v)
return nil
case channelmonitor.FieldLastCheckedAt:
v, ok := value.(time.Time)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetLastCheckedAt(v)
return nil
case channelmonitor.FieldCreatedBy:
v, ok := value.(int64)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetCreatedBy(v)
return nil
}
return fmt.Errorf("unknown ChannelMonitor field %s", name)
}
// AddedFields returns all numeric fields that were incremented/decremented during
// this mutation.
func (m *ChannelMonitorMutation) AddedFields() []string {
var fields []string
if m.addinterval_seconds != nil {
fields = append(fields, channelmonitor.FieldIntervalSeconds)
}
if m.addcreated_by != nil {
fields = append(fields, channelmonitor.FieldCreatedBy)
}
return fields
}
// AddedField returns the numeric value that was incremented/decremented on a field
// with the given name. The second boolean return value indicates that this field
// was not set, or was not defined in the schema.
func (m *ChannelMonitorMutation) AddedField(name string) (ent.Value, bool) {
switch name {
case channelmonitor.FieldIntervalSeconds:
return m.AddedIntervalSeconds()
case channelmonitor.FieldCreatedBy:
return m.AddedCreatedBy()
}
return nil, false
}
// AddField adds the value to the field with the given name. It returns an error if
// the field is not defined in the schema, or if the type mismatched the field
// type.
func (m *ChannelMonitorMutation) AddField(name string, value ent.Value) error {
switch name {
case channelmonitor.FieldIntervalSeconds:
v, ok := value.(int)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.AddIntervalSeconds(v)
return nil
case channelmonitor.FieldCreatedBy:
v, ok := value.(int64)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.AddCreatedBy(v)
return nil
}
return fmt.Errorf("unknown ChannelMonitor numeric field %s", name)
}
// ClearedFields returns all nullable fields that were cleared during this
// mutation.
func (m *ChannelMonitorMutation) ClearedFields() []string {
var fields []string
if m.FieldCleared(channelmonitor.FieldGroupName) {
fields = append(fields, channelmonitor.FieldGroupName)
}
if m.FieldCleared(channelmonitor.FieldLastCheckedAt) {
fields = append(fields, channelmonitor.FieldLastCheckedAt)
}
return fields
}
// FieldCleared returns a boolean indicating if a field with the given name was
// cleared in this mutation.
func (m *ChannelMonitorMutation) FieldCleared(name string) bool {
_, ok := m.clearedFields[name]
return ok
}
// ClearField clears the value of the field with the given name. It returns an
// error if the field is not defined in the schema.
func (m *ChannelMonitorMutation) ClearField(name string) error {
switch name {
case channelmonitor.FieldGroupName:
m.ClearGroupName()
return nil
case channelmonitor.FieldLastCheckedAt:
m.ClearLastCheckedAt()
return nil
}
return fmt.Errorf("unknown ChannelMonitor nullable field %s", name)
}
// ResetField resets all changes in the mutation for the field with the given name.
// It returns an error if the field is not defined in the schema.
func (m *ChannelMonitorMutation) ResetField(name string) error {
switch name {
case channelmonitor.FieldCreatedAt:
m.ResetCreatedAt()
return nil
case channelmonitor.FieldUpdatedAt:
m.ResetUpdatedAt()
return nil
case channelmonitor.FieldName:
m.ResetName()
return nil
case channelmonitor.FieldProvider:
m.ResetProvider()
return nil
case channelmonitor.FieldEndpoint:
m.ResetEndpoint()
return nil
case channelmonitor.FieldAPIKeyEncrypted:
m.ResetAPIKeyEncrypted()
return nil
case channelmonitor.FieldPrimaryModel:
m.ResetPrimaryModel()
return nil
case channelmonitor.FieldExtraModels:
m.ResetExtraModels()
return nil
case channelmonitor.FieldGroupName:
m.ResetGroupName()
return nil
case channelmonitor.FieldEnabled:
m.ResetEnabled()
return nil
case channelmonitor.FieldIntervalSeconds:
m.ResetIntervalSeconds()
return nil
case channelmonitor.FieldLastCheckedAt:
m.ResetLastCheckedAt()
return nil
case channelmonitor.FieldCreatedBy:
m.ResetCreatedBy()
return nil
}
return fmt.Errorf("unknown ChannelMonitor field %s", name)
}
// AddedEdges returns all edge names that were set/added in this mutation.
func (m *ChannelMonitorMutation) AddedEdges() []string {
edges := make([]string, 0, 1)
if m.history != nil {
edges = append(edges, channelmonitor.EdgeHistory)
}
return edges
}
// AddedIDs returns all IDs (to other nodes) that were added for the given edge
// name in this mutation.
func (m *ChannelMonitorMutation) AddedIDs(name string) []ent.Value {
switch name {
case channelmonitor.EdgeHistory:
ids := make([]ent.Value, 0, len(m.history))
for id := range m.history {
ids = append(ids, id)
}
return ids
}
return nil
}
// RemovedEdges returns all edge names that were removed in this mutation.
func (m *ChannelMonitorMutation) RemovedEdges() []string {
edges := make([]string, 0, 1)
if m.removedhistory != nil {
edges = append(edges, channelmonitor.EdgeHistory)
}
return edges
}
// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with
// the given name in this mutation.
func (m *ChannelMonitorMutation) RemovedIDs(name string) []ent.Value {
switch name {
case channelmonitor.EdgeHistory:
ids := make([]ent.Value, 0, len(m.removedhistory))
for id := range m.removedhistory {
ids = append(ids, id)
}
return ids
}
return nil
}
// ClearedEdges returns all edge names that were cleared in this mutation.
func (m *ChannelMonitorMutation) ClearedEdges() []string {
edges := make([]string, 0, 1)
if m.clearedhistory {
edges = append(edges, channelmonitor.EdgeHistory)
}
return edges
}
// EdgeCleared returns a boolean which indicates if the edge with the given name
// was cleared in this mutation.
func (m *ChannelMonitorMutation) EdgeCleared(name string) bool {
switch name {
case channelmonitor.EdgeHistory:
return m.clearedhistory
}
return false
}
// ClearEdge clears the value of the edge with the given name. It returns an error
// if that edge is not defined in the schema.
func (m *ChannelMonitorMutation) ClearEdge(name string) error {
switch name {
}
return fmt.Errorf("unknown ChannelMonitor unique edge %s", name)
}
// ResetEdge resets all changes to the edge with the given name in this mutation.
// It returns an error if the edge is not defined in the schema.
func (m *ChannelMonitorMutation) ResetEdge(name string) error {
switch name {
case channelmonitor.EdgeHistory:
m.ResetHistory()
return nil
}
return fmt.Errorf("unknown ChannelMonitor edge %s", name)
}
// ChannelMonitorHistoryMutation represents an operation that mutates the ChannelMonitorHistory nodes in the graph.
type ChannelMonitorHistoryMutation struct {
config
op Op
typ string
id *int64
model *string
status *channelmonitorhistory.Status
latency_ms *int
addlatency_ms *int
ping_latency_ms *int
addping_latency_ms *int
message *string
checked_at *time.Time
clearedFields map[string]struct{}
monitor *int64
clearedmonitor bool
done bool
oldValue func(context.Context) (*ChannelMonitorHistory, error)
predicates []predicate.ChannelMonitorHistory
}
var _ ent.Mutation = (*ChannelMonitorHistoryMutation)(nil)
// channelmonitorhistoryOption allows management of the mutation configuration using functional options.
type channelmonitorhistoryOption func(*ChannelMonitorHistoryMutation)
// newChannelMonitorHistoryMutation creates new mutation for the ChannelMonitorHistory entity.
func newChannelMonitorHistoryMutation(c config, op Op, opts ...channelmonitorhistoryOption) *ChannelMonitorHistoryMutation {
m := &ChannelMonitorHistoryMutation{
config: c,
op: op,
typ: TypeChannelMonitorHistory,
clearedFields: make(map[string]struct{}),
}
for _, opt := range opts {
opt(m)
}
return m
}
// withChannelMonitorHistoryID sets the ID field of the mutation.
func withChannelMonitorHistoryID(id int64) channelmonitorhistoryOption {
return func(m *ChannelMonitorHistoryMutation) {
var (
err error
once sync.Once
value *ChannelMonitorHistory
)
m.oldValue = func(ctx context.Context) (*ChannelMonitorHistory, error) {
once.Do(func() {
if m.done {
err = errors.New("querying old values post mutation is not allowed")
} else {
value, err = m.Client().ChannelMonitorHistory.Get(ctx, id)
}
})
return value, err
}
m.id = &id
}
}
// withChannelMonitorHistory sets the old ChannelMonitorHistory of the mutation.
func withChannelMonitorHistory(node *ChannelMonitorHistory) channelmonitorhistoryOption {
return func(m *ChannelMonitorHistoryMutation) {
m.oldValue = func(context.Context) (*ChannelMonitorHistory, error) {
return node, nil
}
m.id = &node.ID
}
}
// Client returns a new `ent.Client` from the mutation. If the mutation was
// executed in a transaction (ent.Tx), a transactional client is returned.
func (m ChannelMonitorHistoryMutation) Client() *Client {
client := &Client{config: m.config}
client.init()
return client
}
// Tx returns an `ent.Tx` for mutations that were executed in transactions;
// it returns an error otherwise.
func (m ChannelMonitorHistoryMutation) Tx() (*Tx, error) {
if _, ok := m.driver.(*txDriver); !ok {
return nil, errors.New("ent: mutation is not running in a transaction")
}
tx := &Tx{config: m.config}
tx.init()
return tx, nil
}
// ID returns the ID value in the mutation. Note that the ID is only available
// if it was provided to the builder or after it was returned from the database.
func (m *ChannelMonitorHistoryMutation) ID() (id int64, exists bool) {
if m.id == nil {
return
}
return *m.id, true
}
// IDs queries the database and returns the entity ids that match the mutation's predicate.
// That means, if the mutation is applied within a transaction with an isolation level such
// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated
// or updated by the mutation.
func (m *ChannelMonitorHistoryMutation) IDs(ctx context.Context) ([]int64, error) {
switch {
case m.op.Is(OpUpdateOne | OpDeleteOne):
id, exists := m.ID()
if exists {
return []int64{id}, nil
}
fallthrough
case m.op.Is(OpUpdate | OpDelete):
return m.Client().ChannelMonitorHistory.Query().Where(m.predicates...).IDs(ctx)
default:
return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op)
}
}
// SetMonitorID sets the "monitor_id" field.
func (m *ChannelMonitorHistoryMutation) SetMonitorID(i int64) {
m.monitor = &i
}
// MonitorID returns the value of the "monitor_id" field in the mutation.
func (m *ChannelMonitorHistoryMutation) MonitorID() (r int64, exists bool) {
v := m.monitor
if v == nil {
return
}
return *v, true
}
// OldMonitorID returns the old "monitor_id" field's value of the ChannelMonitorHistory entity.
// If the ChannelMonitorHistory object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorHistoryMutation) OldMonitorID(ctx context.Context) (v int64, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldMonitorID is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldMonitorID requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldMonitorID: %w", err)
}
return oldValue.MonitorID, nil
}
// ResetMonitorID resets all changes to the "monitor_id" field.
func (m *ChannelMonitorHistoryMutation) ResetMonitorID() {
m.monitor = nil
}
// SetModel sets the "model" field.
func (m *ChannelMonitorHistoryMutation) SetModel(s string) {
m.model = &s
}
// Model returns the value of the "model" field in the mutation.
func (m *ChannelMonitorHistoryMutation) Model() (r string, exists bool) {
v := m.model
if v == nil {
return
}
return *v, true
}
// OldModel returns the old "model" field's value of the ChannelMonitorHistory entity.
// If the ChannelMonitorHistory object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorHistoryMutation) OldModel(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldModel is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldModel requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldModel: %w", err)
}
return oldValue.Model, nil
}
// ResetModel resets all changes to the "model" field.
func (m *ChannelMonitorHistoryMutation) ResetModel() {
m.model = nil
}
// SetStatus sets the "status" field.
func (m *ChannelMonitorHistoryMutation) SetStatus(c channelmonitorhistory.Status) {
m.status = &c
}
// Status returns the value of the "status" field in the mutation.
func (m *ChannelMonitorHistoryMutation) Status() (r channelmonitorhistory.Status, exists bool) {
v := m.status
if v == nil {
return
}
return *v, true
}
// OldStatus returns the old "status" field's value of the ChannelMonitorHistory entity.
// If the ChannelMonitorHistory object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorHistoryMutation) OldStatus(ctx context.Context) (v channelmonitorhistory.Status, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldStatus is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldStatus requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldStatus: %w", err)
}
return oldValue.Status, nil
}
// ResetStatus resets all changes to the "status" field.
func (m *ChannelMonitorHistoryMutation) ResetStatus() {
m.status = nil
}
// SetLatencyMs sets the "latency_ms" field.
func (m *ChannelMonitorHistoryMutation) SetLatencyMs(i int) {
m.latency_ms = &i
m.addlatency_ms = nil
}
// LatencyMs returns the value of the "latency_ms" field in the mutation.
func (m *ChannelMonitorHistoryMutation) LatencyMs() (r int, exists bool) {
v := m.latency_ms
if v == nil {
return
}
return *v, true
}
// OldLatencyMs returns the old "latency_ms" field's value of the ChannelMonitorHistory entity.
// If the ChannelMonitorHistory object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorHistoryMutation) OldLatencyMs(ctx context.Context) (v *int, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldLatencyMs is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldLatencyMs requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldLatencyMs: %w", err)
}
return oldValue.LatencyMs, nil
}
// AddLatencyMs adds i to the "latency_ms" field.
func (m *ChannelMonitorHistoryMutation) AddLatencyMs(i int) {
if m.addlatency_ms != nil {
*m.addlatency_ms += i
} else {
m.addlatency_ms = &i
}
}
// AddedLatencyMs returns the value that was added to the "latency_ms" field in this mutation.
func (m *ChannelMonitorHistoryMutation) AddedLatencyMs() (r int, exists bool) {
v := m.addlatency_ms
if v == nil {
return
}
return *v, true
}
// ClearLatencyMs clears the value of the "latency_ms" field.
func (m *ChannelMonitorHistoryMutation) ClearLatencyMs() {
m.latency_ms = nil
m.addlatency_ms = nil
m.clearedFields[channelmonitorhistory.FieldLatencyMs] = struct{}{}
}
// LatencyMsCleared returns if the "latency_ms" field was cleared in this mutation.
func (m *ChannelMonitorHistoryMutation) LatencyMsCleared() bool {
_, ok := m.clearedFields[channelmonitorhistory.FieldLatencyMs]
return ok
}
// ResetLatencyMs resets all changes to the "latency_ms" field.
func (m *ChannelMonitorHistoryMutation) ResetLatencyMs() {
m.latency_ms = nil
m.addlatency_ms = nil
delete(m.clearedFields, channelmonitorhistory.FieldLatencyMs)
}
// SetPingLatencyMs sets the "ping_latency_ms" field.
func (m *ChannelMonitorHistoryMutation) SetPingLatencyMs(i int) {
m.ping_latency_ms = &i
m.addping_latency_ms = nil
}
// PingLatencyMs returns the value of the "ping_latency_ms" field in the mutation.
func (m *ChannelMonitorHistoryMutation) PingLatencyMs() (r int, exists bool) {
v := m.ping_latency_ms
if v == nil {
return
}
return *v, true
}
// OldPingLatencyMs returns the old "ping_latency_ms" field's value of the ChannelMonitorHistory entity.
// If the ChannelMonitorHistory object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorHistoryMutation) OldPingLatencyMs(ctx context.Context) (v *int, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldPingLatencyMs is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldPingLatencyMs requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldPingLatencyMs: %w", err)
}
return oldValue.PingLatencyMs, nil
}
// AddPingLatencyMs adds i to the "ping_latency_ms" field.
func (m *ChannelMonitorHistoryMutation) AddPingLatencyMs(i int) {
if m.addping_latency_ms != nil {
*m.addping_latency_ms += i
} else {
m.addping_latency_ms = &i
}
}
// AddedPingLatencyMs returns the value that was added to the "ping_latency_ms" field in this mutation.
func (m *ChannelMonitorHistoryMutation) AddedPingLatencyMs() (r int, exists bool) {
v := m.addping_latency_ms
if v == nil {
return
}
return *v, true
}
// ClearPingLatencyMs clears the value of the "ping_latency_ms" field.
func (m *ChannelMonitorHistoryMutation) ClearPingLatencyMs() {
m.ping_latency_ms = nil
m.addping_latency_ms = nil
m.clearedFields[channelmonitorhistory.FieldPingLatencyMs] = struct{}{}
}
// PingLatencyMsCleared returns if the "ping_latency_ms" field was cleared in this mutation.
func (m *ChannelMonitorHistoryMutation) PingLatencyMsCleared() bool {
_, ok := m.clearedFields[channelmonitorhistory.FieldPingLatencyMs]
return ok
}
// ResetPingLatencyMs resets all changes to the "ping_latency_ms" field.
func (m *ChannelMonitorHistoryMutation) ResetPingLatencyMs() {
m.ping_latency_ms = nil
m.addping_latency_ms = nil
delete(m.clearedFields, channelmonitorhistory.FieldPingLatencyMs)
}
// SetMessage sets the "message" field.
func (m *ChannelMonitorHistoryMutation) SetMessage(s string) {
m.message = &s
}
// Message returns the value of the "message" field in the mutation.
func (m *ChannelMonitorHistoryMutation) Message() (r string, exists bool) {
v := m.message
if v == nil {
return
}
return *v, true
}
// OldMessage returns the old "message" field's value of the ChannelMonitorHistory entity.
// If the ChannelMonitorHistory object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorHistoryMutation) OldMessage(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldMessage is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldMessage requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldMessage: %w", err)
}
return oldValue.Message, nil
}
// ClearMessage clears the value of the "message" field.
func (m *ChannelMonitorHistoryMutation) ClearMessage() {
m.message = nil
m.clearedFields[channelmonitorhistory.FieldMessage] = struct{}{}
}
// MessageCleared returns if the "message" field was cleared in this mutation.
func (m *ChannelMonitorHistoryMutation) MessageCleared() bool {
_, ok := m.clearedFields[channelmonitorhistory.FieldMessage]
return ok
}
// ResetMessage resets all changes to the "message" field.
func (m *ChannelMonitorHistoryMutation) ResetMessage() {
m.message = nil
delete(m.clearedFields, channelmonitorhistory.FieldMessage)
}
// SetCheckedAt sets the "checked_at" field.
func (m *ChannelMonitorHistoryMutation) SetCheckedAt(t time.Time) {
m.checked_at = &t
}
// CheckedAt returns the value of the "checked_at" field in the mutation.
func (m *ChannelMonitorHistoryMutation) CheckedAt() (r time.Time, exists bool) {
v := m.checked_at
if v == nil {
return
}
return *v, true
}
// OldCheckedAt returns the old "checked_at" field's value of the ChannelMonitorHistory entity.
// If the ChannelMonitorHistory object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ChannelMonitorHistoryMutation) OldCheckedAt(ctx context.Context) (v time.Time, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldCheckedAt is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldCheckedAt requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldCheckedAt: %w", err)
}
return oldValue.CheckedAt, nil
}
// ResetCheckedAt resets all changes to the "checked_at" field.
func (m *ChannelMonitorHistoryMutation) ResetCheckedAt() {
m.checked_at = nil
}
// ClearMonitor clears the "monitor" edge to the ChannelMonitor entity.
func (m *ChannelMonitorHistoryMutation) ClearMonitor() {
m.clearedmonitor = true
m.clearedFields[channelmonitorhistory.FieldMonitorID] = struct{}{}
}
// MonitorCleared reports if the "monitor" edge to the ChannelMonitor entity was cleared.
func (m *ChannelMonitorHistoryMutation) MonitorCleared() bool {
return m.clearedmonitor
}
// MonitorIDs returns the "monitor" edge IDs in the mutation.
// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use
// MonitorID instead. It exists only for internal usage by the builders.
func (m *ChannelMonitorHistoryMutation) MonitorIDs() (ids []int64) {
if id := m.monitor; id != nil {
ids = append(ids, *id)
}
return
}
// ResetMonitor resets all changes to the "monitor" edge.
func (m *ChannelMonitorHistoryMutation) ResetMonitor() {
m.monitor = nil
m.clearedmonitor = false
}
// Where appends a list predicates to the ChannelMonitorHistoryMutation builder.
func (m *ChannelMonitorHistoryMutation) Where(ps ...predicate.ChannelMonitorHistory) {
m.predicates = append(m.predicates, ps...)
}
// WhereP appends storage-level predicates to the ChannelMonitorHistoryMutation builder. Using this method,
// users can use type-assertion to append predicates that do not depend on any generated package.
func (m *ChannelMonitorHistoryMutation) WhereP(ps ...func(*sql.Selector)) {
p := make([]predicate.ChannelMonitorHistory, len(ps))
for i := range ps {
p[i] = ps[i]
}
m.Where(p...)
}
// Op returns the operation name.
func (m *ChannelMonitorHistoryMutation) Op() Op {
return m.op
}
// SetOp allows setting the mutation operation.
func (m *ChannelMonitorHistoryMutation) SetOp(op Op) {
m.op = op
}
// Type returns the node type of this mutation (ChannelMonitorHistory).
func (m *ChannelMonitorHistoryMutation) Type() string {
return m.typ
}
// Fields returns all fields that were changed during this mutation. Note that in
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *ChannelMonitorHistoryMutation) Fields() []string {
fields := make([]string, 0, 7)
if m.monitor != nil {
fields = append(fields, channelmonitorhistory.FieldMonitorID)
}
if m.model != nil {
fields = append(fields, channelmonitorhistory.FieldModel)
}
if m.status != nil {
fields = append(fields, channelmonitorhistory.FieldStatus)
}
if m.latency_ms != nil {
fields = append(fields, channelmonitorhistory.FieldLatencyMs)
}
if m.ping_latency_ms != nil {
fields = append(fields, channelmonitorhistory.FieldPingLatencyMs)
}
if m.message != nil {
fields = append(fields, channelmonitorhistory.FieldMessage)
}
if m.checked_at != nil {
fields = append(fields, channelmonitorhistory.FieldCheckedAt)
}
return fields
}
// Field returns the value of a field with the given name. The second boolean
// return value indicates that this field was not set, or was not defined in the
// schema.
func (m *ChannelMonitorHistoryMutation) Field(name string) (ent.Value, bool) {
switch name {
case channelmonitorhistory.FieldMonitorID:
return m.MonitorID()
case channelmonitorhistory.FieldModel:
return m.Model()
case channelmonitorhistory.FieldStatus:
return m.Status()
case channelmonitorhistory.FieldLatencyMs:
return m.LatencyMs()
case channelmonitorhistory.FieldPingLatencyMs:
return m.PingLatencyMs()
case channelmonitorhistory.FieldMessage:
return m.Message()
case channelmonitorhistory.FieldCheckedAt:
return m.CheckedAt()
}
return nil, false
}
// OldField returns the old value of the field from the database. An error is
// returned if the mutation operation is not UpdateOne, or the query to the
// database failed.
func (m *ChannelMonitorHistoryMutation) OldField(ctx context.Context, name string) (ent.Value, error) {
switch name {
case channelmonitorhistory.FieldMonitorID:
return m.OldMonitorID(ctx)
case channelmonitorhistory.FieldModel:
return m.OldModel(ctx)
case channelmonitorhistory.FieldStatus:
return m.OldStatus(ctx)
case channelmonitorhistory.FieldLatencyMs:
return m.OldLatencyMs(ctx)
case channelmonitorhistory.FieldPingLatencyMs:
return m.OldPingLatencyMs(ctx)
case channelmonitorhistory.FieldMessage:
return m.OldMessage(ctx)
case channelmonitorhistory.FieldCheckedAt:
return m.OldCheckedAt(ctx)
}
return nil, fmt.Errorf("unknown ChannelMonitorHistory field %s", name)
}
// SetField sets the value of a field with the given name. It returns an error if
// the field is not defined in the schema, or if the type mismatched the field
// type.
func (m *ChannelMonitorHistoryMutation) SetField(name string, value ent.Value) error {
switch name {
case channelmonitorhistory.FieldMonitorID:
v, ok := value.(int64)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetMonitorID(v)
return nil
case channelmonitorhistory.FieldModel:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetModel(v)
return nil
case channelmonitorhistory.FieldStatus:
v, ok := value.(channelmonitorhistory.Status)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetStatus(v)
return nil
case channelmonitorhistory.FieldLatencyMs:
v, ok := value.(int)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetLatencyMs(v)
return nil
case channelmonitorhistory.FieldPingLatencyMs:
v, ok := value.(int)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetPingLatencyMs(v)
return nil
case channelmonitorhistory.FieldMessage:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetMessage(v)
return nil
case channelmonitorhistory.FieldCheckedAt:
v, ok := value.(time.Time)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetCheckedAt(v)
return nil
}
return fmt.Errorf("unknown ChannelMonitorHistory field %s", name)
}
// AddedFields returns all numeric fields that were incremented/decremented during
// this mutation.
func (m *ChannelMonitorHistoryMutation) AddedFields() []string {
var fields []string
if m.addlatency_ms != nil {
fields = append(fields, channelmonitorhistory.FieldLatencyMs)
}
if m.addping_latency_ms != nil {
fields = append(fields, channelmonitorhistory.FieldPingLatencyMs)
}
return fields
}
// AddedField returns the numeric value that was incremented/decremented on a field
// with the given name. The second boolean return value indicates that this field
// was not set, or was not defined in the schema.
func (m *ChannelMonitorHistoryMutation) AddedField(name string) (ent.Value, bool) {
switch name {
case channelmonitorhistory.FieldLatencyMs:
return m.AddedLatencyMs()
case channelmonitorhistory.FieldPingLatencyMs:
return m.AddedPingLatencyMs()
}
return nil, false
}
// AddField adds the value to the field with the given name. It returns an error if
// the field is not defined in the schema, or if the type mismatched the field
// type.
func (m *ChannelMonitorHistoryMutation) AddField(name string, value ent.Value) error {
switch name {
case channelmonitorhistory.FieldLatencyMs:
v, ok := value.(int)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.AddLatencyMs(v)
return nil
case channelmonitorhistory.FieldPingLatencyMs:
v, ok := value.(int)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.AddPingLatencyMs(v)
return nil
}
return fmt.Errorf("unknown ChannelMonitorHistory numeric field %s", name)
}
// ClearedFields returns all nullable fields that were cleared during this
// mutation.
func (m *ChannelMonitorHistoryMutation) ClearedFields() []string {
var fields []string
if m.FieldCleared(channelmonitorhistory.FieldLatencyMs) {
fields = append(fields, channelmonitorhistory.FieldLatencyMs)
}
if m.FieldCleared(channelmonitorhistory.FieldPingLatencyMs) {
fields = append(fields, channelmonitorhistory.FieldPingLatencyMs)
}
if m.FieldCleared(channelmonitorhistory.FieldMessage) {
fields = append(fields, channelmonitorhistory.FieldMessage)
}
return fields
}
// FieldCleared returns a boolean indicating if a field with the given name was
// cleared in this mutation.
func (m *ChannelMonitorHistoryMutation) FieldCleared(name string) bool {
_, ok := m.clearedFields[name]
return ok
}
// ClearField clears the value of the field with the given name. It returns an
// error if the field is not defined in the schema.
func (m *ChannelMonitorHistoryMutation) ClearField(name string) error {
switch name {
case channelmonitorhistory.FieldLatencyMs:
m.ClearLatencyMs()
return nil
case channelmonitorhistory.FieldPingLatencyMs:
m.ClearPingLatencyMs()
return nil
case channelmonitorhistory.FieldMessage:
m.ClearMessage()
return nil
}
return fmt.Errorf("unknown ChannelMonitorHistory nullable field %s", name)
}
// ResetField resets all changes in the mutation for the field with the given name.
// It returns an error if the field is not defined in the schema.
func (m *ChannelMonitorHistoryMutation) ResetField(name string) error {
switch name {
case channelmonitorhistory.FieldMonitorID:
m.ResetMonitorID()
return nil
case channelmonitorhistory.FieldModel:
m.ResetModel()
return nil
case channelmonitorhistory.FieldStatus:
m.ResetStatus()
return nil
case channelmonitorhistory.FieldLatencyMs:
m.ResetLatencyMs()
return nil
case channelmonitorhistory.FieldPingLatencyMs:
m.ResetPingLatencyMs()
return nil
case channelmonitorhistory.FieldMessage:
m.ResetMessage()
return nil
case channelmonitorhistory.FieldCheckedAt:
m.ResetCheckedAt()
return nil
}
return fmt.Errorf("unknown ChannelMonitorHistory field %s", name)
}
// AddedEdges returns all edge names that were set/added in this mutation.
func (m *ChannelMonitorHistoryMutation) AddedEdges() []string {
edges := make([]string, 0, 1)
if m.monitor != nil {
edges = append(edges, channelmonitorhistory.EdgeMonitor)
}
return edges
}
// AddedIDs returns all IDs (to other nodes) that were added for the given edge
// name in this mutation.
func (m *ChannelMonitorHistoryMutation) AddedIDs(name string) []ent.Value {
switch name {
case channelmonitorhistory.EdgeMonitor:
if id := m.monitor; id != nil {
return []ent.Value{*id}
}
}
return nil
}
// RemovedEdges returns all edge names that were removed in this mutation.
func (m *ChannelMonitorHistoryMutation) RemovedEdges() []string {
edges := make([]string, 0, 1)
return edges
}
// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with
// the given name in this mutation.
func (m *ChannelMonitorHistoryMutation) RemovedIDs(name string) []ent.Value {
return nil
}
// ClearedEdges returns all edge names that were cleared in this mutation.
func (m *ChannelMonitorHistoryMutation) ClearedEdges() []string {
edges := make([]string, 0, 1)
if m.clearedmonitor {
edges = append(edges, channelmonitorhistory.EdgeMonitor)
}
return edges
}
// EdgeCleared returns a boolean which indicates if the edge with the given name
// was cleared in this mutation.
func (m *ChannelMonitorHistoryMutation) EdgeCleared(name string) bool {
switch name {
case channelmonitorhistory.EdgeMonitor:
return m.clearedmonitor
}
return false
}
// ClearEdge clears the value of the edge with the given name. It returns an error
// if that edge is not defined in the schema.
func (m *ChannelMonitorHistoryMutation) ClearEdge(name string) error {
switch name {
case channelmonitorhistory.EdgeMonitor:
m.ClearMonitor()
return nil
}
return fmt.Errorf("unknown ChannelMonitorHistory unique edge %s", name)
}
// ResetEdge resets all changes to the edge with the given name in this mutation.
// It returns an error if the edge is not defined in the schema.
func (m *ChannelMonitorHistoryMutation) ResetEdge(name string) error {
switch name {
case channelmonitorhistory.EdgeMonitor:
m.ResetMonitor()
return nil
}
return fmt.Errorf("unknown ChannelMonitorHistory edge %s", name)
}
// ErrorPassthroughRuleMutation represents an operation that mutates the ErrorPassthroughRule nodes in the graph. // ErrorPassthroughRuleMutation represents an operation that mutates the ErrorPassthroughRule nodes in the graph.
type ErrorPassthroughRuleMutation struct { type ErrorPassthroughRuleMutation struct {
config config
...@@ -27,6 +27,12 @@ type AuthIdentity func(*sql.Selector) ...@@ -27,6 +27,12 @@ type AuthIdentity func(*sql.Selector)
// AuthIdentityChannel is the predicate function for authidentitychannel builders. // AuthIdentityChannel is the predicate function for authidentitychannel builders.
type AuthIdentityChannel func(*sql.Selector) type AuthIdentityChannel func(*sql.Selector)
// ChannelMonitor is the predicate function for channelmonitor builders.
type ChannelMonitor func(*sql.Selector)
// ChannelMonitorHistory is the predicate function for channelmonitorhistory builders.
type ChannelMonitorHistory func(*sql.Selector)
// ErrorPassthroughRule is the predicate function for errorpassthroughrule builders. // ErrorPassthroughRule is the predicate function for errorpassthroughrule builders.
type ErrorPassthroughRule func(*sql.Selector) type ErrorPassthroughRule func(*sql.Selector)
......
...@@ -12,6 +12,8 @@ import ( ...@@ -12,6 +12,8 @@ import (
"github.com/Wei-Shaw/sub2api/ent/apikey" "github.com/Wei-Shaw/sub2api/ent/apikey"
"github.com/Wei-Shaw/sub2api/ent/authidentity" "github.com/Wei-Shaw/sub2api/ent/authidentity"
"github.com/Wei-Shaw/sub2api/ent/authidentitychannel" "github.com/Wei-Shaw/sub2api/ent/authidentitychannel"
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
"github.com/Wei-Shaw/sub2api/ent/errorpassthroughrule" "github.com/Wei-Shaw/sub2api/ent/errorpassthroughrule"
"github.com/Wei-Shaw/sub2api/ent/group" "github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/idempotencyrecord" "github.com/Wei-Shaw/sub2api/ent/idempotencyrecord"
...@@ -427,6 +429,127 @@ func init() { ...@@ -427,6 +429,127 @@ func init() {
authidentitychannelDescMetadata := authidentitychannelFields[6].Descriptor() authidentitychannelDescMetadata := authidentitychannelFields[6].Descriptor()
// authidentitychannel.DefaultMetadata holds the default value on creation for the metadata field. // authidentitychannel.DefaultMetadata holds the default value on creation for the metadata field.
authidentitychannel.DefaultMetadata = authidentitychannelDescMetadata.Default.(func() map[string]interface{}) authidentitychannel.DefaultMetadata = authidentitychannelDescMetadata.Default.(func() map[string]interface{})
channelmonitorMixin := schema.ChannelMonitor{}.Mixin()
channelmonitorMixinFields0 := channelmonitorMixin[0].Fields()
_ = channelmonitorMixinFields0
channelmonitorFields := schema.ChannelMonitor{}.Fields()
_ = channelmonitorFields
// channelmonitorDescCreatedAt is the schema descriptor for created_at field.
channelmonitorDescCreatedAt := channelmonitorMixinFields0[0].Descriptor()
// channelmonitor.DefaultCreatedAt holds the default value on creation for the created_at field.
channelmonitor.DefaultCreatedAt = channelmonitorDescCreatedAt.Default.(func() time.Time)
// channelmonitorDescUpdatedAt is the schema descriptor for updated_at field.
channelmonitorDescUpdatedAt := channelmonitorMixinFields0[1].Descriptor()
// channelmonitor.DefaultUpdatedAt holds the default value on creation for the updated_at field.
channelmonitor.DefaultUpdatedAt = channelmonitorDescUpdatedAt.Default.(func() time.Time)
// channelmonitor.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
channelmonitor.UpdateDefaultUpdatedAt = channelmonitorDescUpdatedAt.UpdateDefault.(func() time.Time)
// channelmonitorDescName is the schema descriptor for name field.
channelmonitorDescName := channelmonitorFields[0].Descriptor()
// channelmonitor.NameValidator is a validator for the "name" field. It is called by the builders before save.
channelmonitor.NameValidator = func() func(string) error {
validators := channelmonitorDescName.Validators
fns := [...]func(string) error{
validators[0].(func(string) error),
validators[1].(func(string) error),
}
return func(name string) error {
for _, fn := range fns {
if err := fn(name); err != nil {
return err
}
}
return nil
}
}()
// channelmonitorDescEndpoint is the schema descriptor for endpoint field.
channelmonitorDescEndpoint := channelmonitorFields[2].Descriptor()
// channelmonitor.EndpointValidator is a validator for the "endpoint" field. It is called by the builders before save.
channelmonitor.EndpointValidator = func() func(string) error {
validators := channelmonitorDescEndpoint.Validators
fns := [...]func(string) error{
validators[0].(func(string) error),
validators[1].(func(string) error),
}
return func(endpoint string) error {
for _, fn := range fns {
if err := fn(endpoint); err != nil {
return err
}
}
return nil
}
}()
// channelmonitorDescAPIKeyEncrypted is the schema descriptor for api_key_encrypted field.
channelmonitorDescAPIKeyEncrypted := channelmonitorFields[3].Descriptor()
// channelmonitor.APIKeyEncryptedValidator is a validator for the "api_key_encrypted" field. It is called by the builders before save.
channelmonitor.APIKeyEncryptedValidator = channelmonitorDescAPIKeyEncrypted.Validators[0].(func(string) error)
// channelmonitorDescPrimaryModel is the schema descriptor for primary_model field.
channelmonitorDescPrimaryModel := channelmonitorFields[4].Descriptor()
// channelmonitor.PrimaryModelValidator is a validator for the "primary_model" field. It is called by the builders before save.
channelmonitor.PrimaryModelValidator = func() func(string) error {
validators := channelmonitorDescPrimaryModel.Validators
fns := [...]func(string) error{
validators[0].(func(string) error),
validators[1].(func(string) error),
}
return func(primary_model string) error {
for _, fn := range fns {
if err := fn(primary_model); err != nil {
return err
}
}
return nil
}
}()
// channelmonitorDescExtraModels is the schema descriptor for extra_models field.
channelmonitorDescExtraModels := channelmonitorFields[5].Descriptor()
// channelmonitor.DefaultExtraModels holds the default value on creation for the extra_models field.
channelmonitor.DefaultExtraModels = channelmonitorDescExtraModels.Default.([]string)
// channelmonitorDescGroupName is the schema descriptor for group_name field.
channelmonitorDescGroupName := channelmonitorFields[6].Descriptor()
// channelmonitor.DefaultGroupName holds the default value on creation for the group_name field.
channelmonitor.DefaultGroupName = channelmonitorDescGroupName.Default.(string)
// channelmonitor.GroupNameValidator is a validator for the "group_name" field. It is called by the builders before save.
channelmonitor.GroupNameValidator = channelmonitorDescGroupName.Validators[0].(func(string) error)
// channelmonitorDescEnabled is the schema descriptor for enabled field.
channelmonitorDescEnabled := channelmonitorFields[7].Descriptor()
// channelmonitor.DefaultEnabled holds the default value on creation for the enabled field.
channelmonitor.DefaultEnabled = channelmonitorDescEnabled.Default.(bool)
// channelmonitorDescIntervalSeconds is the schema descriptor for interval_seconds field.
channelmonitorDescIntervalSeconds := channelmonitorFields[8].Descriptor()
// channelmonitor.IntervalSecondsValidator is a validator for the "interval_seconds" field. It is called by the builders before save.
channelmonitor.IntervalSecondsValidator = channelmonitorDescIntervalSeconds.Validators[0].(func(int) error)
channelmonitorhistoryFields := schema.ChannelMonitorHistory{}.Fields()
_ = channelmonitorhistoryFields
// channelmonitorhistoryDescModel is the schema descriptor for model field.
channelmonitorhistoryDescModel := channelmonitorhistoryFields[1].Descriptor()
// channelmonitorhistory.ModelValidator is a validator for the "model" field. It is called by the builders before save.
channelmonitorhistory.ModelValidator = func() func(string) error {
validators := channelmonitorhistoryDescModel.Validators
fns := [...]func(string) error{
validators[0].(func(string) error),
validators[1].(func(string) error),
}
return func(model string) error {
for _, fn := range fns {
if err := fn(model); err != nil {
return err
}
}
return nil
}
}()
// channelmonitorhistoryDescMessage is the schema descriptor for message field.
channelmonitorhistoryDescMessage := channelmonitorhistoryFields[5].Descriptor()
// channelmonitorhistory.DefaultMessage holds the default value on creation for the message field.
channelmonitorhistory.DefaultMessage = channelmonitorhistoryDescMessage.Default.(string)
// channelmonitorhistory.MessageValidator is a validator for the "message" field. It is called by the builders before save.
channelmonitorhistory.MessageValidator = channelmonitorhistoryDescMessage.Validators[0].(func(string) error)
// channelmonitorhistoryDescCheckedAt is the schema descriptor for checked_at field.
channelmonitorhistoryDescCheckedAt := channelmonitorhistoryFields[6].Descriptor()
// channelmonitorhistory.DefaultCheckedAt holds the default value on creation for the checked_at field.
channelmonitorhistory.DefaultCheckedAt = channelmonitorhistoryDescCheckedAt.Default.(func() time.Time)
errorpassthroughruleMixin := schema.ErrorPassthroughRule{}.Mixin() errorpassthroughruleMixin := schema.ErrorPassthroughRule{}.Mixin()
errorpassthroughruleMixinFields0 := errorpassthroughruleMixin[0].Fields() errorpassthroughruleMixinFields0 := errorpassthroughruleMixin[0].Fields()
_ = errorpassthroughruleMixinFields0 _ = errorpassthroughruleMixinFields0
......
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"
)
// ChannelMonitor holds the schema definition for the ChannelMonitor entity.
// 渠道监控配置:定期对指定 provider/endpoint/api_key 下的模型做心跳测试。
type ChannelMonitor struct {
ent.Schema
}
func (ChannelMonitor) Annotations() []schema.Annotation {
return []schema.Annotation{
entsql.Annotation{Table: "channel_monitors"},
}
}
func (ChannelMonitor) Mixin() []ent.Mixin {
return []ent.Mixin{
mixins.TimeMixin{},
}
}
func (ChannelMonitor) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty().
MaxLen(100),
field.Enum("provider").
Values("openai", "anthropic", "gemini"),
field.String("endpoint").
NotEmpty().
MaxLen(500).
Comment("Provider base origin, e.g. https://api.openai.com"),
field.String("api_key_encrypted").
NotEmpty().
Sensitive().
Comment("AES-256-GCM encrypted API key"),
field.String("primary_model").
NotEmpty().
MaxLen(200),
field.JSON("extra_models", []string{}).
Default([]string{}).
Comment("Additional model names to test alongside primary_model"),
field.String("group_name").
Optional().
Default("").
MaxLen(100),
field.Bool("enabled").
Default(true),
field.Int("interval_seconds").
Range(15, 3600),
field.Time("last_checked_at").
Optional().
Nillable(),
field.Int64("created_by"),
}
}
func (ChannelMonitor) Edges() []ent.Edge {
return []ent.Edge{
edge.To("history", ChannelMonitorHistory.Type).
Annotations(entsql.OnDelete(entsql.Cascade)),
}
}
func (ChannelMonitor) Indexes() []ent.Index {
return []ent.Index{
index.Fields("enabled", "last_checked_at"),
index.Fields("provider"),
index.Fields("group_name"),
}
}
package schema
import (
"time"
"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"
)
// ChannelMonitorHistory holds the schema definition for the ChannelMonitorHistory entity.
// 渠道监控历史:每次检测每个模型一行记录,由调度器写入,定期清理 30 天前的旧数据。
type ChannelMonitorHistory struct {
ent.Schema
}
func (ChannelMonitorHistory) Annotations() []schema.Annotation {
return []schema.Annotation{
entsql.Annotation{Table: "channel_monitor_histories"},
}
}
func (ChannelMonitorHistory) Fields() []ent.Field {
return []ent.Field{
field.Int64("monitor_id"),
field.String("model").
NotEmpty().
MaxLen(200),
field.Enum("status").
Values("operational", "degraded", "failed", "error"),
field.Int("latency_ms").
Optional().
Nillable(),
field.Int("ping_latency_ms").
Optional().
Nillable(),
field.String("message").
Optional().
Default("").
MaxLen(500),
field.Time("checked_at").
Default(time.Now),
}
}
func (ChannelMonitorHistory) Edges() []ent.Edge {
return []ent.Edge{
edge.From("monitor", ChannelMonitor.Type).
Ref("history").
Field("monitor_id").
Unique().
Required(),
}
}
func (ChannelMonitorHistory) Indexes() []ent.Index {
return []ent.Index{
index.Fields("monitor_id", "model", "checked_at"),
index.Fields("checked_at"),
}
}
...@@ -28,6 +28,10 @@ type Tx struct { ...@@ -28,6 +28,10 @@ type Tx struct {
AuthIdentity *AuthIdentityClient AuthIdentity *AuthIdentityClient
// AuthIdentityChannel is the client for interacting with the AuthIdentityChannel builders. // AuthIdentityChannel is the client for interacting with the AuthIdentityChannel builders.
AuthIdentityChannel *AuthIdentityChannelClient AuthIdentityChannel *AuthIdentityChannelClient
// ChannelMonitor is the client for interacting with the ChannelMonitor builders.
ChannelMonitor *ChannelMonitorClient
// ChannelMonitorHistory is the client for interacting with the ChannelMonitorHistory builders.
ChannelMonitorHistory *ChannelMonitorHistoryClient
// ErrorPassthroughRule is the client for interacting with the ErrorPassthroughRule builders. // ErrorPassthroughRule is the client for interacting with the ErrorPassthroughRule builders.
ErrorPassthroughRule *ErrorPassthroughRuleClient ErrorPassthroughRule *ErrorPassthroughRuleClient
// Group is the client for interacting with the Group builders. // Group is the client for interacting with the Group builders.
...@@ -212,6 +216,8 @@ func (tx *Tx) init() { ...@@ -212,6 +216,8 @@ func (tx *Tx) init() {
tx.AnnouncementRead = NewAnnouncementReadClient(tx.config) tx.AnnouncementRead = NewAnnouncementReadClient(tx.config)
tx.AuthIdentity = NewAuthIdentityClient(tx.config) tx.AuthIdentity = NewAuthIdentityClient(tx.config)
tx.AuthIdentityChannel = NewAuthIdentityChannelClient(tx.config) tx.AuthIdentityChannel = NewAuthIdentityChannelClient(tx.config)
tx.ChannelMonitor = NewChannelMonitorClient(tx.config)
tx.ChannelMonitorHistory = NewChannelMonitorHistoryClient(tx.config)
tx.ErrorPassthroughRule = NewErrorPassthroughRuleClient(tx.config) tx.ErrorPassthroughRule = NewErrorPassthroughRuleClient(tx.config)
tx.Group = NewGroupClient(tx.config) tx.Group = NewGroupClient(tx.config)
tx.IdempotencyRecord = NewIdempotencyRecordClient(tx.config) tx.IdempotencyRecord = NewIdempotencyRecordClient(tx.config)
......
...@@ -162,6 +162,8 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 ...@@ -162,6 +162,8 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
...@@ -181,6 +183,8 @@ github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4= ...@@ -181,6 +183,8 @@ github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI= github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00= github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
...@@ -216,6 +220,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk ...@@ -216,6 +220,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
...@@ -249,6 +255,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= ...@@ -249,6 +255,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
...@@ -278,6 +286,8 @@ github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEv ...@@ -278,6 +286,8 @@ github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEv
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
...@@ -310,6 +320,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= ...@@ -310,6 +320,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
......
package admin
import (
"strconv"
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
const (
// monitorMaxPageSize 列表分页上限。
monitorMaxPageSize = 100
// monitorAPIKeyMaskPrefix 脱敏时保留的明文前缀长度。
monitorAPIKeyMaskPrefix = 4
// monitorAPIKeyMaskSuffix 脱敏后追加的占位字符串。
monitorAPIKeyMaskSuffix = "***"
)
// ChannelMonitorHandler 渠道监控管理后台 handler。
type ChannelMonitorHandler struct {
monitorService *service.ChannelMonitorService
}
// NewChannelMonitorHandler 创建 handler。
func NewChannelMonitorHandler(monitorService *service.ChannelMonitorService) *ChannelMonitorHandler {
return &ChannelMonitorHandler{monitorService: monitorService}
}
// --- Request / Response ---
type channelMonitorCreateRequest struct {
Name string `json:"name" binding:"required,max=100"`
Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini"`
Endpoint string `json:"endpoint" binding:"required,max=500"`
APIKey string `json:"api_key" binding:"required,max=2000"`
PrimaryModel string `json:"primary_model" binding:"required,max=200"`
ExtraModels []string `json:"extra_models"`
GroupName string `json:"group_name" binding:"max=100"`
Enabled *bool `json:"enabled"`
IntervalSeconds int `json:"interval_seconds" binding:"required,min=15,max=3600"`
}
type channelMonitorUpdateRequest struct {
Name *string `json:"name" binding:"omitempty,max=100"`
Provider *string `json:"provider" binding:"omitempty,oneof=openai anthropic gemini"`
Endpoint *string `json:"endpoint" binding:"omitempty,max=500"`
APIKey *string `json:"api_key" binding:"omitempty,max=2000"`
PrimaryModel *string `json:"primary_model" binding:"omitempty,max=200"`
ExtraModels *[]string `json:"extra_models"`
GroupName *string `json:"group_name" binding:"omitempty,max=100"`
Enabled *bool `json:"enabled"`
IntervalSeconds *int `json:"interval_seconds" binding:"omitempty,min=15,max=3600"`
}
type channelMonitorResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
Endpoint string `json:"endpoint"`
APIKeyMasked string `json:"api_key_masked"`
APIKeyDecryptFailed bool `json:"api_key_decrypt_failed"`
PrimaryModel string `json:"primary_model"`
ExtraModels []string `json:"extra_models"`
GroupName string `json:"group_name"`
Enabled bool `json:"enabled"`
IntervalSeconds int `json:"interval_seconds"`
LastCheckedAt *string `json:"last_checked_at"`
CreatedBy int64 `json:"created_by"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
PrimaryStatus string `json:"primary_status"`
PrimaryLatencyMs *int `json:"primary_latency_ms"`
Availability7d float64 `json:"availability_7d"`
ExtraModelsStatus []dto.ChannelMonitorExtraModelStatus `json:"extra_models_status"`
}
type channelMonitorCheckResultResponse struct {
Model string `json:"model"`
Status string `json:"status"`
LatencyMs *int `json:"latency_ms"`
PingLatencyMs *int `json:"ping_latency_ms"`
Message string `json:"message"`
CheckedAt string `json:"checked_at"`
}
type channelMonitorHistoryItemResponse struct {
ID int64 `json:"id"`
Model string `json:"model"`
Status string `json:"status"`
LatencyMs *int `json:"latency_ms"`
PingLatencyMs *int `json:"ping_latency_ms"`
Message string `json:"message"`
CheckedAt string `json:"checked_at"`
}
// maskAPIKey 对 API Key 明文做脱敏:前 4 字符 + "***",长度 ≤ 4 时只显示 "***"。
func maskAPIKey(plain string) string {
if len(plain) <= monitorAPIKeyMaskPrefix {
return monitorAPIKeyMaskSuffix
}
return plain[:monitorAPIKeyMaskPrefix] + monitorAPIKeyMaskSuffix
}
func channelMonitorToResponse(m *service.ChannelMonitor) *channelMonitorResponse {
if m == nil {
return nil
}
extras := m.ExtraModels
if extras == nil {
extras = []string{}
}
resp := &channelMonitorResponse{
ID: m.ID,
Name: m.Name,
Provider: m.Provider,
Endpoint: m.Endpoint,
APIKeyMasked: maskAPIKey(m.APIKey),
APIKeyDecryptFailed: m.APIKeyDecryptFailed,
PrimaryModel: m.PrimaryModel,
ExtraModels: extras,
GroupName: m.GroupName,
Enabled: m.Enabled,
IntervalSeconds: m.IntervalSeconds,
CreatedBy: m.CreatedBy,
CreatedAt: m.CreatedAt.UTC().Format(time.RFC3339),
UpdatedAt: m.UpdatedAt.UTC().Format(time.RFC3339),
// PrimaryStatus / PrimaryLatencyMs / Availability7d 由 List handler 在批量聚合后填充。
}
if m.LastCheckedAt != nil {
s := m.LastCheckedAt.UTC().Format(time.RFC3339)
resp.LastCheckedAt = &s
}
return resp
}
func checkResultToResponse(r *service.CheckResult) channelMonitorCheckResultResponse {
return channelMonitorCheckResultResponse{
Model: r.Model,
Status: r.Status,
LatencyMs: r.LatencyMs,
PingLatencyMs: r.PingLatencyMs,
Message: r.Message,
CheckedAt: r.CheckedAt.UTC().Format(time.RFC3339),
}
}
func historyEntryToResponse(e *service.ChannelMonitorHistoryEntry) channelMonitorHistoryItemResponse {
return channelMonitorHistoryItemResponse{
ID: e.ID,
Model: e.Model,
Status: e.Status,
LatencyMs: e.LatencyMs,
PingLatencyMs: e.PingLatencyMs,
Message: e.Message,
CheckedAt: e.CheckedAt.UTC().Format(time.RFC3339),
}
}
// ParseChannelMonitorID 提取并校验路径参数 :id(admin 与 user handler 共享)。
// 校验失败时已写入 4xx 响应,调用方只需 return。
func ParseChannelMonitorID(c *gin.Context) (int64, bool) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil || id <= 0 {
response.ErrorFrom(c, infraerrors.BadRequest("INVALID_MONITOR_ID", "invalid monitor id"))
return 0, false
}
return id, true
}
// parseListEnabled 解析 enabled query 参数:true/false 转为 *bool,空或非法则返回 nil。
func parseListEnabled(raw string) *bool {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "true", "1", "yes":
v := true
return &v
case "false", "0", "no":
v := false
return &v
default:
return nil
}
}
// --- Handlers ---
// List GET /api/v1/admin/channel-monitors
func (h *ChannelMonitorHandler) List(c *gin.Context) {
page, pageSize := response.ParsePagination(c)
if pageSize > monitorMaxPageSize {
pageSize = monitorMaxPageSize
}
params := service.ChannelMonitorListParams{
Page: page,
PageSize: pageSize,
Provider: strings.TrimSpace(c.Query("provider")),
Enabled: parseListEnabled(c.Query("enabled")),
Search: strings.TrimSpace(c.Query("search")),
}
items, total, err := h.monitorService.List(c.Request.Context(), params)
if err != nil {
response.ErrorFrom(c, err)
return
}
summaries := h.batchSummaryFor(c, items)
out := make([]*channelMonitorResponse, 0, len(items))
for _, m := range items {
out = append(out, buildListItemResponse(m, summaries[m.ID]))
}
response.Paginated(c, out, total, page, pageSize)
}
// batchSummaryFor 批量聚合 latest + 7d 可用率,避免每行 2 次 SQL(消除 N+1)。
func (h *ChannelMonitorHandler) batchSummaryFor(c *gin.Context, items []*service.ChannelMonitor) map[int64]service.MonitorStatusSummary {
ids := make([]int64, 0, len(items))
primaryByID := make(map[int64]string, len(items))
extrasByID := make(map[int64][]string, len(items))
for _, m := range items {
ids = append(ids, m.ID)
primaryByID[m.ID] = m.PrimaryModel
extrasByID[m.ID] = m.ExtraModels
}
return h.monitorService.BatchMonitorStatusSummary(c.Request.Context(), ids, primaryByID, extrasByID)
}
// buildListItemResponse 把 monitor + summary 装成 admin list 的响应行。
func buildListItemResponse(m *service.ChannelMonitor, summary service.MonitorStatusSummary) *channelMonitorResponse {
resp := channelMonitorToResponse(m)
resp.PrimaryStatus = summary.PrimaryStatus
resp.PrimaryLatencyMs = summary.PrimaryLatencyMs
resp.Availability7d = summary.Availability7d
resp.ExtraModelsStatus = make([]dto.ChannelMonitorExtraModelStatus, 0, len(summary.ExtraModels))
for _, e := range summary.ExtraModels {
resp.ExtraModelsStatus = append(resp.ExtraModelsStatus, dto.ChannelMonitorExtraModelStatus{
Model: e.Model,
Status: e.Status,
LatencyMs: e.LatencyMs,
})
}
return resp
}
// Get GET /api/v1/admin/channel-monitors/:id
func (h *ChannelMonitorHandler) Get(c *gin.Context) {
id, ok := ParseChannelMonitorID(c)
if !ok {
return
}
m, err := h.monitorService.Get(c.Request.Context(), id)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, channelMonitorToResponse(m))
}
// Create POST /api/v1/admin/channel-monitors
func (h *ChannelMonitorHandler) Create(c *gin.Context) {
var req channelMonitorCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorFrom(c, infraerrors.BadRequest("VALIDATION_ERROR", err.Error()))
return
}
subject, _ := middleware2.GetAuthSubjectFromContext(c)
enabled := true
if req.Enabled != nil {
enabled = *req.Enabled
}
m, err := h.monitorService.Create(c.Request.Context(), service.ChannelMonitorCreateParams{
Name: req.Name,
Provider: req.Provider,
Endpoint: req.Endpoint,
APIKey: req.APIKey,
PrimaryModel: req.PrimaryModel,
ExtraModels: req.ExtraModels,
GroupName: req.GroupName,
Enabled: enabled,
IntervalSeconds: req.IntervalSeconds,
CreatedBy: subject.UserID,
})
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Created(c, channelMonitorToResponse(m))
}
// Update PUT /api/v1/admin/channel-monitors/:id
func (h *ChannelMonitorHandler) Update(c *gin.Context) {
id, ok := ParseChannelMonitorID(c)
if !ok {
return
}
var req channelMonitorUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorFrom(c, infraerrors.BadRequest("VALIDATION_ERROR", err.Error()))
return
}
m, err := h.monitorService.Update(c.Request.Context(), id, service.ChannelMonitorUpdateParams{
Name: req.Name,
Provider: req.Provider,
Endpoint: req.Endpoint,
APIKey: req.APIKey,
PrimaryModel: req.PrimaryModel,
ExtraModels: req.ExtraModels,
GroupName: req.GroupName,
Enabled: req.Enabled,
IntervalSeconds: req.IntervalSeconds,
})
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, channelMonitorToResponse(m))
}
// Delete DELETE /api/v1/admin/channel-monitors/:id
func (h *ChannelMonitorHandler) Delete(c *gin.Context) {
id, ok := ParseChannelMonitorID(c)
if !ok {
return
}
if err := h.monitorService.Delete(c.Request.Context(), id); err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, nil)
}
// Run POST /api/v1/admin/channel-monitors/:id/run
func (h *ChannelMonitorHandler) Run(c *gin.Context) {
id, ok := ParseChannelMonitorID(c)
if !ok {
return
}
results, err := h.monitorService.RunCheck(c.Request.Context(), id)
if err != nil {
response.ErrorFrom(c, err)
return
}
out := make([]channelMonitorCheckResultResponse, 0, len(results))
for _, r := range results {
out = append(out, checkResultToResponse(r))
}
response.Success(c, gin.H{"results": out})
}
// History GET /api/v1/admin/channel-monitors/:id/history
func (h *ChannelMonitorHandler) History(c *gin.Context) {
id, ok := ParseChannelMonitorID(c)
if !ok {
return
}
limit := parseHistoryLimit(c.Query("limit"))
model := strings.TrimSpace(c.Query("model"))
entries, err := h.monitorService.ListHistory(c.Request.Context(), id, model, limit)
if err != nil {
response.ErrorFrom(c, err)
return
}
out := make([]channelMonitorHistoryItemResponse, 0, len(entries))
for _, e := range entries {
out = append(out, historyEntryToResponse(e))
}
response.Success(c, gin.H{"items": out})
}
// parseHistoryLimit 解析 history 接口的 limit query。
// 使用 service 包的统一上下限常量,避免在 handler 重复定义同名魔法值。
func parseHistoryLimit(raw string) int {
if strings.TrimSpace(raw) == "" {
return service.MonitorHistoryDefaultLimit
}
v, err := strconv.Atoi(raw)
if err != nil || v <= 0 {
return service.MonitorHistoryDefaultLimit
}
if v > service.MonitorHistoryMaxLimit {
return service.MonitorHistoryMaxLimit
}
return v
}
package handler
import (
"github.com/Wei-Shaw/sub2api/internal/handler/admin"
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
// ChannelMonitorUserHandler 渠道监控用户只读 handler。
type ChannelMonitorUserHandler struct {
monitorService *service.ChannelMonitorService
}
// NewChannelMonitorUserHandler 创建 handler。
func NewChannelMonitorUserHandler(monitorService *service.ChannelMonitorService) *ChannelMonitorUserHandler {
return &ChannelMonitorUserHandler{monitorService: monitorService}
}
// --- Response ---
type channelMonitorUserListItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
GroupName string `json:"group_name"`
PrimaryModel string `json:"primary_model"`
PrimaryStatus string `json:"primary_status"`
PrimaryLatencyMs *int `json:"primary_latency_ms"`
Availability7d float64 `json:"availability_7d"`
ExtraModels []dto.ChannelMonitorExtraModelStatus `json:"extra_models"`
}
type channelMonitorUserDetailResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
GroupName string `json:"group_name"`
Models []channelMonitorUserModelStat `json:"models"`
}
type channelMonitorUserModelStat struct {
Model string `json:"model"`
LatestStatus string `json:"latest_status"`
LatestLatencyMs *int `json:"latest_latency_ms"`
Availability7d float64 `json:"availability_7d"`
Availability15d float64 `json:"availability_15d"`
Availability30d float64 `json:"availability_30d"`
AvgLatency7dMs *int `json:"avg_latency_7d_ms"`
}
func userMonitorViewToItem(v *service.UserMonitorView) channelMonitorUserListItem {
extras := make([]dto.ChannelMonitorExtraModelStatus, 0, len(v.ExtraModels))
for _, e := range v.ExtraModels {
extras = append(extras, dto.ChannelMonitorExtraModelStatus{
Model: e.Model,
Status: e.Status,
LatencyMs: e.LatencyMs,
})
}
return channelMonitorUserListItem{
ID: v.ID,
Name: v.Name,
Provider: v.Provider,
GroupName: v.GroupName,
PrimaryModel: v.PrimaryModel,
PrimaryStatus: v.PrimaryStatus,
PrimaryLatencyMs: v.PrimaryLatencyMs,
Availability7d: v.Availability7d,
ExtraModels: extras,
}
}
func userMonitorDetailToResponse(d *service.UserMonitorDetail) *channelMonitorUserDetailResponse {
models := make([]channelMonitorUserModelStat, 0, len(d.Models))
for _, m := range d.Models {
models = append(models, channelMonitorUserModelStat{
Model: m.Model,
LatestStatus: m.LatestStatus,
LatestLatencyMs: m.LatestLatencyMs,
Availability7d: m.Availability7d,
Availability15d: m.Availability15d,
Availability30d: m.Availability30d,
AvgLatency7dMs: m.AvgLatency7dMs,
})
}
return &channelMonitorUserDetailResponse{
ID: d.ID,
Name: d.Name,
Provider: d.Provider,
GroupName: d.GroupName,
Models: models,
}
}
// --- Handlers ---
// List GET /api/v1/channel-monitors
func (h *ChannelMonitorUserHandler) List(c *gin.Context) {
views, err := h.monitorService.ListUserView(c.Request.Context())
if err != nil {
response.ErrorFrom(c, err)
return
}
items := make([]channelMonitorUserListItem, 0, len(views))
for _, v := range views {
items = append(items, userMonitorViewToItem(v))
}
response.Success(c, gin.H{"items": items})
}
// GetStatus GET /api/v1/channel-monitors/:id/status
func (h *ChannelMonitorUserHandler) GetStatus(c *gin.Context) {
// 复用 admin.ParseChannelMonitorID 保持错误码与日志一致。
id, ok := admin.ParseChannelMonitorID(c)
if !ok {
return
}
detail, err := h.monitorService.GetUserDetail(c.Request.Context(), id)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, userMonitorDetailToResponse(detail))
}
package dto
// ChannelMonitorExtraModelStatus 渠道监控附加模型最近一次状态。
// 同时被 admin handler(List 响应)与 user handler(List 响应)复用,
// 字段必须保持一致以保证前端拿到统一结构。
type ChannelMonitorExtraModelStatus struct {
Model string `json:"model"`
Status string `json:"status"`
LatencyMs *int `json:"latency_ms"`
}
...@@ -31,6 +31,7 @@ type AdminHandlers struct { ...@@ -31,6 +31,7 @@ type AdminHandlers struct {
APIKey *admin.AdminAPIKeyHandler APIKey *admin.AdminAPIKeyHandler
ScheduledTest *admin.ScheduledTestHandler ScheduledTest *admin.ScheduledTestHandler
Channel *admin.ChannelHandler Channel *admin.ChannelHandler
ChannelMonitor *admin.ChannelMonitorHandler
Payment *admin.PaymentHandler Payment *admin.PaymentHandler
} }
...@@ -43,6 +44,7 @@ type Handlers struct { ...@@ -43,6 +44,7 @@ type Handlers struct {
Redeem *RedeemHandler Redeem *RedeemHandler
Subscription *SubscriptionHandler Subscription *SubscriptionHandler
Announcement *AnnouncementHandler Announcement *AnnouncementHandler
ChannelMonitor *ChannelMonitorUserHandler
Admin *AdminHandlers Admin *AdminHandlers
Gateway *GatewayHandler Gateway *GatewayHandler
OpenAIGateway *OpenAIGatewayHandler OpenAIGateway *OpenAIGatewayHandler
......
...@@ -34,6 +34,7 @@ func ProvideAdminHandlers( ...@@ -34,6 +34,7 @@ func ProvideAdminHandlers(
apiKeyHandler *admin.AdminAPIKeyHandler, apiKeyHandler *admin.AdminAPIKeyHandler,
scheduledTestHandler *admin.ScheduledTestHandler, scheduledTestHandler *admin.ScheduledTestHandler,
channelHandler *admin.ChannelHandler, channelHandler *admin.ChannelHandler,
channelMonitorHandler *admin.ChannelMonitorHandler,
paymentHandler *admin.PaymentHandler, paymentHandler *admin.PaymentHandler,
) *AdminHandlers { ) *AdminHandlers {
return &AdminHandlers{ return &AdminHandlers{
...@@ -62,6 +63,7 @@ func ProvideAdminHandlers( ...@@ -62,6 +63,7 @@ func ProvideAdminHandlers(
APIKey: apiKeyHandler, APIKey: apiKeyHandler,
ScheduledTest: scheduledTestHandler, ScheduledTest: scheduledTestHandler,
Channel: channelHandler, Channel: channelHandler,
ChannelMonitor: channelMonitorHandler,
Payment: paymentHandler, Payment: paymentHandler,
} }
} }
...@@ -85,6 +87,7 @@ func ProvideHandlers( ...@@ -85,6 +87,7 @@ func ProvideHandlers(
redeemHandler *RedeemHandler, redeemHandler *RedeemHandler,
subscriptionHandler *SubscriptionHandler, subscriptionHandler *SubscriptionHandler,
announcementHandler *AnnouncementHandler, announcementHandler *AnnouncementHandler,
channelMonitorUserHandler *ChannelMonitorUserHandler,
adminHandlers *AdminHandlers, adminHandlers *AdminHandlers,
gatewayHandler *GatewayHandler, gatewayHandler *GatewayHandler,
openaiGatewayHandler *OpenAIGatewayHandler, openaiGatewayHandler *OpenAIGatewayHandler,
...@@ -103,6 +106,7 @@ func ProvideHandlers( ...@@ -103,6 +106,7 @@ func ProvideHandlers(
Redeem: redeemHandler, Redeem: redeemHandler,
Subscription: subscriptionHandler, Subscription: subscriptionHandler,
Announcement: announcementHandler, Announcement: announcementHandler,
ChannelMonitor: channelMonitorUserHandler,
Admin: adminHandlers, Admin: adminHandlers,
Gateway: gatewayHandler, Gateway: gatewayHandler,
OpenAIGateway: openaiGatewayHandler, OpenAIGateway: openaiGatewayHandler,
...@@ -123,6 +127,7 @@ var ProviderSet = wire.NewSet( ...@@ -123,6 +127,7 @@ var ProviderSet = wire.NewSet(
NewRedeemHandler, NewRedeemHandler,
NewSubscriptionHandler, NewSubscriptionHandler,
NewAnnouncementHandler, NewAnnouncementHandler,
NewChannelMonitorUserHandler,
NewGatewayHandler, NewGatewayHandler,
NewOpenAIGatewayHandler, NewOpenAIGatewayHandler,
NewTotpHandler, NewTotpHandler,
...@@ -156,6 +161,7 @@ var ProviderSet = wire.NewSet( ...@@ -156,6 +161,7 @@ var ProviderSet = wire.NewSet(
admin.NewAdminAPIKeyHandler, admin.NewAdminAPIKeyHandler,
admin.NewScheduledTestHandler, admin.NewScheduledTestHandler,
admin.NewChannelHandler, admin.NewChannelHandler,
admin.NewChannelMonitorHandler,
admin.NewPaymentHandler, admin.NewPaymentHandler,
// AdminHandlers and Handlers constructors // AdminHandlers and Handlers constructors
......
package repository
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
dbent "github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/lib/pq"
)
// channelMonitorRepository 实现 service.ChannelMonitorRepository。
//
// 选型说明:
// - CRUD 走 ent,复用项目的事务上下文支持
// - 聚合查询(latest per model / availability)走原生 SQL,避免 ent 在 GROUP BY 上
// 的样板代码,并保证索引能被命中
type channelMonitorRepository struct {
client *dbent.Client
db *sql.DB
}
// NewChannelMonitorRepository 创建仓储实例。
func NewChannelMonitorRepository(client *dbent.Client, db *sql.DB) service.ChannelMonitorRepository {
return &channelMonitorRepository{client: client, db: db}
}
// ---------- CRUD ----------
func (r *channelMonitorRepository) Create(ctx context.Context, m *service.ChannelMonitor) error {
client := clientFromContext(ctx, r.client)
builder := client.ChannelMonitor.Create().
SetName(m.Name).
SetProvider(channelmonitor.Provider(m.Provider)).
SetEndpoint(m.Endpoint).
SetAPIKeyEncrypted(m.APIKey). // 调用方传入的已是密文
SetPrimaryModel(m.PrimaryModel).
SetExtraModels(emptySliceIfNil(m.ExtraModels)).
SetGroupName(m.GroupName).
SetEnabled(m.Enabled).
SetIntervalSeconds(m.IntervalSeconds).
SetCreatedBy(m.CreatedBy)
created, err := builder.Save(ctx)
if err != nil {
return translatePersistenceError(err, service.ErrChannelMonitorNotFound, nil)
}
m.ID = created.ID
m.CreatedAt = created.CreatedAt
m.UpdatedAt = created.UpdatedAt
return nil
}
func (r *channelMonitorRepository) GetByID(ctx context.Context, id int64) (*service.ChannelMonitor, error) {
row, err := r.client.ChannelMonitor.Query().
Where(channelmonitor.IDEQ(id)).
Only(ctx)
if err != nil {
return nil, translatePersistenceError(err, service.ErrChannelMonitorNotFound, nil)
}
return entToServiceMonitor(row), nil
}
func (r *channelMonitorRepository) Update(ctx context.Context, m *service.ChannelMonitor) error {
client := clientFromContext(ctx, r.client)
updater := client.ChannelMonitor.UpdateOneID(m.ID).
SetName(m.Name).
SetProvider(channelmonitor.Provider(m.Provider)).
SetEndpoint(m.Endpoint).
SetAPIKeyEncrypted(m.APIKey).
SetPrimaryModel(m.PrimaryModel).
SetExtraModels(emptySliceIfNil(m.ExtraModels)).
SetGroupName(m.GroupName).
SetEnabled(m.Enabled).
SetIntervalSeconds(m.IntervalSeconds)
updated, err := updater.Save(ctx)
if err != nil {
return translatePersistenceError(err, service.ErrChannelMonitorNotFound, nil)
}
m.UpdatedAt = updated.UpdatedAt
return nil
}
func (r *channelMonitorRepository) Delete(ctx context.Context, id int64) error {
client := clientFromContext(ctx, r.client)
if err := client.ChannelMonitor.DeleteOneID(id).Exec(ctx); err != nil {
return translatePersistenceError(err, service.ErrChannelMonitorNotFound, nil)
}
return nil
}
func (r *channelMonitorRepository) List(ctx context.Context, params service.ChannelMonitorListParams) ([]*service.ChannelMonitor, int64, error) {
q := r.client.ChannelMonitor.Query()
if params.Provider != "" {
q = q.Where(channelmonitor.ProviderEQ(channelmonitor.Provider(params.Provider)))
}
if params.Enabled != nil {
q = q.Where(channelmonitor.EnabledEQ(*params.Enabled))
}
if s := strings.TrimSpace(params.Search); s != "" {
q = q.Where(channelmonitor.Or(
channelmonitor.NameContainsFold(s),
channelmonitor.GroupNameContainsFold(s),
channelmonitor.PrimaryModelContainsFold(s),
))
}
total, err := q.Count(ctx)
if err != nil {
return nil, 0, fmt.Errorf("count monitors: %w", err)
}
pageSize := params.PageSize
if pageSize <= 0 {
pageSize = 20
}
page := params.Page
if page <= 0 {
page = 1
}
rows, err := q.
Order(dbent.Desc(channelmonitor.FieldID)).
Offset((page - 1) * pageSize).
Limit(pageSize).
All(ctx)
if err != nil {
return nil, 0, fmt.Errorf("list monitors: %w", err)
}
out := make([]*service.ChannelMonitor, 0, len(rows))
for _, row := range rows {
out = append(out, entToServiceMonitor(row))
}
return out, int64(total), nil
}
// ---------- 调度器辅助 ----------
func (r *channelMonitorRepository) ListEnabled(ctx context.Context) ([]*service.ChannelMonitor, error) {
rows, err := r.client.ChannelMonitor.Query().
Where(channelmonitor.EnabledEQ(true)).
All(ctx)
if err != nil {
return nil, fmt.Errorf("list enabled monitors: %w", err)
}
out := make([]*service.ChannelMonitor, 0, len(rows))
for _, row := range rows {
out = append(out, entToServiceMonitor(row))
}
return out, nil
}
func (r *channelMonitorRepository) MarkChecked(ctx context.Context, id int64, checkedAt time.Time) error {
client := clientFromContext(ctx, r.client)
if err := client.ChannelMonitor.UpdateOneID(id).
SetLastCheckedAt(checkedAt).
Exec(ctx); err != nil {
return translatePersistenceError(err, service.ErrChannelMonitorNotFound, nil)
}
return nil
}
func (r *channelMonitorRepository) InsertHistoryBatch(ctx context.Context, rows []*service.ChannelMonitorHistoryRow) error {
if len(rows) == 0 {
return nil
}
client := clientFromContext(ctx, r.client)
bulk := make([]*dbent.ChannelMonitorHistoryCreate, 0, len(rows))
for _, row := range rows {
c := client.ChannelMonitorHistory.Create().
SetMonitorID(row.MonitorID).
SetModel(row.Model).
SetStatus(channelmonitorhistory.Status(row.Status)).
SetMessage(row.Message).
SetCheckedAt(row.CheckedAt)
if row.LatencyMs != nil {
c = c.SetLatencyMs(*row.LatencyMs)
}
if row.PingLatencyMs != nil {
c = c.SetPingLatencyMs(*row.PingLatencyMs)
}
bulk = append(bulk, c)
}
if _, err := client.ChannelMonitorHistory.CreateBulk(bulk...).Save(ctx); err != nil {
return fmt.Errorf("insert history bulk: %w", err)
}
return nil
}
func (r *channelMonitorRepository) DeleteHistoryBefore(ctx context.Context, before time.Time) (int64, error) {
client := clientFromContext(ctx, r.client)
n, err := client.ChannelMonitorHistory.Delete().
Where(channelmonitorhistory.CheckedAtLT(before)).
Exec(ctx)
if err != nil {
return 0, fmt.Errorf("delete history before: %w", err)
}
return int64(n), nil
}
// ListHistory 按 checked_at 倒序返回某个监控的最近 N 条历史记录。
// model 为空时不过滤;非空时只返回该模型的记录。
func (r *channelMonitorRepository) ListHistory(ctx context.Context, monitorID int64, model string, limit int) ([]*service.ChannelMonitorHistoryEntry, error) {
q := r.client.ChannelMonitorHistory.Query().
Where(channelmonitorhistory.MonitorIDEQ(monitorID))
if strings.TrimSpace(model) != "" {
q = q.Where(channelmonitorhistory.ModelEQ(model))
}
rows, err := q.
Order(dbent.Desc(channelmonitorhistory.FieldCheckedAt)).
Limit(limit).
All(ctx)
if err != nil {
return nil, fmt.Errorf("list history: %w", err)
}
out := make([]*service.ChannelMonitorHistoryEntry, 0, len(rows))
for _, row := range rows {
entry := &service.ChannelMonitorHistoryEntry{
ID: row.ID,
Model: row.Model,
Status: string(row.Status),
LatencyMs: row.LatencyMs,
PingLatencyMs: row.PingLatencyMs,
Message: row.Message,
CheckedAt: row.CheckedAt,
}
out = append(out, entry)
}
return out, nil
}
// ---------- 用户视图聚合(原生 SQL) ----------
// ListLatestPerModel 用 DISTINCT ON 取每个 (monitor_id, model) 的最近一条记录。
// 借助 (monitor_id, model, checked_at DESC) 索引可走 Index Scan。
func (r *channelMonitorRepository) ListLatestPerModel(ctx context.Context, monitorID int64) ([]*service.ChannelMonitorLatest, error) {
const q = `
SELECT DISTINCT ON (model)
model, status, latency_ms, checked_at
FROM channel_monitor_histories
WHERE monitor_id = $1
ORDER BY model, checked_at DESC
`
rows, err := r.db.QueryContext(ctx, q, monitorID)
if err != nil {
return nil, fmt.Errorf("query latest per model: %w", err)
}
defer func() { _ = rows.Close() }()
out := make([]*service.ChannelMonitorLatest, 0)
for rows.Next() {
l := &service.ChannelMonitorLatest{}
var latency sql.NullInt64
if err := rows.Scan(&l.Model, &l.Status, &latency, &l.CheckedAt); err != nil {
return nil, fmt.Errorf("scan latest row: %w", err)
}
if latency.Valid {
v := int(latency.Int64)
l.LatencyMs = &v
}
out = append(out, l)
}
return out, rows.Err()
}
// ComputeAvailability 计算指定窗口内每个模型的可用率与平均延迟。
// "可用" = status IN (operational, degraded)。
func (r *channelMonitorRepository) ComputeAvailability(ctx context.Context, monitorID int64, windowDays int) ([]*service.ChannelMonitorAvailability, error) {
if windowDays <= 0 {
windowDays = 7
}
const q = `
SELECT
model,
COUNT(*) AS total_checks,
COUNT(*) FILTER (WHERE status IN ('operational','degraded')) AS ok_checks,
AVG(latency_ms) FILTER (WHERE latency_ms IS NOT NULL) AS avg_latency_ms
FROM channel_monitor_histories
WHERE monitor_id = $1
AND checked_at >= $2
GROUP BY model
`
from := time.Now().AddDate(0, 0, -windowDays)
rows, err := r.db.QueryContext(ctx, q, monitorID, from)
if err != nil {
return nil, fmt.Errorf("query availability: %w", err)
}
defer func() { _ = rows.Close() }()
out := make([]*service.ChannelMonitorAvailability, 0)
for rows.Next() {
row, err := scanAvailabilityRow(rows, windowDays)
if err != nil {
return nil, err
}
out = append(out, row)
}
return out, rows.Err()
}
// scanAvailabilityRow 把单行 (model, total, ok, avg_latency) 扫描为 ChannelMonitorAvailability。
// 仅服务于 ComputeAvailability(4 列);批量版本因为多一列 monitor_id 直接 inline 调 finalizeAvailabilityRow。
func scanAvailabilityRow(rows interface{ Scan(...any) error }, windowDays int) (*service.ChannelMonitorAvailability, error) {
row := &service.ChannelMonitorAvailability{WindowDays: windowDays}
var avgLatency sql.NullFloat64
if err := rows.Scan(&row.Model, &row.TotalChecks, &row.OperationalChecks, &avgLatency); err != nil {
return nil, fmt.Errorf("scan availability row: %w", err)
}
finalizeAvailabilityRow(row, avgLatency)
return row, nil
}
// finalizeAvailabilityRow 根据 OperationalChecks/TotalChecks 算出可用率,
// 并把 sql.NullFloat64 的平均延迟解包为 *int。两处复用避免维护漂移。
func finalizeAvailabilityRow(row *service.ChannelMonitorAvailability, avgLatency sql.NullFloat64) {
if row.TotalChecks > 0 {
row.AvailabilityPct = float64(row.OperationalChecks) * 100.0 / float64(row.TotalChecks)
}
if avgLatency.Valid {
v := int(avgLatency.Float64)
row.AvgLatencyMs = &v
}
}
// ListLatestForMonitorIDs 一次性查询多个监控的"每个 (monitor_id, model) 最近一条"记录。
// 利用 PG 的 DISTINCT ON 特性,借助 (monitor_id, model, checked_at DESC) 索引可走 Index Scan。
func (r *channelMonitorRepository) ListLatestForMonitorIDs(ctx context.Context, ids []int64) (map[int64][]*service.ChannelMonitorLatest, error) {
out := make(map[int64][]*service.ChannelMonitorLatest, len(ids))
if len(ids) == 0 {
return out, nil
}
const q = `
SELECT DISTINCT ON (monitor_id, model)
monitor_id, model, status, latency_ms, checked_at
FROM channel_monitor_histories
WHERE monitor_id = ANY($1)
ORDER BY monitor_id, model, checked_at DESC
`
rows, err := r.db.QueryContext(ctx, q, pq.Array(ids))
if err != nil {
return nil, fmt.Errorf("query latest batch: %w", err)
}
defer func() { _ = rows.Close() }()
for rows.Next() {
var monitorID int64
l := &service.ChannelMonitorLatest{}
var latency sql.NullInt64
if err := rows.Scan(&monitorID, &l.Model, &l.Status, &latency, &l.CheckedAt); err != nil {
return nil, fmt.Errorf("scan latest batch row: %w", err)
}
if latency.Valid {
v := int(latency.Int64)
l.LatencyMs = &v
}
out[monitorID] = append(out[monitorID], l)
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
// ComputeAvailabilityForMonitors 一次性计算多个监控在某个窗口内的每模型可用率与平均延迟。
func (r *channelMonitorRepository) ComputeAvailabilityForMonitors(ctx context.Context, ids []int64, windowDays int) (map[int64][]*service.ChannelMonitorAvailability, error) {
out := make(map[int64][]*service.ChannelMonitorAvailability, len(ids))
if len(ids) == 0 {
return out, nil
}
if windowDays <= 0 {
windowDays = 7
}
const q = `
SELECT
monitor_id,
model,
COUNT(*) AS total_checks,
COUNT(*) FILTER (WHERE status IN ('operational','degraded')) AS ok_checks,
AVG(latency_ms) FILTER (WHERE latency_ms IS NOT NULL) AS avg_latency_ms
FROM channel_monitor_histories
WHERE monitor_id = ANY($1)
AND checked_at >= $2
GROUP BY monitor_id, model
`
from := time.Now().AddDate(0, 0, -windowDays)
rows, err := r.db.QueryContext(ctx, q, pq.Array(ids), from)
if err != nil {
return nil, fmt.Errorf("query availability batch: %w", err)
}
defer func() { _ = rows.Close() }()
for rows.Next() {
var monitorID int64
row := &service.ChannelMonitorAvailability{WindowDays: windowDays}
var avgLatency sql.NullFloat64
if err := rows.Scan(&monitorID, &row.Model, &row.TotalChecks, &row.OperationalChecks, &avgLatency); err != nil {
return nil, fmt.Errorf("scan availability batch row: %w", err)
}
// 批量查询多了首列 monitor_id;其余字段的可用率/平均延迟换算与单 monitor 版本一致,
// 抽出 finalizeAvailabilityRow 复用,避免两处分别维护除法与 NullFloat 解包。
finalizeAvailabilityRow(row, avgLatency)
out[monitorID] = append(out[monitorID], row)
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
// ---------- helpers ----------
func entToServiceMonitor(row *dbent.ChannelMonitor) *service.ChannelMonitor {
if row == nil {
return nil
}
extras := row.ExtraModels
if extras == nil {
extras = []string{}
}
return &service.ChannelMonitor{
ID: row.ID,
Name: row.Name,
Provider: string(row.Provider),
Endpoint: row.Endpoint,
APIKey: row.APIKeyEncrypted, // 仍为密文,service 层负责解密
PrimaryModel: row.PrimaryModel,
ExtraModels: extras,
GroupName: row.GroupName,
Enabled: row.Enabled,
IntervalSeconds: row.IntervalSeconds,
LastCheckedAt: row.LastCheckedAt,
CreatedBy: row.CreatedBy,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}
func emptySliceIfNil(in []string) []string {
if in == nil {
return []string{}
}
return in
}
...@@ -89,6 +89,7 @@ var ProviderSet = wire.NewSet( ...@@ -89,6 +89,7 @@ var ProviderSet = wire.NewSet(
NewErrorPassthroughRepository, NewErrorPassthroughRepository,
NewTLSFingerprintProfileRepository, NewTLSFingerprintProfileRepository,
NewChannelRepository, NewChannelRepository,
NewChannelMonitorRepository,
// Cache implementations // Cache implementations
NewGatewayCache, NewGatewayCache,
......
...@@ -88,6 +88,9 @@ func RegisterAdminRoutes( ...@@ -88,6 +88,9 @@ func RegisterAdminRoutes(
// 渠道管理 // 渠道管理
registerChannelRoutes(admin, h) registerChannelRoutes(admin, h)
// 渠道监控
registerChannelMonitorRoutes(admin, h)
} }
} }
...@@ -564,3 +567,16 @@ func registerChannelRoutes(admin *gin.RouterGroup, h *handler.Handlers) { ...@@ -564,3 +567,16 @@ func registerChannelRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
channels.DELETE("/:id", h.Admin.Channel.Delete) channels.DELETE("/:id", h.Admin.Channel.Delete)
} }
} }
func registerChannelMonitorRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
monitors := admin.Group("/channel-monitors")
{
monitors.GET("", h.Admin.ChannelMonitor.List)
monitors.POST("", h.Admin.ChannelMonitor.Create)
monitors.GET("/:id", h.Admin.ChannelMonitor.Get)
monitors.PUT("/:id", h.Admin.ChannelMonitor.Update)
monitors.DELETE("/:id", h.Admin.ChannelMonitor.Delete)
monitors.POST("/:id/run", h.Admin.ChannelMonitor.Run)
monitors.GET("/:id/history", h.Admin.ChannelMonitor.History)
}
}
...@@ -103,5 +103,12 @@ func RegisterUserRoutes( ...@@ -103,5 +103,12 @@ func RegisterUserRoutes(
subscriptions.GET("/progress", h.Subscription.GetProgress) subscriptions.GET("/progress", h.Subscription.GetProgress)
subscriptions.GET("/summary", h.Subscription.GetSummary) subscriptions.GET("/summary", h.Subscription.GetSummary)
} }
// 渠道监控(用户只读)
monitors := authenticated.Group("/channel-monitors")
{
monitors.GET("", h.ChannelMonitor.List)
monitors.GET("/:id/status", h.ChannelMonitor.GetStatus)
}
} }
} }
package service
import (
"context"
"fmt"
"log/slog"
)
// 渠道监控聚合层:把 latest + availability 拼成 admin/user 视图所需的 summary / detail。
// 所有方法都遵守"失败仅日志,返回零值"的原则,避免 N+1 查询失败拖垮列表渲染。
// BatchMonitorStatusSummary 批量聚合多个监控的 latest + 7d 可用率(admin/user list 用,消除 N+1)。
// 失败时返回空 map,错误仅日志,不影响列表渲染。
//
// 参数:
// - ids: 要聚合的 monitor ID 列表
// - primaryByID: monitor ID -> primary model(用于读 7d 可用率与 latest 状态)
// - extrasByID: monitor ID -> extra models 列表(用于读 latest 状态填充 ExtraModels)
func (s *ChannelMonitorService) BatchMonitorStatusSummary(
ctx context.Context,
ids []int64,
primaryByID map[int64]string,
extrasByID map[int64][]string,
) map[int64]MonitorStatusSummary {
out := make(map[int64]MonitorStatusSummary, len(ids))
if len(ids) == 0 {
return out
}
latestMap, err := s.repo.ListLatestForMonitorIDs(ctx, ids)
if err != nil {
slog.Warn("channel_monitor: batch load latest failed", "error", err)
latestMap = map[int64][]*ChannelMonitorLatest{}
}
availMap, err := s.repo.ComputeAvailabilityForMonitors(ctx, ids, monitorAvailability7Days)
if err != nil {
slog.Warn("channel_monitor: batch compute availability failed", "error", err)
availMap = map[int64][]*ChannelMonitorAvailability{}
}
for _, id := range ids {
out[id] = buildStatusSummary(
indexLatestByModel(latestMap[id]),
indexAvailabilityByModel(availMap[id]),
primaryByID[id],
extrasByID[id],
)
}
return out
}
// ListUserView 用户只读视图:列出所有 enabled 监控的概览。
// 使用批量聚合接口避免 N+1:1 次查 monitors,1 次查 latest(所有 monitor),1 次查 availability。
func (s *ChannelMonitorService) ListUserView(ctx context.Context) ([]*UserMonitorView, error) {
monitors, err := s.repo.ListEnabled(ctx)
if err != nil {
return nil, fmt.Errorf("list enabled monitors: %w", err)
}
if len(monitors) == 0 {
return []*UserMonitorView{}, nil
}
ids := make([]int64, 0, len(monitors))
primaryByID := make(map[int64]string, len(monitors))
extrasByID := make(map[int64][]string, len(monitors))
for _, m := range monitors {
ids = append(ids, m.ID)
primaryByID[m.ID] = m.PrimaryModel
extrasByID[m.ID] = m.ExtraModels
}
summaries := s.BatchMonitorStatusSummary(ctx, ids, primaryByID, extrasByID)
views := make([]*UserMonitorView, 0, len(monitors))
for _, m := range monitors {
summary := summaries[m.ID]
views = append(views, buildUserViewFromSummary(m, summary))
}
return views, nil
}
// GetUserDetail 用户只读视图:单个监控详情(每个模型 7d/15d/30d 可用率与平均延迟)。
// 不暴露 api_key。
func (s *ChannelMonitorService) GetUserDetail(ctx context.Context, id int64) (*UserMonitorDetail, error) {
m, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if !m.Enabled {
return nil, ErrChannelMonitorNotFound
}
latest, err := s.repo.ListLatestPerModel(ctx, id)
if err != nil {
return nil, fmt.Errorf("list latest per model: %w", err)
}
availMap, err := s.collectAvailabilityWindows(ctx, id)
if err != nil {
return nil, err
}
models := mergeModelDetails(m, latest, availMap)
return &UserMonitorDetail{
ID: m.ID,
Name: m.Name,
Provider: m.Provider,
GroupName: m.GroupName,
Models: models,
}, nil
}
// collectAvailabilityWindows 一次性查询 7/15/30 天三个窗口,按模型组织。
func (s *ChannelMonitorService) collectAvailabilityWindows(ctx context.Context, monitorID int64) (map[int]map[string]*ChannelMonitorAvailability, error) {
out := make(map[int]map[string]*ChannelMonitorAvailability, 3)
windows := []int{monitorAvailability7Days, monitorAvailability15Days, monitorAvailability30Days}
for _, w := range windows {
rows, err := s.repo.ComputeAvailability(ctx, monitorID, w)
if err != nil {
return nil, fmt.Errorf("compute availability %dd: %w", w, err)
}
out[w] = indexAvailabilityByModel(rows)
}
return out, nil
}
// ---------- 纯函数 helper(无 IO,可在 batch / 单 monitor / detail 路径复用)----------
// indexLatestByModel 把 latest 切片按 model 索引(小工具,避免在 hot path 重复写)。
func indexLatestByModel(rows []*ChannelMonitorLatest) map[string]*ChannelMonitorLatest {
m := make(map[string]*ChannelMonitorLatest, len(rows))
for _, r := range rows {
m[r.Model] = r
}
return m
}
// indexAvailabilityByModel 把 availability 切片按 model 索引。
func indexAvailabilityByModel(rows []*ChannelMonitorAvailability) map[string]*ChannelMonitorAvailability {
m := make(map[string]*ChannelMonitorAvailability, len(rows))
for _, r := range rows {
m[r.Model] = r
}
return m
}
// buildStatusSummary 由 latest + availability 字典构造 MonitorStatusSummary。
// 不做任何 IO,纯组装,便于在 batch 与单 monitor 路径复用。
func buildStatusSummary(
latestByModel map[string]*ChannelMonitorLatest,
availByModel map[string]*ChannelMonitorAvailability,
primary string,
extras []string,
) MonitorStatusSummary {
summary := MonitorStatusSummary{ExtraModels: make([]ExtraModelStatus, 0, len(extras))}
if primary != "" {
if l, ok := latestByModel[primary]; ok {
summary.PrimaryStatus = l.Status
summary.PrimaryLatencyMs = l.LatencyMs
}
if a, ok := availByModel[primary]; ok {
summary.Availability7d = a.AvailabilityPct
}
}
for _, model := range extras {
entry := ExtraModelStatus{Model: model}
if l, ok := latestByModel[model]; ok {
entry.Status = l.Status
entry.LatencyMs = l.LatencyMs
}
summary.ExtraModels = append(summary.ExtraModels, entry)
}
return summary
}
// buildUserViewFromSummary 用预聚合好的 MonitorStatusSummary 装填 UserMonitorView(无 IO)。
func buildUserViewFromSummary(m *ChannelMonitor, summary MonitorStatusSummary) *UserMonitorView {
return &UserMonitorView{
ID: m.ID,
Name: m.Name,
Provider: m.Provider,
GroupName: m.GroupName,
PrimaryModel: m.PrimaryModel,
PrimaryStatus: summary.PrimaryStatus,
PrimaryLatencyMs: summary.PrimaryLatencyMs,
Availability7d: summary.Availability7d,
ExtraModels: summary.ExtraModels,
}
}
// mergeModelDetails 合并 latest + availability 三个窗口为 ModelDetail 列表。
// 复用 indexLatestByModel,避免在多处重复写 build map 逻辑。
func mergeModelDetails(
m *ChannelMonitor,
latest []*ChannelMonitorLatest,
availMap map[int]map[string]*ChannelMonitorAvailability,
) []ModelDetail {
all := append([]string{m.PrimaryModel}, m.ExtraModels...)
latestByModel := indexLatestByModel(latest)
out := make([]ModelDetail, 0, len(all))
for _, model := range all {
d := ModelDetail{Model: model}
if l, ok := latestByModel[model]; ok {
d.LatestStatus = l.Status
d.LatestLatencyMs = l.LatencyMs
}
if a, ok := availMap[monitorAvailability7Days][model]; ok {
d.Availability7d = a.AvailabilityPct
d.AvgLatency7dMs = a.AvgLatencyMs
}
if a, ok := availMap[monitorAvailability15Days][model]; ok {
d.Availability15d = a.AvailabilityPct
}
if a, ok := availMap[monitorAvailability30Days][model]; ok {
d.Availability30d = a.AvailabilityPct
}
out = append(out, d)
}
return out
}
package service
import (
"fmt"
"math/rand/v2"
"regexp"
"strconv"
)
// monitorChallengePromptTemplate 1:1 复刻 BingZi-233/check-cx 的 few-shot 模板。
const monitorChallengePromptTemplate = `Calculate and respond with ONLY the number, nothing else.
Q: 3 + 5 = ?
A: 8
Q: 12 - 7 = ?
A: 5
Q: %d %s %d = ?
A:`
// monitorChallengeNumberRegex 提取响应中的所有整数(含负号)。
var monitorChallengeNumberRegex = regexp.MustCompile(`-?\d+`)
// monitorChallenge 一次 challenge 的 prompt + 期望答案。
type monitorChallenge struct {
Prompt string
Expected string
}
// generateChallenge 生成一次随机算术 challenge:
// - 随机两个 [monitorChallengeMin, monitorChallengeMax] 整数
// - 50% 加 / 50% 减;减法用 max - min 保证非负
// - 渲染 few-shot 模板
//
// 不强求加密随机:math/rand/v2 足够分散,避免 crypto/rand 的开销。
func generateChallenge() monitorChallenge {
a := randIntInRange(monitorChallengeMin, monitorChallengeMax)
b := randIntInRange(monitorChallengeMin, monitorChallengeMax)
if rand.IntN(2) == 0 { //nolint:gosec // 仅用于生成测试问题,无安全影响
// 加法
return monitorChallenge{
Prompt: fmt.Sprintf(monitorChallengePromptTemplate, a, "+", b),
Expected: strconv.Itoa(a + b),
}
}
// 减法,保证非负
hi, lo := a, b
if lo > hi {
hi, lo = lo, hi
}
return monitorChallenge{
Prompt: fmt.Sprintf(monitorChallengePromptTemplate, hi, "-", lo),
Expected: strconv.Itoa(hi - lo),
}
}
// randIntInRange 返回 [min, max] 闭区间的随机整数。
func randIntInRange(minVal, maxVal int) int {
if maxVal <= minVal {
return minVal
}
return minVal + rand.IntN(maxVal-minVal+1) //nolint:gosec
}
// validateChallenge 在响应文本中查找 expected 整数答案,返回是否通过校验。
func validateChallenge(responseText, expected string) bool {
if responseText == "" || expected == "" {
return false
}
matches := monitorChallengeNumberRegex.FindAllString(responseText, -1)
for _, m := range matches {
if m == expected {
return true
}
}
return false
}
package service
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"github.com/tidwall/gjson"
)
// monitorHTTPClient 共享一个 http.Client,避免每次检测重建 transport。
// 自定义 Transport 在 dial 时强制再次校验 IP,防止 DNS rebinding 绕过 validateEndpoint。
var monitorHTTPClient = newSSRFSafeHTTPClient(monitorRequestTimeout)
// monitorPingHTTPClient 用于 endpoint origin 的 HEAD ping,超时更短。
var monitorPingHTTPClient = newSSRFSafeHTTPClient(monitorPingTimeout)
// newSSRFSafeHTTPClient 返回一个使用 safeDialContext 的 http.Client。
// 仅供监控模块对外发起请求使用——所有目标都应是公网 endpoint。
func newSSRFSafeHTTPClient(timeout time.Duration) *http.Client {
tr := &http.Transport{
DialContext: safeDialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 16,
IdleConnTimeout: monitorIdleConnTimeout,
TLSHandshakeTimeout: monitorTLSHandshakeTimeout,
ResponseHeaderTimeout: monitorResponseHeaderTimeout,
}
return &http.Client{Timeout: timeout, Transport: tr}
}
// runCheckForModel 对单个 (provider, model) 做一次完整检测。
// 不返回 error:所有失败都包装进 CheckResult.Status=error/failed。
func runCheckForModel(ctx context.Context, provider, endpoint, apiKey, model string) *CheckResult {
res := &CheckResult{
Model: model,
Status: MonitorStatusError,
CheckedAt: time.Now(),
}
challenge := generateChallenge()
start := time.Now()
respText, statusCode, err := callProvider(ctx, provider, endpoint, apiKey, model, challenge.Prompt)
latency := time.Since(start)
latencyMs := int(latency / time.Millisecond)
res.LatencyMs = &latencyMs
if err != nil {
res.Status = MonitorStatusError
res.Message = truncateMessage(sanitizeErrorMessage(err.Error()))
return res
}
if statusCode < 200 || statusCode >= 300 {
res.Status = MonitorStatusError
res.Message = truncateMessage(sanitizeErrorMessage(fmt.Sprintf("upstream HTTP %d: %s", statusCode, respText)))
return res
}
if !validateChallenge(respText, challenge.Expected) {
res.Status = MonitorStatusFailed
res.Message = truncateMessage(sanitizeErrorMessage(fmt.Sprintf("challenge mismatch (expected %s, got %q)", challenge.Expected, respText)))
return res
}
if latency >= monitorDegradedThreshold {
res.Status = MonitorStatusDegraded
res.Message = truncateMessage(fmt.Sprintf("slow response: %dms", latencyMs))
return res
}
res.Status = MonitorStatusOperational
return res
}
// pingEndpointOrigin 对 endpoint 的 origin (scheme://host) 发起 HEAD 请求,返回耗时。
// 失败时返回 nil(不影响主状态判定)。
func pingEndpointOrigin(ctx context.Context, endpoint string) *int {
origin, err := extractOrigin(endpoint)
if err != nil || origin == "" {
return nil
}
req, err := http.NewRequestWithContext(ctx, http.MethodHead, origin, nil)
if err != nil {
return nil
}
start := time.Now()
resp, err := monitorPingHTTPClient.Do(req)
if err != nil {
return nil
}
defer func() { _ = resp.Body.Close() }()
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, monitorPingDiscardMaxBytes))
ms := int(time.Since(start) / time.Millisecond)
return &ms
}
// providerAdapter 描述某个 provider 在 challenge 检测中需要的 4 件事:
// - 拼出请求路径(含 model 占位)
// - 序列化请求体
// - 构造鉴权头
// - 从响应 JSON 中按 path 提取文本(gjson path)
//
// 加新 provider 只需要在 providerAdapters 里增加一个条目,无需触碰 callProvider / validateProvider。
type providerAdapter struct {
buildPath func(model string) string
buildBody func(model, prompt string) ([]byte, error)
buildHeaders func(apiKey string) map[string]string
textPath string // gjson 提取响应文本的 path
}
// providerAdapters 全部已支持的 provider。键值即 MonitorProvider* 字符串。
//
//nolint:gochecknoglobals // 适配器表是只读静态数据,初始化后不变更。
var providerAdapters = map[string]providerAdapter{
MonitorProviderOpenAI: {
buildPath: func(string) string { return providerOpenAIPath },
buildBody: func(model, prompt string) ([]byte, error) {
return json.Marshal(map[string]any{
"model": model,
"messages": []map[string]string{{"role": "user", "content": prompt}},
"max_tokens": monitorChallengeMaxTokens,
"stream": false,
})
},
buildHeaders: func(apiKey string) map[string]string {
return map[string]string{"Authorization": "Bearer " + apiKey}
},
textPath: "choices.0.message.content",
},
MonitorProviderAnthropic: {
buildPath: func(string) string { return providerAnthropicPath },
buildBody: func(model, prompt string) ([]byte, error) {
return json.Marshal(map[string]any{
"model": model,
"messages": []map[string]string{{"role": "user", "content": prompt}},
"max_tokens": monitorChallengeMaxTokens,
})
},
buildHeaders: func(apiKey string) map[string]string {
return map[string]string{
"x-api-key": apiKey,
"anthropic-version": monitorAnthropicAPIVersion,
}
},
textPath: "content.0.text",
},
MonitorProviderGemini: {
// Gemini 把 model 名写在 URL path 上:/v1beta/models/{model}:generateContent
buildPath: func(model string) string { return fmt.Sprintf(providerGeminiPathTemplate, model) },
buildBody: func(_, prompt string) ([]byte, error) {
return json.Marshal(map[string]any{
"contents": []map[string]any{
{"parts": []map[string]any{{"text": prompt}}},
},
"generationConfig": map[string]any{"maxOutputTokens": monitorChallengeMaxTokens},
})
},
// 使用 x-goog-api-key header 而不是 ?key= query,避免 *url.Error 把 key 回填到错误日志。
buildHeaders: func(apiKey string) map[string]string {
return map[string]string{"x-goog-api-key": apiKey}
},
textPath: "candidates.0.content.parts.0.text",
},
}
// isSupportedProvider 校验 provider 字符串是否在 adapter 表中。
// 供 validate.go 的 validateProvider 复用,避免两份 switch 漂移。
func isSupportedProvider(p string) bool {
_, ok := providerAdapters[p]
return ok
}
// callProvider 通过 providerAdapters 分发到具体实现。
// 返回值:响应中提取的文本、HTTP status、网络/序列化错误。
func callProvider(ctx context.Context, provider, endpoint, apiKey, model, prompt string) (string, int, error) {
adapter, ok := providerAdapters[provider]
if !ok {
return "", 0, fmt.Errorf("unsupported provider %q", provider)
}
body, err := adapter.buildBody(model, prompt)
if err != nil {
return "", 0, fmt.Errorf("marshal body: %w", err)
}
full := joinURL(endpoint, adapter.buildPath(model))
respBody, status, err := postRawJSON(ctx, full, body, adapter.buildHeaders(apiKey))
if err != nil {
return "", status, err
}
return gjson.GetBytes(respBody, adapter.textPath).String(), status, nil
}
// postRawJSON 发送 POST + 已序列化好的 JSON 字节,限制响应体大小,返回响应字节、HTTP status、错误。
// adapter 自行 marshal 是为了精确控制字段顺序与类型,所以这里直接收 []byte 而不是 any。
func postRawJSON(ctx context.Context, fullURL string, payload []byte, headers map[string]string) ([]byte, int, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(payload))
if err != nil {
return nil, 0, fmt.Errorf("build request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := monitorHTTPClient.Do(req)
if err != nil {
return nil, 0, fmt.Errorf("do request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
respBody, err := io.ReadAll(io.LimitReader(resp.Body, monitorResponseMaxBytes))
if err != nil {
return nil, resp.StatusCode, fmt.Errorf("read body: %w", err)
}
return respBody, resp.StatusCode, nil
}
// joinURL 把 base origin 与 path 拼成完整 URL。
// 容忍 base 末尾有/无斜杠,path 必带前导斜杠。
func joinURL(base, path string) string {
base = strings.TrimRight(base, "/")
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return base + path
}
// extractOrigin 从一个 endpoint URL 中提取 scheme://host[:port] 部分。
func extractOrigin(endpoint string) (string, error) {
u, err := url.Parse(endpoint)
if err != nil {
return "", err
}
if u.Scheme == "" || u.Host == "" {
return "", errors.New("endpoint missing scheme or host")
}
return u.Scheme + "://" + u.Host, nil
}
// monitorSensitiveQueryParamRegex 匹配 URL query 中可能泄露凭证的参数:
// key / api_key / api-key / access_token / token / authorization / x-api-key。
// 大小写不敏感,匹配 `?name=value` 或 `&name=value` 形式(value 截到 & 或字符串末尾)。
var monitorSensitiveQueryParamRegex = regexp.MustCompile(`(?i)([?&](?:key|api[_-]?key|access[_-]?token|token|authorization|x-api-key)=)[^&\s"']+`)
// monitorAPIKeyPatterns 匹配常见 provider 的 API key 字面量。
// 顺序敏感:sk-ant- 必须放在 sk- 之前,否则会被通用 sk- 模式先消费。
var monitorAPIKeyPatterns = []struct {
pattern *regexp.Regexp
replace string
}{
// Anthropic(带前缀,必须先匹配):sk-ant-xxxxxxx
{regexp.MustCompile(`sk-ant-[A-Za-z0-9_-]{20,}`), "sk-ant-***REDACTED***"},
// OpenAI / Anthropic 通用 sk-: sk-xxxxxxx
{regexp.MustCompile(`sk-[A-Za-z0-9-]{20,}`), "sk-***REDACTED***"},
// Gemini / Google API Key:固定前缀 + 35 位
{regexp.MustCompile(`AIza[A-Za-z0-9_-]{35}`), "AIza***REDACTED***"},
// JWT 三段式(Bearer 后常出现):eyJxxx.eyJxxx.signature
{regexp.MustCompile(`eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}`), "eyJ***REDACTED.JWT***"},
}
// sanitizeErrorMessage 擦除错误/响应文本中可能泄露的 API key。
// 处理两类来源:
// 1. URL query 中的 ?key= / ?api_key= 等(Go *url.Error 会回填完整 URL)
// 2. 上游 HTTP body 文本里直接出现的 sk-* / AIza* / JWT 等密钥碎片
//
// 注意:与 gemini_messages_compat_service.go 的 sanitizeUpstreamErrorMessage 关注点类似但参数集更广,
// 监控模块独立维护,避免互相耦合。
func sanitizeErrorMessage(msg string) string {
if msg == "" {
return msg
}
msg = monitorSensitiveQueryParamRegex.ReplaceAllString(msg, `${1}REDACTED`)
for _, p := range monitorAPIKeyPatterns {
msg = p.pattern.ReplaceAllString(msg, p.replace)
}
return msg
}
// truncateMessage 把消息按 monitorMessageMaxBytes 截断,避免 DB 列溢出与日志过长。
func truncateMessage(msg string) string {
if len(msg) <= monitorMessageMaxBytes {
return msg
}
const ellipsis = "...(truncated)"
cutoff := monitorMessageMaxBytes - len(ellipsis)
if cutoff < 0 {
cutoff = 0
}
return msg[:cutoff] + ellipsis
}
package service
import (
"time"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
)
// ChannelMonitor 全局常量。
// 这些是 MVP 阶段的硬编码值,按需可以提到 config 中。
const (
// monitorRequestTimeout 单次模型请求总超时(含 Body 读取)。
monitorRequestTimeout = 45 * time.Second
// monitorPingTimeout HEAD 请求 endpoint origin 的超时。
monitorPingTimeout = 8 * time.Second
// monitorDegradedThreshold 主请求成功但耗时超过该阈值视为 degraded。
monitorDegradedThreshold = 6 * time.Second
// monitorHistoryRetentionDays 历史保留天数(每天清理一次)。
monitorHistoryRetentionDays = 30
// monitorWorkerConcurrency 调度器并发执行的监控数(pond 池容量)。
monitorWorkerConcurrency = 5
// monitorTickerInterval 调度器扫描"到期监控"的间隔。
monitorTickerInterval = 5 * time.Second
// monitorMinIntervalSeconds / monitorMaxIntervalSeconds 用户配置的检测间隔上下限。
monitorMinIntervalSeconds = 15
monitorMaxIntervalSeconds = 3600
// monitorMessageMaxBytes message 字段最大字节数(与 schema/migration 一致)。
monitorMessageMaxBytes = 500
// monitorResponseMaxBytes 单次模型响应最大读取字节,防止 OOM。
monitorResponseMaxBytes = 64 * 1024
// monitorChallengeMin / monitorChallengeMax challenge 操作数范围。
monitorChallengeMin = 1
monitorChallengeMax = 50
// providerOpenAIPath OpenAI Chat Completions 路径。
providerOpenAIPath = "/v1/chat/completions"
// providerAnthropicPath Anthropic Messages 路径。
providerAnthropicPath = "/v1/messages"
// providerGeminiPathTemplate Gemini generateContent 路径模板(含 model 占位)。
providerGeminiPathTemplate = "/v1beta/models/%s:generateContent"
// MonitorProviderOpenAI / Anthropic / Gemini provider 字符串常量(也是 ent enum 的实际值)。
MonitorProviderOpenAI = "openai"
MonitorProviderAnthropic = "anthropic"
MonitorProviderGemini = "gemini"
// MonitorStatusOperational 等监控状态字符串常量(与 ent enum 一致)。
MonitorStatusOperational = "operational"
MonitorStatusDegraded = "degraded"
MonitorStatusFailed = "failed"
MonitorStatusError = "error"
// monitorAvailability7Days / 15 / 30 用于聚合查询窗口。
monitorAvailability7Days = 7
monitorAvailability15Days = 15
monitorAvailability30Days = 30
// monitorCleanupCheckInterval 历史清理调度器的检查频率(每小时检查"是否到 03:00")。
monitorCleanupCheckInterval = time.Hour
// monitorCleanupHour 凌晨 3 点执行历史清理。
monitorCleanupHour = 3
// MonitorHistoryDefaultLimit 历史查询默认返回条数(handler 层共享)。
MonitorHistoryDefaultLimit = 100
// MonitorHistoryMaxLimit 历史查询最大返回条数(handler 层共享)。
MonitorHistoryMaxLimit = 1000
// monitorEndpointResolveTimeout validateEndpoint 解析 hostname 的最长耗时。
monitorEndpointResolveTimeout = 5 * time.Second
// ---- checker / runner 行为参数(消除 magic 值)----
// monitorAnthropicAPIVersion Anthropic Messages API 版本头。
monitorAnthropicAPIVersion = "2023-06-01"
// monitorChallengeMaxTokens 单次 challenge 请求的 max_tokens(足够回答个位数算术)。
monitorChallengeMaxTokens = 50
// monitorListDueTimeout tickDueChecks 查询到期监控的总超时。
monitorListDueTimeout = 10 * time.Second
// monitorRunOneBuffer runOne 的总超时缓冲(除请求超时与 ping 超时外的额外裕量)。
monitorRunOneBuffer = 10 * time.Second
// monitorCleanupTimeout 历史清理任务的总超时。
monitorCleanupTimeout = 30 * time.Second
// monitorCleanupDayLayout 历史清理用于"今日是否已跑过"判定的日期格式。
monitorCleanupDayLayout = "2006-01-02"
// monitorIdleConnTimeout HTTP transport 空闲连接关闭超时。
monitorIdleConnTimeout = 30 * time.Second
// monitorTLSHandshakeTimeout HTTP transport TLS 握手超时。
monitorTLSHandshakeTimeout = 10 * time.Second
// monitorResponseHeaderTimeout HTTP transport 等待响应头超时。
monitorResponseHeaderTimeout = 30 * time.Second
// monitorPingDiscardMaxBytes ping 时丢弃响应体的最大字节数。
monitorPingDiscardMaxBytes = 1024
// monitorDialTimeout 自定义 dialer 单次连接超时。
monitorDialTimeout = 10 * time.Second
// monitorDialKeepAlive 自定义 dialer keep-alive 间隔。
monitorDialKeepAlive = 30 * time.Second
)
// 业务错误(统一在此声明,避免散落)。
var (
ErrChannelMonitorNotFound = infraerrors.NotFound(
"CHANNEL_MONITOR_NOT_FOUND", "channel monitor not found",
)
ErrChannelMonitorInvalidProvider = infraerrors.BadRequest(
"CHANNEL_MONITOR_INVALID_PROVIDER", "provider must be one of openai/anthropic/gemini",
)
ErrChannelMonitorInvalidInterval = infraerrors.BadRequest(
"CHANNEL_MONITOR_INVALID_INTERVAL", "interval_seconds must be in [15, 3600]",
)
ErrChannelMonitorInvalidEndpoint = infraerrors.BadRequest(
"CHANNEL_MONITOR_INVALID_ENDPOINT", "endpoint must be a valid https URL",
)
ErrChannelMonitorEndpointScheme = infraerrors.BadRequest(
"CHANNEL_MONITOR_ENDPOINT_SCHEME", "endpoint must use https scheme",
)
ErrChannelMonitorEndpointPath = infraerrors.BadRequest(
"CHANNEL_MONITOR_ENDPOINT_PATH", "endpoint must be base origin only (no path/query/fragment)",
)
ErrChannelMonitorEndpointPrivate = infraerrors.BadRequest(
"CHANNEL_MONITOR_ENDPOINT_PRIVATE", "endpoint must be a public host",
)
ErrChannelMonitorEndpointUnreachable = infraerrors.BadRequest(
"CHANNEL_MONITOR_ENDPOINT_UNREACHABLE", "endpoint hostname could not be resolved",
)
ErrChannelMonitorMissingAPIKey = infraerrors.BadRequest(
"CHANNEL_MONITOR_MISSING_API_KEY", "api_key is required when creating a monitor",
)
ErrChannelMonitorMissingPrimaryModel = infraerrors.BadRequest(
"CHANNEL_MONITOR_MISSING_PRIMARY_MODEL", "primary_model is required",
)
ErrChannelMonitorAPIKeyDecryptFailed = infraerrors.InternalServer(
"CHANNEL_MONITOR_KEY_DECRYPT_FAILED", "api key decryption failed; please re-edit the monitor with a fresh key",
)
)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment