import { existsSync, readFileSync, writeFileSync } from 'fs'; import logger from '../utils/logger.js'; import { getRequestBody } from '../utils/common.js'; import { getAllProviderModels, getProviderModels } from '../providers/provider-models.js'; import { generateUUID, createProviderConfig, formatSystemPath, detectProviderFromPath, addToUsedPaths, isPathUsed, pathsEqual } from '../utils/provider-utils.js'; import { broadcastEvent } from './event-broadcast.js'; import { getRegisteredProviders } from '../providers/adapter.js'; // 文件级互斥锁:防止并发读写导致数据丢失 // 安全净化:移除用户输入字段中的危险内容(script、事件处理器、javascript:协议等), // 存储原始文本。HTML 转义统一由前端 escHtml() 负责,避免双编码问题。 // 安全净化:移除用户输入字段中的危险内容,并可选地过滤敏感 API 密钥 function sanitizeProviderData(provider, maskSensitive = false) { if (!provider || typeof provider !== 'object') return provider; const sanitized = { ...provider }; // 1. 过滤敏感字段(API Keys, Tokens 等) if (maskSensitive) { const sensitiveKeys = [ 'OPENAI_API_KEY', 'CLAUDE_API_KEY', 'FORWARD_API_KEY', 'GROK_COOKIE_TOKEN', 'GROK_CF_CLEARANCE', 'refreshToken', 'accessToken', 'clientSecret' ]; sensitiveKeys.forEach(key => { if (sanitized[key]) { // 对密钥进行脱敏显示(只保留前 4 位和后 4 位) const val = sanitized[key]; if (typeof val === 'string' && val.length > 10) { sanitized[key] = val.substring(0, 4) + '****' + val.substring(val.length - 4); } else { sanitized[key] = '********'; } } }); } // 2. 净化 customName 中的 HTML/脚本 if (typeof sanitized.customName === 'string') { let name = sanitized.customName; if (/(?:data|javascript|vbscript)\s*:/i.test(name)) { sanitized.customName = ''; return sanitized; } name = name.replace(/<[^>]*>/g, ''); name = name.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, ''); name = name.replace(/&[#\w]+;/g, ''); sanitized.customName = name.trim(); } return sanitized; } function sanitizeProviderPools(pools, maskSensitive = false) { if (!pools || typeof pools !== 'object') return pools; const sanitized = {}; for (const [type, providers] of Object.entries(pools)) { sanitized[type] = Array.isArray(providers) ? providers.map(p => sanitizeProviderData(p, maskSensitive)) : providers; } return sanitized; } // 使用 Promise 链式队列,确保文件操作顺序执行 let _fileLockChain = Promise.resolve(); // 超时包装函数:防止操作永久挂起导致锁链阻塞 function withTimeout(promise, ms = 30000) { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error(`Operation timeout after ${ms}ms`)), ms) ) ]); } function withFileLock(fn) { const next = _fileLockChain .then(() => withTimeout(fn(), 30000)) .catch(err => { // 记录错误并抛出,中断操作 logger.error('[FileLock] Operation failed:', err?.message || err); throw err; }); _fileLockChain = next.then(() => {}).catch(() => {}); return next; } /** * 获取所有提供商的状态(包括支持的类型和号池组) */ export async function handleGetProviders(req, res, currentConfig, providerPoolManager) { if (!providerPoolManager) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Provider pool manager not initialized' } })); return true; } // 1. 获取支持的基础提供商类型 const registeredProviders = getRegisteredProviders(); let poolTypes = []; // 2. 从管理器获取当前所有池的状态 const providerStatus = {}; for (const [type, providers] of Object.entries(providerPoolManager.providerStatus)) { providerStatus[type] = providers.map(p => ({ ...p.config, activeRequests: p.state?.activeCount || 0, waitingRequests: p.state?.waitingCount || 0 })); } // 3. 补全号池配置文件中的所有组 const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; try { if (existsSync(filePath)) { const poolsData = JSON.parse(readFileSync(filePath, 'utf-8')); poolTypes = Object.keys(poolsData); poolTypes.forEach(type => { if (!providerStatus[type]) { providerStatus[type] = []; } }); } } catch (error) { logger.warn('[UI API] Failed to supplement provider status:', error.message); } // 合并生成支持的类型列表 const supportedProviders = [...new Set([...registeredProviders, ...poolTypes])]; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ providers: sanitizeProviderPools(providerStatus, true), // 列表显示进行打码 supportedProviders: supportedProviders })); return true; } /** * 获取特定提供商类型的详细信息 */ export async function handleGetProviderType(req, res, currentConfig, providerPoolManager, providerType) { let providerPools = {}; const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; try { if (providerPoolManager && providerPoolManager.providerPools) { providerPools = providerPoolManager.providerPools; } else if (filePath && existsSync(filePath)) { const poolsData = JSON.parse(readFileSync(filePath, 'utf-8')); providerPools = poolsData; } } catch (error) { logger.warn('[UI API] Failed to load provider pools:', error.message); } const providers = providerPools[providerType] || []; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ providerType, providers: providers.map(p => sanitizeProviderData(p, false)), // 详情页(用于编辑)不打码 totalCount: providers.length, healthyCount: providers.filter(p => p.isHealthy).length })); return true; } /** * 获取支持的提供商类型(已注册适配器的,以及号池中已存在的自定义类型) */ export async function handleGetSupportedProviders(req, res, currentConfig, providerPoolManager) { const registeredProviders = getRegisteredProviders(); let poolTypes = []; const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; try { if (providerPoolManager && providerPoolManager.providerPools) { poolTypes = Object.keys(providerPoolManager.providerPools); } else if (filePath && existsSync(filePath)) { const poolsData = JSON.parse(readFileSync(filePath, 'utf-8')); poolTypes = Object.keys(poolsData); } } catch (error) { logger.warn('[UI API] Failed to load provider pools for supported types:', error.message); } // 合并注册的提供商和号池中的类型 const supportedProviders = [...new Set([...registeredProviders, ...poolTypes])]; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(supportedProviders)); return true; } /** * 获取所有提供商的可用模型(支持动态配置组) */ export async function handleGetProviderModels(req, res, currentConfig, providerPoolManager) { const registeredProviders = getRegisteredProviders(); let poolTypes = []; // 获取所有存在的类型(基础 + 动态) const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; try { if (providerPoolManager && providerPoolManager.providerPools) { poolTypes = Object.keys(providerPoolManager.providerPools); } else if (existsSync(filePath)) { const poolsData = JSON.parse(readFileSync(filePath, 'utf-8')); poolTypes = Object.keys(poolsData); } } catch (error) { logger.warn('[UI API] Failed to load provider pools for models:', error.message); } const allTypes = [...new Set([...registeredProviders, ...poolTypes])]; const allModels = {}; allTypes.forEach(type => { const models = getProviderModels(type); if (models && models.length > 0) { allModels[type] = models; } }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(allModels)); return true; } /** * 获取特定提供商类型的可用模型 */ export async function handleGetProviderTypeModels(req, res, providerType) { const models = getProviderModels(providerType); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ providerType, models })); return true; } /** * 添加新的提供商配置 */ export async function handleAddProvider(req, res, currentConfig, providerPoolManager) { return withFileLock(() => _handleAddProvider(req, res, currentConfig, providerPoolManager)).catch(err => { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'File operation failed: ' + err.message } })); return true; }); } async function _handleAddProvider(req, res, currentConfig, providerPoolManager) { try { const body = await getRequestBody(req); const { providerType, providerConfig } = body; if (!providerType || !providerConfig) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'providerType and providerConfig are required' } })); return true; } // Generate UUID if not provided if (!providerConfig.uuid) { providerConfig.uuid = generateUUID(); } // Set default values providerConfig.isHealthy = providerConfig.isHealthy !== undefined ? providerConfig.isHealthy : true; providerConfig.lastUsed = providerConfig.lastUsed || null; providerConfig.usageCount = providerConfig.usageCount || 0; providerConfig.errorCount = providerConfig.errorCount || 0; providerConfig.lastErrorTime = providerConfig.lastErrorTime || null; const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; let providerPools = {}; // Load existing pools if (existsSync(filePath)) { try { const fileContent = readFileSync(filePath, 'utf-8'); providerPools = JSON.parse(fileContent); } catch (readError) { logger.warn('[UI API] Failed to read existing provider pools:', readError.message); } } // Add new provider to the appropriate type if (!providerPools[providerType]) { providerPools[providerType] = []; } providerPools[providerType].push(providerConfig); // Save to file writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); logger.info(`[UI API] Added new provider to ${providerType}: ${providerConfig.uuid}`); // Update provider pool manager if available if (providerPoolManager) { providerPoolManager.providerPools = providerPools; providerPoolManager.initializeProviderStatus(); } // 广播更新事件 broadcastEvent('config_update', { action: 'add', filePath: filePath, providerType, providerConfig: sanitizeProviderData(providerConfig), timestamp: new Date().toISOString() }); // 广播提供商更新事件 broadcastEvent('provider_update', { action: 'add', providerType, providerConfig: sanitizeProviderData(providerConfig), timestamp: new Date().toISOString() }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: 'Provider added successfully', provider: sanitizeProviderData(providerConfig), providerType })); return true; } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: error.message } })); return true; } } /** * 更新特定提供商配置 */ export async function handleUpdateProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid) { return withFileLock(() => _handleUpdateProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid)).catch(err => { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'File operation failed: ' + err.message } })); return true; }); } async function _handleUpdateProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid) { try { const body = await getRequestBody(req); const { providerConfig } = body; if (!providerConfig) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'providerConfig is required' } })); return true; } const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; let providerPools = {}; // Load existing pools if (existsSync(filePath)) { try { const fileContent = readFileSync(filePath, 'utf-8'); providerPools = JSON.parse(fileContent); } catch (readError) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Provider pools file not found' } })); return true; } } // Find and update the provider const providers = providerPools[providerType] || []; const providerIndex = providers.findIndex(p => p.uuid === providerUuid); if (providerIndex === -1) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Provider not found' } })); return true; } // Update provider while preserving certain fields const existingProvider = providers[providerIndex]; const updatedProvider = { ...existingProvider, ...providerConfig, uuid: providerUuid, // Ensure UUID doesn't change lastUsed: existingProvider.lastUsed, // Preserve usage stats usageCount: existingProvider.usageCount, errorCount: existingProvider.errorCount, lastErrorTime: existingProvider.lastErrorTime }; providerPools[providerType][providerIndex] = updatedProvider; // Save to file writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); logger.info(`[UI API] Updated provider ${providerUuid} in ${providerType}`); // Update provider pool manager if available if (providerPoolManager) { providerPoolManager.providerPools = providerPools; providerPoolManager.initializeProviderStatus(); } // 广播更新事件 broadcastEvent('config_update', { action: 'update', filePath: filePath, providerType, providerConfig: sanitizeProviderData(updatedProvider), timestamp: new Date().toISOString() }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: 'Provider updated successfully', provider: sanitizeProviderData(updatedProvider) })); return true; } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: error.message } })); return true; } } /** * 删除特定提供商配置 */ export async function handleDeleteProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid) { return withFileLock(() => _handleDeleteProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid)).catch(err => { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'File operation failed: ' + err.message } })); return true; }); } async function _handleDeleteProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid) { try { const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; let providerPools = {}; // Load existing pools if (existsSync(filePath)) { try { const fileContent = readFileSync(filePath, 'utf-8'); providerPools = JSON.parse(fileContent); } catch (readError) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Provider pools file not found' } })); return true; } } // Find and remove the provider const providers = providerPools[providerType] || []; const providerIndex = providers.findIndex(p => p.uuid === providerUuid); if (providerIndex === -1) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Provider not found' } })); return true; } const deletedProvider = providers[providerIndex]; providers.splice(providerIndex, 1); // Remove the entire provider type if no providers left if (providers.length === 0) { delete providerPools[providerType]; } // Save to file writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); logger.info(`[UI API] Deleted provider ${providerUuid} from ${providerType}`); // Update provider pool manager if available if (providerPoolManager) { providerPoolManager.providerPools = providerPools; providerPoolManager.initializeProviderStatus(); } // 广播更新事件 broadcastEvent('config_update', { action: 'delete', filePath: filePath, providerType, providerConfig: sanitizeProviderData(deletedProvider), timestamp: new Date().toISOString() }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: 'Provider deleted successfully', deletedProvider: sanitizeProviderData(deletedProvider) })); return true; } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: error.message } })); return true; } } /** * 禁用/启用特定提供商配置 */ export async function handleDisableEnableProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid, action) { return withFileLock(() => _handleDisableEnableProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid, action)).catch(err => { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'File operation failed: ' + err.message } })); return true; }); } async function _handleDisableEnableProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid, action) { try { const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; let providerPools = {}; // Load existing pools if (existsSync(filePath)) { try { const fileContent = readFileSync(filePath, 'utf-8'); providerPools = JSON.parse(fileContent); } catch (readError) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Provider pools file not found' } })); return true; } } // Find and update the provider const providers = providerPools[providerType] || []; const providerIndex = providers.findIndex(p => p.uuid === providerUuid); if (providerIndex === -1) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Provider not found' } })); return true; } // Update isDisabled field const provider = providers[providerIndex]; provider.isDisabled = action === 'disable'; // Save to file writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); logger.info(`[UI API] ${action === 'disable' ? 'Disabled' : 'Enabled'} provider ${providerUuid} in ${providerType}`); // Update provider pool manager if available if (providerPoolManager) { providerPoolManager.providerPools = providerPools; // Call the appropriate method if (action === 'disable') { providerPoolManager.disableProvider(providerType, provider); } else { providerPoolManager.enableProvider(providerType, provider); } } // 广播更新事件 broadcastEvent('config_update', { action: action, filePath: filePath, providerType, providerConfig: sanitizeProviderData(provider), timestamp: new Date().toISOString() }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: `Provider ${action}d successfully`, provider: sanitizeProviderData(provider) })); return true; } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: error.message } })); return true; } } /** * 重置特定提供商类型的所有提供商健康状态 */ export async function handleResetProviderHealth(req, res, currentConfig, providerPoolManager, providerType) { try { const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; let providerPools = {}; // Load existing pools if (existsSync(filePath)) { try { const fileContent = readFileSync(filePath, 'utf-8'); providerPools = JSON.parse(fileContent); } catch (readError) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Provider pools file not found' } })); return true; } } // Reset health status for all providers of this type const providers = providerPools[providerType] || []; if (providers.length === 0) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'No providers found for this type' } })); return true; } let resetCount = 0; providers.forEach(provider => { // 统计 isHealthy 从 false 变为 true 的节点数量 if (!provider.isHealthy) { resetCount++; } // 重置所有节点的状态 provider.isHealthy = true; provider.errorCount = 0; provider.refreshCount = 0; provider.needsRefresh = false; provider.lastErrorTime = null; }); // Save to file writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); logger.info(`[UI API] Reset health status for ${resetCount} providers in ${providerType}`); // Update provider pool manager if available if (providerPoolManager) { providerPoolManager.providerPools = providerPools; providerPoolManager.initializeProviderStatus(); } // 广播更新事件 broadcastEvent('config_update', { action: 'reset_health', filePath: filePath, providerType, resetCount, timestamp: new Date().toISOString() }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: `Successfully reset health status for ${resetCount} providers`, resetCount, totalCount: providers.length })); return true; } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: error.message } })); return true; } } /** * 删除特定提供商类型的所有不健康节点 */ export async function handleDeleteUnhealthyProviders(req, res, currentConfig, providerPoolManager, providerType) { try { const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; let providerPools = {}; // Load existing pools if (existsSync(filePath)) { try { const fileContent = readFileSync(filePath, 'utf-8'); providerPools = JSON.parse(fileContent); } catch (readError) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Provider pools file not found' } })); return true; } } // Find and remove unhealthy providers const providers = providerPools[providerType] || []; if (providers.length === 0) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'No providers found for this type' } })); return true; } // Filter out unhealthy providers (keep only healthy ones) const unhealthyProviders = providers.filter(p => !p.isHealthy); const healthyProviders = providers.filter(p => p.isHealthy); if (unhealthyProviders.length === 0) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: 'No unhealthy providers to delete', deletedCount: 0, remainingCount: providers.length })); return true; } // Update the provider pool with only healthy providers if (healthyProviders.length === 0) { delete providerPools[providerType]; } else { providerPools[providerType] = healthyProviders; } // Save to file writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); logger.info(`[UI API] Deleted ${unhealthyProviders.length} unhealthy providers from ${providerType}`); // Update provider pool manager if available if (providerPoolManager) { providerPoolManager.providerPools = providerPools; providerPoolManager.initializeProviderStatus(); } // 广播更新事件 broadcastEvent('config_update', { action: 'delete_unhealthy', filePath: filePath, providerType, deletedCount: unhealthyProviders.length, deletedProviders: unhealthyProviders.map(p => sanitizeProviderData({ uuid: p.uuid, customName: p.customName })), timestamp: new Date().toISOString() }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: `Successfully deleted ${unhealthyProviders.length} unhealthy providers`, deletedCount: unhealthyProviders.length, remainingCount: healthyProviders.length, deletedProviders: unhealthyProviders.map(p => ({ uuid: p.uuid, customName: p.customName })) })); return true; } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: error.message } })); return true; } } /** * 批量刷新特定提供商类型的所有不健康节点的 UUID */ export async function handleRefreshUnhealthyUuids(req, res, currentConfig, providerPoolManager, providerType) { try { const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; let providerPools = {}; // Load existing pools if (existsSync(filePath)) { try { const fileContent = readFileSync(filePath, 'utf-8'); providerPools = JSON.parse(fileContent); } catch (readError) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Provider pools file not found' } })); return true; } } // Find unhealthy providers const providers = providerPools[providerType] || []; if (providers.length === 0) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'No providers found for this type' } })); return true; } // Filter unhealthy providers and refresh their UUIDs const refreshedProviders = []; for (const provider of providers) { if (!provider.isHealthy) { const oldUuid = provider.uuid; const newUuid = generateUUID(); provider.uuid = newUuid; refreshedProviders.push({ oldUuid, newUuid, customName: provider.customName }); } } if (refreshedProviders.length === 0) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: 'No unhealthy providers to refresh', refreshedCount: 0, totalCount: providers.length })); return true; } // Save to file writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); logger.info(`[UI API] Refreshed UUIDs for ${refreshedProviders.length} unhealthy providers in ${providerType}`); // Update provider pool manager if available if (providerPoolManager) { providerPoolManager.providerPools = providerPools; providerPoolManager.initializeProviderStatus(); } // 广播更新事件 broadcastEvent('config_update', { action: 'refresh_unhealthy_uuids', filePath: filePath, providerType, refreshedCount: refreshedProviders.length, refreshedProviders: refreshedProviders.map(p => sanitizeProviderData(p)), timestamp: new Date().toISOString() }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: `Successfully refreshed UUIDs for ${refreshedProviders.length} unhealthy providers`, refreshedCount: refreshedProviders.length, totalCount: providers.length, refreshedProviders })); return true; } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: error.message } })); return true; } } /** * 对特定提供商类型的所有提供商执行健康检查 */ export async function handleHealthCheck(req, res, currentConfig, providerPoolManager, providerType) { try { if (!providerPoolManager) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Provider pool manager not initialized' } })); return true; } const providers = providerPoolManager.providerStatus[providerType] || []; if (providers.length === 0) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'No providers found for this type' } })); return true; } // 只检测不健康的节点 const unhealthyProviders = providers.filter(ps => !ps.config.isHealthy); if (unhealthyProviders.length === 0) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: 'No unhealthy providers to check', successCount: 0, failCount: 0, totalCount: providers.length, results: [] })); return true; } logger.info(`[UI API] Starting health check for ${unhealthyProviders.length} unhealthy providers in ${providerType} (total: ${providers.length})`); // 执行健康检测(检查所有未禁用的 unhealthy providers) const results = []; for (const providerStatus of unhealthyProviders) { const providerConfig = providerStatus.config; // 跳过已禁用的节点 if (providerConfig.isDisabled) { logger.info(`[UI API] Skipping health check for disabled provider: ${providerConfig.uuid}`); continue; } try { const healthResult = await providerPoolManager._checkProviderHealth(providerType, providerConfig); if (healthResult.success) { providerPoolManager.markProviderHealthy(providerType, providerConfig, false, healthResult.modelName); results.push({ uuid: providerConfig.uuid, success: true, modelName: healthResult.modelName, message: 'Healthy' }); } else { // 检查是否为认证错误(401/403),如果是则立即标记为不健康 const errorMessage = healthResult.errorMessage || 'Check failed'; const isAuthError = /\b(401|403)\b/.test(errorMessage) || /\b(Unauthorized|Forbidden|AccessDenied|InvalidToken|ExpiredToken)\b/i.test(errorMessage); if (isAuthError) { providerPoolManager.markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage); logger.info(`[UI API] Auth error detected for ${providerConfig.uuid}, immediately marked as unhealthy`); } else { providerPoolManager.markProviderUnhealthy(providerType, providerConfig, errorMessage); } providerStatus.config.lastHealthCheckTime = new Date().toISOString(); if (healthResult.modelName) { providerStatus.config.lastHealthCheckModel = healthResult.modelName; } results.push({ uuid: providerConfig.uuid, success: false, modelName: healthResult.modelName, message: errorMessage, isAuthError: isAuthError }); } } catch (error) { const errorMessage = error.message || 'Unknown error'; // 检查是否为认证错误(401/403),如果是则立即标记为不健康 const isAuthError = /\b(401|403)\b/.test(errorMessage) || /\b(Unauthorized|Forbidden|AccessDenied|InvalidToken|ExpiredToken)\b/i.test(errorMessage); if (isAuthError) { providerPoolManager.markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage); logger.info(`[UI API] Auth error detected for ${providerConfig.uuid}, immediately marked as unhealthy`); } else { providerPoolManager.markProviderUnhealthy(providerType, providerConfig, errorMessage); } results.push({ uuid: providerConfig.uuid, success: false, message: errorMessage, isAuthError: isAuthError }); } } // 保存更新后的状态到文件 const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; // 从 providerStatus 构建 providerPools 对象并保存 const providerPools = {}; for (const pType in providerPoolManager.providerStatus) { providerPools[pType] = providerPoolManager.providerStatus[pType].map(ps => ps.config); } writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); const successCount = results.filter(r => r.success === true).length; const failCount = results.filter(r => r.success === false).length; logger.info(`[UI API] Health check completed for ${providerType}: ${successCount} recovered, ${failCount} still unhealthy (checked ${unhealthyProviders.length} unhealthy nodes)`); // 广播更新事件 broadcastEvent('config_update', { action: 'health_check', filePath: filePath, providerType, results: results.map(r => ({ ...r, message: sanitizeProviderData({ message: r.message }).message })), timestamp: new Date().toISOString() }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: `Health check completed: ${successCount} healthy, ${failCount} unhealthy`, successCount, failCount, totalCount: providers.length, results })); return true; } catch (error) { logger.error('[UI API] Health check error:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: error.message } })); return true; } } /** * 快速链接配置文件到对应的提供商 * 支持单个文件路径或文件路径数组 */ export async function handleQuickLinkProvider(req, res, currentConfig, providerPoolManager) { try { const body = await getRequestBody(req); const { filePath, filePaths } = body; // 支持单个文件路径或文件路径数组 const pathsToLink = filePaths || (filePath ? [filePath] : []); if (!pathsToLink || pathsToLink.length === 0) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'filePath or filePaths is required' } })); return true; } const poolsFilePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; // Load existing pools let providerPools = {}; if (existsSync(poolsFilePath)) { try { const fileContent = readFileSync(poolsFilePath, 'utf-8'); providerPools = JSON.parse(fileContent); } catch (readError) { logger.warn('[UI API] Failed to read existing provider pools:', readError.message); } } const results = []; const linkedProviders = []; // 处理每个文件路径 for (const currentFilePath of pathsToLink) { const normalizedPath = currentFilePath.replace(/\\/g, '/').toLowerCase(); // 根据文件路径自动识别提供商类型 const providerMapping = detectProviderFromPath(normalizedPath); if (!providerMapping) { results.push({ filePath: currentFilePath, success: false, error: 'Unable to identify provider type for config file' }); continue; } const { providerType, credPathKey, defaultCheckModel, displayName } = providerMapping; // Ensure provider type array exists if (!providerPools[providerType]) { providerPools[providerType] = []; } // Check if already linked - 使用标准化路径进行比较 const normalizedForComparison = currentFilePath.replace(/\\/g, '/'); const isAlreadyLinked = providerPools[providerType].some(p => { const existingPath = p[credPathKey]; if (!existingPath) return false; const normalizedExistingPath = existingPath.replace(/\\/g, '/'); return normalizedExistingPath === normalizedForComparison || normalizedExistingPath === './' + normalizedForComparison || './' + normalizedExistingPath === normalizedForComparison; }); if (isAlreadyLinked) { results.push({ filePath: currentFilePath, success: false, error: 'This config file is already linked', providerType: providerType }); continue; } // Create new provider config based on provider type const newProvider = createProviderConfig({ credPathKey, credPath: formatSystemPath(currentFilePath), defaultCheckModel, needsProjectId: providerMapping.needsProjectId }); providerPools[providerType].push(newProvider); linkedProviders.push({ providerType, provider: newProvider }); results.push({ filePath: currentFilePath, success: true, providerType: providerType, displayName: displayName, provider: newProvider }); logger.info(`[UI API] Quick linked config: ${currentFilePath} -> ${providerType}`); } // Save to file only if there were successful links const successCount = results.filter(r => r.success).length; if (successCount > 0) { writeFileSync(poolsFilePath, JSON.stringify(providerPools, null, 2), 'utf-8'); // Update provider pool manager if available if (providerPoolManager) { providerPoolManager.providerPools = providerPools; providerPoolManager.initializeProviderStatus(); } // Broadcast update events broadcastEvent('config_update', { action: 'quick_link_batch', filePath: poolsFilePath, results: results, timestamp: new Date().toISOString() }); for (const { providerType, provider } of linkedProviders) { broadcastEvent('provider_update', { action: 'add', providerType, providerConfig: provider, timestamp: new Date().toISOString() }); } } const failCount = results.filter(r => !r.success).length; const message = successCount > 0 ? `Successfully linked ${successCount} config file(s)${failCount > 0 ? `, ${failCount} failed` : ''}` : `Failed to link all ${failCount} config file(s)`; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: successCount > 0, message: message, successCount: successCount, failCount: failCount, results: results })); return true; } catch (error) { logger.error('[UI API] Quick link failed:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Link failed: ' + error.message } })); return true; } } /** * 刷新特定提供商的UUID */ export async function handleRefreshProviderUuid(req, res, currentConfig, providerPoolManager, providerType, providerUuid) { try { const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; let providerPools = {}; // Load existing pools if (existsSync(filePath)) { try { const fileContent = readFileSync(filePath, 'utf-8'); providerPools = JSON.parse(fileContent); } catch (readError) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Provider pools file not found' } })); return true; } } // Find the provider const providers = providerPools[providerType] || []; const providerIndex = providers.findIndex(p => p.uuid === providerUuid); if (providerIndex === -1) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Provider not found' } })); return true; } // Generate new UUID const oldUuid = providerUuid; const newUuid = generateUUID(); // Update provider UUID providerPools[providerType][providerIndex].uuid = newUuid; // Save to file writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); logger.info(`[UI API] Refreshed UUID for provider in ${providerType}: ${oldUuid} -> ${newUuid}`); // Update provider pool manager if available if (providerPoolManager) { providerPoolManager.providerPools = providerPools; providerPoolManager.initializeProviderStatus(); } // 广播更新事件 broadcastEvent('config_update', { action: 'refresh_uuid', filePath: filePath, providerType, oldUuid, newUuid, timestamp: new Date().toISOString() }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: 'UUID refreshed successfully', oldUuid, newUuid, provider: sanitizeProviderData(providerPools[providerType][providerIndex]) })); return true; } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: error.message } })); return true; } }