import type { Page } from "playwright"; import { isRpaAddressRushMode, isRpaBlindAddressFlow, } from "@/lib/rpa/env"; import { pageLocator } from "@/lib/rpa/locator"; import { clickFirstVisibleFromSpecs, getSelectorSpecs, } from "@/lib/rpa/selector-specs"; import { dismissCargoPopoverOnly } from "@/workers/rpa/cargo-lock"; import { scrollQuoteWidgetIntoView } from "@/workers/rpa/page-prep"; import type { QuoteSubmitOptions } from "@/workers/rpa/quote-capture/types"; const DEFAULT_WIDGET = "#react-quoter-landing-plugin"; /** 提交前 widget 是否仍含双地址 + 货物步(防误触后退/刷新后空表单) */ export async function isWidgetQuoteFormReady( page: Page, widgetSelector = DEFAULT_WIDGET, ): Promise { const text = ((await pageLocator(page, widgetSelector).innerText().catch( () => "", )) || "") .replace(/\s+/g, " ") .trim(); const stateHits = text.match(/,\s*[A-Z]{2}\b/g) ?? []; return ( /Deliver to/i.test(text) && /Freight details/i.test(text) && stateHits.length >= 2 ); } /** * 提交前准备:仅收起仍打开的货物弹层 + 滚入视口。 * 禁止点击 Freight details / 坐标盲点(会重开向导或触发后退)。 */ export async function prepareWidgetForQuoteSubmit( page: Page, widgetSelector = DEFAULT_WIDGET, ): Promise { await dismissCargoPopoverOnly(page, widgetSelector); await scrollQuoteWidgetIntoView(page, { force: true }); } /** * 点击 widget 主栏绿色「Get a freight quote」(多策略 + enabled 检查)。 */ export async function clickWidgetQuoteSubmitButton( page: Page, widgetSelector = DEFAULT_WIDGET, options?: Pick, ): Promise { await prepareWidgetForQuoteSubmit(page, widgetSelector); const rushOrBlind = isRpaAddressRushMode() || isRpaBlindAddressFlow() || options?.blindAddressFlow === true; if (!(await isWidgetQuoteFormReady(page, widgetSelector))) { if (rushOrBlind) { console.log( "[rpa] submit: blind/rush — 跳过表单就绪检测,直接尝试点击报价", ); } else { console.log( "[rpa] submit: 警告 — widget 表单未就绪(地址可能已被重置),取消点击以免空提交", ); return false; } } const widget = pageLocator(page, widgetSelector); const candidates: Array<{ label: string; loc: ReturnType }> = [ { label: "widget-get-a-freight-quote", loc: widget.getByRole("button", { name: /Get a freight quote/i }).first(), }, { label: "widget-get-freight-quote", loc: widget.getByRole("button", { name: /Get freight quote/i }).first(), }, { label: "widget-get-quote", loc: widget.getByRole("button", { name: /^GET QUOTE$/i }).first(), }, { label: "widget-text-filter", loc: widget .locator("button") .filter({ hasText: /Get a freight quote/i }) .first(), }, ]; for (const { label, loc } of candidates) { if (!(await loc.isVisible().catch(() => false))) { continue; } const disabled = await loc.isDisabled().catch(() => false); if (disabled) { console.log(`[rpa] submit: ${label} 可见但 disabled,跳过`); continue; } await loc.scrollIntoViewIfNeeded().catch(() => undefined); try { await loc.click({ force: true, timeout: 8_000 }); console.log(`[rpa] submit: 已点击 ${label}`); return true; } catch (error) { console.log( `[rpa] submit: ${label} 点击失败 — ${error instanceof Error ? error.message : String(error)}`, ); } } const fallback = await clickFirstVisibleFromSpecs( page, getSelectorSpecs("RPA_SELECTOR_SUBMIT"), ); if (fallback) { console.log("[rpa] submit: 已通过 RPA_SELECTOR_SUBMIT 回退点击"); } return fallback; }