import { spawn, spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; const root = process.cwd(); const debugPort = 9231; const fixturePort = 17963; const profile = fs.mkdtempSync(path.join(os.tmpdir(), "rpa-studio-e2e-")); const candidates = [process.env.RPA_ELECTRON_DIST, "C:/electron-rpa/dist", path.join(root, "node_modules", "electron", "dist")].filter(Boolean); const dist = candidates.find((candidate) => fs.existsSync(path.join(candidate, "electron.exe"))); if (!dist) throw new Error("找不到 Electron 运行时,请设置 RPA_ELECTRON_DIST"); const env = { ...process.env, RPA_E2E: "1", RPA_FIXTURE_PORT: String(fixturePort) }; delete env.ELECTRON_RUN_AS_NODE; const child = spawn(path.join(dist, "electron.exe"), [root, "--no-sandbox", `--remote-debugging-port=${debugPort}`, `--user-data-dir=${profile}`], { cwd: root, env, stdio: ["ignore", "pipe", "pipe"], windowsHide: true }); let stderr = ""; child.stderr.on("data", (chunk) => { stderr += String(chunk); }); async function waitForTarget() { const deadline = Date.now() + 30000; while (Date.now() < deadline) { if (child.exitCode !== null) throw new Error(`RPA Studio 提前退出:${child.exitCode}\n${stderr}`); try { const targets = await (await fetch(`http://127.0.0.1:${debugPort}/json/list`)).json(); const page = targets.find((item) => item.type === "page" && item.title === "RPA Studio"); if (page) return page; } catch { /* wait for CDP */ } await new Promise((resolve) => setTimeout(resolve, 200)); } throw new Error(`等待 RPA Studio CDP 超时\n${stderr}`); } async function evaluate(page, expression) { const socket = new WebSocket(page.webSocketDebuggerUrl); await new Promise((resolve, reject) => { socket.onopen = resolve; socket.onerror = reject; }); const result = await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error("E2E Runtime.evaluate 超时")), 60000); socket.onmessage = (event) => { const message = JSON.parse(event.data); if (message.id !== 1) return; clearTimeout(timer); if (message.error) reject(new Error(JSON.stringify(message.error))); else if (message.result?.exceptionDetails) reject(new Error(message.result.exceptionDetails.text || "E2E 页面异常")); else resolve(message.result?.result?.value); }; socket.send(JSON.stringify({ id: 1, method: "Runtime.evaluate", params: { expression, returnByValue: true, awaitPromise: true } })); }); socket.close(); return result; } const expression = `(async () => { const flow = await window.rpa.flows.createDemo(); const events = []; const off = window.rpa.run.onEvent((event) => events.push(event)); const started = await window.rpa.run.start(flow); const finished = await new Promise((resolve, reject) => { const deadline = Date.now() + 45000; const timer = setInterval(() => { const event = events.find((item) => item.type === 'run.finished'); if (event) { clearInterval(timer); resolve(event); } else if (Date.now() > deadline) { clearInterval(timer); reject(new Error('demo flow timeout')); } }, 100); }); off(); await window.rpa.flows.delete(flow.id); const result = events.find((item) => item.type === 'node.finished' && item.nodeId === 'node_result'); return { runId: started.runId, status: finished.status, durationMs: finished.durationMs, result: result?.output, failed: events.filter((item) => item.type === 'node.finished' && item.status === 'failed') }; })()`; try { const page = await waitForTarget(); const result = await evaluate(page, expression); if (result?.status !== "succeeded") throw new Error(`演示流程失败:${JSON.stringify(result)}`); if (!String(result?.result || "").includes("订单创建成功:RPA 客户,金额 128")) { throw new Error(`演示结果不正确:${JSON.stringify(result)}`); } if (result.failed?.length) throw new Error(`存在失败节点:${JSON.stringify(result.failed)}`); console.log(JSON.stringify({ ok: true, ...result }, null, 2)); } finally { if (process.platform === "win32") spawnSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" }); else child.kill("SIGKILL"); await new Promise((resolve) => setTimeout(resolve, 500)); try { fs.rmSync(profile, { recursive: true, force: true, maxRetries: 12, retryDelay: 250 }); } catch (error) { console.warn(`E2E 临时目录稍后由系统清理:${profile}`, (error).message); } }