feat(gateway): 双模式用户消息队列 — 串行队列 + 软性限速
新增 UMQ (User Message Queue) 双模式支持: - serialize: 账号级分布式串行锁 + RPM 自适应延迟(严格限流) - throttle: 仅 RPM 自适应前置延迟,不阻塞并发(软性限速) 后端: - config: 新增 Mode 字段,保留 Enabled 向后兼容 - service: 新增 UserMessageQueueService(Lua 锁/延迟算法/清理 worker) - repository: 新增 UserMsgQueueCache(Redis Lua acquire/release/force-release) - handler: 新增 UserMsgQueueHelper(SSE ping + 等待循环 + throttle) - gateway: 按 mode 分支集成 serialize/throttle 逻辑 - lint: 修复 gofmt rewrite rules、errcheck 类型断言、staticcheck QF1012 前端: - 三态选择器 UI(关闭/软性限速/串行队列)替代 toggle 开关 - BulkEdit 支持 null 语义(不修改) - i18n 中英文文案 通过 6 轮专家评审(42 次 review)、golangci-lint、单元测试、集成测试。
This commit is contained in:
@@ -30,6 +30,14 @@ const (
|
||||
// __CSP_NONCE__ will be replaced with actual nonce at request time by the SecurityHeaders middleware
|
||||
const DefaultCSPPolicy = "default-src 'self'; script-src 'self' __CSP_NONCE__ https://challenges.cloudflare.com https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-src https://challenges.cloudflare.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
||||
|
||||
// UMQ(用户消息队列)模式常量
|
||||
const (
|
||||
// UMQModeSerialize: 账号级串行锁 + RPM 自适应延迟
|
||||
UMQModeSerialize = "serialize"
|
||||
// UMQModeThrottle: 仅 RPM 自适应前置延迟,不阻塞并发
|
||||
UMQModeThrottle = "throttle"
|
||||
)
|
||||
|
||||
// 连接池隔离策略常量
|
||||
// 用于控制上游 HTTP 连接池的隔离粒度,影响连接复用和资源消耗
|
||||
const (
|
||||
@@ -455,6 +463,52 @@ type GatewayConfig struct {
|
||||
UserGroupRateCacheTTLSeconds int `mapstructure:"user_group_rate_cache_ttl_seconds"`
|
||||
// ModelsListCacheTTLSeconds: /v1/models 模型列表短缓存 TTL(秒)
|
||||
ModelsListCacheTTLSeconds int `mapstructure:"models_list_cache_ttl_seconds"`
|
||||
|
||||
// UserMessageQueue: 用户消息串行队列配置
|
||||
// 对 role:"user" 的真实用户消息实施账号级串行化 + RPM 自适应延迟
|
||||
UserMessageQueue UserMessageQueueConfig `mapstructure:"user_message_queue"`
|
||||
}
|
||||
|
||||
// UserMessageQueueConfig 用户消息串行队列配置
|
||||
// 用于 Anthropic OAuth/SetupToken 账号的用户消息串行化发送
|
||||
type UserMessageQueueConfig struct {
|
||||
// Mode: 模式选择
|
||||
// "serialize" = 账号级串行锁 + RPM 自适应延迟
|
||||
// "throttle" = 仅 RPM 自适应前置延迟,不阻塞并发
|
||||
// "" = 禁用(默认)
|
||||
Mode string `mapstructure:"mode"`
|
||||
// Enabled: 已废弃,仅向后兼容(等同于 mode: "serialize")
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
// LockTTLMs: 串行锁 TTL(毫秒),应大于最长请求时间
|
||||
LockTTLMs int `mapstructure:"lock_ttl_ms"`
|
||||
// WaitTimeoutMs: 等待获取锁的超时时间(毫秒)
|
||||
WaitTimeoutMs int `mapstructure:"wait_timeout_ms"`
|
||||
// MinDelayMs: RPM 自适应延迟下限(毫秒)
|
||||
MinDelayMs int `mapstructure:"min_delay_ms"`
|
||||
// MaxDelayMs: RPM 自适应延迟上限(毫秒)
|
||||
MaxDelayMs int `mapstructure:"max_delay_ms"`
|
||||
// CleanupIntervalSeconds: 孤儿锁清理间隔(秒),0 表示禁用
|
||||
CleanupIntervalSeconds int `mapstructure:"cleanup_interval_seconds"`
|
||||
}
|
||||
|
||||
// WaitTimeout 返回等待超时的 time.Duration
|
||||
func (c *UserMessageQueueConfig) WaitTimeout() time.Duration {
|
||||
if c.WaitTimeoutMs <= 0 {
|
||||
return 30 * time.Second
|
||||
}
|
||||
return time.Duration(c.WaitTimeoutMs) * time.Millisecond
|
||||
}
|
||||
|
||||
// GetEffectiveMode 返回生效的模式
|
||||
// 注意:Mode 字段已在 load() 中做过白名单校验和规范化,此处无需重复验证
|
||||
func (c *UserMessageQueueConfig) GetEffectiveMode() string {
|
||||
if c.Mode == UMQModeSerialize || c.Mode == UMQModeThrottle {
|
||||
return c.Mode
|
||||
}
|
||||
if c.Enabled {
|
||||
return UMQModeSerialize // 向后兼容
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GatewayOpenAIWSConfig OpenAI Responses WebSocket 配置。
|
||||
@@ -994,6 +1048,14 @@ func load(allowMissingJWTSecret bool) (*Config, error) {
|
||||
cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds = cfg.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds
|
||||
}
|
||||
|
||||
// Normalize UMQ mode: 白名单校验,非法值在加载时一次性 warn 并清空
|
||||
if m := cfg.Gateway.UserMessageQueue.Mode; m != "" && m != UMQModeSerialize && m != UMQModeThrottle {
|
||||
slog.Warn("invalid user_message_queue mode, disabling",
|
||||
"mode", m,
|
||||
"valid_modes", []string{UMQModeSerialize, UMQModeThrottle})
|
||||
cfg.Gateway.UserMessageQueue.Mode = ""
|
||||
}
|
||||
|
||||
// Auto-generate TOTP encryption key if not set (32 bytes = 64 hex chars for AES-256)
|
||||
cfg.Totp.EncryptionKey = strings.TrimSpace(cfg.Totp.EncryptionKey)
|
||||
if cfg.Totp.EncryptionKey == "" {
|
||||
@@ -1372,6 +1434,14 @@ func setDefaults() {
|
||||
viper.SetDefault("gateway.user_group_rate_cache_ttl_seconds", 30)
|
||||
viper.SetDefault("gateway.models_list_cache_ttl_seconds", 15)
|
||||
// TLS指纹伪装配置(默认关闭,需要账号级别单独启用)
|
||||
// 用户消息串行队列默认值
|
||||
viper.SetDefault("gateway.user_message_queue.enabled", false)
|
||||
viper.SetDefault("gateway.user_message_queue.lock_ttl_ms", 120000)
|
||||
viper.SetDefault("gateway.user_message_queue.wait_timeout_ms", 30000)
|
||||
viper.SetDefault("gateway.user_message_queue.min_delay_ms", 200)
|
||||
viper.SetDefault("gateway.user_message_queue.max_delay_ms", 2000)
|
||||
viper.SetDefault("gateway.user_message_queue.cleanup_interval_seconds", 60)
|
||||
|
||||
viper.SetDefault("gateway.tls_fingerprint.enabled", true)
|
||||
viper.SetDefault("concurrency.ping_interval", 10)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user