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-mothership-logged-in-x...

510 lines
15 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.

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