/** * Priority1 人机协作 — 记录人工在浏览器内的操作(至报价页) */ import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import type { Page } from "playwright"; const BROWSER_RECORDER_JS = join( dirname(fileURLToPath(import.meta.url)), "human-assist-recorder.browser.js", ); export type HumanAssistEvent = { ts: number; kind: "click" | "input" | "change"; tag: string; id: string; name: string; inputType: string; value: string; label: string; text: string; selector: string; url: string; }; export async function installHumanAssistRecorder(page: Page): Promise { const script = readFileSync(BROWSER_RECORDER_JS, "utf8"); await page.addInitScript({ content: script }); await page.evaluate(script).catch(() => undefined); } export async function drainHumanAssistEvents(page: Page): Promise { if (page.isClosed()) return []; return page.evaluate("window.__p1HumanEvents || []"); } /** 人工已验证报价但 DOM 事件未捕获时,用清单生成可回放脚本 */ export function buildAccessorialLabelPlaywright(labels: readonly string[]): string { const events: HumanAssistEvent[] = labels.map((label) => ({ ts: 0, kind: "click", tag: "label", id: "", name: "", inputType: "checkbox", value: "", label, text: label, selector: "", url: "", })); return formatHumanEventsAsPlaywright(events); } export function formatHumanEventsAsPlaywright(events: HumanAssistEvent[]): string { const lines: string[] = [ "const { chromium } = require('playwright');", "", "(async () => {", " const browser = await chromium.launch({ headless: false });", " const page = await browser.newPage();", " // --- 人工补录步骤(由 human-assist-recorder 生成) ---", ]; const seen = new Set(); for (const ev of events) { if (ev.kind === "click" || ev.kind === "change") { const accessorial = ev.label || ev.text; if ( accessorial && /Liftgate|Inside|Limited Access|Residential|Delivery Appointment|Call Before|Hazardous/i.test( accessorial, ) ) { const key = `label:${accessorial.slice(0, 60)}`; if (!seen.has(key)) { seen.add(key); lines.push( ` await page.locator('#container-mh').getByLabel(${JSON.stringify(accessorial)}, { exact: false }).click({ force: true });`, ); continue; } } } if (ev.kind === "click") { if (ev.selector) { const key = `sel:${ev.selector}`; if (seen.has(key)) continue; seen.add(key); lines.push(` await page.locator('#container-mh ${ev.selector}').click({ force: true });`); } else if (ev.text && ev.text.length < 40) { const key = `text:${ev.text}`; if (seen.has(key)) continue; seen.add(key); lines.push( ` await page.locator('#container-mh').getByText(${JSON.stringify(ev.text)}).first().click({ force: true });`, ); } continue; } if ((ev.kind === "change" || ev.kind === "input") && ev.selector && ev.value) { if (ev.tag === "select") { lines.push( ` await page.locator('#container-mh ${ev.selector}').selectOption(${JSON.stringify(ev.value)});`, ); } else if (ev.inputType !== "checkbox") { lines.push( ` await page.locator('#container-mh ${ev.selector}').fill(${JSON.stringify(ev.value)});`, ); } } } lines.push(" await page.locator('#container-mh').getByRole('button', { name: /GET INSTANT QUOTES/i }).click({ force: true });"); lines.push("})();"); return lines.join("\n"); } export type HumanAccessorialRecording = { pathId: string; title: string; recordedAt: string; expectedAccessorials: readonly string[]; events: HumanAssistEvent[]; playwrightScript: string; };