From 082891a217a8396bb5b7d47f7da03a7a9ca6b501 Mon Sep 17 00:00:00 2001 From: Cita Date: Thu, 2 Apr 2026 17:27:25 +0800 Subject: [PATCH] [codex] fix runtime settings restart hydration (#392) * fix runtime settings restart hydration * fix review followups for settings hydration * dedupe log cleanup day normalization --- src/server/index.ts | 272 +---------------- src/server/routes/api/settings.ts | 2 +- src/server/runtimeSettingsHydration.test.ts | 44 +++ src/server/runtimeSettingsHydration.ts | 289 ++++++++++++++++++ src/server/services/checkinScheduler.ts | 3 +- src/server/services/logCleanupService.ts | 11 +- .../shared/logCleanupRetentionDays.test.ts | 14 + src/server/shared/logCleanupRetentionDays.ts | 9 + 8 files changed, 365 insertions(+), 279 deletions(-) create mode 100644 src/server/runtimeSettingsHydration.test.ts create mode 100644 src/server/runtimeSettingsHydration.ts create mode 100644 src/server/shared/logCleanupRetentionDays.test.ts create mode 100644 src/server/shared/logCleanupRetentionDays.ts diff --git a/src/server/index.ts b/src/server/index.ts index 42918b1..d226468 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -4,9 +4,7 @@ import fastifyStatic from '@fastify/static'; import { buildFastifyOptions, config, - normalizeTokenRouterFailureCooldownMaxSec, } from './config.js'; -import { normalizePayloadRulesConfig } from './services/payloadRules.js'; import { authMiddleware } from './middleware/auth.js'; import { sitesRoutes } from './routes/api/sites.js'; import { accountsRoutes } from './routes/api/accounts.js'; @@ -53,7 +51,11 @@ import { isPublicApiRoute, registerDesktopRoutes } from './desktop.js'; import { existsSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, normalize, resolve, sep } from 'path'; -import { normalizeLogCleanupRetentionDays } from './services/logCleanupService.js'; +import { + applyRuntimeSettings, + parseSettingFromMap, +} from './runtimeSettingsHydration.js'; +import { normalizeLogCleanupRetentionDays } from './shared/logCleanupRetentionDays.js'; import { db, ensureProxyFileCompatibilityColumns, @@ -73,31 +75,6 @@ function toSettingsMap(rows: Array<{ key: string; value: string }>) { return new Map(rows.map((row) => [row.key, row.value])); } -function parseSettingFromMap(settingsMap: Map, key: string): T | undefined { - const raw = settingsMap.get(key); - if (typeof raw !== 'string' || !raw) return undefined; - try { - return JSON.parse(raw) as T; - } catch { - return undefined; - } -} - -function toStringList(value: unknown): string[] { - if (Array.isArray(value)) { - return value - .map((item) => (typeof item === 'string' ? item.trim() : '')) - .filter((item) => item.length > 0); - } - if (typeof value === 'string') { - return value - .split(',') - .map((item) => item.trim()) - .filter((item) => item.length > 0); - } - return []; -} - function normalizeSavedDbType(value: unknown): RuntimeDbDialect | null { if (typeof value !== 'string') return null; const normalized = value.trim().toLowerCase(); @@ -143,245 +120,6 @@ function hasExplicitLogCleanupSettings(settingsMap: Map): boolea return LOG_CLEANUP_SETTING_KEYS.some((key) => settingsMap.has(key)); } -function applyRuntimeSettings(settingsMap: Map) { - const authToken = parseSettingFromMap(settingsMap, 'auth_token'); - if (typeof authToken === 'string' && authToken) config.authToken = authToken; - - const proxyToken = parseSettingFromMap(settingsMap, 'proxy_token'); - if (typeof proxyToken === 'string' && proxyToken) config.proxyToken = proxyToken; - - const systemProxyUrl = parseSettingFromMap(settingsMap, 'system_proxy_url'); - if (typeof systemProxyUrl === 'string') config.systemProxyUrl = systemProxyUrl; - - const modelAvailabilityProbeEnabled = parseSettingFromMap(settingsMap, 'model_availability_probe_enabled'); - if (typeof modelAvailabilityProbeEnabled === 'boolean') { - config.modelAvailabilityProbeEnabled = modelAvailabilityProbeEnabled; - } - - const codexUpstreamWebsocketEnabled = parseSettingFromMap(settingsMap, 'codex_upstream_websocket_enabled'); - if (typeof codexUpstreamWebsocketEnabled === 'boolean') { - config.codexUpstreamWebsocketEnabled = codexUpstreamWebsocketEnabled; - } - - const proxyErrorKeywords = parseSettingFromMap(settingsMap, 'proxy_error_keywords'); - if (proxyErrorKeywords !== undefined) { - config.proxyErrorKeywords = toStringList(proxyErrorKeywords); - } - - const proxyEmptyContentFailEnabled = parseSettingFromMap(settingsMap, 'proxy_empty_content_fail_enabled'); - if (typeof proxyEmptyContentFailEnabled === 'boolean') { - config.proxyEmptyContentFailEnabled = proxyEmptyContentFailEnabled; - } - - const globalBlockedBrands = parseSettingFromMap(settingsMap, 'global_blocked_brands'); - if (Array.isArray(globalBlockedBrands)) { - config.globalBlockedBrands = globalBlockedBrands.filter((b): b is string => typeof b === 'string').map((b) => b.trim()).filter(Boolean); - } - - const codexHeaderDefaults = parseSettingFromMap(settingsMap, 'codex_header_defaults'); - if (codexHeaderDefaults && typeof codexHeaderDefaults === 'object') { - const next = codexHeaderDefaults as Record; - config.codexHeaderDefaults = { - userAgent: typeof next.userAgent === 'string' - ? next.userAgent.trim() - : (typeof next['user-agent'] === 'string' ? next['user-agent'].trim() : config.codexHeaderDefaults.userAgent), - betaFeatures: typeof next.betaFeatures === 'string' - ? next.betaFeatures.trim() - : (typeof next['beta-features'] === 'string' ? next['beta-features'].trim() : config.codexHeaderDefaults.betaFeatures), - }; - } - - if (settingsMap.has('payload_rules')) { - config.payloadRules = normalizePayloadRulesConfig(parseSettingFromMap(settingsMap, 'payload_rules')); - } - - const checkinCron = parseSettingFromMap(settingsMap, 'checkin_cron'); - if (typeof checkinCron === 'string' && checkinCron) config.checkinCron = checkinCron; - - const checkinScheduleMode = parseSettingFromMap(settingsMap, 'checkin_schedule_mode'); - if (checkinScheduleMode === 'cron' || checkinScheduleMode === 'interval') { - config.checkinScheduleMode = checkinScheduleMode; - } - - const checkinIntervalHours = parseSettingFromMap(settingsMap, 'checkin_interval_hours'); - if (typeof checkinIntervalHours === 'number' && Number.isFinite(checkinIntervalHours) && checkinIntervalHours >= 1 && checkinIntervalHours <= 24) { - config.checkinIntervalHours = Math.trunc(checkinIntervalHours); - } - - const balanceRefreshCron = parseSettingFromMap(settingsMap, 'balance_refresh_cron'); - if (typeof balanceRefreshCron === 'string' && balanceRefreshCron) config.balanceRefreshCron = balanceRefreshCron; - - const logCleanupCron = parseSettingFromMap(settingsMap, 'log_cleanup_cron'); - if (typeof logCleanupCron === 'string' && logCleanupCron) config.logCleanupCron = logCleanupCron; - - const logCleanupUsageLogsEnabled = parseSettingFromMap(settingsMap, 'log_cleanup_usage_logs_enabled'); - if (typeof logCleanupUsageLogsEnabled === 'boolean') { - config.logCleanupUsageLogsEnabled = logCleanupUsageLogsEnabled; - } - - const logCleanupProgramLogsEnabled = parseSettingFromMap(settingsMap, 'log_cleanup_program_logs_enabled'); - if (typeof logCleanupProgramLogsEnabled === 'boolean') { - config.logCleanupProgramLogsEnabled = logCleanupProgramLogsEnabled; - } - - const logCleanupRetentionDays = parseSettingFromMap(settingsMap, 'log_cleanup_retention_days'); - if (typeof logCleanupRetentionDays === 'number' && Number.isFinite(logCleanupRetentionDays) && logCleanupRetentionDays >= 1) { - config.logCleanupRetentionDays = normalizeLogCleanupRetentionDays(logCleanupRetentionDays); - } - - const proxySessionChannelConcurrencyLimit = parseSettingFromMap(settingsMap, 'proxy_session_channel_concurrency_limit'); - if ( - typeof proxySessionChannelConcurrencyLimit === 'number' - && Number.isFinite(proxySessionChannelConcurrencyLimit) - && proxySessionChannelConcurrencyLimit >= 0 - ) { - config.proxySessionChannelConcurrencyLimit = Math.trunc(proxySessionChannelConcurrencyLimit); - } - - const proxySessionChannelQueueWaitMs = parseSettingFromMap(settingsMap, 'proxy_session_channel_queue_wait_ms'); - if ( - typeof proxySessionChannelQueueWaitMs === 'number' - && Number.isFinite(proxySessionChannelQueueWaitMs) - && proxySessionChannelQueueWaitMs >= 0 - ) { - config.proxySessionChannelQueueWaitMs = Math.trunc(proxySessionChannelQueueWaitMs); - } - - const proxyDebugTraceEnabled = parseSettingFromMap(settingsMap, 'proxy_debug_trace_enabled'); - if (typeof proxyDebugTraceEnabled === 'boolean') { - config.proxyDebugTraceEnabled = proxyDebugTraceEnabled; - } - - const proxyDebugCaptureHeaders = parseSettingFromMap(settingsMap, 'proxy_debug_capture_headers'); - if (typeof proxyDebugCaptureHeaders === 'boolean') { - config.proxyDebugCaptureHeaders = proxyDebugCaptureHeaders; - } - - const proxyDebugCaptureBodies = parseSettingFromMap(settingsMap, 'proxy_debug_capture_bodies'); - if (typeof proxyDebugCaptureBodies === 'boolean') { - config.proxyDebugCaptureBodies = proxyDebugCaptureBodies; - } - - const proxyDebugCaptureStreamChunks = parseSettingFromMap(settingsMap, 'proxy_debug_capture_stream_chunks'); - if (typeof proxyDebugCaptureStreamChunks === 'boolean') { - config.proxyDebugCaptureStreamChunks = proxyDebugCaptureStreamChunks; - } - - const proxyDebugTargetSessionId = parseSettingFromMap(settingsMap, 'proxy_debug_target_session_id'); - if (typeof proxyDebugTargetSessionId === 'string') { - config.proxyDebugTargetSessionId = proxyDebugTargetSessionId.trim(); - } - - const proxyDebugTargetClientKind = parseSettingFromMap(settingsMap, 'proxy_debug_target_client_kind'); - if (typeof proxyDebugTargetClientKind === 'string') { - config.proxyDebugTargetClientKind = proxyDebugTargetClientKind.trim(); - } - - const proxyDebugTargetModel = parseSettingFromMap(settingsMap, 'proxy_debug_target_model'); - if (typeof proxyDebugTargetModel === 'string') { - config.proxyDebugTargetModel = proxyDebugTargetModel.trim(); - } - - const proxyDebugRetentionHours = parseSettingFromMap(settingsMap, 'proxy_debug_retention_hours'); - if (typeof proxyDebugRetentionHours === 'number' && Number.isFinite(proxyDebugRetentionHours) && proxyDebugRetentionHours >= 1) { - config.proxyDebugRetentionHours = Math.trunc(proxyDebugRetentionHours); - } - - const proxyDebugMaxBodyBytes = parseSettingFromMap(settingsMap, 'proxy_debug_max_body_bytes'); - if (typeof proxyDebugMaxBodyBytes === 'number' && Number.isFinite(proxyDebugMaxBodyBytes) && proxyDebugMaxBodyBytes >= 1024) { - config.proxyDebugMaxBodyBytes = Math.trunc(proxyDebugMaxBodyBytes); - } - - const routingWeights = parseSettingFromMap>(settingsMap, 'routing_weights'); - if (routingWeights && typeof routingWeights === 'object') { - config.routingWeights = { - ...config.routingWeights, - ...routingWeights, - }; - } - - const routingFallbackUnitCost = parseSettingFromMap(settingsMap, 'routing_fallback_unit_cost'); - if (typeof routingFallbackUnitCost === 'number' && Number.isFinite(routingFallbackUnitCost) && routingFallbackUnitCost > 0) { - config.routingFallbackUnitCost = Math.max(1e-6, routingFallbackUnitCost); - } - - const proxyFirstByteTimeoutSec = parseSettingFromMap(settingsMap, 'proxy_first_byte_timeout_sec'); - if (typeof proxyFirstByteTimeoutSec === 'number' && Number.isFinite(proxyFirstByteTimeoutSec) && proxyFirstByteTimeoutSec >= 0) { - config.proxyFirstByteTimeoutSec = Math.max(0, Math.trunc(proxyFirstByteTimeoutSec)); - } - - const tokenRouterFailureCooldownMaxSec = parseSettingFromMap(settingsMap, 'token_router_failure_cooldown_max_sec'); - const normalizedFailureCooldownMaxSec = normalizeTokenRouterFailureCooldownMaxSec(tokenRouterFailureCooldownMaxSec); - if (normalizedFailureCooldownMaxSec != null) { - config.tokenRouterFailureCooldownMaxSec = normalizedFailureCooldownMaxSec; - } - - const webhookUrl = parseSettingFromMap(settingsMap, 'webhook_url'); - if (typeof webhookUrl === 'string') config.webhookUrl = webhookUrl; - - const barkUrl = parseSettingFromMap(settingsMap, 'bark_url'); - if (typeof barkUrl === 'string') config.barkUrl = barkUrl; - - const serverChanKey = parseSettingFromMap(settingsMap, 'serverchan_key'); - if (typeof serverChanKey === 'string') config.serverChanKey = serverChanKey; - - const telegramEnabled = parseSettingFromMap(settingsMap, 'telegram_enabled'); - if (typeof telegramEnabled === 'boolean') config.telegramEnabled = telegramEnabled; - - const telegramApiBaseUrl = parseSettingFromMap(settingsMap, 'telegram_api_base_url'); - if (typeof telegramApiBaseUrl === 'string' && telegramApiBaseUrl.trim()) { - config.telegramApiBaseUrl = telegramApiBaseUrl.trim().replace(/\/+$/, ''); - } - - const telegramBotToken = parseSettingFromMap(settingsMap, 'telegram_bot_token'); - if (typeof telegramBotToken === 'string') config.telegramBotToken = telegramBotToken; - - const telegramChatId = parseSettingFromMap(settingsMap, 'telegram_chat_id'); - if (typeof telegramChatId === 'string') config.telegramChatId = telegramChatId; - - const telegramUseSystemProxy = parseSettingFromMap(settingsMap, 'telegram_use_system_proxy'); - if (typeof telegramUseSystemProxy === 'boolean') config.telegramUseSystemProxy = telegramUseSystemProxy; - - const telegramMessageThreadId = parseSettingFromMap(settingsMap, 'telegram_message_thread_id'); - if (typeof telegramMessageThreadId === 'string') config.telegramMessageThreadId = telegramMessageThreadId; - - const smtpEnabled = parseSettingFromMap(settingsMap, 'smtp_enabled'); - if (typeof smtpEnabled === 'boolean') config.smtpEnabled = smtpEnabled; - - const smtpHost = parseSettingFromMap(settingsMap, 'smtp_host'); - if (typeof smtpHost === 'string') config.smtpHost = smtpHost; - - const smtpPort = parseSettingFromMap(settingsMap, 'smtp_port'); - if (typeof smtpPort === 'number' && Number.isFinite(smtpPort) && smtpPort > 0) { - config.smtpPort = smtpPort; - } - - const smtpSecure = parseSettingFromMap(settingsMap, 'smtp_secure'); - if (typeof smtpSecure === 'boolean') config.smtpSecure = smtpSecure; - - const smtpUser = parseSettingFromMap(settingsMap, 'smtp_user'); - if (typeof smtpUser === 'string') config.smtpUser = smtpUser; - - const smtpPass = parseSettingFromMap(settingsMap, 'smtp_pass'); - if (typeof smtpPass === 'string') config.smtpPass = smtpPass; - - const smtpFrom = parseSettingFromMap(settingsMap, 'smtp_from'); - if (typeof smtpFrom === 'string') config.smtpFrom = smtpFrom; - - const smtpTo = parseSettingFromMap(settingsMap, 'smtp_to'); - if (typeof smtpTo === 'string') config.smtpTo = smtpTo; - - const notifyCooldownSec = parseSettingFromMap(settingsMap, 'notify_cooldown_sec'); - if (typeof notifyCooldownSec === 'number' && Number.isFinite(notifyCooldownSec) && notifyCooldownSec >= 0) { - config.notifyCooldownSec = Math.trunc(notifyCooldownSec); - } - - const adminIpAllowlist = parseSettingFromMap(settingsMap, 'admin_ip_allowlist'); - if (adminIpAllowlist !== undefined) { - config.adminIpAllowlist = toStringList(adminIpAllowlist); - } -} - // Ensure the current runtime database is bootstrapped before reading settings. await ensureRuntimeDatabaseReady({ dialect: runtimeDbDialect, diff --git a/src/server/routes/api/settings.ts b/src/server/routes/api/settings.ts index aa60231..765164c 100644 --- a/src/server/routes/api/settings.ts +++ b/src/server/routes/api/settings.ts @@ -38,7 +38,7 @@ import { formatUtcSqlDateTime, getResolvedTimeZone } from '../../services/localT import { extractClientIp, findInvalidIpAllowlistEntries, isIpAllowed } from '../../middleware/auth.js'; import { invalidateSiteProxyCache, normalizeSiteProxyUrl, withExplicitProxyRequestInit } from '../../services/siteProxy.js'; import { performFactoryReset } from '../../services/factoryResetService.js'; -import { normalizeLogCleanupRetentionDays } from '../../services/logCleanupService.js'; +import { normalizeLogCleanupRetentionDays } from '../../shared/logCleanupRetentionDays.js'; import { stopProxyLogRetentionService } from '../../services/proxyLogRetentionService.js'; import { startModelAvailabilityProbeScheduler, diff --git a/src/server/runtimeSettingsHydration.test.ts b/src/server/runtimeSettingsHydration.test.ts new file mode 100644 index 0000000..fba01d4 --- /dev/null +++ b/src/server/runtimeSettingsHydration.test.ts @@ -0,0 +1,44 @@ +import { afterEach, describe, expect, it } from 'vitest'; + +import { config } from './config.js'; +import { applyRuntimeSettings } from './runtimeSettingsHydration.js'; + +const originalConfig = structuredClone(config); + +afterEach(() => { + Object.assign(config, structuredClone(originalConfig)); +}); + +describe('applyRuntimeSettings', () => { + it('hydrates persisted runtime settings that should survive restarts', () => { + config.disableCrossProtocolFallback = false; + config.webhookEnabled = true; + config.barkEnabled = true; + config.serverChanEnabled = true; + config.globalAllowedModels = []; + + applyRuntimeSettings(new Map([ + ['disable_cross_protocol_fallback', JSON.stringify(true)], + ['webhook_enabled', JSON.stringify(false)], + ['bark_enabled', JSON.stringify(false)], + ['serverchan_enabled', JSON.stringify(false)], + ['global_allowed_models', JSON.stringify(['gpt-5.4', ' claude-3.7-sonnet '])], + ])); + + expect(config.disableCrossProtocolFallback).toBe(true); + expect(config.webhookEnabled).toBe(false); + expect(config.barkEnabled).toBe(false); + expect(config.serverChanEnabled).toBe(false); + expect(config.globalAllowedModels).toEqual(['gpt-5.4', 'claude-3.7-sonnet']); + }); + + it('normalizes smtpPort to a positive integer during hydration', () => { + config.smtpPort = 587; + + applyRuntimeSettings(new Map([ + ['smtp_port', JSON.stringify(587.9)], + ])); + + expect(config.smtpPort).toBe(587); + }); +}); diff --git a/src/server/runtimeSettingsHydration.ts b/src/server/runtimeSettingsHydration.ts new file mode 100644 index 0000000..1d437c0 --- /dev/null +++ b/src/server/runtimeSettingsHydration.ts @@ -0,0 +1,289 @@ +import { + config, + normalizeTokenRouterFailureCooldownMaxSec, +} from './config.js'; +import { normalizePayloadRulesConfig } from './services/payloadRules.js'; +import { normalizeLogCleanupRetentionDays } from './shared/logCleanupRetentionDays.js'; + +export function parseSettingFromMap(settingsMap: Map, key: string): T | undefined { + const raw = settingsMap.get(key); + if (typeof raw !== 'string' || !raw) return undefined; + try { + return JSON.parse(raw) as T; + } catch { + return undefined; + } +} + +function toStringList(value: unknown): string[] { + if (Array.isArray(value)) { + return value + .map((item) => (typeof item === 'string' ? item.trim() : '')) + .filter((item) => item.length > 0); + } + if (typeof value === 'string') { + return value + .split(',') + .map((item) => item.trim()) + .filter((item) => item.length > 0); + } + return []; +} + +export function applyRuntimeSettings(settingsMap: Map) { + const authToken = parseSettingFromMap(settingsMap, 'auth_token'); + if (typeof authToken === 'string' && authToken) config.authToken = authToken; + + const proxyToken = parseSettingFromMap(settingsMap, 'proxy_token'); + if (typeof proxyToken === 'string' && proxyToken) config.proxyToken = proxyToken; + + const systemProxyUrl = parseSettingFromMap(settingsMap, 'system_proxy_url'); + if (typeof systemProxyUrl === 'string') config.systemProxyUrl = systemProxyUrl; + + const modelAvailabilityProbeEnabled = parseSettingFromMap(settingsMap, 'model_availability_probe_enabled'); + if (typeof modelAvailabilityProbeEnabled === 'boolean') { + config.modelAvailabilityProbeEnabled = modelAvailabilityProbeEnabled; + } + + const codexUpstreamWebsocketEnabled = parseSettingFromMap(settingsMap, 'codex_upstream_websocket_enabled'); + if (typeof codexUpstreamWebsocketEnabled === 'boolean') { + config.codexUpstreamWebsocketEnabled = codexUpstreamWebsocketEnabled; + } + + const disableCrossProtocolFallback = parseSettingFromMap(settingsMap, 'disable_cross_protocol_fallback'); + if (typeof disableCrossProtocolFallback === 'boolean') { + config.disableCrossProtocolFallback = disableCrossProtocolFallback; + } + + const proxyErrorKeywords = parseSettingFromMap(settingsMap, 'proxy_error_keywords'); + if (proxyErrorKeywords !== undefined) { + config.proxyErrorKeywords = toStringList(proxyErrorKeywords); + } + + const proxyEmptyContentFailEnabled = parseSettingFromMap(settingsMap, 'proxy_empty_content_fail_enabled'); + if (typeof proxyEmptyContentFailEnabled === 'boolean') { + config.proxyEmptyContentFailEnabled = proxyEmptyContentFailEnabled; + } + + const globalBlockedBrands = parseSettingFromMap(settingsMap, 'global_blocked_brands'); + if (Array.isArray(globalBlockedBrands)) { + config.globalBlockedBrands = globalBlockedBrands.filter((b): b is string => typeof b === 'string').map((b) => b.trim()).filter(Boolean); + } + + const globalAllowedModels = parseSettingFromMap(settingsMap, 'global_allowed_models'); + if (globalAllowedModels !== undefined) { + config.globalAllowedModels = toStringList(globalAllowedModels); + } + + const codexHeaderDefaults = parseSettingFromMap(settingsMap, 'codex_header_defaults'); + if (codexHeaderDefaults && typeof codexHeaderDefaults === 'object') { + const next = codexHeaderDefaults as Record; + config.codexHeaderDefaults = { + userAgent: typeof next.userAgent === 'string' + ? next.userAgent.trim() + : (typeof next['user-agent'] === 'string' ? next['user-agent'].trim() : config.codexHeaderDefaults.userAgent), + betaFeatures: typeof next.betaFeatures === 'string' + ? next.betaFeatures.trim() + : (typeof next['beta-features'] === 'string' ? next['beta-features'].trim() : config.codexHeaderDefaults.betaFeatures), + }; + } + + if (settingsMap.has('payload_rules')) { + config.payloadRules = normalizePayloadRulesConfig(parseSettingFromMap(settingsMap, 'payload_rules')); + } + + const checkinCron = parseSettingFromMap(settingsMap, 'checkin_cron'); + if (typeof checkinCron === 'string' && checkinCron) config.checkinCron = checkinCron; + + const checkinScheduleMode = parseSettingFromMap(settingsMap, 'checkin_schedule_mode'); + if (checkinScheduleMode === 'cron' || checkinScheduleMode === 'interval') { + config.checkinScheduleMode = checkinScheduleMode; + } + + const checkinIntervalHours = parseSettingFromMap(settingsMap, 'checkin_interval_hours'); + if (typeof checkinIntervalHours === 'number' && Number.isFinite(checkinIntervalHours) && checkinIntervalHours >= 1 && checkinIntervalHours <= 24) { + config.checkinIntervalHours = Math.trunc(checkinIntervalHours); + } + + const balanceRefreshCron = parseSettingFromMap(settingsMap, 'balance_refresh_cron'); + if (typeof balanceRefreshCron === 'string' && balanceRefreshCron) config.balanceRefreshCron = balanceRefreshCron; + + const logCleanupCron = parseSettingFromMap(settingsMap, 'log_cleanup_cron'); + if (typeof logCleanupCron === 'string' && logCleanupCron) config.logCleanupCron = logCleanupCron; + + const logCleanupUsageLogsEnabled = parseSettingFromMap(settingsMap, 'log_cleanup_usage_logs_enabled'); + if (typeof logCleanupUsageLogsEnabled === 'boolean') { + config.logCleanupUsageLogsEnabled = logCleanupUsageLogsEnabled; + } + + const logCleanupProgramLogsEnabled = parseSettingFromMap(settingsMap, 'log_cleanup_program_logs_enabled'); + if (typeof logCleanupProgramLogsEnabled === 'boolean') { + config.logCleanupProgramLogsEnabled = logCleanupProgramLogsEnabled; + } + + const logCleanupRetentionDays = parseSettingFromMap(settingsMap, 'log_cleanup_retention_days'); + if (typeof logCleanupRetentionDays === 'number' && Number.isFinite(logCleanupRetentionDays) && logCleanupRetentionDays >= 1) { + config.logCleanupRetentionDays = normalizeLogCleanupRetentionDays(logCleanupRetentionDays); + } + + const proxySessionChannelConcurrencyLimit = parseSettingFromMap(settingsMap, 'proxy_session_channel_concurrency_limit'); + if ( + typeof proxySessionChannelConcurrencyLimit === 'number' + && Number.isFinite(proxySessionChannelConcurrencyLimit) + && proxySessionChannelConcurrencyLimit >= 0 + ) { + config.proxySessionChannelConcurrencyLimit = Math.trunc(proxySessionChannelConcurrencyLimit); + } + + const proxySessionChannelQueueWaitMs = parseSettingFromMap(settingsMap, 'proxy_session_channel_queue_wait_ms'); + if ( + typeof proxySessionChannelQueueWaitMs === 'number' + && Number.isFinite(proxySessionChannelQueueWaitMs) + && proxySessionChannelQueueWaitMs >= 0 + ) { + config.proxySessionChannelQueueWaitMs = Math.trunc(proxySessionChannelQueueWaitMs); + } + + const proxyDebugTraceEnabled = parseSettingFromMap(settingsMap, 'proxy_debug_trace_enabled'); + if (typeof proxyDebugTraceEnabled === 'boolean') { + config.proxyDebugTraceEnabled = proxyDebugTraceEnabled; + } + + const proxyDebugCaptureHeaders = parseSettingFromMap(settingsMap, 'proxy_debug_capture_headers'); + if (typeof proxyDebugCaptureHeaders === 'boolean') { + config.proxyDebugCaptureHeaders = proxyDebugCaptureHeaders; + } + + const proxyDebugCaptureBodies = parseSettingFromMap(settingsMap, 'proxy_debug_capture_bodies'); + if (typeof proxyDebugCaptureBodies === 'boolean') { + config.proxyDebugCaptureBodies = proxyDebugCaptureBodies; + } + + const proxyDebugCaptureStreamChunks = parseSettingFromMap(settingsMap, 'proxy_debug_capture_stream_chunks'); + if (typeof proxyDebugCaptureStreamChunks === 'boolean') { + config.proxyDebugCaptureStreamChunks = proxyDebugCaptureStreamChunks; + } + + const proxyDebugTargetSessionId = parseSettingFromMap(settingsMap, 'proxy_debug_target_session_id'); + if (typeof proxyDebugTargetSessionId === 'string') { + config.proxyDebugTargetSessionId = proxyDebugTargetSessionId.trim(); + } + + const proxyDebugTargetClientKind = parseSettingFromMap(settingsMap, 'proxy_debug_target_client_kind'); + if (typeof proxyDebugTargetClientKind === 'string') { + config.proxyDebugTargetClientKind = proxyDebugTargetClientKind.trim(); + } + + const proxyDebugTargetModel = parseSettingFromMap(settingsMap, 'proxy_debug_target_model'); + if (typeof proxyDebugTargetModel === 'string') { + config.proxyDebugTargetModel = proxyDebugTargetModel.trim(); + } + + const proxyDebugRetentionHours = parseSettingFromMap(settingsMap, 'proxy_debug_retention_hours'); + if (typeof proxyDebugRetentionHours === 'number' && Number.isFinite(proxyDebugRetentionHours) && proxyDebugRetentionHours >= 1) { + config.proxyDebugRetentionHours = Math.trunc(proxyDebugRetentionHours); + } + + const proxyDebugMaxBodyBytes = parseSettingFromMap(settingsMap, 'proxy_debug_max_body_bytes'); + if (typeof proxyDebugMaxBodyBytes === 'number' && Number.isFinite(proxyDebugMaxBodyBytes) && proxyDebugMaxBodyBytes >= 1024) { + config.proxyDebugMaxBodyBytes = Math.trunc(proxyDebugMaxBodyBytes); + } + + const routingWeights = parseSettingFromMap>(settingsMap, 'routing_weights'); + if (routingWeights && typeof routingWeights === 'object') { + config.routingWeights = { + ...config.routingWeights, + ...routingWeights, + }; + } + + const routingFallbackUnitCost = parseSettingFromMap(settingsMap, 'routing_fallback_unit_cost'); + if (typeof routingFallbackUnitCost === 'number' && Number.isFinite(routingFallbackUnitCost) && routingFallbackUnitCost > 0) { + config.routingFallbackUnitCost = Math.max(1e-6, routingFallbackUnitCost); + } + + const proxyFirstByteTimeoutSec = parseSettingFromMap(settingsMap, 'proxy_first_byte_timeout_sec'); + if (typeof proxyFirstByteTimeoutSec === 'number' && Number.isFinite(proxyFirstByteTimeoutSec) && proxyFirstByteTimeoutSec >= 0) { + config.proxyFirstByteTimeoutSec = Math.max(0, Math.trunc(proxyFirstByteTimeoutSec)); + } + + const tokenRouterFailureCooldownMaxSec = parseSettingFromMap(settingsMap, 'token_router_failure_cooldown_max_sec'); + const normalizedFailureCooldownMaxSec = normalizeTokenRouterFailureCooldownMaxSec(tokenRouterFailureCooldownMaxSec); + if (normalizedFailureCooldownMaxSec != null) { + config.tokenRouterFailureCooldownMaxSec = normalizedFailureCooldownMaxSec; + } + + const webhookUrl = parseSettingFromMap(settingsMap, 'webhook_url'); + if (typeof webhookUrl === 'string') config.webhookUrl = webhookUrl; + + const webhookEnabled = parseSettingFromMap(settingsMap, 'webhook_enabled'); + if (typeof webhookEnabled === 'boolean') config.webhookEnabled = webhookEnabled; + + const barkUrl = parseSettingFromMap(settingsMap, 'bark_url'); + if (typeof barkUrl === 'string') config.barkUrl = barkUrl; + + const barkEnabled = parseSettingFromMap(settingsMap, 'bark_enabled'); + if (typeof barkEnabled === 'boolean') config.barkEnabled = barkEnabled; + + const serverChanEnabled = parseSettingFromMap(settingsMap, 'serverchan_enabled'); + if (typeof serverChanEnabled === 'boolean') config.serverChanEnabled = serverChanEnabled; + + const serverChanKey = parseSettingFromMap(settingsMap, 'serverchan_key'); + if (typeof serverChanKey === 'string') config.serverChanKey = serverChanKey; + + const telegramEnabled = parseSettingFromMap(settingsMap, 'telegram_enabled'); + if (typeof telegramEnabled === 'boolean') config.telegramEnabled = telegramEnabled; + + const telegramApiBaseUrl = parseSettingFromMap(settingsMap, 'telegram_api_base_url'); + if (typeof telegramApiBaseUrl === 'string' && telegramApiBaseUrl.trim()) { + config.telegramApiBaseUrl = telegramApiBaseUrl.trim().replace(/\/+$/, ''); + } + + const telegramBotToken = parseSettingFromMap(settingsMap, 'telegram_bot_token'); + if (typeof telegramBotToken === 'string') config.telegramBotToken = telegramBotToken; + + const telegramChatId = parseSettingFromMap(settingsMap, 'telegram_chat_id'); + if (typeof telegramChatId === 'string') config.telegramChatId = telegramChatId; + + const telegramUseSystemProxy = parseSettingFromMap(settingsMap, 'telegram_use_system_proxy'); + if (typeof telegramUseSystemProxy === 'boolean') config.telegramUseSystemProxy = telegramUseSystemProxy; + + const telegramMessageThreadId = parseSettingFromMap(settingsMap, 'telegram_message_thread_id'); + if (typeof telegramMessageThreadId === 'string') config.telegramMessageThreadId = telegramMessageThreadId; + + const smtpEnabled = parseSettingFromMap(settingsMap, 'smtp_enabled'); + if (typeof smtpEnabled === 'boolean') config.smtpEnabled = smtpEnabled; + + const smtpHost = parseSettingFromMap(settingsMap, 'smtp_host'); + if (typeof smtpHost === 'string') config.smtpHost = smtpHost; + + const smtpPort = parseSettingFromMap(settingsMap, 'smtp_port'); + if (typeof smtpPort === 'number' && Number.isFinite(smtpPort) && smtpPort > 0) { + config.smtpPort = Math.trunc(smtpPort); + } + + const smtpSecure = parseSettingFromMap(settingsMap, 'smtp_secure'); + if (typeof smtpSecure === 'boolean') config.smtpSecure = smtpSecure; + + const smtpUser = parseSettingFromMap(settingsMap, 'smtp_user'); + if (typeof smtpUser === 'string') config.smtpUser = smtpUser; + + const smtpPass = parseSettingFromMap(settingsMap, 'smtp_pass'); + if (typeof smtpPass === 'string') config.smtpPass = smtpPass; + + const smtpFrom = parseSettingFromMap(settingsMap, 'smtp_from'); + if (typeof smtpFrom === 'string') config.smtpFrom = smtpFrom; + + const smtpTo = parseSettingFromMap(settingsMap, 'smtp_to'); + if (typeof smtpTo === 'string') config.smtpTo = smtpTo; + + const notifyCooldownSec = parseSettingFromMap(settingsMap, 'notify_cooldown_sec'); + if (typeof notifyCooldownSec === 'number' && Number.isFinite(notifyCooldownSec) && notifyCooldownSec >= 0) { + config.notifyCooldownSec = Math.trunc(notifyCooldownSec); + } + + const adminIpAllowlist = parseSettingFromMap(settingsMap, 'admin_ip_allowlist'); + if (adminIpAllowlist !== undefined) { + config.adminIpAllowlist = toStringList(adminIpAllowlist); + } +} diff --git a/src/server/services/checkinScheduler.ts b/src/server/services/checkinScheduler.ts index 09e9e2d..90e9701 100644 --- a/src/server/services/checkinScheduler.ts +++ b/src/server/services/checkinScheduler.ts @@ -7,7 +7,8 @@ import { checkinAll } from './checkinService.js'; import * as routeRefreshWorkflow from './routeRefreshWorkflow.js'; import { sendNotification } from './notifyService.js'; import { buildDailySummaryNotification, collectDailySummaryMetrics } from './dailySummaryService.js'; -import { cleanupConfiguredLogs, normalizeLogCleanupRetentionDays } from './logCleanupService.js'; +import { cleanupConfiguredLogs } from './logCleanupService.js'; +import { normalizeLogCleanupRetentionDays } from '../shared/logCleanupRetentionDays.js'; export type CheckinScheduleMode = 'cron' | 'interval'; diff --git a/src/server/services/logCleanupService.ts b/src/server/services/logCleanupService.ts index 0e8203a..b5745f7 100644 --- a/src/server/services/logCleanupService.ts +++ b/src/server/services/logCleanupService.ts @@ -2,6 +2,7 @@ import { lt } from 'drizzle-orm'; import { config } from '../config.js'; import { db, schema } from '../db/index.js'; import { formatUtcSqlDateTime } from './localTimeService.js'; +import { normalizeLogCleanupRetentionDays } from '../shared/logCleanupRetentionDays.js'; const DAY_MS = 24 * 60 * 60 * 1000; @@ -23,16 +24,6 @@ export type LogCleanupResult = { totalDeleted: number; }; -export function normalizeLogCleanupRetentionDays(value: unknown, fallback = 30): number { - const parsed = Number(value); - if (Number.isFinite(parsed) && parsed >= 1) return Math.trunc(parsed); - - const fallbackParsed = Number(fallback); - if (Number.isFinite(fallbackParsed) && fallbackParsed >= 1) return Math.trunc(fallbackParsed); - - return 30; -} - export function getLogCleanupCutoffUtc(retentionDays: number, nowMs = Date.now()): string | null { const normalizedDays = normalizeLogCleanupRetentionDays(retentionDays); return formatUtcSqlDateTime(new Date(nowMs - normalizedDays * DAY_MS)); diff --git a/src/server/shared/logCleanupRetentionDays.test.ts b/src/server/shared/logCleanupRetentionDays.test.ts new file mode 100644 index 0000000..b2dc8ce --- /dev/null +++ b/src/server/shared/logCleanupRetentionDays.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; + +import { normalizeLogCleanupRetentionDays } from './logCleanupRetentionDays.js'; + +describe('normalizeLogCleanupRetentionDays', () => { + it('truncates positive numeric values', () => { + expect(normalizeLogCleanupRetentionDays(14.9)).toBe(14); + }); + + it('falls back to the provided default for invalid values', () => { + expect(normalizeLogCleanupRetentionDays('oops', 7)).toBe(7); + expect(normalizeLogCleanupRetentionDays(-1, 7)).toBe(7); + }); +}); diff --git a/src/server/shared/logCleanupRetentionDays.ts b/src/server/shared/logCleanupRetentionDays.ts new file mode 100644 index 0000000..2bf6dad --- /dev/null +++ b/src/server/shared/logCleanupRetentionDays.ts @@ -0,0 +1,9 @@ +export function normalizeLogCleanupRetentionDays(value: unknown, fallback = 30): number { + const parsed = Number(value); + if (Number.isFinite(parsed) && parsed >= 1) return Math.trunc(parsed); + + const fallbackParsed = Number(fallback); + if (Number.isFinite(fallbackParsed) && fallbackParsed >= 1) return Math.trunc(fallbackParsed); + + return 30; +}