/** 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 isRpaHeaded(); } /** * 有头调试:RPA_HEADED=true 或 RPA_HEADLESS=false * (与 MotherShip logged-in 观测模式一致) */ export function isRpaHeaded(): boolean { if (process.env.RPA_HEADED === "true") { return true; } return process.env.RPA_HEADLESS === "false"; } /** Playwright launch 用:有头时 false */ export function resolveRpaHeadless(): boolean { return !isRpaHeaded(); } /** * 有头默认 slowMo=200(便于肉眼跟手);无头不设;可用 RPA_SLOW_MO_MS 覆盖 */ export function resolveRpaSlowMoMs(): number | undefined { const raw = process.env.RPA_SLOW_MO_MS?.trim(); if (raw !== undefined && raw !== "" && Number.isFinite(Number(raw))) { const n = Number(raw); return n > 0 ? n : undefined; } return isRpaHeaded() ? 200 : undefined; } /** 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"; } /** API 侧 inline direct quote 快乐路径;默认开启,失败再回退队列 */ export function isInlineDirectQuoteEnabled(): boolean { const raw = process.env.RPA_INLINE_DIRECT_QUOTE?.trim().toLowerCase(); return raw !== "false" && raw !== "0"; } /** 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 (isRpaAddressRushMode()) { 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"; } /** 20s 目标:单查询联想 + 短超时 + 跳过 Direct 重试(探针/热路径) */ export function isRpaFastQuoteMode(): boolean { return process.env.RPA_FAST_QUOTE === "true"; } /** 填址/货物 rush 闸门(visual-rush 与 fast-quote 共用) */ export function isRpaAddressRushMode(): boolean { return isRpaVisualRushMode() || isRpaFastQuoteMode(); } /** 盲执行:填址→点联想→继续,不检测 chip/commit,仅最终验报价 */ export function isRpaBlindAddressFlow(): boolean { const raw = process.env.RPA_BLIND_ADDRESS_FLOW?.trim().toLowerCase(); if (raw === "false") { return false; } if (raw === "true") { return true; } return isRpaFastQuoteMode() || isRpaVisualRushMode(); } function isValidMothershipOptionId(id: string | undefined): boolean { const trimmed = id?.trim() ?? ""; if (!trimmed) { return false; } if (trimmed.startsWith("ms_")) { return false; } return true; } /** embed-demo / 消歧弹窗已选 MotherShip 候选:两侧均有真实 optionId */ export function isPreconfirmedMothershipQuote( req: Pick< import("@/modules/providers/quote-provider").QuoteRequest, "pickup" | "delivery" >, ): boolean { const pickupId = req.pickup.mothershipOptionId?.trim() || req.pickup.placeId?.trim(); const deliveryId = req.delivery.mothershipOptionId?.trim() || req.delivery.placeId?.trim(); return ( req.pickup.selectedFromMothership === true && req.delivery.selectedFromMothership === true && isValidMothershipOptionId(pickupId) && isValidMothershipOptionId(deliveryId) ); } /** 环境变量或已确认 MotherShip 候选时启用 blind-flow */ export function resolveBlindAddressFlow( req?: Pick< import("@/modules/providers/quote-provider").QuoteRequest, "pickup" | "delivery" >, ): boolean { if (isRpaBlindAddressFlow()) { return true; } return Boolean(req && isPreconfirmedMothershipQuote(req)); } export type BlindAddressFlowCarrier = { state?: { blindAddressFlow?: boolean }; }; /** 当前任务是否走 blind 地址流(含 embed 已确认候选) */ export function isBlindAddressFlowActive( carrier?: BlindAddressFlowCarrier, ): boolean { return ( isRpaBlindAddressFlow() || carrier?.state?.blindAddressFlow === true ); } /** 跳过 Direct axel/quote,直接 Widget 抓价(Direct 已知 400 的探针用) */ export function isRpaSkipDirectQuote(): boolean { return ( process.env.RPA_SKIP_DIRECT_QUOTE === "true" || isRpaFastQuoteMode() ); } /** 联想等待(毫秒);RPA_SUGGESTION_TIMEOUT_MS 优先,fast-quote 默认 1s */ export function resolveAddressSuggestionTimeoutMs(defaultMs: number): number { const raw = process.env.RPA_SUGGESTION_TIMEOUT_MS?.trim(); if (raw && Number.isFinite(Number(raw)) && Number(raw) > 0) { return Number(raw); } if (isRpaFastQuoteMode()) { return 1_000; } return defaultMs; } /** axel search 结果等待(毫秒) */ export function resolveAxelSearchWaitMs( side: "pickup" | "delivery", preknown: boolean, ): number { if (isRpaFastQuoteMode()) { return 400; } if (preknown) { return 800; } if (side === "delivery") { return 1_500; } return 8_000; } /** 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() || isRpaFastQuoteMode()) { 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"; }