- 将配置文件统一迁移至configs目录 - 更新所有相关代码中对配置文件的引用路径 - 删除不再使用的run-docker脚本文件 - 更新文档中关于配置文件路径的说明 - 调整默认配置参数和路径引用方式
718 lines
No EOL
31 KiB
JavaScript
718 lines
No EOL
31 KiB
JavaScript
import * as fs from 'fs'; // Import fs module
|
||
import { getServiceAdapter } from './adapter.js';
|
||
import { MODEL_PROVIDER, getProtocolPrefix } from './common.js';
|
||
import { getProviderModels } from './provider-models.js';
|
||
import axios from 'axios';
|
||
|
||
/**
|
||
* Manages a pool of API service providers, handling their health and selection.
|
||
*/
|
||
export class ProviderPoolManager {
|
||
// 默认健康检查模型配置
|
||
// 键名必须与 MODEL_PROVIDER 常量值一致
|
||
static DEFAULT_HEALTH_CHECK_MODELS = {
|
||
'gemini-cli-oauth': 'gemini-2.5-flash',
|
||
'gemini-antigravity': 'gemini-2.5-flash',
|
||
'openai-custom': 'gpt-3.5-turbo',
|
||
'claude-custom': 'claude-3-7-sonnet-20250219',
|
||
'claude-kiro-oauth': 'claude-haiku-4-5',
|
||
'openai-qwen-oauth': 'qwen3-coder-flash',
|
||
'openaiResponses-custom': 'gpt-4o-mini'
|
||
};
|
||
|
||
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
|
||
// 使用 ?? 运算符确保 0 也能被正确设置,而不是被 || 替换为默认值
|
||
this.maxErrorCount = options.maxErrorCount ?? 3; // Default to 3 errors before marking unhealthy
|
||
this.healthCheckInterval = options.healthCheckInterval ?? 10 * 60 * 1000; // Default to 10 minutes
|
||
|
||
// 日志级别控制
|
||
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
|
||
|
||
// Fallback 链配置
|
||
this.fallbackChain = options.globalConfig?.providerFallbackChain || {};
|
||
|
||
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.
|
||
*/
|
||
initializeProviderStatus() {
|
||
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) => {
|
||
// Ensure initial health and usage stats are present in the config
|
||
providerConfig.isHealthy = providerConfig.isHealthy !== undefined ? providerConfig.isHealthy : true;
|
||
providerConfig.isDisabled = providerConfig.isDisabled !== undefined ? providerConfig.isDisabled : false;
|
||
providerConfig.lastUsed = providerConfig.lastUsed !== undefined ? providerConfig.lastUsed : null;
|
||
providerConfig.usageCount = providerConfig.usageCount !== undefined ? providerConfig.usageCount : 0;
|
||
providerConfig.errorCount = providerConfig.errorCount !== undefined ? providerConfig.errorCount : 0;
|
||
|
||
// 优化2: 简化 lastErrorTime 处理逻辑
|
||
providerConfig.lastErrorTime = providerConfig.lastErrorTime instanceof Date
|
||
? providerConfig.lastErrorTime.toISOString()
|
||
: (providerConfig.lastErrorTime || null);
|
||
|
||
// 健康检测相关字段
|
||
providerConfig.lastHealthCheckTime = providerConfig.lastHealthCheckTime || null;
|
||
providerConfig.lastHealthCheckModel = providerConfig.lastHealthCheckModel || null;
|
||
providerConfig.lastErrorMessage = providerConfig.lastErrorMessage || null;
|
||
|
||
this.providerStatus[providerType].push({
|
||
config: providerConfig,
|
||
uuid: providerConfig.uuid, // Still keep uuid at the top level for easy access
|
||
});
|
||
});
|
||
}
|
||
this._log('info', `Initialized provider statuses: ok (maxErrorCount: ${this.maxErrorCount})`);
|
||
}
|
||
|
||
/**
|
||
* Selects a provider from the pool for a given provider type.
|
||
* Currently uses a simple round-robin for healthy providers.
|
||
* If requestedModel is provided, providers that don't support the model will be excluded.
|
||
* @param {string} providerType - The type of provider to select (e.g., 'gemini-cli', 'openai-custom').
|
||
* @param {string} [requestedModel] - Optional. The model name to filter providers by.
|
||
* @returns {object|null} The selected provider's configuration, or null if no healthy provider is found.
|
||
*/
|
||
selectProvider(providerType, requestedModel = null, options = {}) {
|
||
// 参数校验
|
||
if (!providerType || typeof providerType !== 'string') {
|
||
this._log('error', `Invalid providerType: ${providerType}`);
|
||
return null;
|
||
}
|
||
|
||
const availableProviders = this.providerStatus[providerType] || [];
|
||
let availableAndHealthyProviders = availableProviders.filter(p =>
|
||
p.config.isHealthy && !p.config.isDisabled
|
||
);
|
||
|
||
// 如果指定了模型,则排除不支持该模型的提供商
|
||
if (requestedModel) {
|
||
const modelFilteredProviders = availableAndHealthyProviders.filter(p => {
|
||
// 如果提供商没有配置 notSupportedModels,则认为它支持所有模型
|
||
if (!p.config.notSupportedModels || !Array.isArray(p.config.notSupportedModels)) {
|
||
return true;
|
||
}
|
||
// 检查 notSupportedModels 数组中是否包含请求的模型,如果包含则排除
|
||
return !p.config.notSupportedModels.includes(requestedModel);
|
||
});
|
||
|
||
if (modelFilteredProviders.length === 0) {
|
||
this._log('warn', `No available providers for type: ${providerType} that support model: ${requestedModel}`);
|
||
return null;
|
||
}
|
||
|
||
availableAndHealthyProviders = modelFilteredProviders;
|
||
this._log('debug', `Filtered ${modelFilteredProviders.length} providers supporting model: ${requestedModel}`);
|
||
}
|
||
|
||
if (availableAndHealthyProviders.length === 0) {
|
||
this._log('warn', `No available and healthy providers for type: ${providerType}`);
|
||
return null;
|
||
}
|
||
|
||
// 改进:使用“最久未被使用”策略(LRU)代替取模轮询
|
||
// 这样即使可用列表长度动态变化,也能确保每个账号被平均轮到
|
||
const selected = availableAndHealthyProviders.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;
|
||
// 优先选择从未用过的,或者最久没用的
|
||
if (timeA !== timeB) return timeA - timeB;
|
||
// 如果时间相同,使用使用次数辅助判断
|
||
return (a.config.usageCount || 0) - (b.config.usageCount || 0);
|
||
})[0];
|
||
|
||
// 更新使用信息(除非明确跳过)
|
||
if (!options.skipUsageCount) {
|
||
selected.config.lastUsed = new Date().toISOString();
|
||
selected.config.usageCount++;
|
||
// 使用防抖保存
|
||
this._debouncedSave(providerType);
|
||
}
|
||
|
||
this._log('debug', `Selected provider for ${providerType} (round-robin): ${selected.config.uuid}${requestedModel ? ` for model: ${requestedModel}` : ''}${options.skipUsageCount ? ' (skip usage count)' : ''}`);
|
||
|
||
return selected.config;
|
||
}
|
||
|
||
/**
|
||
* Selects a provider from the pool with fallback support.
|
||
* When the primary provider type has no healthy providers, it will try fallback types.
|
||
* @param {string} providerType - The primary type of provider to select.
|
||
* @param {string} [requestedModel] - Optional. The model name to filter providers by.
|
||
* @param {Object} [options] - Optional. Additional options.
|
||
* @param {boolean} [options.skipUsageCount] - Optional. If true, skip incrementing usage count.
|
||
* @returns {object|null} An object containing the selected provider's configuration and the actual provider type used, or null if no healthy provider is found.
|
||
*/
|
||
selectProviderWithFallback(providerType, requestedModel = null, options = {}) {
|
||
// 参数校验
|
||
if (!providerType || typeof providerType !== 'string') {
|
||
this._log('error', `Invalid providerType: ${providerType}`);
|
||
return null;
|
||
}
|
||
|
||
// 记录尝试过的类型,避免循环
|
||
const triedTypes = new Set();
|
||
const typesToTry = [providerType];
|
||
|
||
// 添加 fallback 类型到尝试列表
|
||
const fallbackTypes = this.fallbackChain[providerType];
|
||
if (!fallbackTypes || fallbackTypes.length === 0) {
|
||
this._log('info', `No fallback types configured for ${providerType}`);
|
||
const selectedConfig = this.selectProvider(providerType, requestedModel, options);
|
||
if (selectedConfig) {
|
||
return {
|
||
config: selectedConfig,
|
||
actualProviderType: providerType,
|
||
isFallback: false
|
||
};
|
||
}
|
||
}
|
||
|
||
if (Array.isArray(fallbackTypes)) {
|
||
typesToTry.push(...fallbackTypes);
|
||
}
|
||
for (const currentType of typesToTry) {
|
||
// 避免重复尝试
|
||
if (triedTypes.has(currentType)) {
|
||
continue;
|
||
}
|
||
triedTypes.add(currentType);
|
||
|
||
// 检查该类型是否有配置的池
|
||
if (!this.providerStatus[currentType] || this.providerStatus[currentType].length === 0) {
|
||
this._log('info', `No provider pool configured for type: ${currentType}`);
|
||
continue;
|
||
}
|
||
|
||
// 如果是 fallback 类型,需要检查模型兼容性
|
||
if (currentType !== providerType && requestedModel) {
|
||
// 检查协议前缀是否兼容
|
||
const primaryProtocol = getProtocolPrefix(providerType);
|
||
const fallbackProtocol = getProtocolPrefix(currentType);
|
||
|
||
if (primaryProtocol !== fallbackProtocol) {
|
||
this._log('info', `Skipping fallback type ${currentType}: protocol mismatch (${primaryProtocol} vs ${fallbackProtocol})`);
|
||
continue;
|
||
}
|
||
|
||
// 检查 fallback 类型是否支持请求的模型
|
||
const supportedModels = getProviderModels(currentType);
|
||
if (supportedModels.length > 0 && !supportedModels.includes(requestedModel)) {
|
||
this._log('info', `Skipping fallback type ${currentType}: model ${requestedModel} not supported`);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// 尝试从当前类型选择提供商
|
||
const selectedConfig = this.selectProvider(currentType, requestedModel, options);
|
||
|
||
if (selectedConfig) {
|
||
if (currentType !== providerType) {
|
||
this._log('info', `Fallback activated: ${providerType} -> ${currentType} (uuid: ${selectedConfig.uuid})`);
|
||
}
|
||
return {
|
||
config: selectedConfig,
|
||
actualProviderType: currentType,
|
||
isFallback: currentType !== providerType
|
||
};
|
||
}
|
||
}
|
||
|
||
this._log('warn', `None available provider found for ${providerType} or any of its fallback types: ${fallbackTypes?.join(', ') || 'none configured'}`);
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Gets the fallback chain for a given provider type.
|
||
* @param {string} providerType - The provider type to get fallback chain for.
|
||
* @returns {Array<string>} The fallback chain array, or empty array if not configured.
|
||
*/
|
||
getFallbackChain(providerType) {
|
||
return this.fallbackChain[providerType] || [];
|
||
}
|
||
|
||
/**
|
||
* Sets or updates the fallback chain for a provider type.
|
||
* @param {string} providerType - The provider type to set fallback chain for.
|
||
* @param {Array<string>} fallbackTypes - Array of fallback provider types.
|
||
*/
|
||
setFallbackChain(providerType, fallbackTypes) {
|
||
if (!Array.isArray(fallbackTypes)) {
|
||
this._log('error', `Invalid fallbackTypes: must be an array`);
|
||
return;
|
||
}
|
||
this.fallbackChain[providerType] = fallbackTypes;
|
||
this._log('info', `Updated fallback chain for ${providerType}: ${fallbackTypes.join(' -> ')}`);
|
||
}
|
||
|
||
/**
|
||
* Checks if all providers of a given type are unhealthy.
|
||
* @param {string} providerType - The provider type to check.
|
||
* @returns {boolean} True if all providers are unhealthy or disabled.
|
||
*/
|
||
isAllProvidersUnhealthy(providerType) {
|
||
const providers = this.providerStatus[providerType] || [];
|
||
if (providers.length === 0) {
|
||
return true;
|
||
}
|
||
return providers.every(p => !p.config.isHealthy || p.config.isDisabled);
|
||
}
|
||
|
||
/**
|
||
* Gets statistics about provider health for a given type.
|
||
* @param {string} providerType - The provider type to get stats for.
|
||
* @returns {Object} Statistics object with total, healthy, unhealthy, and disabled counts.
|
||
*/
|
||
getProviderStats(providerType) {
|
||
const providers = this.providerStatus[providerType] || [];
|
||
const stats = {
|
||
total: providers.length,
|
||
healthy: 0,
|
||
unhealthy: 0,
|
||
disabled: 0
|
||
};
|
||
|
||
for (const p of providers) {
|
||
if (p.config.isDisabled) {
|
||
stats.disabled++;
|
||
} else if (p.config.isHealthy) {
|
||
stats.healthy++;
|
||
} else {
|
||
stats.unhealthy++;
|
||
}
|
||
}
|
||
|
||
return stats;
|
||
}
|
||
|
||
/**
|
||
* Marks a provider as unhealthy (e.g., after an API error).
|
||
* @param {string} providerType - The type of the provider.
|
||
* @param {object} providerConfig - The configuration of the provider to mark.
|
||
* @param {string} [errorMessage] - Optional error message to store.
|
||
*/
|
||
markProviderUnhealthy(providerType, providerConfig, errorMessage = null) {
|
||
if (!providerConfig?.uuid) {
|
||
this._log('error', 'Invalid providerConfig in markProviderUnhealthy');
|
||
return;
|
||
}
|
||
|
||
const provider = this._findProvider(providerType, providerConfig.uuid);
|
||
if (provider) {
|
||
provider.config.errorCount++;
|
||
provider.config.lastErrorTime = new Date().toISOString();
|
||
// 更新 lastUsed 时间,避免因 LRU 策略导致失败节点被重复选中
|
||
provider.config.lastUsed = new Date().toISOString();
|
||
|
||
// 保存错误信息
|
||
if (errorMessage) {
|
||
provider.config.lastErrorMessage = errorMessage;
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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} resetUsageCount - Whether to reset usage count (optional, default: false).
|
||
* @param {string} [healthCheckModel] - Optional model name used for health check.
|
||
*/
|
||
markProviderHealthy(providerType, providerConfig, resetUsageCount = false, healthCheckModel = null) {
|
||
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;
|
||
provider.config.lastErrorMessage = null;
|
||
|
||
// 更新健康检测信息
|
||
provider.config.lastHealthCheckTime = new Date().toISOString();
|
||
if (healthCheckModel) {
|
||
provider.config.lastHealthCheckModel = healthCheckModel;
|
||
}
|
||
|
||
// 只有在明确要求重置使用计数时才重置
|
||
if (resetUsageCount) {
|
||
provider.config.usageCount = 0;
|
||
}else{
|
||
provider.config.usageCount++;
|
||
provider.config.lastUsed = new Date().toISOString();
|
||
}
|
||
this._log('info', `Marked provider as healthy: ${provider.config.uuid} for type ${providerType}${resetUsageCount ? ' (usage count reset)' : ''}`);
|
||
|
||
this._debouncedSave(providerType);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重置提供商的计数器(错误计数和使用计数)
|
||
* @param {string} providerType - The type of the provider.
|
||
* @param {object} providerConfig - The configuration of the provider to mark.
|
||
*/
|
||
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);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 禁用指定提供商
|
||
* @param {string} providerType - 提供商类型
|
||
* @param {object} providerConfig - 提供商配置
|
||
*/
|
||
disableProvider(providerType, providerConfig) {
|
||
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);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 启用指定提供商
|
||
* @param {string} providerType - 提供商类型
|
||
* @param {object} providerConfig - 提供商配置
|
||
*/
|
||
enableProvider(providerType, providerConfig) {
|
||
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);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Performs health checks on all providers in the pool.
|
||
* This method would typically be called periodically (e.g., via cron job).
|
||
*/
|
||
async performHealthChecks(isInit = false) {
|
||
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;
|
||
|
||
// 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)) {
|
||
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 healthResult = await this._checkProviderHealth(providerType, providerConfig);
|
||
|
||
if (healthResult === null) {
|
||
this._log('debug', `Health check for ${providerConfig.uuid} (${providerType}) skipped: Check not implemented.`);
|
||
this.resetProviderCounters(providerType, providerConfig);
|
||
continue;
|
||
}
|
||
|
||
if (healthResult.success) {
|
||
if (!providerStatus.config.isHealthy) {
|
||
// Provider was unhealthy but is now healthy
|
||
// 恢复健康时不重置使用计数,保持原有值
|
||
this.markProviderHealthy(providerType, providerConfig, true, healthResult.modelName);
|
||
this._log('info', `Health check for ${providerConfig.uuid} (${providerType}): Marked Healthy (actual check)`);
|
||
} else {
|
||
// Provider was already healthy and still is
|
||
// 只在初始化时重置使用计数
|
||
this.markProviderHealthy(providerType, providerConfig, true, healthResult.modelName);
|
||
this._log('debug', `Health check for ${providerConfig.uuid} (${providerType}): Still Healthy`);
|
||
}
|
||
} else {
|
||
// Provider is not healthy
|
||
this._log('warn', `Health check for ${providerConfig.uuid} (${providerType}) failed: ${healthResult.errorMessage || 'Provider is not responding correctly.'}`);
|
||
this.markProviderUnhealthy(providerType, providerConfig, healthResult.errorMessage);
|
||
|
||
// 更新健康检测时间和模型(即使失败也记录)
|
||
providerStatus.config.lastHealthCheckTime = new Date().toISOString();
|
||
if (healthResult.modelName) {
|
||
providerStatus.config.lastHealthCheckModel = healthResult.modelName;
|
||
}
|
||
}
|
||
|
||
} catch (error) {
|
||
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, error.message);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 构建健康检查请求(返回多种格式用于重试)
|
||
* @private
|
||
* @returns {Array} 请求格式数组,按优先级排序
|
||
*/
|
||
_buildHealthCheckRequests(providerType, modelName) {
|
||
const baseMessage = { role: 'user', content: 'Hi' };
|
||
const requests = [];
|
||
|
||
// Gemini 使用 contents 格式
|
||
if (providerType.startsWith('gemini')) {
|
||
requests.push({
|
||
contents: [{
|
||
role: 'user',
|
||
parts: [{ text: baseMessage.content }]
|
||
}]
|
||
});
|
||
return requests;
|
||
}
|
||
|
||
// Kiro OAuth 同时支持 messages 和 contents 格式
|
||
if (providerType.startsWith('claude-kiro')) {
|
||
// 优先使用 messages 格式
|
||
requests.push({
|
||
messages: [baseMessage],
|
||
model: modelName,
|
||
max_tokens: 1
|
||
});
|
||
// 备用 contents 格式
|
||
requests.push({
|
||
contents: [{
|
||
role: 'user',
|
||
parts: [{ text: baseMessage.content }]
|
||
}],
|
||
max_tokens: 1
|
||
});
|
||
return requests;
|
||
}
|
||
|
||
// OpenAI Custom Responses 使用特殊格式
|
||
if (providerType === MODEL_PROVIDER.OPENAI_CUSTOM_RESPONSES) {
|
||
requests.push({
|
||
input: [baseMessage],
|
||
model: modelName
|
||
});
|
||
return requests;
|
||
}
|
||
|
||
// 其他提供商(OpenAI、Claude、Qwen)使用标准 messages 格式
|
||
requests.push({
|
||
messages: [baseMessage],
|
||
model: modelName
|
||
});
|
||
|
||
return requests;
|
||
}
|
||
|
||
/**
|
||
* 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.
|
||
* @param {boolean} forceCheck - If true, ignore checkHealth config and force the check.
|
||
* @returns {Promise<{success: boolean, modelName: string, errorMessage: string}|null>} - Health check result object or null if check not implemented.
|
||
*/
|
||
async _checkProviderHealth(providerType, providerConfig, forceCheck = false) {
|
||
// 确定健康检查使用的模型名称
|
||
const modelName = providerConfig.checkModelName ||
|
||
ProviderPoolManager.DEFAULT_HEALTH_CHECK_MODELS[providerType];
|
||
|
||
// 如果未启用健康检查且不是强制检查,返回 null
|
||
if (!providerConfig.checkHealth && !forceCheck) {
|
||
return null;
|
||
}
|
||
|
||
if (!modelName) {
|
||
this._log('warn', `Unknown provider type for health check: ${providerType}`);
|
||
return { success: false, modelName: null, errorMessage: 'Unknown provider type for health check' };
|
||
}
|
||
|
||
// 使用内部服务适配器方式进行健康检查
|
||
const proxyKeys = ['GEMINI', 'OPENAI', 'CLAUDE', 'QWEN', 'KIRO'];
|
||
const tempConfig = {
|
||
...providerConfig,
|
||
MODEL_PROVIDER: providerType
|
||
};
|
||
|
||
proxyKeys.forEach(key => {
|
||
const proxyKey = `USE_SYSTEM_PROXY_${key}`;
|
||
if (this.globalConfig[proxyKey] !== undefined) {
|
||
tempConfig[proxyKey] = this.globalConfig[proxyKey];
|
||
}
|
||
});
|
||
|
||
const serviceAdapter = getServiceAdapter(tempConfig);
|
||
|
||
// 获取所有可能的请求格式
|
||
const healthCheckRequests = this._buildHealthCheckRequests(providerType, modelName);
|
||
|
||
// 重试机制:尝试不同的请求格式
|
||
const maxRetries = healthCheckRequests.length;
|
||
let lastError = null;
|
||
|
||
for (let i = 0; i < maxRetries; i++) {
|
||
const healthCheckRequest = healthCheckRequests[i];
|
||
try {
|
||
this._log('debug', `Health check attempt ${i + 1}/${maxRetries} for ${modelName}: ${JSON.stringify(healthCheckRequest)}`);
|
||
await serviceAdapter.generateContent(modelName, healthCheckRequest);
|
||
return { success: true, modelName, errorMessage: null };
|
||
} catch (error) {
|
||
lastError = error;
|
||
this._log('debug', `Health check attempt ${i + 1} failed for ${providerType}: ${error.message}`);
|
||
// 继续尝试下一个格式
|
||
}
|
||
}
|
||
|
||
// 所有尝试都失败
|
||
this._log('error', `Health check failed for ${providerType} after ${maxRetries} attempts: ${lastError?.message}`);
|
||
return { success: false, modelName, errorMessage: lastError?.message || 'All health check attempts failed' };
|
||
}
|
||
|
||
/**
|
||
* 优化1: 添加防抖保存方法
|
||
* 延迟保存操作,避免频繁的文件 I/O
|
||
* @private
|
||
*/
|
||
_debouncedSave(providerType) {
|
||
// 将待保存的 providerType 添加到集合中
|
||
this.pendingSaves.add(providerType);
|
||
|
||
// 清除之前的定时器
|
||
if (this.saveTimer) {
|
||
clearTimeout(this.saveTimer);
|
||
}
|
||
|
||
// 设置新的定时器
|
||
this.saveTimer = setTimeout(() => {
|
||
this._flushPendingSaves();
|
||
}, this.saveDebounceTime);
|
||
}
|
||
|
||
/**
|
||
* 批量保存所有待保存的 providerType(优化为单次文件写入)
|
||
* @private
|
||
*/
|
||
async _flushPendingSaves() {
|
||
const typesToSave = Array.from(this.pendingSaves);
|
||
if (typesToSave.length === 0) return;
|
||
|
||
this.pendingSaves.clear();
|
||
this.saveTimer = null;
|
||
|
||
try {
|
||
const filePath = this.globalConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json';
|
||
let currentPools = {};
|
||
|
||
// 一次性读取文件
|
||
try {
|
||
const fileContent = await fs.promises.readFile(filePath, 'utf8');
|
||
currentPools = JSON.parse(fileContent);
|
||
} catch (readError) {
|
||
if (readError.code === 'ENOENT') {
|
||
this._log('info', 'configs/provider_pools.json does not exist, creating new file.');
|
||
} else {
|
||
throw readError;
|
||
}
|
||
}
|
||
|
||
// 更新所有待保存的 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();
|
||
}
|
||
if (config.lastHealthCheckTime instanceof Date) {
|
||
config.lastHealthCheckTime = config.lastHealthCheckTime.toISOString();
|
||
}
|
||
return config;
|
||
});
|
||
} else {
|
||
this._log('warn', `Attempted to save unknown providerType: ${providerType}`);
|
||
}
|
||
}
|
||
|
||
// 一次性写入文件
|
||
await fs.promises.writeFile(filePath, JSON.stringify(currentPools, null, 2), 'utf8');
|
||
this._log('info', `configs/provider_pools.json updated successfully for types: ${typesToSave.join(', ')}`);
|
||
} catch (error) {
|
||
this._log('error', `Failed to write provider_pools.json: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
} |