|
|
/**
|
|
|
* Playwright codegen headed 录制(跨平台,Windows 无需 bash)
|
|
|
* task-121 / GD-15(匿名)+ Step 12.61(登录后查价)
|
|
|
*/
|
|
|
import { spawn } from "node:child_process";
|
|
|
import fs from "node:fs";
|
|
|
import path from "node:path";
|
|
|
import { isForbiddenQuoteUrl } from "@/lib/rpa/env";
|
|
|
|
|
|
const ROOT = process.cwd();
|
|
|
const OUTPUT_DIR = path.join(ROOT, ".rpa", "recordings");
|
|
|
|
|
|
export type RecordMode = "anonymous" | "logged-in";
|
|
|
|
|
|
export type ParsedRecordArgs = {
|
|
|
mode: RecordMode;
|
|
|
startUrl?: string;
|
|
|
help: boolean;
|
|
|
};
|
|
|
|
|
|
export const ANON_STORAGE_DEFAULT = path.join(
|
|
|
ROOT,
|
|
|
".rpa",
|
|
|
"mothership-storage.json",
|
|
|
);
|
|
|
export const LOGGED_IN_STORAGE_DEFAULT = path.join(
|
|
|
ROOT,
|
|
|
".rpa",
|
|
|
"mothership-logged-in-storage.json",
|
|
|
);
|
|
|
|
|
|
export function parseRecordArgs(argv: string[]): ParsedRecordArgs {
|
|
|
const tokens = argv.slice(2).filter(Boolean);
|
|
|
let mode: RecordMode = "anonymous";
|
|
|
let help = false;
|
|
|
let startUrl: string | undefined;
|
|
|
|
|
|
for (const token of tokens) {
|
|
|
if (token === "-h" || token === "--help") {
|
|
|
help = true;
|
|
|
continue;
|
|
|
}
|
|
|
if (token === "--logged-in" || token === "--login") {
|
|
|
mode = "logged-in";
|
|
|
continue;
|
|
|
}
|
|
|
if (token.startsWith("http://") || token.startsWith("https://")) {
|
|
|
startUrl = token;
|
|
|
continue;
|
|
|
}
|
|
|
// 忽略未知 flag,避免误伤
|
|
|
}
|
|
|
|
|
|
return { mode, startUrl, help };
|
|
|
}
|
|
|
|
|
|
export function resolveStoragePath(mode: RecordMode): string {
|
|
|
if (mode === "logged-in") {
|
|
|
return (
|
|
|
process.env.RPA_LOGGED_IN_STORAGE_STATE_PATH?.trim() ||
|
|
|
LOGGED_IN_STORAGE_DEFAULT
|
|
|
);
|
|
|
}
|
|
|
return (
|
|
|
process.env.RPA_STORAGE_STATE_PATH?.trim() || ANON_STORAGE_DEFAULT
|
|
|
);
|
|
|
}
|
|
|
|
|
|
export function resolveOutputBasename(mode: RecordMode, ts: string): string {
|
|
|
return mode === "logged-in"
|
|
|
? `mothership-logged-in-${ts}.js`
|
|
|
: `mothership-${ts}.js`;
|
|
|
}
|
|
|
|
|
|
export function shouldEnforceGd15(mode: RecordMode): boolean {
|
|
|
return mode === "anonymous";
|
|
|
}
|
|
|
|
|
|
function printHelp(): void {
|
|
|
console.log(`
|
|
|
用法:
|
|
|
npm run record:mothership [-- 起始URL]
|
|
|
npm run record:mothership -- --logged-in [起始URL]
|
|
|
npm run record:mothership -- --login [起始URL]
|
|
|
|
|
|
模式:
|
|
|
匿名(默认) 录制公开报价 widget;禁止 dashboard / www.mothership.com/quote(GD-15)
|
|
|
--logged-in 录制账号登录后查价完整路径;允许登录/dashboard 入口
|
|
|
|
|
|
产出(匿名):
|
|
|
.rpa/recordings/mothership-<时间戳>.js
|
|
|
.rpa/mothership-storage.json
|
|
|
|
|
|
产出(登录后):
|
|
|
.rpa/recordings/mothership-logged-in-<时间戳>.js
|
|
|
.rpa/mothership-logged-in-storage.json
|
|
|
|
|
|
登录后录制说明:
|
|
|
- 请在 codegen 浏览器中手动粘贴 MOTHERSHIP_EMAIL / MOTHERSHIP_PASSWORD(脚本不自动填)
|
|
|
- SOP: docs/rpa-mothership-logged-in-sop.md
|
|
|
|
|
|
匿名标准样例: LA 90001 → Dallas 75201
|
|
|
`);
|
|
|
}
|
|
|
|
|
|
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())}`;
|
|
|
}
|
|
|
|
|
|
function hasMothershipEnvCredentials(): boolean {
|
|
|
const email = process.env.MOTHERSHIP_EMAIL?.trim() ?? "";
|
|
|
const password = process.env.MOTHERSHIP_PASSWORD?.trim() ?? "";
|
|
|
return Boolean(email && password);
|
|
|
}
|
|
|
|
|
|
function main(): void {
|
|
|
const parsed = parseRecordArgs(process.argv);
|
|
|
|
|
|
if (parsed.help) {
|
|
|
printHelp();
|
|
|
process.exit(0);
|
|
|
}
|
|
|
|
|
|
const { mode, startUrl } = parsed;
|
|
|
const storagePath = resolveStoragePath(mode);
|
|
|
|
|
|
if (shouldEnforceGd15(mode) && startUrl && isForbiddenQuoteUrl(startUrl)) {
|
|
|
console.error("错误:禁止使用入口 URL(GD-15)");
|
|
|
console.error(
|
|
|
"请使用 headed 录制验证的匿名公开报价 URL;登录后路径请加 --logged-in",
|
|
|
);
|
|
|
process.exit(1);
|
|
|
}
|
|
|
|
|
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
|
fs.mkdirSync(path.dirname(storagePath), { recursive: true });
|
|
|
|
|
|
const outputFile = path.join(
|
|
|
OUTPUT_DIR,
|
|
|
resolveOutputBasename(mode, formatTimestamp()),
|
|
|
);
|
|
|
|
|
|
const args = [
|
|
|
"playwright",
|
|
|
"codegen",
|
|
|
"--target=javascript",
|
|
|
"-o",
|
|
|
outputFile,
|
|
|
`--save-storage=${storagePath}`,
|
|
|
];
|
|
|
|
|
|
if (fs.existsSync(storagePath)) {
|
|
|
args.push(`--load-storage=${storagePath}`);
|
|
|
console.log(`已加载已有 storageState: ${storagePath}`);
|
|
|
}
|
|
|
|
|
|
if (startUrl) {
|
|
|
args.push(startUrl);
|
|
|
} else if (mode === "logged-in") {
|
|
|
console.log(
|
|
|
"未指定起始 URL:请在 codegen 中导航至登录页或登录后查价页(允许 dashboard)",
|
|
|
);
|
|
|
} else {
|
|
|
console.log("未指定起始 URL:请在 codegen 浏览器中手动导航至匿名报价页");
|
|
|
}
|
|
|
|
|
|
if (mode === "logged-in") {
|
|
|
console.log("=== Mothership 登录后查价录制(headed)===");
|
|
|
console.log("RPA_HEADLESS=false");
|
|
|
console.log("模式: --logged-in(GD-15 黑名单不生效)");
|
|
|
if (hasMothershipEnvCredentials()) {
|
|
|
console.log(
|
|
|
"已检测到 .env 中 MOTHERSHIP_EMAIL/PASSWORD:请在浏览器中手动粘贴登录(脚本不自动填)",
|
|
|
);
|
|
|
} else {
|
|
|
console.log(
|
|
|
"未检测到 MOTHERSHIP_EMAIL/PASSWORD:请准备账号后在浏览器中手动登录",
|
|
|
);
|
|
|
}
|
|
|
console.log(`脚本产出: ${outputFile}`);
|
|
|
console.log(`storageState: ${storagePath}`);
|
|
|
console.log("SOP: docs/rpa-mothership-logged-in-sop.md");
|
|
|
} else {
|
|
|
console.log("=== Mothership RPA 录制(headed / 匿名)===");
|
|
|
console.log("RPA_HEADLESS=false");
|
|
|
console.log("禁止入口: dashboard.mothership.com / www.mothership.com/quote");
|
|
|
console.log(`脚本产出: ${outputFile}`);
|
|
|
console.log(`storageState: ${storagePath}`);
|
|
|
console.log("SOP: docs/rpa-mothership-logged-in-sop.md(登录后)");
|
|
|
}
|
|
|
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) => {
|
|
|
process.exit(code ?? 0);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// vitest 导入本模块时勿启动 codegen
|
|
|
const isRecordEntrypoint = /record-mothership\.(ts|js|mjs|cjs)$/.test(
|
|
|
process.argv[1] ?? "",
|
|
|
);
|
|
|
if (isRecordEntrypoint && !process.env.VITEST && !process.env.VITEST_WORKER_ID) {
|
|
|
main();
|
|
|
}
|