diff --git a/admin/handler.go b/admin/handler.go index d1cb9f8..34779f1 100644 --- a/admin/handler.go +++ b/admin/handler.go @@ -1746,55 +1746,61 @@ func (h *Handler) DeleteAPIKey(c *gin.Context) { // ==================== Settings ==================== type settingsResponse struct { - MaxConcurrency int `json:"max_concurrency"` - GlobalRPM int `json:"global_rpm"` - TestModel string `json:"test_model"` - TestConcurrency int `json:"test_concurrency"` - ProxyURL string `json:"proxy_url"` - PgMaxConns int `json:"pg_max_conns"` - RedisPoolSize int `json:"redis_pool_size"` - AutoCleanUnauthorized bool `json:"auto_clean_unauthorized"` - AutoCleanRateLimited bool `json:"auto_clean_rate_limited"` - AdminSecret string `json:"admin_secret"` - AdminAuthSource string `json:"admin_auth_source"` - AutoCleanFullUsage bool `json:"auto_clean_full_usage"` - AutoCleanError bool `json:"auto_clean_error"` - AutoCleanExpired bool `json:"auto_clean_expired"` - ProxyPoolEnabled bool `json:"proxy_pool_enabled"` - FastSchedulerEnabled bool `json:"fast_scheduler_enabled"` - MaxRetries int `json:"max_retries"` - AllowRemoteMigration bool `json:"allow_remote_migration"` - DatabaseDriver string `json:"database_driver"` - DatabaseLabel string `json:"database_label"` - CacheDriver string `json:"cache_driver"` - CacheLabel string `json:"cache_label"` - ExpiredCleaned int `json:"expired_cleaned,omitempty"` - ModelMapping string `json:"model_mapping"` - ResinURL string `json:"resin_url"` - ResinPlatformName string `json:"resin_platform_name"` + MaxConcurrency int `json:"max_concurrency"` + GlobalRPM int `json:"global_rpm"` + TestModel string `json:"test_model"` + TestConcurrency int `json:"test_concurrency"` + BackgroundRefreshIntervalMinutes int `json:"background_refresh_interval_minutes"` + UsageProbeMaxAgeMinutes int `json:"usage_probe_max_age_minutes"` + RecoveryProbeIntervalMinutes int `json:"recovery_probe_interval_minutes"` + ProxyURL string `json:"proxy_url"` + PgMaxConns int `json:"pg_max_conns"` + RedisPoolSize int `json:"redis_pool_size"` + AutoCleanUnauthorized bool `json:"auto_clean_unauthorized"` + AutoCleanRateLimited bool `json:"auto_clean_rate_limited"` + AdminSecret string `json:"admin_secret"` + AdminAuthSource string `json:"admin_auth_source"` + AutoCleanFullUsage bool `json:"auto_clean_full_usage"` + AutoCleanError bool `json:"auto_clean_error"` + AutoCleanExpired bool `json:"auto_clean_expired"` + ProxyPoolEnabled bool `json:"proxy_pool_enabled"` + FastSchedulerEnabled bool `json:"fast_scheduler_enabled"` + MaxRetries int `json:"max_retries"` + AllowRemoteMigration bool `json:"allow_remote_migration"` + DatabaseDriver string `json:"database_driver"` + DatabaseLabel string `json:"database_label"` + CacheDriver string `json:"cache_driver"` + CacheLabel string `json:"cache_label"` + ExpiredCleaned int `json:"expired_cleaned,omitempty"` + ModelMapping string `json:"model_mapping"` + ResinURL string `json:"resin_url"` + ResinPlatformName string `json:"resin_platform_name"` } type updateSettingsReq struct { - MaxConcurrency *int `json:"max_concurrency"` - GlobalRPM *int `json:"global_rpm"` - TestModel *string `json:"test_model"` - TestConcurrency *int `json:"test_concurrency"` - ProxyURL *string `json:"proxy_url"` - PgMaxConns *int `json:"pg_max_conns"` - RedisPoolSize *int `json:"redis_pool_size"` - AutoCleanUnauthorized *bool `json:"auto_clean_unauthorized"` - AutoCleanRateLimited *bool `json:"auto_clean_rate_limited"` - AdminSecret *string `json:"admin_secret"` - AutoCleanFullUsage *bool `json:"auto_clean_full_usage"` - AutoCleanError *bool `json:"auto_clean_error"` - AutoCleanExpired *bool `json:"auto_clean_expired"` - ProxyPoolEnabled *bool `json:"proxy_pool_enabled"` - FastSchedulerEnabled *bool `json:"fast_scheduler_enabled"` - MaxRetries *int `json:"max_retries"` - AllowRemoteMigration *bool `json:"allow_remote_migration"` - ModelMapping *string `json:"model_mapping"` - ResinURL *string `json:"resin_url"` - ResinPlatformName *string `json:"resin_platform_name"` + MaxConcurrency *int `json:"max_concurrency"` + GlobalRPM *int `json:"global_rpm"` + TestModel *string `json:"test_model"` + TestConcurrency *int `json:"test_concurrency"` + BackgroundRefreshIntervalMinutes *int `json:"background_refresh_interval_minutes"` + UsageProbeMaxAgeMinutes *int `json:"usage_probe_max_age_minutes"` + RecoveryProbeIntervalMinutes *int `json:"recovery_probe_interval_minutes"` + ProxyURL *string `json:"proxy_url"` + PgMaxConns *int `json:"pg_max_conns"` + RedisPoolSize *int `json:"redis_pool_size"` + AutoCleanUnauthorized *bool `json:"auto_clean_unauthorized"` + AutoCleanRateLimited *bool `json:"auto_clean_rate_limited"` + AdminSecret *string `json:"admin_secret"` + AutoCleanFullUsage *bool `json:"auto_clean_full_usage"` + AutoCleanError *bool `json:"auto_clean_error"` + AutoCleanExpired *bool `json:"auto_clean_expired"` + ProxyPoolEnabled *bool `json:"proxy_pool_enabled"` + FastSchedulerEnabled *bool `json:"fast_scheduler_enabled"` + MaxRetries *int `json:"max_retries"` + AllowRemoteMigration *bool `json:"allow_remote_migration"` + ModelMapping *string `json:"model_mapping"` + ResinURL *string `json:"resin_url"` + ResinPlatformName *string `json:"resin_platform_name"` } // GetSettings 获取当前系统设置 @@ -1813,31 +1819,34 @@ func (h *Handler) GetSettings(c *gin.Context) { resinPlatformName = dbSettings.ResinPlatformName } c.JSON(http.StatusOK, settingsResponse{ - MaxConcurrency: h.store.GetMaxConcurrency(), - GlobalRPM: h.rateLimiter.GetRPM(), - TestModel: h.store.GetTestModel(), - TestConcurrency: h.store.GetTestConcurrency(), - ProxyURL: h.store.GetProxyURL(), - PgMaxConns: h.pgMaxConns, - RedisPoolSize: h.redisPoolSize, - AutoCleanUnauthorized: h.store.GetAutoCleanUnauthorized(), - AutoCleanRateLimited: h.store.GetAutoCleanRateLimited(), - AdminSecret: adminSecret, - AdminAuthSource: adminAuthSource, - AutoCleanFullUsage: h.store.GetAutoCleanFullUsage(), - AutoCleanError: h.store.GetAutoCleanError(), - AutoCleanExpired: h.store.GetAutoCleanExpired(), - ProxyPoolEnabled: h.store.GetProxyPoolEnabled(), - FastSchedulerEnabled: h.store.FastSchedulerEnabled(), - MaxRetries: h.store.GetMaxRetries(), - AllowRemoteMigration: h.store.GetAllowRemoteMigration() && adminAuthSource != "disabled", - DatabaseDriver: h.databaseDriver, - DatabaseLabel: h.databaseLabel, - CacheDriver: h.cacheDriver, - CacheLabel: h.cacheLabel, - ModelMapping: h.store.GetModelMapping(), - ResinURL: resinURL, - ResinPlatformName: resinPlatformName, + MaxConcurrency: h.store.GetMaxConcurrency(), + GlobalRPM: h.rateLimiter.GetRPM(), + TestModel: h.store.GetTestModel(), + TestConcurrency: h.store.GetTestConcurrency(), + BackgroundRefreshIntervalMinutes: h.store.GetBackgroundRefreshIntervalMinutes(), + UsageProbeMaxAgeMinutes: h.store.GetUsageProbeMaxAgeMinutes(), + RecoveryProbeIntervalMinutes: h.store.GetRecoveryProbeIntervalMinutes(), + ProxyURL: h.store.GetProxyURL(), + PgMaxConns: h.pgMaxConns, + RedisPoolSize: h.redisPoolSize, + AutoCleanUnauthorized: h.store.GetAutoCleanUnauthorized(), + AutoCleanRateLimited: h.store.GetAutoCleanRateLimited(), + AdminSecret: adminSecret, + AdminAuthSource: adminAuthSource, + AutoCleanFullUsage: h.store.GetAutoCleanFullUsage(), + AutoCleanError: h.store.GetAutoCleanError(), + AutoCleanExpired: h.store.GetAutoCleanExpired(), + ProxyPoolEnabled: h.store.GetProxyPoolEnabled(), + FastSchedulerEnabled: h.store.FastSchedulerEnabled(), + MaxRetries: h.store.GetMaxRetries(), + AllowRemoteMigration: h.store.GetAllowRemoteMigration() && adminAuthSource != "disabled", + DatabaseDriver: h.databaseDriver, + DatabaseLabel: h.databaseLabel, + CacheDriver: h.cacheDriver, + CacheLabel: h.cacheLabel, + ModelMapping: h.store.GetModelMapping(), + ResinURL: resinURL, + ResinPlatformName: resinPlatformName, }) } @@ -1901,6 +1910,42 @@ func (h *Handler) UpdateSettings(c *gin.Context) { log.Printf("设置已更新: test_concurrency = %d", v) } + if req.BackgroundRefreshIntervalMinutes != nil { + v := *req.BackgroundRefreshIntervalMinutes + if v < 1 { + v = 1 + } + if v > 1440 { + v = 1440 + } + h.store.SetBackgroundRefreshInterval(time.Duration(v) * time.Minute) + log.Printf("设置已更新: background_refresh_interval_minutes = %d", v) + } + + if req.UsageProbeMaxAgeMinutes != nil { + v := *req.UsageProbeMaxAgeMinutes + if v < 1 { + v = 1 + } + if v > 10080 { + v = 10080 + } + h.store.SetUsageProbeMaxAge(time.Duration(v) * time.Minute) + log.Printf("设置已更新: usage_probe_max_age_minutes = %d", v) + } + + if req.RecoveryProbeIntervalMinutes != nil { + v := *req.RecoveryProbeIntervalMinutes + if v < 1 { + v = 1 + } + if v > 10080 { + v = 10080 + } + h.store.SetRecoveryProbeInterval(time.Duration(v) * time.Minute) + log.Printf("设置已更新: recovery_probe_interval_minutes = %d", v) + } + if req.ProxyURL != nil { h.store.SetProxyURL(*req.ProxyURL) log.Printf("设置已更新: proxy_url = %s", *req.ProxyURL) @@ -2027,26 +2072,29 @@ func (h *Handler) UpdateSettings(c *gin.Context) { // 持久化保存到数据库 err := h.db.UpdateSystemSettings(c.Request.Context(), &database.SystemSettings{ - MaxConcurrency: h.store.GetMaxConcurrency(), - GlobalRPM: h.rateLimiter.GetRPM(), - TestModel: h.store.GetTestModel(), - TestConcurrency: h.store.GetTestConcurrency(), - ProxyURL: h.store.GetProxyURL(), - PgMaxConns: h.pgMaxConns, - RedisPoolSize: h.redisPoolSize, - AutoCleanUnauthorized: h.store.GetAutoCleanUnauthorized(), - AutoCleanRateLimited: h.store.GetAutoCleanRateLimited(), - AdminSecret: currentAdminSecret, - AutoCleanFullUsage: h.store.GetAutoCleanFullUsage(), - AutoCleanError: h.store.GetAutoCleanError(), - AutoCleanExpired: h.store.GetAutoCleanExpired(), - ProxyPoolEnabled: h.store.GetProxyPoolEnabled(), - FastSchedulerEnabled: h.store.FastSchedulerEnabled(), - MaxRetries: h.store.GetMaxRetries(), - AllowRemoteMigration: h.store.GetAllowRemoteMigration() && hasAdminSecret, - ModelMapping: h.store.GetModelMapping(), - ResinURL: resinURL, - ResinPlatformName: resinPlatformName, + MaxConcurrency: h.store.GetMaxConcurrency(), + GlobalRPM: h.rateLimiter.GetRPM(), + TestModel: h.store.GetTestModel(), + TestConcurrency: h.store.GetTestConcurrency(), + BackgroundRefreshIntervalMinutes: h.store.GetBackgroundRefreshIntervalMinutes(), + UsageProbeMaxAgeMinutes: h.store.GetUsageProbeMaxAgeMinutes(), + RecoveryProbeIntervalMinutes: h.store.GetRecoveryProbeIntervalMinutes(), + ProxyURL: h.store.GetProxyURL(), + PgMaxConns: h.pgMaxConns, + RedisPoolSize: h.redisPoolSize, + AutoCleanUnauthorized: h.store.GetAutoCleanUnauthorized(), + AutoCleanRateLimited: h.store.GetAutoCleanRateLimited(), + AdminSecret: currentAdminSecret, + AutoCleanFullUsage: h.store.GetAutoCleanFullUsage(), + AutoCleanError: h.store.GetAutoCleanError(), + AutoCleanExpired: h.store.GetAutoCleanExpired(), + ProxyPoolEnabled: h.store.GetProxyPoolEnabled(), + FastSchedulerEnabled: h.store.FastSchedulerEnabled(), + MaxRetries: h.store.GetMaxRetries(), + AllowRemoteMigration: h.store.GetAllowRemoteMigration() && hasAdminSecret, + ModelMapping: h.store.GetModelMapping(), + ResinURL: resinURL, + ResinPlatformName: resinPlatformName, }) if err != nil { log.Printf("无法持久化保存设置: %v", err) @@ -2066,32 +2114,35 @@ func (h *Handler) UpdateSettings(c *gin.Context) { } c.JSON(http.StatusOK, settingsResponse{ - MaxConcurrency: h.store.GetMaxConcurrency(), - GlobalRPM: h.rateLimiter.GetRPM(), - TestModel: h.store.GetTestModel(), - TestConcurrency: h.store.GetTestConcurrency(), - ProxyURL: h.store.GetProxyURL(), - PgMaxConns: h.pgMaxConns, - RedisPoolSize: h.redisPoolSize, - AutoCleanUnauthorized: h.store.GetAutoCleanUnauthorized(), - AutoCleanRateLimited: h.store.GetAutoCleanRateLimited(), - AdminSecret: adminSecretForDisplay, - AdminAuthSource: adminAuthSource, - AutoCleanFullUsage: h.store.GetAutoCleanFullUsage(), - AutoCleanError: h.store.GetAutoCleanError(), - AutoCleanExpired: h.store.GetAutoCleanExpired(), - ProxyPoolEnabled: h.store.GetProxyPoolEnabled(), - FastSchedulerEnabled: h.store.FastSchedulerEnabled(), - MaxRetries: h.store.GetMaxRetries(), - AllowRemoteMigration: h.store.GetAllowRemoteMigration() && adminAuthSource != "disabled", - DatabaseDriver: h.databaseDriver, - DatabaseLabel: h.databaseLabel, - CacheDriver: h.cacheDriver, - CacheLabel: h.cacheLabel, - ExpiredCleaned: expiredCleaned, - ModelMapping: h.store.GetModelMapping(), - ResinURL: resinURL, - ResinPlatformName: resinPlatformName, + MaxConcurrency: h.store.GetMaxConcurrency(), + GlobalRPM: h.rateLimiter.GetRPM(), + TestModel: h.store.GetTestModel(), + TestConcurrency: h.store.GetTestConcurrency(), + BackgroundRefreshIntervalMinutes: h.store.GetBackgroundRefreshIntervalMinutes(), + UsageProbeMaxAgeMinutes: h.store.GetUsageProbeMaxAgeMinutes(), + RecoveryProbeIntervalMinutes: h.store.GetRecoveryProbeIntervalMinutes(), + ProxyURL: h.store.GetProxyURL(), + PgMaxConns: h.pgMaxConns, + RedisPoolSize: h.redisPoolSize, + AutoCleanUnauthorized: h.store.GetAutoCleanUnauthorized(), + AutoCleanRateLimited: h.store.GetAutoCleanRateLimited(), + AdminSecret: adminSecretForDisplay, + AdminAuthSource: adminAuthSource, + AutoCleanFullUsage: h.store.GetAutoCleanFullUsage(), + AutoCleanError: h.store.GetAutoCleanError(), + AutoCleanExpired: h.store.GetAutoCleanExpired(), + ProxyPoolEnabled: h.store.GetProxyPoolEnabled(), + FastSchedulerEnabled: h.store.FastSchedulerEnabled(), + MaxRetries: h.store.GetMaxRetries(), + AllowRemoteMigration: h.store.GetAllowRemoteMigration() && adminAuthSource != "disabled", + DatabaseDriver: h.databaseDriver, + DatabaseLabel: h.databaseLabel, + CacheDriver: h.cacheDriver, + CacheLabel: h.cacheLabel, + ExpiredCleaned: expiredCleaned, + ModelMapping: h.store.GetModelMapping(), + ResinURL: resinURL, + ResinPlatformName: resinPlatformName, }) } diff --git a/auth/store.go b/auth/store.go index 8f5756f..4f4e1e6 100644 --- a/auth/store.go +++ b/auth/store.go @@ -93,6 +93,12 @@ type Account struct { } +const ( + defaultBackgroundRefreshInterval = 2 * time.Minute + defaultUsageProbeMaxAge = 10 * time.Minute + defaultRecoveryProbeInterval = 30 * time.Minute +) + // SchedulerBreakdown 调度评分拆解 type SchedulerBreakdown struct { UnauthorizedPenalty float64 @@ -705,27 +711,31 @@ func (a *Account) GetLastUsedAt() time.Time { // Store 多账号管理器(数据库 + Token 缓存) type Store struct { - mu sync.RWMutex - accounts []*Account - globalProxy string - maxConcurrency int64 // 每账号最大并发数 - testConcurrency int64 // 批量测试并发数 - testModel atomic.Value // 测试连接使用的模型(string) - db *database.DB - tokenCache cache.TokenCache - usageProbeMu sync.RWMutex - usageProbe func(context.Context, *Account) error - usageProbeBatch atomic.Bool - recoveryProbeBatch atomic.Bool - autoCleanUnauthorized atomic.Bool - autoCleanRateLimited atomic.Bool - autoCleanFullUsage atomic.Bool - autoCleanError atomic.Bool - autoCleanExpired atomic.Bool - autoCleanupBatch atomic.Bool - maxRetries int64 // 请求失败最大重试次数(换号重试) - stopCh chan struct{} - wg sync.WaitGroup + mu sync.RWMutex + accounts []*Account + globalProxy string + maxConcurrency int64 // 每账号最大并发数 + testConcurrency int64 // 批量测试并发数 + testModel atomic.Value // 测试连接使用的模型(string) + db *database.DB + tokenCache cache.TokenCache + usageProbeMu sync.RWMutex + usageProbe func(context.Context, *Account) error + usageProbeBatch atomic.Bool + recoveryProbeBatch atomic.Bool + autoCleanUnauthorized atomic.Bool + autoCleanRateLimited atomic.Bool + autoCleanFullUsage atomic.Bool + autoCleanError atomic.Bool + autoCleanExpired atomic.Bool + autoCleanupBatch atomic.Bool + maxRetries int64 // 请求失败最大重试次数(换号重试) + backgroundRefreshInterval int64 // 后台刷新/探针巡检间隔(ns) + usageProbeMaxAge int64 // 用量探针快照最大缓存时长(ns) + recoveryProbeInterval int64 // 恢复探测最小间隔(ns) + backgroundRefreshWakeCh chan struct{} + stopCh chan struct{} + wg sync.WaitGroup // 代理池 proxyPool []string // 已启用的代理 URL 列表 @@ -775,23 +785,30 @@ func truthyEnv(v string) bool { func NewStore(db *database.DB, tc cache.TokenCache, settings *database.SystemSettings) *Store { if settings == nil { settings = &database.SystemSettings{ - MaxConcurrency: 2, - TestConcurrency: 50, - TestModel: "gpt-5.4", - ProxyURL: "", + MaxConcurrency: 2, + TestConcurrency: 50, + TestModel: "gpt-5.4", + BackgroundRefreshIntervalMinutes: 2, + UsageProbeMaxAgeMinutes: 10, + RecoveryProbeIntervalMinutes: 30, + ProxyURL: "", } } s := &Store{ - globalProxy: settings.ProxyURL, - maxConcurrency: int64(settings.MaxConcurrency), - testConcurrency: int64(settings.TestConcurrency), - db: db, - tokenCache: tc, - stopCh: make(chan struct{}), - proxyPoolEnabled: settings.ProxyPoolEnabled, - sessionBindings: make(map[string]sessionAffinity), + globalProxy: settings.ProxyURL, + maxConcurrency: int64(settings.MaxConcurrency), + testConcurrency: int64(settings.TestConcurrency), + db: db, + tokenCache: tc, + backgroundRefreshWakeCh: make(chan struct{}, 1), + stopCh: make(chan struct{}), + proxyPoolEnabled: settings.ProxyPoolEnabled, + sessionBindings: make(map[string]sessionAffinity), } s.testModel.Store(settings.TestModel) + s.SetBackgroundRefreshInterval(time.Duration(settings.BackgroundRefreshIntervalMinutes) * time.Minute) + s.SetUsageProbeMaxAge(time.Duration(settings.UsageProbeMaxAgeMinutes) * time.Minute) + s.SetRecoveryProbeInterval(time.Duration(settings.RecoveryProbeIntervalMinutes) * time.Minute) s.autoCleanUnauthorized.Store(settings.AutoCleanUnauthorized) s.autoCleanRateLimited.Store(settings.AutoCleanRateLimited) s.autoCleanFullUsage.Store(settings.AutoCleanFullUsage) @@ -1015,6 +1032,61 @@ func (s *Store) SetAutoCleanExpired(enabled bool) { s.autoCleanExpired.Store(enabled) } +// SetBackgroundRefreshInterval 设置后台刷新/探针巡检间隔。 +func (s *Store) SetBackgroundRefreshInterval(d time.Duration) { + if d <= 0 { + d = defaultBackgroundRefreshInterval + } + atomic.StoreInt64(&s.backgroundRefreshInterval, int64(d)) + select { + case s.backgroundRefreshWakeCh <- struct{}{}: + default: + } +} + +// GetBackgroundRefreshInterval 获取后台刷新/探针巡检间隔。 +func (s *Store) GetBackgroundRefreshInterval() time.Duration { + d := time.Duration(atomic.LoadInt64(&s.backgroundRefreshInterval)) + if d <= 0 { + return defaultBackgroundRefreshInterval + } + return d +} + +// SetUsageProbeMaxAge 设置用量探针最大缓存时长。 +func (s *Store) SetUsageProbeMaxAge(d time.Duration) { + if d <= 0 { + d = defaultUsageProbeMaxAge + } + atomic.StoreInt64(&s.usageProbeMaxAge, int64(d)) +} + +// GetUsageProbeMaxAge 获取用量探针最大缓存时长。 +func (s *Store) GetUsageProbeMaxAge() time.Duration { + d := time.Duration(atomic.LoadInt64(&s.usageProbeMaxAge)) + if d <= 0 { + return defaultUsageProbeMaxAge + } + return d +} + +// SetRecoveryProbeInterval 设置恢复探测最小间隔。 +func (s *Store) SetRecoveryProbeInterval(d time.Duration) { + if d <= 0 { + d = defaultRecoveryProbeInterval + } + atomic.StoreInt64(&s.recoveryProbeInterval, int64(d)) +} + +// GetRecoveryProbeInterval 获取恢复探测最小间隔。 +func (s *Store) GetRecoveryProbeInterval() time.Duration { + d := time.Duration(atomic.LoadInt64(&s.recoveryProbeInterval)) + if d <= 0 { + return defaultRecoveryProbeInterval + } + return d +} + // CleanExpiredNow 立即执行一次过期清理,返回清理数量 func (s *Store) CleanExpiredNow() int { return s.CleanExpiredAccounts(context.Background(), 30*time.Minute) @@ -1149,24 +1221,37 @@ func (s *Store) StartBackgroundRefresh() { s.wg.Add(1) go func() { defer s.wg.Done() - refreshTicker := time.NewTicker(2 * time.Minute) + refreshTimer := time.NewTimer(s.GetBackgroundRefreshInterval()) autoCleanupTicker := time.NewTicker(30 * time.Second) fullUsageCleanupTicker := time.NewTicker(5 * time.Minute) expiredCleanupTicker := time.NewTicker(15 * time.Minute) // 添加定时重建 FastScheduler 以优化性能 rebuildSchedulerTicker := time.NewTicker(10 * time.Minute) - defer refreshTicker.Stop() + defer refreshTimer.Stop() defer autoCleanupTicker.Stop() defer fullUsageCleanupTicker.Stop() defer expiredCleanupTicker.Stop() defer rebuildSchedulerTicker.Stop() + resetRefreshTimer := func() { + if !refreshTimer.Stop() { + select { + case <-refreshTimer.C: + default: + } + } + refreshTimer.Reset(s.GetBackgroundRefreshInterval()) + } + for { select { - case <-refreshTicker.C: + case <-refreshTimer.C: s.parallelRefreshAll(context.Background()) s.TriggerUsageProbeAsync() s.TriggerRecoveryProbeAsync() + refreshTimer.Reset(s.GetBackgroundRefreshInterval()) + case <-s.backgroundRefreshWakeCh: + resetRefreshTimer() case <-autoCleanupTicker.C: s.TriggerAutoCleanupAsync() case <-fullUsageCleanupTicker.C: @@ -1518,6 +1603,21 @@ func (s *Store) GetTestConcurrency() int { return int(atomic.LoadInt64(&s.testConcurrency)) } +// GetBackgroundRefreshIntervalMinutes 获取后台巡检间隔(分钟)。 +func (s *Store) GetBackgroundRefreshIntervalMinutes() int { + return int(s.GetBackgroundRefreshInterval() / time.Minute) +} + +// GetUsageProbeMaxAgeMinutes 获取用量探针最大缓存时长(分钟)。 +func (s *Store) GetUsageProbeMaxAgeMinutes() int { + return int(s.GetUsageProbeMaxAge() / time.Minute) +} + +// GetRecoveryProbeIntervalMinutes 获取恢复探测最小间隔(分钟)。 +func (s *Store) GetRecoveryProbeIntervalMinutes() int { + return int(s.GetRecoveryProbeInterval() / time.Minute) +} + // SetModelMapping 动态更新模型映射 JSON func (s *Store) SetModelMapping(mapping string) { s.modelMapping.Store(mapping) @@ -2008,7 +2108,7 @@ func (s *Store) parallelProbeUsage(ctx context.Context) { var wg sync.WaitGroup for _, acc := range accounts { - if !acc.NeedsUsageProbe(10 * time.Minute) { + if !acc.NeedsUsageProbe(s.GetUsageProbeMaxAge()) { continue } if !acc.TryBeginUsageProbe() { @@ -2050,7 +2150,7 @@ func (s *Store) parallelRecoveryProbe(ctx context.Context) { var wg sync.WaitGroup for _, acc := range accounts { - if !acc.NeedsRecoveryProbe(30 * time.Minute) { + if !acc.NeedsRecoveryProbe(s.GetRecoveryProbeInterval()) { continue } if !acc.TryBeginRecoveryProbe() { diff --git a/database/postgres.go b/database/postgres.go index 18f82e0..93f5a2d 100644 --- a/database/postgres.go +++ b/database/postgres.go @@ -253,18 +253,21 @@ func (db *DB) migrate(ctx context.Context) error { created_at TIMESTAMPTZ DEFAULT NOW() ); - CREATE TABLE IF NOT EXISTS system_settings ( - id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1), - max_concurrency INT DEFAULT 2, - global_rpm INT DEFAULT 0, - test_model VARCHAR(100) DEFAULT 'gpt-5.4', - test_concurrency INT DEFAULT 50, - proxy_url VARCHAR(500) DEFAULT '', - pg_max_conns INT DEFAULT 50, - redis_pool_size INT DEFAULT 30, - auto_clean_unauthorized BOOLEAN DEFAULT FALSE, - auto_clean_rate_limited BOOLEAN DEFAULT FALSE - ); + CREATE TABLE IF NOT EXISTS system_settings ( + id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1), + max_concurrency INT DEFAULT 2, + global_rpm INT DEFAULT 0, + test_model VARCHAR(100) DEFAULT 'gpt-5.4', + test_concurrency INT DEFAULT 50, + proxy_url VARCHAR(500) DEFAULT '', + pg_max_conns INT DEFAULT 50, + redis_pool_size INT DEFAULT 30, + auto_clean_unauthorized BOOLEAN DEFAULT FALSE, + auto_clean_rate_limited BOOLEAN DEFAULT FALSE, + background_refresh_interval_minutes INT DEFAULT 2, + usage_probe_max_age_minutes INT DEFAULT 10, + recovery_probe_interval_minutes INT DEFAULT 30 + ); ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS pg_max_conns INT DEFAULT 50; ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS redis_pool_size INT DEFAULT 30; ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS auto_clean_unauthorized BOOLEAN DEFAULT FALSE; @@ -275,13 +278,16 @@ func (db *DB) migrate(ctx context.Context) error { ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS fast_scheduler_enabled BOOLEAN DEFAULT FALSE; ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS max_retries INT DEFAULT 2; ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS allow_remote_migration BOOLEAN DEFAULT FALSE; - ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS auto_clean_error BOOLEAN DEFAULT FALSE; - ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS auto_clean_expired BOOLEAN DEFAULT FALSE; - ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS model_mapping TEXT DEFAULT '{}'; - ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS resin_url TEXT DEFAULT ''; - ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS resin_platform_name TEXT DEFAULT ''; - - CREATE TABLE IF NOT EXISTS proxies ( + ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS auto_clean_error BOOLEAN DEFAULT FALSE; + ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS auto_clean_expired BOOLEAN DEFAULT FALSE; + ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS model_mapping TEXT DEFAULT '{}'; + ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS background_refresh_interval_minutes INT DEFAULT 2; + ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS usage_probe_max_age_minutes INT DEFAULT 10; + ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS recovery_probe_interval_minutes INT DEFAULT 30; + ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS resin_url TEXT DEFAULT ''; + ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS resin_platform_name TEXT DEFAULT ''; + + CREATE TABLE IF NOT EXISTS proxies ( id SERIAL PRIMARY KEY, url VARCHAR(500) NOT NULL UNIQUE, label VARCHAR(255) DEFAULT '', @@ -382,26 +388,29 @@ func (db *DB) InsertAPIKey(ctx context.Context, name, key string) (int64, error) // SystemSettings 运行时设置项 type SystemSettings struct { - MaxConcurrency int - GlobalRPM int - TestModel string - TestConcurrency int - ProxyURL string - PgMaxConns int - RedisPoolSize int - AutoCleanUnauthorized bool - AutoCleanRateLimited bool - AdminSecret string - AutoCleanFullUsage bool - AutoCleanError bool - AutoCleanExpired bool - ProxyPoolEnabled bool - FastSchedulerEnabled bool - MaxRetries int - AllowRemoteMigration bool - ModelMapping string // JSON: {"anthropic_model": "codex_model", ...} - ResinURL string // Resin 代理池地址(含 Token),例如 http://127.0.0.1:2260/my-token - ResinPlatformName string // Resin 平台标识,例如 codex2api + MaxConcurrency int + GlobalRPM int + TestModel string + TestConcurrency int + ProxyURL string + PgMaxConns int + RedisPoolSize int + AutoCleanUnauthorized bool + AutoCleanRateLimited bool + AdminSecret string + AutoCleanFullUsage bool + AutoCleanError bool + AutoCleanExpired bool + ProxyPoolEnabled bool + FastSchedulerEnabled bool + MaxRetries int + AllowRemoteMigration bool + ModelMapping string // JSON: {"anthropic_model": "codex_model", ...} + BackgroundRefreshIntervalMinutes int + UsageProbeMaxAgeMinutes int + RecoveryProbeIntervalMinutes int + ResinURL string // Resin 代理池地址(含 Token),例如 http://127.0.0.1:2260/my-token + ResinPlatformName string // Resin 平台标识,例如 codex2api } // GetSystemSettings 加载全局设置 @@ -417,6 +426,9 @@ func (db *DB) GetSystemSettings(ctx context.Context) (*SystemSettings, error) { COALESCE(auto_clean_error, false), COALESCE(auto_clean_expired, false), COALESCE(model_mapping, '{}'), + COALESCE(background_refresh_interval_minutes, 2), + COALESCE(usage_probe_max_age_minutes, 10), + COALESCE(recovery_probe_interval_minutes, 30), COALESCE(resin_url, ''), COALESCE(resin_platform_name, '') FROM system_settings WHERE id = 1 @@ -425,6 +437,7 @@ func (db *DB) GetSystemSettings(ctx context.Context) (*SystemSettings, error) { &s.AutoCleanUnauthorized, &s.AutoCleanRateLimited, &s.AdminSecret, &s.AutoCleanFullUsage, &s.ProxyPoolEnabled, &s.FastSchedulerEnabled, &s.MaxRetries, &s.AllowRemoteMigration, &s.AutoCleanError, &s.AutoCleanExpired, &s.ModelMapping, + &s.BackgroundRefreshIntervalMinutes, &s.UsageProbeMaxAgeMinutes, &s.RecoveryProbeIntervalMinutes, &s.ResinURL, &s.ResinPlatformName, ) if err == sql.ErrNoRows { @@ -436,37 +449,42 @@ func (db *DB) GetSystemSettings(ctx context.Context) (*SystemSettings, error) { // UpdateSystemSettings 更新全局设置(upsert:无行时自动插入) func (db *DB) UpdateSystemSettings(ctx context.Context, s *SystemSettings) error { _, err := db.conn.ExecContext(ctx, ` - INSERT INTO system_settings ( - id, max_concurrency, global_rpm, test_model, test_concurrency, proxy_url, pg_max_conns, redis_pool_size, - auto_clean_unauthorized, auto_clean_rate_limited, admin_secret, auto_clean_full_usage, proxy_pool_enabled, - fast_scheduler_enabled, max_retries, allow_remote_migration, auto_clean_error, auto_clean_expired, model_mapping, - resin_url, resin_platform_name - ) - VALUES (1, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) - ON CONFLICT (id) DO UPDATE SET - max_concurrency = EXCLUDED.max_concurrency, - global_rpm = EXCLUDED.global_rpm, - test_model = EXCLUDED.test_model, - test_concurrency = EXCLUDED.test_concurrency, - proxy_url = EXCLUDED.proxy_url, + INSERT INTO system_settings ( + id, max_concurrency, global_rpm, test_model, test_concurrency, proxy_url, pg_max_conns, redis_pool_size, + auto_clean_unauthorized, auto_clean_rate_limited, admin_secret, auto_clean_full_usage, proxy_pool_enabled, + fast_scheduler_enabled, max_retries, allow_remote_migration, auto_clean_error, auto_clean_expired, model_mapping, + background_refresh_interval_minutes, usage_probe_max_age_minutes, recovery_probe_interval_minutes, + resin_url, resin_platform_name + ) + VALUES (1, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23) + ON CONFLICT (id) DO UPDATE SET + max_concurrency = EXCLUDED.max_concurrency, + global_rpm = EXCLUDED.global_rpm, + test_model = EXCLUDED.test_model, + test_concurrency = EXCLUDED.test_concurrency, + proxy_url = EXCLUDED.proxy_url, pg_max_conns = EXCLUDED.pg_max_conns, redis_pool_size = EXCLUDED.redis_pool_size, auto_clean_unauthorized = EXCLUDED.auto_clean_unauthorized, auto_clean_rate_limited = EXCLUDED.auto_clean_rate_limited, - admin_secret = EXCLUDED.admin_secret, - auto_clean_full_usage = EXCLUDED.auto_clean_full_usage, - proxy_pool_enabled = EXCLUDED.proxy_pool_enabled, - fast_scheduler_enabled = EXCLUDED.fast_scheduler_enabled, - max_retries = EXCLUDED.max_retries, - allow_remote_migration = EXCLUDED.allow_remote_migration, - auto_clean_error = EXCLUDED.auto_clean_error, - auto_clean_expired = EXCLUDED.auto_clean_expired, - model_mapping = EXCLUDED.model_mapping, - resin_url = EXCLUDED.resin_url, - resin_platform_name = EXCLUDED.resin_platform_name - `, s.MaxConcurrency, s.GlobalRPM, s.TestModel, s.TestConcurrency, s.ProxyURL, s.PgMaxConns, s.RedisPoolSize, + admin_secret = EXCLUDED.admin_secret, + auto_clean_full_usage = EXCLUDED.auto_clean_full_usage, + proxy_pool_enabled = EXCLUDED.proxy_pool_enabled, + fast_scheduler_enabled = EXCLUDED.fast_scheduler_enabled, + max_retries = EXCLUDED.max_retries, + allow_remote_migration = EXCLUDED.allow_remote_migration, + auto_clean_error = EXCLUDED.auto_clean_error, + auto_clean_expired = EXCLUDED.auto_clean_expired, + model_mapping = EXCLUDED.model_mapping, + background_refresh_interval_minutes = EXCLUDED.background_refresh_interval_minutes, + usage_probe_max_age_minutes = EXCLUDED.usage_probe_max_age_minutes, + recovery_probe_interval_minutes = EXCLUDED.recovery_probe_interval_minutes, + resin_url = EXCLUDED.resin_url, + resin_platform_name = EXCLUDED.resin_platform_name + `, s.MaxConcurrency, s.GlobalRPM, s.TestModel, s.TestConcurrency, s.ProxyURL, s.PgMaxConns, s.RedisPoolSize, s.AutoCleanUnauthorized, s.AutoCleanRateLimited, s.AdminSecret, s.AutoCleanFullUsage, s.ProxyPoolEnabled, s.FastSchedulerEnabled, s.MaxRetries, s.AllowRemoteMigration, s.AutoCleanError, s.AutoCleanExpired, s.ModelMapping, + s.BackgroundRefreshIntervalMinutes, s.UsageProbeMaxAgeMinutes, s.RecoveryProbeIntervalMinutes, s.ResinURL, s.ResinPlatformName) return err } diff --git a/database/sqlite.go b/database/sqlite.go index 666e9fb..064c77c 100644 --- a/database/sqlite.go +++ b/database/sqlite.go @@ -70,21 +70,24 @@ func (db *DB) migrateSQLite(ctx context.Context) error { created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );`, `CREATE TABLE IF NOT EXISTS system_settings ( - id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1), - max_concurrency INTEGER DEFAULT 2, - global_rpm INTEGER DEFAULT 0, - test_model TEXT DEFAULT 'gpt-5.4', - test_concurrency INTEGER DEFAULT 50, - proxy_url TEXT DEFAULT '', - pg_max_conns INTEGER DEFAULT 50, - redis_pool_size INTEGER DEFAULT 30, - auto_clean_unauthorized INTEGER DEFAULT 0, - auto_clean_rate_limited INTEGER DEFAULT 0, - admin_secret TEXT DEFAULT '', - auto_clean_full_usage INTEGER DEFAULT 0, - auto_clean_error INTEGER DEFAULT 0, - auto_clean_expired INTEGER DEFAULT 0, - proxy_pool_enabled INTEGER DEFAULT 0, + id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1), + max_concurrency INTEGER DEFAULT 2, + global_rpm INTEGER DEFAULT 0, + test_model TEXT DEFAULT 'gpt-5.4', + test_concurrency INTEGER DEFAULT 50, + proxy_url TEXT DEFAULT '', + pg_max_conns INTEGER DEFAULT 50, + redis_pool_size INTEGER DEFAULT 30, + auto_clean_unauthorized INTEGER DEFAULT 0, + auto_clean_rate_limited INTEGER DEFAULT 0, + background_refresh_interval_minutes INTEGER DEFAULT 2, + usage_probe_max_age_minutes INTEGER DEFAULT 10, + recovery_probe_interval_minutes INTEGER DEFAULT 30, + admin_secret TEXT DEFAULT '', + auto_clean_full_usage INTEGER DEFAULT 0, + auto_clean_error INTEGER DEFAULT 0, + auto_clean_expired INTEGER DEFAULT 0, + proxy_pool_enabled INTEGER DEFAULT 0, fast_scheduler_enabled INTEGER DEFAULT 0, max_retries INTEGER DEFAULT 2, allow_remote_migration INTEGER DEFAULT 0 @@ -137,6 +140,9 @@ func (db *DB) migrateSQLite(ctx context.Context) error { {"system_settings", "redis_pool_size", "INTEGER DEFAULT 30"}, {"system_settings", "auto_clean_unauthorized", "INTEGER DEFAULT 0"}, {"system_settings", "auto_clean_rate_limited", "INTEGER DEFAULT 0"}, + {"system_settings", "background_refresh_interval_minutes", "INTEGER DEFAULT 2"}, + {"system_settings", "usage_probe_max_age_minutes", "INTEGER DEFAULT 10"}, + {"system_settings", "recovery_probe_interval_minutes", "INTEGER DEFAULT 30"}, {"system_settings", "admin_secret", "TEXT DEFAULT ''"}, {"system_settings", "auto_clean_full_usage", "INTEGER DEFAULT 0"}, {"system_settings", "auto_clean_error", "INTEGER DEFAULT 0"}, diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index b61fa27..e8b0629 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -564,6 +564,12 @@ "testModelLabel": "Test Connection Model", "testModelHint": "Model used for account connection testing", "testConcurrencyRange": "Concurrency for batch testing (1~200)", + "backgroundRefreshInterval": "Background Sweep Interval", + "backgroundRefreshIntervalDesc": "Controls how often RT refresh, usage probe batches, and recovery probes are triggered, in minutes (1~1440). Saving resets the timer to the new interval.", + "usageProbeMaxAge": "Usage Probe Cache Age", + "usageProbeMaxAgeDesc": "Requests the responses endpoint again only after the usage snapshot is older than this many minutes (1~10080).", + "recoveryProbeInterval": "Recovery Probe Interval", + "recoveryProbeIntervalDesc": "Minimum gap between recovery probes for banned accounts, in minutes (1~10080).", "pgMaxConnsRange": "Max database connections (5~500, effective immediately)", "redisPoolSizeRange": "Redis pool size (5~500, requires restart)", "saveSettings": "Save Settings", diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index f45c665..e9b07e2 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -564,6 +564,12 @@ "testModelLabel": "测试连接模型", "testModelHint": "账号测试连接时使用的模型", "testConcurrencyRange": "批量测试连接时的并发数(范围 1~200)", + "backgroundRefreshInterval": "后台巡检间隔", + "backgroundRefreshIntervalDesc": "控制 RT 刷新、用量探针和恢复探测批次的触发频率,单位分钟(1~1440)。保存后按新间隔重置计时。", + "usageProbeMaxAge": "用量探针缓存时长", + "usageProbeMaxAgeDesc": "账号用量快照超过这个时长后,才会再次请求 responses 接口刷新用量,单位分钟(1~10080)。", + "recoveryProbeInterval": "恢复探测最小间隔", + "recoveryProbeIntervalDesc": "被封禁账号两次恢复探测之间的最短间隔,单位分钟(1~10080)。", "pgMaxConnsRange": "数据库最大连接数(范围 5~500,实时生效)", "redisPoolSizeRange": "Redis 连接池大小(范围 5~500,重启后生效)", "saveSettings": "保存设置", diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index a3d6893..78b2ada 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -132,6 +132,9 @@ export default function Settings() { global_rpm: 0, test_model: '', test_concurrency: 50, + background_refresh_interval_minutes: 2, + usage_probe_max_age_minutes: 10, + recovery_probe_interval_minutes: 30, pg_max_conns: 50, redis_pool_size: 30, auto_clean_unauthorized: false, @@ -489,6 +492,39 @@ export default function Settings() { />

{t('settings.testConcurrencyRange')}

+
+ + ) => setSettingsForm(f => ({ ...f, background_refresh_interval_minutes: parseInt(e.target.value) || 1 }))} + /> +

