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:
QTom
2026-03-03 01:02:39 +08:00
parent 7abec1888f
commit a9285b8a94
21 changed files with 1099 additions and 15 deletions

View File

@@ -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)