import type { Locator, Page } from "playwright"; import type { AddressLookupInput } from "@/modules/address/types"; import { AxelSelectionBridge, axelPlaceFetchCandidates, normalizeAxelPlaceId, } from "@/workers/rpa/axel-selection-bridge"; import { cdpClickSelector } from "@/workers/rpa/fix-place/cdp-click"; const AXEL_PLACE_BASE = "https://services.mothership.com/axel/location/place/"; export type AxelPlaceLocation = Record; const DISPATCH_PAC_ITEM_AT_INDEX = `(idx) => { const items = document.querySelectorAll(".pac-container .pac-item"); const item = items[idx]; if (!item) { return false; } item.scrollIntoView({ block: "center" }); const target = item.querySelector(".pac-item-query") || item; const types = ["mousedown", "mouseup", "click"]; for (let i = 0; i < types.length; i += 1) { target.dispatchEvent( new MouseEvent(types[i], { bubbles: true, cancelable: true, view: window, }), ); } return true; }`; async function dispatchPacItemAtIndex(page: Page, index: number): Promise { return page.evaluate(DISPATCH_PAC_ITEM_AT_INDEX, index); } async function buildCookieHeader(page: Page): Promise { const cookies = await page.context().cookies(); return cookies.map((c) => `${c.name}=${c.value}`).join("; "); } /** * Node 侧 GET axel/location/place(不经过 page.evaluate fetch,避免污染 AxelSelectionBridge)。 */ export async function fetchAxelPlaceLocation( page: Page, placeId: string, ): Promise<{ ok: boolean; status: number; location: AxelPlaceLocation | null }> { const cookieHeader = await buildCookieHeader(page); const request = page.context().request; for (const candidate of axelPlaceFetchCandidates(placeId)) { const url = `${AXEL_PLACE_BASE}${encodeURIComponent(candidate)}`; try { const res = await request.get(url, { headers: { cookie: cookieHeader, accept: "application/json", }, }); const status = res.status(); if (!res.ok()) { continue; } const location = (await res.json()) as Record; return { ok: true, status, location }; } catch { continue; } } return { ok: false, status: 0, location: null }; } /** * DOM 点选未触发 selection handler 时:GET place + 尝试 pac 点选,最后才 fill 回退。 */ export async function commitViaAxelPlacePrefetch( page: Page, bridge: AxelSelectionBridge, placeId: string, description: string, input: Locator, options?: { query?: AddressLookupInput; candidateIndex?: number }, ): Promise<{ ok: boolean; location: AxelPlaceLocation | null }> { const fetched = await fetchAxelPlaceLocation(page, placeId); if (!fetched.ok || !fetched.location) { console.log( `[rpa] axel-place-prefetch: GET place 失败 status=${fetched.status}`, ); return { ok: false, location: null }; } const normalized = normalizeAxelPlaceId(placeId); bridge.setPlaceDetails(normalized, fetched.location); const index = options?.candidateIndex ?? bridge.getCandidateIndex(normalized); const text = description.replace(/\s+/g, " ").trim(); await input.click({ force: true }).catch(() => undefined); if (index >= 0) { const cdpPac = await cdpClickSelector(page, ".pac-container .pac-item", index); if (cdpPac) { await page.waitForTimeout(400); if (bridge.hasPlaceCommit(normalized)) { await page.keyboard.press("Escape").catch(() => undefined); await page.keyboard.press("Tab").catch(() => undefined); console.log( `[rpa] axel-place-prefetch: cdp-pac place/${normalized} status=${fetched.status}`, ); return { ok: true, location: fetched.location }; } } await dispatchPacItemAtIndex(page, index); await page.waitForTimeout(400); if (bridge.hasPlaceCommit(normalized)) { await page.keyboard.press("Escape").catch(() => undefined); await page.keyboard.press("Tab").catch(() => undefined); console.log( `[rpa] axel-place-prefetch: pac-dispatch place/${normalized} status=${fetched.status}`, ); return { ok: true, location: fetched.location }; } } await input.fill(text).catch(() => undefined); await page.keyboard.press("Escape").catch(() => undefined); await page.keyboard.press("Tab").catch(() => undefined); await page.waitForTimeout(300); console.log( `[rpa] axel-place-prefetch: place/${normalized} status=${fetched.status}(fill 回退,未合成 commit)`, ); return { ok: false, location: fetched.location }; }