【AgentDaily】Hermes 新 Profile Gateway 飞书 WebSocket 超时排查记
systemd 显示 Gateway 正在运行,飞书却始终连不上。最后发现,问题不在凭据,而在新 Profile 的服务没有继承代理环境变量。
问题现象
我为 Hermes 新建了 blog-writer Profile,并为它启动了独立的 systemd 用户服务:
1 | systemctl --user status hermes-gateway-blog-writer.service |
服务状态显示为 active (running),乍看没有异常。但 Gateway 日志中的飞书连接会等待 30 秒后超时:
1 | feishu connect timed out after 30s |
后续自动重连也没有成功:
1 | Reconnect feishu error: feishu connect timed out after 30s, next retry in 60s |
这里最容易被 active 误导。它只能说明 Gateway 主进程没有退出,不能证明飞书适配器已经建立 WebSocket 连接。即使所有消息平台都连接失败,Gateway 仍可继续运行定时任务,因此 systemd 不会把它判为失败。
排查思路
先确认服务是否活着,再确认平台是否真的连上。这两个状态不能混在一起判断。
我按下面的顺序排查:
- 使用
systemctl --user status确认进程没有崩溃或反复重启。 - 使用
journalctl查看启动阶段的完整日志,确认失败点是飞书 WebSocket 连接超时,而不是 Hermes 配置解析失败。 - 检查
blog-writerProfile 的飞书配置是否已被 Gateway 读取,但不在终端或文章中输出任何凭据。 - 对比默认 Gateway 与新 Profile Gateway 的 systemd 环境变量和 drop-in 配置,包括
HOME、XDG 路径和代理变量。 - 重启服务后,以飞书连接成功日志作为最终判断依据,而不是只看 systemd 的
active状态。
查看日志可以使用:
1 | journalctl --user -u hermes-gateway-blog-writer.service -n 100 --no-pager |
对比服务加载的 drop-in 和环境变量可以使用:
1 | systemctl --user show hermes-gateway.service \ |
对比结果很直接:默认服务加载了自己的代理 drop-in,新服务最初没有。
非标准 HOME/XDG 路径场景
代理变量不是唯一的排查方向。某些服务器账号的登录目录不在 /home 下,systemd 用户管理器里的 HOME 却仍然保留着账号记录中的旧路径,这也会让飞书适配器不断重连。
例如,账号记录的 HOME 是:
1 | /home/tlj |
实际用户目录却是:
1 | /mnt/nfs_share/tlj |
如果 Gateway 进程继承了错误的 HOME=/home/tlj,Hermes 会尝试在下面的默认状态目录创建 Gateway 锁文件:
1 | ~/.local/state/hermes/gateway-locks |
此时日志可能出现:
1 | PermissionError: [Errno 13] Permission denied: '/home/tlj' |
表面上看,飞书适配器仍在反复重连;实际失败点并不是 WebSocket 网络连接,而是进程无法创建状态目录。只检查代理或飞书凭据,很容易绕过这个真正的错误。
排查时应同时比较 Gateway 服务中的以下变量:
1 | systemctl --user show hermes-gateway-blog-writer.service \ |
HOME应指向实际可读写的用户目录。XDG_CONFIG_HOME应指向 Hermes 能读取配置的目录。XDG_STATE_HOME应指向可创建hermes/gateway-locks的状态目录。XDG_CACHE_HOME和XDG_DATA_HOME也应与实际用户目录一致。HTTP_PROXY、HTTPS_PROXY、ALL_PROXY、NO_PROXY及其小写版本应符合当前网络环境。
也可以直接查看正在运行的 Gateway 进程环境。先通过 systemctl --user status 找到 Main PID,再执行:
1 | tr '\0' '\n' < /proc/<MainPID>/environ \ |
不要把完整环境直接贴到公开位置,其中可能包含其他凭据。
根因
Clash 代理变量原本配置在 ~/.bashrc 中,所以从交互式 Shell 启动的命令可以正常访问外网。但 WSL 中由 systemd 用户管理器启动的服务不会读取 ~/.bashrc,它拿不到这些代理变量。
默认 Gateway 之所以能连接飞书,是因为它已有独立的 drop-in:
1 | ~/.config/systemd/user/hermes-gateway.service.d/proxy.conf |
新 Profile 使用的是另一个 systemd unit:
1 | hermes-gateway-blog-writer.service |
它对应的 drop-in 目录也必须使用自己的 unit 名称。默认服务的配置不会因为两个服务都属于 Hermes 就自动继承给新服务。
因此,这次故障的完整链路是:
blog-writerGateway 由 systemd 用户服务启动,没有读取~/.bashrc。- 新 unit 没有独立的代理 drop-in,进程环境中缺少 Clash 代理变量。
- Gateway 主进程能够正常启动,所以 systemd 显示
active。 - 飞书 WebSocket 无法在 30 秒内建立连接,平台适配器进入重试队列。
在非标准 HOME/XDG 场景中,故障链路略有不同:
- systemd 用户管理器为 Gateway 注入了错误的
HOME,或者 XDG 目录仍指向旧路径。 - Hermes 尝试创建
~/.local/state/hermes/gateway-locks时触发PermissionError。 - 飞书适配器无法完成启动,随后进入反复重连。
- Gateway 主进程仍可能保持运行,因此 systemd 继续显示
active (running)。
修复步骤
先为新服务创建与 unit 名称完全对应的 drop-in 目录:
1 | mkdir -p ~/.config/systemd/user/hermes-gateway-blog-writer.service.d |
然后创建:
1 | ~/.config/systemd/user/hermes-gateway-blog-writer.service.d/proxy.conf |
内容如下:
1 | [Service] |
这里同时写入大写和小写变量,是为了兼容不同 HTTP、WebSocket 库读取代理环境变量时的差异。ALL_PROXY 使用 socks5h,让域名解析也经由代理完成;NO_PROXY 则保留本机回环地址的直连,避免本地服务被错误送入代理。
如果用户目录不是账号记录中的默认 HOME,应在同一个 unit drop-in 中显式设置正确的 HOME/XDG 路径。例如实际目录是 /mnt/nfs_share/tlj:
1 | [Service] |
这里的路径只是示例,应换成该账号真实、可读写的目录。修复时优先使用对应 systemd unit 的 drop-in,不要为了绕过路径问题直接修改 Hermes Agent 源码。环境问题留在服务配置层处理,后续升级 Hermes 时也不会被覆盖。
配置写入后,重新加载 systemd 用户配置并重启新服务:
1 | systemctl --user daemon-reload |
这一步不能只执行 daemon-reload。它只让 systemd 重新读取 unit 和 drop-in,正在运行的进程不会自动获得新的环境变量,必须重启服务。
验证方式
先确认新服务确实加载了正确的 drop-in:
1 | systemctl --user show hermes-gateway-blog-writer.service \ |
输出中应包含:
1 | DropInPaths=/home/tianlejin/.config/systemd/user/hermes-gateway-blog-writer.service.d/proxy.conf |
同时应能看到正确的 HOME、XDG_CONFIG_HOME、XDG_STATE_HOME、XDG_CACHE_HOME、XDG_DATA_HOME,以及 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY、NO_PROXY 及其小写版本。检查时不要把包含凭据的其他环境变量复制到公开位置。
接着查看本次启动日志:
1 | journalctl --user -u hermes-gateway-blog-writer.service \ |
最终应出现这两条成功标志:
1 | [Feishu] Connected in websocket mode |
本次修复后,飞书 WebSocket 已成功连接,Gateway 也确认有一个平台处于运行状态。相比 active (running),这两条平台层日志才是验证完成的依据。
不同版本的日志措辞可能略有差异,也可能显示为 WebSocket connected。判断标准不变:必须看到飞书 WebSocket 已连接的明确日志,同时确认不再出现 PermissionError 和周期性重连。
如果日志里出现包含连接参数的 WebSocket URL,不要直接复制到博客、Issue 或聊天记录中,其中可能带有临时票据或访问凭据。
为什么每个 Profile 都需要独立 drop-in
Hermes Profile 隔离的不只是配置目录,也会对应独立的 Gateway 服务。例如:
1 | hermes-gateway.service |
systemd 的 drop-in 以完整 unit 名称为作用域:
1 | ~/.config/systemd/user/<完整 unit 名称>.service.d/*.conf |
所以:
hermes-gateway.service.d/proxy.conf只修改默认 Gateway。hermes-gateway-blog-writer.service.d/proxy.conf只修改blog-writerGateway。- 创建新的 Profile Gateway 后,不能假设它会继承默认 Gateway 的 drop-in。
~/.bashrc面向交互式 Shell,不是 systemd 用户服务的通用环境配置入口。
独立 drop-in 也符合 Profile 隔离的目标。不同 Profile 可以使用不同代理、不同本地服务地址或完全不使用代理,不会互相污染。
新 Profile Gateway 检查清单
以后新建 Hermes Profile 并启用 Gateway,可以按这份清单逐项检查:
- 确认 Profile 已创建,并确认 Gateway 使用的是目标 Profile 的
HERMES_HOME。 - 确认对应的 systemd unit 名称,不要把默认服务和 Profile 服务混为一谈。
- 使用
systemctl --user status <unit>检查主进程是否稳定运行。 - 使用
journalctl --user -u <unit>检查平台适配器的连接结果。 - 如果 WSL 通过 Clash 访问外网,为该 unit 创建独立的
*.service.d/proxy.conf。 - 如果账号使用 NFS 等非标准用户目录,比较 Gateway 进程的
HOME、XDG_CONFIG_HOME、XDG_STATE_HOME、XDG_CACHE_HOME和XDG_DATA_HOME。 - 如果日志出现
PermissionError: [Errno 13] Permission denied: '/home/...',检查 Gateway 是否把锁目录写到了错误的 HOME,而不是继续只查网络。 - 同时配置大写和小写的代理变量,并为本地回环地址设置
NO_PROXY与no_proxy。 - 在对应 unit 的 drop-in 中显式设置正确的 HOME/XDG 路径和代理变量,不要直接修改 Hermes Agent 源码。
- 修改 drop-in 后执行
systemctl --user daemon-reload,随后重启对应服务。 - 使用
systemctl --user show <unit> -p DropInPaths -p Environment确认配置已进入进程环境。 - 不要只看
active,还要确认日志出现飞书 WebSocket connected 和 Gateway 平台计数,并确认不再反复重连。 - 分享日志前删除 token、ticket、access_key、App ID、App Secret 以及带连接参数的完整 WebSocket URL。
参考资料
- Hermes Agent Profiles 文档: https://hermes-agent.nousresearch.com/docs/user-guide/profiles
- Hermes Agent Messaging 文档: https://hermes-agent.nousresearch.com/docs/user-guide/messaging/
- systemd.unit Drop-in 配置说明: https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html
- systemd.exec 环境变量说明: https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html
创建时间:2026-07-23 · 本文档由 Hermes 用户排查并记录



