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.
177 lines
5.9 KiB
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);
|
|
});
|