/** * 逆向工程 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, 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)[fiberKey] as { stateNode?: { state?: Record }; 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)[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 { 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); });