|
|
/** 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";
|
|
|
}
|