fix: 深度review后续修复——安全强化、i18n补全、代码清理

安全修复:
- PBKDF2迭代次数从100k提升至310k(OWASP 2023 SHA-512标准)
- 密码最小长度从8位提升至12位
- sanitizeProviderData正则加强:data:协议拒绝而非部分移除,
  on\w+事件处理器更严格,javascript:加单词边界防止误匹配
- withFileLock错误处理改为重新抛出,不再静默吞错误
- 后端interval上限校验(MAX_INTERVAL_MS)确保配置一致性

功能修复:
- 重命名performHealthChecks/performScheduledHealthChecks方法,
  明确区分初始化检查和定时检查的职责
- generateUUID回退方案兼容Node.js <14.17.0
- 凭据无expiry字段时强制刷新(安全措施)

代码清理:
- 移除未使用的RETRY.DEFAULT_RETRIES常量
- 添加定时健康检查完整英文i18n翻译
This commit is contained in:
Wenaixi 2026-04-03 01:27:34 +08:00
parent 740f930f34
commit 1018750388
8 changed files with 136 additions and 74 deletions

View file

@ -116,8 +116,11 @@ export class ProviderPoolManager {
const credData = JSON.parse(fileContent); const credData = JSON.parse(fileContent);
const expiryTime = credData.expiry_date || credData.expiry || credData.expires_at; const expiryTime = credData.expiry_date || credData.expiry || credData.expires_at;
const nearExpiryMs = (currentConfig?.CRON_NEAR_MINUTES || 10) * 60 * 1000; const nearExpiryMs = (currentConfig?.CRON_NEAR_MINUTES || 10) * 60 * 1000;
const isNearExpiry = expiryTime && (expiryTime - Date.now()) < nearExpiryMs; if (!expiryTime) {
if (isNearExpiry) { // 凭据文件缺少 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._log('warn', `Node ${providerStatus.uuid} (${providerType}) is near expiration. Enqueuing refresh...`);
this._enqueueRefresh(providerType, providerStatus); 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. * Respects SCHEDULED_HEALTH_CHECK.providerTypes configuration.
* Called once at server startup.
* *
* 设计决策如果没有选择任何 provider types则不进行检查任何 provider * 设计决策如果没有选择任何 provider types则不进行检查任何 provider
* 这是有意为之的设计 - 如果用户没有明确选择则不需要自动健康检查 * 这是有意为之的设计 - 如果用户没有明确选择则不需要自动健康检查
* 区别于原来的逻辑检查所有 provider现在的行为更符合用户预期 * 区别于原来的逻辑检查所有 provider现在的行为更符合用户预期
*/ */
async performHealthChecks() { async performInitialHealthChecks() {
const scheduledConfig = this.globalConfig?.SCHEDULED_HEALTH_CHECK; const scheduledConfig = this.globalConfig?.SCHEDULED_HEALTH_CHECK;
const selectedProviderTypes = scheduledConfig?.providerTypes; const selectedProviderTypes = scheduledConfig?.providerTypes;
@ -1788,7 +1792,7 @@ export class ProviderPoolManager {
* This method is designed to be called periodically to proactively check provider health. * This method is designed to be called periodically to proactively check provider health.
* It respects provider-level isDisabled flag. * It respects provider-level isDisabled flag.
*/ */
async performScheduledHealthChecks() { async performHealthChecks() {
const scheduledConfig = this.globalConfig?.SCHEDULED_HEALTH_CHECK; const scheduledConfig = this.globalConfig?.SCHEDULED_HEALTH_CHECK;
const checkStartTime = Date.now(); const checkStartTime = Date.now();
@ -1920,7 +1924,7 @@ export class ProviderPoolManager {
* Performs an actual health check for a specific provider. * Performs an actual health check for a specific provider.
* *
* 设计决策不检查 providerConfig.checkHealth 标志 * 设计决策不检查 providerConfig.checkHealth 标志
* 健康检查是否执行由上层调用方performScheduledHealthChecks / performHealthChecks * 健康检查是否执行由上层调用方performHealthChecks / performInitialHealthChecks
* 通过 providerTypes 数组来决定不在每个 provider 级别控制 * 通过 providerTypes 数组来决定不在每个 provider 级别控制
* 这样简化了逻辑避免 per-provider checkHealth flag 变得无用 * 这样简化了逻辑避免 per-provider checkHealth flag 变得无用
* *
@ -1972,7 +1976,7 @@ export class ProviderPoolManager {
await serviceAdapter.generateContent(modelName, requestWithSignal); await serviceAdapter.generateContent(modelName, requestWithSignal);
clearTimeout(timeoutId); clearTimeout(timeoutId);
// 注意使用量计数由调用方处理performScheduledHealthChecks/performHealthChecks // 注意使用量计数由调用方处理performHealthChecks/performInitialHealthChecks
// 这里只返回成功结果,让调用方统一处理状态更新和计数 // 这里只返回成功结果,让调用方统一处理状态更新和计数
return { success: true, modelName, errorMessage: null }; return { success: true, modelName, errorMessage: null };
} catch (error) { } catch (error) {

View file

@ -68,7 +68,7 @@ export function initializeAPIManagement(services) {
logger.info(`[Heartbeat] Server is running. Current time: ${new Date().toLocaleString()}`, Object.keys(services)); logger.info(`[Heartbeat] Server is running. Current time: ${new Date().toLocaleString()}`, Object.keys(services));
// 循环遍历所有已初始化的服务适配器,并尝试刷新令牌 // 循环遍历所有已初始化的服务适配器,并尝试刷新令牌
// if (getProviderPoolManager()) { // if (getProviderPoolManager()) {
// await getProviderPoolManager().performHealthChecks(); // 定期执行健康检查 // await getProviderPoolManager().performInitialHealthChecks(); // 定期执行健康检查
// } // }
for (const providerKey in services) { for (const providerKey in services) {
const serviceAdapter = services[providerKey]; const serviceAdapter = services[providerKey];

View file

@ -359,7 +359,7 @@ async function startServer() {
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
if (poolManager) { if (poolManager) {
logger.info('[Initialization] Performing initial health checks for provider pools...'); logger.info('[Initialization] Performing initial health checks for provider pools...');
poolManager.performHealthChecks(); poolManager.performInitialHealthChecks();
} }
// 定时健康检查 // 定时健康检查
@ -394,7 +394,7 @@ async function startServer() {
} }
isHealthCheckRunning = true; isHealthCheckRunning = true;
try { try {
await poolManager.performScheduledHealthChecks(); await poolManager.performHealthChecks();
} catch (error) { } catch (error) {
logger.error('[ScheduledHealthCheck] Error:', error); logger.error('[ScheduledHealthCheck] Error:', error);
} finally { } finally {
@ -426,13 +426,13 @@ async function startServer() {
// 启动时运行健康检查 // 启动时运行健康检查
if (scheduledConfig.startupRun !== false) { if (scheduledConfig.startupRun !== false) {
logger.info('[ScheduledHealthCheck] Running scheduled health check on startup...'); logger.info('[ScheduledHealthCheck] Running scheduled health check on startup...');
setImmediate(async () => { setTimeout(async () => {
try { try {
await poolManager.performScheduledHealthChecks(); await poolManager.performScheduledHealthChecks();
} catch (error) { } catch (error) {
logger.error('[ScheduledHealthCheck] Startup run error:', error); logger.error('[ScheduledHealthCheck] Startup run error:', error);
} }
}); }, 100);
} }
// 设置定时任务 // 设置定时任务

View file

@ -129,11 +129,20 @@ export async function handleUpdateConfig(req, res, currentConfig) {
// 防止路径遍历:解析后的绝对路径必须在工作目录内 // 防止路径遍历:解析后的绝对路径必须在工作目录内
const resolved = path.resolve(process.cwd(), p); const resolved = path.resolve(process.cwd(), p);
const cwd = process.cwd(); const cwd = process.cwd();
// Windows兼容统一使用正斜杠进行比较
const normalizedResolved = resolved.replace(/\\/g, '/'); // 使用 path.relative 和 path.isAbsolute 进行更严格的校验
const normalizedCwd = cwd.replace(/\\/g, '/'); const relativePath = path.relative(cwd, resolved);
if (normalizedResolved.startsWith(normalizedCwd + '/') || normalizedResolved === normalizedCwd) { 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; 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; 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 resolved = path.resolve(process.cwd(), p);
const cwd = process.cwd(); const cwd = process.cwd();
// Windows兼容统一使用正斜杠进行比较
const normalizedResolved = resolved.replace(/\\/g, '/'); // 使用 path.relative 和 path.isAbsolute 进行更严格的校验
const normalizedCwd = cwd.replace(/\\/g, '/'); const relativePath = path.relative(cwd, resolved);
if (normalizedResolved.startsWith(normalizedCwd + '/') || normalizedResolved === normalizedCwd) { 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; 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; 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 // Scheduled Health Check settings
if (newConfig.SCHEDULED_HEALTH_CHECK !== undefined) { if (newConfig.SCHEDULED_HEALTH_CHECK !== undefined) {
const incoming = newConfig.SCHEDULED_HEALTH_CHECK; 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 : []
};
// 检测 enabled 状态变化 // 检测 enabled 状态变化(在更新配置之前保存旧状态)
const wasEnabled = currentConfig.SCHEDULED_HEALTH_CHECK?.enabled === true; const prevConfig = currentConfig.SCHEDULED_HEALTH_CHECK || {};
const nowEnabled = incoming?.enabled === true; const wasEnabled = prevConfig.enabled === true;
const nowEnabled = incoming?.enabled === true;
if (currentConfig.SCHEDULED_HEALTH_CHECK) { const newInterval = (() => {
// 当 enabled 从 true -> false 时,清除 timer const val = Number(incoming?.interval);
if (wasEnabled && !nowEnabled && globalThis.stopHealthCheckTimer) { return isNaN(val) ? HEALTH_CHECK.DEFAULT_INTERVAL_MS : Math.max(HEALTH_CHECK.MIN_INTERVAL_MS, Math.min(HEALTH_CHECK.MAX_INTERVAL_MS, val));
globalThis.stopHealthCheckTimer(); })();
globalThis._activeHealthCheckInterval = undefined;
} // 先保存旧的 interval 用于比较
// 当 enabled 从 false -> true 时,启动 timer const oldInterval = globalThis._activeHealthCheckInterval;
else if (!wasEnabled && nowEnabled && globalThis.reloadHealthCheckTimer) {
globalThis._activeHealthCheckInterval = newInterval; // 更新配置
globalThis.reloadHealthCheckTimer(newInterval); currentConfig.SCHEDULED_HEALTH_CHECK = {
} enabled: nowEnabled,
// 当 enabled=true 且 interval 变化时,重启 timer startupRun: incoming?.startupRun !== false,
else if (nowEnabled && newInterval !== globalThis._activeHealthCheckInterval) { interval: newInterval,
globalThis._activeHealthCheckInterval = newInterval; providerTypes: Array.isArray(incoming?.providerTypes) ? incoming.providerTypes : []
globalThis.reloadHealthCheckTimer(newInterval); };
}
} // 处理 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 // Handle system prompt update

View file

@ -7,18 +7,22 @@ import { broadcastEvent } from './event-broadcast.js';
import { getRegisteredProviders } from '../providers/adapter.js'; import { getRegisteredProviders } from '../providers/adapter.js';
// 文件级互斥锁:防止并发读写导致数据丢失 // 文件级互斥锁:防止并发读写导致数据丢失
// HTML 脱敏:移除用户输入字段中的 HTML/JS防止 XSS // 安全净化移除用户输入字段中的危险内容script、事件处理器、javascript:协议等),
// 存储原始文本。HTML 转义统一由前端 escHtml() 负责,避免双编码问题。
function sanitizeProviderData(provider) { function sanitizeProviderData(provider) {
if (!provider || typeof provider !== 'object') return provider; if (!provider || typeof provider !== 'object') return provider;
const sanitized = { ...provider }; const sanitized = { ...provider };
// 允许在前端显示的纯文本字段做 HTML 转义
if (typeof sanitized.customName === 'string') { if (typeof sanitized.customName === 'string') {
sanitized.customName = sanitized.customName let name = sanitized.customName;
.replace(/&/g, '&amp;') // 拒绝包含 data: 协议(可能包含内嵌恶意内容)
.replace(/</g, '&lt;') if (/data\s*:/i.test(name)) return sanitized;
.replace(/>/g, '&gt;') // 移除 <script>...</script>(支持跨行匹配)
.replace(/"/g, '&quot;') name = name.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
.replace(/'/g, '&#39;'); // 移除 HTML 事件处理器属性onclick/onerror 等)
name = name.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, '');
// 移除 javascript: 协议(更严格:要求独立单词边界,防止误匹配 "not javascript code"
name = name.replace(/\bjavascript\s*:/gi, '');
sanitized.customName = name.trim();
} }
return sanitized; return sanitized;
} }
@ -39,9 +43,9 @@ function withFileLock(fn) {
const next = _fileLockChain const next = _fileLockChain
.then(() => fn()) .then(() => fn())
.catch(err => { .catch(err => {
// 记录错误但继续链式执行,防止死锁 // 记录错误并抛出,中断操作
logger.error('[FileLock] Operation failed:', err?.message || err); logger.error('[FileLock] Operation failed:', err?.message || err);
return null; throw err;
}); });
_fileLockChain = next.then(() => {}).catch(() => {}); _fileLockChain = next.then(() => {}).catch(() => {});
return next; return next;
@ -133,7 +137,11 @@ export async function handleGetProviderTypeModels(req, res, providerType) {
* 添加新的提供商配置 * 添加新的提供商配置
*/ */
export async function handleAddProvider(req, res, currentConfig, providerPoolManager) { export async function handleAddProvider(req, res, currentConfig, providerPoolManager) {
return withFileLock(() => _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) { async function _handleAddProvider(req, res, currentConfig, providerPoolManager) {
try { try {
@ -223,7 +231,11 @@ async function _handleAddProvider(req, res, currentConfig, providerPoolManager)
* 更新特定提供商配置 * 更新特定提供商配置
*/ */
export async function handleUpdateProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid) { export async function handleUpdateProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid) {
return withFileLock(() => _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) { async function _handleUpdateProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid) {
try { try {
@ -312,7 +324,11 @@ async function _handleUpdateProvider(req, res, currentConfig, providerPoolManage
* 删除特定提供商配置 * 删除特定提供商配置
*/ */
export async function handleDeleteProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid) { export async function handleDeleteProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid) {
return withFileLock(() => _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) { async function _handleDeleteProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid) {
try { try {
@ -386,7 +402,11 @@ async function _handleDeleteProvider(req, res, currentConfig, providerPoolManage
* 禁用/启用特定提供商配置 * 禁用/启用特定提供商配置
*/ */
export async function handleDisableEnableProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid, action) { export async function handleDisableEnableProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid, action) {
return withFileLock(() => _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) { async function _handleDisableEnableProvider(req, res, currentConfig, providerPoolManager, providerType, providerUuid, action) {
try { try {

View file

@ -15,10 +15,10 @@ export const HEALTH_CHECK = {
// 密码安全相关常量 // 密码安全相关常量
export const PASSWORD = { export const PASSWORD = {
// 最小密码长度 // 最小密码长度最少12位与现代安全实践一致
MIN_LENGTH: 8, MIN_LENGTH: 12,
// PBKDF2迭代次数 // PBKDF2迭代次数OWASP 2023建议 SHA-512 ≥310,000次
PBKDF2_ITERATIONS: 100000, PBKDF2_ITERATIONS: 310000,
// PBKDF2密钥长度字节 // PBKDF2密钥长度字节
PBKDF2_KEYLEN: 64, PBKDF2_KEYLEN: 64,
// PBKDF2哈希算法 // PBKDF2哈希算法
@ -38,7 +38,5 @@ export const NETWORK = {
// 请求重试相关常量 // 请求重试相关常量
export const RETRY = { export const RETRY = {
// 最大重试次数 // 最大重试次数
MAX_RETRIES: 100, MAX_RETRIES: 100
// 默认重试次数
DEFAULT_RETRIES: 3
}; };

View file

@ -94,10 +94,19 @@ export const PROVIDER_MAPPINGS = [
/** /**
* 生成 UUID * 生成 UUID
* 兼容旧版 Node.js<14.17.0如果 crypto.randomUUID 不存在则使用 Math.random 回退方案
* @returns {string} UUID 字符串 * @returns {string} UUID 字符串
*/ */
export function generateUUID() { export function generateUUID() {
return crypto.randomUUID(); if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
// 回退方案:使用 Math.random 生成标准 UUID v4
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
} }
/** /**

View file

@ -1195,6 +1195,13 @@ const translations = {
'config.proxy.tlsSidecarProxyUrl': 'Sidecar Upstream Proxy', 'config.proxy.tlsSidecarProxyUrl': 'Sidecar Upstream Proxy',
'config.proxy.tlsSidecarEnabledProviders': 'Providers Using TLS Sidecar', 'config.proxy.tlsSidecarEnabledProviders': 'Providers Using TLS Sidecar',
'config.proxy.tlsSidecarNote': 'When enabled, requests for selected providers are routed through Go uTLS sidecar for perfect Chrome TLS/H2 fingerprint to bypass Cloudflare (requires restart)', 'config.proxy.tlsSidecarNote': 'When enabled, requests for selected providers are routed through Go uTLS sidecar for perfect Chrome TLS/H2 fingerprint to bypass Cloudflare (requires restart)',
'config.healthCheck.title': 'Scheduled Health Check',
'config.healthCheck.enabled': 'Enable Scheduled Check',
'config.healthCheck.startupRun': 'Run on Startup',
'config.healthCheck.interval': 'Check Interval',
'config.healthCheck.intervalNote': 'In milliseconds. Minimum 60000ms (1 min), maximum 3600000ms (1 hour). Enter manually or use quick select buttons',
'config.healthCheck.providerTypes': 'Providers to Check',
'config.healthCheck.providerTypesNote': 'Select provider types for scheduled health checks. Leave empty to skip all checks',
'config.log.title': 'Log Settings', 'config.log.title': 'Log Settings',
'config.log.enabled': 'Enable Logging', 'config.log.enabled': 'Enable Logging',
'config.log.outputMode': 'Log Output Mode', 'config.log.outputMode': 'Log Output Mode',