/** * 与 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 = { "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());