0%

把 Hermes Desktop 接到远程 Hermes Backend

当 Hermes Agent 跑在一台“永远在线”的设备上,笔记本再用本地桌面端连过去,是个很常见的理想状态:Agent 有了稳定的文件、会话和工具宿主,你则保留原生桌面 UI。但这套方案曾长期被“远程 RPC 怎么调”和“WebSocket 握手容易失败”卡住。Sudolabs 有一篇很清晰的文章讲清了现状;我最近在本地实际重建过一次这条链路,其中还踩到了 hermes dashboard 前端产物缺失的问题,顺便一并写出来。

这篇文章在讲什么

hermes dashboard 跑在远程机器上,通过桌面端连过去,理论上一行命令就行:

1
hermes dashboard --host 0.0.0.0 --port 9119 --insecure --tui --no-open --skip-build

但真要做“可落地”,就比想象中脏很多。核心要处理两件事:

  • Dashboard 怎么安全暴露(尤其是 --insecure 和 Tailscale 的配合);
  • 连接怎样在重启后保持不变(session token 被随机刷新是主要坑)。

下面把 Sudolabs 的方案、我实际遇到的构建错误,以及我现在的最终命令串成一个完整链路。

为什么这件事以前很麻烦

桌面端本质是一个 Electron 外壳。它连远程 Backend 时做两件事:一次 REST 握手 GET /api/status,然后建一个持续 WebSocket 推送聊天和事件流。

前几次做远程连接最容易忽略的是 WebSocket 握手,而不只是 REST。REST 往往能通关,所以“Test connection”看起来没问题,但 WebSocket 有三个额外检查会静默拒绝:

  1. 聊天通道默认是开的--tui 没带时,/api/ws/api/events 不存在,协议升级会直接 403,于是桌面端无限循环出现 “reconnecting / repair”。
  2. Host-header 白名单。这本身是一个 DNS-rebinding 防守,只接受 Dashboard 绑定主机名的 Host。
  3. Origin 检查。旧的 Hermes 版本在非回环绑定时,会拒绝 Electron 里的 file:// origin。

这三条叠加起来,直接导致 0.0.0.0 对外绑定时 REST 能通、WebSocket 被拒。过去大家只好用 127.0.0.1 + SSH 隧道,或用反向代理改写 Host 并剥掉 Origin。

最新的修正(Sudolabs 提到的 June 2026 更新)已经把这些限制放宽了。在 --insecure 下,由于 token 本身承担认证职责,Dashboard 会直接接受 file:// origin。从攻击模型看,一个恶意网页是无法同时伪造 file:// origin 又拿到你的 token 的,所以这个 Guard 在 --insecure 场景下其实是多余的。

Step 1:停掉旧的 Hermes Dashboard

先清理可能占用 9119 的旧进程,否则 hermes dashboard 绑定会失败:

1
2
pkill -f "hermes dashboard"
lsof -nP -iTCP:9119 -sTCP:LISTEN

如果你是用 launchd 或 systemd 管理的,要停掉对应 service。

Step 2:修复 Hermes 前端产物缺失(容易漏的坑)

在 macOS 本地这步非常容易踩坑。--skip-build 不会帮你做构建;一旦 hermes_cli/web_dist 目录不存在就会报错。

1
2
3
ls /Users/mac/.hermes/hermes-agent/hermes_cli/web_dist
# 如果显示 “No such file or directory”,就要先构建:
cd ~/.hermes/hermes-agent && npm install && npm run build -w web

一次完整成功输出大概是这样:

1
2
3
4
5
6
7
8
> web@0.0.0 build
> tsc -b && vite build
...
transforming (1) src/main.tsx
2308 modules transformed.
../hermes_cli/web_dist/index.html 0.51 kB
../hermes_cli/web_dist/assets/index-....js 1,831.63 kB
✓ built in 6.52s

如果前面已经顺手执行过 hermes dashboard --skip-build 但还没构建,一定要补这一个 step,不然桌面端会反复连不上。

Step 3:固定 Session Token,确保重启不断连

Hermes Dashboard 默认每次启动都会换随机 token。好处是安全,坏处是桌面端保存的 token 会在你重启服务后失效,又要重新粘贴。

把 token 固定到 ~/.hermes/.env

1
2
openssl rand -base64 32
# 会得到类似:hKL...== 的长字符串

按示例:

1
2
3
cat << 'EOF' >> ~/.hermes/.env
HERMES_DASHBOARD_SESSION_TOKEN=<生成的 token>
EOF

修复了这个,以后无论 hermes dashboard 还是系统重启,桌面端的连接都保持有效。

Step 4:启动 Dashboard,解释每一个 flag

1
hermes dashboard --host 0.0.0.0 --port 9119 --insecure --tui --no-open --skip-build
Flag 作用
--host 0.0.0.0 允许从其他机器和 tailscale 接口访问;127.0.0.1 只能回环
--port 9119 默认端口
--insecure 开启 token 认证,放弃 OAuth;只在可信网络里用
--tui 最容易被漏掉:开启聊天 WebSocket 通道
--no-open 不在远程服务器上弹浏览器
--skip-build 跳过前端构建直接加载现有 web_dist

