|
|
/**
|
|
|
* 自动化审计:Convention Center pickup → place 网络 + DOM 结论
|
|
|
* 输出:.rpa/pickup-commit-audit.json
|
|
|
*/
|
|
|
process.env.RPA_PLACE_DIAG = "true";
|
|
|
process.env.RPA_HEADLESS = "true";
|
|
|
process.env.RPA_MOCK_MODE = "false";
|
|
|
process.env.RPA_ADDRESS_MODE = "real";
|
|
|
|
|
|
import { randomUUID } from "node:crypto";
|
|
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
import { join } from "node:path";
|
|
|
import { loadDevEnv } from "@/lib/dev-env";
|
|
|
import { hostFetchMothershipCandidates } from "@/lib/frontend/api-client";
|
|
|
import { applyMothershipCandidate } from "@/lib/frontend/mothership-address";
|
|
|
import { MotherShipAddressAdapter } from "@/workers/rpa/address-adapter/mothership-address-adapter";
|
|
|
import { openAddressSide, visibleStreetInput } from "@/workers/rpa/address-side";
|
|
|
import {
|
|
|
SCAN_INPUT_GM_AND_DOM,
|
|
|
SCAN_SUGGESTION_ROWS,
|
|
|
} from "@/workers/rpa/fix-place/axel-anatomy-scripts";
|
|
|
import {
|
|
|
P0_PLACE_DIAG_INIT,
|
|
|
P2_FETCH_HIJACK_INIT,
|
|
|
} from "@/workers/rpa/fix-place/place-diagnostic-init";
|
|
|
import { collectPlaceDiagSnapshot } from "@/workers/rpa/fix-place/place-diagnostic-probe";
|
|
|
import { RPAContext } from "@/workers/rpa/kernel/context";
|
|
|
import { getSelector } from "@/lib/rpa/selectors";
|
|
|
import {
|
|
|
stabilizeQuoteLandingPage,
|
|
|
waitForQuoterWidgetHydrated,
|
|
|
} from "@/workers/rpa/page-prep";
|
|
|
import { closeBrowser, openQuotePage } from "@/workers/rpa/session-manager";
|
|
|
import { quotePageAdapter } from "@/workers/rpa/quote-page-adapter";
|
|
|
import type { QuoteRequest } from "@/modules/providers/quote-provider";
|
|
|
|
|
|
loadDevEnv();
|
|
|
|
|
|
const OUT = join(process.cwd(), ".rpa", "pickup-commit-audit.json");
|
|
|
const BASE = process.env.PROVE_BASE_URL ?? "http://localhost:3000";
|
|
|
|
|
|
async function evalScan<T>(page: import("playwright").Page, fn: string): Promise<T> {
|
|
|
return page.evaluate((body) => {
|
|
|
const runner = eval(`(${body})`) as () => T;
|
|
|
return runner();
|
|
|
}, fn);
|
|
|
}
|
|
|
|
|
|
async function main(): Promise<void> {
|
|
|
const runId = randomUUID().slice(0, 8);
|
|
|
const pickupDraft = {
|
|
|
street: "Washington State Convention Center Main Garage, Pike Street",
|
|
|
city: "Seattle",
|
|
|
state: "WA",
|
|
|
zip: "",
|
|
|
place_id: "draft_pickup",
|
|
|
formatted_address: "Washington State Convention Center Main Garage, Pike Street, Seattle, WA",
|
|
|
selected_from_suggestions: true,
|
|
|
};
|
|
|
const deliveryDraft = {
|
|
|
street: "Ocean Drive",
|
|
|
city: "Miami Beach",
|
|
|
state: "FL",
|
|
|
zip: "",
|
|
|
place_id: "draft_delivery",
|
|
|
formatted_address: "Ocean Drive, Miami Beach, FL",
|
|
|
selected_from_suggestions: true,
|
|
|
};
|
|
|
|
|
|
const cand = await hostFetchMothershipCandidates(
|
|
|
BASE,
|
|
|
"demo-host-token",
|
|
|
"CUST_001",
|
|
|
pickupDraft,
|
|
|
deliveryDraft,
|
|
|
);
|
|
|
if (cand.code !== 0 || !cand.data?.pickup_candidates[0]) {
|
|
|
throw new Error(cand.message ?? "候选失败");
|
|
|
}
|
|
|
const pc = cand.data.pickup_candidates[0];
|
|
|
const applied = applyMothershipCandidate(pickupDraft, pc);
|
|
|
const pickup: QuoteRequest["pickup"] = {
|
|
|
street: applied.street,
|
|
|
city: applied.city,
|
|
|
state: applied.state,
|
|
|
zip: applied.zip ?? "",
|
|
|
placeId: pc.option_id,
|
|
|
formattedAddress: applied.formatted_address,
|
|
|
selectedFromSuggestions: true,
|
|
|
mothershipOptionId: pc.option_id,
|
|
|
mothershipDisplayLabel: pc.display_label,
|
|
|
selectedFromMothership: true,
|
|
|
};
|
|
|
|
|
|
const net: Array<{ url: string; status?: number; method: string }> = [];
|
|
|
const opened = await openQuotePage(quotePageAdapter, false, undefined, {
|
|
|
freshCargoContext: true,
|
|
|
});
|
|
|
await opened.context.addInitScript({ content: P0_PLACE_DIAG_INIT });
|
|
|
await opened.context.addInitScript({ content: P2_FETCH_HIJACK_INIT });
|
|
|
await opened.page.reload({ waitUntil: "domcontentloaded" });
|
|
|
|
|
|
opened.page.on("response", (res) => {
|
|
|
const url = res.url();
|
|
|
if (url.includes("axel/location/")) {
|
|
|
net.push({
|
|
|
url: url.slice(0, 260),
|
|
|
status: res.status(),
|
|
|
method: res.request().method(),
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
|
|
|
const ctx = RPAContext.fromSession(opened.page, opened.context);
|
|
|
await stabilizeQuoteLandingPage(ctx);
|
|
|
await waitForQuoterWidgetHydrated(ctx.page, 90_000);
|
|
|
await openAddressSide(ctx, "pickup");
|
|
|
const street = await visibleStreetInput(ctx, "pickup");
|
|
|
if (!street) throw new Error("无输入框");
|
|
|
|
|
|
const adapter = new MotherShipAddressAdapter();
|
|
|
let adapterError: string | null = null;
|
|
|
let result: Awaited<ReturnType<MotherShipAddressAdapter["selectAddress"]>> | null =
|
|
|
null;
|
|
|
try {
|
|
|
result = await adapter.selectAddress({
|
|
|
ctx,
|
|
|
side: "pickup",
|
|
|
address: pickup,
|
|
|
street,
|
|
|
widgetSelector: getSelector("RPA_SELECTOR_QUOTE_WIDGET"),
|
|
|
});
|
|
|
} catch (e) {
|
|
|
adapterError = e instanceof Error ? e.message : String(e);
|
|
|
}
|
|
|
|
|
|
const dom = await evalScan<Record<string, unknown>>(
|
|
|
ctx.page,
|
|
|
SCAN_INPUT_GM_AND_DOM,
|
|
|
);
|
|
|
const suggestions = await evalScan<Record<string, unknown>>(
|
|
|
ctx.page,
|
|
|
SCAN_SUGGESTION_ROWS,
|
|
|
);
|
|
|
const p0 = await collectPlaceDiagSnapshot(ctx.page);
|
|
|
|
|
|
const placeCommits = net.filter(
|
|
|
(n) =>
|
|
|
n.url.includes("/axel/location/place/") &&
|
|
|
!n.url.includes("reverse-geocode") &&
|
|
|
n.status === 200,
|
|
|
);
|
|
|
|
|
|
const report = {
|
|
|
runId,
|
|
|
label: pickup.mothershipDisplayLabel,
|
|
|
placeId: pc.option_id,
|
|
|
verdict: {
|
|
|
componentType:
|
|
|
(dom as { inputs?: Array<{ gmKeys?: string[] }> }).inputs?.some(
|
|
|
(i) => (i.gmKeys?.length ?? 0) > 0,
|
|
|
)
|
|
|
? "google-pac-hybrid"
|
|
|
: "axel-styled-search",
|
|
|
googleMapsOnWindow: Boolean(
|
|
|
await ctx.page.evaluate(() => typeof google !== "undefined"),
|
|
|
),
|
|
|
adapterCommitted: result?.committed ?? false,
|
|
|
adapterError,
|
|
|
commitState: adapter.getCommitState(),
|
|
|
diagnostics: adapter.getDiagnostics(),
|
|
|
placeCommitHttp200Count: placeCommits.length,
|
|
|
placeCommitUrls: placeCommits.map((p) => p.url),
|
|
|
p0Listeners: p0.listeners?.total ?? 0,
|
|
|
p0HandlerEvents: (p0.handlerEvents ?? []).length,
|
|
|
p2FetchPlaceCount: (p0.fetchLog ?? []).filter((f) =>
|
|
|
String((f as { url?: string }).url ?? "").includes("/place/"),
|
|
|
).length,
|
|
|
},
|
|
|
dom,
|
|
|
suggestions,
|
|
|
network: net,
|
|
|
p0,
|
|
|
finishedAt: new Date().toISOString(),
|
|
|
};
|
|
|
|
|
|
mkdirSync(join(process.cwd(), ".rpa"), { recursive: true });
|
|
|
writeFileSync(OUT, JSON.stringify(report, null, 2), "utf8");
|
|
|
console.log(JSON.stringify(report.verdict, null, 2));
|
|
|
console.log(`\n完整报告: ${OUT}`);
|
|
|
|
|
|
await opened.page.close().catch(() => undefined);
|
|
|
await opened.context.close().catch(() => undefined);
|
|
|
await closeBrowser();
|
|
|
}
|
|
|
|
|
|
main().catch((e) => {
|
|
|
console.error(e);
|
|
|
process.exit(1);
|
|
|
});
|