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.

359 lines
11 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.

/**
* Playwright codegen headed 录制 Flock Freight
* 匿名 get-a-quote + Step 12.78 登录后查价(--logged-in
*
* 用法:
* npm run record:flock
* npm run record:flock -- list
* npm run record:flock -- --logged-in
* npm run record:flock -- --logged-in https://app.flockfreight.com/login
*/
import { spawn } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import {
DEFAULT_FLOCK_DEMO,
DEFAULT_FLOCK_QUOTE_URL,
FLOCK_CANONICAL_PATHS,
formatFlockRecordingChecklist,
} from "@/workers/rpa/flock/demo-input";
const ROOT = process.cwd();
const OUTPUT_DIR = path.join(ROOT, ".rpa", "recordings");
export type FlockRecordMode = "anonymous" | "logged-in";
export type ParsedFlockRecordArgs = {
mode: FlockRecordMode;
startUrl?: string;
pathId?: string;
help: boolean;
list: boolean;
fresh: boolean;
reuseStorage: boolean;
};
export const ANON_FLOCK_STORAGE_DEFAULT = path.join(
ROOT,
".rpa",
"flock-storage.json",
);
export const LOGGED_IN_FLOCK_STORAGE_DEFAULT = path.join(
ROOT,
".rpa",
"flock-logged-in-storage.json",
);
export const DEFAULT_FLOCK_LOGIN_URL =
process.env.FLOCK_LOGIN_URL?.trim() ||
"https://app.flockfreight.com/login";
export function parseFlockRecordArgs(argv: string[]): ParsedFlockRecordArgs {
const tokens = argv.slice(2).filter(Boolean);
let mode: FlockRecordMode = "anonymous";
let help = false;
let list = false;
let fresh = false;
let reuseStorage = false;
let startUrl: string | undefined;
let pathId: string | undefined;
for (const token of tokens) {
if (token === "-h" || token === "--help") {
help = true;
continue;
}
if (token === "list") {
list = true;
continue;
}
if (token === "--logged-in" || token === "--login") {
mode = "logged-in";
continue;
}
if (token === "--fresh") {
fresh = true;
continue;
}
if (token === "--reuse-storage") {
reuseStorage = true;
continue;
}
if (token.startsWith("http://") || token.startsWith("https://")) {
startUrl = token;
continue;
}
if (!token.startsWith("-") && !pathId) {
pathId = token;
continue;
}
}
if (process.env.FLOCK_RECORD_FRESH === "true") {
fresh = true;
}
if (process.env.FLOCK_RECORD_REUSE_STORAGE === "true") {
reuseStorage = true;
}
return { mode, startUrl, pathId, help, list, fresh, reuseStorage };
}
export function resolveFlockStoragePath(mode: FlockRecordMode): string {
if (mode === "logged-in") {
return (
process.env.FLOCK_LOGGED_IN_STORAGE_STATE_PATH?.trim() ||
LOGGED_IN_FLOCK_STORAGE_DEFAULT
);
}
return (
process.env.FLOCK_STORAGE_STATE_PATH?.trim() || ANON_FLOCK_STORAGE_DEFAULT
);
}
export function resolveFlockOutputBasename(
mode: FlockRecordMode,
ts: string,
pathId?: string,
): string {
if (mode === "logged-in") {
return `flock-logged-in-${ts}.js`;
}
const suffix = pathId ?? "manual";
return `flock-${suffix}-${ts}.js`;
}
export function formatTimestamp(d = new Date()): string {
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
}
export function formatFlockLoggedInRecordingChecklist(): string {
const demo = DEFAULT_FLOCK_DEMO;
return [
"【Flock 登录后查价录制清单】",
"1. 打开登录页,手动粘贴 FLOCK_LOGIN_EMAIL / FLOCK_LOGIN_PASSWORD 并提交",
"2. 进入登录后查价入口(记录最终 URL常见 Let's build a quote / Quick",
`3. 标准样例Pickup ZIP ${demo.pickupZip} → Delivery ZIP ${demo.deliveryZip}`,
`4. 托盘/重量/尺寸:${demo.palletCount} pallets / ${demo.totalWeightLb} lb / ${demo.lengthIn}×${demo.widthIn}×${demo.heightIn} in`,
"5. 填 Type / Packaging / Freight Class / Description 等登录后必填项",
"6. 点 Generate my quote或等价提交等到报价结果出现",
"7. 关闭 codegen确认 flock-logged-in-*.js + flock-logged-in-storage.json 落盘",
].join("\n");
}
function hasFlockEnvCredentials(): boolean {
const email = process.env.FLOCK_LOGIN_EMAIL?.trim() ?? "";
const password = process.env.FLOCK_LOGIN_PASSWORD?.trim() ?? "";
return Boolean(email && password);
}
function printHelp(): void {
console.log(`
用法:
npm run record:flock
npm run record:flock -- list
npm run record:flock -- <path-id>
npm run record:flock -- <起始URL>
npm run record:flock -- --logged-in [起始URL]
npm run record:flock -- --login [起始URL]
模式:
匿名(默认) 录制 marketing get-a-quote可用 --fresh 无痕
--logged-in 录制账号登录后查价完整路径storage 与匿名隔离
默认入口:
匿名: ${DEFAULT_FLOCK_QUOTE_URL}
登录后: ${DEFAULT_FLOCK_LOGIN_URL}
产出(匿名):
.rpa/recordings/flock-<path|manual>-<时间戳>.js
.rpa/flock-storage.json
产出(登录后):
.rpa/recordings/flock-logged-in-<时间戳>.js
.rpa/flock-logged-in-storage.json
登录后录制说明:
- 请在 codegen 浏览器中手动粘贴账密(脚本不自动填)
- SOP: docs/rpa-flock-logged-in-sop.md
标准样例(匿名):
${formatFlockRecordingChecklist(DEFAULT_FLOCK_DEMO)}
canonical 路径:
${FLOCK_CANONICAL_PATHS.map((p) => ` - ${p.id}: ${p.title}`).join("\n")}
`);
}
function resolveAnonymousStart(
arg: string | undefined,
): { startUrl: string; pathId?: string; demo: typeof DEFAULT_FLOCK_DEMO } {
if (!arg) {
return { startUrl: DEFAULT_FLOCK_QUOTE_URL, demo: DEFAULT_FLOCK_DEMO };
}
if (arg.startsWith("http://") || arg.startsWith("https://")) {
return { startUrl: arg, demo: DEFAULT_FLOCK_DEMO };
}
const canonical = FLOCK_CANONICAL_PATHS.find((p) => p.id === arg);
if (canonical) {
console.log(`\n路径: ${canonical.id}${canonical.title}`);
console.log(`说明: ${canonical.notes}\n`);
console.log(formatFlockRecordingChecklist(canonical.input));
console.log("");
return {
startUrl: DEFAULT_FLOCK_QUOTE_URL,
pathId: canonical.id,
demo: canonical.input,
};
}
console.warn(`未知 path-id「${arg}」,使用默认样例`);
return { startUrl: DEFAULT_FLOCK_QUOTE_URL, demo: DEFAULT_FLOCK_DEMO };
}
function main(): void {
const parsed = parseFlockRecordArgs(process.argv);
if (parsed.help) {
printHelp();
process.exit(0);
}
if (parsed.list) {
console.log("Flock canonical 录制路径:\n");
for (const p of FLOCK_CANONICAL_PATHS) {
console.log(` ${p.id}`);
console.log(` ${p.title}`);
console.log(` ${p.notes}\n`);
}
process.exit(0);
}
const mode = parsed.mode;
const storagePath = resolveFlockStoragePath(mode);
const ts = formatTimestamp();
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
fs.mkdirSync(path.dirname(storagePath), { recursive: true });
let startUrl: string;
let pathId: string | undefined;
let demo = DEFAULT_FLOCK_DEMO;
let checklistBody: string;
if (mode === "logged-in") {
startUrl = parsed.startUrl ?? DEFAULT_FLOCK_LOGIN_URL;
checklistBody = formatFlockLoggedInRecordingChecklist();
} else {
const resolved = resolveAnonymousStart(parsed.startUrl ?? parsed.pathId);
startUrl = resolved.startUrl;
pathId = resolved.pathId;
demo = resolved.demo;
checklistBody = formatFlockRecordingChecklist(demo);
}
const outputFile = path.join(
OUTPUT_DIR,
resolveFlockOutputBasename(mode, ts, pathId),
);
const checklistFile = outputFile.replace(/\.js$/, ".checklist.txt");
fs.writeFileSync(checklistFile, checklistBody, "utf8");
const args = [
"playwright",
"codegen",
"--target=javascript",
"-o",
outputFile,
];
if (mode === "logged-in") {
// 登录态必须持久 session禁止 --fresh 跳过 save
args.push(`--save-storage=${storagePath}`);
if (fs.existsSync(storagePath)) {
args.push(`--load-storage=${storagePath}`);
console.log(`已加载已有 storageState: ${storagePath}`);
}
} else if (parsed.fresh) {
console.log("【无痕录制 --fresh】不加载/保存 storage");
} else if (parsed.reuseStorage) {
args.push(`--save-storage=${storagePath}`);
if (fs.existsSync(storagePath)) {
args.push(`--load-storage=${storagePath}`);
console.log(`已加载已有 storageState: ${storagePath}`);
}
} else {
args.push(`--save-storage=${storagePath}`);
console.log(`录制结束将保存 storage: ${storagePath}`);
console.log("若已达报价上限,请使用: npm run record:flock -- --fresh");
}
args.push(startUrl);
if (mode === "logged-in") {
console.log("=== Flock Freight 登录后查价录制headed===");
console.log("RPA_HEADLESS=false");
console.log("模式: --logged-instorage 与匿名 flock-storage.json 隔离)");
if (hasFlockEnvCredentials()) {
console.log(
"已检测到 .env 中 FLOCK_LOGIN_EMAIL/PASSWORD请在浏览器中手动粘贴登录脚本不自动填",
);
} else {
console.log(
"未检测到 FLOCK_LOGIN_EMAIL/PASSWORD请用管理端客户账密或准备官网账号后手动登录",
);
}
console.log(`起始 URL: ${startUrl}`);
console.log(`脚本产出: ${outputFile}`);
console.log(`清单文件: ${checklistFile}`);
console.log(`storageState: ${storagePath}`);
console.log("SOP: docs/rpa-flock-logged-in-sop.md");
} else {
console.log("=== Flock Freight RPA 录制headed / 匿名)===");
console.log("RPA_HEADLESS=false");
console.log(`起始 URL: ${startUrl}`);
console.log(`脚本产出: ${outputFile}`);
console.log(`清单文件: ${checklistFile}`);
console.log(`storageState: ${storagePath}`);
console.log("登录后请用: npm run record:flock -- --logged-in");
}
console.log("");
console.log(checklistBody);
console.log("");
const child = spawn("npx", args, {
stdio: "inherit",
shell: true,
cwd: ROOT,
env: { ...process.env, RPA_HEADLESS: "false" },
});
child.on("error", (error) => {
console.error("启动 playwright codegen 失败:", error);
process.exit(1);
});
child.on("exit", (code) => {
if (code === 0) {
if (mode === "logged-in") {
console.log(
"\n录制结束。请回填 docs/rpa-flock-logged-in-sop.md 附录 A可运行: npx tsx scripts/finalize-flock-logged-in-recording.ts",
);
} else {
console.log(
"\n录制结束。请将 flock-*.js 中的 selector 填入 .env FLOCK_SELECTOR_*",
);
}
}
process.exit(code ?? 0);
});
}
// vitest 导入本模块时勿启动 codegen
const isRecordEntrypoint = /record-flock\.(ts|js|mjs|cjs)$/.test(
process.argv[1] ?? "",
);
if (isRecordEntrypoint && !process.env.VITEST && !process.env.VITEST_WORKER_ID) {
main();
}