/** * 方案 A 随机地址报价探针 * * 成功标准:询价 status=done 且 tiers>=1(真实 axel/quote) * * PowerShell: * npm run probe:random-address-quote * $env:PROBE_RANDOM_LOOPS="5"; npm run probe:random-address-quote */ import { randomUUID } from "node:crypto"; import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { loadDevEnv } from "@/lib/dev-env"; import { ensureNativeInfraIfNeeded } from "@/lib/infra/ensure-native-infra"; import { hostCreateQuote, hostFetchMothershipCandidates, hostGetQuote, } from "@/lib/frontend/api-client"; import { applyMothershipCandidate } from "@/lib/frontend/mothership-address"; import type { MothershipAddressCandidate, QuoteRequestBody } from "@/lib/frontend/types"; import { pollQuoteUntilDone } from "@/hooks/use-quote-polling"; import { pickBestMothershipCandidate } from "./lib/pick-mothership-candidate"; import { isAnyRpaWorkerHealthy } from "@/lib/rpa/worker-health"; import { ensureRedisReady, resetRedisClient } from "@/lib/redis"; process.env.RPA_MOCK_MODE = "false"; process.env.RPA_ADDRESS_MODE = "real"; process.env.RPA_DUAL_ADDRESS_PATCH = "true"; loadDevEnv(); const BASE = process.env.PROBE_BASE_URL ?? "http://localhost:3000"; const TOKEN = "demo-host-token"; const CUSTOMER_ID = "CUST_001"; const LOOPS = Math.max(1, Number(process.env.PROBE_RANDOM_LOOPS ?? 3)); const POLL_TIMEOUT_MS = Number( process.env.PROBE_RANDOM_POLL_MS ?? process.env.QUOTE_TIMEOUT_MS ?? 420_000, ); type AddressDraft = { street: string; city: string; state: string; zip: string; }; type AddressPair = { id: string; label: string; pickup: Omit; delivery: Omit; cargo: { pallet_count: number; weight_lb: number; length_in: number; width_in: number; height_in: number; cargo_type: QuoteRequestBody["cargo_type"]; }; }; const ADDRESS_POOL: AddressPair[] = [ { id: "01", label: "洛杉矶 CA → Farmers Branch TX", pickup: { street: "1234 Warehouse Street", city: "Los Angeles", state: "CA" }, delivery: { street: "5678 Distribution Way", city: "Farmers Branch", state: "TX" }, cargo: { pallet_count: 2, weight_lb: 500, length_in: 48, width_in: 40, height_in: 48, cargo_type: "general_freight", }, }, { id: "02", label: "西雅图 WA → 迈阿密 FL", pickup: { street: "3131 Western Ave", city: "Seattle", state: "WA" }, delivery: { street: "7000 NW 52nd St", city: "Miami", state: "FL" }, cargo: { pallet_count: 3, weight_lb: 550, length_in: 48, width_in: 40, height_in: 45, cargo_type: "general_freight", }, }, { id: "06", label: "丹佛 CO → 夏洛特 NC", pickup: { street: "4800 Race St", city: "Denver", state: "CO" }, delivery: { street: "9300 South Blvd", city: "Charlotte", state: "NC" }, cargo: { pallet_count: 3, weight_lb: 450, length_in: 48, width_in: 40, height_in: 44, cargo_type: "general_freight", }, }, { id: "10", label: "西雅图 Convention Center → 迈阿密 Beach", pickup: { street: "Washington State Convention Center Main Garage, Pike Street", city: "Seattle", state: "WA", }, delivery: { street: "Ocean Drive", city: "Miami Beach", state: "FL" }, cargo: { pallet_count: 2, weight_lb: 500, length_in: 48, width_in: 40, height_in: 48, cargo_type: "general_freight", }, }, { id: "99", label: "Key West FL → Los Angeles CA", pickup: { street: "Duval Street", city: "Key West", state: "FL" }, delivery: { street: "Hollywood Boulevard", city: "Los Angeles", state: "CA" }, cargo: { pallet_count: 2, weight_lb: 503, length_in: 48, width_in: 40, height_in: 48, cargo_type: "general_freight", }, }, { id: "03", label: "芝加哥 IL → 亚特兰大 GA", pickup: { street: "233 S Wacker Dr", city: "Chicago", state: "IL" }, delivery: { street: "285 Andrew Young Intl Blvd", city: "Atlanta", state: "GA" }, cargo: { pallet_count: 2, weight_lb: 480, length_in: 48, width_in: 40, height_in: 48, cargo_type: "general_freight", }, }, { id: "04", label: "达拉斯 TX → 凤凰城 AZ", pickup: { street: "1500 Marilla St", city: "Dallas", state: "TX" }, delivery: { street: "200 W Washington St", city: "Phoenix", state: "AZ" }, cargo: { pallet_count: 2, weight_lb: 520, length_in: 48, width_in: 40, height_in: 48, cargo_type: "general_freight", }, }, { id: "05", label: "波士顿 MA → 纳什维尔 TN", pickup: { street: "1 Congress St", city: "Boston", state: "MA" }, delivery: { street: "501 Broadway", city: "Nashville", state: "TN" }, cargo: { pallet_count: 2, weight_lb: 490, length_in: 48, width_in: 40, height_in: 48, cargo_type: "general_freight", }, }, ]; function pickRandomPair(): AddressPair { const idx = Math.floor(Math.random() * ADDRESS_POOL.length); return ADDRESS_POOL[idx]!; } function uniqueWeight(base: number): number { const nonce = (parseInt(randomUUID().replace(/-/g, ""), 16) % 900) + 1; return base + nonce; } async function waitForHttpOk(url: string, attempts = 30): Promise { for (let i = 0; i < attempts; i += 1) { try { const res = await fetch(url); if (res.ok) { return; } } catch { /* retry */ } await new Promise((r) => setTimeout(r, 2000)); } throw new Error(`服务未就绪: ${url}`); } function draftAddress( draft: Omit, ): QuoteRequestBody["pickup_address"] { const street = draft.street.trim(); const city = draft.city.trim(); const state = draft.state.trim(); 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, }; } async function runOneCase(pair: AddressPair): Promise<{ ok: boolean; quoteId?: string; tierCount?: number; source?: string; error?: string; }> { const userPickup = { ...pair.pickup, zip: "" }; const userDelivery = { ...pair.delivery, zip: "" }; const cand = await hostFetchMothershipCandidates( BASE, TOKEN, CUSTOMER_ID, userPickup, userDelivery, ); if (cand.code !== 0 || !cand.data) { return { ok: false, error: cand.message ?? "地址候选失败" }; } const selectedPickup = pickBestMothershipCandidate( cand.data.pickup_candidates, userPickup, ); const selectedDelivery = pickBestMothershipCandidate( cand.data.delivery_candidates, userDelivery, ); if (!selectedPickup || !selectedDelivery) { return { ok: false, error: "候选为空" }; } const weightLb = uniqueWeight(pair.cargo.weight_lb); const body: QuoteRequestBody = { request_id: randomUUID(), customer_id: CUSTOMER_ID, quote_session_id: cand.data.quote_session_id, pickup_address: applyMothershipCandidate( draftAddress(userPickup), selectedPickup, ), delivery_address: applyMothershipCandidate( draftAddress(userDelivery), selectedDelivery, ), weight: { value: weightLb, unit: "lb" }, dimensions: { length: pair.cargo.length_in, width: pair.cargo.width_in, height: pair.cargo.height_in, unit: "in", }, pallet_count: pair.cargo.pallet_count, cargo_type: pair.cargo.cargo_type, }; const created = await hostCreateQuote(BASE, TOKEN, body); if (created.code !== 0 || !created.data?.quote_id) { return { ok: false, error: created.message ?? "创建询价失败" }; } const quoteId = created.data.quote_id; let finalDetail = created.data; if (finalDetail.status === "processing") { const poll = await pollQuoteUntilDone( async () => { const detail = await hostGetQuote(BASE, TOKEN, CUSTOMER_ID, quoteId); if (detail.code !== 0) { return { ok: false, errorMessage: detail.message }; } return { ok: true, data: detail.data }; }, { timeoutMs: POLL_TIMEOUT_MS }, ); if (poll.type !== "done") { return { ok: false, quoteId, error: poll.type === "timeout" ? "轮询超时" : poll.message, }; } finalDetail = poll.quote; } const tierCount = finalDetail?.quotes?.length ?? 0; const source = finalDetail?.source_type ?? ""; const ok = finalDetail?.status === "done" && tierCount >= 1 && (source === "rpa" || source === "cache"); return { ok, quoteId, tierCount, source: String(source), error: ok ? undefined : `status=${finalDetail?.status} tiers=${tierCount} source=${source}`, }; } async function main(): Promise { const runId = randomUUID().slice(0, 8); await ensureNativeInfraIfNeeded(); resetRedisClient(); await ensureRedisReady(); await waitForHttpOk(`${BASE}/`); const workerOk = await isAnyRpaWorkerHealthy(); if (!workerOk) { console.warn("[probe:random] Worker 心跳未就绪,仍尝试运行(可能排队失败)"); } console.log( `[probe:random] 开始 ${LOOPS} 次随机地址 widget 报价(dual-address-patch=on)`, ); const results: Array<{ pairId: string; label: string; ok: boolean; quoteId?: string; tierCount?: number; source?: string; error?: string; }> = []; let pass = 0; let fail = 0; for (let i = 0; i < LOOPS; i += 1) { const pair = pickRandomPair(); console.log(`\n[probe:random] case ${i + 1}/${LOOPS}: ${pair.label}`); const outcome = await runOneCase(pair); results.push({ pairId: pair.id, label: pair.label, ...outcome, }); if (outcome.ok) { pass += 1; console.log( `[PASS] ${pair.id} quote=${outcome.quoteId} tiers=${outcome.tierCount} source=${outcome.source}`, ); } else { fail += 1; console.log(`[FAIL] ${pair.id} ${outcome.error ?? "unknown"}`); } } const report = { runId, finishedAt: new Date().toISOString(), loops: LOOPS, pass, fail, verdict: fail === 0 ? "PASS" : "FAIL", env: { quoteStrategy: "direct-first-widget-fallback", RPA_DUAL_ADDRESS_PATCH: "true", RPA_ADDRESS_MODE: "real", }, results, }; const outDir = join(process.cwd(), ".rpa"); if (!existsSync(outDir)) { mkdirSync(outDir, { recursive: true }); } const outPath = join(outDir, `random-address-quote-${runId}.json`); writeFileSync(outPath, JSON.stringify(report, null, 2), "utf8"); console.log(`\n[probe:random] ${pass}/${LOOPS} 通过 | 报告: ${outPath}`); process.exit(fail === 0 ? 0 : 1); } main().catch((e) => { console.error(e); process.exit(1); });