You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
chajia/scripts/demo-screenshot-case-a.ts

271 lines
8.0 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 与 MotherShip 人工页同参验收Case A 截图)
* 1234 Warehouse St LA → 5678 Distribution Way Farmers Branch TX
* 2托 500lb 40×48×48 — 禁止引用截图价格,仅跑真实 RPA
*/
import { randomUUID } from "node:crypto";
import { loadDevEnv } from "@/lib/dev-env";
import { getRpaAddressMode } from "@/lib/rpa/address-mode";
import { ensureNativeInfraIfNeeded } from "@/lib/infra/ensure-native-infra";
import {
hostCreateQuote,
hostFetchMothershipCandidates,
hostConfirmMothershipAddresses,
hostGetQuote,
hostPreheatQuoteSession,
} from "@/lib/frontend/api-client";
import { applyMothershipCandidate } from "@/lib/frontend/mothership-address";
import type { QuoteRequestBody } from "@/lib/frontend/types";
import { pollQuoteUntilDone } from "@/hooks/use-quote-polling";
import { isAnyRpaWorkerHealthy } from "@/lib/rpa/worker-health";
import { prisma } from "@/lib/prisma";
import { ensureRedisReady, resetRedisClient } from "@/lib/redis";
loadDevEnv();
const BASE = process.env.PROVE_BASE_URL ?? "http://localhost:3000";
const TOKEN = "demo-host-token";
const CUSTOMER_ID = "CUST_001";
const PICKUP = {
street: "1234 Warehouse Street",
city: "Los Angeles",
state: "CA",
} as const;
const DELIVERY = {
street: "5678 Distribution Way",
city: "Farmers Branch",
state: "TX",
} as const;
const CARGO = {
pallet_count: 2,
weight_lb: 500,
width_in: 40,
length_in: 48,
height_in: 48,
} as const;
function draftAddress(street: string, city: string, state: string) {
return {
street,
city,
state,
zip: "",
place_id: `draft_${street}_${city}_${state}`.replace(/\s+/g, "_").slice(0, 64),
formatted_address: `${street}, ${city}, ${state}`,
selected_from_suggestions: true,
};
}
function pickCandidate<
T extends { street: string; display_label: string },
>(candidates: T[], draft: { street: string }): T {
const num = draft.street.match(/^\d+/)?.[0];
if (num) {
const matched = candidates.find((c) => {
const blob = `${c.street} ${c.display_label}`.replace(/\s+/g, " ");
return blob.includes(num);
});
if (matched) return matched;
}
const first = candidates[0];
if (!first) throw new Error("联想无候选");
return first;
}
const EXPECTED_TIERS: Record<string, { price: number; days: string }> = {
"standard/lowest": { price: 287.23, days: "4" },
"standard/bestValue": { price: 345.0, days: "3" },
"standard/fastest": { price: 369.72, days: "2" },
"guaranteed/lowest": { price: 457.36, days: "3" },
"guaranteed/fastest": { price: 480.37, days: "2" },
};
function assertAlignedWithScreenshot(
tiers: Array<{
service_level: string;
rate_option: string;
final_total: number;
transit_days?: string;
}>,
): void {
const byKey = new Map(
tiers.map((t) => [`${t.service_level}/${t.rate_option}`, t]),
);
const mismatches: string[] = [];
for (const [key, exp] of Object.entries(EXPECTED_TIERS)) {
const got = byKey.get(key);
if (!got) {
mismatches.push(`${key}: 缺失`);
continue;
}
if (Math.abs(got.final_total - exp.price) > 0.02) {
mismatches.push(`${key}: 期望 $${exp.price} 实际 $${got.final_total}`);
}
const dayStr = got.transit_days ?? "";
if (!dayStr.includes(exp.days)) {
mismatches.push(`${key}: 期望 ${exp.days} 天 实际 ${dayStr}`);
}
}
if (mismatches.length > 0) {
throw new Error(`与截图未对齐:\n${mismatches.join("\n")}`);
}
console.log("\n[PASS] 五档价格与截图对齐");
}
async function main() {
if (getRpaAddressMode() !== "real") {
throw new Error("须 RPA_ADDRESS_MODE=real");
}
ensureNativeInfraIfNeeded();
resetRedisClient();
await ensureRedisReady(12, 1_000);
await prisma.$queryRaw`SELECT 1`;
const res = await fetch(BASE);
if (!res.ok && res.status !== 304) {
throw new Error("Next 未响应,请先 npm run dev:start");
}
if (!(await isAnyRpaWorkerHealthy())) {
throw new Error("RPA Worker 无心跳");
}
console.log("=== 截图同参真实询价500lb / 5678 Distribution Way===\n");
const candRes = await hostFetchMothershipCandidates(
BASE,
TOKEN,
CUSTOMER_ID,
{ ...PICKUP, zip: "" },
{ ...DELIVERY, zip: "" },
);
if (candRes.code !== 0 || !candRes.data) {
throw new Error(candRes.message ?? "联想失败");
}
const pickup = pickCandidate(candRes.data.pickup_candidates, PICKUP);
const delivery = pickCandidate(candRes.data.delivery_candidates, DELIVERY);
console.log("联想 pickup:", pickup.display_label);
console.log("联想 delivery:", delivery.display_label);
console.log(
"delivery 候选:",
candRes.data.delivery_candidates.map((c) => c.display_label).join(" | "),
);
if (candRes.data.quote_session_id) {
const sessionId = candRes.data.quote_session_id;
await hostPreheatQuoteSession(BASE, TOKEN, CUSTOMER_ID, sessionId);
const confirmRes = await hostConfirmMothershipAddresses(
BASE,
TOKEN,
CUSTOMER_ID,
sessionId,
pickup,
delivery,
);
if (confirmRes.code !== 0) {
console.warn(
"[warn] 地址确认未成功,询价将重跑 fillAddress:",
confirmRes.message,
);
} else {
console.log("地址已确认并驻留 session:", sessionId.slice(0, 8));
}
}
const quoteSessionId = candRes.data.quote_session_id;
const body: QuoteRequestBody = {
request_id: randomUUID(),
customer_id: CUSTOMER_ID,
quote_session_id: quoteSessionId,
pickup_address: applyMothershipCandidate(draftAddress(PICKUP.street, PICKUP.city, PICKUP.state), pickup),
delivery_address: applyMothershipCandidate(
draftAddress(DELIVERY.street, DELIVERY.city, DELIVERY.state),
delivery,
),
weight: { value: CARGO.weight_lb, unit: "lb" },
dimensions: {
length: CARGO.length_in,
width: CARGO.width_in,
height: CARGO.height_in,
unit: "in",
},
pallet_count: CARGO.pallet_count,
cargo_type: "general_freight",
};
let created = await hostCreateQuote(BASE, TOKEN, body);
for (let i = 0; i < 5; i++) {
if (
created.code === 0 &&
created.data?.status === "done" &&
created.data.source_type === "cache"
) {
body.request_id = randomUUID();
body.weight = { value: CARGO.weight_lb + i + 1, unit: "lb" };
created = await hostCreateQuote(BASE, TOKEN, body);
continue;
}
break;
}
if (created.code !== 0 || !created.data?.quote_id) {
throw new Error(created.message ?? "创建询价失败");
}
const quoteId = created.data.quote_id;
let detail;
if (created.data.status === "done") {
detail = (await hostGetQuote(BASE, TOKEN, CUSTOMER_ID, quoteId)).data;
} else {
const poll = await pollQuoteUntilDone(
async () => {
const got = await hostGetQuote(BASE, TOKEN, CUSTOMER_ID, quoteId);
if (got.code !== 0) return { ok: false, errorMessage: got.message };
return { ok: true, data: got.data };
},
{ timeoutMs: 480_000 },
);
if (poll.type !== "done") {
throw new Error(poll.type === "timeout" ? "轮询超时" : poll.message);
}
detail = poll.quote;
}
const out = {
quoteId,
input: {
pickup: `${PICKUP.street}, ${PICKUP.city}, ${PICKUP.state}`,
delivery: `${DELIVERY.street}, ${DELIVERY.city}, ${DELIVERY.state}`,
confirmed_delivery: delivery.display_label,
pallets: CARGO.pallet_count,
weight_lb: body.weight.value,
dims_in: `${CARGO.width_in}W×${CARGO.length_in}L×${CARGO.height_in}H`,
},
result: {
status: detail?.status,
source_type: detail?.source_type,
tiers: detail?.quotes?.map((q) => ({
tier: `${q.service_level}/${q.rate_option}`,
final_total_usd: q.final_total,
transit_days: q.transit_days,
})),
},
};
console.log("\n" + JSON.stringify(out, null, 2));
if (detail?.quotes?.length) {
assertAlignedWithScreenshot(detail.quotes);
}
}
main()
.catch((e) => {
console.error("[FAIL]", e);
process.exit(1);
})
.finally(() => prisma.$disconnect());