|
|
import type { Page } from "playwright";
|
|
|
import type { AddressLookupInput } from "@/modules/address/types";
|
|
|
import {
|
|
|
AxelSelectionBridge,
|
|
|
normalizeAxelPlaceId,
|
|
|
} from "@/workers/rpa/axel-selection-bridge";
|
|
|
import { cdpClickSelector } from "@/workers/rpa/fix-place/cdp-click";
|
|
|
import {
|
|
|
buildDescriptionProbes,
|
|
|
labelsMatch,
|
|
|
} from "@/workers/rpa/mothership-styled-suggestion-click";
|
|
|
|
|
|
const COMMIT_TIMEOUT_MS = 8_000;
|
|
|
|
|
|
const RESOLVE_WIDGET_ROW_BY_TEXT = `(arg) => {
|
|
|
var probes = arg.probes || [];
|
|
|
var widget = document.querySelector("#react-quoter-landing-plugin");
|
|
|
if (!widget) {
|
|
|
return null;
|
|
|
}
|
|
|
var rows = widget.querySelectorAll("div[class*='sc-']");
|
|
|
var best = null;
|
|
|
for (var i = 0; i < rows.length; i += 1) {
|
|
|
var row = rows[i];
|
|
|
if (row.querySelector("input, textarea, button")) {
|
|
|
continue;
|
|
|
}
|
|
|
var text = (row.textContent || "").replace(/\\s+/g, " ").trim();
|
|
|
if (text.length < 12) {
|
|
|
continue;
|
|
|
}
|
|
|
var matched = false;
|
|
|
for (var j = 0; j < probes.length; j += 1) {
|
|
|
var probe = probes[j].toLowerCase();
|
|
|
var lower = text.toLowerCase();
|
|
|
if (!probe) continue;
|
|
|
if (lower === probe || lower.indexOf(probe) >= 0 || probe.indexOf(lower) >= 0) {
|
|
|
matched = true;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
if (!matched) {
|
|
|
continue;
|
|
|
}
|
|
|
row.scrollIntoView({ block: "center" });
|
|
|
var rect = row.getBoundingClientRect();
|
|
|
if (!rect.width || rect.height < 18) {
|
|
|
continue;
|
|
|
}
|
|
|
if (!best || text.length > best.textLength) {
|
|
|
best = {
|
|
|
x: rect.left + rect.width / 2,
|
|
|
y: rect.top + rect.height / 2,
|
|
|
width: rect.width,
|
|
|
height: rect.height,
|
|
|
textLength: text.length,
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
if (!best) {
|
|
|
return null;
|
|
|
}
|
|
|
return {
|
|
|
x: best.x,
|
|
|
y: best.y,
|
|
|
width: best.width,
|
|
|
height: best.height,
|
|
|
};
|
|
|
}`;
|
|
|
|
|
|
function buildPlaceIdSelectors(placeId: string): string[] {
|
|
|
const normalized = normalizeAxelPlaceId(placeId);
|
|
|
const selectors = new Set<string>();
|
|
|
if (normalized) {
|
|
|
selectors.add(`[data-place-id="${normalized}"]`);
|
|
|
selectors.add(`[data-option-id="${normalized}"]`);
|
|
|
}
|
|
|
if (placeId.trim() && placeId !== normalized) {
|
|
|
selectors.add(`[data-place-id="${placeId.trim()}"]`);
|
|
|
selectors.add(`[data-option-id="${placeId.trim()}"]`);
|
|
|
}
|
|
|
return [...selectors];
|
|
|
}
|
|
|
|
|
|
async function cdpClickResolvedTarget(
|
|
|
page: Page,
|
|
|
target: { x: number; y: number; width: number; height: number },
|
|
|
): Promise<void> {
|
|
|
const client = await page.context().newCDPSession(page);
|
|
|
await client.send("Input.dispatchMouseEvent", {
|
|
|
type: "mouseMoved",
|
|
|
x: target.x,
|
|
|
y: target.y,
|
|
|
buttons: 0,
|
|
|
});
|
|
|
await client.send("Input.dispatchMouseEvent", {
|
|
|
type: "mousePressed",
|
|
|
x: target.x,
|
|
|
y: target.y,
|
|
|
button: "left",
|
|
|
clickCount: 1,
|
|
|
buttons: 1,
|
|
|
});
|
|
|
await client.send("Input.dispatchMouseEvent", {
|
|
|
type: "mouseReleased",
|
|
|
x: target.x,
|
|
|
y: target.y,
|
|
|
button: "left",
|
|
|
clickCount: 1,
|
|
|
buttons: 0,
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* CDP 真坐标点击:data-place-id → pac-item → styled 联想行。
|
|
|
*/
|
|
|
export async function commitViaCdpDomClick(
|
|
|
page: Page,
|
|
|
bridge: AxelSelectionBridge,
|
|
|
placeId: string,
|
|
|
description: string,
|
|
|
options?: {
|
|
|
candidateIndex?: number;
|
|
|
query?: AddressLookupInput;
|
|
|
},
|
|
|
): Promise<{ ok: boolean; method: string }> {
|
|
|
const normalized = normalizeAxelPlaceId(placeId);
|
|
|
const index = options?.candidateIndex ?? bridge.getCandidateIndex(normalized);
|
|
|
|
|
|
for (const selector of buildPlaceIdSelectors(placeId)) {
|
|
|
const clicked = await cdpClickSelector(page, selector, 0);
|
|
|
if (!clicked) {
|
|
|
continue;
|
|
|
}
|
|
|
await page.waitForTimeout(300);
|
|
|
if (await bridge.waitForPlaceCommit(normalized, 2_500)) {
|
|
|
console.log(`[rpa] cdp-place-commit: ${selector} 成功`);
|
|
|
return { ok: true, method: "cdp-data-place-id" };
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (index >= 0) {
|
|
|
const pacClicked = await cdpClickSelector(
|
|
|
page,
|
|
|
".pac-container .pac-item",
|
|
|
index,
|
|
|
);
|
|
|
if (pacClicked) {
|
|
|
await page.waitForTimeout(400);
|
|
|
if (await bridge.waitForPlaceCommit(normalized, COMMIT_TIMEOUT_MS)) {
|
|
|
console.log(`[rpa] cdp-place-commit: pac-item-${index} 成功`);
|
|
|
return { ok: true, method: `cdp-pac-item-${index}` };
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (options?.query) {
|
|
|
const probes = buildDescriptionProbes(description, options.query);
|
|
|
const rowTarget = (await page.evaluate(RESOLVE_WIDGET_ROW_BY_TEXT, {
|
|
|
probes,
|
|
|
})) as { x: number; y: number; width: number; height: number } | null;
|
|
|
if (rowTarget) {
|
|
|
await cdpClickResolvedTarget(page, rowTarget);
|
|
|
await page.waitForTimeout(400);
|
|
|
if (await bridge.waitForPlaceCommit(normalized, COMMIT_TIMEOUT_MS)) {
|
|
|
const matchedProbe = probes.find((probe) => labelsMatch(probe, description));
|
|
|
console.log(
|
|
|
`[rpa] cdp-place-commit: styled-row 成功 probe=${matchedProbe?.slice(0, 32) ?? "text"}`,
|
|
|
);
|
|
|
return { ok: true, method: "cdp-styled-row" };
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
console.log("[rpa] cdp-place-commit: 全部 CDP 路径未观测 place 200");
|
|
|
return { ok: false, method: "cdp-dom-click-failed" };
|
|
|
}
|