1
0
forked from boos/BoosAPI

[codex] fix runtime settings restart hydration (#392)

* fix runtime settings restart hydration

* fix review followups for settings hydration

* dedupe log cleanup day normalization
This commit is contained in:
Cita
2026-04-02 17:27:25 +08:00
committed by GitHub
parent 1407f754b1
commit 082891a217
8 changed files with 365 additions and 279 deletions
+5 -267
View File
@@ -4,9 +4,7 @@ import fastifyStatic from '@fastify/static';
import { import {
buildFastifyOptions, buildFastifyOptions,
config, config,
normalizeTokenRouterFailureCooldownMaxSec,
} from './config.js'; } from './config.js';
import { normalizePayloadRulesConfig } from './services/payloadRules.js';
import { authMiddleware } from './middleware/auth.js'; import { authMiddleware } from './middleware/auth.js';
import { sitesRoutes } from './routes/api/sites.js'; import { sitesRoutes } from './routes/api/sites.js';
import { accountsRoutes } from './routes/api/accounts.js'; import { accountsRoutes } from './routes/api/accounts.js';
@@ -53,7 +51,11 @@ import { isPublicApiRoute, registerDesktopRoutes } from './desktop.js';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { dirname, normalize, resolve, sep } from 'path'; 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 { import {
db, db,
ensureProxyFileCompatibilityColumns, ensureProxyFileCompatibilityColumns,
@@ -73,31 +75,6 @@ function toSettingsMap(rows: Array<{ key: string; value: string }>) {
return new Map(rows.map((row) => [row.key, row.value])); return new Map(rows.map((row) => [row.key, row.value]));
} }
function parseSettingFromMap<T>(settingsMap: Map<string, string>, 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 { function normalizeSavedDbType(value: unknown): RuntimeDbDialect | null {
if (typeof value !== 'string') return null; if (typeof value !== 'string') return null;
const normalized = value.trim().toLowerCase(); const normalized = value.trim().toLowerCase();
@@ -143,245 +120,6 @@ function hasExplicitLogCleanupSettings(settingsMap: Map<string, string>): boolea
return LOG_CLEANUP_SETTING_KEYS.some((key) => settingsMap.has(key)); return LOG_CLEANUP_SETTING_KEYS.some((key) => settingsMap.has(key));
} }
function applyRuntimeSettings(settingsMap: Map<string, string>) {
const authToken = parseSettingFromMap<string>(settingsMap, 'auth_token');
if (typeof authToken === 'string' && authToken) config.authToken = authToken;
const proxyToken = parseSettingFromMap<string>(settingsMap, 'proxy_token');
if (typeof proxyToken === 'string' && proxyToken) config.proxyToken = proxyToken;
const systemProxyUrl = parseSettingFromMap<string>(settingsMap, 'system_proxy_url');
if (typeof systemProxyUrl === 'string') config.systemProxyUrl = systemProxyUrl;
const modelAvailabilityProbeEnabled = parseSettingFromMap<boolean>(settingsMap, 'model_availability_probe_enabled');
if (typeof modelAvailabilityProbeEnabled === 'boolean') {
config.modelAvailabilityProbeEnabled = modelAvailabilityProbeEnabled;
}
const codexUpstreamWebsocketEnabled = parseSettingFromMap<boolean>(settingsMap, 'codex_upstream_websocket_enabled');
if (typeof codexUpstreamWebsocketEnabled === 'boolean') {
config.codexUpstreamWebsocketEnabled = codexUpstreamWebsocketEnabled;
}
const proxyErrorKeywords = parseSettingFromMap<string[] | string>(settingsMap, 'proxy_error_keywords');
if (proxyErrorKeywords !== undefined) {
config.proxyErrorKeywords = toStringList(proxyErrorKeywords);
}
const proxyEmptyContentFailEnabled = parseSettingFromMap<boolean>(settingsMap, 'proxy_empty_content_fail_enabled');
if (typeof proxyEmptyContentFailEnabled === 'boolean') {
config.proxyEmptyContentFailEnabled = proxyEmptyContentFailEnabled;
}
const globalBlockedBrands = parseSettingFromMap<string[]>(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<unknown>(settingsMap, 'codex_header_defaults');
if (codexHeaderDefaults && typeof codexHeaderDefaults === 'object') {
const next = codexHeaderDefaults as Record<string, unknown>;
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<unknown>(settingsMap, 'payload_rules'));
}
const checkinCron = parseSettingFromMap<string>(settingsMap, 'checkin_cron');
if (typeof checkinCron === 'string' && checkinCron) config.checkinCron = checkinCron;
const checkinScheduleMode = parseSettingFromMap<string>(settingsMap, 'checkin_schedule_mode');
if (checkinScheduleMode === 'cron' || checkinScheduleMode === 'interval') {
config.checkinScheduleMode = checkinScheduleMode;
}
const checkinIntervalHours = parseSettingFromMap<number>(settingsMap, 'checkin_interval_hours');
if (typeof checkinIntervalHours === 'number' && Number.isFinite(checkinIntervalHours) && checkinIntervalHours >= 1 && checkinIntervalHours <= 24) {
config.checkinIntervalHours = Math.trunc(checkinIntervalHours);
}
const balanceRefreshCron = parseSettingFromMap<string>(settingsMap, 'balance_refresh_cron');
if (typeof balanceRefreshCron === 'string' && balanceRefreshCron) config.balanceRefreshCron = balanceRefreshCron;
const logCleanupCron = parseSettingFromMap<string>(settingsMap, 'log_cleanup_cron');
if (typeof logCleanupCron === 'string' && logCleanupCron) config.logCleanupCron = logCleanupCron;
const logCleanupUsageLogsEnabled = parseSettingFromMap<boolean>(settingsMap, 'log_cleanup_usage_logs_enabled');
if (typeof logCleanupUsageLogsEnabled === 'boolean') {
config.logCleanupUsageLogsEnabled = logCleanupUsageLogsEnabled;
}
const logCleanupProgramLogsEnabled = parseSettingFromMap<boolean>(settingsMap, 'log_cleanup_program_logs_enabled');
if (typeof logCleanupProgramLogsEnabled === 'boolean') {
config.logCleanupProgramLogsEnabled = logCleanupProgramLogsEnabled;
}
const logCleanupRetentionDays = parseSettingFromMap<number>(settingsMap, 'log_cleanup_retention_days');
if (typeof logCleanupRetentionDays === 'number' && Number.isFinite(logCleanupRetentionDays) && logCleanupRetentionDays >= 1) {
config.logCleanupRetentionDays = normalizeLogCleanupRetentionDays(logCleanupRetentionDays);
}
const proxySessionChannelConcurrencyLimit = parseSettingFromMap<number>(settingsMap, 'proxy_session_channel_concurrency_limit');
if (
typeof proxySessionChannelConcurrencyLimit === 'number'
&& Number.isFinite(proxySessionChannelConcurrencyLimit)
&& proxySessionChannelConcurrencyLimit >= 0
) {
config.proxySessionChannelConcurrencyLimit = Math.trunc(proxySessionChannelConcurrencyLimit);
}
const proxySessionChannelQueueWaitMs = parseSettingFromMap<number>(settingsMap, 'proxy_session_channel_queue_wait_ms');
if (
typeof proxySessionChannelQueueWaitMs === 'number'
&& Number.isFinite(proxySessionChannelQueueWaitMs)
&& proxySessionChannelQueueWaitMs >= 0
) {
config.proxySessionChannelQueueWaitMs = Math.trunc(proxySessionChannelQueueWaitMs);
}
const proxyDebugTraceEnabled = parseSettingFromMap<boolean>(settingsMap, 'proxy_debug_trace_enabled');
if (typeof proxyDebugTraceEnabled === 'boolean') {
config.proxyDebugTraceEnabled = proxyDebugTraceEnabled;
}
const proxyDebugCaptureHeaders = parseSettingFromMap<boolean>(settingsMap, 'proxy_debug_capture_headers');
if (typeof proxyDebugCaptureHeaders === 'boolean') {
config.proxyDebugCaptureHeaders = proxyDebugCaptureHeaders;
}
const proxyDebugCaptureBodies = parseSettingFromMap<boolean>(settingsMap, 'proxy_debug_capture_bodies');
if (typeof proxyDebugCaptureBodies === 'boolean') {
config.proxyDebugCaptureBodies = proxyDebugCaptureBodies;
}
const proxyDebugCaptureStreamChunks = parseSettingFromMap<boolean>(settingsMap, 'proxy_debug_capture_stream_chunks');
if (typeof proxyDebugCaptureStreamChunks === 'boolean') {
config.proxyDebugCaptureStreamChunks = proxyDebugCaptureStreamChunks;
}
const proxyDebugTargetSessionId = parseSettingFromMap<string>(settingsMap, 'proxy_debug_target_session_id');
if (typeof proxyDebugTargetSessionId === 'string') {
config.proxyDebugTargetSessionId = proxyDebugTargetSessionId.trim();
}
const proxyDebugTargetClientKind = parseSettingFromMap<string>(settingsMap, 'proxy_debug_target_client_kind');
if (typeof proxyDebugTargetClientKind === 'string') {
config.proxyDebugTargetClientKind = proxyDebugTargetClientKind.trim();
}
const proxyDebugTargetModel = parseSettingFromMap<string>(settingsMap, 'proxy_debug_target_model');
if (typeof proxyDebugTargetModel === 'string') {
config.proxyDebugTargetModel = proxyDebugTargetModel.trim();
}
const proxyDebugRetentionHours = parseSettingFromMap<number>(settingsMap, 'proxy_debug_retention_hours');
if (typeof proxyDebugRetentionHours === 'number' && Number.isFinite(proxyDebugRetentionHours) && proxyDebugRetentionHours >= 1) {
config.proxyDebugRetentionHours = Math.trunc(proxyDebugRetentionHours);
}
const proxyDebugMaxBodyBytes = parseSettingFromMap<number>(settingsMap, 'proxy_debug_max_body_bytes');
if (typeof proxyDebugMaxBodyBytes === 'number' && Number.isFinite(proxyDebugMaxBodyBytes) && proxyDebugMaxBodyBytes >= 1024) {
config.proxyDebugMaxBodyBytes = Math.trunc(proxyDebugMaxBodyBytes);
}
const routingWeights = parseSettingFromMap<Partial<typeof config.routingWeights>>(settingsMap, 'routing_weights');
if (routingWeights && typeof routingWeights === 'object') {
config.routingWeights = {
...config.routingWeights,
...routingWeights,
};
}
const routingFallbackUnitCost = parseSettingFromMap<number>(settingsMap, 'routing_fallback_unit_cost');
if (typeof routingFallbackUnitCost === 'number' && Number.isFinite(routingFallbackUnitCost) && routingFallbackUnitCost > 0) {
config.routingFallbackUnitCost = Math.max(1e-6, routingFallbackUnitCost);
}
const proxyFirstByteTimeoutSec = parseSettingFromMap<number>(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<number>(settingsMap, 'token_router_failure_cooldown_max_sec');
const normalizedFailureCooldownMaxSec = normalizeTokenRouterFailureCooldownMaxSec(tokenRouterFailureCooldownMaxSec);
if (normalizedFailureCooldownMaxSec != null) {
config.tokenRouterFailureCooldownMaxSec = normalizedFailureCooldownMaxSec;
}
const webhookUrl = parseSettingFromMap<string>(settingsMap, 'webhook_url');
if (typeof webhookUrl === 'string') config.webhookUrl = webhookUrl;
const barkUrl = parseSettingFromMap<string>(settingsMap, 'bark_url');
if (typeof barkUrl === 'string') config.barkUrl = barkUrl;
const serverChanKey = parseSettingFromMap<string>(settingsMap, 'serverchan_key');
if (typeof serverChanKey === 'string') config.serverChanKey = serverChanKey;
const telegramEnabled = parseSettingFromMap<boolean>(settingsMap, 'telegram_enabled');
if (typeof telegramEnabled === 'boolean') config.telegramEnabled = telegramEnabled;
const telegramApiBaseUrl = parseSettingFromMap<string>(settingsMap, 'telegram_api_base_url');
if (typeof telegramApiBaseUrl === 'string' && telegramApiBaseUrl.trim()) {
config.telegramApiBaseUrl = telegramApiBaseUrl.trim().replace(/\/+$/, '');
}
const telegramBotToken = parseSettingFromMap<string>(settingsMap, 'telegram_bot_token');
if (typeof telegramBotToken === 'string') config.telegramBotToken = telegramBotToken;
const telegramChatId = parseSettingFromMap<string>(settingsMap, 'telegram_chat_id');
if (typeof telegramChatId === 'string') config.telegramChatId = telegramChatId;
const telegramUseSystemProxy = parseSettingFromMap<boolean>(settingsMap, 'telegram_use_system_proxy');
if (typeof telegramUseSystemProxy === 'boolean') config.telegramUseSystemProxy = telegramUseSystemProxy;
const telegramMessageThreadId = parseSettingFromMap<string>(settingsMap, 'telegram_message_thread_id');
if (typeof telegramMessageThreadId === 'string') config.telegramMessageThreadId = telegramMessageThreadId;
const smtpEnabled = parseSettingFromMap<boolean>(settingsMap, 'smtp_enabled');
if (typeof smtpEnabled === 'boolean') config.smtpEnabled = smtpEnabled;
const smtpHost = parseSettingFromMap<string>(settingsMap, 'smtp_host');
if (typeof smtpHost === 'string') config.smtpHost = smtpHost;
const smtpPort = parseSettingFromMap<number>(settingsMap, 'smtp_port');
if (typeof smtpPort === 'number' && Number.isFinite(smtpPort) && smtpPort > 0) {
config.smtpPort = smtpPort;
}
const smtpSecure = parseSettingFromMap<boolean>(settingsMap, 'smtp_secure');
if (typeof smtpSecure === 'boolean') config.smtpSecure = smtpSecure;
const smtpUser = parseSettingFromMap<string>(settingsMap, 'smtp_user');
if (typeof smtpUser === 'string') config.smtpUser = smtpUser;
const smtpPass = parseSettingFromMap<string>(settingsMap, 'smtp_pass');
if (typeof smtpPass === 'string') config.smtpPass = smtpPass;
const smtpFrom = parseSettingFromMap<string>(settingsMap, 'smtp_from');
if (typeof smtpFrom === 'string') config.smtpFrom = smtpFrom;
const smtpTo = parseSettingFromMap<string>(settingsMap, 'smtp_to');
if (typeof smtpTo === 'string') config.smtpTo = smtpTo;
const notifyCooldownSec = parseSettingFromMap<number>(settingsMap, 'notify_cooldown_sec');
if (typeof notifyCooldownSec === 'number' && Number.isFinite(notifyCooldownSec) && notifyCooldownSec >= 0) {
config.notifyCooldownSec = Math.trunc(notifyCooldownSec);
}
const adminIpAllowlist = parseSettingFromMap<string[] | string>(settingsMap, 'admin_ip_allowlist');
if (adminIpAllowlist !== undefined) {
config.adminIpAllowlist = toStringList(adminIpAllowlist);
}
}
// Ensure the current runtime database is bootstrapped before reading settings. // Ensure the current runtime database is bootstrapped before reading settings.
await ensureRuntimeDatabaseReady({ await ensureRuntimeDatabaseReady({
dialect: runtimeDbDialect, dialect: runtimeDbDialect,
+1 -1
View File
@@ -38,7 +38,7 @@ import { formatUtcSqlDateTime, getResolvedTimeZone } from '../../services/localT
import { extractClientIp, findInvalidIpAllowlistEntries, isIpAllowed } from '../../middleware/auth.js'; import { extractClientIp, findInvalidIpAllowlistEntries, isIpAllowed } from '../../middleware/auth.js';
import { invalidateSiteProxyCache, normalizeSiteProxyUrl, withExplicitProxyRequestInit } from '../../services/siteProxy.js'; import { invalidateSiteProxyCache, normalizeSiteProxyUrl, withExplicitProxyRequestInit } from '../../services/siteProxy.js';
import { performFactoryReset } from '../../services/factoryResetService.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 { stopProxyLogRetentionService } from '../../services/proxyLogRetentionService.js';
import { import {
startModelAvailabilityProbeScheduler, startModelAvailabilityProbeScheduler,
@@ -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);
});
});
+289
View File
@@ -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<T>(settingsMap: Map<string, string>, 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<string, string>) {
const authToken = parseSettingFromMap<string>(settingsMap, 'auth_token');
if (typeof authToken === 'string' && authToken) config.authToken = authToken;
const proxyToken = parseSettingFromMap<string>(settingsMap, 'proxy_token');
if (typeof proxyToken === 'string' && proxyToken) config.proxyToken = proxyToken;
const systemProxyUrl = parseSettingFromMap<string>(settingsMap, 'system_proxy_url');
if (typeof systemProxyUrl === 'string') config.systemProxyUrl = systemProxyUrl;
const modelAvailabilityProbeEnabled = parseSettingFromMap<boolean>(settingsMap, 'model_availability_probe_enabled');
if (typeof modelAvailabilityProbeEnabled === 'boolean') {
config.modelAvailabilityProbeEnabled = modelAvailabilityProbeEnabled;
}
const codexUpstreamWebsocketEnabled = parseSettingFromMap<boolean>(settingsMap, 'codex_upstream_websocket_enabled');
if (typeof codexUpstreamWebsocketEnabled === 'boolean') {
config.codexUpstreamWebsocketEnabled = codexUpstreamWebsocketEnabled;
}
const disableCrossProtocolFallback = parseSettingFromMap<boolean>(settingsMap, 'disable_cross_protocol_fallback');
if (typeof disableCrossProtocolFallback === 'boolean') {
config.disableCrossProtocolFallback = disableCrossProtocolFallback;
}
const proxyErrorKeywords = parseSettingFromMap<string[] | string>(settingsMap, 'proxy_error_keywords');
if (proxyErrorKeywords !== undefined) {
config.proxyErrorKeywords = toStringList(proxyErrorKeywords);
}
const proxyEmptyContentFailEnabled = parseSettingFromMap<boolean>(settingsMap, 'proxy_empty_content_fail_enabled');
if (typeof proxyEmptyContentFailEnabled === 'boolean') {
config.proxyEmptyContentFailEnabled = proxyEmptyContentFailEnabled;
}
const globalBlockedBrands = parseSettingFromMap<string[]>(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<string[] | string>(settingsMap, 'global_allowed_models');
if (globalAllowedModels !== undefined) {
config.globalAllowedModels = toStringList(globalAllowedModels);
}
const codexHeaderDefaults = parseSettingFromMap<unknown>(settingsMap, 'codex_header_defaults');
if (codexHeaderDefaults && typeof codexHeaderDefaults === 'object') {
const next = codexHeaderDefaults as Record<string, unknown>;
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<unknown>(settingsMap, 'payload_rules'));
}
const checkinCron = parseSettingFromMap<string>(settingsMap, 'checkin_cron');
if (typeof checkinCron === 'string' && checkinCron) config.checkinCron = checkinCron;
const checkinScheduleMode = parseSettingFromMap<string>(settingsMap, 'checkin_schedule_mode');
if (checkinScheduleMode === 'cron' || checkinScheduleMode === 'interval') {
config.checkinScheduleMode = checkinScheduleMode;
}
const checkinIntervalHours = parseSettingFromMap<number>(settingsMap, 'checkin_interval_hours');
if (typeof checkinIntervalHours === 'number' && Number.isFinite(checkinIntervalHours) && checkinIntervalHours >= 1 && checkinIntervalHours <= 24) {
config.checkinIntervalHours = Math.trunc(checkinIntervalHours);
}
const balanceRefreshCron = parseSettingFromMap<string>(settingsMap, 'balance_refresh_cron');
if (typeof balanceRefreshCron === 'string' && balanceRefreshCron) config.balanceRefreshCron = balanceRefreshCron;
const logCleanupCron = parseSettingFromMap<string>(settingsMap, 'log_cleanup_cron');
if (typeof logCleanupCron === 'string' && logCleanupCron) config.logCleanupCron = logCleanupCron;
const logCleanupUsageLogsEnabled = parseSettingFromMap<boolean>(settingsMap, 'log_cleanup_usage_logs_enabled');
if (typeof logCleanupUsageLogsEnabled === 'boolean') {
config.logCleanupUsageLogsEnabled = logCleanupUsageLogsEnabled;
}
const logCleanupProgramLogsEnabled = parseSettingFromMap<boolean>(settingsMap, 'log_cleanup_program_logs_enabled');
if (typeof logCleanupProgramLogsEnabled === 'boolean') {
config.logCleanupProgramLogsEnabled = logCleanupProgramLogsEnabled;
}
const logCleanupRetentionDays = parseSettingFromMap<number>(settingsMap, 'log_cleanup_retention_days');
if (typeof logCleanupRetentionDays === 'number' && Number.isFinite(logCleanupRetentionDays) && logCleanupRetentionDays >= 1) {
config.logCleanupRetentionDays = normalizeLogCleanupRetentionDays(logCleanupRetentionDays);
}
const proxySessionChannelConcurrencyLimit = parseSettingFromMap<number>(settingsMap, 'proxy_session_channel_concurrency_limit');
if (
typeof proxySessionChannelConcurrencyLimit === 'number'
&& Number.isFinite(proxySessionChannelConcurrencyLimit)
&& proxySessionChannelConcurrencyLimit >= 0
) {
config.proxySessionChannelConcurrencyLimit = Math.trunc(proxySessionChannelConcurrencyLimit);
}
const proxySessionChannelQueueWaitMs = parseSettingFromMap<number>(settingsMap, 'proxy_session_channel_queue_wait_ms');
if (
typeof proxySessionChannelQueueWaitMs === 'number'
&& Number.isFinite(proxySessionChannelQueueWaitMs)
&& proxySessionChannelQueueWaitMs >= 0
) {
config.proxySessionChannelQueueWaitMs = Math.trunc(proxySessionChannelQueueWaitMs);
}
const proxyDebugTraceEnabled = parseSettingFromMap<boolean>(settingsMap, 'proxy_debug_trace_enabled');
if (typeof proxyDebugTraceEnabled === 'boolean') {
config.proxyDebugTraceEnabled = proxyDebugTraceEnabled;
}
const proxyDebugCaptureHeaders = parseSettingFromMap<boolean>(settingsMap, 'proxy_debug_capture_headers');
if (typeof proxyDebugCaptureHeaders === 'boolean') {
config.proxyDebugCaptureHeaders = proxyDebugCaptureHeaders;
}
const proxyDebugCaptureBodies = parseSettingFromMap<boolean>(settingsMap, 'proxy_debug_capture_bodies');
if (typeof proxyDebugCaptureBodies === 'boolean') {
config.proxyDebugCaptureBodies = proxyDebugCaptureBodies;
}
const proxyDebugCaptureStreamChunks = parseSettingFromMap<boolean>(settingsMap, 'proxy_debug_capture_stream_chunks');
if (typeof proxyDebugCaptureStreamChunks === 'boolean') {
config.proxyDebugCaptureStreamChunks = proxyDebugCaptureStreamChunks;
}
const proxyDebugTargetSessionId = parseSettingFromMap<string>(settingsMap, 'proxy_debug_target_session_id');
if (typeof proxyDebugTargetSessionId === 'string') {
config.proxyDebugTargetSessionId = proxyDebugTargetSessionId.trim();
}
const proxyDebugTargetClientKind = parseSettingFromMap<string>(settingsMap, 'proxy_debug_target_client_kind');
if (typeof proxyDebugTargetClientKind === 'string') {
config.proxyDebugTargetClientKind = proxyDebugTargetClientKind.trim();
}
const proxyDebugTargetModel = parseSettingFromMap<string>(settingsMap, 'proxy_debug_target_model');
if (typeof proxyDebugTargetModel === 'string') {
config.proxyDebugTargetModel = proxyDebugTargetModel.trim();
}
const proxyDebugRetentionHours = parseSettingFromMap<number>(settingsMap, 'proxy_debug_retention_hours');
if (typeof proxyDebugRetentionHours === 'number' && Number.isFinite(proxyDebugRetentionHours) && proxyDebugRetentionHours >= 1) {
config.proxyDebugRetentionHours = Math.trunc(proxyDebugRetentionHours);
}
const proxyDebugMaxBodyBytes = parseSettingFromMap<number>(settingsMap, 'proxy_debug_max_body_bytes');
if (typeof proxyDebugMaxBodyBytes === 'number' && Number.isFinite(proxyDebugMaxBodyBytes) && proxyDebugMaxBodyBytes >= 1024) {
config.proxyDebugMaxBodyBytes = Math.trunc(proxyDebugMaxBodyBytes);
}
const routingWeights = parseSettingFromMap<Partial<typeof config.routingWeights>>(settingsMap, 'routing_weights');
if (routingWeights && typeof routingWeights === 'object') {
config.routingWeights = {
...config.routingWeights,
...routingWeights,
};
}
const routingFallbackUnitCost = parseSettingFromMap<number>(settingsMap, 'routing_fallback_unit_cost');
if (typeof routingFallbackUnitCost === 'number' && Number.isFinite(routingFallbackUnitCost) && routingFallbackUnitCost > 0) {
config.routingFallbackUnitCost = Math.max(1e-6, routingFallbackUnitCost);
}
const proxyFirstByteTimeoutSec = parseSettingFromMap<number>(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<number>(settingsMap, 'token_router_failure_cooldown_max_sec');
const normalizedFailureCooldownMaxSec = normalizeTokenRouterFailureCooldownMaxSec(tokenRouterFailureCooldownMaxSec);
if (normalizedFailureCooldownMaxSec != null) {
config.tokenRouterFailureCooldownMaxSec = normalizedFailureCooldownMaxSec;
}
const webhookUrl = parseSettingFromMap<string>(settingsMap, 'webhook_url');
if (typeof webhookUrl === 'string') config.webhookUrl = webhookUrl;
const webhookEnabled = parseSettingFromMap<boolean>(settingsMap, 'webhook_enabled');
if (typeof webhookEnabled === 'boolean') config.webhookEnabled = webhookEnabled;
const barkUrl = parseSettingFromMap<string>(settingsMap, 'bark_url');
if (typeof barkUrl === 'string') config.barkUrl = barkUrl;
const barkEnabled = parseSettingFromMap<boolean>(settingsMap, 'bark_enabled');
if (typeof barkEnabled === 'boolean') config.barkEnabled = barkEnabled;
const serverChanEnabled = parseSettingFromMap<boolean>(settingsMap, 'serverchan_enabled');
if (typeof serverChanEnabled === 'boolean') config.serverChanEnabled = serverChanEnabled;
const serverChanKey = parseSettingFromMap<string>(settingsMap, 'serverchan_key');
if (typeof serverChanKey === 'string') config.serverChanKey = serverChanKey;
const telegramEnabled = parseSettingFromMap<boolean>(settingsMap, 'telegram_enabled');
if (typeof telegramEnabled === 'boolean') config.telegramEnabled = telegramEnabled;
const telegramApiBaseUrl = parseSettingFromMap<string>(settingsMap, 'telegram_api_base_url');
if (typeof telegramApiBaseUrl === 'string' && telegramApiBaseUrl.trim()) {
config.telegramApiBaseUrl = telegramApiBaseUrl.trim().replace(/\/+$/, '');
}
const telegramBotToken = parseSettingFromMap<string>(settingsMap, 'telegram_bot_token');
if (typeof telegramBotToken === 'string') config.telegramBotToken = telegramBotToken;
const telegramChatId = parseSettingFromMap<string>(settingsMap, 'telegram_chat_id');
if (typeof telegramChatId === 'string') config.telegramChatId = telegramChatId;
const telegramUseSystemProxy = parseSettingFromMap<boolean>(settingsMap, 'telegram_use_system_proxy');
if (typeof telegramUseSystemProxy === 'boolean') config.telegramUseSystemProxy = telegramUseSystemProxy;
const telegramMessageThreadId = parseSettingFromMap<string>(settingsMap, 'telegram_message_thread_id');
if (typeof telegramMessageThreadId === 'string') config.telegramMessageThreadId = telegramMessageThreadId;
const smtpEnabled = parseSettingFromMap<boolean>(settingsMap, 'smtp_enabled');
if (typeof smtpEnabled === 'boolean') config.smtpEnabled = smtpEnabled;
const smtpHost = parseSettingFromMap<string>(settingsMap, 'smtp_host');
if (typeof smtpHost === 'string') config.smtpHost = smtpHost;
const smtpPort = parseSettingFromMap<number>(settingsMap, 'smtp_port');
if (typeof smtpPort === 'number' && Number.isFinite(smtpPort) && smtpPort > 0) {
config.smtpPort = Math.trunc(smtpPort);
}
const smtpSecure = parseSettingFromMap<boolean>(settingsMap, 'smtp_secure');
if (typeof smtpSecure === 'boolean') config.smtpSecure = smtpSecure;
const smtpUser = parseSettingFromMap<string>(settingsMap, 'smtp_user');
if (typeof smtpUser === 'string') config.smtpUser = smtpUser;
const smtpPass = parseSettingFromMap<string>(settingsMap, 'smtp_pass');
if (typeof smtpPass === 'string') config.smtpPass = smtpPass;
const smtpFrom = parseSettingFromMap<string>(settingsMap, 'smtp_from');
if (typeof smtpFrom === 'string') config.smtpFrom = smtpFrom;
const smtpTo = parseSettingFromMap<string>(settingsMap, 'smtp_to');
if (typeof smtpTo === 'string') config.smtpTo = smtpTo;
const notifyCooldownSec = parseSettingFromMap<number>(settingsMap, 'notify_cooldown_sec');
if (typeof notifyCooldownSec === 'number' && Number.isFinite(notifyCooldownSec) && notifyCooldownSec >= 0) {
config.notifyCooldownSec = Math.trunc(notifyCooldownSec);
}
const adminIpAllowlist = parseSettingFromMap<string[] | string>(settingsMap, 'admin_ip_allowlist');
if (adminIpAllowlist !== undefined) {
config.adminIpAllowlist = toStringList(adminIpAllowlist);
}
}
+2 -1
View File
@@ -7,7 +7,8 @@ import { checkinAll } from './checkinService.js';
import * as routeRefreshWorkflow from './routeRefreshWorkflow.js'; import * as routeRefreshWorkflow from './routeRefreshWorkflow.js';
import { sendNotification } from './notifyService.js'; import { sendNotification } from './notifyService.js';
import { buildDailySummaryNotification, collectDailySummaryMetrics } from './dailySummaryService.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'; export type CheckinScheduleMode = 'cron' | 'interval';
+1 -10
View File
@@ -2,6 +2,7 @@ import { lt } from 'drizzle-orm';
import { config } from '../config.js'; import { config } from '../config.js';
import { db, schema } from '../db/index.js'; import { db, schema } from '../db/index.js';
import { formatUtcSqlDateTime } from './localTimeService.js'; import { formatUtcSqlDateTime } from './localTimeService.js';
import { normalizeLogCleanupRetentionDays } from '../shared/logCleanupRetentionDays.js';
const DAY_MS = 24 * 60 * 60 * 1000; const DAY_MS = 24 * 60 * 60 * 1000;
@@ -23,16 +24,6 @@ export type LogCleanupResult = {
totalDeleted: number; 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 { export function getLogCleanupCutoffUtc(retentionDays: number, nowMs = Date.now()): string | null {
const normalizedDays = normalizeLogCleanupRetentionDays(retentionDays); const normalizedDays = normalizeLogCleanupRetentionDays(retentionDays);
return formatUtcSqlDateTime(new Date(nowMs - normalizedDays * DAY_MS)); return formatUtcSqlDateTime(new Date(nowMs - normalizedDays * DAY_MS));
@@ -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);
});
});
@@ -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;
}