import process from "node:process"; import { runtimeConfig } from "../config/runtime.js"; import { runDatabaseMigrations } from "../db/migrate.js"; import { ensureAdminUsersBootstrapped } from "../services/admin/admin-auth-service.js"; import { ensureFulfillmentCatalogBootstrapped } from "../services/bootstrap/fulfillment-bootstrap-service.js"; import { startScheduledJobs } from "../services/scheduler/scheduler-service.js"; import { warmupOcrService } from "../services/session/ocr.js"; import { warmupTencentBrowser } from "../services/session/session.js"; import { logError, logInfo, logWarn } from "../utils/logger.js"; import { formatStartupError, type StartupState } from "./state.js"; const CORE_BOOT_RETRY_DELAY_MS = 5_000; export async function bootstrapCoreServices( startupState: StartupState, isShutdownStarted: () => boolean ) { if (startupState.core.running || isShutdownStarted()) { return; } startupState.core.running = true; while (!isShutdownStarted() && !startupState.core.ready) { startupState.phase = startupState.core.attemptCount === 0 ? "starting" : "retrying"; startupState.core.attemptCount += 1; startupState.core.lastAttemptAt = new Date().toISOString(); try { logInfo("[startup]", "开始执行核心启动步骤", { attempt: startupState.core.attemptCount, }); await runDatabaseMigrations(); await ensureFulfillmentCatalogBootstrapped(); await ensureAdminUsersBootstrapped(); startupState.core.ready = true; startupState.core.lastError = ""; startupState.phase = "ready"; startupState.core.readyAt = new Date().toISOString(); logInfo("[startup]", "核心启动步骤完成,服务已就绪", { attempt: startupState.core.attemptCount, }); startScheduledJobs(); void bootstrapBrowser(startupState); void bootstrapOcr(startupState); break; } catch (error) { const message = formatStartupError(error); startupState.phase = "retrying"; startupState.core.lastError = message; logError("[startup]", "核心启动步骤失败,将自动重试", { attempt: startupState.core.attemptCount, retryDelayMs: CORE_BOOT_RETRY_DELAY_MS, error, }); await sleep(CORE_BOOT_RETRY_DELAY_MS); } } startupState.core.running = false; } async function bootstrapBrowser(startupState: StartupState) { startupState.browser.status = "starting"; startupState.browser.lastAttemptAt = new Date().toISOString(); try { const result = await warmupTencentBrowser(); if (!result.warmed) { startupState.browser.status = "skipped"; startupState.browser.message = "browser prewarm skipped"; logInfo("[startup]", "browser prewarm skipped"); return; } startupState.browser.status = "ready"; startupState.browser.message = "browser prewarm ready"; logInfo("[startup]", "browser prewarm ready"); } catch (error) { if (isMissingPlaywrightBrowserError(error)) { const installCommand = resolveBrowserInstallCommand(); startupState.browser.status = "degraded"; startupState.browser.message = formatErrorMessage(error); logWarn("[startup]", `browser prewarm skipped: ${formatErrorMessage(error)}`); logWarn("[startup]", `请先安装浏览器依赖: ${installCommand}`); if (String(runtimeConfig.browser.chromePath || "").trim()) { logWarn("[startup]", "当前设置了 CHROME_PATH,也请确认该路径指向的浏览器可执行文件真实存在"); } return; } startupState.browser.status = "degraded"; startupState.browser.message = formatStartupError(error); logError("[startup]", "browser prewarm failed", error); } } async function bootstrapOcr(startupState: StartupState) { startupState.ocr.status = "starting"; startupState.ocr.lastAttemptAt = new Date().toISOString(); try { await warmupOcrService(); startupState.ocr.status = "ready"; startupState.ocr.message = "OCR service ready"; logInfo("[startup]", "OCR service ready"); } catch (error) { startupState.ocr.status = "degraded"; startupState.ocr.message = formatStartupError(error); logWarn("[startup]", `OCR service skipped: ${formatStartupError(error)}`); } } function isMissingPlaywrightBrowserError(error: unknown) { const message = formatErrorMessage(error).toLowerCase(); return ( message.includes("executable doesn't exist") || message.includes("please run the following command to download new browsers") || (message.includes("browserType.launch".toLowerCase()) && message.includes("please run")) ); } function formatErrorMessage(error: unknown) { if (error instanceof Error && error.message) { return error.message.split("\n")[0].trim(); } return String(error || "未知错误").trim(); } function resolveBrowserInstallCommand() { if (process.platform === "linux") { return "npm run browser:install:linux"; } return "npm run browser:install"; } function sleep(ms: number) { return new Promise((resolve) => { setTimeout(resolve, ms); }); }