Spaces:
Sleeping
Sleeping
| import { existsSync, readFileSync, writeFileSync } from 'fs'; | |
| 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'; | |
| /** | |
| * 获取提供商池摘要 | |
| */ | |
| export async function handleGetProviders(req, res, currentConfig, providerPoolManager) { | |
| 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) { | |
| console.warn('[UI API] Failed to load provider pools:', error.message); | |
| } | |
| res.writeHead(200, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify(providerPools)); | |
| 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) { | |
| console.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, | |
| totalCount: providers.length, | |
| healthyCount: providers.filter(p => p.isHealthy).length | |
| })); | |
| return true; | |
| } | |
| /** | |
| * 获取所有提供商的可用模型 | |
| */ | |
| export async function handleGetProviderModels(req, res) { | |
| const allModels = getAllProviderModels(); | |
| 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) { | |
| 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 || 'provider_pools.json'; | |
| let providerPools = {}; | |
| // Load existing pools | |
| if (existsSync(filePath)) { | |
| try { | |
| const fileContent = readFileSync(filePath, 'utf-8'); | |
| providerPools = JSON.parse(fileContent); | |
| } catch (readError) { | |
| console.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'); | |
| console.log(`[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, | |
| timestamp: new Date().toISOString() | |
| }); | |
| // 广播提供商更新事件 | |
| broadcastEvent('provider_update', { | |
| action: 'add', | |
| providerType, | |
| providerConfig, | |
| timestamp: new Date().toISOString() | |
| }); | |
| res.writeHead(200, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| success: true, | |
| message: 'Provider added successfully', | |
| provider: 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) { | |
| 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'); | |
| console.log(`[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: updatedProvider, | |
| timestamp: new Date().toISOString() | |
| }); | |
| res.writeHead(200, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| success: true, | |
| message: 'Provider updated successfully', | |
| provider: 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) { | |
| 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'); | |
| console.log(`[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: deletedProvider, | |
| timestamp: new Date().toISOString() | |
| }); | |
| res.writeHead(200, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| success: true, | |
| message: 'Provider deleted successfully', | |
| 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) { | |
| 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'); | |
| console.log(`[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: provider, | |
| timestamp: new Date().toISOString() | |
| }); | |
| res.writeHead(200, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| success: true, | |
| message: `Provider ${action}d successfully`, | |
| provider: 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'); | |
| console.log(`[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'); | |
| console.log(`[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 => ({ 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'); | |
| console.log(`[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, | |
| 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; | |
| } | |
| console.log(`[UI API] Starting health check for ${unhealthyProviders.length} unhealthy providers in ${providerType} (total: ${providers.length})`); | |
| // 执行健康检测(强制检查,忽略 checkHealth 配置) | |
| const results = []; | |
| for (const providerStatus of unhealthyProviders) { | |
| const providerConfig = providerStatus.config; | |
| // 跳过已禁用的节点 | |
| if (providerConfig.isDisabled) { | |
| console.log(`[UI API] Skipping health check for disabled provider: ${providerConfig.uuid}`); | |
| continue; | |
| } | |
| try { | |
| // 传递 forceCheck = true 强制执行健康检查,忽略 checkHealth 配置 | |
| const healthResult = await providerPoolManager._checkProviderHealth(providerType, providerConfig, true); | |
| if (healthResult === null) { | |
| results.push({ | |
| uuid: providerConfig.uuid, | |
| success: null, | |
| message: 'Health check not supported for this provider type' | |
| }); | |
| continue; | |
| } | |
| 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); | |
| console.log(`[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); | |
| console.log(`[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; | |
| console.log(`[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, | |
| 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) { | |
| console.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 } = body; | |
| if (!filePath) { | |
| res.writeHead(400, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ error: { message: 'filePath is required' } })); | |
| return true; | |
| } | |
| const normalizedPath = filePath.replace(/\\/g, '/').toLowerCase(); | |
| // 根据文件路径自动识别提供商类型 | |
| const providerMapping = detectProviderFromPath(normalizedPath); | |
| if (!providerMapping) { | |
| res.writeHead(400, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| error: { | |
| message: 'Unable to identify provider type for config file, please ensure file is in configs/kiro/, configs/gemini/, configs/qwen/ or configs/antigravity/ directory' | |
| } | |
| })); | |
| return true; | |
| } | |
| const { providerType, credPathKey, defaultCheckModel, displayName } = providerMapping; | |
| 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) { | |
| console.warn('[UI API] Failed to read existing provider pools:', readError.message); | |
| } | |
| } | |
| // Ensure provider type array exists | |
| if (!providerPools[providerType]) { | |
| providerPools[providerType] = []; | |
| } | |
| // Check if already linked - 使用标准化路径进行比较 | |
| const normalizedForComparison = filePath.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) { | |
| res.writeHead(400, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ error: { message: 'This config file is already linked' } })); | |
| return true; | |
| } | |
| // Create new provider config based on provider type | |
| const newProvider = createProviderConfig({ | |
| credPathKey, | |
| credPath: formatSystemPath(filePath), | |
| defaultCheckModel, | |
| needsProjectId: providerMapping.needsProjectId | |
| }); | |
| providerPools[providerType].push(newProvider); | |
| // Save to file | |
| writeFileSync(poolsFilePath, JSON.stringify(providerPools, null, 2), 'utf-8'); | |
| console.log(`[UI API] Quick linked config: ${filePath} -> ${providerType}`); | |
| // Update provider pool manager if available | |
| if (providerPoolManager) { | |
| providerPoolManager.providerPools = providerPools; | |
| providerPoolManager.initializeProviderStatus(); | |
| } | |
| // Broadcast update event | |
| broadcastEvent('config_update', { | |
| action: 'quick_link', | |
| filePath: poolsFilePath, | |
| providerType, | |
| newProvider, | |
| timestamp: new Date().toISOString() | |
| }); | |
| broadcastEvent('provider_update', { | |
| action: 'add', | |
| providerType, | |
| providerConfig: newProvider, | |
| timestamp: new Date().toISOString() | |
| }); | |
| res.writeHead(200, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| success: true, | |
| message: `Config successfully linked to ${displayName}`, | |
| provider: newProvider, | |
| providerType: providerType | |
| })); | |
| return true; | |
| } catch (error) { | |
| console.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'); | |
| console.log(`[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: providerPools[providerType][providerIndex] | |
| })); | |
| return true; | |
| } catch (error) { | |
| res.writeHead(500, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ error: { message: error.message } })); | |
| return true; | |
| } | |
| } |