Hermes Agent 实战 03|Gateway 运维:systemd、裸进程,和一个 Telegram token 撞车原创
# Gateway 运维:systemd、裸进程,和一个 Telegram token 撞车
系列第 03 篇。Gateway 是把一个 Profile 接到 Telegram / Discord 等 IM 入口的常驻进程。这篇讲它的两种跑法、一个会无限崩溃重启的「幽灵网关」、以及一个 Telegram token 撞车——后者的现象诡异到值得每个做 Bot 的人都看一眼。命令全部可照抄。
# 1. Gateway 是什么,怎么起
一句话:hermes gateway 把某个 Profile 变成一个常驻轮询进程,对接 IM 平台。手动起一个:
hermes gateway <profile> # 前台跑,调试用
但生产上你不会前台跑。这里有个很多人栽过的认知差:
不是每个 Profile 都有 systemd 服务。 一部分 Profile 装了 user 级 systemd unit,其余的就是裸进程。
给一个 Profile 装上 systemd 托管:
hermes gateway <profile> install # 生成 user 级 systemd unit
systemctl --user enable --now hermes-gateway-<profile>.service
2
# 1.1 先列清单,再动手
多网关环境下,操作前一定先列出当前所有 unit,否则你会对着一个不存在的服务发愣:
systemctl --user list-units 'hermes-gateway*'
我遇到过一个没有 unit 文件、却赖在 failed 列表里的 hermes-gateway-telebot——这种僵尸状态用 reset-failed 清掉:
systemctl --user reset-failed hermes-gateway-telebot.service
# 1.2 hermes gateway <profile> install 生成的 unit 长什么样
install 不是装一个 systemd 包,是按模板写一个 user 级 unit。下面是从我机器上 cat 出来的真实内容(去掉无关行):
# ~/.config/systemd/user/hermes-gateway-casa.service
[Unit]
Description=Hermes gateway (profile: casa)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
Environment="HERMES_HOME=/home/<USERNAME>/.hermes"
ExecStart=/home/<USERNAME>/.hermes/hermes-agent/venv/bin/hermes gateway run --p<PASSWORD> casa --replace
Restart=on-failure
RestartSec=5s
StandardOutput=append:/home/<USERNAME>/.hermes/profiles/casa/logs/gateway.log
StandardError=append:/home/<USERNAME>/.hermes/profiles/casa/logs/gateway.err.log
[Install]
WantedBy=default.target
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
三个容易踩空的点:
HERMES_HOME永远是根——--p<PASSWORD> casa才是切换 Profile 的开关,别把 HERMES_HOME 指到profiles/casa(那一级没有state.db)。venv/bin/hermes是 hermes-agent 里的 venv 符号链接(2026-06-05 之后venv/统一指向.venv/);裸which hermes可能指向另一个。--replace启动时主动顶掉同 Profile 的旧 PID(per-HERMES_HOME 的 PID 锁),免得address already in use卡死。
# 1.3 重启顺序(很重要)
- 改了源码或配置,必须重启对应网关——Python 把模块缓存在内存里,光改文件等于没改。
- 同时重启网关和 WebUI 时,先网关、后 WebUI——WebUI 启动时会去连网关。
# 改完配置后的标准动作
systemctl --user restart 'hermes-gateway-*'
sudo systemctl restart hermes-webui # WebUI 是系统级服务,单独一个仓库
2
3
# 2. 幽灵网关:一个会无限崩溃重启的 unit
我有一个「通用」网关 unit hermes-gateway.service——它的 ExecStart 是 gateway run,不带 --profile,本意是当一个独立的默认网关跑在 8713 端口。结果它疯狂崩溃重启。
# 2.1 根因:它偷偷跟随了「粘性 Profile」
排查下来不是端口冲突(8713 本身是空的),而是:
- 这个 unit 的
HERMES_HOME指向 Hermes 根目录; - 但
hermes_cli/main.py故意不信任一个指向根目录的HERMES_HOME,于是它退而去读active_profile这个「粘性 Profile」文件(值是casa),把自己覆盖成了 casa 的副本; - 于是它和真正的
hermes-gateway-casa.service抢同一个 PID/运行锁 → 干净退出 → systemdRestart=always→ 崩溃重启循环。
教训:端口不冲突 ≠ 不冲突。Hermes 的冲突是 per-HERMES_HOME 的 PID 锁,一旦解析到同一个 Profile 就撞。
# 2.2 修法:drop-in 让它别跟随粘性 Profile
不删服务,而是用一个 systemd drop-in 把它钉死在根(=default Profile)上:
# ~/.config/systemd/user/hermes-gateway.service.d/api-server.conf
[Service]
# 跳过「粘性 active_profile」的读取,让它老老实实留在根 HERMES_HOME
Environment="HERMES_S6_SUPERVISED_CHILD=1"
# 先清空再重设 ExecStart,--replace 实现干净接管,避免 stale-pid 死锁
ExecStart=
ExecStart=/path/to/hermes gateway run --replace
Environment="API_SERVER_MODEL_NAME=hermes-default"
2
3
4
5
6
7
8
systemctl --user daemon-reload
systemctl --user restart hermes-gateway.service
# 验证:NRestarts=0、监听 8713、/v1/models 返回 hermes-default
systemctl --user show hermes-gateway.service -p <PASSWORD>
2
3
4
可复现的通用教训:如果你的 agent 网关也有「跟随某个全局当前状态」的逻辑,一个不带显式参数的常驻实例极可能悄悄变成另一个实例的副本去抢锁。让每个常驻进程都显式绑定它该服务的目标,别依赖隐式的「当前 Profile」。
# 3. Telegram token 撞车:现象比你想的更诡异
我把一个 Telegram bot token(原本属于 Freqtrade 交易容器自带的 bot)复用给了 Hermes 的 freqtrade Profile 网关。结果两边同时在线,触发了这个经典错误:
Conflict: terminated by other getUpdates request;
make sure that only one bot instance is running
2
# 3.1 关键现象:推送还在,接收挂了
最迷惑人的地方在这里——
一个 Telegram token 只允许一个
getUpdates轮询者,但sendMessage不受限。
所以撞车时:你照样能收到 bot 推过来的消息(sendMessage 正常),但 bot 收不到你发的消息(getUpdates 被另一个实例抢着,陷入 Conflict 循环)。如果你只测「能不能收到通知」,会以为一切正常,根本发现不了。
# 3.2 排障时直接调 Telegram API 查「token 是谁在用」
别猜,直接拿 token 调 getWebhookInfo 看出当前轮询者是谁:
TOKEN="<paste-token-here>" # 从 .env 复制,不要 echo 出来
# 1) token 还活不活(401 = token 失效)
curl -s "https://api.telegram.org/bot${TOKEN}/getMe" | jq .
# 2) 现在是 webhook 模式还是 polling(getUpdates)
curl -s "https://api.telegram.org/bot${TOKEN}/getWebhookInfo" | jq .
# 3) 主动 drop 所有 pending update,把 polling 锁释放掉
curl -s "https://api.telegram.org/bot${TOKEN}/getUpdates?offset=-1&timeout=0" | jq .
# ↑ offset=-1 + timeout=0 表示「把所有没确认的 update 标记为已读,立即返回」
# 4) 看进程侧:哪些 PID 在拿同一个 token getUpdates
ps -ef | grep -i 'freqtrade\|gateway' | grep -v grep
ss -tnp | grep 443 | grep python # python 在跟 Telegram 443 保持长连
2
3
4
5
6
7
8
9
10
11
12
13
14
15
getUpdates?offset=-1 这条特别值钱——它把 polling 锁立刻释放给真正的轮询者,业务侧不用重启就能恢复。
# 3.3 修法:一个 token 只喂一个轮询者
把交易容器那边的 Telegram 关掉,token 留给 Hermes 网关独占:
// 交易容器 config.json —— 关掉它自己的 Telegram 轮询
"telegram": { "enabled": false } // token 留着但不再 poll
2
# Hermes 这边:profiles/freqtrade/.env
TELEGRAM_BOT_TOKEN=<your-bot-token>
TELEGRAM_ALLOWED_USERS=<uid1>,<uid2>,<uid3>
2
3
# profiles/freqtrade/config.yaml
telegram:
allowed_users: <uid1>,<uid2>,<uid3>
gateway:
platforms:
telegram:
streaming: true
2
3
4
5
6
7
改完重启 hermes-gateway-freqtrade.service,冲突消失。
可复现的铁律:两个程序绝不要共用一个 Telegram token 去
getUpdates。如果两边都需要 Telegram,去 @BotFather 再申请一个 token。
# 4. 排障速查表
| 现象 | 大概率原因 | 动作 |
|---|---|---|
| 网关疯狂重启,端口却是空的 | 隐式跟随粘性 Profile,抢了别人的 PID 锁 | drop-in 显式绑定目标 + --replace |
address already in use | 真端口冲突 / 僵尸进程 | ss -tlnp \| grep <port>、lsof -i :<port> |
| failed 列表里有个没 unit 文件的服务 | 残留的僵尸状态 | systemctl --user reset-failed <svc> |
| Bot 能推送、但收不到你的消息 | Telegram token 被两个实例 getUpdates 撞车 | 让 token 单一轮询者独占 |
| 改了代码/配置没生效 | Python 模块内存缓存 | 重启对应网关进程 |
下一篇换个维度,讲模型路由:哪些模型真的支持 thinking、怎么触发、辅助模型怎么配,以及两个能让你 401 / 503 排到怀疑人生的小坑。