import type { Page } from "playwright"; import type { AxelSelectionBridge } from "@/workers/rpa/axel-selection-bridge"; import { normalizeAxelPlaceId } from "@/workers/rpa/axel-selection-bridge"; const AXEL_PLACE_BASE = "https://services.mothership.com/axel/location/place/"; const COMMIT_TIMEOUT_MS = 10_000; /** * 在浏览器上下文 GET place 并尝试沿 React Fiber 写入地址(widget 无 Google pac 时的兜底)。 * 为何:Node 侧 prefetch 不触发 widget 状态机;需在页面内 fetch 才可能被监听。 */ /** Playwright evaluate 须为可执行 async 函数体,勿用未调用的 function 表达式 */ export const BROWSER_FETCH_AND_INJECT_PLACE = `async (arg) => { var sideArg = arg.sideArg; var pid = arg.pid; var desc = arg.desc; var base = arg.base; var url = base + encodeURIComponent(pid); var res; try { res = await fetch(url, { credentials: "include", headers: { accept: "application/json" } }); } catch (e) { return { ok: false, reason: "fetch-failed" }; } if (!res.ok) { return { ok: false, reason: "http-" + res.status, status: res.status }; } var loc; try { loc = await res.json(); } catch (e2) { return { ok: false, reason: "json-failed" }; } var widget = document.querySelector("#react-quoter-landing-plugin"); if (!widget) { return { ok: false, reason: "no-widget", status: res.status }; } var fiberKey = Object.keys(widget).find(function(k) { return k.indexOf("__reactFiber$") === 0 || k.indexOf("__reactInternalInstance$") === 0; }); if (!fiberKey) { return { ok: false, reason: "no-fiber", status: res.status }; } var addressData = { placeId: pid, location: loc, formattedAddress: desc, isCommitted: true, }; var stateKey = sideArg === "pickup" ? "pickupAddress" : "deliveryAddress"; var injected = false; var fiber = widget[fiberKey]; while (fiber) { var stateNode = fiber.stateNode; if (stateNode && typeof stateNode.setState === "function") { var state = stateNode.state || {}; if ( "pickupAddress" in state || "deliveryAddress" in state || "pickup" in state || "delivery" in state || "addresses" in state ) { var patch = {}; patch[stateKey] = addressData; if (sideArg === "delivery" && state.pickupAddress) { patch.pickupAddress = state.pickupAddress; } stateNode.setState(patch); injected = true; break; } } var props = fiber.memoizedProps || {}; if (typeof props.onSelect === "function") { try { props.onSelect({ placeId: pid, description: desc, location: loc }); injected = true; break; } catch (e3) {} } if (typeof props.onAddressSelect === "function") { try { props.onAddressSelect({ placeId: pid, description: desc, location: loc }); injected = true; break; } catch (e4) {} } fiber = fiber.return; } return { ok: injected, reason: injected ? "injected" : "inject-failed", status: res.status }; }`; export async function commitViaBrowserPlaceFetch( page: Page, bridge: AxelSelectionBridge, placeId: string, description: string, side: "pickup" | "delivery", ): Promise<{ ok: boolean; method: string }> { const normalized = normalizeAxelPlaceId(placeId); const result = (await page.evaluate(BROWSER_FETCH_AND_INJECT_PLACE, { sideArg: side, pid: normalized, desc: description, base: AXEL_PLACE_BASE, })) as { ok: boolean; reason?: string; status?: number }; if (!result?.ok) { console.log( `[rpa] browser-place-fetch: failed reason=${result?.reason ?? "unknown"} status=${result?.status ?? 0}`, ); return { ok: false, method: "browser-place-fetch" }; } const committed = await bridge.waitForPlaceCommit(normalized, COMMIT_TIMEOUT_MS); if (committed) { bridge.setPlaceDetails(normalized, {}); console.log(`[rpa] browser-place-fetch: place/${normalized} status=${result.status}`); return { ok: true, method: "browser-place-fetch" }; } bridge.recordPlaceCommit(normalized); return { ok: true, method: "browser-place-fetch-inject" }; }