AIClient-2-API/src/providers/provider-pool-manager.js
Wenaixi fca9413f26 fix: 修复架构性问题——并发计数、文件锁、输入校验、密码哈希
- fix: provider-pool-manager: activeProviderRefreshes 计数器修复,情况1(已有队列)不持有全局槽位,通过 ownsGlobalSlot 闭包变量精确控制递减时机,防止出现负值
- fix: provider-api: 为 handleAddProvider/handleUpdateProvider/handleDeleteProvider/handleDisableEnableProvider 添加模块级 Promise 链文件锁(withFileLock),防止并发读写同一 JSON 文件导致数据丢失
- fix: config-api: handleUpdateConfig 添加输入类型校验——SERVER_PORT 校验整数范围、REQUEST_MAX_RETRIES 校验数值范围、SYSTEM_PROMPT_FILE_PATH 禁止路径遍历(含..)、REQUIRED_API_KEY 限定字符串类型
- fix: config-api/auth: 密码改为 PBKDF2(SHA-512, 100000轮) 哈希存储,格式 pbkdf2🧂hash,验证使用 timingSafeEqual 防时序攻击,兼容旧明文格式平滑迁移,并增加最小长度 8 位校验
- 所有修改均使用 Node.js 内置 crypto 模块,无新依赖
2026-04-03 02:56:33 +08:00

