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(); 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 { 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" }; }