远程机器上的 WebSocket 升级是否正常,可以用 curl 先验证:

1
2
3
4
5
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Origin: file://" \
"http://<server-ip>:9119/api/ws?token=<your-token>"

返回 101 才算握手成功,返回 403 说明 --tui 漏加或 Hermes 版本太旧。

Step 5:桌面端连过去

桌面端 GUI 路径:Settings > Gateway > Remote connection,填入:

  • Remote URL: http://<server-ip>:9119
  • Session Token: 上面固定的 token

如果希望跳过 GUI 预配置,也可以在启动 App 前设置环境变量:

1
2
3
export HERMES_DESKTOP_REMOTE_URL="http://<server-ip>:9119"
export HERMES_DESKTOP_REMOTE_TOKEN="your-long-random-token-here"
# 之后启动 Hermes.app

macOS 上会存储到 ~/Library/Application Support/Hermes/connection.json;Windows/Linux 也有对应路径。GUI 和 env var 是相通的。

Step 6:用 Tailscale 把服务放回“私有网”

--insecure 不是不能开,而是不能像公网那样开。Tailscale 刚好解决这个问题。

1
2
3
4
# 服务端和客户端都加入同一个 tailnet
tailscale up
tailscale ip -4
# 得到一个 100.x.y.z 的私有地址

桌面端的 Remote URL 直接改成:

1
http://100.x.y.z:9119

因为 Dashboard 绑定在 0.0.0.0,Tailscale 虚拟网卡已经天然监听。只要两边同属一个 tailnet,不需要额外 NAT/代理。

附加安全建议:

  • 用 Tailscale ACLs 限制只有指定设备能碰 9119
  • 不要用 Tailscale Funnel 把仪表盘裸暴露到公网
  • 可视化公网访问请走 OAuth 模式 + 反向代理,而不是 --insecure

Step 7:开机自启

macOS 上可以用 LaunchAgent,文件放 ~/Library/LaunchAgents/ai.hermes.dashboard.plist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>ai.hermes.dashboard</string>
<key>ProgramArguments</key>
<array>
<string>/Users/you/.hermes/hermes-agent/venv/bin/hermes</string>
<string>dashboard</string>
<string>--host</string><string>0.0.0.0</string>
<string>--port</string><string>9119</string>
<string>--insecure</string>
<string>--tui</string>
<string>--no-open</string>
<string>--skip-build</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
<key>StandardOutPath</key><string>/Users/you/.hermes/logs/dashboard.log</string>
<key>StandardErrorPath</key><string>/Users/you/.hermes/logs/dashboard.error.log</string>
</dict>
</plist>

加载方式:

1
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.hermes.dashboard.plist

Linux 下换成 systemd 同理。

我实际跑通的流程简版

以我当前的 macOS 环境为例,完整链路如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1. 旧进程清理
pkill -f "hermes dashboard"

# 2. 补齐 web dist(只在缺失时需要)
cd ~/.hermes/hermes-agent && npm install && npm run build -w web

# 3. 钉令牌(先做一次)
openssl rand -base64 32 | pbcopy
# 粘贴进 ~/.hermes/.env:
# HERMES_DASHBOARD_SESSION_TOKEN=<纸上的值>

# 4. 启动 Dashboard,改成你想用的参数
hermes dashboard --host 0.0.0.0 --port 9119 --insecure --tui --no-open --skip-build

其中,前两步是 Sudolabs 原文的基础;但我在起草这篇时,顺手也把“构建产物缺失”这段补了,因为很多人在第一篇 Follow 到这个点时,刚好会在 --skip-build 上卡一下。

Troubleshooting

现象 原因 修复
桌面端反复 “reconnecting / repair”,REST 正常 --tui 漏加,或版本太旧 加上 --tui,并运行 hermes update
WebSocket 上行握手返回 403 Origin 限制(旧版本) 升级到 June 2026 或更新版本
“Invalid Host header” 实际是通过 IP 访问但绑了 127.0.0.1 改为 --host 0.0.0.0
重启后连接断开 token 被随机重置 把 token 钉到 ~/.hermes/.env
认证总是失败 桌面端缓存了旧 token 重新粘贴或确认 .env 中的值

总结

这条链路靠三件事串起来:

  1. --tui 把聊天 WebSocket 打开;
  2. --insecure 让远程 Dashboard 接受 token 认证和 file:// origin;
  3. 固定的 HERMES_DASHBOARD_SESSION_TOKEN 避免重启断开。

把 Dashboard 跑在 Tailscale 后面,就得到了一套“私有、免公网暴露、可跨设备操作”的远程 Agent 使用方式。不需要 SSH 隧道,不需要反代,也不需要 Host 改写。Sudolabs 的方案足够 Clean;真正落地时,唯一容易绊人的反倒是本地构建顺序和 token 持久化。