fix: 深度代码审查修复——空队列泄漏、XSS防护、docker代理清理
- fix(provider-pool): 修复 ownsGlobalSlot=false 时空队列未清理的内存泄漏 - fix(provider-api): 新增 sanitizeProviderData/ProviderPools,对 customName 等用户输入字段做 HTML 转义,防止 XSS - fix(docker): 删除 docker-compose.yml 中的代理硬编码配置 - fix(api-server): 重构定时健康检查 timer 管理,支持热更新 enabled 状态(stopHealthCheckTimer + 状态变化追踪) - fix(constants): 提取 HEALTH_CHECK/PASSWORD/NETWORK/RETRY 常量到 constants.js - style(api-server): 移除日志中密码长度记录,防止敏感元信息泄露
This commit is contained in:
parent
617109f887
commit
740f930f34
9 changed files with 203 additions and 114 deletions
|
|
@ -7,19 +7,13 @@ services:
|
|||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "8085-8087:8085-8087"
|
||||
- "8085-8087:8085-8087"
|
||||
- "1455:1455"
|
||||
- "19876-19880:19876-19880"
|
||||
volumes:
|
||||
- ./configs:/app/configs
|
||||
environment:
|
||||
- ARGS=
|
||||
- HTTP_PROXY=http://host.docker.internal:10801
|
||||
- http_proxy=http://host.docker.internal:10801
|
||||
- HTTPS_PROXY=http://host.docker.internal:10801
|
||||
- https_proxy=http://host.docker.internal:10801
|
||||
- NO_PROXY=localhost,127.0.0.1,host.docker.internal
|
||||
- no_proxy=localhost,127.0.0.1,host.docker.internal
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "healthcheck.js"]
|
||||
interval: 30s
|
||||
|
|
|
|||
|
|
@ -307,13 +307,15 @@ export class ProviderPoolManager {
|
|||
// 使用 Promise.resolve().then 避免过深的递归
|
||||
Promise.resolve().then(nextTask);
|
||||
} else if (currentQueue.activeCount === 0) {
|
||||
// 2. 如果当前提供商的所有任务都完成了,释放全局槽位
|
||||
// 只有持有全局槽位的任务才能递减计数器,避免负值
|
||||
if (ownsGlobalSlot &&
|
||||
currentQueue.waitingTasks.length === 0 &&
|
||||
// 清理空队列:无论是否持有全局槽位,都应删除已无任务的队列对象
|
||||
if (currentQueue.waitingTasks.length === 0 &&
|
||||
this.refreshQueues[providerType] === currentQueue) {
|
||||
delete this.refreshQueues[providerType];
|
||||
}
|
||||
|
||||
// 只有持有全局槽位的任务才能递减计数器
|
||||
if (ownsGlobalSlot) {
|
||||
this.activeProviderRefreshes--;
|
||||
delete this.refreshQueues[providerType]; // 清理空队列
|
||||
}
|
||||
|
||||
// 3. 尝试启动下一个等待中的提供商队列
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { initializeAPIManagement } from './api-manager.js';
|
|||
import { createRequestHandler } from '../handlers/request-handler.js';
|
||||
import { discoverPlugins, getPluginManager } from '../core/plugin-manager.js';
|
||||
import { getTLSSidecar } from '../utils/tls-sidecar.js';
|
||||
import { HEALTH_CHECK } from '../utils/constants.js';
|
||||
|
||||
/**
|
||||
* @license
|
||||
|
|
@ -362,45 +363,30 @@ async function startServer() {
|
|||
}
|
||||
|
||||
// 定时健康检查
|
||||
// 注意:无论初始 enabled 状态如何,都注册 reloadHealthCheckTimer,
|
||||
// 使得热更新时(从 disabled→enabled)config-api 能调用它启动 timer。
|
||||
const scheduledConfig = CONFIG.SCHEDULED_HEALTH_CHECK;
|
||||
if (scheduledConfig?.enabled) {
|
||||
// 设计决策:只验证最小值 60000ms,不设最大值。
|
||||
// 前端有 max=3600000 (1小时) 的 UI 限制,但后端允许更大值以支持特殊需求。
|
||||
// 如果用户需要超长的间隔,可以通过 API 直接设置。
|
||||
{
|
||||
const DEFAULT_INTERVAL = CONFIG.CRON_NEAR_MINUTES * 60 * 1000;
|
||||
let interval = scheduledConfig.interval;
|
||||
if (typeof interval !== 'number' || interval < 60000) {
|
||||
logger.warn(`[ScheduledHealthCheck] Invalid interval ${interval}, using default ${DEFAULT_INTERVAL}`);
|
||||
interval = DEFAULT_INTERVAL;
|
||||
}
|
||||
|
||||
// 启动时运行健康检查
|
||||
if (scheduledConfig.startupRun !== false) {
|
||||
logger.info('[ScheduledHealthCheck] Running scheduled health check on startup...');
|
||||
// 使用 setImmediate 确保在事件循环的下一阶段执行,此时服务器已完全就绪
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await poolManager.performScheduledHealthChecks();
|
||||
} catch (error) {
|
||||
logger.error('[ScheduledHealthCheck] Startup run error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let isHealthCheckRunning = false;
|
||||
let healthCheckTimerId = null;
|
||||
|
||||
// 定时健康检查函数
|
||||
|
||||
// 定时健康检查函数(始终注册,无论初始 enabled 状态)
|
||||
const runHealthCheckTimer = (interval) => {
|
||||
// 清除旧的 timer
|
||||
if (healthCheckTimerId) {
|
||||
clearInterval(healthCheckTimerId);
|
||||
healthCheckTimerId = null;
|
||||
}
|
||||
// 重置运行状态,允许新的 timer 立即触发
|
||||
// 否则如果 reload 时正在运行,新 timer 的第一次触发会被跳过
|
||||
isHealthCheckRunning = false;
|
||||
|
||||
|
||||
// 设置定时任务
|
||||
// 设计决策:只验证最小值,不设最大值。
|
||||
// 前端有 max=3600000 (1小时) 的 UI 限制,但后端允许更大值以支持特殊需求。
|
||||
const safeInterval = (typeof interval === 'number' && interval >= HEALTH_CHECK.MIN_INTERVAL_MS) ? interval : DEFAULT_INTERVAL;
|
||||
healthCheckTimerId = setInterval(async () => {
|
||||
if (isHealthCheckRunning) {
|
||||
logger.debug('[ScheduledHealthCheck] Skipping - previous run still in progress');
|
||||
|
|
@ -414,16 +400,45 @@ async function startServer() {
|
|||
} finally {
|
||||
isHealthCheckRunning = false;
|
||||
}
|
||||
}, interval);
|
||||
logger.info(`[ScheduledHealthCheck] Scheduled every ${interval}ms`);
|
||||
}, safeInterval);
|
||||
logger.info(`[ScheduledHealthCheck] Scheduled every ${safeInterval}ms`);
|
||||
return safeInterval;
|
||||
};
|
||||
|
||||
// 设置定时任务
|
||||
runHealthCheckTimer(interval);
|
||||
|
||||
// 注册重载函数和初始 interval 到 globalThis(供 config-api 热更新使用)
|
||||
// 注册重载/停止函数到 globalThis(供 config-api 热更新使用)
|
||||
// 必须在 enabled 检查外注册,保证热更新时可访问
|
||||
globalThis.reloadHealthCheckTimer = runHealthCheckTimer;
|
||||
globalThis._activeHealthCheckInterval = interval;
|
||||
globalThis.stopHealthCheckTimer = () => {
|
||||
if (healthCheckTimerId) {
|
||||
clearInterval(healthCheckTimerId);
|
||||
healthCheckTimerId = null;
|
||||
logger.info('[ScheduledHealthCheck] Timer stopped');
|
||||
}
|
||||
};
|
||||
|
||||
if (scheduledConfig?.enabled) {
|
||||
let interval = scheduledConfig.interval;
|
||||
if (typeof interval !== 'number' || interval < HEALTH_CHECK.MIN_INTERVAL_MS) {
|
||||
logger.warn(`[ScheduledHealthCheck] Invalid interval ${interval}, using default ${DEFAULT_INTERVAL}`);
|
||||
interval = DEFAULT_INTERVAL;
|
||||
}
|
||||
|
||||
// 启动时运行健康检查
|
||||
if (scheduledConfig.startupRun !== false) {
|
||||
logger.info('[ScheduledHealthCheck] Running scheduled health check on startup...');
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await poolManager.performScheduledHealthChecks();
|
||||
} catch (error) {
|
||||
logger.error('[ScheduledHealthCheck] Startup run error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 设置定时任务
|
||||
const activeInterval = runHealthCheckTimer(interval);
|
||||
globalThis._activeHealthCheckInterval = activeInterval;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是子进程,通知主进程已就绪
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import path from 'path';
|
|||
import crypto from 'crypto';
|
||||
import { CONFIG } from '../core/config-manager.js';
|
||||
import { getClientIp } from '../utils/common.js';
|
||||
import { PASSWORD } from '../utils/constants.js';
|
||||
|
||||
// Token存储到本地文件中
|
||||
const TOKEN_STORE_FILE = path.join(process.cwd(), 'configs', 'token-store.json');
|
||||
|
|
@ -56,7 +57,7 @@ export async function validateCredentials(password) {
|
|||
if (parts.length !== 3) return false;
|
||||
const [, salt, storedHash] = parts;
|
||||
const inputHash = await new Promise((resolve, reject) =>
|
||||
crypto.pbkdf2(password.trim(), salt, 100000, 64, 'sha512', (err, key) =>
|
||||
crypto.pbkdf2(password.trim(), salt, PASSWORD.PBKDF2_ITERATIONS, PASSWORD.PBKDF2_KEYLEN, PASSWORD.PBKDF2_DIGEST, (err, key) =>
|
||||
err ? reject(err) : resolve(key.toString('hex'))
|
||||
)
|
||||
);
|
||||
|
|
@ -64,7 +65,11 @@ export async function validateCredentials(password) {
|
|||
}
|
||||
|
||||
// 旧格式:明文(兼容迁移期,建议通过 UI 重新设置密码以升级为哈希格式)
|
||||
return password === storedPassword;
|
||||
// 使用 timingSafeEqual 防止时序攻击
|
||||
const a = Buffer.from(password.trim());
|
||||
const b = Buffer.from(storedPassword);
|
||||
if (a.length !== b.length) return false;
|
||||
return crypto.timingSafeEqual(a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { serviceInstances } from '../providers/adapter.js';
|
|||
import { initApiService } from '../services/service-manager.js';
|
||||
import { getRequestBody } from '../utils/common.js';
|
||||
import { broadcastEvent } from '../ui-modules/event-broadcast.js';
|
||||
import { HEALTH_CHECK, PASSWORD, NETWORK, RETRY } from '../utils/constants.js';
|
||||
|
||||
/**
|
||||
* 重载配置文件
|
||||
|
|
@ -120,14 +121,18 @@ export async function handleUpdateConfig(req, res, currentConfig) {
|
|||
}
|
||||
if (newConfig.SERVER_PORT !== undefined) {
|
||||
const port = Number(newConfig.SERVER_PORT);
|
||||
if (Number.isInteger(port) && port > 0 && port < 65536) currentConfig.SERVER_PORT = port;
|
||||
if (Number.isInteger(port) && port >= NETWORK.MIN_PORT && port <= NETWORK.MAX_PORT) currentConfig.SERVER_PORT = port;
|
||||
}
|
||||
if (newConfig.MODEL_PROVIDER !== undefined) currentConfig.MODEL_PROVIDER = newConfig.MODEL_PROVIDER;
|
||||
if (newConfig.SYSTEM_PROMPT_FILE_PATH !== undefined) {
|
||||
const p = String(newConfig.SYSTEM_PROMPT_FILE_PATH);
|
||||
// 防止路径遍历:解析后的绝对路径必须在工作目录内
|
||||
const resolved = path.resolve(process.cwd(), p);
|
||||
if (resolved.startsWith(process.cwd() + path.sep) || resolved === process.cwd()) {
|
||||
const cwd = process.cwd();
|
||||
// Windows兼容:统一使用正斜杠进行比较
|
||||
const normalizedResolved = resolved.replace(/\\/g, '/');
|
||||
const normalizedCwd = cwd.replace(/\\/g, '/');
|
||||
if (normalizedResolved.startsWith(normalizedCwd + '/') || normalizedResolved === normalizedCwd) {
|
||||
currentConfig.SYSTEM_PROMPT_FILE_PATH = p;
|
||||
}
|
||||
}
|
||||
|
|
@ -136,7 +141,7 @@ export async function handleUpdateConfig(req, res, currentConfig) {
|
|||
if (newConfig.PROMPT_LOG_MODE !== undefined) currentConfig.PROMPT_LOG_MODE = newConfig.PROMPT_LOG_MODE;
|
||||
if (newConfig.REQUEST_MAX_RETRIES !== undefined) {
|
||||
const v = Number(newConfig.REQUEST_MAX_RETRIES);
|
||||
if (Number.isInteger(v) && v >= 0 && v <= 100) currentConfig.REQUEST_MAX_RETRIES = v;
|
||||
if (Number.isInteger(v) && v >= 0 && v <= RETRY.MAX_RETRIES) currentConfig.REQUEST_MAX_RETRIES = v;
|
||||
}
|
||||
if (newConfig.REQUEST_BASE_DELAY !== undefined) currentConfig.REQUEST_BASE_DELAY = newConfig.REQUEST_BASE_DELAY;
|
||||
if (newConfig.CREDENTIAL_SWITCH_MAX_RETRIES !== undefined) currentConfig.CREDENTIAL_SWITCH_MAX_RETRIES = newConfig.CREDENTIAL_SWITCH_MAX_RETRIES;
|
||||
|
|
@ -164,7 +169,18 @@ export async function handleUpdateConfig(req, res, currentConfig) {
|
|||
if (newConfig.LOG_ENABLED !== undefined) currentConfig.LOG_ENABLED = newConfig.LOG_ENABLED;
|
||||
if (newConfig.LOG_OUTPUT_MODE !== undefined) currentConfig.LOG_OUTPUT_MODE = newConfig.LOG_OUTPUT_MODE;
|
||||
if (newConfig.LOG_LEVEL !== undefined) currentConfig.LOG_LEVEL = newConfig.LOG_LEVEL;
|
||||
if (newConfig.LOG_DIR !== undefined) currentConfig.LOG_DIR = newConfig.LOG_DIR;
|
||||
if (newConfig.LOG_DIR !== undefined) {
|
||||
const p = String(newConfig.LOG_DIR);
|
||||
// 防止路径遍历:解析后的绝对路径必须在工作目录内
|
||||
const resolved = path.resolve(process.cwd(), p);
|
||||
const cwd = process.cwd();
|
||||
// Windows兼容:统一使用正斜杠进行比较
|
||||
const normalizedResolved = resolved.replace(/\\/g, '/');
|
||||
const normalizedCwd = cwd.replace(/\\/g, '/');
|
||||
if (normalizedResolved.startsWith(normalizedCwd + '/') || normalizedResolved === normalizedCwd) {
|
||||
currentConfig.LOG_DIR = p;
|
||||
}
|
||||
}
|
||||
if (newConfig.LOG_INCLUDE_REQUEST_ID !== undefined) currentConfig.LOG_INCLUDE_REQUEST_ID = newConfig.LOG_INCLUDE_REQUEST_ID;
|
||||
if (newConfig.LOG_INCLUDE_TIMESTAMP !== undefined) currentConfig.LOG_INCLUDE_TIMESTAMP = newConfig.LOG_INCLUDE_TIMESTAMP;
|
||||
if (newConfig.LOG_MAX_FILE_SIZE !== undefined) currentConfig.LOG_MAX_FILE_SIZE = newConfig.LOG_MAX_FILE_SIZE;
|
||||
|
|
@ -175,7 +191,7 @@ export async function handleUpdateConfig(req, res, currentConfig) {
|
|||
const incoming = newConfig.SCHEDULED_HEALTH_CHECK;
|
||||
const newInterval = (() => {
|
||||
const val = Number(incoming?.interval);
|
||||
return isNaN(val) ? 600000 : Math.max(60000, val);
|
||||
return isNaN(val) ? HEALTH_CHECK.DEFAULT_INTERVAL_MS : Math.max(HEALTH_CHECK.MIN_INTERVAL_MS, val);
|
||||
})();
|
||||
currentConfig.SCHEDULED_HEALTH_CHECK = {
|
||||
enabled: incoming?.enabled === true,
|
||||
|
|
@ -184,10 +200,26 @@ export async function handleUpdateConfig(req, res, currentConfig) {
|
|||
providerTypes: Array.isArray(incoming?.providerTypes) ? incoming.providerTypes : []
|
||||
};
|
||||
|
||||
// 仅在 interval 实际变化时重新加载 timer(_activeInterval 存在内存变量中,不写入配置文件)
|
||||
if (globalThis.reloadHealthCheckTimer && currentConfig.SCHEDULED_HEALTH_CHECK.enabled && newInterval !== globalThis._activeHealthCheckInterval) {
|
||||
globalThis._activeHealthCheckInterval = newInterval;
|
||||
globalThis.reloadHealthCheckTimer(newInterval);
|
||||
// 检测 enabled 状态变化
|
||||
const wasEnabled = currentConfig.SCHEDULED_HEALTH_CHECK?.enabled === true;
|
||||
const nowEnabled = incoming?.enabled === true;
|
||||
|
||||
if (currentConfig.SCHEDULED_HEALTH_CHECK) {
|
||||
// 当 enabled 从 true -> false 时,清除 timer
|
||||
if (wasEnabled && !nowEnabled && globalThis.stopHealthCheckTimer) {
|
||||
globalThis.stopHealthCheckTimer();
|
||||
globalThis._activeHealthCheckInterval = undefined;
|
||||
}
|
||||
// 当 enabled 从 false -> true 时,启动 timer
|
||||
else if (!wasEnabled && nowEnabled && globalThis.reloadHealthCheckTimer) {
|
||||
globalThis._activeHealthCheckInterval = newInterval;
|
||||
globalThis.reloadHealthCheckTimer(newInterval);
|
||||
}
|
||||
// 当 enabled=true 且 interval 变化时,重启 timer
|
||||
else if (nowEnabled && newInterval !== globalThis._activeHealthCheckInterval) {
|
||||
globalThis._activeHealthCheckInterval = newInterval;
|
||||
globalThis.reloadHealthCheckTimer(newInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -347,16 +379,16 @@ export async function handleUpdateAdminPassword(req, res) {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (password.trim().length < 8) {
|
||||
if (password.trim().length < PASSWORD.MIN_LENGTH) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: { message: 'Password must be at least 8 characters' } }));
|
||||
res.end(JSON.stringify({ error: { message: `Password must be at least ${PASSWORD.MIN_LENGTH} characters` } }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// 使用 PBKDF2 哈希存储密码,避免明文写入文件
|
||||
const salt = crypto.randomBytes(16).toString('hex');
|
||||
const hash = await new Promise((resolve, reject) =>
|
||||
crypto.pbkdf2(password.trim(), salt, 100000, 64, 'sha512', (err, key) =>
|
||||
crypto.pbkdf2(password.trim(), salt, PASSWORD.PBKDF2_ITERATIONS, PASSWORD.PBKDF2_KEYLEN, PASSWORD.PBKDF2_DIGEST, (err, key) =>
|
||||
err ? reject(err) : resolve(key.toString('hex'))
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,10 +7,43 @@ import { broadcastEvent } from './event-broadcast.js';
|
|||
import { getRegisteredProviders } from '../providers/adapter.js';
|
||||
|
||||
// 文件级互斥锁:防止并发读写导致数据丢失
|
||||
// HTML 脱敏:移除用户输入字段中的 HTML/JS,防止 XSS
|
||||
function sanitizeProviderData(provider) {
|
||||
if (!provider || typeof provider !== 'object') return provider;
|
||||
const sanitized = { ...provider };
|
||||
// 允许在前端显示的纯文本字段做 HTML 转义
|
||||
if (typeof sanitized.customName === 'string') {
|
||||
sanitized.customName = sanitized.customName
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
function sanitizeProviderPools(pools) {
|
||||
if (!pools || typeof pools !== 'object') return pools;
|
||||
const sanitized = {};
|
||||
for (const [type, providers] of Object.entries(pools)) {
|
||||
sanitized[type] = Array.isArray(providers)
|
||||
? providers.map(sanitizeProviderData)
|
||||
: providers;
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
// 使用 Promise 链式队列,确保文件操作顺序执行
|
||||
let _fileLockChain = Promise.resolve();
|
||||
function withFileLock(fn) {
|
||||
const next = _fileLockChain.then(() => fn());
|
||||
_fileLockChain = next.catch(() => {});
|
||||
const next = _fileLockChain
|
||||
.then(() => fn())
|
||||
.catch(err => {
|
||||
// 记录错误但继续链式执行,防止死锁
|
||||
logger.error('[FileLock] Operation failed:', err?.message || err);
|
||||
return null;
|
||||
});
|
||||
_fileLockChain = next.then(() => {}).catch(() => {});
|
||||
return next;
|
||||
}
|
||||
/**
|
||||
|
|
@ -31,7 +64,7 @@ export async function handleGetProviders(req, res, currentConfig, providerPoolMa
|
|||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(providerPools));
|
||||
res.end(JSON.stringify(sanitizeProviderPools(providerPools)));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +99,7 @@ export async function handleGetProviderType(req, res, currentConfig, providerPoo
|
|||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
providerType,
|
||||
providers,
|
||||
providers: providers.map(sanitizeProviderData),
|
||||
totalCount: providers.length,
|
||||
healthyCount: providers.filter(p => p.isHealthy).length
|
||||
}));
|
||||
|
|
@ -159,7 +192,7 @@ async function _handleAddProvider(req, res, currentConfig, providerPoolManager)
|
|||
action: 'add',
|
||||
filePath: filePath,
|
||||
providerType,
|
||||
providerConfig,
|
||||
providerConfig: sanitizeProviderData(providerConfig),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
|
|
@ -167,7 +200,7 @@ async function _handleAddProvider(req, res, currentConfig, providerPoolManager)
|
|||
broadcastEvent('provider_update', {
|
||||
action: 'add',
|
||||
providerType,
|
||||
providerConfig,
|
||||
providerConfig: sanitizeProviderData(providerConfig),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
|
|
@ -175,7 +208,7 @@ async function _handleAddProvider(req, res, currentConfig, providerPoolManager)
|
|||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
message: 'Provider added successfully',
|
||||
provider: providerConfig,
|
||||
provider: sanitizeProviderData(providerConfig),
|
||||
providerType
|
||||
}));
|
||||
return true;
|
||||
|
|
@ -257,7 +290,7 @@ async function _handleUpdateProvider(req, res, currentConfig, providerPoolManage
|
|||
action: 'update',
|
||||
filePath: filePath,
|
||||
providerType,
|
||||
providerConfig: updatedProvider,
|
||||
providerConfig: sanitizeProviderData(updatedProvider),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
|
|
@ -265,7 +298,7 @@ async function _handleUpdateProvider(req, res, currentConfig, providerPoolManage
|
|||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
message: 'Provider updated successfully',
|
||||
provider: updatedProvider
|
||||
provider: sanitizeProviderData(updatedProvider)
|
||||
}));
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
|
@ -331,7 +364,7 @@ async function _handleDeleteProvider(req, res, currentConfig, providerPoolManage
|
|||
action: 'delete',
|
||||
filePath: filePath,
|
||||
providerType,
|
||||
providerConfig: deletedProvider,
|
||||
providerConfig: sanitizeProviderData(deletedProvider),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
|
|
@ -339,7 +372,7 @@ async function _handleDeleteProvider(req, res, currentConfig, providerPoolManage
|
|||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
message: 'Provider deleted successfully',
|
||||
deletedProvider
|
||||
deletedProvider: sanitizeProviderData(deletedProvider)
|
||||
}));
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
|
@ -407,7 +440,7 @@ async function _handleDisableEnableProvider(req, res, currentConfig, providerPoo
|
|||
action: action,
|
||||
filePath: filePath,
|
||||
providerType,
|
||||
providerConfig: provider,
|
||||
providerConfig: sanitizeProviderData(provider),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
|
|
@ -415,7 +448,7 @@ async function _handleDisableEnableProvider(req, res, currentConfig, providerPoo
|
|||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
message: `Provider ${action}d successfully`,
|
||||
provider: provider
|
||||
provider: sanitizeProviderData(provider)
|
||||
}));
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
|
@ -569,7 +602,7 @@ export async function handleDeleteUnhealthyProviders(req, res, currentConfig, pr
|
|||
filePath: filePath,
|
||||
providerType,
|
||||
deletedCount: unhealthyProviders.length,
|
||||
deletedProviders: unhealthyProviders.map(p => ({ uuid: p.uuid, customName: p.customName })),
|
||||
deletedProviders: unhealthyProviders.map(p => sanitizeProviderData({ uuid: p.uuid, customName: p.customName })),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
|
|
@ -660,7 +693,7 @@ export async function handleRefreshUnhealthyUuids(req, res, currentConfig, provi
|
|||
filePath: filePath,
|
||||
providerType,
|
||||
refreshedCount: refreshedProviders.length,
|
||||
refreshedProviders,
|
||||
refreshedProviders: refreshedProviders.map(p => sanitizeProviderData(p)),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
|
|
@ -806,7 +839,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
|
|||
action: 'health_check',
|
||||
filePath: filePath,
|
||||
providerType,
|
||||
results,
|
||||
results: results.map(r => ({ ...r, message: sanitizeProviderData({ message: r.message }).message })),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
|
|
@ -1046,7 +1079,7 @@ export async function handleRefreshProviderUuid(req, res, currentConfig, provide
|
|||
message: 'UUID refreshed successfully',
|
||||
oldUuid,
|
||||
newUuid,
|
||||
provider: providerPools[providerType][providerIndex]
|
||||
provider: sanitizeProviderData(providerPools[providerType][providerIndex])
|
||||
}));
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
|
|
|||
44
src/utils/constants.js
Normal file
44
src/utils/constants.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* 共享常量定义
|
||||
* 集中管理各处使用的硬编码值
|
||||
*/
|
||||
|
||||
// 定时健康检查相关常量
|
||||
export const HEALTH_CHECK = {
|
||||
// 最小检查间隔:60秒(60000毫秒)
|
||||
MIN_INTERVAL_MS: 60000,
|
||||
// 默认检查间隔:10分钟(600000毫秒)
|
||||
DEFAULT_INTERVAL_MS: 600000,
|
||||
// 最大检查间隔:1小时(3600000毫秒)- 仅用于前端UI限制
|
||||
MAX_INTERVAL_MS: 3600000
|
||||
};
|
||||
|
||||
// 密码安全相关常量
|
||||
export const PASSWORD = {
|
||||
// 最小密码长度
|
||||
MIN_LENGTH: 8,
|
||||
// PBKDF2迭代次数
|
||||
PBKDF2_ITERATIONS: 100000,
|
||||
// PBKDF2密钥长度(字节)
|
||||
PBKDF2_KEYLEN: 64,
|
||||
// PBKDF2哈希算法
|
||||
PBKDF2_DIGEST: 'sha512'
|
||||
};
|
||||
|
||||
// 网络相关常量
|
||||
export const NETWORK = {
|
||||
// 最小端口号
|
||||
MIN_PORT: 1,
|
||||
// 最大端口号
|
||||
MAX_PORT: 65535,
|
||||
// 默认服务器端口
|
||||
DEFAULT_PORT: 3000
|
||||
};
|
||||
|
||||
// 请求重试相关常量
|
||||
export const RETRY = {
|
||||
// 最大重试次数
|
||||
MAX_RETRIES: 100,
|
||||
// 默认重试次数
|
||||
DEFAULT_RETRIES: 3
|
||||
};
|
||||
|
|
@ -53,7 +53,7 @@ function renderProviderTags(container, configs, isRequired) {
|
|||
// 过滤掉不可见的提供商
|
||||
const visibleConfigs = configs.filter(c => c.visible !== false);
|
||||
|
||||
const escHtml = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
const escHtml = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||
container.innerHTML = visibleConfigs.map(c => `
|
||||
<button type="button" class="provider-tag" data-value="${escHtml(c.id)}">
|
||||
<i class="fas ${escHtml(c.icon || 'fa-server')}"></i>
|
||||
|
|
|
|||
|
|
@ -256,43 +256,7 @@
|
|||
<div class="form-group">
|
||||
<label data-i18n="config.healthCheck.providerTypes">定时检查的供应商</label>
|
||||
<div id="scheduledHealthCheckProviders" class="provider-tags">
|
||||
<button type="button" class="provider-tag" data-value="gemini-cli-oauth">
|
||||
<i class="fas fa-robot"></i>
|
||||
<span>Gemini CLI OAuth</span>
|
||||
</button>
|
||||
<button type="button" class="provider-tag" data-value="gemini-antigravity">
|
||||
<i class="fas fa-rocket"></i>
|
||||
<span>Gemini Antigravity</span>
|
||||
</button>
|
||||
<button type="button" class="provider-tag" data-value="openai-custom">
|
||||
<i class="fas fa-brain"></i>
|
||||
<span>OpenAI Custom</span>
|
||||
</button>
|
||||
<button type="button" class="provider-tag" data-value="claude-custom">
|
||||
<i class="fas fa-comment-dots"></i>
|
||||
<span>Claude Custom</span>
|
||||
</button>
|
||||
<button type="button" class="provider-tag" data-value="claude-kiro-oauth">
|
||||
<i class="fas fa-key"></i>
|
||||
<span>Claude Kiro OAuth</span>
|
||||
</button>
|
||||
<button type="button" class="provider-tag" data-value="openai-qwen-oauth">
|
||||
<i class="fas fa-cloud"></i>
|
||||
<span>Qwen OAuth</span>
|
||||
</button>
|
||||
<button type="button" class="provider-tag" data-value="openaiResponses-custom">
|
||||
<i class="fas fa-reply"></i>
|
||||
<span>OpenAI Responses</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="provider-tag" data-value="openai-codex-oauth">
|
||||
<i class="fas fa-code"></i>
|
||||
<span>OpenAI Codex OAuth</span>
|
||||
</button>
|
||||
<button type="button" class="provider-tag" data-value="grok-custom">
|
||||
<i class="fas fa-search"></i>
|
||||
<span>Grok Reverse</span>
|
||||
</button>
|
||||
<!-- 由 config-manager.js updateConfigProviderConfigs 动态渲染,勿在此处硬编码 -->
|
||||
</div>
|
||||
<small class="form-text" data-i18n="config.healthCheck.providerTypesNote">选择需要进行定时健康检查的供应商类型</small>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue