Hermes Agent 桌面端 Windows 崩溃问题排查实录:一个四重连锁故障的故事
前言
最近在使用 Hermes Agent(一个基于 Electron 的 AI Agent 桌面应用)时,遇到了一个棘手的问题:桌面端启动后窗口闪一下就消失,每次启动都需要手动运行修复工具,自动更新后问题还会循环复发。
经过深入排查,发现这不是一个单独的 bug,而是四个问题相互交织形成的死循环。整个排查过程涉及 Electron 底层机制、Windows 进程模型、Python 虚拟环境、以及 node-pty 终端模拟器等多个技术领域,记录下来供大家参考。
环境信息
| 项目 | 版本 |
|---|---|
| 操作系统 | Windows 10 Build 10.0.22621 |
| 显卡 | NVIDIA GeForce RTX 2060 |
| 显卡驱动 | 32.0.16.1062 (2026-06-11) |
| Hermes | v0.17.0 |
| Electron | 40.10.2 |
| Python | 3.11.0 (venv) |
问题现象
- 双击桌面快捷方式启动 Hermes,窗口闪一下就消失
- 之前有个修复工具(.bat),每次启动前都要跑一次
- Hermes 自动更新后,问题复发,修复工具也要重新跑
第一层:GPU 渲染崩溃
查看 desktop.log,第一条线索很明确:
ini
[hermes] [renderer] render-process-gone reason=crashed exitCode=-2147483645
exitCode=-2147483645 即 0x80000003,是 Windows 的断点异常(BREAKPOINT EXCEPTION)。这是 Electron 的 GPU 进程崩溃了。
之前的修复工具设置了这些环境变量:
batch
set ELECTRON_DISABLE_GPU=1
set ELECTRON_DISABLE_GPU_SANDBOX=1
set ELECTRON_NO_SANDBOX=1
看起来没问题对吧?但当我读了 main.cjs 的源码后发现------这些变量根本没被检查。
关键发现:正确的环境变量名
在 main.cjs 第 74 行,Hermes 检查的是:
javascript
const override = String(env2.HERMES_DESKTOP_DISABLE_GPU || "").trim().toLowerCase();
if (GPU_OVERRIDE_ON.has(override)) return "override (HERMES_DESKTOP_DISABLE_GPU)";
正确的变量名是 HERMES_DESKTOP_DISABLE_GPU,不是 ELECTRON_DISABLE_GPU。
但环境变量也不够用
即使设置了 HERMES_DESKTOP_DISABLE_GPU=1,它调用的是:
javascript
app.disableHardwareAcceleration();
app.commandLine.appendSwitch("disable-gpu-compositing");
实测发现,app.disableHardwareAcceleration() 对 RTX 2060 不够用 ,GPU 进程仍然崩溃。只有 --disable-gpu 这个 Chromium 命令行参数才能彻底阻止 GPU 进程启动。
bash
# 测试对比
HERMES_DESKTOP_DISABLE_GPU=1 ./Hermes.exe # GPU 仍然崩溃 ❌
./Hermes.exe --disable-gpu # GPU 不崩溃 ✅
这是 Electron/Chromium 层面的问题,disableHardwareAcceleration() 只是禁用了硬件加速合成,但 GPU 进程本身还是会启动。--disable-gpu 才是彻底禁用 GPU 进程的开关。
第二层:Bootstrap 标记文件缺失
GPU 崩溃只是第一个问题。即使 GPU 修好了,桌面端还是无法正常工作。
查看 main.cjs 的启动逻辑:
javascript
function resolveHermesBackend(dashboardArgs) {
// 第一步:检查 bootstrap 是否完成
if (isBootstrapComplete()) {
return createActiveBackend(dashboardArgs); // ✅ 正常路径
}
// 第二步:在 PATH 上找 hermes CLI
const hermesCommand = findOnPath("hermes");
if (hermesCommand) {
// 尝试探测 CLI 是否可用
if (verifyHermesCli(hermesCommand)) {
return { /* 正常后端 */ };
}
// 探测失败 → 认为需要 bootstrap
}
// 第三步:触发 bootstrap 流程
return { kind: "bootstrap-needed" };
}
isBootstrapComplete() 检查一个标记文件 .hermes-bootstrap-complete:
javascript
function isBootstrapComplete() {
const marker = readBootstrapMarker();
if (!marker || typeof marker !== "object") return false;
if (marker.schemaVersion !== 1) return false;
if (typeof marker.pinnedCommit !== "string" || marker.pinnedCommit.length < 7) return false;
return isHermesSourceRoot(ACTIVE_HERMES_ROOT) && fileExists(getVenvPython(VENV_ROOT));
}
这个标记文件不存在。 所以桌面端每次都走"从未完成初始化"的路径,触发 bootstrap 流程。
第三层:CLI 探测失败
标记文件不存在,代码降级到 PATH 探测。它在 PATH 上找到了 hermes.EXE:
css
[hermes] Ignoring existing Hermes CLI at C:\...\venv\Scripts\hermes.EXE:
--version probe failed; falling through to bootstrap.
有意思的是,我在终端里直接运行 hermes.EXE --version 完全正常。但在 Electron 进程内探测失败。
查看探测代码:
javascript
function verifyHermesCli(hermesCommand, opts = {}) {
try {
execFileSync(hermesCommand, ["--version"], {
stdio: "ignore",
timeout: 5000, // 5秒超时
shell: false,
windowsHide: true
});
return true;
} catch {
return false; // 超时或异常都返回 false
}
}
探测失败的可能原因:
- Electron 进程的 DLL 搜索路径与终端不同
- Python shim(hermes.exe)加载 Python DLL 时环境差异
- 5秒超时在某些情况下不够
第四层:更新死循环
当 CLI 探测也失败后,代码触发 handOffWindowsBootstrapRecovery():
javascript
async function handOffWindowsBootstrapRecovery(reason) {
const updater = resolveUpdaterBinary();
const child = spawn(updater, ["--update", "--branch", "main"], {
detached: true,
stdio: "ignore",
windowsHide: false // 注意:这里是 false!
});
child.unref();
setTimeout(() => app.quit(), UPDATE_HANDOFF_DWELL_MS);
return true;
}
这个函数做了两件事:
- 启动
hermes-setup.exe --update(一个终端程序,windowsHide: false,所以会弹黑窗口) - 退出桌面端
hermes-setup.exe 更新完成后会重启桌面端,但不带 --disable-gpu 参数,于是 GPU 又崩了,又触发探测失败,又触发 bootstrap......死循环形成。
arduino
GPU 崩溃 → CLI 探测失败 → 触发 bootstrap
→ hermes-setup.exe 弹出(闪一下的黑窗口)
→ 更新完成 → 重启桌面端(不带 --disable-gpu)
→ GPU 又崩 → 循环 ♻️
额外发现:node-pty 的 CMD 窗口闪烁
解决了上述四个问题后,桌面端能正常启动了。但又发现:每次 agent 执行终端命令时,都会有一个 CMD 黑窗口闪烁一下。
排查发现是 node-pty(Electron 内嵌的终端模拟器)在 Windows 上的行为:
javascript
const ptyProcess = nodePty.spawn(command, args, {
cols, cwd, env: terminalShellEnv(),
name: "xterm-256color", rows
});
node-pty 在 Windows 上使用 conpty(Windows Console API)创建伪终端,每次都会创建一个 conhost.exe 进程,表现为短暂的 CMD 窗口闪烁。
这是 node-pty 的已知限制------windowsHide: true 对它无效,因为它不走 Node.js 的 child_process.spawn,而是直接调用 Windows Console API。
修复方案
1. 创建 Bootstrap 标记文件
json
{
"schemaVersion": 1,
"pinnedCommit": "7cd5eaa646f1...",
"pinnedBranch": "main",
"completedAt": "2026-06-26T08:50:00.000Z",
"desktopVersion": "0.17.0"
}
放到 C:\Users\cj\AppData\Local\hermes\hermes-agent\.hermes-bootstrap-complete。
2. 用 VBScript 启动隐藏窗口
vbscript
Set WshShell = CreateObject("WScript.Shell")
WshShell.Run """...\Hermes.exe"" --disable-gpu --disable-software-rasterizer --no-sandbox --no-update-check", 0, False
WshShell.Run 的第二个参数 0 表示完全隐藏窗口。
3. 设置正确的环境变量(保底)
powershell
# 唯一被代码检查的变量
[Environment]::SetEnvironmentVariable('HERMES_DESKTOP_DISABLE_GPU', '1', 'User')
4. 清理无效的环境变量
之前的修复工具设置了 9 个代码根本不检查的变量,全部删除。
排查过程中的收获
1. 不要盲信环境变量名
ELECTRON_DISABLE_GPU 看起来很官方,但 Hermes 检查的是 HERMES_DESKTOP_DISABLE_GPU。读源码比猜变量名靠谱。
2. app.disableHardwareAcceleration() 和 --disable-gpu 不是一回事
前者只禁用硬件加速合成,GPU 进程仍然会启动;后者才彻底禁用 GPU 进程。对某些显卡来说,只有后者才有效。
3. 日志时间戳很重要
desktop.log 最后写入时间是 6 月 21 日,但问题发生在 6 月 26 日。说明新版本的崩溃比旧版本更严重------连日志都来不及写就崩了。
4. 检查标记文件和状态文件
很多应用用标记文件记录初始化状态。当出现"重复初始化"的问题时,先检查标记文件是否存在。
5. node-pty 在 Windows 上会弹窗
如果 Electron 应用内嵌了终端功能,node-pty 在 Windows 上会短暂显示 CMD 窗口。这是底层限制,需要用 CREATE_NO_WINDOW 标志或替代方案解决。
已提交 Issue
这个问题已向 Hermes 上游提交: github.com/NousResearc...
排查这类问题的核心思路:不要假设,要验证。 每一个"看起来对"的修复,都要实际测试确认它真的生效了。尤其是环境变量、启动参数这类配置,写错了不会报错,只会默默无效。