修复外部浏览器 CDP 无响应: ws 从主进程 bundle 外部化

根因: 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 已产出。
This commit is contained in:
yu 2026-11-04 17:19:32 +08:00
parent 3a9a8b63c2
commit 22a7ee1595
8 changed files with 29 additions and 11 deletions

View File

@ -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" },

View File

@ -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") },

View File

@ -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": {

View File

@ -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)
};
}

View File

@ -60,7 +60,11 @@ export class ExternalTransport implements CdpTransport {
readonly description: string
) {
this.openPromise = new Promise<void>((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)));

View File

@ -1,5 +1,5 @@
{
"generatedAt": "2026-11-04T04:34:06.782Z",
"generatedAt": "2026-11-04T09:12:00.449Z",
"count": 909,
"ids": [
"App_GetPid",

View File

@ -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;

View File

@ -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",