refactor(provider-pool): 重构提供者池管理器的日志和健康检查逻辑

- 添加日志级别控制和统一的日志输出方法
- 重构健康检查逻辑,使用默认模型配置和标准化的请求构建
- 优化批量保存机制,减少文件I/O操作
- 添加参数校验和错误处理
- 重命名方法以更准确表达其功能
This commit is contained in:
hex2077 2025-11-25 16:02:30 +08:00
parent 08881ae144
commit 1c22b8c5c9

View file

@ -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<boolean>} - True if the provider is healthy, false otherwise.
* @returns {Promise<boolean|null>} - 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}`);
}
}