server-host.js
Rama main · réplica de lectura del repositorio original.
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (c) 2026 开发者猫叔 (DeveloperCatUncle) · 商业使用需授权:COMMERCIAL-LICENSE.md
"use strict";
/**
* 独立服务进程的入口:electron-main.js 用 utilityProcess.fork 拉起它,它再 require server.js。
*
* 2026-09-29 审计:几个对话一起跑时主进程事件循环 p99 68ms、最长 330ms——SSE 推流、工具调用、
* 大 JSON 解析跟窗口重绘抢同一条线程,用户原话「开几个对话整个应用连带电脑都卡」。
* 服务端挪到这里以后,主进程只剩窗口、托盘、菜单。
*
* 以前 server.js 在主进程里 require,它跟壳之间靠几根全局变量说话(__wbOnListen、__wbBootFail、
* __wbWin、__openworkbuddyPet、__wbRegisterShortcuts)。这里把同名的几根接上,另一头换成
* 发消息给主进程——server.js 一行不用知道自己跑在哪个进程里。
*
* 这个文件不在 require 链上(主进程按路径 fork),装机包白名单靠 scripts/check-package-files.js 的 ENTRIES 盯着。
*/
const path = require("path");
const bridge = require("./src/platform/electron-bridge");
let LISTENING = false;
let STOPPING = false;
const errText = (e) => String((e && (e.stack || e.message)) || e);
/** 起不来:原因交回主进程(它会退回在自己进程里跑,那边会把同一个错画在窗口上) */
function bootFail(e) {
console.error("[服务进程] 启动失败:", errText(e));
if (!bridge.send({ t: "boot", ok: false, error: bridge.serializeError(e) })) process.exit(1);
}
// 起来之前的异常 = 启动失败;起来之后的,照主进程里跑时的规矩只记一笔(electron-main.js 的 fatal 过了 PAGE_UP 也只记日志)。
// 不接的话 utilityProcess 默认直接退出——几个正在跑的对话一起断,比以前在主进程里还脆
process.on("uncaughtException", (e) => {
if (!LISTENING) return bootFail(e);
console.error("[服务进程] 未捕获异常(已记下,服务继续):", errText(e));
});
process.on("unhandledRejection", (e) => {
if (!LISTENING) return bootFail(e);
console.error("[服务进程] 有个没人接的 Promise 拒绝:", String((e && /** @type {any} */ (e).message) || e));
});
// ---------- 退出:先把自己底下的子进程送走,再走 server.js 自己的 SIGINT 收尾(停 MCP) ----------
/** posix:ps 列一遍,自己底下的子孙(连同自立门户的进程组)先 SIGTERM,半秒后还在的 SIGKILL */
async function reapDescendants() {
if (process.platform === "win32") return 0; // Windows 上由主进程按父进程号 taskkill /T(electron-main.js killChildren)
const cp = require("child_process");
const list = () => {
// 不加 windowsHide:Windows 在函数开头就 return 了
const r = cp.spawnSync("ps", ["-A", "-o", "pid=,ppid=,pgid="], { encoding: "utf8", timeout: 5000 });
const rows = [];
for (const line of String(r.stdout || "").split("\n")) {
const m = /^\s*(\d+)\s+(\d+)\s+(\d+)/.exec(line);
if (m) rows.push({ pid: +m[1], ppid: +m[2], pgid: +m[3] });
}
const mine = new Set([process.pid]);
for (let grew = true; grew;) {
grew = false;
for (const x of rows) if (!mine.has(x.pid) && mine.has(x.ppid) && x.pid !== r.pid) { mine.add(x.pid); grew = true; }
}
mine.delete(process.pid);
return { pids: [...mine], groups: rows.filter((x) => mine.has(x.pid) && x.pgid === x.pid).map((x) => x.pid) };
};
const hit = (sig) => {
const { pids, groups } = list();
for (const g of groups) { try { process.kill(-g, sig); } catch {} }
for (const p of pids) { try { process.kill(p, sig); } catch {} }
return pids.length;
};
const n = hit("SIGTERM");
if (n) { await new Promise((r) => setTimeout(r, 500)); hit("SIGKILL"); }
return n;
}
function stop(why) {
if (STOPPING) return;
STOPPING = true;
// 兜底:收尾本身卡住也得退,主进程那边等不到还会再杀一次
setTimeout(() => process.exit(0), 3000);
console.log(`[服务进程] 收到退出(${why}),收尾中`);
reapDescendants()
.catch(() => 0)
.then(() => {
if (process.listenerCount("SIGINT") > 0) process.emit(/** @type {any} */ ("SIGINT"));
else process.exit(0);
});
}
process.on("SIGTERM", () => stop("SIGTERM"));
bridge.onCtl((op) => { if (op === "shutdown") stop("主进程要求"); });
// ---------- 数据根对账 ----------
// 装机版的数据根要靠「是不是装机包」来定,而 utilityProcess 里 app.isPackaged 问不到(paths.js 改用 OWB_PACKAGED)。
// 两边算出来的不一样就是会读错账号、会话的那种错——不将就,交回主进程在它自己那儿跑
const dataDir = path.resolve(require("./src/platform/paths").DATA_DIR);
const shellDataDir = process.env.OWB_DATA_DIR ? path.resolve(process.env.OWB_DATA_DIR) : dataDir;
if (dataDir !== shellDataDir) {
bootFail(new Error(`服务进程算出来的数据目录(${dataDir})跟主进程的(${shellDataDir})不一样`));
setTimeout(() => process.exit(1), 200);
} else {
start();
}
function start() {
// 测试用的故障开关(test/server-process.js 验退回主进程那条路)。平时不设,不影响任何行为
const fault = process.env.OWB_SERVER_FAULT || "";
if (fault === "crash-on-boot") { console.error("[服务进程] OWB_SERVER_FAULT=crash-on-boot,按测试要求直接退出"); process.exit(3); }
if (fault === "no-listen") { console.error("[服务进程] OWB_SERVER_FAULT=no-listen,按测试要求不启动服务"); setInterval(() => {}, 60000); return; }
global.__wbBootFail = (e) => bootFail(e);
global.__wbOnListen = (port, meta) => {
LISTENING = true;
bridge.send({ t: "listening", port, reused: !!(meta && meta.reused) });
};
global.__wbRegisterShortcuts = (s) => { bridge.notify("shortcuts.register", s || {}); };
// 主窗口:server.js 只用它做全屏切换(/api/app/fullscreen)。全屏状态主进程一变就推过来
const winProxy = {
isDestroyed: () => !bridge.state().winAlive,
isFullScreen: () => !!bridge.state().fullScreen,
setFullScreen: (on) => { bridge.notify("win.setFullScreen", { on: !!on }); },
};
const syncWin = (s) => { global.__wbWin = s && s.winAlive ? winProxy : null; };
syncWin(bridge.state());
bridge.onState(syncWin);
// 桌面宠物活在主进程里(它是个窗口)。server.js 用到的就这几个动作,全是「做了就行」
if (bridge.caps().pet) {
global.__openworkbuddyPet = {
setState: (state, text) => { bridge.notify("pet.setState", { state, text }); },
alertAsk: (question) => { bridge.notify("pet.alertAsk", { question }); },
clearAsk: (stillWorking) => { bridge.notify("pet.clearAsk", { stillWorking: !!stillWorking }); },
applyConfig: (c) => { bridge.notify("pet.applyConfig", c || {}); },
show: () => { bridge.notify("pet.show"); },
hide: () => { bridge.notify("pet.hide"); },
isVisible: () => false,
destroy: () => {},
};
}
// 审批提醒(窗口没聚焦时 Dock 跳一下):审批队列在这个进程里,提醒的手在主进程
try {
require("./src/core/safety/security").watchApprovals((m) => {
if (m && m.type === "open") bridge.notify("approval.open", { type: "open" });
});
} catch {}
bridge.send({ t: "hello", pid: process.pid, dataDir, packaged: process.env.OWB_PACKAGED === "1" });
require(path.join(__dirname, "server.js"));
}