#!/usr/bin/env tsx /** * Flock 人机协作录制 — 有头浏览器 + 分步暂停,便于熟悉流程与截图 * * 用法: * npm run visual:flock-human-assist * $env:FLOCK_PATH_ID="F02-register-after-quote"; npm run visual:flock-human-assist * * 操作: * - 浏览器打开后按 Playwright Inspector 提示继续 * - 每步 pause 时人工点击/填写,熟悉按钮位置 * - Ctrl+C 结束仍会保存 storageState 与截图目录 */ import fs from "node:fs"; import path from "node:path"; import { chromium } from "playwright"; import { DEFAULT_FLOCK_DEMO, DEFAULT_FLOCK_QUOTE_URL, FLOCK_CANONICAL_PATHS, formatFlockRecordingChecklist, type FlockDemoInput, } from "@/workers/rpa/flock/demo-input"; import { getFlockStorageStatePath } from "@/lib/flock/env"; const ROOT = process.cwd(); function formatTimestamp(): string { const d = new Date(); 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())}`; } async function screenshotStep( outDir: string, page: import("playwright").Page, name: string, ): Promise { const file = path.join(outDir, `${name}.png`); await page.screenshot({ path: file, fullPage: true }); console.log(`[截图] ${file}`); } async function main(): Promise { const pathId = process.env.FLOCK_PATH_ID?.trim() ?? "F01-ltl-zip-quote"; const canonical = FLOCK_CANONICAL_PATHS.find((p) => p.id === pathId); const demo: FlockDemoInput = canonical?.input ?? DEFAULT_FLOCK_DEMO; const outDir = path.join(ROOT, ".rpa", "recordings", `flock-human-${pathId}-${formatTimestamp()}`); fs.mkdirSync(outDir, { recursive: true }); const checklistPath = path.join(outDir, "checklist.txt"); fs.writeFileSync(checklistPath, formatFlockRecordingChecklist(demo), "utf8"); console.log(`\n=== Flock 人机协作录制 ===`); console.log(`路径: ${pathId}${canonical ? ` — ${canonical.title}` : ""}`); if (canonical?.notes) console.log(`说明: ${canonical.notes}`); console.log(`产出目录: ${outDir}`); console.log(`\n${formatFlockRecordingChecklist(demo)}\n`); const statePath = getFlockStorageStatePath(); fs.mkdirSync(path.dirname(statePath), { recursive: true }); const browser = await chromium.launch({ headless: false, slowMo: Number(process.env.RPA_SLOW_MO_MS ?? 80), }); const context = await browser.newContext( fs.existsSync(statePath) ? { storageState: statePath } : {}, ); const page = await context.newPage(); const saveAll = async () => { try { await context.storageState({ path: statePath }); console.log(`[storage] ${statePath}`); } catch { /* ignore */ } }; process.on("SIGINT", async () => { console.log("\n[中断] 保存会话…"); await saveAll(); await browser.close(); process.exit(130); }); try { await page.goto(DEFAULT_FLOCK_QUOTE_URL, { waitUntil: "domcontentloaded", timeout: 60_000, }); await screenshotStep(outDir, page, "00-entry"); console.log("\n【步骤 1】请在浏览器中填写取货日期、ZIP、托盘、总重、尺寸"); console.log("参考值见上方清单;填完后在 Inspector 点 Resume"); await page.pause(); await screenshotStep(outDir, page, "01-form-filled"); console.log("\n【步骤 2】点击 Next / 下一个,等待进入报价或注册页"); await page.pause(); await screenshotStep(outDir, page, "02-after-next"); console.log("\n【步骤 3】若出现两档报价,录制完成(勿点 Complete your order)"); console.log("若出现 Create your free account,请填写测试账号并点 Get instant quote"); await page.pause(); await screenshotStep(outDir, page, "03-final"); await saveAll(); console.log("\n完成。可对照截图 + checklist,再运行 npm run record:flock 生成 codegen 脚本"); } finally { await browser.close(); } } main().catch((error) => { console.error(error); process.exit(1); });