From b60593085edb71290aa94652533dec1dbac1cdf7 Mon Sep 17 00:00:00 2001 From: hex2077 Date: Sat, 30 Aug 2025 14:02:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(provider):=20=E6=B7=BB=E5=8A=A0=E5=81=A5?= =?UTF-8?q?=E5=BA=B7=E7=8A=B6=E6=80=81=E8=B7=9F=E8=B8=AA=E5=B9=B6=E6=8C=81?= =?UTF-8?q?=E4=B9=85=E5=8C=96=E5=88=B0JSON=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加provider健康状态跟踪功能,包括使用次数、错误计数和最后使用时间 将provider状态持久化到JSON文件,确保重启后状态不丢失 重构provider选择逻辑,将状态管理移至config对象 --- provider_pools.json | 60 ++++++++++++++++++---- src/provider-pool-manager.js | 97 ++++++++++++++++++++++++++++-------- 2 files changed, 125 insertions(+), 32 deletions(-) diff --git a/provider_pools.json b/provider_pools.json index 5ac445b..40310f7 100644 --- a/provider_pools.json +++ b/provider_pools.json @@ -3,46 +3,86 @@ { "OPENAI_API_KEY": "sk-openai-key1", "OPENAI_BASE_URL": "https://api.openai.com/v1", - "uuid": "2f579c65-d3c5-41b1-9985-9f6e3d7bf39c" + "uuid": "2f579c65-d3c5-41b1-9985-9f6e3d7bf39c", + "isHealthy": true, + "lastUsed": null, + "usageCount": 0, + "errorCount": 0, + "lastErrorTime": null }, { "OPENAI_API_KEY": "sk-openai-key2", "OPENAI_BASE_URL": "https://api.openai.com/v1", - "uuid": "e284628d-302f-456d-91f3-6095386fb3b8" + "uuid": "e284628d-302f-456d-91f3-6095386fb3b8", + "isHealthy": true, + "lastUsed": null, + "usageCount": 0, + "errorCount": 0, + "lastErrorTime": null } ], - "gemini-cli": [ + "gemini-cli-oauth": [ { "GEMINI_OAUTH_CREDS_FILE_PATH": "./credentials1.json", "PROJECT_ID": "your-project-id-1", - "uuid": "ac200154-26b8-4f5f-8650-e8cc738b06e3" + "uuid": "ac200154-26b8-4f5f-8650-e8cc738b06e3", + "isHealthy": true, + "lastUsed": null, + "usageCount": 0, + "errorCount": 0, + "lastErrorTime": null }, { "GEMINI_OAUTH_CREDS_FILE_PATH": "./credentials2.json", "PROJECT_ID": "your-project-id-2", - "uuid": "4f8afcc2-a9bb-4b96-bb50-3b9667a71f54" + "uuid": "4f8afcc2-a9bb-4b96-bb50-3b9667a71f54", + "isHealthy": true, + "lastUsed": null, + "usageCount": 0, + "errorCount": 0, + "lastErrorTime": null } ], "claude-custom": [ { "CLAUDE_API_KEY": "sk-claude-key1", "CLAUDE_BASE_URL": "https://api.anthropic.com", - "uuid": "bb87047a-3b1d-4249-adbb-1087ecd58128" + "uuid": "bb87047a-3b1d-4249-adbb-1087ecd58128", + "isHealthy": true, + "lastUsed": null, + "usageCount": 0, + "errorCount": 0, + "lastErrorTime": null }, { "CLAUDE_API_KEY": "sk-claude-key2", "CLAUDE_BASE_URL": "https://api.anthropic.com", - "uuid": "7c2002c6-122a-4db0-af06-8a0ff433801a" + "uuid": "7c2002c6-122a-4db0-af06-8a0ff433801a", + "isHealthy": true, + "lastUsed": null, + "usageCount": 0, + "errorCount": 0, + "lastErrorTime": null } ], - "kiro-api": [ + "claude-kiro-oauth": [ { "KIRO_OAUTH_CREDS_FILE_PATH": "./kiro_creds1.json", - "uuid": "2c69d0ac-b86f-43d8-9d17-0d300afc5cfd" + "uuid": "2c69d0ac-b86f-43d8-9d17-0d300afc5cfd", + "isHealthy": true, + "lastUsed": null, + "usageCount": 0, + "errorCount": 0, + "lastErrorTime": null }, { "KIRO_OAUTH_CREDS_FILE_PATH": "./kiro_creds2.json", - "uuid": "7482abe6-8083-4288-bb7d-d8ecb7c461e2" + "uuid": "7482abe6-8083-4288-bb7d-d8ecb7c461e2", + "isHealthy": true, + "lastUsed": null, + "usageCount": 0, + "errorCount": 0, + "lastErrorTime": null } ] } \ No newline at end of file diff --git a/src/provider-pool-manager.js b/src/provider-pool-manager.js index 2e122fb..bd8ff60 100644 --- a/src/provider-pool-manager.js +++ b/src/provider-pool-manager.js @@ -1,3 +1,5 @@ +import * as fs from 'fs'; // Import fs module + /** * Manages a pool of API service providers, handling their health and selection. */ @@ -19,15 +21,21 @@ export class ProviderPoolManager { for (const providerType in this.providerPools) { this.providerStatus[providerType] = []; this.roundRobinIndex[providerType] = 0; // Initialize round-robin index for each type - this.providerPools[providerType].forEach((providerConfig, index) => { + this.providerPools[providerType].forEach((providerConfig) => { + // Ensure initial health and usage stats are present in the config + providerConfig.isHealthy = providerConfig.isHealthy !== undefined ? providerConfig.isHealthy : true; + providerConfig.lastUsed = providerConfig.lastUsed !== undefined ? providerConfig.lastUsed : null; + providerConfig.usageCount = providerConfig.usageCount !== undefined ? providerConfig.usageCount : 0; + providerConfig.errorCount = providerConfig.errorCount !== undefined ? providerConfig.errorCount : 0; + if (providerConfig.lastErrorTime && typeof providerConfig.lastErrorTime === 'string') { + providerConfig.lastErrorTime = new Date(providerConfig.lastErrorTime); + } else if (providerConfig.lastErrorTime === undefined) { + providerConfig.lastErrorTime = null; + } + this.providerStatus[providerType].push({ config: providerConfig, - uuid: providerConfig.uuid, - isHealthy: true, - lastUsed: null, - usageCount: 0, - errorCount: 0, - lastErrorTime: null, // New: Timestamp of the last error + uuid: providerConfig.uuid, // Still keep uuid at the top level for easy access }); }); } @@ -42,7 +50,7 @@ export class ProviderPoolManager { */ selectProvider(providerType) { const availableProviders = this.providerStatus[providerType] || []; - const healthyProviders = availableProviders.filter(p => p.isHealthy); + const healthyProviders = availableProviders.filter(p => p.config.isHealthy); if (healthyProviders.length === 0) { console.warn(`[ProviderPoolManager] No healthy providers available for type: ${providerType}`); @@ -65,9 +73,11 @@ export class ProviderPoolManager { } if (selected) { - selected.lastUsed = new Date(); - selected.usageCount++; // Increment usage count + selected.config.lastUsed = new Date(); + selected.config.usageCount++; // Increment usage count + console.log(`[ProviderPoolManager] Selected provider for ${providerType} (round-robin): ${JSON.stringify(selected.config)}`); + this._saveProviderPoolsToJson(providerType); // Persist changes return selected.config; } @@ -84,15 +94,16 @@ export class ProviderPoolManager { if (pool) { const provider = pool.find(p => p.uuid === providerConfig.uuid); if (provider) { - provider.errorCount++; - provider.lastErrorTime = new Date(); // Update last error time + provider.config.errorCount++; + provider.config.lastErrorTime = new Date(); // Update last error time in config - if (provider.errorCount >= this.maxErrorCount) { - provider.isHealthy = false; - console.warn(`[ProviderPoolManager] Marked provider as unhealthy: ${JSON.stringify(providerConfig)} for type ${providerType}. Total errors: ${provider.errorCount}`); + if (provider.config.errorCount >= this.maxErrorCount) { + provider.config.isHealthy = false; + console.warn(`[ProviderPoolManager] Marked provider as unhealthy: ${JSON.stringify(providerConfig)} for type ${providerType}. Total errors: ${provider.config.errorCount}`); } else { - console.warn(`[ProviderPoolManager] Provider ${JSON.stringify(providerConfig)} for type ${providerType} error count: ${provider.errorCount}/${this.maxErrorCount}. Still healthy.`); + console.warn(`[ProviderPoolManager] Provider ${JSON.stringify(providerConfig)} for type ${providerType} error count: ${provider.config.errorCount}/${this.maxErrorCount}. Still healthy.`); } + this._saveProviderPoolsToJson(providerType); // Persist changes } } } @@ -105,11 +116,13 @@ export class ProviderPoolManager { markProviderHealthy(providerType, providerConfig) { const pool = this.providerStatus[providerType]; if (pool) { - const provider = pool.find(p => p.uuid === providerConfig.uuid);a + const provider = pool.find(p => p.uuid === providerConfig.uuid); if (provider) { - provider.isHealthy = true; - provider.errorCount = 0; // Reset error count on health recovery + provider.config.isHealthy = true; + provider.config.errorCount = 0; // Reset error count on health recovery + provider.config.lastErrorTime = null; // Reset lastErrorTime when healthy console.log(`[ProviderPoolManager] Marked provider as healthy: ${JSON.stringify(providerConfig)} for type ${providerType}`); + this._saveProviderPoolsToJson(providerType); // Persist changes } } } @@ -126,8 +139,8 @@ export class ProviderPoolManager { const providerConfig = providerStatus.config; // Only attempt to health check unhealthy providers after a certain interval - if (!providerStatus.isHealthy && providerStatus.lastErrorTime && - (now.getTime() - providerStatus.lastErrorTime.getTime() < this.healthCheckInterval)) { + if (!providerStatus.config.isHealthy && providerStatus.config.lastErrorTime && + (now.getTime() - providerStatus.config.lastErrorTime.getTime() < this.healthCheckInterval)) { console.log(`[ProviderPoolManager] Skipping health check for ${JSON.stringify(providerConfig)} (${providerType}). Last error too recent.`); continue; } @@ -137,7 +150,7 @@ export class ProviderPoolManager { // For now, if a provider was unhealthy and enough time has passed, // we optimistically mark it healthy and reset error count. // A more robust system would involve actual API calls or pings here. - if (!providerStatus.isHealthy) { + if (!providerStatus.config.isHealthy) { // Only reset and mark healthy if it was unhealthy and we are attempting a check after interval this.markProviderHealthy(providerType, providerConfig); console.log(`[ProviderPoolManager] Health check for ${JSON.stringify(providerConfig)} (${providerType}): Marked Healthy (re-evaluation)`); @@ -154,4 +167,44 @@ export class ProviderPoolManager { } } } + /** + * Saves the current provider pools configuration to the JSON file. + * @private + */ + async _saveProviderPoolsToJson(providerTypeToUpdate) { + try { + const filePath = 'provider_pools.json'; + let currentPools = {}; + try { + const fileContent = await fs.promises.readFile(filePath, 'utf8'); + currentPools = JSON.parse(fileContent); + } catch (readError) { + if (readError.code === 'ENOENT') { + console.log('[ProviderPoolManager] provider_pools.json does not exist, creating new file.'); + } else { + throw readError; + } + } + + if (this.providerStatus[providerTypeToUpdate]) { + currentPools[providerTypeToUpdate] = this.providerStatus[providerTypeToUpdate].map(p => { + if (p.config.lastUsed instanceof Date) { + p.config.lastUsed = p.config.lastUsed.toISOString(); + } + if (p.config.lastErrorTime instanceof Date) { + p.config.lastErrorTime = p.config.lastErrorTime.toISOString(); + } + return p.config; + }); + } else { + console.warn(`[ProviderPoolManager] Attempted to save unknown providerType: ${providerTypeToUpdate}`); + } + + await fs.promises.writeFile(filePath, JSON.stringify(currentPools, null, 2), 'utf8'); + console.log(`[ProviderPoolManager] provider_pools.json for ${providerTypeToUpdate} updated successfully.`); + } catch (error) { + console.error('[ProviderPoolManager] Failed to write provider_pools.json:', error); + } + } + } \ No newline at end of file