import type { Page } from "playwright"; import type { QuoteRequest } from "@/modules/providers/quote-provider"; import { pageLocator } from "@/lib/rpa/locator"; import { DETECT_MOTHERSHIP_ADDRESS_CHIP } from "@/workers/rpa/kernel/browser-scripts"; type AddressFields = QuoteRequest["pickup"]; export type AddressChipVisualProbe = { committed: boolean; reason: string; streetLine?: string; cityLine?: string; preview?: string; }; /** 纯函数:截图 chip 两行布局(门牌街名行 + City, ST 行) */ export function evaluateMotherShipAddressChipLines( lines: string[], streetNum: string, cityToken: string, ): AddressChipVisualProbe { const num = streetNum.trim(); const cityWord = cityToken.trim().split(/\s+/)[0]?.toLowerCase() ?? ""; if (!num) { return { committed: false, reason: "no-street-num" }; } for (let i = 0; i < lines.length - 1; i++) { const streetLine = lines[i]!.trim(); const cityLine = lines[i + 1]!.trim(); if (!streetLine.includes(num)) { continue; } // 未点选:整段查询串仍在同一行(含多个逗号分隔段) if (streetLine.split(",").filter((p) => p.trim().length > 0).length > 1) { continue; } if (!/,\s*[A-Z]{2}\b/.test(cityLine)) { continue; } if (cityWord && !cityLine.toLowerCase().includes(cityWord)) { continue; } return { committed: true, reason: "two-line-chip", streetLine, cityLine, }; } return { committed: false, reason: "no-two-line-chip" }; } function chipProbeArgs(address: AddressFields, widgetSelector: string) { return { widgetSelector, streetNum: address.street.match(/^\d+/)?.[0] ?? "", cityToken: address.city ?? "", streetCore: address.street.split(",")[0]?.trim() ?? address.street.trim(), }; } /** * 唯一联想确认标准:widget 内出现截图同款两行 chip(1234 Warehouse Street / Los Angeles, CA)。 * 仍在 Search 输入框内打字(多逗号长串)一律视为未确认。 */ export async function probeAddressChipVisual( page: Page, widgetSelector: string, address: AddressFields, ): Promise { const args = chipProbeArgs(address, widgetSelector); const raw = (await page .evaluate(DETECT_MOTHERSHIP_ADDRESS_CHIP, args) .catch(() => null)) as AddressChipVisualProbe | null; if (raw && typeof raw.committed === "boolean") { return raw; } return { committed: false, reason: "probe-failed" }; } export async function isAddressChipVisuallyCommitted( page: Page, widgetSelector: string, address: AddressFields, ): Promise { const probe = await probeAddressChipVisual(page, widgetSelector, address); if (probe.committed) { console.log( `[rpa] chip-visual: committed reason=${probe.reason} ` + `street="${probe.streetLine?.slice(0, 48) ?? ""}" ` + `city="${probe.cityLine?.slice(0, 32) ?? ""}"`, ); } else { console.log( `[rpa] chip-visual: not-committed reason=${probe.reason}` + (probe.preview ? ` preview="${probe.preview.slice(0, 60)}"` : ""), ); } return probe.committed; } /** 提货 chip 视觉确认 + 「Deliver to」占位可见(截图态) */ export async function isPickupChipVisuallyReady( page: Page, widgetSelector: string, pickup: AddressFields, ): Promise { if (!(await isAddressChipVisuallyCommitted(page, widgetSelector, pickup))) { return false; } return pageLocator(page, widgetSelector) .getByText(/^Deliver to$/i) .first() .isVisible() .catch(() => false); }