薅元宝Bot(OpenClaw)羊毛来养马(Hermes)(二):消失的爱马仕

起因:Hermes飞书突然不回话了

某天中午,常用的飞书机器人突然没了动静。发消息过去,没有任何响应。

第一反应是网络问题?或者服务挂了?登上去一看,果然,Hermes Gateway 进程不在了。


第一轮修复:能跑就行?

快速处理了一下:

  1. 重新解压安装 hermes-agent 包
  1. 装上所有依赖(lark-oapi、websockets、croniter 等)
  1. 从备份恢复配置文件
  1. 重新注册 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 镜像里。

还原事发经过

  1. 最初状态:Hermes 装在宿主机上,不是 Docker 镜像的一部分(Dockerfile 里没有)
  1. 7月6日容器重建:用 Docker 镜像重新创建了容器,新容器基于干净镜像,里面没有 Hermes
  1. 7月6日-9日:系统一直在运行,OpenClaw 正常工作,但 Hermes 实际上已经不存在了——只是没人发现
  1. 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)?

镜像层的,重建不丢;可写层的,重建就没。想持久化?要么打进镜像,要么挂持久卷。

就这么简单。