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:
+5
-267
@@ -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<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 {
|
||||
if (typeof value !== 'string') return null;
|
||||
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));
|
||||
}
|
||||
|
||||
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.
|
||||
await ensureRuntimeDatabaseReady({
|
||||
dialect: runtimeDbDialect,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user