首页
/ Hermes Desktop 技术指南:Hermes Agent 原生桌面客户端的安装、架构与开发实践

Hermes Desktop 技术指南:Hermes Agent 原生桌面客户端的安装、架构与开发实践

2026-09-05 20:55:53作者:侯霆垣

Hermes Desktop 是 Hermes Agent 的原生桌面应用,与 CLI 和 Gateway 共享同一套 agent、技能与记忆,在原生窗口中提供流式工具输出、并排预览、文件浏览器、语音和设置管理,覆盖 macOS、Windows 与 Linux。本文基于仓库中 apps/desktop/README.md 的核心内容成文,并结合 electron 主进程源码后端命令解析模块工程规范文档 深入讲解其三层架构、后端解析梯队(backend resolution ladder)、远程网关连接配置与完整开发工作流。读完本文,你既能完成 Desktop 的部署与更新,也能理解它如何找到并拉起 headless 后端、如何安全地切换本地/远程连接,以及如何在本地跑通开发与打包验证链路。

Hermes Desktop 界面:左侧会话栏按 Cron、Telegram、Discord、WebUI 等来源分组展示会话列表,右侧为主聊天区

一、Desktop 是什么:同一个 Agent,原生窗口

README 对 Hermes Desktop 的定位非常明确:它是 Hermes Agent 的原生桌面壳(native desktop shell),而不是浏览器 dashboard 的套壳,也不内嵌 TUI。核心能力如下(继承自原文档的功能表):

  • 完整 Agent 对话:流式回复、实时工具活动、结构化工具摘要,与其他 Hermes 界面共享同一份会话历史;
  • 并排预览:在右侧面板中渲染网页、文件与工具输出,同时保持聊天不中断;
  • 文件浏览器:不离开应用即可浏览和预览工作目录;
  • 语音:与 Hermes 语音对话并听到回复(macOS 权限文案见 package.jsonNSMicrophoneUsageDescriptionextendInfo 字段);
  • 设置与引导:通过真实 UI 管理 provider、模型、工具与凭据,首次运行引导可在数秒内到达第一条消息;
  • 自动保持最新:内置更新机制拉取最新 agent 并就地重建应用。

从源码结构看,这三者被严格拆分为三个责任方,各自对一件事负权威责任(见 AGENTS.md):

  • Electron 拥有"机器":进程生命周期、原生文件系统/git/窗口能力、安装与更新,以及一个窄而类型化的 preload 能力桥;
  • 渲染层(React) 拥有体验:Desktop 路由、面板、交互状态,以及基于 @assistant-ui/react 的 transcript;
  • Hermes Agent 后端 拥有工作本身:会话、工具、模型调用、流式输出。它以 headless 的 hermes serve 进程运行,暴露 tui_gateway JSON-RPC/WebSocket API;渲染层通过共享传输层连接该 API(README 指出该共享层即 apps/shared,浏览器 dashboard 亦复用同一层)。

