|
|
/**
|
|
|
* P0/P2 place commit 诊断探针
|
|
|
*
|
|
|
* PowerShell:
|
|
|
* $env:RPA_PLACE_DIAG="true"
|
|
|
* $env:RPA_HEADLESS="false"
|
|
|
* npx tsx scripts/probe-place-diagnostic.ts
|
|
|
*/
|
|
|
process.env.RPA_PLACE_DIAG = "true";
|
|
|
|
|
|
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 type { QuoteRequest } from "@/modules/providers/quote-provider";
|
|
|
import { getSelector } from "@/lib/rpa/selectors";
|
|
|
import { AxelSelectionBridge } from "@/workers/rpa/axel-selection-bridge";
|
|
|
import {
|
|
|
ensureAddressSuggestions,
|
|
|
} from "@/workers/rpa/address-suggestions";
|
|
|
import { openAddressSide, visibleStreetInput } from "@/workers/rpa/address-side";
|
|
|
import { commitPlaceWithStrategy } from "@/workers/rpa/fix-place";
|
|
|
import {
|
|
|
collectPlaceDiagSnapshot,
|
|
|
interpretPlaceDiagnostic,
|
|
|
setPlaceDiagMark,
|
|
|
} from "@/workers/rpa/fix-place/place-diagnostic-probe";
|
|
|
import {
|
|
|
P0_PLACE_DIAG_INIT,
|
|
|
P2_FETCH_HIJACK_INIT,
|
|
|
} from "@/workers/rpa/fix-place/place-diagnostic-init";
|
|
|
import { RPAContext } from "@/workers/rpa/kernel/context";
|
|
|
import {
|
|
|
stabilizeQuoteLandingPage,
|
|
|
waitForQuoterWidgetHydrated,
|
|
|
} from "@/workers/rpa/page-prep";
|
|
|
import { closeBrowser, openQuotePage } from "@/workers/rpa/session-manager";
|
|
|
import { quotePageAdapter } from "@/workers/rpa/quote-page-adapter";
|
|
|
|
|
|
loadDevEnv();
|
|
|
|
|
|
process.env.RPA_MOCK_MODE = "false";
|
|
|
process.env.RPA_ADDRESS_MODE = "real";
|
|
|
if (!process.env.RPA_HEADLESS) {
|
|
|
process.env.RPA_HEADLESS = "false";
|
|
|
}
|
|
|
|
|
|
const BASE = process.env.PROVE_BASE_URL ?? "http://localhost:3000";
|
|
|
const TOKEN = "demo-host-token";
|
|
|
const CUSTOMER_ID = "CUST_001";
|
|
|
const OUT_DIR = join(process.cwd(), ".rpa", "place-diagnostic");
|
|
|
|
|
|
const PAIRS = {
|
|
|
pickup: {
|
|
|
street: "Washington State Convention Center Main Garage, Pike Street",
|
|
|
city: "Seattle",
|
|
|
state: "WA",
|
|
|
},
|
|
|
delivery: {
|
|
|
street: "Ocean Drive",
|
|
|
city: "Miami Beach",
|
|
|
state: "FL",
|
|
|
},
|
|
|
} as const;
|
|
|
|
|
|
function draftAddress(side: keyof typeof PAIRS) {
|
|
|
const d = PAIRS[side];
|
|
|
return {
|
|
|
street: d.street,
|
|
|
city: d.city,
|
|
|
state: d.state,
|
|
|
zip: "",
|
|
|
place_id: `draft_${side}`,
|
|
|
formatted_address: `${d.street}, ${d.city}, ${d.state}`,
|
|
|
selected_from_suggestions: true,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
async function main(): Promise<void> {
|
|
|
const runId = randomUUID().slice(0, 8);
|
|
|
const side = (process.env.PROBE_PLACE_SIDE === "delivery" ? "delivery" : "pickup") as
|
|
|
| "pickup"
|
|
|
| "delivery";
|
|
|
|
|
|
mkdirSync(OUT_DIR, { recursive: true });
|
|
|
|
|
|
const candRes = await hostFetchMothershipCandidates(
|
|
|
BASE,
|
|
|
TOKEN,
|
|
|
CUSTOMER_ID,
|
|
|
draftAddress("pickup"),
|
|
|
draftAddress("delivery"),
|
|
|
);
|
|
|
if (candRes.code !== 0 || !candRes.data) {
|
|
|
throw new Error(candRes.message ?? "候选 API 失败");
|
|
|
}
|
|
|
|
|
|
const candidate =
|
|
|
side === "pickup"
|
|
|
? candRes.data.pickup_candidates[0]
|
|
|
: candRes.data.delivery_candidates[0];
|
|
|
if (!candidate) {
|
|
|
throw new Error(`${side} 无候选`);
|
|
|
}
|
|
|
|
|
|
const applied = applyMothershipCandidate(draftAddress(side), candidate);
|
|
|
const address: QuoteRequest["pickup"] = {
|
|
|
street: applied.street,
|
|
|
city: applied.city,
|
|
|
state: applied.state,
|
|
|
zip: applied.zip ?? "",
|
|
|
placeId: candidate.option_id,
|
|
|
formattedAddress: applied.formatted_address,
|
|
|
selectedFromSuggestions: true,
|
|
|
mothershipOptionId: candidate.option_id,
|
|
|
mothershipDisplayLabel: candidate.display_label,
|
|
|
selectedFromMothership: true,
|
|
|
};
|
|
|
|
|
|
const queryHint = {
|
|
|
street: address.street,
|
|
|
city: address.city ?? "",
|
|
|
state: address.state ?? "",
|
|
|
zip: address.zip ?? "",
|
|
|
};
|
|
|
|
|
|
console.log(`\n=== place-diagnostic probe run=${runId} side=${side} ===`);
|
|
|
console.log(`label: ${address.mothershipDisplayLabel}`);
|
|
|
console.log(`placeId: ${candidate.option_id?.slice(0, 32)}…\n`);
|
|
|
|
|
|
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" });
|
|
|
const ctx = RPAContext.fromSession(opened.page, opened.context);
|
|
|
await stabilizeQuoteLandingPage(ctx);
|
|
|
await waitForQuoterWidgetHydrated(ctx.page, 60_000);
|
|
|
|
|
|
await setPlaceDiagMark(ctx.page, `${runId}-page-ready`);
|
|
|
let baseline = await collectPlaceDiagSnapshot(ctx.page);
|
|
|
if (!baseline?.ok) {
|
|
|
console.warn("[probe] baseline 快照失败,P0 可能未就绪", baseline);
|
|
|
baseline = baseline ?? { ok: false, reason: "snapshot-undefined" };
|
|
|
}
|
|
|
writeFileSync(
|
|
|
join(OUT_DIR, `${runId}-baseline.json`),
|
|
|
JSON.stringify(baseline, null, 2),
|
|
|
"utf8",
|
|
|
);
|
|
|
for (const line of interpretPlaceDiagnostic(baseline, "baseline")) {
|
|
|
console.log(line);
|
|
|
}
|
|
|
|
|
|
await openAddressSide(ctx, side);
|
|
|
const street = await visibleStreetInput(ctx, side, {
|
|
|
deliverySectionOpened: side === "delivery",
|
|
|
});
|
|
|
if (!street) {
|
|
|
throw new Error("地址输入框不可见");
|
|
|
}
|
|
|
|
|
|
const bridge = new AxelSelectionBridge();
|
|
|
bridge.attach(ctx.page);
|
|
|
bridge.clearCandidates();
|
|
|
|
|
|
await ensureAddressSuggestions(ctx.page, street, queryHint, {
|
|
|
mothershipLabel: address.mothershipDisplayLabel!,
|
|
|
preferConfirmedLabel: true,
|
|
|
});
|
|
|
await bridge.waitForSearchResults(12_000);
|
|
|
|
|
|
const binding = bridge.resolveSelection(address.mothershipDisplayLabel!, queryHint);
|
|
|
if (!binding?.placeId) {
|
|
|
throw new Error("search 未绑定 placeId");
|
|
|
}
|
|
|
|
|
|
await setPlaceDiagMark(ctx.page, `${runId}-before-fix-place`);
|
|
|
const fixResult = await commitPlaceWithStrategy(
|
|
|
ctx.page,
|
|
|
bridge,
|
|
|
binding.placeId,
|
|
|
binding.description,
|
|
|
side,
|
|
|
undefined,
|
|
|
{ input: street, query: queryHint },
|
|
|
);
|
|
|
|
|
|
const finalSnap = await collectPlaceDiagSnapshot(ctx.page);
|
|
|
const report = {
|
|
|
runId,
|
|
|
side,
|
|
|
address: {
|
|
|
label: address.mothershipDisplayLabel,
|
|
|
placeId: binding.placeId,
|
|
|
},
|
|
|
fixResult,
|
|
|
bridge: {
|
|
|
placeNetworkLog: bridge.getPlaceNetworkLog(),
|
|
|
hasPlaceCommit: bridge.hasPlaceCommit(binding.placeId),
|
|
|
httpOk: bridge.hasSuccessfulPlaceNetworkRequest(binding.placeId),
|
|
|
},
|
|
|
interpretation: interpretPlaceDiagnostic(finalSnap, "after-fix-place"),
|
|
|
snapshot: finalSnap,
|
|
|
finishedAt: new Date().toISOString(),
|
|
|
};
|
|
|
|
|
|
const reportPath = join(OUT_DIR, `${runId}-report.json`);
|
|
|
writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
|
|
|
|
|
|
console.log("\n=== fix-place 结果 ===");
|
|
|
console.log(JSON.stringify(fixResult, null, 2));
|
|
|
console.log("\n=== P0/P2 结论 ===");
|
|
|
for (const line of report.interpretation) {
|
|
|
console.log(line);
|
|
|
}
|
|
|
console.log(`\n报告:${reportPath}`);
|
|
|
|
|
|
await opened.page.close().catch(() => undefined);
|
|
|
await opened.context.close().catch(() => undefined);
|
|
|
await closeBrowser();
|
|
|
}
|
|
|
|
|
|
main().catch((error) => {
|
|
|
console.error(error);
|
|
|
process.exit(1);
|
|
|
});
|