|
|
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<string, unknown>;
|
|
|
|
|
|
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<boolean> {
|
|
|
return page.evaluate(DISPATCH_PAC_ITEM_AT_INDEX, index);
|
|
|
}
|
|
|
|
|
|
async function buildCookieHeader(page: Page): Promise<string> {
|
|
|
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<string, unknown>;
|
|
|
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 };
|
|
|
}
|