AIClient-2-API/src/ui-modules/provider-api.js
2026-04-05 15:20:48 +08:00

1215 lines
No EOL
47 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 { 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;
}
}