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