{t('settings.backgroundRefreshIntervalDesc')}

+
+
+ + ) => setSettingsForm(f => ({ ...f, usage_probe_max_age_minutes: parseInt(e.target.value) || 1 }))} + /> +

{t('settings.usageProbeMaxAgeDesc')}

+
+
+ + ) => setSettingsForm(f => ({ ...f, recovery_probe_interval_minutes: parseInt(e.target.value) || 1 }))} + /> +

{t('settings.recoveryProbeIntervalDesc')}

+
{showConnectionPool ? ( <> diff --git a/frontend/src/types.ts b/frontend/src/types.ts index ef1a82d..d9566c7 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -171,6 +171,9 @@ export interface SystemSettings { global_rpm: number test_model: string test_concurrency: number + background_refresh_interval_minutes: number + usage_probe_max_age_minutes: number + recovery_probe_interval_minutes: number proxy_url?: string pg_max_conns: number redis_pool_size: number diff --git a/main.go b/main.go index 907a356..0b6f305 100644 --- a/main.go +++ b/main.go @@ -61,20 +61,33 @@ func main() { // 初次运行,保存初始安全设置到数据库 log.Printf("初次运行,初始化系统默认设置...") settings = &database.SystemSettings{ - MaxConcurrency: 2, - GlobalRPM: 0, - TestModel: "gpt-5.4", - TestConcurrency: 50, - ProxyURL: "", - PgMaxConns: 50, - RedisPoolSize: 30, - AutoCleanUnauthorized: false, - AutoCleanRateLimited: false, + MaxConcurrency: 2, + GlobalRPM: 0, + TestModel: "gpt-5.4", + TestConcurrency: 50, + BackgroundRefreshIntervalMinutes: 2, + UsageProbeMaxAgeMinutes: 10, + RecoveryProbeIntervalMinutes: 30, + ProxyURL: "", + PgMaxConns: 50, + RedisPoolSize: 30, + AutoCleanUnauthorized: false, + AutoCleanRateLimited: false, } _ = db.UpdateSystemSettings(context.Background(), settings) } else if err != nil { log.Printf("警告: 读取系统设置失败: %v,将采用安全后备策略", err) - settings = &database.SystemSettings{MaxConcurrency: 2, GlobalRPM: 0, TestModel: "gpt-5.4", TestConcurrency: 50, PgMaxConns: 50, RedisPoolSize: 30} + settings = &database.SystemSettings{ + MaxConcurrency: 2, + GlobalRPM: 0, + TestModel: "gpt-5.4", + TestConcurrency: 50, + BackgroundRefreshIntervalMinutes: 2, + UsageProbeMaxAgeMinutes: 10, + RecoveryProbeIntervalMinutes: 30, + PgMaxConns: 50, + RedisPoolSize: 30, + } } else { log.Printf("已加载持久化业务设置: ProxyURL=%s, MaxConcurrency=%d, GlobalRPM=%d, PgMaxConns=%d, RedisPoolSize=%d", settings.ProxyURL, settings.MaxConcurrency, settings.GlobalRPM, settings.PgMaxConns, settings.RedisPoolSize)