这一"三个边界"的划分在 package.json 的技术选型上也能得到印证:Electron 40.10.2、React 19.2.7、@assistant-ui/react 0.14.24(对话 UI)、@tanstack/react-query + nanostores(请求缓存与轻量共享状态)、@xterm/*(内嵌终端)、simple-git(原生 git 能力)、node-pty(伪终端)。渲染层从不直接触碰 Node/Electron,原生能力一律经由有意的 capability 桥暴露——当某次改动模糊了这个接缝,就应当修接缝而不是扩大接缝。

二、安装与更新

2.1 用 Hermes CLI 安装(推荐)

已有 Hermes CLI 的用户只需一条命令:

hermes desktop

它会基于现有安装构建并启动 GUI——同一份配置、密钥、会话与技能。如果 Desktop 找不到可用的运行时或已保存的远程连接,首次启动会先让你连接到一个已存在的 Hermes Gateway,或者在本地安装 Hermes;本地安装引导随后带你完成 provider 与模型选择。

2.2 预编译安装包

官方预编译安装包(macOS / Windows / Linux)通过 Hermes Desktop 官网分发,仓库 package.jsonbuild 段定义了产物形态:com.nousresearch.hermes 为 appId,macOS 产出 DMG + zip,Windows 产出 NSIS + MSI,Linux 产出 AppImage + deb + rpm,产物统一输出到 release/ 目录,macOS 打包后还会执行 afterSign: scripts/notarize.mjs 完成公证。

2.3 运行环境要求

安装器会替你处理好全部依赖:Python 3.11+、便携版 Git、ripgrep(README 原文)。开发侧另有 Node 要求:package.json 声明 engines.node >= 22.22.0

2.4 更新机制

应用会在后台检查更新,就绪后提供一键更新;任何时候也可以从 CLI 手动更新:

hermes update

三、架构深潜:后端解析梯队与 serve 回退

这是 Hermes Desktop 最值得理解的设计。应用启动时必须先解决一个问题:去哪里找到一个可运行的 Hermes 后端?

README 给出的解析顺序是一级"梯子"(ladder):

  1. HERMES_DESKTOP_HERMES_ROOT(显式指向某个源码 checkout);
  2. 开发期当前的源码 checkout(npm run dev 场景);
  3. 一次已完成的受管安装(managed install);
  4. HERMES_DESKTOP_HERMES 指定的命令,或 PATH 上的 hermes
  5. 能够 import Hermes 运行时的系统 Python;
  6. 首次启动引导安装器(bootstrap installer)。

electron/main.ts 中,resolveHermesBackend() 按上述编号逐级实现,且每一级都有明确的取舍逻辑:

  • 第 1 级 HERMES_DESKTOP_HERMES_ROOT "永远优先,以便开发者固定某个 worktree",命中即直接作为 Python 后端使用,不做 bootstrap;
  • 第 2 级仅在未打包(!IS_PACKAGED)时生效,保证开发时本地 Python 改动真正被运行;
  • 第 3 级检查 ACTIVE_HERMES_ROOT(即 ~/.hermes/hermes-agent 或 Windows 的 %LOCALAPPDATA%\hermes\hermes-agent)。源码注释特别强调:bootstrap 标记(marker)的存在不等于运行时可用——CLI 也能创建相同的 repo+venv 布局,旧版 Desktop 也可能留下健康安装却缺标记。只要运行时可用就直接启动,只有运行时本身不可用才落入 bootstrap;
  • 第 4 级若命中 PATH 上的 hermes,会先做"烟测":用 --version 探测(见 backend-probes.ts),避免半卸载残留的 shim 或指向已删除解释器的 venv 入口在 spawn 时爆炸。README 中"候选在使用前会被探测;仅存在 shim 或解释器是不够的"正是对此的概括;
  • 使用受管安装或 PATH 上的既有 hermes不写 bootstrap 标记——"这不是我们执行的安装,我们不接管它的所有权"。

候选探测(probe)是梯子的核心纪律backend-probes.ts 中的 verifyHermesClicanImportHermesCli 等函数在信任任何一个候选前实际执行探测,而不是检查文件是否存在。AGENTS.md 把这一模式总结为通用工程法则:"候选只有在正确的边界上被验证后才可信;存在不是证明,探测你即将依赖的东西。"

3.1 serve 与旧版 dashboard --no-open 的兼容回退

后端命令的选择还有一个精细的兼容层,实现在 electron/backend-command.ts。标准调用是:

// serveBackendArgs(): 规范的 headless 后端 argv
// → ['--profile', profile?, 'serve', '--host', '127.0.0.1', '--port', '0']

注意 --port 0:端口由后端自行分配后通告,Desktop 通过 backend-ready.ts 中的端口通告等待机制发现它,避免固定端口的冲突。

serve 是一个较新的子命令。对于还没有更新到的旧版受管安装或旧版 PATH 上的 hermes,它们只认识 dashboard --no-open。为了不把这些用户在升级途中"锁死",Desktop 会检测运行时是否理解 serve,仅在不懂时回退到旧的 dashboard --no-open 调用:

  • sourceDeclaresServe(dashboardPySource) 通过正则 add_parser("serve" 检查 hermes_cli/subcommands/dashboard.py 源码是否注册了 serve 子命令——特意精确匹配带引号的 serve,避免 start_serverweb server 之类的子串造成误判;
  • dashboardFallbackArgs(args) 把 argv 中的 serve 原位改写为 dashboard --no-open,同时保留其余所有参数(包括前导的 -m hermes_cli.main--profile <name>)。

两种调用产出完全相同的 headless gateway——serve 只是解耦后的名字。这正是 README 所述"运行时早于 serve 的回退到 headless dashboard --no-open,这只针对后端命令做兼容,并不启动或内嵌 dashboard UI"的源码实现。

3.2 HERMES_HOME 与首次安装

打包应用把 Electron 壳与原生 React 聊天界面带在身上,首次启动时可以把 Hermes Agent 运行时安装进 HERMES_HOME~/.hermes,Windows 为 %LOCALAPPDATA%\hermes),布局与 CLI 安装完全相同。Windows 下如果用户挪动了 HERMES_HOME,需要显式设置 HERMES_HOME 环境变量。

四、开发工作流

4.1 跑起开发服务器

在仓库根目录安装一次工作区依赖,然后从 Desktop 目录启动:

npm install          # 仓库根目录 —— 链接 apps/desktop、web、apps/shared
cd apps/desktop
npm run dev          # Vite 渲染层 + Electron,并拉起 Python 后端

package.json 的脚本定义可以看到 dev 的真实构成:concurrently 同时跑 dev:renderer(Vite dev server,绑定 127.0.0.1:5174,且先经 assert-root-install.mjs 断言根安装存在)与 dev:electrontsc 编译主进程 → wait-on 等渲染层就绪 → esbuild 打包主进程 → 以 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 启动 Electron)。

4.2 指向特定 checkout 或沙箱化运行

# 一次性 HERMES_HOME、独立 Electron userData、独立应用名以避开单实例锁
../scripts/dev-sandbox.sh npm run dev
HERMES_DESKTOP_HERMES_ROOT=/path/to/clone npm run dev
HERMES_HOME=/tmp/throwaway npm run dev
npm run dev:fake-boot   # 用确定性延迟演练启动 overlay

各变量的作用与源码对应关系:

  • HERMES_DESKTOP_HERMES_ROOT:即解析梯第 1 级,指向一个源码 checkout,见 main.ts 第 4448-4458 行
  • HERMES_HOME:重定向运行时主目录,实现与真实配置隔离的沙箱;
  • dev:fake-boot 的实为 cross-env HERMES_DESKTOP_BOOT_FAKE=1 HERMES_DESKTOP_BOOT_FAKE_STEP_MS=650 npm run dev(见 package.json),用固定 650ms 的假步骤演练启动 overlay。

4.3 构建安装包

npm run dist:mac     # DMG + zip
npm run dist:win     # NSIS + MSI
npm run dist:linux   # AppImage + deb + rpm
npm run pack         # 解压态应用输出到 release/(不产安装包)

安装器手动构建并上传到 Releases;macOS/Windows 的签名与公证在环境中存在相应凭据时自动发生(macOS 用 CSC_LINK / CSC_KEY_PASSWORD / APPLE_*,Windows 用 WIN_CSC_*)。

4.4 验证门禁

提 PR 前运行(lint 允许暴露既有警告,但必须干净退出):

npm run fix
npm run typecheck
npm run lint
npm run test:ui
npm run test:desktop:platforms

涉及安装、启动、更新、打包等发布路径的变更,跑 npm run test:desktop:all。从 package.json 可看到各脚本的落点:typecheck 同时检查三套 tsconfig(渲染层、electron 主进程、e2e);test:uitest:desktop:platforms 是 vitest 的两个 project(UI 组件与平台策略模块);e2e 则由 Playwright 驱动(test:e2e,且视觉快照类用例在 Linux 上经 cage 无头 Wayland 容器运行)。Desktop 目录下的 e2e/ 套件覆盖了启动、更新、会话切换、队列边界等真实用户路径。

五、连接、项目与切换

5.1 三种连接模式

Desktop 支持三种运行模式:受管本地后端显式远程 GatewayHermes Cloud 连接。远程与云端模式走同一条"远程能力路径"——差异只在认证与发现方式,渲染层的功能模型不变。

当既无可用本地运行时、也无已保存的远程连接时,首启界面会先提供 Connect to existing Hermes 选项,再进入本地安装流程。连接建立过程包括:

  • 探测 Gateway 以确定使用 token 还是 OAuth 认证;
  • 要求 HTTP 与 WebSocket 两条腿都测试通过才保存连接——AGENTS.md 特别强调"连接测试必须实际走一遍你将来要用的那条腿:HTTP 状态探测通过而 WebSocket/认证腿失败是假阳性";
  • 连接以与 Settings 相同的加密 Desktop 配置保存(密钥字段使用 safeStorage 编码,见 connection-config.ts);之后启动自动跳过该选择。

官方标准 Desktop 构建仍包含本地安装选项——远程模式是一种运行模式,而不是独立的"纯客户端"应用。

执行边界:远程模式下,Gateway 主机就是执行边界——agent 工具、终端命令、文件操作都作用在远端 Hermes 主机上,而非显示 Desktop UI 的这台电脑。

5.2 访问代理后的额外网关头(Extra Gateway Headers)

经过访问代理(如 Cloudflare Access)的远程 Gateway 可能要求每个 HTTP 与 WebSocket 请求携带额外头。两种配置方式:

  1. 在 Settings → Connections 的 Extra gateway headers 中按连接配置;
  2. 直接在 Desktop 的 Electron userData/connection.json 远程块中添加 headers 对象:
{
  "mode": "remote",
  "remote": {
    "url": "https://hermes.example.com",
    "authMode": "token",
    "token": { "encoding": "safeStorage", "value": "..." },
    "headers": {
      "CF-Access-Client-Id": { "encoding": "safeStorage", "value": "..." },
      "CF-Access-Client-Secret": { "encoding": "safeStorage", "value": "..." }
    }
  }
}

按 profile 的远程条目 profiles[name].headers 使用相同结构。Desktop 对这套头的处理规则(README 明确列出):

  • 只应用于匹配的远程 Gateway 请求
  • httpswss 视为同一 Gateway origin 用于 WebSocket 升级;
  • 丢弃传输层或 Hermes 管理的头名,包括 AuthorizationCookieHostOriginRefererX-Hermes-Session-Token

connection-config.ts 中可以看到解析端:tokenheaders 以密文存储,由 main.ts 负责解密;normalizeRemoteHeaders 完成清洗,结果为空则整个 headers 键被省略。

5.3 项目(Projects)与工作区软切换

项目是 Desktop 的工作区抽象:一个项目可以拥有多个文件夹、仓库、worktree 与会话;新建的裸会话在用户显式进入某个项目或配置默认项目目录之前保持游离(detached)。设计上要求继续使用 Projects UI,而不是再造一套按会话的目录选择流程。

切换 profile 或连接模式是软工作区切换,不是冷启动:窗口外壳与当前管理 overlay 保持挂载,与此同时——

  • gateway 绑定的 nanostores 被显式清空(仅靠 query 失效清不掉活的会话 store);
  • query 层数据被 invalidate,新连接重新填充骨架;
  • 切换只改变前台视图与请求路由:不取消进行中的 turn、不停止后端,保留的后台 socket 继续接收运行中任务的事件。

这样既防止上一个 gateway 的行或 transcript 泄漏进下一个,也避免了一次本可软切换却整窗闪烁的硬重启。AGENTS.md 进一步区分了三种切换形态并给出各自的正确姿势:连接/模式应用(local ↔ remote ↔ cloud)是软 re-home;底层 HERMES_HOME profile 变更是硬 re-home(窗口合理重载);同窗口内的 live profile 切换则激活另一 profile 的 socket,列表合并而非清空。

六、故障排查

启动日志落在 HERMES_HOME/logs/desktop.log(包含后端输出与最近的 Python traceback)——应用报告启动失败时,第一站就是它。日志行格式由 desktop-log-line.ts 统一(ISO-8601 UTC 时间戳前缀,desktop.log、应用内"RECENT LOGS"视图与崩溃取证共用);主进程故障还会由 crash-forensics.ts 同步写入 desktop.log,因为进程已死时内存中的日志会丢。

macOS / Linux:

# 强制干净的首次启动安装
rm "$HOME/.hermes/hermes-agent/.hermes-bootstrap-complete"
# 重建损坏的 Python venv
rm -rf "$HOME/.hermes/hermes-agent/venv"
# 重置卡住的 macOS 麦克风权限提示(仅 macOS)
tccutil reset Microphone com.nousresearch.hermes

Windows(PowerShell):

# 强制干净的首次启动安装
Remove-Item "$env:LOCALAPPDATA\hermes\hermes-agent\.hermes-bootstrap-complete"
# 重建损坏的 Python venv
Remove-Item -Recurse -Force "$env:LOCALAPPDATA\hermes\hermes-agent\venv"

Windows 上 Hermes 主目录默认为 %LOCALAPPDATA%\hermes;如果你把它挪到了别处,请设置 HERMES_HOME 环境变量。

七、动手改代码前:两份必读文档

README 要求在改动 Desktop 之前先读两份文档,它们构成了 Desktop 的工程契约:

  • AGENTS.md:架构、状态归属、解析/回退、传输、性能与测试规则。其中几条核心不变量值得摘录:
    • 状态按权威归属:任何状态先问"谁有权对它说对"——后端对其他 Hermes 界面可改的东西负权威,渲染层的副本只是缓存;
    • 一切跨界都做成可观测的梯子:优先级写死在一处纯函数里,候选先验证再信任,失败的降级到下一级,失败的权威写则上报或回滚而不是悄悄改道——后端发现、连接与认证解析、工作区 cwd 选择共用同一形态;
    • 一次性凭据永不复用:OAuth Gateway 连接每次拨号都铸造新的 WebSocket ticket,绝不回退到缓存 URL;只有确认的 401/403 才意味着重新认证;
    • 兼容性不背历史包袱:回退必须窄、与可识别的旧运行时绑定、并有测试覆盖——悄悄降级的回退比它替换掉的崩溃更糟。
  • DESIGN.md:视觉系统、信息架构、动效、直接操纵与键盘行为的约定。

小结

Hermes Desktop 的工程重心不在"画一个聊天窗口",而在两条被源码反复验证的纪律上:一条有序、可探测、可回退的后端解析梯队main.ts 的六级梯子 + backend-command.ts 的 serve/dashboard 兼容层),以及一条清晰的责任接缝(Electron 管机器、React 管体验、headless hermes serve 管工作)。理解了这两点,README 里的安装命令、开发脚本、连接配置与故障排查手法就都能对号入座;继续深入时,electron/ 下每个 *.test.ts 与对应实现并排放置的测试、以及 e2e/ 的发布路径套件,是逐条验证这些不变量的最佳材料。

登录后查看全文
热门项目推荐
相关项目推荐