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/scripts/recon-place-trigger.ts

269 lines
8.6 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.

/**
* 逆向工程 R1-R7Google Places Autocomplete 可行性探测
*/
import "dotenv/config";
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { loadDevEnv } from "@/lib/dev-env";
import { openAddressSide, visibleStreetInput } from "@/workers/rpa/address-side";
import { ensureAddressSuggestions } from "@/workers/rpa/address-suggestions";
import { RPAContext } from "@/workers/rpa/kernel/context";
import {
AUTOCOMPLETE_MONKEY_PATCH_INIT,
buildVerdict,
} from "@/workers/rpa/fix-place/recon-helpers";
import { closeBrowser, openQuotePage } from "@/workers/rpa/session-manager";
import { quotePageAdapter } from "@/workers/rpa/quote-page-adapter";
import {
stabilizeQuoteLandingPage,
waitForQuoterWidgetHydrated,
} from "@/workers/rpa/page-prep";
loadDevEnv();
const OUT_DIR = join(process.cwd(), ".rpa", "recon");
declare global {
interface Window {
__rpaAutocompleteInstances?: Array<{
getPlace: () => unknown;
}>;
__rpaAutocompletePatched?: boolean;
}
}
function runReconProbeInBrowser() {
try {
const result = {
googleMapsLoaded:
typeof google !== "undefined" && typeof google.maps !== "undefined",
triggerAvailable: typeof google?.maps?.event?.trigger === "function",
placesLoaded:
typeof google?.maps?.places?.Autocomplete === "function",
globalAutocompleteVars: [] as string[],
inputProbes: [] as Array<{
index: number;
gmKeys: number;
pacTarget: boolean;
placeholder: string;
}>,
pacTargetInputSelector: "",
autocompleteAccessPath: "not-found",
autocompleteInstanceFound: false,
reactFiberKey: "not-found",
widgetComponentName: "unknown",
widgetStateStructure: {} as Record<string, unknown>,
cspRestrictions: "none",
placeChangedTriggerOk: false,
};
if (result.googleMapsLoaded) {
result.globalAutocompleteVars = Object.keys(window).filter((k) => {
try {
return window[k] instanceof google.maps.places.Autocomplete;
} catch {
return false;
}
});
}
const widget = document.querySelector("#react-quoter-landing-plugin");
if (widget) {
const fiberKey = Object.keys(widget).find(
(k) =>
k.startsWith("__reactFiber$") ||
k.startsWith("__reactInternalInstance$"),
);
if (fiberKey) {
result.reactFiberKey = fiberKey;
let fiber = (widget as Record<string, unknown>)[fiberKey] as {
stateNode?: { state?: Record<string, unknown> };
type?: { name?: string; displayName?: string };
return?: unknown;
};
while (fiber) {
if (fiber.stateNode?.state) {
const stateKeys = Object.keys(fiber.stateNode.state);
if (stateKeys.some((k) => /pickup|delivery|address/i.test(k))) {
result.widgetComponentName =
fiber.type?.name || fiber.type?.displayName || "unknown";
result.widgetStateStructure = Object.fromEntries(
stateKeys
.filter((k) => /pickup|delivery|address/i.test(k))
.map((k) => [k, typeof fiber.stateNode!.state![k]]),
);
}
}
fiber = fiber.return as typeof fiber;
}
}
}
const inputs = widget
? Array.from(widget.querySelectorAll('input[type="text"]'))
: Array.from(document.querySelectorAll('input[type="text"]'));
for (let i = 0; i < inputs.length; i += 1) {
const input = inputs[i]!;
const gm = Object.keys(input).filter((k) => k.startsWith("gm_"));
const pac = input.classList.contains("pac-target-input");
result.inputProbes.push({
index: i,
gmKeys: gm.length,
pacTarget: pac,
placeholder: (input as HTMLInputElement).placeholder || "",
});
if (pac && !result.pacTargetInputSelector) {
result.pacTargetInputSelector = ".pac-target-input";
}
}
if (!result.pacTargetInputSelector && inputs.length > 0) {
result.pacTargetInputSelector = "#react-quoter-landing-plugin input";
}
const resolveInstance = () => {
const instances = window.__rpaAutocompleteInstances ?? [];
if (instances.length > 0) {
result.autocompleteAccessPath = "window.__rpaAutocompleteInstances";
return instances[instances.length - 1] ?? null;
}
if (result.globalAutocompleteVars.length > 0) {
result.autocompleteAccessPath = `window.${result.globalAutocompleteVars[0]}`;
return window[result.globalAutocompleteVars[0]!] as {
getPlace: () => unknown;
};
}
const pacInput =
document.querySelector(".pac-target-input") ||
document.querySelector("#react-quoter-landing-plugin input");
if (!pacInput) {
return null;
}
for (const key of Object.keys(pacInput)) {
if (!key.startsWith("gm_")) {
continue;
}
const val = (pacInput as Record<string, unknown>)[key];
if (
val &&
typeof val === "object" &&
typeof (val as { getPlace?: unknown }).getPlace === "function"
) {
result.autocompleteAccessPath = "input[gm_*].getPlace";
return val as { getPlace: () => unknown };
}
}
return null;
};
const instance = resolveInstance();
result.autocompleteInstanceFound = Boolean(instance);
const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
result.cspRestrictions = meta?.getAttribute("content") ?? "none";
if (
instance &&
result.triggerAvailable &&
typeof instance.getPlace === "function"
) {
try {
const originalGetPlace = instance.getPlace.bind(instance);
instance.getPlace = () => ({
place_id: "ChIJReconProbePlaceId0000000",
formatted_address: "1234 Recon Probe St, Los Angeles, CA",
name: "1234 Recon Probe St",
geometry: { location: { lat: () => 34.05, lng: () => -118.25 } },
});
google.maps.event.trigger(instance, "place_changed");
result.placeChangedTriggerOk = true;
setTimeout(() => {
instance.getPlace = originalGetPlace;
}, 50);
} catch {
result.placeChangedTriggerOk = false;
}
}
return result;
} catch (error) {
return {
googleMapsLoaded: false,
triggerAvailable: false,
placesLoaded: false,
globalAutocompleteVars: [],
inputProbes: [],
pacTargetInputSelector: "",
autocompleteAccessPath: "not-found",
autocompleteInstanceFound: false,
reactFiberKey: "not-found",
widgetComponentName: "unknown",
widgetStateStructure: {},
cspRestrictions: "none",
placeChangedTriggerOk: false,
probeError: String(error),
};
}
}
async function main(): Promise<void> {
process.env.RPA_HEADLESS = "false";
process.env.RPA_ADDRESS_MODE = "real";
process.env.RPA_MOCK_MODE = "false";
mkdirSync(OUT_DIR, { recursive: true });
const opened = await openQuotePage(quotePageAdapter, false, undefined, {
freshCargoContext: true,
});
const { page, context } = opened;
await context.addInitScript(AUTOCOMPLETE_MONKEY_PATCH_INIT);
await page.reload({ waitUntil: "domcontentloaded" });
const rpaCtx = RPAContext.fromSession(page, context);
await stabilizeQuoteLandingPage(rpaCtx);
await waitForQuoterWidgetHydrated(page);
await openAddressSide(rpaCtx, "pickup");
const street = await visibleStreetInput(rpaCtx, "pickup", {
deliverySectionOpened: false,
});
if (!street) {
throw new Error("recon: pickup street input not found");
}
const queryHint = {
street: "1234 Warehouse Street",
city: "Los Angeles",
state: "CA",
zip: "",
};
await ensureAddressSuggestions(page, street, queryHint, {
mothershipLabel: "1234 Warehouse Street, Los Angeles, CA",
});
await page.waitForTimeout(1500);
const probe = await page.evaluate(runReconProbeInBrowser);
const verdict = buildVerdict(probe);
const outPath = join(OUT_DIR, "verdict.json");
writeFileSync(outPath, JSON.stringify(verdict, null, 2), "utf8");
console.log("[recon] verdict:", verdict.verdict);
console.log("[recon] googleMapsLoaded:", verdict.googleMapsLoaded);
console.log("[recon] autocompleteAccessPath:", verdict.autocompleteAccessPath);
console.log("[recon] placeChangedTriggerOk:", verdict.placeChangedTriggerOk);
console.log("[recon] written:", outPath);
await closeBrowser();
}
main().catch(async (error) => {
console.error("[recon] failed:", error);
await closeBrowser().catch(() => undefined);
process.exit(1);
});