You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

220 lines
6.5 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/** GD-15Mothership 报价入口 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(
`禁止的报价入口 URLGD-15: ${url};须使用 headed 录制验证的匿名公开入口`,
);
}
}
return errors;
}
/** rpa-worker 启动前 fail-fastRPA_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_URLGD-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";
}
/** 方案 Awidget 双地址 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";
}
/** 人工对照:地址步 rushsubmit 前走 cargo+business-ready */
export function isRpaHumanSubmitStrict(): boolean {
return process.env.RPA_HUMAN_SUBMIT_STRICT === "true";
}