From 22a7ee159550710c3a1b3d7e970988502b371ad9 Mon Sep 17 00:00:00 2001 From: yu <1919727607@qq.com> Date: Wed, 4 Nov 2026 17:19:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=A4=96=E9=83=A8=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=20CDP=20=E6=97=A0=E5=93=8D=E5=BA=94:=20ws=20?= =?UTF-8?q?=E4=BB=8E=E4=B8=BB=E8=BF=9B=E7=A8=8B=20bundle=20=E5=A4=96?= =?UTF-8?q?=E9=83=A8=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: ws 被打进主进程 bundle 时, 其可选依赖 bufferutil 的打包解析产物 破坏了客户端发帧的 mask fallback; permessage-deflate 协商开启时错误被 压缩路径静默吞掉 —— 表现为 ws 握手成功但所有 CDP 命令零字节发出、 无响应(输入文本节点 10s 超时), dev 与打包版皆受影响。 - electron.vite.config: externalizeDepsPlugin include ws, 运行时 require - package.json: ws 提升到根依赖, electron-builder 随包收集 - electron-builder.config: 确认 node_modules/ws 进入 asar - transport: 显式 perMessageDeflate:false(双保险, 附注释) - package.mjs: verifyLaunch 剥离 ELECTRON_RUN_AS_NODE/NODE_OPTIONS, 防止外部工具链环境把打包应用带进纯 Node 模式(bad option 退出码 9) - flows: 演示流程支持 RPA_DEMO_BROWSER 强制指定浏览器(Chrome 缺装回退 Edge) 验证: 全量测试 349 绿 + 浏览器 e2e 绿 + portable/NSIS 双冒烟通过, 安装包/zip/SHA256 已产出。 --- electron-builder.config.mjs | 5 ++++- electron.vite.config.ts | 4 +++- package.json | 1 + packages/rpa-core/src/browser/external.ts | 2 +- packages/rpa-core/src/browser/transport.ts | 6 +++++- resources/capability-snapshot.json | 2 +- scripts/package.mjs | 17 +++++++++++------ src/main/flows.ts | 3 +++ 8 files changed, 29 insertions(+), 11 deletions(-) diff --git a/electron-builder.config.mjs b/electron-builder.config.mjs index 9b4f704..ffaccb7 100644 --- a/electron-builder.config.mjs +++ b/electron-builder.config.mjs @@ -18,7 +18,10 @@ export default { }, files: [ "out/**/*", - "package.json" + "package.json", + // ws 已从主进程 bundle 外部化(运行时 require),必须随包发布; + // ws 为零依赖纯 JS 库,asar 内即可解析 + "node_modules/ws/**/*" ], extraResources: [ { from: path.join(root, "resources", "catalog.json"), to: "catalog.json" }, diff --git a/electron.vite.config.ts b/electron.vite.config.ts index de253f4..a26c507 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -4,7 +4,9 @@ import react from "@vitejs/plugin-react"; export default defineConfig({ main: { - plugins: [externalizeDepsPlugin()], + // ws 必须外部化运行时 require:打包进 bundle 时其可选原生依赖 bufferutil 的解析产物 + // 会破坏 mask fallback(客户端发帧静默丢字节 / 报 mask is not a function) + plugins: [externalizeDepsPlugin({ include: ["ws"] })], resolve: { alias: [ { find: "@shared", replacement: resolve("src/shared") }, diff --git a/package.json b/package.json index 65468e4..8bc97fc 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "react-dom": "^18.3.1", "sm-crypto": "^0.5.8", "tesseract.js": "^7.0.0", + "ws": "^8.22.0", "zustand": "^5.0.2" }, "devDependencies": { diff --git a/packages/rpa-core/src/browser/external.ts b/packages/rpa-core/src/browser/external.ts index 900adb1..775ebf9 100644 --- a/packages/rpa-core/src/browser/external.ts +++ b/packages/rpa-core/src/browser/external.ts @@ -302,7 +302,7 @@ export async function openNewPageTarget(httpBase: string, url = "about:blank"): } return { id: String(target.id ?? ""), - webSocketDebuggerUrl: target.webSocketDebuggerUrl, + webSocketDebuggerUrl: String(target.webSocketDebuggerUrl), url: String(target.url ?? url) }; } diff --git a/packages/rpa-core/src/browser/transport.ts b/packages/rpa-core/src/browser/transport.ts index 1dfcb1c..1893db4 100644 --- a/packages/rpa-core/src/browser/transport.ts +++ b/packages/rpa-core/src/browser/transport.ts @@ -60,7 +60,11 @@ export class ExternalTransport implements CdpTransport { readonly description: string ) { this.openPromise = new Promise((resolve, reject) => { - const ws = new WebSocket(wsUrl, { maxPayload: 256 * 1024 * 1024 }); + // perMessageDeflate 必须显式关闭:ws 客户端默认开启压缩协商后,若 ws 被打包进 + // 主进程 bundle,其可选依赖 bufferutil 的解析产物会破坏 mask fallback, + // 压缩发送路径的错误又被静默吞掉 —— 表现为帧零字节写出且无任何报错。 + // (ws 已在 electron.vite.config 外部化运行时 require,此项为双保险) + const ws = new WebSocket(wsUrl, { maxPayload: 256 * 1024 * 1024, perMessageDeflate: false }); this.ws = ws; ws.on("open", () => resolve()); ws.on("message", (data: unknown) => this.handleMessage(String(data))); diff --git a/resources/capability-snapshot.json b/resources/capability-snapshot.json index 8e7d48a..55dad12 100644 --- a/resources/capability-snapshot.json +++ b/resources/capability-snapshot.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-11-04T04:34:06.782Z", + "generatedAt": "2026-11-04T09:12:00.449Z", "count": 909, "ids": [ "App_GetPid", diff --git a/scripts/package.mjs b/scripts/package.mjs index 4f23244..6808e45 100644 --- a/scripts/package.mjs +++ b/scripts/package.mjs @@ -46,16 +46,21 @@ function removeTree(target) { async function verifyLaunch(executable, label) { const userData = path.join(os.tmpdir(), `RPA-Studio-UserData-${Date.now()}-${Math.random().toString(16).slice(2)}`); + // ELECTRON_RUN_AS_NODE / NODE_OPTIONS 来自外部工具链环境时会把打包应用带进纯 Node 模式 + // (表现为「bad option」退出码 9),冒烟必须剥离这些不属于应用的控制变量 + const env = { + ...process.env, + RPA_SKIP_SINGLE_INSTANCE: "1", + RPA_E2E: "1", + RPA_FIXTURE_PORT: String(18000 + Math.floor(Math.random() * 1000)) + }; + delete env.ELECTRON_RUN_AS_NODE; + delete env.NODE_OPTIONS; const child = spawn(executable, [`--user-data-dir=${userData}`], { cwd: path.dirname(executable), stdio: "ignore", windowsHide: false, - env: { - ...process.env, - RPA_SKIP_SINGLE_INSTANCE: "1", - RPA_E2E: "1", - RPA_FIXTURE_PORT: String(18000 + Math.floor(Math.random() * 1000)) - } + env }); await delay(8000); const exited = child.exitCode !== null || child.signalCode !== null; diff --git a/src/main/flows.ts b/src/main/flows.ts index d8029ea..c673f3b 100644 --- a/src/main/flows.ts +++ b/src/main/flows.ts @@ -74,6 +74,9 @@ export function createFlow(name = "未命名流程"): FlowDefinition { /** 演示流程浏览器类型:优先 Chrome,未安装时回退 Edge;两者都缺失则保持 chrome 由能力层给出友好错误 */ function demoBrowserType(): string { + // RPA_DEMO_BROWSER 可强制指定(e2e/诊断用) + const forced = String(process.env.RPA_DEMO_BROWSER || "").trim().toLowerCase(); + if (forced === "chrome" || forced === "edge") return forced; const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"); const chromePaths = [ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",