systemd 显示 Gateway 正在运行,飞书却始终连不上。最后发现,问题不在凭据,而在新 Profile 的服务没有继承代理环境变量。


问题现象

我为 Hermes 新建了 blog-writer Profile,并为它启动了独立的 systemd 用户服务:

1
systemctl --user status hermes-gateway-blog-writer.service

服务状态显示为 active (running),乍看没有异常。但 Gateway 日志中的飞书连接会等待 30 秒后超时:

1
2
feishu connect timed out after 30s
Gateway started with no connected platforms — 1 platform(s) queued for retry

后续自动重连也没有成功:

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-writer Profile 的飞书配置是否已被 Gateway 读取,但不在终端或文章中输出任何凭据。
  • 对比默认 Gateway 与新 Profile Gateway 的 systemd 环境变量和 drop-in 配置。
  • 重启服务后,以飞书连接成功日志作为最终判断依据,而不是只看 systemd 的 active 状态。

查看日志可以使用:

1
journalctl --user -u hermes-gateway-blog-writer.service -n 100 --no-pager

对比服务加载的 drop-in 和环境变量可以使用:

1
2
3
4
5
systemctl --user show hermes-gateway.service \
-p DropInPaths -p Environment --no-pager

systemctl --user show hermes-gateway-blog-writer.service \
-p DropInPaths -p Environment --no-pager

对比结果很直接:默认服务加载了自己的代理 drop-in,新服务最初没有。


根因

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-writer Gateway 由 systemd 用户服务启动,没有读取 ~/.bashrc
  • 新 unit 没有独立的代理 drop-in,进程环境中缺少 Clash 代理变量。
  • Gateway 主进程能够正常启动,所以 systemd 显示 active
  • 飞书 WebSocket 无法在 30 秒内建立连接,平台适配器进入重试队列。

修复步骤

先为新服务创建与 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
2
3
4
5
6
7
8
9
[Service]
Environment="HTTP_PROXY=http://127.0.0.1:7890"
Environment="HTTPS_PROXY=http://127.0.0.1:7890"
Environment="ALL_PROXY=socks5h://127.0.0.1:7890"
Environment="NO_PROXY=localhost,127.0.0.1,::1"
Environment="http_proxy=http://127.0.0.1:7890"
Environment="https_proxy=http://127.0.0.1:7890"
Environment="all_proxy=socks5h://127.0.0.1:7890"
Environment="no_proxy=localhost,127.0.0.1,::1"

这里同时写入大写和小写变量,是为了兼容不同 HTTP、WebSocket 库读取代理环境变量时的差异。ALL_PROXY 使用 socks5h,让域名解析也经由代理完成;NO_PROXY 则保留本机回环地址的直连,避免本地服务被错误送入代理。

配置写入后,重新加载 systemd 用户配置并重启新服务:

1
2
systemctl --user daemon-reload
systemctl --user restart hermes-gateway-blog-writer.service

这一步不能只执行 daemon-reload。它只让 systemd 重新读取 unit 和 drop-in,正在运行的进程不会自动获得新的环境变量,必须重启服务。


验证方式

先确认新服务确实加载了正确的 drop-in:

1
2
systemctl --user show hermes-gateway-blog-writer.service \
-p DropInPaths -p Environment --no-pager

输出中应包含:

1
DropInPaths=/home/tianlejin/.config/systemd/user/hermes-gateway-blog-writer.service.d/proxy.conf

同时应能看到 HTTP_PROXYHTTPS_PROXYALL_PROXYNO_PROXY 及其小写版本。检查时不要把包含凭据的其他环境变量复制到公开位置。

接着查看本次启动日志:

1
2
journalctl --user -u hermes-gateway-blog-writer.service \
--since "5 minutes ago" --no-pager

最终应出现这两条成功标志:

1
2
[Feishu] Connected in websocket mode
Gateway running with 1 platform(s)

本次修复后,飞书 WebSocket 已成功连接,Gateway 也确认有一个平台处于运行状态。相比 active (running),这两条平台层日志才是验证完成的依据。

如果日志里出现包含连接参数的 WebSocket URL,不要直接复制到博客、Issue 或聊天记录中,其中可能带有临时票据或访问凭据。


为什么每个 Profile 都需要独立 drop-in

Hermes Profile 隔离的不只是配置目录,也会对应独立的 Gateway 服务。例如:

1
2
hermes-gateway.service
hermes-gateway-blog-writer.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-writer Gateway。
  • 创建新的 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
  • 同时配置大写和小写的代理变量,并为本地回环地址设置 NO_PROXYno_proxy
  • 修改 drop-in 后执行 systemctl --user daemon-reload,随后重启对应服务。
  • 使用 systemctl --user show <unit> -p DropInPaths -p Environment 确认配置已进入进程环境。
  • 不要只看 active,还要确认日志出现平台连接成功和 Gateway 平台计数。
  • 分享日志前删除 token、ticket、access_key、App ID、App Secret 以及带连接参数的完整 WebSocket URL。

参考资料


创建时间:2026-07-23 · 本文档由 Hermes 用户排查并记录