|
|
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<string, unknown>;
|
|
|
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<string, unknown>;
|
|
|
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,
|
|
|
};
|
|
|
}
|