From 1c22b8c5c97c83fbffdb56270e5462cd6325d0ea Mon Sep 17 00:00:00 2001 From: hex2077 Date: Tue, 25 Nov 2025 16:02:30 +0800 Subject: [PATCH] =?UTF-8?q?refactor(provider-pool):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E8=80=85=E6=B1=A0=E7=AE=A1=E7=90=86=E5=99=A8?= =?UTF-8?q?=E7=9A=84=E6=97=A5=E5=BF=97=E5=92=8C=E5=81=A5=E5=BA=B7=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加日志级别控制和统一的日志输出方法 - 重构健康检查逻辑,使用默认模型配置和标准化的请求构建 - 优化批量保存机制,减少文件I/O操作 - 添加参数校验和错误处理 - 重命名方法以更准确表达其功能 --- src/provider-pool-manager.js | 394 +++++++++++++++++++---------------- 1 file changed, 220 insertions(+), 174 deletions(-) diff --git a/src/provider-pool-manager.js b/src/provider-pool-manager.js index 6282f10..8cbc326 100644 --- a/src/provider-pool-manager.js +++ b/src/provider-pool-manager.js @@ -6,15 +6,28 @@ import { MODEL_PROVIDER } from './common.js'; * Manages a pool of API service providers, handling their health and selection. */ export class ProviderPoolManager { + // 默认健康检查模型配置 + static DEFAULT_HEALTH_CHECK_MODELS = { + 'gemini-cli': 'gemini-2.5-flash', + 'openai-custom': 'gpt-3.5-turbo', + 'claude-custom': 'claude-3-7-sonnet-20250219', + 'kiro-api': 'claude-3-7-sonnet-20250219', + 'qwen-api': 'qwen3-coder-flash', + 'openai-custom-responses': 'gpt-5-low' + }; + constructor(providerPools, options = {}) { this.providerPools = providerPools; this.globalConfig = options.globalConfig || {}; // 存储全局配置 this.providerStatus = {}; // Tracks health and usage for each provider instance this.roundRobinIndex = {}; // Tracks the current index for round-robin selection for each provider type - this.maxErrorCount = options.maxErrorCount || 3; // Default to 1 errors before marking unhealthy + this.maxErrorCount = options.maxErrorCount || 3; // Default to 3 errors before marking unhealthy this.healthCheckInterval = options.healthCheckInterval || 10 * 60 * 1000; // Default to 10 minutes - // 优化1: 添加防抖机制,避免频繁的文件 I/O 操作 + // 日志级别控制 + this.logLevel = options.logLevel || 'info'; // 'debug', 'info', 'warn', 'error' + + // 添加防抖机制,避免频繁的文件 I/O 操作 this.saveDebounceTime = options.saveDebounceTime || 1000; // 默认1秒防抖 this.saveTimer = null; this.pendingSaves = new Set(); // 记录待保存的 providerType @@ -22,6 +35,31 @@ export class ProviderPoolManager { this.initializeProviderStatus(); } + /** + * 日志输出方法,支持日志级别控制 + * @private + */ + _log(level, message) { + const levels = { debug: 0, info: 1, warn: 2, error: 3 }; + if (levels[level] >= levels[this.logLevel]) { + const logMethod = level === 'debug' ? 'log' : level; + console[logMethod](`[ProviderPoolManager] ${message}`); + } + } + + /** + * 查找指定的 provider + * @private + */ + _findProvider(providerType, uuid) { + if (!providerType || !uuid) { + this._log('error', `Invalid parameters: providerType=${providerType}, uuid=${uuid}`); + return null; + } + const pool = this.providerStatus[providerType]; + return pool?.find(p => p.uuid === uuid) || null; + } + /** * Initializes the status for each provider in the pools. * Initially, all providers are considered healthy and have zero usage. @@ -49,7 +87,7 @@ export class ProviderPoolManager { }); }); } - console.log('[ProviderPoolManager] Initialized provider statuses: ok'); + this._log('info', 'Initialized provider statuses: ok'); } /** @@ -59,17 +97,23 @@ export class ProviderPoolManager { * @returns {object|null} The selected provider's configuration, or null if no healthy provider is found. */ selectProvider(providerType) { + // 参数校验 + if (!providerType || typeof providerType !== 'string') { + this._log('error', `Invalid providerType: ${providerType}`); + return null; + } + const availableProviders = this.providerStatus[providerType] || []; const availableAndHealthyProviders = availableProviders.filter(p => p.config.isHealthy && !p.config.isDisabled ); if (availableAndHealthyProviders.length === 0) { - console.warn(`[ProviderPoolManager] No available and healthy providers for type: ${providerType}`); + this._log('warn', `No available and healthy providers for type: ${providerType}`); return null; } - // 优化3: 简化轮询逻辑,移除不必要的循环 + // 简化轮询逻辑 const currentIndex = this.roundRobinIndex[providerType] || 0; const providerIndex = currentIndex % availableAndHealthyProviders.length; const selected = availableAndHealthyProviders[providerIndex]; @@ -81,9 +125,9 @@ export class ProviderPoolManager { selected.config.lastUsed = new Date().toISOString(); selected.config.usageCount++; - console.log(`[ProviderPoolManager] Selected provider for ${providerType} (round-robin): ${JSON.stringify(selected.config)}`); + this._log('debug', `Selected provider for ${providerType} (round-robin): ${selected.config.uuid}`); - // 优化1: 使用防抖保存 + // 使用防抖保存 this._debouncedSave(providerType); return selected.config; @@ -95,23 +139,24 @@ export class ProviderPoolManager { * @param {object} providerConfig - The configuration of the provider to mark. */ markProviderUnhealthy(providerType, providerConfig) { - const pool = this.providerStatus[providerType]; - if (pool) { - const provider = pool.find(p => p.uuid === providerConfig.uuid); - if (provider) { - provider.config.errorCount++; - provider.config.lastErrorTime = new Date().toISOString(); // Update last error time in config + if (!providerConfig?.uuid) { + this._log('error', 'Invalid providerConfig in markProviderUnhealthy'); + return; + } - if (provider.config.errorCount >= this.maxErrorCount) { - provider.config.isHealthy = false; - console.warn(`[ProviderPoolManager] Marked provider as unhealthy: ${providerConfig.uuid} for type ${providerType}. Total errors: ${provider.config.errorCount}`); - } else { - console.warn(`[ProviderPoolManager] Provider ${providerConfig.uuid} for type ${providerType} error count: ${provider.config.errorCount}/${this.maxErrorCount}. Still healthy.`); - } - - // 优化1: 使用防抖保存 - this._debouncedSave(providerType); + const provider = this._findProvider(providerType, providerConfig.uuid); + if (provider) { + provider.config.errorCount++; + provider.config.lastErrorTime = new Date().toISOString(); + + if (provider.config.errorCount >= this.maxErrorCount) { + provider.config.isHealthy = false; + this._log('warn', `Marked provider as unhealthy: ${providerConfig.uuid} for type ${providerType}. Total errors: ${provider.config.errorCount}`); + } else { + this._log('warn', `Provider ${providerConfig.uuid} for type ${providerType} error count: ${provider.config.errorCount}/${this.maxErrorCount}. Still healthy.`); } + + this._debouncedSave(providerType); } } @@ -119,43 +164,46 @@ export class ProviderPoolManager { * Marks a provider as healthy. * @param {string} providerType - The type of the provider. * @param {object} providerConfig - The configuration of the provider to mark. + * @param {boolean} isInit - Whether to reset usage count (optional, default: false). */ - markProviderHealthy(isInit, providerType, providerConfig) { - const pool = this.providerStatus[providerType]; - if (pool) { - const provider = pool.find(p => p.uuid === providerConfig.uuid); - if (provider) { - provider.config.isHealthy = true; - provider.config.errorCount = 0; // Reset error count on health recovery - provider.config.lastErrorTime = null; // Reset lastErrorTime when healthy - if (isInit) { - provider.config.usageCount = 0; // Reset usage count on health recovery - } - console.log(`[ProviderPoolManager] Marked provider as healthy: ${provider.config.uuid} for type ${providerType}`); - - // 优化1: 使用防抖保存 - this._debouncedSave(providerType); + markProviderHealthy(providerType, providerConfig, isInit = false) { + if (!providerConfig?.uuid) { + this._log('error', 'Invalid providerConfig in markProviderHealthy'); + return; + } + + const provider = this._findProvider(providerType, providerConfig.uuid); + if (provider) { + provider.config.isHealthy = true; + provider.config.errorCount = 0; + provider.config.lastErrorTime = null; + if (isInit) { + provider.config.usageCount = 0; } + this._log('info', `Marked provider as healthy: ${provider.config.uuid} for type ${providerType}`); + + this._debouncedSave(providerType); } } - /** - * Marks a provider as healthy. + /** + * 重置提供商的计数器(错误计数和使用计数) * @param {string} providerType - The type of the provider. * @param {object} providerConfig - The configuration of the provider to mark. */ - markProviderZero(providerType, providerConfig) { - const pool = this.providerStatus[providerType]; - if (pool) { - const provider = pool.find(p => p.uuid === providerConfig.uuid); - if (provider) { - provider.config.errorCount = 0; // Reset error count on health recovery - provider.config.usageCount = 0; // Reset usage count on health recovery - console.log(`[ProviderPoolManager] Marked provider as zero: ${provider.config.uuid} for type ${providerType}`); - - // 优化1: 使用防抖保存 - this._debouncedSave(providerType); - } + resetProviderCounters(providerType, providerConfig) { + if (!providerConfig?.uuid) { + this._log('error', 'Invalid providerConfig in resetProviderCounters'); + return; + } + + const provider = this._findProvider(providerType, providerConfig.uuid); + if (provider) { + provider.config.errorCount = 0; + provider.config.usageCount = 0; + this._log('info', `Reset provider counters: ${provider.config.uuid} for type ${providerType}`); + + this._debouncedSave(providerType); } } @@ -165,16 +213,16 @@ export class ProviderPoolManager { * @param {object} providerConfig - 提供商配置 */ disableProvider(providerType, providerConfig) { - const pool = this.providerStatus[providerType]; - if (pool) { - const provider = pool.find(p => p.uuid === providerConfig.uuid); - if (provider) { - provider.config.isDisabled = true; - console.log(`[ProviderPoolManager] Disabled provider: ${providerConfig.uuid} for type ${providerType}`); - - // 使用防抖保存 - this._debouncedSave(providerType); - } + if (!providerConfig?.uuid) { + this._log('error', 'Invalid providerConfig in disableProvider'); + return; + } + + const provider = this._findProvider(providerType, providerConfig.uuid); + if (provider) { + provider.config.isDisabled = true; + this._log('info', `Disabled provider: ${providerConfig.uuid} for type ${providerType}`); + this._debouncedSave(providerType); } } @@ -184,16 +232,16 @@ export class ProviderPoolManager { * @param {object} providerConfig - 提供商配置 */ enableProvider(providerType, providerConfig) { - const pool = this.providerStatus[providerType]; - if (pool) { - const provider = pool.find(p => p.uuid === providerConfig.uuid); - if (provider) { - provider.config.isDisabled = false; - console.log(`[ProviderPoolManager] Enabled provider: ${providerConfig.uuid} for type ${providerType}`); - - // 使用防抖保存 - this._debouncedSave(providerType); - } + if (!providerConfig?.uuid) { + this._log('error', 'Invalid providerConfig in enableProvider'); + return; + } + + const provider = this._findProvider(providerType, providerConfig.uuid); + if (provider) { + provider.config.isDisabled = false; + this._log('info', `Enabled provider: ${providerConfig.uuid} for type ${providerType}`); + this._debouncedSave(providerType); } } @@ -202,8 +250,9 @@ export class ProviderPoolManager { * This method would typically be called periodically (e.g., via cron job). */ async performHealthChecks(isInit = false) { - console.log('[ProviderPoolManager] Performing health checks on all providers...'); + this._log('info', 'Performing health checks on all providers...'); const now = new Date(); + for (const providerType in this.providerStatus) { for (const providerStatus of this.providerStatus[providerType]) { const providerConfig = providerStatus.config; @@ -211,37 +260,38 @@ export class ProviderPoolManager { // Only attempt to health check unhealthy providers after a certain interval if (!providerStatus.config.isHealthy && providerStatus.config.lastErrorTime && (now.getTime() - new Date(providerStatus.config.lastErrorTime).getTime() < this.healthCheckInterval)) { - console.log(`[ProviderPoolManager] Skipping health check for ${providerConfig.uuid} (${providerType}). Last error too recent.`); + this._log('debug', `Skipping health check for ${providerConfig.uuid} (${providerType}). Last error too recent.`); continue; } try { // Perform actual health check based on provider type const isHealthy = await this._checkProviderHealth(providerType, providerConfig); - if(isHealthy === null){ - console.log(`[ProviderPoolManager] Health check for ${providerConfig.uuid} (${providerType}) skipped: Check not implemented.`); - this.markProviderZero(providerType, providerConfig); + + if (isHealthy === null) { + this._log('debug', `Health check for ${providerConfig.uuid} (${providerType}) skipped: Check not implemented.`); + this.resetProviderCounters(providerType, providerConfig); continue; } if (isHealthy) { if (!providerStatus.config.isHealthy) { // Provider was unhealthy but is now healthy - this.markProviderHealthy(isInit, providerType, providerConfig); - console.log(`[ProviderPoolManager] Health check for ${providerConfig.uuid} (${providerType}): Marked Healthy (actual check)`); + this.markProviderHealthy(providerType, providerConfig, isInit); + this._log('info', `Health check for ${providerConfig.uuid} (${providerType}): Marked Healthy (actual check)`); } else { // Provider was already healthy and still is - this.markProviderHealthy(isInit, providerType, providerConfig); - console.log(`[ProviderPoolManager] Health check for ${providerConfig.uuid} (${providerType}): Still Healthy`); + this.markProviderHealthy(providerType, providerConfig, isInit); + this._log('debug', `Health check for ${providerConfig.uuid} (${providerType}): Still Healthy`); } } else { // Provider is not healthy - console.warn(`[ProviderPoolManager] Health check for ${providerConfig.uuid} (${providerType}) failed: Provider is not responding correctly.`); + this._log('warn', `Health check for ${providerConfig.uuid} (${providerType}) failed: Provider is not responding correctly.`); this.markProviderUnhealthy(providerType, providerConfig); } } catch (error) { - console.error(`[ProviderPoolManager] Health check for ${providerConfig.uuid} (${providerType}) failed: ${error.message}`); + this._log('error', `Health check for ${providerConfig.uuid} (${providerType}) failed: ${error.message}`); // If a health check fails, mark it unhealthy, which will update error count and lastErrorTime this.markProviderUnhealthy(providerType, providerConfig); } @@ -249,88 +299,85 @@ export class ProviderPoolManager { } } + /** + * 构建健康检查请求 + * @private + */ + _buildHealthCheckRequest(providerType, modelName) { + const baseMessage = { role: 'user', content: 'Hello, are you ok?' }; + + // Gemini 使用不同的请求格式 + if (providerType === 'gemini-cli') { + return { + contents: [{ + role: 'user', + parts: [{ text: baseMessage.content }] + }] + }; + } + + // OpenAI Custom Responses 使用特殊格式 + if (providerType === 'openai-custom-responses') { + return { + input: [baseMessage], + model: modelName + }; + } + + // 其他提供商(OpenAI、Claude、Kiro、Qwen)使用标准格式 + return { + messages: [baseMessage], + model: modelName + }; + } + /** * Performs an actual health check for a specific provider. * @param {string} providerType - The type of the provider. * @param {object} providerConfig - The configuration of the provider to check. - * @returns {Promise} - True if the provider is healthy, false otherwise. + * @returns {Promise} - True if healthy, false if unhealthy, null if check not implemented. */ async _checkProviderHealth(providerType, providerConfig) { try { - // Create a temporary service adapter for health check - // 合并全局配置和 provider 配置 - const tempConfig = { - // ...this.globalConfig, - ...providerConfig, - MODEL_PROVIDER: providerType, - USE_SYSTEM_PROXY_GEMINI: this.globalConfig.USE_SYSTEM_PROXY_GEMINI, - USE_SYSTEM_PROXY_OPENAI: this.globalConfig.USE_SYSTEM_PROXY_OPENAI, - USE_SYSTEM_PROXY_CLAUDE: this.globalConfig.USE_SYSTEM_PROXY_CLAUDE, - USE_SYSTEM_PROXY_QWEN: this.globalConfig.USE_SYSTEM_PROXY_QWEN, - USE_SYSTEM_PROXY_KIRO: this.globalConfig.USE_SYSTEM_PROXY_KIRO, - }; - const serviceAdapter = getServiceAdapter(tempConfig); - if(!providerConfig.checkHealth){ + // 如果未启用健康检查,返回 null + if (!providerConfig.checkHealth) { return null; } - - // Determine a suitable model name for health check - // First, try to get it from the provider configuration - let modelName = providerConfig.checkModelName; - - // If not specified in config, use default model names based on provider type - if (!modelName) { - switch (providerType) { - case MODEL_PROVIDER.GEMINI_CLI: - modelName = 'gemini-2.5-flash'; // Example model name for Gemini - break; - case MODEL_PROVIDER.OPENAI_CUSTOM: - modelName = 'gpt-3.5-turbo'; // Example model name for OpenAI - break; - case MODEL_PROVIDER.CLAUDE_CUSTOM: - modelName = 'claude-3-7-sonnet-20250219'; // Example model name for Claude - break; - case MODEL_PROVIDER.KIRO_API: - modelName = 'claude-3-7-sonnet-20250219'; // Example model name for Kiro API - break; - case MODEL_PROVIDER.QWEN_API: - modelName = 'qwen3-coder-flash'; // Example model name for Qwen - break; - case MODEL_PROVIDER.OPENAI_CUSTOM_RESPONSES: - modelName = 'gpt-5-low'; // Example model name for OpenAI Custom Responses - break; - default: - console.warn(`[ProviderPoolManager] Unknown provider type for health check: ${providerType}`); - return false; - } - } - - // Perform a lightweight API call to check health - const healthCheckRequest = { - contents: [{ - role: 'user', - parts: [{ text: 'Hello, are you ok?' }] - }] + + // 合并全局配置和 provider 配置(简化代理配置) + const proxyKeys = ['GEMINI', 'OPENAI', 'CLAUDE', 'QWEN', 'KIRO']; + const tempConfig = { + ...providerConfig, + MODEL_PROVIDER: providerType }; - if (providerType === MODEL_PROVIDER.OPENAI_CUSTOM_RESPONSES) { - healthCheckRequest.input = [{ role: 'user', content: 'Hello, are you ok?' }]; - healthCheckRequest.model = modelName; - delete healthCheckRequest.contents; + // 动态添加代理配置 + proxyKeys.forEach(key => { + const proxyKey = `USE_SYSTEM_PROXY_${key}`; + if (this.globalConfig[proxyKey] !== undefined) { + tempConfig[proxyKey] = this.globalConfig[proxyKey]; + } + }); + + const serviceAdapter = getServiceAdapter(tempConfig); + + // 确定健康检查使用的模型名称 + const modelName = providerConfig.checkModelName || + ProviderPoolManager.DEFAULT_HEALTH_CHECK_MODELS[providerType]; + + if (!modelName) { + this._log('warn', `Unknown provider type for health check: ${providerType}`); + return false; } - // For OpenAI and Claude providers, we need a different request format - if (providerType === MODEL_PROVIDER.OPENAI_CUSTOM || providerType === MODEL_PROVIDER.CLAUDE_CUSTOM || providerType === MODEL_PROVIDER.KIRO_API || providerType === MODEL_PROVIDER.QWEN_API) { - healthCheckRequest.messages = [{ role: 'user', content: 'Hello, are you ok?' }]; - healthCheckRequest.model = modelName; - delete healthCheckRequest.contents; - } + // 构建健康检查请求 + const healthCheckRequest = this._buildHealthCheckRequest(providerType, modelName); - // console.log(`[ProviderPoolManager] Health check request for ${modelName}: ${JSON.stringify(healthCheckRequest)}`); + this._log('debug', `Health check request for ${modelName}: ${JSON.stringify(healthCheckRequest)}`); await serviceAdapter.generateContent(modelName, healthCheckRequest); return true; } catch (error) { - console.error(`[ProviderPoolManager] Health check failed for ${providerType}: ${error.message}`); + this._log('error', `Health check failed for ${providerType}: ${error.message}`); return false; } } @@ -356,57 +403,56 @@ export class ProviderPoolManager { } /** - * 优化1: 批量保存所有待保存的 providerType + * 批量保存所有待保存的 providerType(优化为单次文件写入) * @private */ async _flushPendingSaves() { const typesToSave = Array.from(this.pendingSaves); + if (typesToSave.length === 0) return; + this.pendingSaves.clear(); this.saveTimer = null; - for (const providerType of typesToSave) { - await this._saveProviderPoolsToJson(providerType); - } - } - - /** - * Saves the current provider pools configuration to the JSON file. - * @private - */ - async _saveProviderPoolsToJson(providerTypeToUpdate) { try { const filePath = this.globalConfig.PROVIDER_POOLS_FILE_PATH || '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.'); + this._log('info', 'provider_pools.json does not exist, creating new file.'); } else { throw readError; } } - if (this.providerStatus[providerTypeToUpdate]) { - currentPools[providerTypeToUpdate] = this.providerStatus[providerTypeToUpdate].map(p => { - // Convert Date objects to ISOString if they exist - 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}`); + // 更新所有待保存的 providerType + for (const providerType of typesToSave) { + if (this.providerStatus[providerType]) { + currentPools[providerType] = this.providerStatus[providerType].map(p => { + // Convert Date objects to ISOString if they exist + const config = { ...p.config }; + if (config.lastUsed instanceof Date) { + config.lastUsed = config.lastUsed.toISOString(); + } + if (config.lastErrorTime instanceof Date) { + config.lastErrorTime = config.lastErrorTime.toISOString(); + } + return config; + }); + } else { + this._log('warn', `Attempted to save unknown providerType: ${providerType}`); + } } + // 一次性写入文件 await fs.promises.writeFile(filePath, JSON.stringify(currentPools, null, 2), 'utf8'); - console.log(`[ProviderPoolManager] provider_pools.json for ${providerTypeToUpdate} updated successfully.`); + this._log('info', `provider_pools.json updated successfully for types: ${typesToSave.join(', ')}`); } catch (error) { - console.error('[ProviderPoolManager] Failed to write provider_pools.json:', error); + this._log('error', `Failed to write provider_pools.json: ${error.message}`); } }