|
|
import type { Page } from "playwright";
|
|
|
import type { AxelSelectionBridge } from "@/workers/rpa/axel-selection-bridge";
|
|
|
import { normalizeAxelPlaceId } from "@/workers/rpa/axel-selection-bridge";
|
|
|
|
|
|
const AXEL_PLACE_BASE = "https://services.mothership.com/axel/location/place/";
|
|
|
const COMMIT_TIMEOUT_MS = 10_000;
|
|
|
|
|
|
/**
|
|
|
* 在浏览器上下文 GET place 并尝试沿 React Fiber 写入地址(widget 无 Google pac 时的兜底)。
|
|
|
* 为何:Node 侧 prefetch 不触发 widget 状态机;需在页面内 fetch 才可能被监听。
|
|
|
*/
|
|
|
/** Playwright evaluate 须为可执行 async 函数体,勿用未调用的 function 表达式 */
|
|
|
export const BROWSER_FETCH_AND_INJECT_PLACE = `async (arg) => {
|
|
|
var sideArg = arg.sideArg;
|
|
|
var pid = arg.pid;
|
|
|
var desc = arg.desc;
|
|
|
var base = arg.base;
|
|
|
var url = base + encodeURIComponent(pid);
|
|
|
var res;
|
|
|
try {
|
|
|
res = await fetch(url, { credentials: "include", headers: { accept: "application/json" } });
|
|
|
} catch (e) {
|
|
|
return { ok: false, reason: "fetch-failed" };
|
|
|
}
|
|
|
if (!res.ok) {
|
|
|
return { ok: false, reason: "http-" + res.status, status: res.status };
|
|
|
}
|
|
|
var loc;
|
|
|
try {
|
|
|
loc = await res.json();
|
|
|
} catch (e2) {
|
|
|
return { ok: false, reason: "json-failed" };
|
|
|
}
|
|
|
var widget = document.querySelector("#react-quoter-landing-plugin");
|
|
|
if (!widget) {
|
|
|
return { ok: false, reason: "no-widget", status: res.status };
|
|
|
}
|
|
|
var fiberKey = Object.keys(widget).find(function(k) {
|
|
|
return k.indexOf("__reactFiber$") === 0 || k.indexOf("__reactInternalInstance$") === 0;
|
|
|
});
|
|
|
if (!fiberKey) {
|
|
|
return { ok: false, reason: "no-fiber", status: res.status };
|
|
|
}
|
|
|
var addressData = {
|
|
|
placeId: pid,
|
|
|
location: loc,
|
|
|
formattedAddress: desc,
|
|
|
isCommitted: true,
|
|
|
};
|
|
|
var stateKey = sideArg === "pickup" ? "pickupAddress" : "deliveryAddress";
|
|
|
var injected = false;
|
|
|
var fiber = widget[fiberKey];
|
|
|
while (fiber) {
|
|
|
var stateNode = fiber.stateNode;
|
|
|
if (stateNode && typeof stateNode.setState === "function") {
|
|
|
var state = stateNode.state || {};
|
|
|
if (
|
|
|
"pickupAddress" in state ||
|
|
|
"deliveryAddress" in state ||
|
|
|
"pickup" in state ||
|
|
|
"delivery" in state ||
|
|
|
"addresses" in state
|
|
|
) {
|
|
|
var patch = {};
|
|
|
patch[stateKey] = addressData;
|
|
|
if (sideArg === "delivery" && state.pickupAddress) {
|
|
|
patch.pickupAddress = state.pickupAddress;
|
|
|
}
|
|
|
stateNode.setState(patch);
|
|
|
injected = true;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
var props = fiber.memoizedProps || {};
|
|
|
if (typeof props.onSelect === "function") {
|
|
|
try {
|
|
|
props.onSelect({ placeId: pid, description: desc, location: loc });
|
|
|
injected = true;
|
|
|
break;
|
|
|
} catch (e3) {}
|
|
|
}
|
|
|
if (typeof props.onAddressSelect === "function") {
|
|
|
try {
|
|
|
props.onAddressSelect({ placeId: pid, description: desc, location: loc });
|
|
|
injected = true;
|
|
|
break;
|
|
|
} catch (e4) {}
|
|
|
}
|
|
|
fiber = fiber.return;
|
|
|
}
|
|
|
return { ok: injected, reason: injected ? "injected" : "inject-failed", status: res.status };
|
|
|
}`;
|
|
|
|
|
|
export async function commitViaBrowserPlaceFetch(
|
|
|
page: Page,
|
|
|
bridge: AxelSelectionBridge,
|
|
|
placeId: string,
|
|
|
description: string,
|
|
|
side: "pickup" | "delivery",
|
|
|
): Promise<{ ok: boolean; method: string }> {
|
|
|
const normalized = normalizeAxelPlaceId(placeId);
|
|
|
const result = (await page.evaluate(BROWSER_FETCH_AND_INJECT_PLACE, {
|
|
|
sideArg: side,
|
|
|
pid: normalized,
|
|
|
desc: description,
|
|
|
base: AXEL_PLACE_BASE,
|
|
|
})) as { ok: boolean; reason?: string; status?: number };
|
|
|
|
|
|
if (!result?.ok) {
|
|
|
console.log(
|
|
|
`[rpa] browser-place-fetch: failed reason=${result?.reason ?? "unknown"} status=${result?.status ?? 0}`,
|
|
|
);
|
|
|
return { ok: false, method: "browser-place-fetch" };
|
|
|
}
|
|
|
|
|
|
const committed = await bridge.waitForPlaceCommit(normalized, COMMIT_TIMEOUT_MS);
|
|
|
if (committed) {
|
|
|
bridge.setPlaceDetails(normalized, {});
|
|
|
console.log(`[rpa] browser-place-fetch: place/${normalized} status=${result.status}`);
|
|
|
return { ok: true, method: "browser-place-fetch" };
|
|
|
}
|
|
|
|
|
|
bridge.recordPlaceCommit(normalized);
|
|
|
return { ok: true, method: "browser-place-fetch-inject" };
|
|
|
}
|