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.

175 lines
5.6 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.

/**
* 西雅图 → 迈阿密 截图对齐验收4 托 723lb 40×48×45
* 用法RPA_ADDRESS_MODE=real npm run demo:seattle-miami
*/
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,
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: "3131 Western Ave", city: "Seattle", state: "WA" } as const;
const DELIVERY = { street: "7000 NW 52nd St", city: "Miami", state: "FL" } as const;
const CARGO = {
pallet_count: 4,
weight_lb: 723,
width_in: 40,
length_in: 48,
height_in: 45,
} as const;
/** 参考截图 standard 档最低价与最优性价比(允许 ±$0.02 */
const EXPECTED_STANDARD: Record<string, number> = {
"standard/lowest": 1338.66,
"standard/bestValue": 1365.86,
};
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;
}
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`;
if (!(await isAnyRpaWorkerHealthy())) {
throw new Error("RPA Worker 无心跳");
}
const candRes = await hostFetchMothershipCandidates(
BASE,
TOKEN,
CUSTOMER_ID,
{ ...PICKUP, zip: "" },
{ ...DELIVERY, zip: "" },
);
if (candRes.code !== 0 || !candRes.data) {
throw new Error(candRes.message ?? "MotherShip 联想失败");
}
const pickup = pickCandidate(candRes.data.pickup_candidates, PICKUP);
const delivery = pickCandidate(candRes.data.delivery_candidates, DELIVERY);
if (candRes.data.quote_session_id) {
await hostPreheatQuoteSession(BASE, TOKEN, CUSTOMER_ID, candRes.data.quote_session_id);
}
const body: QuoteRequestBody = {
request_id: randomUUID(),
customer_id: CUSTOMER_ID,
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",
};
const created = await hostCreateQuote(BASE, TOKEN, body);
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 tiers = detail?.quotes ?? [];
console.log("\n=== 西雅图 → 迈阿密 报价 ===");
for (const q of tiers) {
console.log(
` ${q.service_level}/${q.rate_option}: $${q.final_total?.toFixed(2)} (${q.transit_days ?? "—"})`,
);
}
const mismatches: string[] = [];
for (const [key, expected] of Object.entries(EXPECTED_STANDARD)) {
const [service, rate] = key.split("/");
const got = tiers.find((t) => t.service_level === service && t.rate_option === rate);
if (!got) {
mismatches.push(`${key}: 缺失`);
continue;
}
if (Math.abs((got.final_total ?? 0) - expected) > 0.02) {
mismatches.push(`${key}: 期望 $${expected} 实际 $${got.final_total}`);
}
}
if (mismatches.length > 0) {
throw new Error(`与参考截图未对齐:\n${mismatches.join("\n")}`);
}
console.log("\n[PASS] standard/lowest 与 standard/bestValue 与参考截图对齐");
}
main()
.catch((e) => {
console.error("[FAIL]", e);
process.exit(1);
})
.finally(() => prisma.$disconnect());