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/capture-flock-quote-body.ts

177 lines
5.9 KiB

/**
* 用 page.route + window 钩子捕获 POST /user/quotes body
*/
import fs from "node:fs";
import path from "node:path";
import { resolveFlockQuoteAccount } from "@/lib/flock/resolve-account";
import { getFlockQuoteUrl, getFlockQuoteWaitMs } from "@/lib/flock/env";
import { createRpaBrowserContext } from "@/lib/rpa/browser-context";
import { launchRpaBrowser } from "@/lib/rpa/browser-launch";
import { gotoWithResilience } from "@/lib/rpa/page-goto";
import { DEFAULT_FLOCK_DEMO } from "@/workers/rpa/flock/demo-input";
import {
clickFlockQuoteSubmit,
fillFlockQuoteForm,
waitForFlockQuoteForm,
} from "@/workers/rpa/flock/form-interactions";
import type { FlockQuoteInput } from "@/workers/rpa/flock/types";
const OUT_DIR = path.join(process.cwd(), ".rpa", "flock-network-poc");
async function main(): Promise<void> {
fs.mkdirSync(OUT_DIR, { recursive: true });
const input: FlockQuoteInput = {
pickupDate: DEFAULT_FLOCK_DEMO.pickupDateDisplay,
pickupZip: DEFAULT_FLOCK_DEMO.pickupZip,
deliveryZip: DEFAULT_FLOCK_DEMO.deliveryZip,
palletCount: DEFAULT_FLOCK_DEMO.palletCount,
totalWeightLb: DEFAULT_FLOCK_DEMO.totalWeightLb,
lengthIn: DEFAULT_FLOCK_DEMO.lengthIn,
widthIn: DEFAULT_FLOCK_DEMO.widthIn,
heightIn: DEFAULT_FLOCK_DEMO.heightIn,
};
const account = resolveFlockQuoteAccount(input);
const captured: string[] = [];
const browser = await launchRpaBrowser({
headless: process.env.RPA_HEADLESS !== "false",
incognito: true,
});
try {
const context = await createRpaBrowserContext(browser);
await context.addInitScript(() => {
const w = window as unknown as {
__flockQuoteBodies?: string[];
};
w.__flockQuoteBodies = [];
const origFetch = window.fetch.bind(window);
window.fetch = async (...args: Parameters<typeof fetch>) => {
try {
const inputArg = args[0];
const init = args[1];
const url =
typeof inputArg === "string"
? inputArg
: inputArg instanceof Request
? inputArg.url
: String(inputArg);
if (/api\.flockfreight\.com\/user\/quotes/i.test(url)) {
let bodyText = "";
if (init?.body) {
if (typeof init.body === "string") bodyText = init.body;
else if (init.body instanceof URLSearchParams) {
bodyText = init.body.toString();
} else if (init.body instanceof Blob) {
bodyText = await init.body.text();
} else {
bodyText = String(init.body);
}
} else if (inputArg instanceof Request) {
bodyText = await inputArg.clone().text();
}
w.__flockQuoteBodies?.push(bodyText);
}
} catch {
/* ignore */
}
return origFetch(...args);
};
const XO = XMLHttpRequest.prototype.open;
const XS = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (
method: string,
url: string | URL,
...rest: unknown[]
) {
(this as unknown as { __flockUrl?: string }).__flockUrl = String(url);
return XO.apply(this, [method, url, ...rest] as never);
};
XMLHttpRequest.prototype.send = function (body?: Document | XMLHttpRequestBodyInit | null) {
const url = (this as unknown as { __flockUrl?: string }).__flockUrl ?? "";
if (/api\.flockfreight\.com\/user\/quotes/i.test(url)) {
const text =
typeof body === "string"
? body
: body == null
? ""
: String(body);
w.__flockQuoteBodies?.push(text);
}
return XS.call(this, body);
};
});
await context.route("**/api.flockfreight.com/user/quotes**", async (route) => {
const req = route.request();
const post = req.postData();
if (post) {
captured.push(post);
console.log(`[route] bodyLen=${post.length}`);
} else {
console.log(`[route] no postData method=${req.method()}`);
}
await route.continue();
});
const page = await context.newPage();
await gotoWithResilience(page, getFlockQuoteUrl());
if (!(await waitForFlockQuoteForm(page))) throw new Error("表单未出现");
const filled = await fillFlockQuoteForm(page, input);
if (!filled.ok) throw new Error(filled.error ?? "填表失败");
if (!(await clickFlockQuoteSubmit(page))) throw new Error("无提交按钮");
const deadline = Date.now() + getFlockQuoteWaitMs();
while (Date.now() < deadline) {
const fromPage = await page.evaluate(() => {
const w = window as unknown as { __flockQuoteBodies?: string[] };
return w.__flockQuoteBodies ?? [];
});
if (fromPage.length || captured.length) {
for (const b of fromPage) {
if (b && !captured.includes(b)) captured.push(b);
}
break;
}
await page.waitForTimeout(400);
}
await page.waitForTimeout(2000);
const late = await page.evaluate(() => {
const w = window as unknown as { __flockQuoteBodies?: string[] };
return w.__flockQuoteBodies ?? [];
});
for (const b of late) {
if (b && !captured.includes(b)) captured.push(b);
}
const outPath = path.join(OUT_DIR, "user-quotes-request-body.json");
fs.writeFileSync(
outPath,
JSON.stringify(
{
capturedAt: new Date().toISOString(),
email: account.email,
bodies: captured,
},
null,
2,
),
"utf8",
);
console.log(`wrote ${outPath} count=${captured.length}`);
if (!captured.length || !captured.some((b) => b.trim())) {
console.error("仍未捕获到 body");
process.exit(3);
}
console.log(captured.find((b) => b.trim())!.slice(0, 5000));
} finally {
await browser.close();
}
}
main().catch((e) => {
console.error(e);
process.exit(1);
});