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