You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
chajia/workers/rpa/axel-place-prefetch.ts

138 lines
4.5 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 };
}