import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; /** 逆向工程辅助:Google Places Autocomplete 探测(headed 调试用) */ const DEFAULT_VERDICT: ReconVerdict = { googleMapsLoaded: false, triggerAvailable: false, autocompleteAccessPath: "not-found", pacTargetInputSelector: "", reactFiberKey: "not-found", widgetComponentName: "unknown", widgetStateStructure: {}, cspRestrictions: "none", placeChangedTriggerOk: false, verdict: "PLAN_B", }; /** 读取 headed 逆向产物;缺失时默认 PLAN_B(自绘联想路径) */ export function loadReconVerdict(): ReconVerdict { const path = join(process.cwd(), ".rpa", "recon", "verdict.json"); if (!existsSync(path)) { return DEFAULT_VERDICT; } try { return { ...DEFAULT_VERDICT, ...JSON.parse(readFileSync(path, "utf8")) }; } catch { return DEFAULT_VERDICT; } } export const AUTOCOMPLETE_MONKEY_PATCH_INIT = `(() => { if (window.__rpaAutocompletePatched) return; window.__rpaAutocompletePatched = true; window.__rpaAutocompleteInstances = window.__rpaAutocompleteInstances || []; window.__rpaPlaceChangedHandlers = window.__rpaPlaceChangedHandlers || []; const waitForGoogle = () => { if (typeof google === "undefined" || !google.maps || !google.maps.places || !google.maps.places.Autocomplete) { setTimeout(waitForGoogle, 200); return; } const orig = google.maps.places.Autocomplete; google.maps.places.Autocomplete = function (...args) { const inputEl = args[0] || null; const instance = new orig(...args); window.__rpaAutocompleteInstances.push(instance); if (inputEl) { instance.__rpaInputEl = inputEl; } if (typeof instance.addListener === "function") { const origAddListener = instance.addListener.bind(instance); instance.addListener = function(eventName, handler) { if (eventName === "place_changed" && typeof handler === "function") { window.__rpaPlaceChangedHandlers.push({ instance: instance, handler: handler, inputEl: inputEl, }); } return origAddListener(eventName, handler); }; } return instance; }; google.maps.places.Autocomplete.prototype = orig.prototype; }; waitForGoogle(); })();`; export type ReconVerdict = { googleMapsLoaded: boolean; triggerAvailable: boolean; autocompleteAccessPath: string; pacTargetInputSelector: string; reactFiberKey: string; widgetComponentName: string; widgetStateStructure: Record; cspRestrictions: string; placeChangedTriggerOk: boolean; verdict: "PLAN_A" | "PLAN_B"; }; export const RUN_RECON_PROBE = `() => { 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: [], inputProbes: [], pacTargetInputSelector: "", autocompleteAccessPath: "not-found", autocompleteInstanceFound: false, reactFiberKey: "not-found", widgetComponentName: "unknown", widgetStateStructure: {}, cspRestrictions: "none", placeChangedTriggerOk: false, }; if (result.googleMapsLoaded) { try { result.globalAutocompleteVars = Object.keys(window).filter((k) => { try { return window[k] instanceof google.maps.places.Autocomplete; } catch { return false; } }); } catch { result.globalAutocompleteVars = []; } } 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[fiberKey]; const components = []; while (fiber) { if (fiber.stateNode && typeof fiber.stateNode.setState === "function") { const state = fiber.stateNode.state || {}; const stateKeys = Object.keys(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 state[k]]), ); } components.push({ name: fiber.type?.name || fiber.type?.displayName || "unknown", stateKeys, }); } fiber = fiber.return; } } } 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.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]; } if (result.globalAutocompleteVars.length > 0) { result.autocompleteAccessPath = "window." + result.globalAutocompleteVars[0]; return window[result.globalAutocompleteVars[0]]; } const pacInput = document.querySelector(".pac-target-input") || document.querySelector("#react-quoter-landing-plugin input"); if (pacInput) { for (const key of Object.keys(pacInput)) { if (!key.startsWith("gm_")) continue; const val = pacInput[key]; if (val && typeof val.getPlace === "function") { result.autocompleteAccessPath = "input[gm_*].getPlace"; return val; } } } return null; }; const instance = resolveInstance(); result.autocompleteInstanceFound = Boolean(instance); const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]'); result.cspRestrictions = meta?.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), }; } }`; export function buildVerdict( probe: { googleMapsLoaded: boolean; triggerAvailable: boolean; placesLoaded: boolean; autocompleteAccessPath: string; pacTargetInputSelector: string; reactFiberKey: string; widgetComponentName: string; widgetStateStructure: Record; cspRestrictions: string; autocompleteInstanceFound: boolean; placeChangedTriggerOk: boolean; }, ): ReconVerdict { const r2Pass = probe.googleMapsLoaded && probe.triggerAvailable && probe.placesLoaded; const r3Pass = probe.autocompleteInstanceFound; const r4Pass = probe.placeChangedTriggerOk; let verdict: "PLAN_A" | "PLAN_B" = "PLAN_A"; if (!r2Pass || !r3Pass || !r4Pass) { verdict = "PLAN_B"; } return { googleMapsLoaded: probe.googleMapsLoaded, triggerAvailable: probe.triggerAvailable, autocompleteAccessPath: probe.autocompleteAccessPath, pacTargetInputSelector: probe.pacTargetInputSelector, reactFiberKey: probe.reactFiberKey, widgetComponentName: probe.widgetComponentName, widgetStateStructure: probe.widgetStateStructure, cspRestrictions: probe.cspRestrictions, placeChangedTriggerOk: probe.placeChangedTriggerOk, verdict, }; }