背景

Hermes Agent 是一个开源 AI Agent 框架,部署在本地 Linux 主机上,通过微信、QQ、元宝等多平台提供服务。整个运行环境包括:

  • Hermes 源码与配置~/.hermes/
  • 会话数据库state.db,约 140MB)
  • TDAI 记忆网关(独立服务,数据量较小)
  • 技能、脚本、插件等扩展

备份由 cron 每天凌晨 3:00 自动执行,脚本负责:调用 hermes backup 打包 → 合并 TDAI 数据 → 加密 → 上传 OpenList WebDAV 远程存储 → 清理旧包。

问题发现

某天检查备份时,发现一个 1.5GB 的异常 zip 包,而正常备份通常只有 67~89MB。更诡异的是,用 Python zipfile 模块打不开它——中央目录损坏,仅 1 个条目且文件名乱码。

进一步排查发现这不是唯一的问题:

1
2
正常备份:   hermes-full-20260528_030012.tar.enc  (78.6 MB)
异常备份: hermes-backup-2026-05-29-034652.zip (1.5 GB) ← 损坏

根因分析

第一层:循环打包

hermes backup 默认将 zip 输出到 ~/hermes-backup-*.zip。备份脚本用 glob.glob(BACKUP_NAME + "*.zip") 匹配产物——但这同时匹配到了昨天的旧备份包。旧包被重新打进新 zip,新包又被明天的打包进去,体积指数膨胀。

1
2
3
hermes-backup-2026-05-29.zip (内含 hermes-backup-2026-05-28.zip)
└── hermes-backup-2026-05-28.zip (内含 hermes-backup-2026-05-27.zip)
└── ...

修复:将输出路径改为 /tmp/,打包完成后用 shutil.move() 移回,彻底隔离输入输出。

第二层:未排除缓存目录

hermes backup 扫描 ~/.hermes/ 全目录,没有排除浏览器缓存、npm 缓存等可再生数据:

目录 大小 说明
.agent-browser/browsers/ 266 MB Chromium 浏览器二进制
.npm-global/ + .npm/ 1.7 GB npm 缓存
node_modules/ - JS 依赖(可重装)
cache/ + .cache/ 306 MB 各类临时缓存
backups/ - 旧备份包(防套娃)
logs/ - 日志文件
checkpoints/ - 文件系统快照

解决方案

修改 hermes backup 排除规则

直接修改 Hermes 源码 /opt/hermes/hermes_cli/backup.py 中的排除变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_EXCLUDED_DIRS = {
"hermes-agent", # 源码,可重新 clone
"__pycache__", # 字节码
".git", # Git 数据
"node_modules", # JS 依赖
"backups", # 防套娃
"checkpoints", # 会话轨迹
"logs", # 日志
"cache", # 可再生缓存
".cache", # 同上(隐藏目录)
".agent-browser", # 浏览器二进制 266MB
".npm-global", # npm 全局 1.7GB
".npm", # npm 缓存
}

_EXCLUDED_SUFFIXES = {
".pyc", ".pyo", # 字节码
".db-wal", ".db-shm", # SQLite WAL
".db-journal", # SQLite 日志
".tar.enc", # 已加密的旧备份包
}

排除逻辑在 _should_exclude() 函数中实现——对路径的每一级组件做精确匹配,确保 cache/.cache/ 都被正确过滤,同时不影响包含 cache 子串的正常文件名(如 models_dev_cache.json)。

修复备份脚本

1
2
3
4
5
6
# 之前:输出到当前目录,被 glob 匹配到
hermes backup # → ~/hermes-backup-*.zip

# 之后:输出到 /tmp,打包完再移回
hermes backup -o /tmp/hermes-backup-<timestamp>.zip
shutil.move(tmp_path, local_path)

同时将超时从 300s 提升到 900s——优化后扫描 2291 个文件仍需约 538 秒。

调整保留策略

1
2
LOCAL_KEEP = 1    # 本地只保留最新 1 份(从 3 改为 1)
REMOTE_KEEP = 7 # 远程保留 7 份不变

优化后备份包仅 111MB,本地留 1 份足够,远程 7 份提供回滚空间。

效果

指标 优化前 优化后
备份包大小 1.5 GB(异常) 111 MB
扫描文件数 22,768 2,291
压缩耗时 >300s(超时) ~538s(含扫描)
核心数据保留 - ✅ state.db、.env、sessions、skills、scripts

排除规则速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
排除项              类型    备注
─────────────────────────────────────
hermes-agent 目录 源码,重新 clone
__pycache__ 目录 字节码
.git 目录 Git 数据
node_modules 目录 JS 依赖
backups 目录 防套娃
checkpoints 目录 会话轨迹
logs 目录 日志
cache / .cache 目录 可再生缓存
.agent-browser 目录 浏览器 266MB
.npm-global / .npm 目录 npm 1.7GB
.pyc/.pyo 后缀 字节码
.db-wal/.db-shm 后缀 SQLite WAL
.db-journal 后缀 SQLite 日志
.tar.enc 后缀 旧加密备份包
gateway.pid 文件 运行时锁
cron.pid 文件 运行时锁

经验总结

  1. 备份的输入和输出必须物理隔离——不要让打包工具扫描自己的产物
  2. 排除规则是第一道防线——缓存、日志、可再生数据不该进备份
  3. 加密文件无法用常规工具校验——zip 的完整性检查对 .tar.enc 无意义,需要解密后验证
  4. SQLite WAL 文件需注意——.db-wal.db-shm 是 SQLite 的事务日志,正常备份时应排除(恢复时数据库本身已包含一致状态)
  5. 修改源码比脚本 hack 更可靠——临时移走目录再恢复的方案容易在异常退出时丢数据,直接改排除规则一劳永逸

📝 本文由 AI 助手诺亚辅助生成