Problem:
Upstream channels can reject monitor probes based on client fingerprint
(e.g. "only Claude Code clients allowed"). The monitor had no way to
customize the outgoing request to bypass such restrictions.
Solution:
Introduce reusable request templates that carry extra_headers plus an
optional body override; monitors reference a template and receive a
snapshot copy on apply. Template edits do NOT auto-propagate — users
must click "apply to associated monitors" to refresh snapshots, so a
bad template edit cannot instantly break all production monitors.
Data model (migration 112):
- channel_monitor_request_templates: id, name, provider, description,
extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'),
body_override jsonb. Unique (provider, name).
- channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers,
+body_override_mode, +body_override (the three runtime snapshot fields).
Checker (channel_monitor_checker.go):
- callProvider + runCheckForModel accept a CheckOptions carrying the
snapshot fields. mergeHeaders applies user headers on top of adapter
defaults (forbidden list: Host / Content-Length / Transfer-Encoding /
Connection / Content-Encoding).
- buildRequestBody:
off -> adapter default body
merge -> shallow-merge over default; per-provider deny list
(model/messages/contents) protects the challenge contract
replace -> user body verbatim
- Replace mode skips challenge validation; instead HTTP 2xx + non-empty
extracted response text = operational, empty = failed.
- 4 new unit tests cover all three modes + replace/empty-response case.
Admin API:
- /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot
on all template_id=id monitors, returns affected count).
- channel_monitor request/response DTOs gain the 4 new fields.
Frontend:
- channelMonitorTemplate.ts API client.
- MonitorAdvancedRequestConfig.vue shared component for headers textarea
+ body mode radio + body JSON editor; used by both template and monitor
forms.
- MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/
delete/apply, live "associated monitors" count per row.
- MonitorFiltersBar: new 模板管理 button next to 新增监控.
- MonitorFormDialog: collapsible 高级 section with template dropdown
(filtered by form.provider, clears on provider change) + embedded
AdvancedRequestConfig. Picking a template copies its fields into the
form (snapshot semantics mirrored on the client).
- i18n zh/en entries for all new copy.
chore: bump version to 0.1.114.32
194 lines
6.1 KiB
Go
194 lines
6.1 KiB
Go
package repository
|
||
|
||
import (
|
||
"database/sql"
|
||
"errors"
|
||
|
||
entsql "entgo.io/ent/dialect/sql"
|
||
"github.com/Wei-Shaw/sub2api/ent"
|
||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||
"github.com/google/wire"
|
||
"github.com/redis/go-redis/v9"
|
||
)
|
||
|
||
// ProvideConcurrencyCache 创建并发控制缓存,从配置读取 TTL 参数
|
||
// 性能优化:TTL 可配置,支持长时间运行的 LLM 请求场景
|
||
func ProvideConcurrencyCache(rdb *redis.Client, cfg *config.Config) service.ConcurrencyCache {
|
||
waitTTLSeconds := int(cfg.Gateway.Scheduling.StickySessionWaitTimeout.Seconds())
|
||
if cfg.Gateway.Scheduling.FallbackWaitTimeout > cfg.Gateway.Scheduling.StickySessionWaitTimeout {
|
||
waitTTLSeconds = int(cfg.Gateway.Scheduling.FallbackWaitTimeout.Seconds())
|
||
}
|
||
if waitTTLSeconds <= 0 {
|
||
waitTTLSeconds = cfg.Gateway.ConcurrencySlotTTLMinutes * 60
|
||
}
|
||
return NewConcurrencyCache(rdb, cfg.Gateway.ConcurrencySlotTTLMinutes, waitTTLSeconds)
|
||
}
|
||
|
||
// ProvideGitHubReleaseClient 创建 GitHub Release 客户端
|
||
// 从配置中读取代理设置,支持国内服务器通过代理访问 GitHub
|
||
func ProvideGitHubReleaseClient(cfg *config.Config) service.GitHubReleaseClient {
|
||
return NewGitHubReleaseClient(cfg.Update.ProxyURL, cfg.Security.ProxyFallback.AllowDirectOnError)
|
||
}
|
||
|
||
// ProvidePricingRemoteClient 创建定价数据远程客户端
|
||
// 从配置中读取代理设置,支持国内服务器通过代理访问 GitHub 上的定价数据
|
||
func ProvidePricingRemoteClient(cfg *config.Config) service.PricingRemoteClient {
|
||
return NewPricingRemoteClient(cfg.Update.ProxyURL, cfg.Security.ProxyFallback.AllowDirectOnError)
|
||
}
|
||
|
||
// ProvideSessionLimitCache 创建会话限制缓存
|
||
// 用于 Anthropic OAuth/SetupToken 账号的并发会话数量控制
|
||
func ProvideSessionLimitCache(rdb *redis.Client, cfg *config.Config) service.SessionLimitCache {
|
||
defaultIdleTimeoutMinutes := 5 // 默认 5 分钟空闲超时
|
||
if cfg != nil && cfg.Gateway.SessionIdleTimeoutMinutes > 0 {
|
||
defaultIdleTimeoutMinutes = cfg.Gateway.SessionIdleTimeoutMinutes
|
||
}
|
||
return NewSessionLimitCache(rdb, defaultIdleTimeoutMinutes)
|
||
}
|
||
|
||
// ProvideSchedulerCache 创建调度快照缓存,并注入快照分块参数。
|
||
func ProvideSchedulerCache(rdb *redis.Client, cfg *config.Config) service.SchedulerCache {
|
||
mgetChunkSize := defaultSchedulerSnapshotMGetChunkSize
|
||
writeChunkSize := defaultSchedulerSnapshotWriteChunkSize
|
||
if cfg != nil {
|
||
if cfg.Gateway.Scheduling.SnapshotMGetChunkSize > 0 {
|
||
mgetChunkSize = cfg.Gateway.Scheduling.SnapshotMGetChunkSize
|
||
}
|
||
if cfg.Gateway.Scheduling.SnapshotWriteChunkSize > 0 {
|
||
writeChunkSize = cfg.Gateway.Scheduling.SnapshotWriteChunkSize
|
||
}
|
||
}
|
||
return newSchedulerCacheWithChunkSizes(rdb, mgetChunkSize, writeChunkSize)
|
||
}
|
||
|
||
// ProviderSet is the Wire provider set for all repositories
|
||
var ProviderSet = wire.NewSet(
|
||
NewUserRepository,
|
||
NewAPIKeyRepository,
|
||
NewGroupRepository,
|
||
NewAccountRepository,
|
||
NewScheduledTestPlanRepository, // 定时测试计划仓储
|
||
NewScheduledTestResultRepository, // 定时测试结果仓储
|
||
NewProxyRepository,
|
||
NewRedeemCodeRepository,
|
||
NewPromoCodeRepository,
|
||
NewAnnouncementRepository,
|
||
NewAnnouncementReadRepository,
|
||
NewUsageLogRepository,
|
||
NewUsageBillingRepository,
|
||
NewIdempotencyRepository,
|
||
NewUsageCleanupRepository,
|
||
NewDashboardAggregationRepository,
|
||
NewSettingRepository,
|
||
NewOpsRepository,
|
||
NewUserSubscriptionRepository,
|
||
NewUserAttributeDefinitionRepository,
|
||
NewUserAttributeValueRepository,
|
||
NewUserGroupRateRepository,
|
||
NewErrorPassthroughRepository,
|
||
NewTLSFingerprintProfileRepository,
|
||
NewChannelRepository,
|
||
NewChannelMonitorRepository,
|
||
NewChannelMonitorRequestTemplateRepository,
|
||
|
||
// Cache implementations
|
||
NewGatewayCache,
|
||
NewBillingCache,
|
||
NewAPIKeyCache,
|
||
NewTempUnschedCache,
|
||
NewTimeoutCounterCache,
|
||
NewInternal500CounterCache,
|
||
ProvideConcurrencyCache,
|
||
ProvideSessionLimitCache,
|
||
NewRPMCache,
|
||
NewUserMsgQueueCache,
|
||
NewDashboardCache,
|
||
NewEmailCache,
|
||
NewIdentityCache,
|
||
NewRedeemCache,
|
||
NewUpdateCache,
|
||
NewGeminiTokenCache,
|
||
ProvideSchedulerCache,
|
||
NewSchedulerOutboxRepository,
|
||
NewProxyLatencyCache,
|
||
NewTotpCache,
|
||
NewRefreshTokenCache,
|
||
NewErrorPassthroughCache,
|
||
NewTLSFingerprintProfileCache,
|
||
|
||
// Encryptors
|
||
NewAESEncryptor,
|
||
|
||
// Backup infrastructure
|
||
NewPgDumper,
|
||
NewS3BackupStoreFactory,
|
||
|
||
// HTTP service ports (DI Strategy A: return interface directly)
|
||
NewTurnstileVerifier,
|
||
ProvidePricingRemoteClient,
|
||
ProvideGitHubReleaseClient,
|
||
NewProxyExitInfoProber,
|
||
NewClaudeUsageFetcher,
|
||
NewClaudeOAuthClient,
|
||
NewHTTPUpstream,
|
||
NewOpenAIOAuthClient,
|
||
NewGeminiOAuthClient,
|
||
NewGeminiCliCodeAssistClient,
|
||
NewGeminiDriveClient,
|
||
|
||
ProvideEnt,
|
||
ProvideSQLDB,
|
||
ProvideRedis,
|
||
)
|
||
|
||
// ProvideEnt 为依赖注入提供 Ent 客户端。
|
||
//
|
||
// 该函数是 InitEnt 的包装器,符合 Wire 的依赖提供函数签名要求。
|
||
// Wire 会在编译时分析依赖关系,自动生成初始化代码。
|
||
//
|
||
// 依赖:config.Config
|
||
// 提供:*ent.Client
|
||
func ProvideEnt(cfg *config.Config) (*ent.Client, error) {
|
||
client, _, err := InitEnt(cfg)
|
||
return client, err
|
||
}
|
||
|
||
// ProvideSQLDB 从 Ent 客户端提取底层的 *sql.DB 连接。
|
||
//
|
||
// 某些 Repository 需要直接执行原生 SQL(如复杂的批量更新、聚合查询),
|
||
// 此时需要访问底层的 sql.DB 而不是通过 Ent ORM。
|
||
//
|
||
// 设计说明:
|
||
// - Ent 底层使用 sql.DB,通过 Driver 接口可以访问
|
||
// - 这种设计允许在同一事务中混用 Ent 和原生 SQL
|
||
//
|
||
// 依赖:*ent.Client
|
||
// 提供:*sql.DB
|
||
func ProvideSQLDB(client *ent.Client) (*sql.DB, error) {
|
||
if client == nil {
|
||
return nil, errors.New("nil ent client")
|
||
}
|
||
// 从 Ent 客户端获取底层驱动
|
||
drv, ok := client.Driver().(*entsql.Driver)
|
||
if !ok {
|
||
return nil, errors.New("ent driver does not expose *sql.DB")
|
||
}
|
||
// 返回驱动持有的 sql.DB 实例
|
||
return drv.DB(), nil
|
||
}
|
||
|
||
// ProvideRedis 为依赖注入提供 Redis 客户端。
|
||
//
|
||
// Redis 用于:
|
||
// - 分布式锁(如并发控制)
|
||
// - 缓存(如用户会话、API 响应缓存)
|
||
// - 速率限制
|
||
// - 实时统计数据
|
||
//
|
||
// 依赖:config.Config
|
||
// 提供:*redis.Client
|
||
func ProvideRedis(cfg *config.Config) *redis.Client {
|
||
return InitRedis(cfg)
|
||
}
|