import { getServiceAdapter, serviceInstances } from '../providers/adapter.js'; import { ProviderPoolManager } from '../providers/provider-pool-manager.js'; import deepmerge from 'deepmerge'; import * as fs from 'fs'; import { promises as pfs } from 'fs'; import * as path from 'path'; import { PROVIDER_MAPPINGS, createProviderConfig, addToUsedPaths, isPathUsed, getFileName, formatSystemPath } from '../utils/provider-utils.js'; // 存储 ProviderPoolManager 实例 let providerPoolManager = null; /** * 扫描 configs 目录并自动关联未关联的配置文件到对应的提供商 * @param {Object} config - 服务器配置对象 * @returns {Promise} 更新后的 providerPools 对象 */ export async function autoLinkProviderConfigs(config) { // 确保 providerPools 对象存在 if (!config.providerPools) { config.providerPools = {}; } let totalNewProviders = 0; const allNewProviders = {}; // 遍历所有提供商映射 for (const mapping of PROVIDER_MAPPINGS) { const configsPath = path.join(process.cwd(), 'configs', mapping.dirName); const { providerType, credPathKey, defaultCheckModel, displayName, needsProjectId } = mapping; // 确保提供商类型数组存在 if (!config.providerPools[providerType]) { config.providerPools[providerType] = []; } // 检查目录是否存在 if (!fs.existsSync(configsPath)) { continue; } // 获取已关联的配置文件路径集合 const linkedPaths = new Set(); for (const provider of config.providerPools[providerType]) { if (provider[credPathKey]) { // 使用公共方法添加路径的所有变体格式 addToUsedPaths(linkedPaths, provider[credPathKey]); } } // 递归扫描目录 const newProviders = []; await scanProviderDirectory(configsPath, linkedPaths, newProviders, { credPathKey, defaultCheckModel, needsProjectId }); // 如果有新的配置文件需要关联 if (newProviders.length > 0) { config.providerPools[providerType].push(...newProviders); totalNewProviders += newProviders.length; allNewProviders[displayName] = newProviders; } } // 如果有新的配置文件需要关联,保存更新后的 provider_pools.json if (totalNewProviders > 0) { const filePath = config.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; try { await pfs.writeFile(filePath, JSON.stringify(config.providerPools, null, 2), 'utf8'); console.log(`[Auto-Link] Added ${totalNewProviders} new config(s) to provider pools:`); for (const [displayName, providers] of Object.entries(allNewProviders)) { console.log(` ${displayName}: ${providers.length} config(s)`); providers.forEach(p => { // 获取凭据路径键(支持 _CREDS_FILE_PATH 和 _TOKEN_FILE_PATH 两种格式) const credKey = Object.keys(p).find(k => k.endsWith('_CREDS_FILE_PATH') || k.endsWith('_TOKEN_FILE_PATH') ); if (credKey) { console.log(` - ${p[credKey]}`); } }); } } catch (error) { console.error(`[Auto-Link] Failed to save provider_pools.json: ${error.message}`); } } else { console.log('[Auto-Link] No new configs to link'); } // Update provider pool manager if available if (providerPoolManager) { providerPoolManager.providerPools = config.providerPools; providerPoolManager.initializeProviderStatus(); } return config.providerPools; } /** * 递归扫描提供商配置目录 * @param {string} dirPath - 目录路径 * @param {Set} linkedPaths - 已关联的路径集合 * @param {Array} newProviders - 新提供商配置数组 * @param {Object} options - 配置选项 * @param {string} options.credPathKey - 凭据路径键名 * @param {string} options.defaultCheckModel - 默认检测模型 * @param {boolean} options.needsProjectId - 是否需要 PROJECT_ID */ async function scanProviderDirectory(dirPath, linkedPaths, newProviders, options) { const { credPathKey, defaultCheckModel, needsProjectId } = options; try { const files = await pfs.readdir(dirPath, { withFileTypes: true }); for (const file of files) { const fullPath = path.join(dirPath, file.name); if (file.isFile()) { const ext = path.extname(file.name).toLowerCase(); // 只处理 JSON 文件 if (ext === '.json') { const relativePath = path.relative(process.cwd(), fullPath); const fileName = getFileName(fullPath); // 使用与 ui-manager.js 相同的 isPathUsed 函数检查是否已关联 const isLinked = isPathUsed(relativePath, fileName, linkedPaths); if (!isLinked) { // 使用公共方法创建新的提供商配置 const newProvider = createProviderConfig({ credPathKey, credPath: formatSystemPath(relativePath), defaultCheckModel, needsProjectId }); newProviders.push(newProvider); } } } else if (file.isDirectory()) { // 递归扫描子目录(限制深度为 3 层) const relativePath = path.relative(process.cwd(), fullPath); const depth = relativePath.split(path.sep).length; if (depth < 5) { // configs/{provider}/subfolder/subsubfolder await scanProviderDirectory(fullPath, linkedPaths, newProviders, options); } } } } catch (error) { console.warn(`[Auto-Link] Failed to scan directory ${dirPath}: ${error.message}`); } } // 注意:isValidOAuthCredentials 已移至 provider-utils.js 公共模块 /** * Initialize API services and provider pool manager * @param {Object} config - The server configuration * @returns {Promise} The initialized services */ export async function initApiService(config) { if (config.providerPools && Object.keys(config.providerPools).length > 0) { providerPoolManager = new ProviderPoolManager(config.providerPools, { globalConfig: config, maxErrorCount: config.MAX_ERROR_COUNT ?? 3, providerFallbackChain: config.providerFallbackChain || {}, }); console.log('[Initialization] ProviderPoolManager initialized with configured pools.'); // --- V2: 触发系统预热 --- // 预热逻辑是异步的,不会阻塞服务器启动 providerPoolManager.warmupNodes().catch(err => { console.error(`[Initialization] Warmup failed: ${err.message}`); }); // 检查并刷新即将过期的节点(异步调用,不阻塞启动) providerPoolManager.checkAndRefreshExpiringNodes().catch(err => { console.error(`[Initialization] Check and refresh expiring nodes failed: ${err.message}`); }); // 健康检查将在服务器完全启动后执行 } else { console.log('[Initialization] No provider pools configured. Using single provider mode.'); } // Initialize all provider pool nodes at startup // 初始化号池中所有提供商的所有节点,以避免首个请求的额外延迟 if (config.providerPools && Object.keys(config.providerPools).length > 0) { let totalInitialized = 0; let totalFailed = 0; for (const [providerType, providerConfigs] of Object.entries(config.providerPools)) { // 验证提供商类型是否在 DEFAULT_MODEL_PROVIDERS 中 if (config.DEFAULT_MODEL_PROVIDERS && Array.isArray(config.DEFAULT_MODEL_PROVIDERS)) { if (!config.DEFAULT_MODEL_PROVIDERS.includes(providerType)) { console.log(`[Initialization] Skipping provider type '${providerType}' (not in DEFAULT_MODEL_PROVIDERS).`); continue; } } if (!Array.isArray(providerConfigs) || providerConfigs.length === 0) { continue; } console.log(`[Initialization] Initializing ${providerConfigs.length} node(s) for provider '${providerType}'...`); // 初始化该提供商类型的所有节点 for (const providerConfig of providerConfigs) { // 跳过已禁用的节点 if (providerConfig.isDisabled) { continue; } try { // 合并全局配置和节点配置 const nodeConfig = deepmerge(config, { ...providerConfig, MODEL_PROVIDER: providerType }); delete nodeConfig.providerPools; // 移除 providerPools 避免递归 // 初始化服务适配器 getServiceAdapter(nodeConfig); totalInitialized++; const identifier = providerConfig.customName || providerConfig.uuid || 'unknown'; console.log(` ✓ Initialized node: ${identifier}`); } catch (error) { totalFailed++; const identifier = providerConfig.customName || providerConfig.uuid || 'unknown'; console.warn(` ✗ Failed to initialize node ${identifier}: ${error.message}`); } } } console.log(`[Initialization] Provider pool initialization complete: ${totalInitialized} succeeded, ${totalFailed} failed.`); } else { console.log('[Initialization] No provider pools configured. Skipping node initialization.'); } return serviceInstances; // Return the collection of initialized service instances } /** * Get API service adapter, considering provider pools * @param {Object} config - The current request configuration * @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} The API service adapter */ export async function getApiService(config, requestedModel = null, options = {}) { let serviceConfig = config; if (providerPoolManager && config.providerPools && config.providerPools[config.MODEL_PROVIDER]) { // 如果有号池管理器,并且当前模型提供者类型有对应的号池,则从号池中选择一个提供者配置 // selectProvider 现在是异步的,使用链式锁确保并发安全 const selectedProviderConfig = await providerPoolManager.selectProvider(config.MODEL_PROVIDER, requestedModel, { skipUsageCount: true }); if (selectedProviderConfig) { // 合并选中的提供者配置到当前请求的 config 中 serviceConfig = deepmerge(config, selectedProviderConfig); delete serviceConfig.providerPools; // 移除 providerPools 属性 config.uuid = serviceConfig.uuid; config.customName = serviceConfig.customName; const customNameDisplay = serviceConfig.customName ? ` (${serviceConfig.customName})` : ''; console.log(`[API Service] Using pooled configuration for ${config.MODEL_PROVIDER}: ${serviceConfig.uuid}${customNameDisplay}${requestedModel ? ` (model: ${requestedModel})` : ''}`); } else { const errorMsg = `[API Service] No healthy provider found in pool for ${config.MODEL_PROVIDER}${requestedModel ? ` supporting model: ${requestedModel}` : ''}`; console.error(errorMsg); throw new Error(errorMsg); } } return getServiceAdapter(serviceConfig); } /** * Get API service adapter with fallback support and return detailed result * @param {Object} config - The current request configuration * @param {string} [requestedModel] - Optional. The model name to filter providers by. * @param {Object} [options] - Optional. Additional options. * @returns {Promise} Object containing service adapter and metadata */ export async function getApiServiceWithFallback(config, requestedModel = null, options = {}) { let serviceConfig = config; let actualProviderType = config.MODEL_PROVIDER; let isFallback = false; let selectedUuid = null; let actualModel = null; if (providerPoolManager && config.providerPools && config.providerPools[config.MODEL_PROVIDER]) { // selectProviderWithFallback 现在是异步的,使用链式锁确保并发安全 const selectedResult = await providerPoolManager.selectProviderWithFallback( config.MODEL_PROVIDER, requestedModel, { skipUsageCount: true } ); if (selectedResult) { const { config: selectedProviderConfig, actualProviderType: selectedType, isFallback: fallbackUsed, actualModel: fallbackModel } = selectedResult; // 合并选中的提供者配置到当前请求的 config 中 serviceConfig = deepmerge(config, selectedProviderConfig); delete serviceConfig.providerPools; actualProviderType = selectedType; isFallback = fallbackUsed; selectedUuid = selectedProviderConfig.uuid; actualModel = fallbackModel; // 如果发生了 fallback,需要更新 MODEL_PROVIDER if (isFallback) { serviceConfig.MODEL_PROVIDER = actualProviderType; } } else { const errorMsg = `[API Service] No healthy provider found in pool (including fallback) for ${config.MODEL_PROVIDER}${requestedModel ? ` supporting model: ${requestedModel}` : ''}`; console.error(errorMsg); throw new Error(errorMsg); } } const service = getServiceAdapter(serviceConfig); return { service, serviceConfig, actualProviderType, isFallback, uuid: selectedUuid, actualModel }; } /** * Get the provider pool manager instance * @returns {Object} The provider pool manager */ export function getProviderPoolManager() { return providerPoolManager; } /** * Mark provider as unhealthy * @param {string} provider - The model provider * @param {Object} providerInfo - Provider information including uuid */ export function markProviderUnhealthy(provider, providerInfo) { if (providerPoolManager) { providerPoolManager.markProviderUnhealthy(provider, providerInfo); } } /** * Get providers status * @param {Object} config - The current request configuration * @param {Object} [options] - Optional. Additional options. * @param {boolean} [options.provider] - Optional.provider filter by provider type * @param {boolean} [options.customName] - Optional.customName filter by customName * @returns {Promise} The API service adapter */ export async function getProviderStatus(config, options = {}) { let providerPools = {}; const filePath = config.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; try { if (providerPoolManager && providerPoolManager.providerPools) { providerPools = providerPoolManager.providerPools; } else if (filePath && fs.existsSync(filePath)) { const poolsData = JSON.parse(fs.readFileSync(filePath, 'utf-8')); providerPools = poolsData; } } catch (error) { console.warn('[API Service] Failed to load provider pools:', error.message); } // providerPoolsSlim 只保留顶级 key 及部分字段,过滤 isDisabled 为 true 的元素 const slimFields = [ 'customName', 'isHealthy', 'lastErrorTime', 'lastErrorMessage' ]; // identify 字段映射表 const identifyFieldMap = { 'openai-custom': 'OPENAI_BASE_URL', 'openaiResponses-custom': 'OPENAI_BASE_URL', 'gemini-cli-oauth': 'GEMINI_OAUTH_CREDS_FILE_PATH', 'claude-custom': 'CLAUDE_BASE_URL', 'claude-kiro-oauth': 'KIRO_OAUTH_CREDS_FILE_PATH', 'openai-qwen-oauth': 'QWEN_OAUTH_CREDS_FILE_PATH', 'gemini-antigravity': 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH', 'openai-iflow': 'IFLOW_TOKEN_FILE_PATH' }; let providerPoolsSlim = []; let unhealthyProvideIdentifyList = []; let count = 0; let unhealthyCount = 0; let unhealthyRatio = 0; const filterProvider = options && options.provider; const filterCustomName = options && options.customName; for (const key of Object.keys(providerPools)) { if (!Array.isArray(providerPools[key])) continue; if (filterProvider && key !== filterProvider) continue; const identifyField = identifyFieldMap[key] || null; const slimArr = providerPools[key] .filter(item => { if (item.isDisabled) return false; if (filterCustomName && item.customName !== filterCustomName) return false; return true; }) .map(item => { const slim = {}; for (const f of slimFields) { slim[f] = item.hasOwnProperty(f) ? item[f] : null; } // identify 字段 if (identifyField && item.hasOwnProperty(identifyField)) { let tmpCustomName = item.customName ? `${item.customName}` : 'NoCustomName'; let identifyStr = `${tmpCustomName}::${key}::${item[identifyField]}`; slim.identify = identifyStr; } else { slim.identify = null; } slim.provider = key; // 统计 count++; if (slim.isHealthy === false) { unhealthyCount++; if (slim.identify) unhealthyProvideIdentifyList.push(slim.identify); } return slim; }); providerPoolsSlim.push(...slimArr); } if (count > 0) { unhealthyRatio = Number((unhealthyCount / count).toFixed(2)); } let unhealthySummeryMessage = unhealthyProvideIdentifyList.join('\n'); if (unhealthySummeryMessage === '') unhealthySummeryMessage = null; return { providerPoolsSlim, unhealthySummeryMessage, count, unhealthyCount, unhealthyRatio }; }