diff --git a/src/providers/provider-pool-manager.js b/src/providers/provider-pool-manager.js index 9449576..cf2053b 100644 --- a/src/providers/provider-pool-manager.js +++ b/src/providers/provider-pool-manager.js @@ -29,7 +29,11 @@ export class ProviderPoolManager { // 使用 ?? 运算符确保 0 也能被正确设置,而不是被 || 替换为默认值 this.maxErrorCount = options.maxErrorCount ?? 3; // Default to 3 errors before marking unhealthy this.healthCheckInterval = options.healthCheckInterval ?? 10 * 60 * 1000; // Default to 10 minutes - + + // 账号池上限配置:每个 providerType 最多使用多少个健康凭证进行轮询 + // 0 或 undefined 表示不限制,使用所有健康凭证 + this.poolSizeLimit = options.globalConfig?.POOL_SIZE_LIMIT ?? 0; + // 日志级别控制 this.logLevel = options.logLevel || 'info'; // 'debug', 'info', 'warn', 'error' @@ -185,9 +189,20 @@ export class ProviderPoolManager { return null; } + // 账号池上限:如果配置了 poolSizeLimit,只使用 Top N 个健康凭证 + // 按 lastUsed 排序后取前 N 个,确保轮询范围受限 + let candidateProviders = availableAndHealthyProviders; + if (this.poolSizeLimit > 0 && availableAndHealthyProviders.length > this.poolSizeLimit) { + // 先按 usageCount 升序排序,取使用次数最少的 Top N 个作为候选池 + candidateProviders = [...availableAndHealthyProviders] + .sort((a, b) => (a.config.usageCount || 0) - (b.config.usageCount || 0)) + .slice(0, this.poolSizeLimit); + this._log('debug', `Pool size limited to ${this.poolSizeLimit}, using top ${candidateProviders.length} providers for ${providerType}`); + } + // 改进:使用"最久未被使用"策略(LRU)代替取模轮询 // 这样即使可用列表长度动态变化,也能确保每个账号被平均轮到 - const selected = availableAndHealthyProviders.sort((a, b) => { + const selected = candidateProviders.sort((a, b) => { const timeA = a.config.lastUsed ? new Date(a.config.lastUsed).getTime() : 0; const timeB = b.config.lastUsed ? new Date(b.config.lastUsed).getTime() : 0; // 优先选择从未用过的,或者最久没用的 diff --git a/src/ui-modules/config-api.js b/src/ui-modules/config-api.js index 1ddfdfd..0438137 100644 --- a/src/ui-modules/config-api.js +++ b/src/ui-modules/config-api.js @@ -88,6 +88,7 @@ export async function handleUpdateConfig(req, res, currentConfig) { if (newConfig.CRON_REFRESH_TOKEN !== undefined) currentConfig.CRON_REFRESH_TOKEN = newConfig.CRON_REFRESH_TOKEN; if (newConfig.PROVIDER_POOLS_FILE_PATH !== undefined) currentConfig.PROVIDER_POOLS_FILE_PATH = newConfig.PROVIDER_POOLS_FILE_PATH; if (newConfig.MAX_ERROR_COUNT !== undefined) currentConfig.MAX_ERROR_COUNT = newConfig.MAX_ERROR_COUNT; + if (newConfig.POOL_SIZE_LIMIT !== undefined) currentConfig.POOL_SIZE_LIMIT = newConfig.POOL_SIZE_LIMIT; if (newConfig.providerFallbackChain !== undefined) currentConfig.providerFallbackChain = newConfig.providerFallbackChain; if (newConfig.modelFallbackMapping !== undefined) currentConfig.modelFallbackMapping = newConfig.modelFallbackMapping; diff --git a/src/utils/credential-cache-manager.js b/src/utils/credential-cache-manager.js index bb44e67..8c29052 100644 --- a/src/utils/credential-cache-manager.js +++ b/src/utils/credential-cache-manager.js @@ -100,6 +100,12 @@ export class CredentialCacheManager { const existingPid = await fs.readFile(pidPath, 'utf8'); const pid = parseInt(existingPid.trim(), 10); + // 如果是当前进程的 PID,说明是配置重载,直接返回(已持有锁) + if (pid === process.pid) { + console.log(`[CredentialCache] Instance lock already held by current process (PID: ${pid})`); + return; + } + // 检查进程是否还在运行 try { process.kill(pid, 0); // 0 信号仅检查进程存在性 diff --git a/static/app/config-manager.js b/static/app/config-manager.js index a669f85..6e56edc 100644 --- a/static/app/config-manager.js +++ b/static/app/config-manager.js @@ -99,6 +99,10 @@ async function loadConfiguration() { if (cronRefreshTokenEl) cronRefreshTokenEl.checked = data.CRON_REFRESH_TOKEN || false; if (providerPoolsFilePathEl) providerPoolsFilePathEl.value = data.PROVIDER_POOLS_FILE_PATH; if (maxErrorCountEl) maxErrorCountEl.value = data.MAX_ERROR_COUNT || 3; + + // 账号池轮询上限 + const poolSizeLimitEl = document.getElementById('poolSizeLimit'); + if (poolSizeLimitEl) poolSizeLimitEl.value = data.POOL_SIZE_LIMIT || 0; // 加载 Fallback 链配置 if (providerFallbackChainEl) { @@ -197,6 +201,7 @@ async function saveConfiguration() { config.CRON_REFRESH_TOKEN = document.getElementById('cronRefreshToken')?.checked || false; config.PROVIDER_POOLS_FILE_PATH = document.getElementById('providerPoolsFilePath')?.value || ''; config.MAX_ERROR_COUNT = parseInt(document.getElementById('maxErrorCount')?.value || 3); + config.POOL_SIZE_LIMIT = parseInt(document.getElementById('poolSizeLimit')?.value || 0); // 保存 Fallback 链配置 const fallbackChainValue = document.getElementById('providerFallbackChain')?.value?.trim() || ''; diff --git a/static/app/i18n.js b/static/app/i18n.js index cb472a9..eba443c 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -285,6 +285,9 @@ const translations = { 'config.advanced.maxErrorCount': '提供商最大错误次数', 'config.advanced.maxErrorCountPlaceholder': '默认: 3', 'config.advanced.maxErrorCountNote': '提供商连续错误达到此次数后将被标记为不健康,默认为 3 次', + 'config.advanced.poolSizeLimit': '账号池轮询上限', + 'config.advanced.poolSizeLimitPlaceholder': '默认: 0 (不限制)', + 'config.advanced.poolSizeLimitNote': '每个提供商类型参与轮询的最大健康凭证数量,0 表示不限制,使用所有健康凭证', 'config.advanced.credentialSwitchMaxRetries': '坏凭证切换最大重试次数', 'config.advanced.credentialSwitchMaxRetriesNote': '认证错误(401/403)后切换凭证的最大重试次数,默认 5 次', 'config.advanced.fallbackChain': '跨类型 Fallback 链配置', @@ -847,6 +850,9 @@ const translations = { 'config.advanced.maxErrorCount': 'Provider Max Error Count', 'config.advanced.maxErrorCountPlaceholder': 'Default: 3', 'config.advanced.maxErrorCountNote': 'Provider will be marked as unhealthy after consecutive errors reach this count, default is 3', + 'config.advanced.poolSizeLimit': 'Pool Size Limit', + 'config.advanced.poolSizeLimitPlaceholder': 'Default: 0 (no limit)', + 'config.advanced.poolSizeLimitNote': 'Maximum number of healthy credentials per provider type for rotation. 0 means no limit, use all healthy credentials', 'config.advanced.credentialSwitchMaxRetries': 'Credential Switch Max Retries', 'config.advanced.credentialSwitchMaxRetriesNote': 'Maximum retries for switching credentials after authentication errors (401/403), default is 5', 'config.advanced.fallbackChain': 'Cross-Type Fallback Chain Config', diff --git a/static/components/section-config.html b/static/components/section-config.html index 1f6637e..b907499 100644 --- a/static/components/section-config.html +++ b/static/components/section-config.html @@ -195,6 +195,12 @@ 提供商连续错误达到此次数后将被标记为不健康,默认为 3 次 + +