/** * MotherShip 登录态 ×20:多线路 / 多货物 / 多附加服务组合 * DIAG_HEADED=false npx tsx scripts/diag-mothership-logged-in-x20.ts * * 报告:.rpa/diag/logged-in-x20-*.md */ import fs from "node:fs"; import path from "node:path"; import { loadDevEnv } from "@/lib/dev-env"; loadDevEnv(); const wantHeaded = process.env.DIAG_HEADED === "true"; process.env.RPA_HEADED = wantHeaded ? "true" : "false"; process.env.RPA_HEADLESS = wantHeaded ? "false" : "true"; if (!wantHeaded) delete process.env.RPA_SLOW_MO_MS; import { resolveAxelMothershipSuggest } from "@/lib/axel/candidates"; import { defaultMothershipReadyDateIso, MOTHERSHIP_MAX_WEIGHT_EACH_LB, snapMothershipReadyDateToWeekday, } from "@/lib/mothership/logged-in-constraints"; import { runWithMothershipLoginContext } from "@/lib/rpa/mothership-login-context"; import { getCustomerProviderLogin } from "@/modules/customer/provider-credentials"; import type { QuoteRequest } from "@/modules/providers/quote-provider"; import { RpaError } from "@/modules/rpa/errors"; import { buildLoggedInAddressSearchQuery, runMothershipLoggedInDashboardQuote, } from "@/workers/rpa/mothership-logged-in-quote"; const READY_BASE = snapMothershipReadyDateToWeekday( process.env.DIAG_READY_DATE?.trim() || defaultMothershipReadyDateIso(), ); type CaseDef = { name: string; pickup: string; delivery: string; weightEach: number; qty: number; dims: readonly [number, number, number]; cargoType: string; readyTime: string; pickupAccessorials?: string[]; deliveryAccessorials?: string[]; /** 相对 READY_BASE 再偏移工作日天数(仍会 snap) */ readyOffsetDays?: number; }; /** 20 组:地址/重量/件数/尺寸/货类/时刻/附加服务均有差异 */ const CASES: CaseDef[] = [ { name: "Salem→SanCarlos 基线", pickup: "888 Liberty Street Northeast unit d 3, Salem, OR, USA", delivery: "789 Elm Street suite 301, San Carlos, CA, USA", weightEach: 500, qty: 1, dims: [48, 40, 48], cargoType: "pallet", readyTime: "11:00 AM", }, { name: "LA→SF + liftgate 派送", pickup: "2020 West Washington Boulevard floor 5 rm 508, Los Angeles, CA, USA", delivery: "555 Market St ste 1001, San Francisco, CA, USA", weightEach: 480, qty: 2, dims: [48, 40, 48], cargoType: "pallet", readyTime: "12:00 PM", deliveryAccessorials: ["liftgate"], }, { name: "Brea→Fairhope 预约", pickup: "789 East Elm Street suite 301, Brea, CA, USA", delivery: "456 Oak Avenue unit 207, Fairhope, AL, USA", weightEach: 250, qty: 2, dims: [60, 48, 48], cargoType: "pallet", readyTime: "9:00 AM", deliveryAccessorials: ["appointment"], }, { name: "Albany→SF 轻货", pickup: "101 South Pine Avenue, Albany, NY 12208, USA", delivery: "555 Market St ste 1001, San Francisco, CA, USA", weightEach: 120, qty: 2, dims: [48, 40, 40], cargoType: "box", readyTime: "2:00 PM", }, { name: "Dallas→Chicago + 提货尾板", pickup: "1234 Main Street, Dallas, TX, USA", delivery: "200 East Randolph Street, Chicago, IL, USA", weightEach: 500, qty: 2, dims: [48, 40, 40], cargoType: "pallet", readyTime: "10:00 AM", pickupAccessorials: ["liftgate"], }, { name: "Atlanta→Denver crate", pickup: "100 Peachtree Street NW, Atlanta, GA, USA", delivery: "1701 California Street, Denver, CO, USA", weightEach: 300, qty: 2, dims: [48, 40, 48], cargoType: "crate", readyTime: "1:00 PM", }, { name: "Brea→LA 小件", pickup: "789 East Elm Street, Brea, CA, USA", delivery: "2020 West Washington Boulevard floor 5 rm 508, Los Angeles, CA, USA", weightEach: 50, qty: 1, dims: [40, 40, 40], cargoType: "piece", readyTime: "8:00 AM", }, { name: "SF→Albany + residential 派送", pickup: "555 Market St ste 1001, San Francisco, CA, USA", delivery: "101 South Pine Avenue, Albany, NY 12208, USA", weightEach: 150, qty: 2, dims: [48, 40, 48], cargoType: "pallet", readyTime: "3:00 PM", deliveryAccessorials: ["residential"], }, { name: "Chicago→Dallas 单托重货", pickup: "200 East Randolph Street, Chicago, IL, USA", delivery: "1234 Main Street, Dallas, TX, USA", weightEach: 1200, qty: 1, dims: [48, 40, 60], cargoType: "pallet", readyTime: "11:00 AM", }, { name: "Denver→Atlanta + limitedAccess", pickup: "1701 California Street, Denver, CO, USA", delivery: "100 Peachtree Street NW, Atlanta, GA, USA", weightEach: 220, qty: 2, dims: [48, 40, 40], cargoType: "skid", readyTime: "4:00 PM", deliveryAccessorials: ["limitedAccess"], }, { name: "Seattle→Miami 3托", pickup: "3131 Western Ave, Seattle, WA, USA", delivery: "7000 NW 52nd St, Miami, FL, USA", weightEach: 400, qty: 3, dims: [48, 40, 48], cargoType: "pallet", readyTime: "9:00 AM", }, { name: "Phoenix→Houston carton", pickup: "1 E Washington St, Phoenix, AZ, USA", delivery: "1201 Louisiana St, Houston, TX, USA", weightEach: 180, qty: 4, dims: [40, 36, 36], cargoType: "carton", readyTime: "12:00 PM", }, { name: "Boston→Philly + 双侧尾板", pickup: "1 Federal St, Boston, MA, USA", delivery: "30 S 17th Street, Philadelphia, PA, USA", weightEach: 350, qty: 2, dims: [48, 40, 48], cargoType: "pallet", readyTime: "10:00 AM", pickupAccessorials: ["liftgate"], deliveryAccessorials: ["liftgate"], }, { name: "Portland→Sacramento", pickup: "1000 SW Broadway, Portland, OR, USA", delivery: "1201 K Street, Sacramento, CA, USA", weightEach: 275, qty: 2, dims: [48, 40, 45], cargoType: "pallet", readyTime: "2:00 PM", readyOffsetDays: 1, }, { name: "Minneapolis→KC", pickup: "800 Nicollet Mall, Minneapolis, MN, USA", delivery: "1200 Main St, Kansas City, MO, USA", weightEach: 600, qty: 1, dims: [48, 42, 48], cargoType: "pallet", readyTime: "7:00 AM", }, { name: "Detroit→Cleveland drum", pickup: "1 Campus Martius, Detroit, MI, USA", delivery: "200 Public Square, Cleveland, OH, USA", weightEach: 90, qty: 5, dims: [24, 24, 36], cargoType: "drum", readyTime: "1:00 PM", }, { name: "Nashville→Memphis + inside 派送", pickup: "501 Broadway, Nashville, TN, USA", delivery: "100 Peabody Place, Memphis, TN, USA", weightEach: 200, qty: 2, dims: [48, 40, 40], cargoType: "pallet", readyTime: "11:00 AM", deliveryAccessorials: ["inside"], }, { name: "SaltLake→Boise", pickup: "15 W South Temple, Salt Lake City, UT, USA", delivery: "101 S Capitol Blvd, Boise, ID, USA", weightEach: 320, qty: 2, dims: [48, 40, 48], cargoType: "crate", readyTime: "3:00 PM", readyOffsetDays: 2, }, { name: "Charlotte→Raleigh 预约+尾板", pickup: "100 N Tryon St, Charlotte, NC, USA", delivery: "421 Fayetteville St, Raleigh, NC, USA", weightEach: 160, qty: 2, dims: [48, 40, 40], cargoType: "box", readyTime: "9:00 AM", deliveryAccessorials: ["appointment", "liftgate"], }, { name: "LasVegas→SanDiego 近重限", pickup: "3799 S Las Vegas Blvd, Las Vegas, NV, USA", delivery: "600 B St, San Diego, CA, USA", weightEach: Math.min(4500, MOTHERSHIP_MAX_WEIGHT_EACH_LB), qty: 1, dims: [48, 48, 48], cargoType: "pallet", readyTime: "5:00 PM", }, ]; type Row = { n: number; name: string; ok: boolean; ms: number; lastStep: string; tiers: number; detail: string; code?: string; }; const timingBuf: string[] = []; const origLog = console.log.bind(console); console.log = (...args: unknown[]) => { const line = args.map(String).join(" "); if ( line.includes("[rpa] logged-in timing:") || line.includes("[rpa] logged-in step:") || line.includes("still no rate-card") || line.includes("rate-card ready") || line.includes("FAILED") || line.includes("附加服务") ) { timingBuf.push(line); } origLog(...args); }; function offsetReadyDate(baseIso: string, offsetDays: number): string { const d = new Date(`${baseIso}T12:00:00`); d.setDate(d.getDate() + offsetDays); const yyyy = d.getFullYear(); const mm = String(d.getMonth() + 1).padStart(2, "0"); const dd = String(d.getDate()).padStart(2, "0"); return snapMothershipReadyDateToWeekday(`${yyyy}-${mm}-${dd}`); } async function pickFirst(query: string) { const list = await resolveAxelMothershipSuggest(query); const hit = list.find((c) => c.selectable !== false && c.option_id.trim()); if (!hit) throw new Error(`无联想: ${query.slice(0, 60)}`); return hit; } function toAddr(c: Awaited>) { return { street: c.street, city: c.city, state: c.state, zip: c.zip || "", placeId: c.option_id, formattedAddress: c.formatted_address || c.display_label, selectedFromSuggestions: true, mothershipOptionId: c.option_id, mothershipDisplayLabel: c.display_label, selectedFromMothership: true, }; } function lastStep(): string { for (let i = timingBuf.length - 1; i >= 0; i -= 1) { const m = timingBuf[i]!.match(/step="([^"]+)"/); if (m) return m[1]!; if (timingBuf[i]!.includes("still no rate-card")) return "wait rate-card"; } return "(none)"; } async function resolveLogin(): Promise<{ email: string; password: string; source: "customer" | "env" | "storage"; }> { const customerId = process.env.DIAG_CUSTOMER_ID?.trim() || "CUST_001"; const fromDb = await getCustomerProviderLogin(customerId, "mothership"); if (fromDb) return { ...fromDb, source: "customer" }; const email = process.env.MOTHERSHIP_EMAIL?.trim() ?? ""; const password = process.env.MOTHERSHIP_PASSWORD?.trim() ?? ""; if (email && password) return { email, password, source: "env" }; if (fs.existsSync(".rpa/mothership-logged-in-storage.json")) { return { email: "storage-session@local", password: "storage-session", source: "storage", }; } throw new Error("无 MotherShip 账密 / storage"); } async function main(): Promise { for (const c of CASES) { if (c.weightEach > MOTHERSHIP_MAX_WEIGHT_EACH_LB) { throw new Error(`case ${c.name} weightEach 超限`); } } /** DIAG_ONLY=13 或 DIAG_ONLY=Boston→Philly 仅跑子集 */ const onlyRaw = process.env.DIAG_ONLY?.trim() ?? ""; const selected = CASES.map((c, i) => ({ c, n: i })).filter(({ c, n }) => { if (!onlyRaw) return true; if (/^\d+$/.test(onlyRaw)) return n + 1 === Number(onlyRaw); return c.name.includes(onlyRaw); }); if (selected.length === 0) { throw new Error(`DIAG_ONLY=${onlyRaw} 无匹配用例`); } const login = await resolveLogin(); console.log("=== MotherShip 登录态 ×20 多线路探测 ==="); console.log( `headed=${wantHeaded} login=${login.source} readyBase=${READY_BASE} cases=${selected.length}/${CASES.length}` + (onlyRaw ? ` only=${onlyRaw}` : ""), ); const rows: Row[] = []; for (const { c, n } of selected) { timingBuf.length = 0; const t0 = Date.now(); const readyDate = offsetReadyDate(READY_BASE, c.readyOffsetDays ?? 0); console.log( `\n---------- ${n + 1}/20 ${c.name} wt=${c.weightEach}×${c.qty} type=${c.cargoType} ----------`, ); console.log( ` accessorials pickup=[${(c.pickupAccessorials ?? []).join(",")}] delivery=[${(c.deliveryAccessorials ?? []).join(",")}] ready=${readyDate} ${c.readyTime}`, ); try { const [p, d] = await Promise.all([ pickFirst(c.pickup), pickFirst(c.delivery), ]); const req: QuoteRequest = { cargoHash: `diag-x20-${n}-${Date.now()}`, pickup: toAddr(p), delivery: toAddr(d), weightLb: c.weightEach, dimsIn: { l: c.dims[0], w: c.dims[1], h: c.dims[2] }, palletCount: c.qty, cargoType: "general_freight", readyDate, readyTime: c.readyTime, pickupAccessorials: c.pickupAccessorials, deliveryAccessorials: c.deliveryAccessorials, cargoLines: [ { cargoType: c.cargoType, quantity: c.qty, weightLb: c.weightEach, lengthIn: c.dims[0], widthIn: c.dims[1], heightIn: c.dims[2], }, ], }; console.log( ` ${buildLoggedInAddressSearchQuery(req.pickup)} → ${buildLoggedInAddressSearchQuery(req.delivery)}`, ); const items = await runWithMothershipLoginContext( { email: login.email, password: login.password }, login.source === "customer" ? "customer" : "env", () => runMothershipLoggedInDashboardQuote(req), ); const ms = Date.now() - t0; const carriers = items .slice(0, 4) .map((i) => `${i.carrier}=$${i.rawTotal}`) .join(", "); rows.push({ n: n + 1, name: c.name, ok: true, ms, lastStep: lastStep(), tiers: items.length, detail: `${items.length}档 ${carriers}${items.length > 4 ? "…" : ""}`, }); console.log(`[OK] ${ms}ms tiers=${items.length} ${carriers}`); } catch (err) { const ms = Date.now() - t0; const msg = err instanceof Error ? err.message : String(err); const code = err instanceof RpaError ? err.code : undefined; rows.push({ n: n + 1, name: c.name, ok: false, ms, lastStep: lastStep(), tiers: 0, detail: msg.slice(0, 240), code, }); console.log( `[FAIL] ${ms}ms code=${code ?? "-"} step=${lastStep()} ${msg.slice(0, 180)}`, ); } } const okN = rows.filter((r) => r.ok).length; const failN = rows.length - okN; const avgOkMs = okN > 0 ? Math.round( rows.filter((r) => r.ok).reduce((s, r) => s + r.ms, 0) / okN, ) : 0; const reportPath = path.join( process.cwd(), ".rpa", "diag", `logged-in-x20-${new Date().toISOString().replace(/[:.]/g, "-")}.md`, ); const md = [ `# MotherShip 登录态 ×20`, "", `- readyBase=${READY_BASE}`, `- login=${login.source}`, `- ok=${okN}/20 fail=${failN} avgOkMs=${avgOkMs}`, "", "| # | case | ok | ms | tiers | lastStep | code | detail |", "|---|------|----|----|-------|----------|------|--------|", ...rows.map( (r) => `| ${r.n} | ${r.name} | ${r.ok ? "Y" : "N"} | ${r.ms} | ${r.tiers} | ${r.lastStep} | ${r.code ?? ""} | ${r.detail.replace(/\|/g, "/")} |`, ), "", ].join("\n"); fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, md, "utf8"); console.log(`\n=== SUMMARY ok=${okN}/20 fail=${failN} avgOkMs=${avgOkMs} ===`); console.log(`report=${reportPath}`); for (const r of rows) { console.log( ` #${String(r.n).padStart(2)} ${r.ok ? "OK " : "FAIL"} ${r.name} ${r.ms}ms tiers=${r.tiers}${r.code ? ` [${r.code}]` : ""}`, ); } process.exit(failN === 0 ? 0 : 1); } main().catch((e) => { console.error(e); process.exit(1); });