|
|
/**
|
|
|
* 逆向工程 R1-R7:Google 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);
|
|
|
});
|