You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

132 lines
3.9 KiB

/**
* 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<void> {
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<HumanAssistEvent[]> {
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<string>();
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;
};