|
|
#!/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<void> {
|
|
|
const file = path.join(outDir, `${name}.png`);
|
|
|
await page.screenshot({ path: file, fullPage: true });
|
|
|
console.log(`[截图] ${file}`);
|
|
|
}
|
|
|
|
|
|
async function main(): Promise<void> {
|
|
|
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);
|
|
|
});
|