import { RPA_REQUIRED_SELECTOR_ENV_KEYS, RPA_SELECTOR_ENV_KEYS, } from "@/lib/rpa/env"; import { getSelectorSpecs } from "@/lib/rpa/selector-specs"; import { RpaError } from "@/modules/rpa/errors"; export type RpaSelectorKey = (typeof RPA_SELECTOR_ENV_KEYS)[number]; export type RpaRequiredSelectorKey = (typeof RPA_REQUIRED_SELECTOR_ENV_KEYS)[number]; /** CI/单测 mock 用占位 selector */ const MOCK_SELECTOR_VALUES: Record = { RPA_SELECTOR_PICKUP_STREET: '[data-testid="pickup-street"]', RPA_SELECTOR_DELIVERY_STREET: '[data-testid="delivery-street"]', RPA_SELECTOR_ADDRESS_SUGGESTION: ".address-suggestion-item", RPA_SELECTOR_SUBMIT: 'button[type="submit"]', RPA_SELECTOR_STANDARD_TAB: '[data-testid="service-standard"]', RPA_SELECTOR_GUARANTEED_TAB: '[data-testid="service-guaranteed"]', RPA_SELECTOR_LOWEST_OPTION: '[data-testid="rate-lowest"]', RPA_SELECTOR_FASTEST_OPTION: '[data-testid="rate-fastest"]', RPA_SELECTOR_PRICE: '[data-testid="rate-price"]', RPA_SELECTOR_QUOTE_WIDGET: "#react-quoter-landing-plugin", }; export function isRpaMockMode(): boolean { return process.env.RPA_MOCK_MODE === "true"; } /** 非空 selector;禁止空字符串 fallback(env 显式空串时走内置回退,仍空则 fail-fast) */ export function getSelector(key: RpaSelectorKey): string { const spec = getSelectorSpecs(key).find((s) => s.trim().length > 0)?.trim(); if (spec) { return spec; } if (isRpaMockMode()) { const mock = MOCK_SELECTOR_VALUES[key]?.trim(); if (mock) { return mock; } } throw new RpaError("STRUCT_CHANGE", `${key} 未配置或为空`, { retryable: false, }); } /** @deprecated 使用 getSelector */ export function getRpaSelector(key: RpaSelectorKey): string { return getSelector(key); } /** rpa-worker 启动前校验 registry:任一必填 key 无法解析为非空 selector 则 fail-fast */ export function assertRpaSelectorsEnv(): void { if (isRpaMockMode()) { return; } const failures: string[] = []; for (const key of RPA_REQUIRED_SELECTOR_ENV_KEYS) { try { getSelector(key); } catch { failures.push(key); } } if (failures.length > 0) { throw new Error( `[rpa-worker] RPA selector 未初始化或为空: ${failures.join(", ")}`, ); } }