薅元宝Bot(OpenClaw)羊毛来养马(Hermes)(二):消失的爱马仕
起因:Hermes飞书突然不回话了
某天中午,常用的飞书机器人突然没了动静。发消息过去,没有任何响应。
第一反应是网络问题?或者服务挂了?登上去一看,果然,Hermes Gateway 进程不在了。
第一轮修复:能跑就行?
快速处理了一下:
- 重新解压安装 hermes-agent 包
- 装上所有依赖(lark-oapi、websockets、croniter 等)
- 从备份恢复配置文件
- 重新注册 systemd 服务,启动
跑起来了。飞书连接恢复,WebSocket 也连上了。
但事情没这么简单。
很快又出状况——飞书那边发消息,收到的是:
Hi~ I don't recognize you yet!
Here's your pairing code: XXXXXXXX
配对白名单丢了。重新审批一次,好了。
然后又报 TypeError:set_session_vars() got an unexpected keyword argument 'profile' —— 旧版本 gateway 包冲突。加个兜底,修好了。
然后又报 401 无效凭证 —— 备份没完全恢复,API Key 丢了。重新加载,好了。
一个接一个的问题,像打地鼠。
这时候一个疑问浮上来:为什么会一次性丢这么多东西?不像是某个服务崩溃,更像是……整个环境被重置了。
追问:到底是"被删除"还是"本来就不在"?
用户抛来一个问题:用第一性原则,查一下前几天 hermes 为什么被自动删除了。
"自动删除"是直觉判断。但直觉可能错。
先不预设结论,列证据:
时间 | 事件 |
6月13日 | Hermes 安装在宿主机上,备份磁盘里有记录 |
6月30日 | 磁盘快照,当时宿主机完整文件系统里 Hermes 还在 |
7月6日 21:37 | 容器首次启动,journal 里只有 openclaw-gateway,没有 hermes-gateway |
7月9日 12:34 | pip 重新安装 hermes-agent |
7月9日 12:40 | hermes-gateway 第一次启动,随即崩溃(缺飞书配置) |
时间线一读,真相就出来了:
Hermes 不是被"删除"的——它从来就不在 Docker 镜像里。
还原事发经过
- 最初状态:Hermes 装在宿主机上,不是 Docker 镜像的一部分(Dockerfile 里没有)
- 7月6日容器重建:用 Docker 镜像重新创建了容器,新容器基于干净镜像,里面没有 Hermes
- 7月6日-9日:系统一直在运行,OpenClaw 正常工作,但 Hermes 实际上已经不存在了——只是没人发现
- 7月9日中午:Systemd 尝试启动 hermes-gateway → 立即崩溃(因为 Hermes 不存在)→ 发现问题 → 开始排查
这解释了为什么一次性丢了这么多东西:不是删了某个文件,而是整个可写层被重置了。
根因:Docker 的分层机制
为什么 OpenClaw 没事,Hermes 就丢了?本质区别在于安装位置不同:
OpenClaw → 在镜像层(只读 base layer)
- Docker 镜像构建时就打进去了
- 存在 overlay 的只读基础层
- 容器重启、重新部署,只要还是同一个镜像层,OpenClaw 就在,不会丢
Hermes → 在可写层(upperdir)
- 用 pip install 后装进系统目录
- 写入的是 overlay 的可写层(upperdir)
- 每次重建容器,upperdir 被重置,Hermes 就消失了
一句话:一个是"原厂自带",一个是"后装的APP"——手机恢复出厂设置,原厂的还在,后装的全没。
为什么 Docker 镜像里没有 Hermes?很简单——基础镜像的 Dockerfile 里没有写,公开的 OpenClaw Docker 镜像也不带。是我们手动装进去的,没被固化到任何镜像层。
解决方案:启动钩子 + 持久卷备份
直接改 Docker 镜像?做不到——镜像托管在云上的构建系统(GitHub Actions + 容器仓库),没有仓库的写权限。
但换个思路,不一定非要打进镜像。做两件事:
一、启动时自动检测+重装
在容器 entrypoint 加一个钩子脚本:
容器启动
↓
检查 hermes 命令是否存在且可用
├─ 存在 → 跳过,正常启动(毫秒级)
└─ 不存在 → 自动 pip install 安装 → 启动
效果:
- 容器重建后第一次启动,多花 3-5 秒自动装好
- 正常重启完全不影响速度
- 只要 pip 能访问,就一定能恢复
二、配置/记忆实时备份到持久卷
光装回来不够,配置、记忆、技能这些数据也要保留。
把这些关键目录实时备份到持久卷:
- 配置文件
- 记忆文件
- 技能目录
- 对话历史
容器重建后,自动从持久卷恢复。备份大小约 4MB,毫秒级完成。
效果对比
维度 | 装进镜像 | 启动钩子+备份 |
重建后状态完好 | ✅ | ✅ |
自动恢复,无需人工 | ✅ | ✅ |
启动速度 | 立即就绪 | 首次启动多3-5秒 |
依赖外部网络 | ❌ | ✅(需要pip) |
配置变更自动同步 | ❌(需重打镜像) | ✅(实时备份) |
版本升级 | 需重打镜像 | 自动最新版 |
严格说,这不是"完美持久化"——完美持久化确实应该写进 Dockerfile。但在没有镜像仓库写权限的环境下,这个方案的实际效果几乎等同于装进镜像,甚至在配置同步和版本升级上还更灵活。
几点启示
1. 出问题时,先别急着修,先问"为什么会这样"
第一轮修复只花了十几分钟,但如果停在那里,下次容器重建还会丢。根因不解决,问题就会反复出现。
2. "被删除了"是直觉,不是结论
第一反应往往是"谁删了我的东西"。但用第一性原则往下挖一层——东西不见了,不一定是被删了,也可能是从来就不在这个环境里。区分"被删除"和"未被包含",决定了后续的修复方向完全不同。
3. 持久化的本质:数据放在哪一层?
Docker 环境里判断一个东西会不会丢,不用记复杂规则,就问一个问题:
它在镜像层(只读),还是在可写层(upperdir)?
镜像层的,重建不丢;可写层的,重建就没。想持久化?要么打进镜像,要么挂持久卷。
就这么简单。