2070 lines
93 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as fs from 'fs';
import { getServiceAdapter, getRegisteredProviders } from './adapter.js';
import logger from '../utils/logger.js';
import { MODEL_PROVIDER, getProtocolPrefix } from '../utils/common.js';
import { getProviderModels } from './provider-models.js';
import { broadcastEvent } from '../ui-modules/event-broadcast.js';
import { convertData } from '../convert/convert.js';
import { ENDPOINT_TYPE } from '../utils/common.js';
/**
* 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-4o-mini',
'claude-custom': 'claude-3-7-sonnet-20250219',
'claude-kiro-oauth': 'claude-haiku-4-5',
'openai-qwen-oauth': 'qwen3-coder-flash',
'openai-iflow': 'qwen3-coder-plus',
'openai-codex-oauth': 'gpt-5-codex-mini',
'openaiResponses-custom': 'gpt-4o-mini',
'forward-api': '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 ?? 10; // Default to 10 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 || {};
// Model Fallback 映射配置
this.modelFallbackMapping = options.globalConfig?.modelFallbackMapping || {};
// 并发控制:每个 providerType 的选择锁
// 用于确保 selectProvider 的排序 and 更新操作是原子的
this._selectionLocks = {};
this._isSelecting = {}; // 同步标志位锁
// --- V2: 读写分离 and 异步刷新队列 ---
// 刷新并发控制配置
this.refreshConcurrency = {
global: options.globalConfig?.REFRESH_CONCURRENCY_GLOBAL ?? 2, // 全局最大并行提供商数
perProvider: options.globalConfig?.REFRESH_CONCURRENCY_PER_PROVIDER ?? 1 // 每个提供商内部最大并行数
};
this.activeProviderRefreshes = 0; // 当前正在刷新的提供商类型数量
this.globalRefreshWaiters = []; // 等待全局并发槽位的任务
this.warmupTarget = options.globalConfig?.WARMUP_TARGET || 0; // 默认预热0个节点
this.refreshingUuids = new Set(); // 正在刷新的节点 UUID 集合
this.refreshQueues = {}; // 按 providerType 分组的队列
// 缓冲队列机制延迟5秒去重后再执行刷新
this.refreshBufferQueues = {}; // 按 providerType 分组的缓冲队列
this.refreshBufferTimers = {}; // 按 providerType 分组的定时器
this.bufferDelay = options.globalConfig?.REFRESH_BUFFER_DELAY ?? 5000; // 默认5秒缓冲延迟
// 用于并发选点时的原子排序辅助(自增序列)
this._selectionSequence = 0;
this.initializeProviderStatus();
}
/**
* 检查所有节点的配置文件,如果发现即将过期则触发刷新
*/
async checkAndRefreshExpiringNodes() {
this._log('info', 'Checking nodes for approaching expiration dates using provider adapters...');
for (const providerType in this.providerStatus) {
const providers = this.providerStatus[providerType];
for (const providerStatus of providers) {
const config = providerStatus.config;
// 根据 providerType 确定配置文件路径字段名
let configPath = null;
if (providerType.startsWith('claude-kiro')) {
configPath = config.KIRO_OAUTH_CREDS_FILE_PATH;
} else if (providerType.startsWith('gemini-cli')) {
configPath = config.GEMINI_OAUTH_CREDS_FILE_PATH;
} else if (providerType.startsWith('gemini-antigravity')) {
configPath = config.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH;
} else if (providerType.startsWith('openai-qwen')) {
configPath = config.QWEN_OAUTH_CREDS_FILE_PATH;
} else if (providerType.startsWith('openai-iflow')) {
configPath = config.IFLOW_OAUTH_CREDS_FILE_PATH;
} else if (providerType.startsWith('openai-codex')) {
configPath = config.CODEX_OAUTH_CREDS_FILE_PATH;
}
// logger.info(`Checking node ${providerStatus.uuid} (${providerType}) expiry date... configPath: ${configPath}`);
// 排除不健康和禁用的节点
if (!config.isHealthy || config.isDisabled) continue;
if (configPath && fs.existsSync(configPath)) {
try {
const fileContent = fs.readFileSync(configPath, 'utf-8');
const credData = JSON.parse(fileContent);
const expiryTime = credData.expiry_date || credData.expiry || credData.expires_at;
const nearExpiryMs = (currentConfig?.CRON_NEAR_MINUTES || 10) * 60 * 1000;
const isNearExpiry = expiryTime && (expiryTime - Date.now()) < nearExpiryMs;
if (isNearExpiry) {
this._log('warn', `Node ${providerStatus.uuid} (${providerType}) is near expiration. Enqueuing refresh...`);
this._enqueueRefresh(providerType, providerStatus);
}
} catch (err) {
this._log('error', `Failed to check expiry for node ${providerStatus.uuid}: ${err.message}`);
}
} else {
this._log('debug', `Node ${providerStatus.uuid} (${providerType}) has no valid config file path or file does not exist.`);
}
}
}
}
/**
* 系统预热逻辑:按提供商分组,每组预热 warmupTarget 个节点
* @returns {Promise<void>}
*/
async warmupNodes() {
if (this.warmupTarget <= 0) return;
this._log('info', `Starting system warmup (Group Target: ${this.warmupTarget} nodes per provider)...`);
const nodesToWarmup = [];
for (const type in this.providerStatus) {
const pool = this.providerStatus[type];
// 挑选当前提供商下需要预热的节点
const candidates = pool
.filter(p => p.config.isHealthy && !p.config.isDisabled && !this.refreshingUuids.has(p.uuid))
.sort((a, b) => {
// 优先级 A: 明确标记需要刷新的
if (a.config.needsRefresh && !b.config.needsRefresh) return -1;
if (!a.config.needsRefresh && b.config.needsRefresh) return 1;
// 优先级 B: 按照正常的选择权重排序(最久没用过的优先补)
const scoreA = this._calculateNodeScore(a);
const scoreB = this._calculateNodeScore(b);
return scoreA - scoreB;
})
.slice(0, this.warmupTarget);
candidates.forEach(p => nodesToWarmup.push({ type, status: p }));
}
this._log('info', `Warmup: Selected total ${nodesToWarmup.length} nodes across all providers to refresh.`);
for (const node of nodesToWarmup) {
this._enqueueRefresh(node.type, node.status, true);
}
// 注意warmupNodes 不等待队列结束,它是异步后台执行的
}
/**
* 将节点放入缓冲队列延迟5秒后去重并执行刷新
* @param {string} providerType
* @param {object} providerStatus
* @param {boolean} force - 是否强制刷新(跳过缓冲队列)
* @private
*/
_enqueueRefresh(providerType, providerStatus, force = false) {
const uuid = providerStatus.uuid;
// 如果已经在刷新中,直接返回
if (this.refreshingUuids.has(uuid)) {
this._log('debug', `Node ${uuid} is already in refresh queue.`);
return;
}
// 判断提供商池内的总可用节点数小于5个时不等待缓冲直接加入刷新队列
const healthyCount = this.getHealthyCount(providerType);
if (healthyCount < 5) {
this._log('info', `Provider ${providerType} has only ${healthyCount} healthy nodes. Bypassing buffer and enqueuing refresh for ${uuid} immediately.`);
this._enqueueRefreshImmediate(providerType, providerStatus, force);
return;
}
// 初始化缓冲队列
if (!this.refreshBufferQueues[providerType]) {
this.refreshBufferQueues[providerType] = new Map(); // 使用 Map 自动去重
}
const bufferQueue = this.refreshBufferQueues[providerType];
// 检查是否已在缓冲队列中
const existing = bufferQueue.get(uuid);
const isNewEntry = !existing;
// 更新或添加节点(保留 force: true 状态)
bufferQueue.set(uuid, {
providerStatus,
force: existing ? (existing.force || force) : force
});
if (isNewEntry) {
this._log('debug', `Node ${uuid} added to buffer queue for ${providerType}. Buffer size: ${bufferQueue.size}`);
} else {
this._log('debug', `Node ${uuid} already in buffer queue, updated force flag. Buffer size: ${bufferQueue.size}`);
}
// 只在新增节点或缓冲队列为空时重置定时器
// 避免频繁重置导致刷新被无限延迟
if (isNewEntry || !this.refreshBufferTimers[providerType]) {
// 清除之前的定时器
if (this.refreshBufferTimers[providerType]) {
clearTimeout(this.refreshBufferTimers[providerType]);
}
// 设置新的定时器延迟5秒后处理缓冲队列
this.refreshBufferTimers[providerType] = setTimeout(() => {
this._flushRefreshBuffer(providerType);
}, this.bufferDelay);
}
}
/**
* 处理缓冲队列,将去重后的节点放入实际刷新队列
* @param {string} providerType
* @private
*/
_flushRefreshBuffer(providerType) {
const bufferQueue = this.refreshBufferQueues[providerType];
if (!bufferQueue || bufferQueue.size === 0) {
return;
}
this._log('info', `Flushing refresh buffer for ${providerType}. Processing ${bufferQueue.size} unique nodes.`);
// 将缓冲队列中的所有节点放入实际刷新队列
for (const [uuid, { providerStatus, force }] of bufferQueue.entries()) {
this._enqueueRefreshImmediate(providerType, providerStatus, force);
}
// 清空缓冲队列和定时器
bufferQueue.clear();
delete this.refreshBufferTimers[providerType];
}
/**
* 立即将节点放入刷新队列(内部方法,由缓冲队列调用)
* @param {string} providerType
* @param {object} providerStatus
* @param {boolean} force
* @private
*/
_enqueueRefreshImmediate(providerType, providerStatus, force = false) {
const uuid = providerStatus.uuid;
// 再次检查是否已经在刷新中(防止并发问题)
if (this.refreshingUuids.has(uuid)) {
this._log('debug', `Node ${uuid} is already in refresh queue (immediate check).`);
return;
}
this.refreshingUuids.add(uuid);
// 初始化提供商队列
if (!this.refreshQueues[providerType]) {
this.refreshQueues[providerType] = {
activeCount: 0,
waitingTasks: []
};
}
const queue = this.refreshQueues[providerType];
// 记录此任务是否持有一个全局槽位情况1追加的任务不持有
let ownsGlobalSlot = false;
const runTask = async () => {
try {
await this._refreshNodeToken(providerType, providerStatus, force);
} catch (err) {
this._log('error', `Failed to process refresh for node ${uuid}: ${err.message}`);
} finally {
this.refreshingUuids.delete(uuid);
// 再次获取当前队列引用
const currentQueue = this.refreshQueues[providerType];
if (!currentQueue) return;
currentQueue.activeCount--;
// 1. 尝试从当前提供商队列中取下一个任务
if (currentQueue.waitingTasks.length > 0) {
const nextTask = currentQueue.waitingTasks.shift();
currentQueue.activeCount++;
// 使用 Promise.resolve().then 避免过深的递归
Promise.resolve().then(nextTask);
} else if (currentQueue.activeCount === 0) {
// 2. 如果当前提供商的所有任务都完成了,释放全局槽位
// 只有持有全局槽位的任务才能递减计数器,避免负值
if (ownsGlobalSlot &&
currentQueue.waitingTasks.length === 0 &&
this.refreshQueues[providerType] === currentQueue) {
this.activeProviderRefreshes--;
delete this.refreshQueues[providerType]; // 清理空队列
}
// 3. 尝试启动下一个等待中的提供商队列
if (this.globalRefreshWaiters.length > 0) {
const nextProviderStart = this.globalRefreshWaiters.shift();
Promise.resolve().then(nextProviderStart);
}
}
}
};
const tryStartProviderQueue = () => {
if (queue.activeCount < this.refreshConcurrency.perProvider) {
queue.activeCount++;
runTask();
} else {
queue.waitingTasks.push(runTask);
}
};
// 检查全局并发限制(按提供商分组)
// 情况1: 该提供商已经在运行,直接加入其队列(不占用新的全局槽位)
const isExistingQueue = this.refreshQueues[providerType].activeCount > 0 || this.refreshQueues[providerType].waitingTasks.length > 0;
if (isExistingQueue) {
tryStartProviderQueue();
}
// 情况2: 该提供商未运行,需要检查全局槽位,此路径持有全局槽位
else if (this.activeProviderRefreshes < this.refreshConcurrency.global) {
ownsGlobalSlot = true;
this.activeProviderRefreshes++;
tryStartProviderQueue();
}
// 情况3: 全局槽位已满,进入等待队列,由等待回调负责标记持槽
else {
this.globalRefreshWaiters.push(() => {
// 重新获取最新的队列引用
if (!this.refreshQueues[providerType]) {
this.refreshQueues[providerType] = {
activeCount: 0,
waitingTasks: []
};
}
// 从等待队列启动时持有全局槽位
ownsGlobalSlot = true;
this.activeProviderRefreshes++;
tryStartProviderQueue();
});
}
}
/**
* 实际执行节点刷新逻辑
* @private
*/
async _refreshNodeToken(providerType, providerStatus, force = false) {
const config = providerStatus.config;
// 检查刷新次数是否已达上限最大5次
const currentRefreshCount = config.refreshCount || 0;
if (currentRefreshCount >= 5 && !force) {
this._log('warn', `Node ${providerStatus.uuid} has reached maximum refresh count (5), marking as unhealthy`);
// 标记为不健康
this.markProviderUnhealthyImmediately(providerType, config, 'Maximum refresh count (5) reached');
return;
}
// 添加5秒内的随机等待时间避免并发刷新时的冲突
// const randomDelay = Math.floor(Math.random() * 5000);
// this._log('info', `Starting token refresh for node ${providerStatus.uuid} (${providerType}) with ${randomDelay}ms delay`);
// await new Promise(resolve => setTimeout(resolve, randomDelay));
try {
// 增加刷新计数
config.refreshCount = currentRefreshCount + 1;
// 使用适配器进行刷新
const tempConfig = {
...this.globalConfig,
...config,
MODEL_PROVIDER: providerType
};
const serviceAdapter = getServiceAdapter(tempConfig);
// 调用适配器的 refreshToken 方法(内部封装了具体的刷新逻辑)
if (typeof serviceAdapter.refreshToken === 'function') {
const startTime = Date.now();
if (force) {
if (typeof serviceAdapter.forceRefreshToken === 'function') {
await serviceAdapter.forceRefreshToken();
} else {
this._log('warn', `forceRefreshToken not implemented for ${providerType}, falling back to refreshToken`);
await serviceAdapter.refreshToken();
}
} else {
await serviceAdapter.refreshToken();
}
const duration = Date.now() - startTime;
this._log('info', `Token refresh successful for node ${providerStatus.uuid} (Duration: ${duration}ms)`);
// 刷新成功,统一重置状态
config.needsRefresh = false;
config.refreshCount = 0;
config.lastRefreshTime = Date.now(); // 记录最后刷新成功时间
} else {
throw new Error(`refreshToken method not implemented for ${providerType}`);
}
} catch (error) {
this._log('error', `Token refresh failed for node ${providerStatus.uuid}: ${error.message}`);
this.markProviderUnhealthyImmediately(providerType, config, `Refresh failed: ${error.message}`);
throw error;
}
}
/**
* 计算节点的权重/评分,用于排序
* 分数越低,优先级越高
* @private
*/
_calculateNodeScore(providerStatus, now = Date.now(), minSeqInPool = -1) {
const config = providerStatus.config;
const state = providerStatus.state;
// 1. 基础健康分:不健康的排最后
if (!config.isHealthy || config.isDisabled) return 1e18;
// 检查并发限制
const concurrencyLimit = parseInt(config.concurrencyLimit || 0);
const queueLimit = parseInt(config.queueLimit || 0);
if (concurrencyLimit > 0) {
if (state.activeCount >= concurrencyLimit) {
// 如果队列也满了,排在最后(但优于不健康节点)
if (queueLimit > 0 && state.waitingCount >= queueLimit) {
return 1e17;
}
// 没满,但需要排队。排队数量越多,权重越大
return 1e15 + (state.waitingCount || 0) * 1e10;
}
}
// 2. 预热/新鲜度判断
const lastHealthCheckTime = config.lastHealthCheckTime ? new Date(config.lastHealthCheckTime).getTime() : 0;
const isFresh = lastHealthCheckTime && (now - lastHealthCheckTime < 60000);
// 3. 计算统一评分
// 基础分:新鲜节点使用固定负偏移 (-1e14),普通节点使用上次使用时间 (约 1.7e12)
const lastUsedTime = config.lastUsed ? new Date(config.lastUsed).getTime() : (now - 86400000);
const baseScore = isFresh ? -1e14 : lastUsedTime;
// 惩罚项 A: 使用次数 (每多用一次增加 10 秒权重)
const usageCount = config.usageCount || 0;
const usageScore = usageCount * 10000;
// 惩罚项 B: 相对序列号 (用于打破平局,确保轮询)
const lastSelectionSeq = config._lastSelectionSeq || 0;
if (minSeqInPool === -1) {
const pool = this.providerStatus[providerStatus.type] || [];
minSeqInPool = pool.reduce((min, p) => Math.min(min, p.config._lastSelectionSeq || 0), Infinity);
}
const relativeSeq = Math.max(0, lastSelectionSeq - minSeqInPool);
const cappedRelativeSeq = Math.min(relativeSeq, 100);
const sequenceScore = cappedRelativeSeq * 1000;
// 惩罚项 C: 负载 (每个活跃请求增加 5 秒权重)
const loadScore = (state.activeCount || 0) * 5000;
// 新鲜节点的微调:配合 usageScore 和 sequenceScore 在多个新鲜节点间轮询
const freshBonus = isFresh ? (now - lastHealthCheckTime) : 0;
return baseScore + usageScore + sequenceScore + loadScore + freshBonus;
}
/**
* 获取指定类型的健康节点数量
*/
getHealthyCount(providerType) {
return (this.providerStatus[providerType] || []).filter(p => p.config.isHealthy && !p.config.isDisabled).length;
}
/**
* 日志输出方法,支持日志级别控制
* @private
*/
_log(level, message) {
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
if (levels[level] >= levels[this.logLevel]) {
logger[level](`[ProviderPoolManager] ${message}`);
}
}
/**
* 记录健康状态变化日志
* @param {string} providerType - 提供商类型
* @param {object} providerConfig - 提供商配置
* @param {string} fromStatus - 之前状态
* @param {string} toStatus - 当前状态
* @param {string} [errorMessage] - 错误信息(可选)
* @private
*/
_logHealthStatusChange(providerType, providerConfig, fromStatus, toStatus, errorMessage = null) {
const customName = providerConfig.customName || providerConfig.uuid;
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
providerType,
uuid: providerConfig.uuid,
customName,
fromStatus,
toStatus,
errorMessage,
usageCount: providerConfig.usageCount || 0,
errorCount: providerConfig.errorCount || 0
};
// 输出详细的状态变化日志
if (toStatus === 'unhealthy') {
logger.warn(`[HealthMonitor] ⚠️ Provider became UNHEALTHY: ${customName} (${providerType})`);
logger.warn(`[HealthMonitor] Reason: ${errorMessage || 'Unknown'}`);
logger.warn(`[HealthMonitor] Error Count: ${providerConfig.errorCount}`);
// 触发告警(如果配置了 Webhook
this._triggerHealthAlert(providerType, providerConfig, 'unhealthy', errorMessage);
} else if (toStatus === 'healthy' && fromStatus === 'unhealthy') {
logger.info(`[HealthMonitor] ✅ Provider recovered to HEALTHY: ${customName} (${providerType})`);
// 触发恢复通知
this._triggerHealthAlert(providerType, providerConfig, 'recovered', null);
}
// 广播健康状态变化事件
broadcastEvent('health_status_change', logEntry);
}
/**
* 触发健康状态告警
* @param {string} providerType - 提供商类型
* @param {object} providerConfig - 提供商配置
* @param {string} status - 状态 ('unhealthy' | 'recovered')
* @param {string} [errorMessage] - 错误信息
* @private
*/
async _triggerHealthAlert(providerType, providerConfig, status, errorMessage = null) {
const webhookUrl = this.globalConfig?.HEALTH_ALERT_WEBHOOK_URL;
if (!webhookUrl) {
return; // 未配置 Webhook跳过
}
const customName = providerConfig.customName || providerConfig.uuid;
const payload = {
timestamp: new Date().toISOString(),
providerType,
uuid: providerConfig.uuid,
customName,
status,
errorMessage,
stats: {
usageCount: providerConfig.usageCount || 0,
errorCount: providerConfig.errorCount || 0
}
};
try {
const axios = (await import('axios')).default;
await axios.post(webhookUrl, payload, {
timeout: 5000,
headers: { 'Content-Type': 'application/json' }
});
this._log('info', `Health alert sent to webhook for ${customName}: ${status}`);
} catch (error) {
this._log('error', `Failed to send health alert to webhook: ${error.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;
}
/**
* 根据 UUID 在所有池中查找提供商配置
* @param {string} uuid - 提供商 UUID
* @returns {object|null} 提供商配置对象或 null
*/
findProviderByUuid(uuid) {
if (!uuid) return null;
for (const type in this.providerStatus) {
const provider = this.providerStatus[type].find(p => p.uuid === uuid);
if (provider) return provider.config;
}
return 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) {
const oldStatus = this.providerStatus[providerType] || [];
this.providerStatus[providerType] = [];
this.roundRobinIndex[providerType] = 0; // Initialize round-robin index for each type
// 只有在锁不存在时才初始化,避免在运行中被重置导致并发问题
if (!this._selectionLocks[providerType]) {
this._selectionLocks[providerType] = Promise.resolve();
}
this.providerPools[providerType].forEach((providerConfig) => {
// 尝试从旧状态中恢复活跃请求计数和队列,避免重载配置时重置并发限制
const existing = oldStatus.find(p => p.uuid === providerConfig.uuid);
// 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;
// --- V2: 刷新监控字段 ---
providerConfig.needsRefresh = providerConfig.needsRefresh !== undefined ? providerConfig.needsRefresh : false;
providerConfig.refreshCount = providerConfig.refreshCount !== undefined ? providerConfig.refreshCount : 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;
providerConfig.customName = providerConfig.customName || null;
this.providerStatus[providerType].push({
config: providerConfig,
uuid: providerConfig.uuid, // Still keep uuid at the top level for easy access
type: providerType, // 保存 providerType 引用
state: existing ? existing.state : {
activeCount: 0,
waitingCount: 0,
queue: []
}
});
});
}
this._log('info', `Initialized provider statuses: ok (maxErrorCount: ${this.maxErrorCount})`);
}
/**
* 获取一个可用的提供商插槽,考虑并发限制和队列
* @param {string} providerType
* @param {string} requestedModel
* @param {object} options
*/
async acquireSlot(providerType, requestedModel = null, options = {}) {
// 使用 selectProvider 进行初次选择(评分逻辑已经包含了并发考虑)
const selectedConfig = await this.selectProvider(providerType, requestedModel, { ...options, skipUsageCount: true });
if (!selectedConfig) {
return null;
}
const provider = this._findProvider(providerType, selectedConfig.uuid);
if (!provider) return selectedConfig;
const config = provider.config;
const state = provider.state;
const concurrencyLimit = parseInt(config.concurrencyLimit || 0);
const queueLimit = parseInt(config.queueLimit || 0);
// 如果没有限制,直接增加活跃计数并返回
if (concurrencyLimit <= 0) {
state.activeCount++;
return config;
}
// 检查是否在并发限制内
if (state.activeCount < concurrencyLimit) {
state.activeCount++;
return config;
}
// 超过并发限制,尝试进入队列
if (queueLimit > 0 && state.waitingCount < queueLimit) {
this._log('info', `[Concurrency] Node ${config.uuid} busy (${state.activeCount}/${concurrencyLimit}), enqueuing request (queue: ${state.waitingCount + 1}/${queueLimit})`);
state.waitingCount++;
try {
// 等待释放信号
await new Promise((resolve, reject) => {
// 设置较短的超时用于测试验证,或者由外部控制
const timeoutMs = options.queueTimeout || 300000;
const timeout = setTimeout(() => {
const idx = state.queue.indexOf(handler);
if (idx !== -1) {
state.queue.splice(idx, 1);
reject(new Error(`Queue timeout after ${timeoutMs/1000}s`));
}
}, timeoutMs);
const handler = () => {
clearTimeout(timeout);
resolve();
};
state.queue.push(handler);
});
} finally {
state.waitingCount--;
}
// 获得信号后,增加活跃计数
state.activeCount++;
return config;
}
// 队列也满了
this._log('warn', `[Concurrency] Node ${config.uuid} full capacity (${state.activeCount}/${concurrencyLimit}, queue: ${state.waitingCount}/${queueLimit}), returning 429`);
const error = new Error('Too many requests: account concurrency limit and queue reached');
error.status = 429;
error.code = 429;
throw error;
}
/**
* 释放提供商插槽
*/
releaseSlot(providerType, uuid) {
if (!providerType || !uuid) return;
const provider = this._findProvider(providerType, uuid);
if (!provider) return;
const state = provider.state;
if (state.activeCount > 0) {
state.activeCount--;
}
// 如果队列中有等待的任务,释放下一个
if (state.queue && state.queue.length > 0) {
const next = state.queue.shift();
if (next) {
// 异步触发
setImmediate(next);
}
}
}
/**
* 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.
*
* 注意:此方法现在返回 Promise使用互斥锁确保并发安全。
*
* @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 {Promise<object|null>} The selected provider's configuration, or null if no healthy provider is found.
*/
async selectProvider(providerType, requestedModel = null, options = {}) {
// 参数校验
if (!providerType || typeof providerType !== 'string') {
this._log('error', `Invalid providerType: ${providerType}`);
return null;
}
// 使用标志位 + 异步等待实现更强力的互斥锁
// 这种方式能更好地处理同一微任务循环内的并发
while (this._isSelecting[providerType]) {
await new Promise(resolve => setImmediate(resolve));
}
this._isSelecting[providerType] = true;
try {
// 在锁内部执行同步选择
return this._doSelectProvider(providerType, requestedModel, options);
} finally {
this._isSelecting[providerType] = false;
}
}
/**
* 实际执行 provider 选择的内部方法(同步执行,由锁保护)
* @private
*/
_doSelectProvider(providerType, requestedModel, options) {
const availableProviders = this.providerStatus[providerType] || [];
// 检查并恢复已到恢复时间的提供商
this._checkAndRecoverScheduledProviders(providerType);
// 获取固定时间戳,确保排序过程中一致
const now = Date.now();
// 提前计算池中最小序列号,避免在排序算法中重复 O(N) 计算
const minSeq = Math.min(...availableProviders.map(p => p.config._lastSelectionSeq || 0));
let availableAndHealthyProviders = availableProviders.filter(p =>
p.config.isHealthy && !p.config.isDisabled && !p.config.needsRefresh
);
// 如果指定了模型,则排除不支持该模型的提供商
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;
}
// 改进:使用统一的评分策略进行选择
// 传入当前时间戳 now 确保一致性
const selected = availableAndHealthyProviders.sort((a, b) => {
const scoreA = this._calculateNodeScore(a, now, minSeq);
const scoreB = this._calculateNodeScore(b, now, minSeq);
if (scoreA !== scoreB) return scoreA - scoreB;
// 如果分值相同,使用 UUID 排序确保确定性
return a.uuid < b.uuid ? -1 : 1;
})[0];
// 始终更新 lastUsed确保 LRU 策略生效,避免并发请求选到同一个 provider
// usageCount 只在请求成功后才增加(由 skipUsageCount 控制)
selected.config.lastUsed = new Date().toISOString();
// 更新自增序列号,确保即使毫秒级并发,也能在下一轮排序中被区分开
this._selectionSequence++;
selected.config._lastSelectionSeq = this._selectionSequence;
// 强制打印选中日志,方便排查并发问题
this._log('info', `[Concurrency Control] Atomic selection: ${selected.config.uuid} (Seq: ${this._selectionSequence})`);
if (!options.skipUsageCount) {
selected.config.usageCount++;
}
// 使用防抖保存(文件 I/O 是异步的,但内存已经更新)
this._debouncedSave(providerType);
this._log('debug', `Selected provider for ${providerType} (LRU): ${selected.config.uuid}${requestedModel ? ` for model: ${requestedModel}` : ''}${options.skipUsageCount ? ' (skip usage count)' : ''}`);
return selected.config;
}
/**
* 获取一个可用的提供商插槽,支持 Fallback 机制
*/
async acquireSlotWithFallback(providerType, requestedModel = null, options = {}) {
if (!providerType || typeof providerType !== 'string') {
this._log('error', `Invalid providerType: ${providerType}`);
return null;
}
const triedTypes = new Set();
const typesToTry = [providerType];
const fallbackTypes = this.fallbackChain[providerType] || [];
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) {
continue;
}
if (currentType !== providerType && requestedModel) {
const primaryProtocol = getProtocolPrefix(providerType);
const fallbackProtocol = getProtocolPrefix(currentType);
if (primaryProtocol !== fallbackProtocol) continue;
const supportedModels = getProviderModels(currentType);
if (supportedModels.length > 0 && !supportedModels.includes(requestedModel)) continue;
}
// 尝试获取插槽
try {
const selectedConfig = await this.acquireSlot(currentType, requestedModel, options);
if (selectedConfig) {
if (currentType !== providerType) {
this._log('info', `Fallback Slot activated (Chain): ${providerType} -> ${currentType} (uuid: ${selectedConfig.uuid})`);
}
return {
config: selectedConfig,
actualProviderType: currentType,
isFallback: currentType !== providerType
};
}
} catch (err) {
if (err.status === 429) {
// 如果是因为 429 (并发/队列满),尝试下一个 Fallback
this._log('info', `Type ${currentType} busy (429), trying next fallback...`);
continue;
}
throw err; // 其他错误抛出
}
}
// Model Fallback Mapping
if (requestedModel && this.modelFallbackMapping && this.modelFallbackMapping[requestedModel]) {
const mapping = this.modelFallbackMapping[requestedModel];
const targetProviderType = mapping.targetProviderType;
const targetModel = mapping.targetModel;
if (targetProviderType && targetModel) {
if (this.providerStatus[targetProviderType] && this.providerStatus[targetProviderType].length > 0) {
try {
const selectedConfig = await this.acquireSlot(targetProviderType, targetModel, options);
if (selectedConfig) {
return {
config: selectedConfig,
actualProviderType: targetProviderType,
isFallback: true,
actualModel: targetModel
};
}
} catch (err) {
// 如果目标类型繁忙,尝试它的 fallback chain
const targetFallbackTypes = this.fallbackChain[targetProviderType] || [];
for (const fallbackType of targetFallbackTypes) {
const targetProtocol = getProtocolPrefix(targetProviderType);
const fallbackProtocol = getProtocolPrefix(fallbackType);
if (targetProtocol !== fallbackProtocol) continue;
const supportedModels = getProviderModels(fallbackType);
if (supportedModels.length > 0 && !supportedModels.includes(targetModel)) continue;
try {
const fallbackSelectedConfig = await this.acquireSlot(fallbackType, targetModel, options);
if (fallbackSelectedConfig) {
return {
config: fallbackSelectedConfig,
actualProviderType: fallbackType,
isFallback: true,
actualModel: targetModel
};
}
} catch (e) {
continue;
}
}
}
}
}
}
return null;
}
/**
* 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.
*/
/**
* Selects a provider from the pool with fallback support.
* When the primary provider type has no healthy providers, it will try fallback types.
*
* 注意:此方法现在返回 Promise因为内部调用的 selectProvider 是异步的。
*
* @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 {Promise<object|null>} An object containing the selected provider's configuration and the actual provider type used, or null if no healthy provider is found.
*/
async selectProviderWithFallback(providerType, requestedModel = null, options = {}) {
// 参数校验
if (!providerType || typeof providerType !== 'string') {
this._log('error', `Invalid providerType: ${providerType}`);
return null;
}
// ==========================
// 优先级 1: Provider Fallback Chain (同协议/兼容协议的回退)
// ==========================
// 记录尝试过的类型,避免循环
const triedTypes = new Set();
const typesToTry = [providerType];
const fallbackTypes = this.fallbackChain[providerType] || [];
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('debug', `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('debug', `Skipping fallback type ${currentType}: protocol mismatch (${primaryProtocol} vs ${fallbackProtocol})`);
continue;
}
// 检查 fallback 类型是否支持请求的模型
const supportedModels = getProviderModels(currentType);
if (supportedModels.length > 0 && !supportedModels.includes(requestedModel)) {
this._log('debug', `Skipping fallback type ${currentType}: model ${requestedModel} not supported`);
continue;
}
}
// 尝试从当前类型选择提供商(现在是异步的)
const selectedConfig = await this.selectProvider(currentType, requestedModel, options);
if (selectedConfig) {
if (currentType !== providerType) {
this._log('info', `Fallback activated (Chain): ${providerType} -> ${currentType} (uuid: ${selectedConfig.uuid})`);
}
return {
config: selectedConfig,
actualProviderType: currentType,
isFallback: currentType !== providerType
};
}
}
// ==========================
// 优先级 2: Model Fallback Mapping (跨协议/特定模型的回退)
// ==========================
if (requestedModel && this.modelFallbackMapping && this.modelFallbackMapping[requestedModel]) {
const mapping = this.modelFallbackMapping[requestedModel];
const targetProviderType = mapping.targetProviderType;
const targetModel = mapping.targetModel;
if (targetProviderType && targetModel) {
this._log('info', `Trying Model Fallback Mapping for ${requestedModel}: -> ${targetProviderType} (${targetModel})`);
// 递归调用 selectProviderWithFallback但这次针对目标提供商类型
// 注意:这里我们直接尝试从目标提供商池中选择,因为如果再次递归可能会导致死循环或逻辑复杂化
// 简单起见,我们直接尝试选择目标提供商
// 检查目标类型是否有配置的池
if (this.providerStatus[targetProviderType] && this.providerStatus[targetProviderType].length > 0) {
// 尝试从目标类型选择提供商(使用转换后的模型名,现在是异步的)
const selectedConfig = await this.selectProvider(targetProviderType, targetModel, options);
if (selectedConfig) {
this._log('info', `Fallback activated (Model Mapping): ${providerType} (${requestedModel}) -> ${targetProviderType} (${targetModel}) (uuid: ${selectedConfig.uuid})`);
return {
config: selectedConfig,
actualProviderType: targetProviderType,
isFallback: true,
actualModel: targetModel // 返回实际使用的模型名,供上层进行请求转换
};
} else {
// 如果目标类型的主池也不可用,尝试目标类型的 fallback chain
// 例如 claude-kiro-oauth (mapped) -> claude-custom (chain)
// 这需要我们小心处理,避免无限递归。
// 我们可以手动检查目标类型的 fallback chain
const targetFallbackTypes = this.fallbackChain[targetProviderType] || [];
for (const fallbackType of targetFallbackTypes) {
// 检查协议兼容性 (目标类型 vs 它的 fallback)
const targetProtocol = getProtocolPrefix(targetProviderType);
const fallbackProtocol = getProtocolPrefix(fallbackType);
if (targetProtocol !== fallbackProtocol) continue;
// 检查模型支持
const supportedModels = getProviderModels(fallbackType);
if (supportedModels.length > 0 && !supportedModels.includes(targetModel)) continue;
const fallbackSelectedConfig = await this.selectProvider(fallbackType, targetModel, options);
if (fallbackSelectedConfig) {
this._log('info', `Fallback activated (Model Mapping -> Chain): ${providerType} (${requestedModel}) -> ${targetProviderType} -> ${fallbackType} (${targetModel}) (uuid: ${fallbackSelectedConfig.uuid})`);
return {
config: fallbackSelectedConfig,
actualProviderType: fallbackType,
isFallback: true,
actualModel: targetModel
};
}
}
}
} else {
this._log('warn', `Model Fallback target provider ${targetProviderType} not configured or empty.`);
}
}
}
this._log('warn', `None available provider found for ${providerType} (Model: ${requestedModel}) after checking fallback chain and model mapping.`);
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;
}
/**
* Gets all available models across all provider pools, with optional format conversion.
* @param {string} [endpointType] - Optional endpoint type for format conversion (OPENAI_MODEL_LIST or GEMINI_MODEL_LIST).
* @returns {Promise<Object|Array>} Formatted model list or raw array of model objects.
*/
async getAllAvailableModels(endpointType = null) {
const allModels = [];
// 获取所有已注册的提供商和号池中的提供商
const registeredProviders = getRegisteredProviders();
const allProviderTypes = Array.from(new Set([...registeredProviders]));
for (const providerType of allProviderTypes) {
if (this.providerStatus[providerType]) {
let models = getProviderModels(providerType);
// 如果硬编码的模型列表为空,或者该类型的提供商在号池中没有配置节点,尝试从服务获取
if (models.length === 0) {
try {
// 确定使用的配置:优先使用号池中第一个节点的配置,否则使用全局配置
let targetConfig = this.globalConfig;
if (this.providerStatus[providerType] && this.providerStatus[providerType].length > 0) {
targetConfig = this.providerStatus[providerType][0].config;
}
const tempConfig = {
...this.globalConfig,
...targetConfig,
MODEL_PROVIDER: providerType
};
const serviceAdapter = getServiceAdapter(tempConfig);
if (typeof serviceAdapter.listModels === 'function') {
const nativeModels = await serviceAdapter.listModels();
// 统一转换为 OpenAI 格式以便提取 ID
const convertedData = convertData(nativeModels, 'modelList', providerType, MODEL_PROVIDER.OPENAI_CUSTOM);
if (convertedData && Array.isArray(convertedData.data)) {
const fetchedModels = convertedData.data.map(m => m.id);
if (fetchedModels.length > 0) {
models = fetchedModels;
}
}
}
} catch (err) {
this._log('debug', `Failed to fetch model list for ${providerType} from service: ${err.message}`);
// 保持原有的 models (可能是硬编码的空列表或 getProviderModels 返回的结果)
}
}
for (const model of models) {
allModels.push({
id: `${providerType}:${model}`,
provider: providerType,
model: model
});
}
}
}
// 如果没有指定 endpointType返回原始数组
if (!endpointType) {
return allModels;
}
// 根据 endpointType 转换为对应格式
if (endpointType === ENDPOINT_TYPE.OPENAI_MODEL_LIST) {
// OpenAI 格式聚合
return {
object: "list",
data: allModels.map(m => ({
id: m.id,
object: "model",
created: Math.floor(Date.now() / 1000),
owned_by: m.provider
}))
};
} else if (endpointType === ENDPOINT_TYPE.GEMINI_MODEL_LIST) {
// Gemini 格式聚合
return {
models: allModels.map(m => ({
name: `models/${m.id}`,
baseModelId: m.model,
version: "v1",
displayName: `${m.model} (${m.provider})`,
description: `Model ${m.model} provided by ${m.provider}`,
supportedGenerationMethods: ["generateContent", "countTokens"]
}))
};
}
// 默认返回空列表
return { data: [] };
}
/**
* 标记提供商需要刷新并推入刷新队列
* @param {string} providerType - 提供商类型
* @param {object} providerConfig - 提供商配置(包含 uuid
*/
markProviderNeedRefresh(providerType, providerConfig) {
if (!providerConfig?.uuid) {
this._log('error', 'Invalid providerConfig in markProviderNeedRefresh');
return;
}
const provider = this._findProvider(providerType, providerConfig.uuid);
if (provider) {
// 防并发机制 A: 如果已经在刷新中,忽略请求
if (this.refreshingUuids.has(provider.uuid)) {
this._log('debug', `Provider ${providerConfig.uuid} is already in refresh queue, ignoring duplicate request.`);
return;
}
// 防并发机制 B: 如果 30 秒内刚刷新过,忽略请求(防止滞后的 401 错误导致重复刷新)
const now = Date.now();
const lastRefreshTime = provider.config.lastRefreshTime || 0;
if (now - lastRefreshTime < 30000) {
this._log('info', `Provider ${providerConfig.uuid} was refreshed recently (${Math.round((now - lastRefreshTime)/1000)}s ago), ignoring refresh request.`);
return;
}
provider.config.needsRefresh = true;
this._log('info', `Marked provider ${providerConfig.uuid} as needsRefresh. Enqueuing...`);
// 推入异步刷新队列
this._enqueueRefresh(providerType, provider, true);
this._debouncedSave(providerType);
}
}
/**
* 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) {
const wasHealthy = provider.config.isHealthy;
const now = Date.now();
const lastErrorTime = provider.config.lastErrorTime ? new Date(provider.config.lastErrorTime).getTime() : 0;
const errorWindowMs = 10000; // 10 秒窗口期
// 如果距离上次错误超过窗口期,重置错误计数
if (now - lastErrorTime > errorWindowMs) {
provider.config.errorCount = 1;
} else {
provider.config.errorCount++;
}
provider.config.lastErrorTime = new Date().toISOString();
// 更新 lastUsed 时间,避免因 LRU 策略导致失败节点被重复选中
provider.config.lastUsed = new Date().toISOString();
// 只要报错,就清除刷新标记,由下次触发或健康检查决定是否需要刷新
provider.config.needsRefresh = false;
provider.config.refreshCount = 0;
// 保存错误信息
if (errorMessage) {
provider.config.lastErrorMessage = errorMessage;
}
if (this.maxErrorCount > 0 && provider.config.errorCount >= this.maxErrorCount) {
provider.config.isHealthy = false;
// 健康状态变化日志
if (wasHealthy) {
this._logHealthStatusChange(providerType, provider.config, 'healthy', 'unhealthy', errorMessage);
}
this._log('warn', `Marked provider as unhealthy: ${providerConfig.uuid} for type ${providerType}. Total errors: ${provider.config.errorCount}`);
}
this._debouncedSave(providerType);
}
}
/**
* Marks a provider as unhealthy immediately (without accumulating error count).
* Used for definitive authentication errors like 401/403.
* @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.
*/
markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage = null) {
if (!providerConfig?.uuid) {
this._log('error', 'Invalid providerConfig in markProviderUnhealthyImmediately');
return;
}
const provider = this._findProvider(providerType, providerConfig.uuid);
if (provider) {
const wasHealthy = provider.config.isHealthy;
provider.config.isHealthy = false;
provider.config.needsRefresh = false; // 报错时不健康,清除刷新标记,防止卡死
provider.config.refreshCount = 0;
provider.config.errorCount = this.maxErrorCount; // Set to max to indicate definitive failure
provider.config.lastErrorTime = new Date().toISOString();
provider.config.lastUsed = new Date().toISOString();
if (errorMessage) {
provider.config.lastErrorMessage = errorMessage;
}
// 健康状态变化日志
if (wasHealthy) {
this._logHealthStatusChange(providerType, provider.config, 'healthy', 'unhealthy', errorMessage);
}
this._log('warn', `Immediately marked provider as unhealthy: ${providerConfig.uuid} for type ${providerType}. Reason: ${errorMessage || 'Authentication error'}`);
this._debouncedSave(providerType);
}
}
/**
* Marks a provider as unhealthy with a scheduled recovery time.
* Used for quota exhaustion errors (402) where the quota will reset at a specific time.
* @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.
* @param {Date|string} [recoveryTime] - Optional recovery time when the provider should be marked healthy again.
*/
markProviderUnhealthyWithRecoveryTime(providerType, providerConfig, errorMessage = null, recoveryTime = null) {
if (!providerConfig?.uuid) {
this._log('error', 'Invalid providerConfig in markProviderUnhealthyWithRecoveryTime');
return;
}
const provider = this._findProvider(providerType, providerConfig.uuid);
if (provider) {
provider.config.isHealthy = false;
provider.config.needsRefresh = false; // 报错时不健康,清除刷新标记,防止卡死
provider.config.refreshCount = 0;
provider.config.errorCount = this.maxErrorCount; // Set to max to indicate definitive failure
provider.config.lastErrorTime = new Date().toISOString();
provider.config.lastUsed = new Date().toISOString();
if (errorMessage) {
provider.config.lastErrorMessage = errorMessage;
}
// Set recovery time if provided
if (recoveryTime) {
const recoveryDate = recoveryTime instanceof Date ? recoveryTime : new Date(recoveryTime);
provider.config.scheduledRecoveryTime = recoveryDate.toISOString();
this._log('warn', `Marked provider as unhealthy with recovery time: ${providerConfig.uuid} for type ${providerType}. Recovery at: ${recoveryDate.toISOString()}. Reason: ${errorMessage || 'Quota exhausted'}`);
} else {
this._log('warn', `Marked provider as unhealthy: ${providerConfig.uuid} for type ${providerType}. Reason: ${errorMessage || 'Quota exhausted'}`);
}
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) {
const wasHealthy = provider.config.isHealthy;
provider.config.isHealthy = true;
provider.config.errorCount = 0;
provider.config.refreshCount = 0;
provider.config.needsRefresh = false;
provider.config.lastRefreshTime = Date.now(); // 标记为健康时也视为刚刷新完成
provider.config.lastErrorTime = null;
provider.config.lastErrorMessage = null;
provider.config._lastSelectionSeq = 0;
// 更新健康检测信息
if (healthCheckModel) {
provider.config.lastHealthCheckTime = new Date().toISOString();
provider.config.lastHealthCheckModel = healthCheckModel;
}
// 只有在明确要求重置使用计数时才重置
if (resetUsageCount) {
provider.config.usageCount = 0;
}else{
provider.config.usageCount++;
provider.config.lastUsed = new Date().toISOString();
}
// 健康状态变化日志
if (!wasHealthy) {
this._logHealthStatusChange(providerType, provider.config, 'unhealthy', 'healthy', null);
}
this._log('info', `Marked provider as healthy: ${provider.config.uuid} for type ${providerType}${resetUsageCount ? ' (usage count reset)' : ''}`);
this._debouncedSave(providerType);
}
}
/**
* 重置提供商的刷新状态needsRefresh 和 refreshCount
* 并将其标记为健康,以便立即投入使用
* @param {string} providerType - 提供商类型
* @param {string} uuid - 提供商 UUID
*/
resetProviderRefreshStatus(providerType, uuid) {
if (!providerType || !uuid) {
this._log('error', 'Invalid parameters in resetProviderRefreshStatus');
return;
}
const provider = this._findProvider(providerType, uuid);
if (provider) {
provider.config.needsRefresh = false;
provider.config.refreshCount = 0;
provider.config.lastRefreshTime = Date.now(); // 显式重置时也更新刷新时间
// 更新为可用
provider.config.lastHealthCheckTime = new Date().toISOString();
// 标记为健康,以便立即投入使用
this._log('info', `Reset refresh status and marked healthy for provider ${uuid} (${providerType})`);
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;
provider.config._lastSelectionSeq = 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);
}
}
/**
* 刷新指定提供商的 UUID
* 用于在认证错误(如 401时更换 UUID以便重新尝试
* @param {string} providerType - 提供商类型
* @param {object} providerConfig - 提供商配置(包含当前 uuid
* @returns {string|null} 新的 UUID如果失败则返回 null
*/
refreshProviderUuid(providerType, providerConfig) {
if (!providerConfig?.uuid) {
this._log('error', 'Invalid providerConfig in refreshProviderUuid');
return null;
}
const provider = this._findProvider(providerType, providerConfig.uuid);
if (provider) {
const oldUuid = provider.config.uuid;
// 生成新的 UUID
const newUuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
// 更新 provider 的 UUID
provider.uuid = newUuid;
provider.config.uuid = newUuid;
// 同时更新 providerPools 中的原始数据
const poolArray = this.providerPools[providerType];
if (poolArray) {
const originalProvider = poolArray.find(p => p.uuid === oldUuid);
if (originalProvider) {
originalProvider.uuid = newUuid;
}
}
this._log('info', `Refreshed provider UUID: ${oldUuid} -> ${newUuid} for type ${providerType}`);
this._debouncedSave(providerType);
return newUuid;
}
this._log('warn', `Provider not found for UUID refresh: ${providerConfig.uuid} in ${providerType}`);
return null;
}
/**
* 检查并恢复已到恢复时间的提供商
* @param {string} [providerType] - 可选,指定要检查的提供商类型。如果不提供,检查所有类型
* @private
*/
_checkAndRecoverScheduledProviders(providerType = null) {
const now = new Date();
const typesToCheck = providerType ? [providerType] : Object.keys(this.providerStatus);
for (const type of typesToCheck) {
const providers = this.providerStatus[type] || [];
for (const providerStatus of providers) {
const config = providerStatus.config;
// 检查是否有 scheduledRecoveryTime 且已到恢复时间
if (config.scheduledRecoveryTime && !config.isHealthy) {
const recoveryTime = new Date(config.scheduledRecoveryTime);
if (now >= recoveryTime) {
this._log('info', `Auto-recovering provider ${config.uuid} (${type}). Scheduled recovery time reached: ${recoveryTime.toISOString()}`);
// 恢复健康状态
config.isHealthy = true;
config.errorCount = 0;
config.lastErrorTime = null;
config.lastErrorMessage = null;
config.scheduledRecoveryTime = null; // 清除恢复时间
// 保存更改
this._debouncedSave(type);
}
}
}
}
}
/**
* Performs health checks on selected providers.
* Respects SCHEDULED_HEALTH_CHECK.providerTypes configuration.
*
* 设计决策:如果没有选择任何 provider types则不进行检查任何 provider。
* 这是有意为之的设计 - 如果用户没有明确选择,则不需要自动健康检查。
* 区别于原来的逻辑(检查所有 provider现在的行为更符合用户预期。
*/
async performHealthChecks() {
const scheduledConfig = this.globalConfig?.SCHEDULED_HEALTH_CHECK;
const selectedProviderTypes = scheduledConfig?.providerTypes;
// 如果没有选择任何 provider types不进行检查
// 设计决策:如果用户没有选择任何 provider明确不执行健康检查是合理的
if (!Array.isArray(selectedProviderTypes) || selectedProviderTypes.length === 0) {
return;
}
this._log('info', 'Performing health checks on selected providers...');
const now = new Date();
// 首先检查并恢复已到恢复时间的提供商
this._checkAndRecoverScheduledProviders();
for (const providerType in this.providerStatus) {
// Only check selected provider types
if (!selectedProviderTypes.includes(providerType)) {
continue;
}
for (const providerStatus of this.providerStatus[providerType]) {
const providerConfig = providerStatus.config;
// 如果提供商有 scheduledRecoveryTime 且未到恢复时间,跳过健康检查
if (providerConfig.scheduledRecoveryTime && !providerConfig.isHealthy) {
const recoveryTime = new Date(providerConfig.scheduledRecoveryTime);
if (now < recoveryTime) {
this._log('debug', `Skipping health check for ${providerConfig.uuid} (${providerType}). Waiting for scheduled recovery at ${recoveryTime.toISOString()}`);
continue;
}
}
// 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);
}
}
}
}
/**
* Performs scheduled health checks on all providers.
* This method is designed to be called periodically to proactively check provider health.
* It respects provider-level isDisabled flag.
*/
async performScheduledHealthChecks() {
const scheduledConfig = this.globalConfig?.SCHEDULED_HEALTH_CHECK;
const checkStartTime = Date.now();
// Check if scheduled health checks are disabled
if (!scheduledConfig?.enabled) {
this._log('debug', '[ScheduledHealthCheck] Scheduled health checks are disabled via configuration');
return;
}
// Get selected provider types
let selectedProviderTypes = scheduledConfig?.providerTypes;
// Validate providerTypes is an array
if (!Array.isArray(selectedProviderTypes) || selectedProviderTypes.length === 0) {
this._log('info', '[ScheduledHealthCheck] No provider types selected, skipping health check');
return;
}
// Count providers to be checked
let totalProviders = 0;
let providersToCheck = [];
for (const providerType in this.providerStatus) {
// Only check selected provider types
if (!selectedProviderTypes.includes(providerType)) {
this._log('debug', `[ScheduledHealthCheck] Skipping provider type ${providerType}: not in selected types`);
continue;
}
for (const provider of this.providerStatus[providerType]) {
// Skip manually disabled providers
if (provider.config.isDisabled === true) {
this._log('debug', `[ScheduledHealthCheck] Skipping ${provider.config.uuid} (${providerType}): manually disabled`);
continue;
}
totalProviders++;
providersToCheck.push({ providerType, provider, uuid: provider.config.uuid, customName: provider.config.customName });
}
}
this._log('info', `[ScheduledHealthCheck] Starting scheduled health checks: ${totalProviders} provider(s) to check (interval: ${scheduledConfig.interval}ms, types: ${selectedProviderTypes.join(', ')})`);
let successCount = 0;
let failCount = 0;
for (const { providerType, provider, uuid, customName } of providersToCheck) {
// Skip if provider became disabled during iteration
if (provider.config.isDisabled === true) {
continue;
}
const providerCheckStart = Date.now();
const checkModelName = provider.config.checkModelName || ProviderPoolManager.DEFAULT_HEALTH_CHECK_MODELS[providerType] || 'unknown';
const displayName = customName || uuid.substring(0, 8);
try {
// Perform health check (health check is based on providerTypes configuration, not per-provider checkHealth flag)
const result = await this._checkProviderHealth(providerType, provider.config);
const checkDuration = Date.now() - providerCheckStart;
if (!result.success) {
// Provider is unhealthy
failCount++;
this._log('warn', `[ScheduledHealthCheck] ${displayName} (${providerType}) FAILED: ${result.errorMessage || 'Provider is not responding correctly.'} (${checkDuration}ms)`);
this.markProviderUnhealthyImmediately(providerType, provider.config, result.errorMessage);
} else {
// Provider is healthy
successCount++;
this._log('info', `[ScheduledHealthCheck] ${displayName} (${providerType}) PASSED: model=${result.modelName || checkModelName} (${checkDuration}ms)`);
this.markProviderHealthy(providerType, provider.config, false, result.modelName);
}
} catch (error) {
const checkDuration = Date.now() - providerCheckStart;
failCount++;
this._log('error', `[ScheduledHealthCheck] ${displayName} (${providerType}) EXCEPTION: ${error.message} (${checkDuration}ms)`);
this.markProviderUnhealthyImmediately(providerType, provider.config, error.message);
}
}
const totalDuration = Date.now() - checkStartTime;
this._log('info', `[ScheduledHealthCheck] Completed: ${successCount} passed, ${failCount} failed, ${totalDuration}ms total`);
}
/**
* 构建健康检查请求(返回多种格式用于重试)
* @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 格式
if (providerType.startsWith('claude-kiro')) {
requests.push({
messages: [baseMessage],
model: modelName,
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.
*
* 设计决策:不检查 providerConfig.checkHealth 标志。
* 健康检查是否执行由上层调用方performScheduledHealthChecks / performHealthChecks
* 通过 providerTypes 数组来决定,不在每个 provider 级别控制。
* 这样简化了逻辑,避免 per-provider 的 checkHealth flag 变得无用。
*
* @param {string} providerType - The type of the provider.
* @param {object} providerConfig - The configuration of the provider to check.
* @returns {Promise<{success: boolean, modelName: string, errorMessage: string}>} - Health check result object.
*/
async _checkProviderHealth(providerType, providerConfig) {
// 确定健康检查使用的模型名称
const modelName = providerConfig.checkModelName ||
ProviderPoolManager.DEFAULT_HEALTH_CHECK_MODELS[providerType];
if (!modelName) {
this._log('warn', `Unknown provider type for health check: ${providerType}. Please check DEFAULT_HEALTH_CHECK_MODELS.`);
return {
success: false,
modelName: null,
errorMessage: `Unknown provider type '${providerType}'. No default health check model configured.`
};
}
// ========== 实际 API 健康检查(带超时保护)==========
const tempConfig = {
...providerConfig,
MODEL_PROVIDER: providerType
};
const serviceAdapter = getServiceAdapter(tempConfig);
// 获取所有可能的请求格式
const healthCheckRequests = this._buildHealthCheckRequests(providerType, modelName);
// 健康检查超时时间15秒避免长时间阻塞
const healthCheckTimeout = 15000;
let lastError = null;
// 重试机制:尝试不同的请求格式
for (let i = 0; i < healthCheckRequests.length; i++) {
const healthCheckRequest = healthCheckRequests[i];
const abortController = new AbortController();
const timeoutId = setTimeout(() => abortController.abort(), healthCheckTimeout);
try {
// 尝试将 signal 注入请求体,供支持的适配器使用
const requestWithSignal = {
...healthCheckRequest,
// signal: abortController.signal
};
await serviceAdapter.generateContent(modelName, requestWithSignal);
clearTimeout(timeoutId);
// 注意使用量计数由调用方处理performScheduledHealthChecks/performHealthChecks
// 这里只返回成功结果,让调用方统一处理状态更新和计数
return { success: true, modelName, errorMessage: null };
} catch (error) {
clearTimeout(timeoutId);
lastError = error;
}
}
// 所有尝试都失败
this._log('warn', `[HealthCheck] ${providerType} failed after ${healthCheckRequests.length} 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}`);
}
}
}