/** * Flock Freight 登录态 ×20:多线路 / 多货物 / 多地点类型与选项 * DIAG_HEADED=false npx tsx scripts/diag-flock-logged-in-x20.ts * DIAG_ONLY=3 — 仅跑第 3 组 * * 报告:.rpa/diag/flock-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; process.env.FLOCK_WORKER_REUSE_SESSION = process.env.FLOCK_WORKER_REUSE_SESSION?.trim() || "false"; import { fitsFlockTrailerCargo } from "@/lib/constants/flock-limits"; import { defaultFlockPickupDateIso, flockPickupDisplayFromIso, } from "@/lib/flock/pickup-date"; import { isMothershipWeekendIso, snapMothershipReadyDateToWeekday, toIsoDateLocal, } from "@/lib/mothership/logged-in-constraints"; import { getCustomerProviderLogin } from "@/modules/customer/provider-credentials"; import { runFlockQuoteRpa } from "@/workers/rpa/flock/run-quote"; import type { FlockQuoteInput } from "@/workers/rpa/flock/types"; const PICKUP_BASE = snapMothershipReadyDateToWeekday( process.env.DIAG_READY_DATE?.trim() || defaultFlockPickupDateIso(), ); const DELAY_MS = Number(process.env.DIAG_DELAY_MS ?? "2000"); type CaseDef = { name: string; pickupZip: string; deliveryZip: string; palletCount: number; totalWeightLb: number; lengthIn: number; widthIn: number; heightIn: number; pickupLocationType: string; deliveryLocationType: string; packagingType: string; freightClass?: string; description?: string; stackable?: boolean; turnable?: boolean; additionalServices?: string[]; vehicleTypes?: string[]; pickupLiftgate?: boolean; pickupInside?: boolean; pickupPalletJack?: boolean; deliveryLiftgate?: boolean; deliveryInside?: boolean; deliveryPalletJack?: boolean; callBeforePickup?: boolean; callBeforeDelivery?: boolean; /** 相对 PICKUP_BASE 再偏移工作日天数 */ readyOffsetDays?: number; }; /** 20 组:跨区 ZIP + 货物 + 地点类型 / 包装 / 附加服务差异 */ export const FLOCK_LOGGED_IN_X20_CASES: CaseDef[] = [ { name: "Gardena→Pittsburgh 基线", pickupZip: "90248", deliveryZip: "15222", palletCount: 2, totalWeightLb: 500, lengthIn: 48, widthIn: 40, heightIn: 48, pickupLocationType: "business_with_dock", deliveryLocationType: "business_with_dock", packagingType: "bags", freightClass: "density", description: "papers", additionalServices: ["unloading"], stackable: true, }, { name: "LA→SF + 无月台尾板", pickupZip: "90001", deliveryZip: "94102", palletCount: 4, totalWeightLb: 2_800, lengthIn: 48, widthIn: 40, heightIn: 48, pickupLocationType: "business_with_dock", deliveryLocationType: "business_without_dock", packagingType: "pallets_48x40", freightClass: "70", deliveryLiftgate: true, }, { name: "Atlanta→Baltimore 住宅派送", pickupZip: "30326", deliveryZip: "21230", palletCount: 3, totalWeightLb: 1_200, lengthIn: 48, widthIn: 40, heightIn: 60, pickupLocationType: "business_with_dock", deliveryLocationType: "residential", packagingType: "boxes", freightClass: "85", deliveryLiftgate: true, deliveryInside: true, }, { name: "Boston→Chicago bags", pickupZip: "02109", deliveryZip: "60611", palletCount: 6, totalWeightLb: 5_500, lengthIn: 48, widthIn: 40, heightIn: 72, pickupLocationType: "business_with_dock", deliveryLocationType: "business_with_dock", packagingType: "bags", freightClass: "density", description: "papers", stackable: true, additionalServices: ["unloading"], }, { name: "Dallas→Philly + 卸货服务", pickupZip: "75201", deliveryZip: "19106", palletCount: 8, totalWeightLb: 9_200, lengthIn: 48, widthIn: 40, heightIn: 60, pickupLocationType: "business_with_dock", deliveryLocationType: "business_with_dock", packagingType: "pallets_48x40", freightClass: "65", additionalServices: ["unloading"], vehicleTypes: ["box_truck", "dry_van"], }, { name: "Austin→Denver crates", pickupZip: "78701", deliveryZip: "80202", palletCount: 2, totalWeightLb: 800, lengthIn: 48, widthIn: 48, heightIn: 48, pickupLocationType: "business_without_dock", deliveryLocationType: "business_with_dock", packagingType: "crates", freightClass: "100", pickupLiftgate: true, pickupPalletJack: true, }, { name: "Seattle→Miami 大票", pickupZip: "98101", deliveryZip: "33130", palletCount: 10, totalWeightLb: 14_000, lengthIn: 48, widthIn: 40, heightIn: 72, pickupLocationType: "business_with_dock", deliveryLocationType: "business_with_dock", packagingType: "pallets_48x40", freightClass: "70", callBeforePickup: true, }, { name: "Phoenix→Houston 无月台派送", pickupZip: "85004", deliveryZip: "77002", palletCount: 4, totalWeightLb: 3_200, lengthIn: 48, widthIn: 40, heightIn: 48, pickupLocationType: "business_with_dock", deliveryLocationType: "business_without_dock", packagingType: "pallets_48x40", freightClass: "85", deliveryLiftgate: true, deliveryPalletJack: true, }, { name: "Portland→Dallas 长托", pickupZip: "97201", deliveryZip: "75201", palletCount: 4, totalWeightLb: 8_000, lengthIn: 96, widthIn: 48, heightIn: 72, pickupLocationType: "business_with_dock", deliveryLocationType: "business_with_dock", packagingType: "pallets_custom", freightClass: "density", turnable: true, }, { name: "Denver→NYC drums", pickupZip: "80202", deliveryZip: "10001", palletCount: 5, totalWeightLb: 4_000, lengthIn: 48, widthIn: 40, heightIn: 48, pickupLocationType: "business_with_dock", deliveryLocationType: "business_with_dock", packagingType: "drums", freightClass: "92.5", description: "drums", }, { name: "Chicago→Atlanta 学校派送", pickupZip: "60611", deliveryZip: "30326", palletCount: 3, totalWeightLb: 1_500, lengthIn: 48, widthIn: 40, heightIn: 48, pickupLocationType: "business_with_dock", deliveryLocationType: "limited_school", packagingType: "boxes", freightClass: "110", deliveryLiftgate: true, deliveryInside: true, callBeforeDelivery: true, }, { name: "BeverlyHills→Baltimore + load_to_ride", pickupZip: "90212", deliveryZip: "21230", palletCount: 6, totalWeightLb: 6_000, lengthIn: 48, widthIn: 40, heightIn: 60, pickupLocationType: "business_with_dock", deliveryLocationType: "business_with_dock", packagingType: "pallets_48x40", freightClass: "70", additionalServices: ["load_to_ride"], vehicleTypes: ["box_truck", "dry_van", "refrigerated"], }, { name: "Philly→Boston 双侧无月台", pickupZip: "19106", deliveryZip: "02109", palletCount: 2, totalWeightLb: 700, lengthIn: 48, widthIn: 40, heightIn: 48, pickupLocationType: "business_without_dock", deliveryLocationType: "business_without_dock", packagingType: "pallets_48x40", freightClass: "85", pickupLiftgate: true, deliveryLiftgate: true, }, { name: "Pittsburgh→LA 展会", pickupZip: "15222", deliveryZip: "90001", palletCount: 4, totalWeightLb: 2_400, lengthIn: 48, widthIn: 40, heightIn: 48, pickupLocationType: "business_with_dock", deliveryLocationType: "trade_show", packagingType: "crates", freightClass: "125", deliveryPalletJack: true, }, { name: "Houston→Seattle 48x48", pickupZip: "77002", deliveryZip: "98101", palletCount: 8, totalWeightLb: 12_000, lengthIn: 48, widthIn: 48, heightIn: 60, pickupLocationType: "business_with_dock", deliveryLocationType: "business_with_dock", packagingType: "pallets_48x48", freightClass: "65", stackable: true, }, { name: "Miami→Phoenix boxes", pickupZip: "33130", deliveryZip: "85004", palletCount: 2, totalWeightLb: 900, lengthIn: 48, widthIn: 40, heightIn: 48, pickupLocationType: "business_with_dock", deliveryLocationType: "business_with_dock", packagingType: "boxes", freightClass: "150", description: "cartons", turnable: false, }, { name: "NYC→Dallas 住宅派送", pickupZip: "10001", deliveryZip: "75201", palletCount: 5, totalWeightLb: 3_500, lengthIn: 48, widthIn: 40, heightIn: 48, pickupLocationType: "business_with_dock", deliveryLocationType: "residential", packagingType: "boxes", freightClass: "70", deliveryLiftgate: true, deliveryInside: true, }, { name: "SF→Austin + 预约前电联", pickupZip: "94102", deliveryZip: "78701", palletCount: 3, totalWeightLb: 1_800, lengthIn: 48, widthIn: 40, heightIn: 48, pickupLocationType: "residential", deliveryLocationType: "business_with_dock", packagingType: "boxes", freightClass: "92.5", pickupLiftgate: true, pickupInside: true, callBeforePickup: true, callBeforeDelivery: true, readyOffsetDays: 1, }, { name: "Baltimore→Gardena coils", pickupZip: "21230", deliveryZip: "90248", palletCount: 4, totalWeightLb: 6_500, lengthIn: 48, widthIn: 40, heightIn: 48, pickupLocationType: "business_with_dock", deliveryLocationType: "business_with_dock", packagingType: "coils", freightClass: "60", additionalServices: ["unloading"], }, { name: "Chicago→Denver 轻货偏移日", pickupZip: "60611", deliveryZip: "80202", palletCount: 2, totalWeightLb: 600, lengthIn: 40, widthIn: 36, heightIn: 36, pickupLocationType: "business_with_dock", deliveryLocationType: "business_with_dock", packagingType: "boxes", freightClass: "density", stackable: true, additionalServices: ["unloading"], readyOffsetDays: 2, }, ]; function offsetPickupIso(baseIso: string, offsetDays: number): string { if (!offsetDays) return baseIso; const [y, m, d] = baseIso.split("-").map(Number); const dt = new Date(y!, m! - 1, d!); let left = offsetDays; while (left > 0) { dt.setDate(dt.getDate() + 1); const iso = toIsoDateLocal(dt); if (!isMothershipWeekendIso(iso)) left -= 1; } return snapMothershipReadyDateToWeekday(toIsoDateLocal(dt)); } function sleep(ms: number): Promise { return new Promise((r) => setTimeout(r, ms)); } type Row = { n: number; name: string; ok: boolean; ms: number; tiers: number; detail: string; code?: string; }; function assertCasesValid(cases: CaseDef[]): void { for (const c of cases) { if (c.palletCount < 1) throw new Error(`case ${c.name}: palletCount`); if (c.totalWeightLb > 45_000) throw new Error(`case ${c.name}: weight`); if (c.pickupZip === c.deliveryZip) { throw new Error(`case ${c.name}: same zip`); } if ( !fitsFlockTrailerCargo({ palletCount: c.palletCount, lengthIn: c.lengthIn, widthIn: c.widthIn, }) ) { throw new Error(`case ${c.name}: linear feet`); } } } async function main(): Promise { assertCasesValid(FLOCK_LOGGED_IN_X20_CASES); const onlyRaw = process.env.DIAG_ONLY?.trim() ?? ""; const onlyNums = onlyRaw ? onlyRaw .split(/[,,\s]+/) .map((s) => s.trim()) .filter((s) => /^\d+$/.test(s)) .map(Number) : []; const selected = FLOCK_LOGGED_IN_X20_CASES.map((c, i) => ({ c, n: i })).filter( ({ c, n }) => { if (!onlyRaw) return true; if (onlyNums.length > 0) return onlyNums.includes(n + 1); return c.name.includes(onlyRaw); }, ); if (selected.length === 0) { throw new Error(`DIAG_ONLY=${onlyRaw} 无匹配用例`); } const customerId = process.env.DIAG_CUSTOMER_ID?.trim() || "CUST_001"; const login = await getCustomerProviderLogin(customerId, "flock"); if (!login) { throw new Error(`客户 ${customerId} 无可用 Flock 账密`); } console.log("=== Flock Freight 登录态 ×20 多线路探测 ==="); console.log( `headed=${wantHeaded} customer=${customerId} email=${login.email.slice(0, 2)}*** ` + `pickupBase=${PICKUP_BASE} cases=${selected.length}/${FLOCK_LOGGED_IN_X20_CASES.length}` + (onlyRaw ? ` only=${onlyRaw}` : "") + ` delayMs=${DELAY_MS}`, ); const rows: Row[] = []; for (let i = 0; i < selected.length; i += 1) { const { c, n } = selected[i]!; if (i > 0 && DELAY_MS > 0) await sleep(DELAY_MS); const pickupIso = offsetPickupIso(PICKUP_BASE, c.readyOffsetDays ?? 0); const pickupDate = flockPickupDisplayFromIso(pickupIso); const t0 = Date.now(); console.log( `\n---------- ${n + 1}/20 ${c.name} ${c.pickupZip}→${c.deliveryZip} ` + `${c.palletCount}pl ${c.totalWeightLb}lb ----------`, ); console.log( ` loc=${c.pickupLocationType}/${c.deliveryLocationType} pack=${c.packagingType} ` + `svc=[${(c.additionalServices ?? []).join(",")}] date=${pickupDate}`, ); const input: FlockQuoteInput = { formMode: "logged_in_quick", pickupDate, pickupZip: c.pickupZip, deliveryZip: c.deliveryZip, palletCount: c.palletCount, totalWeightLb: c.totalWeightLb, lengthIn: c.lengthIn, widthIn: c.widthIn, heightIn: c.heightIn, pickupLocationType: c.pickupLocationType, deliveryLocationType: c.deliveryLocationType, packagingType: c.packagingType, freightClass: c.freightClass, description: c.description, stackable: c.stackable, turnable: c.turnable, additionalServices: c.additionalServices, vehicleTypes: c.vehicleTypes, pickupLiftgate: c.pickupLiftgate, pickupInside: c.pickupInside, pickupPalletJack: c.pickupPalletJack, deliveryLiftgate: c.deliveryLiftgate, deliveryInside: c.deliveryInside, deliveryPalletJack: c.deliveryPalletJack, callBeforePickup: c.callBeforePickup, callBeforeDelivery: c.callBeforeDelivery, }; const result = await runFlockQuoteRpa(input, { customerId }); const ms = Date.now() - t0; if (result.ok && result.quotes.length > 0) { const preview = result.quotes .slice(0, 4) .map((q) => `${q.label}=$${q.totalUsd}`) .join(", "); rows.push({ n: n + 1, name: c.name, ok: true, ms, tiers: result.quotes.length, detail: `${result.quotes.length}档 ${preview}${result.quotes.length > 4 ? "…" : ""}`, }); console.log(`[OK] ${ms}ms tiers=${result.quotes.length} ${preview}`); } else { const msg = (result.errorMessage ?? "no quotes").slice(0, 240); const codeMatch = /^([A-Z][A-Z0-9_]+):/.exec(msg); rows.push({ n: n + 1, name: c.name, ok: false, ms, tiers: 0, detail: msg, code: codeMatch?.[1], }); console.log(`[FAIL] ${ms}ms ${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 outDir = path.join(process.cwd(), ".rpa", "diag"); fs.mkdirSync(outDir, { recursive: true }); const stamp = new Date().toISOString().replace(/[:.]/g, "-"); const reportPath = path.join(outDir, `flock-logged-in-x20-${stamp}.md`); const lines = [ `# Flock Freight 登录态 ×20`, ``, `- customer: ${customerId}`, `- pickupBase: ${PICKUP_BASE}`, `- ok=${okN} fail=${failN} avgOkMs=${avgOkMs}`, ``, `| # | 结果 | 用例 | ms | tiers | 详情 |`, `|---|------|------|----|-------|------|`, ...rows.map( (r) => `| ${r.n} | ${r.ok ? "OK" : "FAIL"} | ${r.name} | ${r.ms} | ${r.tiers} | ${(r.code ? `[${r.code}] ` : "") + r.detail.replace(/\|/g, "/")} |`, ), ]; fs.writeFileSync(reportPath, lines.join("\n"), "utf8"); console.log( `\n=== SUMMARY ok=${okN}/${FLOCK_LOGGED_IN_X20_CASES.length} fail=${failN} avgOkMs=${avgOkMs} ===`, ); console.log(`report=${reportPath}`); for (const r of rows) { console.log( ` #${r.n} ${r.ok ? "OK " : "FAIL"} ${r.name} ${r.ms}ms tiers=${r.tiers}` + (r.ok ? "" : ` [${r.code ?? "-"}]`), ); } if (failN > 0) process.exitCode = 1; } const isDirectRun = typeof process.argv[1] === "string" && /diag-flock-logged-in-x20\.(ts|js|mts|mjs)$/i.test( process.argv[1].replace(/\\/g, "/"), ); if (isDirectRun) { main().catch((err) => { console.error(err); process.exit(1); }); }