/** GD-15:Mothership 报价入口 URL 解析与校验 */ import { assertRpaSelectorsEnv } from "@/lib/rpa/selectors"; const FORBIDDEN_QUOTE_URL_PATTERNS: RegExp[] = [ /dashboard\.mothership\.com/i, /www\.mothership\.com\/quote/i, ]; export function parseMothershipQuoteUrls(raw: string | undefined): string[] { if (!raw?.trim()) { return []; } return raw .split(",") .map((item) => item.trim()) .filter(Boolean); } export function isForbiddenQuoteUrl(url: string): boolean { return FORBIDDEN_QUOTE_URL_PATTERNS.some((pattern) => pattern.test(url)); } export function validateMothershipQuoteUrls(urls: string[]): string[] { const errors: string[] = []; if (urls.length === 0) { errors.push("MOTHERSHIP_QUOTE_URLS 未配置或为空"); return errors; } for (const url of urls) { try { new URL(url); } catch { errors.push(`无效的报价入口 URL: ${url}`); continue; } if (isForbiddenQuoteUrl(url)) { errors.push( `禁止的报价入口 URL(GD-15): ${url};须使用 headed 录制验证的匿名公开入口`, ); } } return errors; } /** rpa-worker 启动前 fail-fast(RPA_MOCK_MODE=true 时跳过) */ export function assertRpaWorkerEnv(): void { if (process.env.RPA_MOCK_MODE === "true") { return; } const urlErrors = validateMothershipQuoteUrls( parseMothershipQuoteUrls(process.env.MOTHERSHIP_QUOTE_URLS), ); if (urlErrors.length > 0) { throw new Error(`[rpa-worker] ${urlErrors.join(";")}`); } assertRpaSelectorsEnv(); const legacy = process.env.MOTHERSHIP_QUOTE_URL?.trim(); if (legacy && isForbiddenQuoteUrl(legacy)) { throw new Error( `[rpa-worker] 禁止的 MOTHERSHIP_QUOTE_URL(GD-15): ${legacy}`, ); } } /** 基线兼容:取 URL 列表首项,否则回退已废弃的 MOTHERSHIP_QUOTE_URL */ export function getPrimaryMothershipQuoteUrl(): string { const urls = parseMothershipQuoteUrls(process.env.MOTHERSHIP_QUOTE_URLS); if (urls[0]) { return urls[0]; } const legacy = process.env.MOTHERSHIP_QUOTE_URL?.trim(); if (legacy) { return legacy; } throw new Error("报价入口 URL 未配置(MOTHERSHIP_QUOTE_URLS)"); } export const RPA_SELECTOR_ENV_KEYS = [ "RPA_SELECTOR_PICKUP_STREET", "RPA_SELECTOR_DELIVERY_STREET", "RPA_SELECTOR_ADDRESS_SUGGESTION", "RPA_SELECTOR_SUBMIT", /** @deprecated Network-First:报价不再使用 tab/price selector */ "RPA_SELECTOR_STANDARD_TAB", "RPA_SELECTOR_GUARANTEED_TAB", "RPA_SELECTOR_LOWEST_OPTION", "RPA_SELECTOR_FASTEST_OPTION", "RPA_SELECTOR_PRICE", "RPA_SELECTOR_QUOTE_WIDGET", ] as const; /** worker 启动必填(仅填表 + submit;报价来自 network layer) */ export const RPA_REQUIRED_SELECTOR_ENV_KEYS = [ "RPA_SELECTOR_PICKUP_STREET", "RPA_SELECTOR_DELIVERY_STREET", "RPA_SELECTOR_ADDRESS_SUGGESTION", "RPA_SELECTOR_SUBMIT", "RPA_SELECTOR_QUOTE_WIDGET", ] as const; export const DEFAULT_RPA_STORAGE_STATE_PATH = ".rpa/mothership-storage.json"; /** MotherShip 报价 widget 强制 locale;默认 en-US 抑制翻译干扰 */ export function getRpaBrowserLocale(): string { const raw = process.env.RPA_BROWSER_LOCALE?.trim(); return raw || "en-US"; } /** RPA 浏览器会话配置:headed 调试是否保留窗口(唯一来源) */ export function shouldKeepBrowserOpen(): boolean { if (process.env.RPA_KEEP_BROWSER_OPEN === "true") { return true; } if (process.env.RPA_KEEP_BROWSER_OPEN === "false") { return false; } return process.env.RPA_HEADLESS === "false"; } /** Worker headless 默认保活 Browser 实例,避免候选/询价 job 间冷启动 */ export function shouldKeepBrowserWarm(): boolean { if (process.env.RPA_KEEP_BROWSER_WARM === "false") { return false; } if (process.env.RPA_KEEP_BROWSER_WARM === "true") { return true; } return process.env.RPA_HEADLESS !== "false"; } /** 进程内驻留页加速(实验性,默认关;需 worker + confirm 全链路才生效) */ export function isParkedSessionEnabled(): boolean { return process.env.RPA_PARKED_SESSION === "true"; } export type PlaceFixMode = "event" | "state" | "legacy"; /** place commit 修复模式:event=Google事件触发(默认) | state=React注入 | legacy=原逻辑 */ export function getRpaPlaceFixMode(): PlaceFixMode { const mode = process.env.RPA_PLACE_FIX_MODE?.trim().toLowerCase(); if (mode === "state" || mode === "legacy") { return mode; } return "event"; } export type RpaQuoteMode = "direct" | "widget"; /** * 报价模式(兼容旧配置;报价路径恒为 Direct 优先 → Widget fallback)。 * `widget` 不再表示「默认走 Widget 报价」,仅保留 env 兼容。 */ export function getRpaQuoteMode(): RpaQuoteMode { const mode = process.env.RPA_QUOTE_MODE?.trim().toLowerCase(); if (mode === "widget") { return "widget"; } return "direct"; } export function isAxelDirectQuoteMode(): boolean { return getRpaQuoteMode() === "direct"; } /** Widget 报价 fallback 是否启用(Direct 失败后);默认 true */ export function isWidgetQuoteFallbackEnabled(): boolean { const raw = process.env.RPA_DISABLE_WIDGET_QUOTE_FALLBACK?.trim().toLowerCase(); return raw !== "true" && raw !== "1"; } /** 方案 A:widget 双地址 vault + setState 拦截(widget 模式默认开启) */ export function isDualAddressPatchEnabled(): boolean { if (isRpaVisualRushMode()) { return false; } const explicit = process.env.RPA_DUAL_ADDRESS_PATCH?.trim().toLowerCase(); if (explicit === "false") { return false; } if (explicit === "true") { return true; } return false; } /** 可视验收冲刺:填址+货物后直接点报价,跳过锁定/契约/后端检测闸门 */ export function isRpaVisualRushMode(): boolean { return process.env.RPA_VISUAL_RUSH === "true"; } /** visual-rush 下报价网络等待;基准脚本可设 RPA_BENCHMARK_QUOTE_MS 拉长抓价 */ export function resolveRpaQuoteCaptureMs(defaultMs: number): number { const bench = Number(process.env.RPA_BENCHMARK_QUOTE_MS); if (Number.isFinite(bench) && bench > 0) { return bench; } if (isRpaVisualRushMode()) { return 3_000; } return defaultMs; } /** 基准/生产:必须抓到 axel 档位,禁止 sign-up-quote 空报价放行 */ export function isRpaRequireQuoteRates(): boolean { return process.env.RPA_REQUIRE_QUOTE_RATES === "true"; } /** 人工对照:地址步 rush,submit 前走 cargo+business-ready */ export function isRpaHumanSubmitStrict(): boolean { return process.env.RPA_HUMAN_SUBMIT_STRICT === "true"; }