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