diff --git a/src/providers/provider-pool-manager.js b/src/providers/provider-pool-manager.js
index 15bf9f5..f0cb257 100644
--- a/src/providers/provider-pool-manager.js
+++ b/src/providers/provider-pool-manager.js
@@ -116,8 +116,11 @@ export class ProviderPoolManager {
const credData = JSON.parse(fileContent);
const expiryTime = credData.expiry_date || credData.expiry || credData.expires_at;
const nearExpiryMs = (currentConfig?.CRON_NEAR_MINUTES || 10) * 60 * 1000;
- const isNearExpiry = expiryTime && (expiryTime - Date.now()) < nearExpiryMs;
- if (isNearExpiry) {
+ if (!expiryTime) {
+ // 凭据文件缺少 expiry 字段,无法判断是否快过期,作为安全措施强制刷新
+ this._log('warn', `Node ${providerStatus.uuid} (${providerType}) has no expiry field. Forcing refresh as safety measure...`);
+ this._enqueueRefresh(providerType, providerStatus);
+ } else if ((expiryTime - Date.now()) < nearExpiryMs) {
this._log('warn', `Node ${providerStatus.uuid} (${providerType}) is near expiration. Enqueuing refresh...`);
this._enqueueRefresh(providerType, providerStatus);
}
@@ -1692,14 +1695,15 @@ export class ProviderPoolManager {
}
/**
- * Performs health checks on selected providers.
+ * Performs initial (startup) health checks on selected providers.
* Respects SCHEDULED_HEALTH_CHECK.providerTypes configuration.
- *
+ * Called once at server startup.
+ *
* 设计决策:如果没有选择任何 provider types,则不进行检查任何 provider。
* 这是有意为之的设计 - 如果用户没有明确选择,则不需要自动健康检查。
* 区别于原来的逻辑(检查所有 provider),现在的行为更符合用户预期。
*/
- async performHealthChecks() {
+ async performInitialHealthChecks() {
const scheduledConfig = this.globalConfig?.SCHEDULED_HEALTH_CHECK;
const selectedProviderTypes = scheduledConfig?.providerTypes;
@@ -1788,7 +1792,7 @@ export class ProviderPoolManager {
* This method is designed to be called periodically to proactively check provider health.
* It respects provider-level isDisabled flag.
*/
- async performScheduledHealthChecks() {
+ async performHealthChecks() {
const scheduledConfig = this.globalConfig?.SCHEDULED_HEALTH_CHECK;
const checkStartTime = Date.now();
@@ -1920,7 +1924,7 @@ export class ProviderPoolManager {
* Performs an actual health check for a specific provider.
*
* 设计决策:不检查 providerConfig.checkHealth 标志。
- * 健康检查是否执行由上层调用方(performScheduledHealthChecks / performHealthChecks)
+ * 健康检查是否执行由上层调用方(performHealthChecks / performInitialHealthChecks)
* 通过 providerTypes 数组来决定,不在每个 provider 级别控制。
* 这样简化了逻辑,避免 per-provider 的 checkHealth flag 变得无用。
*
@@ -1972,7 +1976,7 @@ export class ProviderPoolManager {
await serviceAdapter.generateContent(modelName, requestWithSignal);
clearTimeout(timeoutId);
- // 注意:使用量计数由调用方处理(performScheduledHealthChecks/performHealthChecks)
+ // 注意:使用量计数由调用方处理(performHealthChecks/performInitialHealthChecks)
// 这里只返回成功结果,让调用方统一处理状态更新和计数
return { success: true, modelName, errorMessage: null };
} catch (error) {
diff --git a/src/services/api-manager.js b/src/services/api-manager.js
index d9cb526..8cacc09 100644
--- a/src/services/api-manager.js
+++ b/src/services/api-manager.js
@@ -68,7 +68,7 @@ export function initializeAPIManagement(services) {
logger.info(`[Heartbeat] Server is running. Current time: ${new Date().toLocaleString()}`, Object.keys(services));
// 循环遍历所有已初始化的服务适配器,并尝试刷新令牌
// if (getProviderPoolManager()) {
- // await getProviderPoolManager().performHealthChecks(); // 定期执行健康检查
+ // await getProviderPoolManager().performInitialHealthChecks(); // 定期执行健康检查
// }
for (const providerKey in services) {
const serviceAdapter = services[providerKey];
diff --git a/src/services/api-server.js b/src/services/api-server.js
index 6caf415..4793776 100644
--- a/src/services/api-server.js
+++ b/src/services/api-server.js
@@ -359,7 +359,7 @@ async function startServer() {
const poolManager = getProviderPoolManager();
if (poolManager) {
logger.info('[Initialization] Performing initial health checks for provider pools...');
- poolManager.performHealthChecks();
+ poolManager.performInitialHealthChecks();
}
// 定时健康检查
@@ -394,7 +394,7 @@ async function startServer() {
}
isHealthCheckRunning = true;
try {
- await poolManager.performScheduledHealthChecks();
+ await poolManager.performHealthChecks();
} catch (error) {
logger.error('[ScheduledHealthCheck] Error:', error);
} finally {
@@ -426,13 +426,13 @@ async function startServer() {
// 启动时运行健康检查
if (scheduledConfig.startupRun !== false) {
logger.info('[ScheduledHealthCheck] Running scheduled health check on startup...');
- setImmediate(async () => {
+ setTimeout(async () => {
try {
await poolManager.performScheduledHealthChecks();
} catch (error) {
logger.error('[ScheduledHealthCheck] Startup run error:', error);
}
- });
+ }, 100);
}
// 设置定时任务
diff --git a/src/ui-modules/config-api.js b/src/ui-modules/config-api.js
index c62376a..b6a796c 100644
--- a/src/ui-modules/config-api.js
+++ b/src/ui-modules/config-api.js
@@ -129,11 +129,20 @@ export async function handleUpdateConfig(req, res, currentConfig) {
// 防止路径遍历:解析后的绝对路径必须在工作目录内
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) {
+
+ // 使用 path.relative 和 path.isAbsolute 进行更严格的校验
+ const relativePath = path.relative(cwd, resolved);
+ const isInsideCwd = !path.isAbsolute(relativePath) && !relativePath.startsWith('..') && relativePath !== '..';
+
+ // Windows 大小写不敏感兼容:统一转换为小写比较
+ const normalizedResolved = resolved.toLowerCase().replace(/\\/g, '/');
+ const normalizedCwd = cwd.toLowerCase().replace(/\\/g, '/');
+ const startsWithCwd = normalizedResolved.startsWith(normalizedCwd + '/') || normalizedResolved === normalizedCwd;
+
+ if (isInsideCwd && startsWithCwd) {
currentConfig.SYSTEM_PROMPT_FILE_PATH = p;
+ } else {
+ logger.warn(`[UI API] Rejected SYSTEM_PROMPT_FILE_PATH traversal attempt: ${p}`);
}
}
if (newConfig.SYSTEM_PROMPT_MODE !== undefined) currentConfig.SYSTEM_PROMPT_MODE = newConfig.SYSTEM_PROMPT_MODE;
@@ -174,11 +183,20 @@ export async function handleUpdateConfig(req, res, currentConfig) {
// 防止路径遍历:解析后的绝对路径必须在工作目录内
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) {
+
+ // 使用 path.relative 和 path.isAbsolute 进行更严格的校验
+ const relativePath = path.relative(cwd, resolved);
+ const isInsideCwd = !path.isAbsolute(relativePath) && !relativePath.startsWith('..') && relativePath !== '..';
+
+ // Windows 大小写不敏感兼容:统一转换为小写比较
+ const normalizedResolved = resolved.toLowerCase().replace(/\\/g, '/');
+ const normalizedCwd = cwd.toLowerCase().replace(/\\/g, '/');
+ const startsWithCwd = normalizedResolved.startsWith(normalizedCwd + '/') || normalizedResolved === normalizedCwd;
+
+ if (isInsideCwd && startsWithCwd) {
currentConfig.LOG_DIR = p;
+ } else {
+ logger.warn(`[UI API] Rejected LOG_DIR traversal attempt: ${p}`);
}
}
if (newConfig.LOG_INCLUDE_REQUEST_ID !== undefined) currentConfig.LOG_INCLUDE_REQUEST_ID = newConfig.LOG_INCLUDE_REQUEST_ID;
@@ -188,39 +206,45 @@ export async function handleUpdateConfig(req, res, currentConfig) {
// Scheduled Health Check settings
if (newConfig.SCHEDULED_HEALTH_CHECK !== undefined) {
- const incoming = newConfig.SCHEDULED_HEALTH_CHECK;
- const newInterval = (() => {
- const val = Number(incoming?.interval);
- return isNaN(val) ? HEALTH_CHECK.DEFAULT_INTERVAL_MS : Math.max(HEALTH_CHECK.MIN_INTERVAL_MS, val);
- })();
- currentConfig.SCHEDULED_HEALTH_CHECK = {
- enabled: incoming?.enabled === true,
- startupRun: incoming?.startupRun !== false,
- interval: newInterval,
- providerTypes: Array.isArray(incoming?.providerTypes) ? incoming.providerTypes : []
- };
+ const incoming = newConfig.SCHEDULED_HEALTH_CHECK;
- // 检测 enabled 状态变化
- const wasEnabled = currentConfig.SCHEDULED_HEALTH_CHECK?.enabled === true;
- const nowEnabled = incoming?.enabled === true;
+ // 检测 enabled 状态变化(在更新配置之前保存旧状态)
+ const prevConfig = currentConfig.SCHEDULED_HEALTH_CHECK || {};
+ const wasEnabled = prevConfig.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);
- }
- }
+ const newInterval = (() => {
+ const val = Number(incoming?.interval);
+ return isNaN(val) ? HEALTH_CHECK.DEFAULT_INTERVAL_MS : Math.max(HEALTH_CHECK.MIN_INTERVAL_MS, Math.min(HEALTH_CHECK.MAX_INTERVAL_MS, val));
+ })();
+
+ // 先保存旧的 interval 用于比较
+ const oldInterval = globalThis._activeHealthCheckInterval;
+
+ // 更新配置
+ currentConfig.SCHEDULED_HEALTH_CHECK = {
+ enabled: nowEnabled,
+ startupRun: incoming?.startupRun !== false,
+ interval: newInterval,
+ providerTypes: Array.isArray(incoming?.providerTypes) ? incoming.providerTypes : []
+ };
+
+ // 处理 timer 状态变化
+ // 当 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 !== oldInterval && globalThis.reloadHealthCheckTimer) {
+ globalThis._activeHealthCheckInterval = newInterval;
+ globalThis.reloadHealthCheckTimer(newInterval);
+ }
}
// Handle system prompt update
diff --git a/src/ui-modules/provider-api.js b/src/ui-modules/provider-api.js
index 534032a..09beaf4 100644
--- a/src/ui-modules/provider-api.js
+++ b/src/ui-modules/provider-api.js
@@ -7,18 +7,22 @@ import { broadcastEvent } from './event-broadcast.js';
import { getRegisteredProviders } from '../providers/adapter.js';
// 文件级互斥锁:防止并发读写导致数据丢失
-// HTML 脱敏:移除用户输入字段中的 HTML/JS,防止 XSS
+// 安全净化:移除用户输入字段中的危险内容(script、事件处理器、javascript:协议等),
+// 存储原始文本。HTML 转义统一由前端 escHtml() 负责,避免双编码问题。
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, ''');
+ let name = sanitized.customName;
+ // 拒绝包含 data: 协议(可能包含内嵌恶意内容)
+ if (/data\s*:/i.test(name)) return sanitized;
+ // 移除 (支持跨行匹配)
+ name = name.replace(/