Ctrl K
ComunidadGuías…

server-host.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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"));
}