import { launchRpaBrowser } from "@/lib/rpa/browser-launch"; import { getRpaBrowserLocale } from "@/lib/rpa/env"; import { RpaError } from "@/modules/rpa/errors"; import { getStorageStatePath, persistContext, storageStateExists, } from "@/lib/rpa/storage-state"; import { quotePageAdapter } from "@/workers/rpa/quote-page-adapter"; const DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"; let bootstrapPromise: Promise | null = null; /** 缺 .rpa/mothership-storage.json 时是否 headless 自动访问 MotherShip(默认 true) */ export function isAutoBootstrapStorageEnabled(): boolean { if (process.env.RPA_MOCK_MODE === "true") { return false; } return process.env.RPA_AUTO_BOOTSTRAP_STORAGE?.trim().toLowerCase() !== "false"; } function missingStorageMessage(): string { const path = getStorageStatePath(); if (isAutoBootstrapStorageEnabled()) { return `缺少 MotherShip 会话文件 ${path},且自动获取失败;请检查 VPN/网络与 MOTHERSHIP_QUOTE_URLS,或运行 npm run record:mothership`; } return `缺少 MotherShip 会话文件 ${path};请运行 npm run record:mothership 录制 cookies,或设 RPA_AUTO_BOOTSTRAP_STORAGE=true(默认开启)`; } export function assertStorageStatePresent(): void { if (!storageStateExists()) { throw new Error(missingStorageMessage()); } } async function runStorageBootstrap(): Promise { const timeoutMs = Number(process.env.RPA_STORAGE_BOOTSTRAP_MS ?? 90_000); await Promise.race([ runStorageBootstrapInner(), new Promise((_, reject) => setTimeout( () => reject( new RpaError( "QUOTE_ENTRY_UNAVAILABLE", `自动获取 MotherShip 会话超时(${Math.round(timeoutMs / 1000)}s);请检查 VPN/网络与 MOTHERSHIP_QUOTE_URLS,或运行 npm run record:mothership`, { retryable: true }, ), ), timeoutMs, ), ), ]); } async function runStorageBootstrapInner(): Promise { if (storageStateExists()) { return; } const statePath = getStorageStatePath(); console.log(`[rpa] 缺少 storageState,自动访问 MotherShip 获取会话 → ${statePath}`); const headless = process.env.RPA_HEADED === "true" ? false : process.env.RPA_HEADLESS !== "false"; const browser = await launchRpaBrowser({ headless }); try { const locale = getRpaBrowserLocale(); const context = await browser.newContext({ viewport: { width: 1280, height: 900 }, locale, extraHTTPHeaders: { "Accept-Language": `${locale},en;q=0.9` }, userAgent: DEFAULT_USER_AGENT, }); const page = await context.newPage(); try { const { activeUrl } = await quotePageAdapter.resolveQuoteEntry(page); await quotePageAdapter.preCheck(page); await persistContext(context); console.log( `[rpa] MotherShip 会话已自动写入 ${statePath}(入口 ${activeUrl})`, ); } finally { await page.close().catch(() => undefined); await context.close().catch(() => undefined); } } finally { await browser.close().catch(() => undefined); } if (!storageStateExists()) { throw new RpaError( "QUOTE_ENTRY_UNAVAILABLE", missingStorageMessage(), { retryable: true }, ); } } /** 确保 MotherShip Playwright storageState 存在(缺失时自动 bootstrap,并发安全) */ export async function ensureMothershipStorageState(): Promise { if (storageStateExists()) { return; } if (!isAutoBootstrapStorageEnabled()) { throw new Error(missingStorageMessage()); } if (!bootstrapPromise) { bootstrapPromise = runStorageBootstrap() .catch((error) => { throw error; }) .finally(() => { bootstrapPromise = null; }); } await bootstrapPromise; }