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.
chajia/scripts/diag-flock-logged-in-x20.ts

609 lines
17 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.

/**
* 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<void> {
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<void> {
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);
});
}