一套 Hermes(AI Agent 框架)从 WSL2 Ubuntu 迁移到原生 Windows,本以为只是换个目录的事,结果前前后后折腾了两天、反复重启 8 次才跑通。本文把踩过的 8 个坑按时间线讲清楚:现象 → 根因 → 解法,所有数据来自真实排障日志,方便后来人直接抄作业。
背景
迁移步骤(附命令)
迁移前 Checklist(照着做,跳过下面 8 个坑)
下面这份清单是踩了所有坑之后反推出来的。对照着把每一条勾掉再迁移,能省下至少半天。
• [ ] 先停掉 WSL 和 Windows 上的所有 Hermes 进程 gateway、WebUI、以及任何正在跑 tool call 的 agent 会话都关掉,避免迁移时数据写入中。
• [ ] 整目录备份 WSL 源数据cp -a /home/bingxxpc/.hermes /home/bingxxpc/.hermes.bak-2026-07-24,备份到 WSL 以外的地方(比如另一块盘或 Windows),别只放在 WSL 文件系统里。
• [ ] rsync 不要用 --exclude=webui/ 侧车文件(webui/sessions/*.json)是前端展示层命根子,漏了就显示不出历史会话。正确写法是排除引擎目录但不排除 webui:
bash rsync -av --delete \ --exclude=hermes-agent/ \ /home/bingxxpc/.hermes/ /mnt/d/opensrc/.hermes/ - [ ] 迁移后验证数据完整性 数一下 Windows 侧的 webui/sessions/*.json 个数,和 WSL 源端对比(源端 693 个,迁完应该一致)。 - [ ] 设 HERMES_HOME 环境变量 用 winreg 写入 HKCU\Environment,指向 Windows 侧数据目录。 - [ ] 批量路径映射 迁移前/后统一跑一遍 state.db 的 cwd 列和各 profile 的 workspaces.json: - /mnt/d/... → D:/... - /mnt/c/... → C:/... - /home/bingxxpc/... → C:/Users/bingxxpc/... - [ ] 逐个 profile 检查 webui_state/workspaces.json + last_workspace.txt 是否齐全,缺失的从 WSL 源端取回。 - [ ] 先不急着 hermes update 如果后续要升级引擎,升级后先确认 venv 依赖齐全(rich/tzdata/concurrent_log_handler 容易被清掉)。 - [ ] 启动后确认一切正常 gateway + WebUI 启动后,用 /health 和侧边栏会话数确认,再开始正常使用。
Hermes 原本在 WSL2 Ubuntu 里跑,数据家目录 /home/bingxxpc/.hermes。早年曾在原生 Windows 装过一套引擎(C:\Users\bingxxpc\AppData\Local\hermes,自带完整 venv),后来转 WSL。现在要把数据搬回 Windows,引擎复用旧安装,两者"杂交"成一套运行态。
迁移前拓扑
WSL2 侧(源数据,需迁移):
/home/bingxxpc/.hermes├── state.db ← 会话 + 消息(SQLite,778 会话 / 305MB)├── config.yaml ← OmniRoute / <省钱路由模型> 配置(ver 28)├── webui/sessions/*.json ← 693 个侧车文件(前端展示依赖)└── profiles/ ├── default/webui_state/ ├── sysops/webui_state/ └── coder/webui_state/ ├── workspaces.json └── last_workspace.txtWindows 侧(已有,待复用):
C:\Users\bingxxpc\AppData\Local\hermes└── hermes-agent\venv ← 唯一完整 venv(引擎居住地)D:\opensrc\github\hermes-agent ← 开发源码(无 venv,不运行)D:\opensrc\github\hermes-webui ← WebUI 前端(独立进程)迁移操作
① 用 rsync 把数据搬到 Windows:
# 从 WSL 的 /home/bingxxpc/.hermes 搬到 /mnt/d/opensrc/.hermes(即 Windows 的 D:\opensrc\.hermes)rsync -av --delete \ --exclude=hermes-agent/ \ --exclude=webui/ \ /home/bingxxpc/.hermes/ /mnt/d/opensrc/.hermes/注意这条命令的
--exclude=webui/直接埋下了坑 2 的雷——sidecar 文件整个没搬。
② 设 HERMES_HOME 指向新数据目录:
# 用 winreg 写入用户环境变量,重启终端或重新登录生效reg add "HKCU\Environment" /v HERMES_HOME /t REG_SZ /d "D:\opensrc\.hermes" /f③ 迁移后拓扑(杂交正确态):
D:\opensrc\.hermes ← 数据家目录(纯数据,无引擎)C:\Users\bingxxpc\AppData\Local\hermes\hermes-agent\venv ← 引擎 venv(复用)→ gateway = AppData 引擎代码 + HERMES_HOME 指向 opensrc 数据迁移数据用 rsync,命令本身很简单,坑全藏在"路径映射"和"进程状态"的细节里。
坑 1:一启动,"配置全没了"
现象:迁移完数据,打开 WebUI 看历史会话,列表基本空的。
根因:Hermes 的 get_hermes_home() 解析顺序是:context override → HERMES_HOME 环境变量 → 平台默认。Windows 默认家目录是 %LOCALAPPDATA%\hermes(C:\Users\bingxxpc\AppData\Local\hermes),那个目录里是空壳(只有引擎、没用户数据)。HERMES_HOME 没设,Hermes 就读空壳去了——数据明明在 D:\opensrc\.hermes,但启动读的是别处。
解法:用 winreg 把 HERMES_HOME=D:\opensrc\.hermes 持久写入 HKCU\Environment(用户环境变量):
reg add "HKCU\Environment" /v HERMES_HOME /t REG_SZ /d "D:\opensrc\.hermes" /f重启终端或重新登录生效。
排查时发现 D:.hermes 是迁移误建的空壳(state.db 仅 1MB),最终确认后删除,避免混淆。
坑 2:历史会话"少了很多"
现象:WebUI 侧边栏只剩 10 条会话,明明记得有几百个。
根因:Hermes 的会话数据分两层: - state.db(SQLite):存真实数据(779 条会话,完好无损); - webui/sessions/*.json:WebUI 前端展示依赖这些侧车文件。
迁移时用了 rsync --exclude=webui/,693 个侧车整个漏掉,Windows 侧只剩 11 个。state.db 有数据,但前端没东西可展示,所以"列表少了很多"。
解法: 1. 从 WSL 把 693 个侧车合并回 Windows(先备份原 11 个); 2. 删除残缺的 webui/sessions/_index.json(存在时 WebUI 直接用旧索引、不扫描全部侧车); 3. 重启 WebUI,让它从全部侧车重建索引。
最终重建出 699 个唯一会话(703 个文件里有 4 个同 session_id 重复,按消息数择优去重)。
教训:迁移时千万不要
--exclude=webui/,侧车文件是展示层的命根子。
坑 3:各 profile 工作空间全丢了
现象:切到 writer、sysops 等 profile,工作空间列表全空,切换就报路径不存在。
根因:迁移只覆盖了 default 数据,各 profile 的 webui_state/ 是独立目录——writer 的 webui_state 完全没搬过来,sysops 缺 workspaces.json,qa-expert 的 last_workspace.txt 残留着 WSL 坏路径 /mnt/d/<项目目录>/...(Windows 拼出来就是 D:\mnt\d\<项目目录>\...,不存在的路径)。
解法:逐个 profile 从 WSL 取 workspaces.json + last_workspace.txt,批量路径转换后写回 Windows:
# 路径映射规则/mnt/d/... → D:/.../mnt/c/... → C:/.../home/bingxxpc/... → C:/Users/bingxxpc/...sysops 补回了 3 个工作空间(Home / deployscript / 某项目目录),qa-expert 的坏路径修正。writer 的 workspaces.json + last_workspace.txt 一起取回,公众号等工作空间恢复。
坑 4:WebUI 报 "Agent was updated"
现象:在 WebUI 里操作,弹出:
Error: Hermes Agent was updated while Hermes WebUI was running.Restart Hermes WebUI before retrying this action.根因:Hermes 的 agent 源码在 WebUI 运行期间被改动过(某个文件 mtime 变化),WebUI 启动时记录了当时的 agent 版本,发现对不上就拒绝操作,要求重启以加载新版本。
解法:gateway 和 WebUI 都干净重启,并删掉陈旧的 gateway.pid / gateway.lock(否则报 "PID file race lost"):
# 清锁rm -f D:/opensrc/.hermes/gateway.pid D:/opensrc/.hermes/gateway.lock# 启动 gatewayHERMES_HOME=D:/opensrc/.hermes \ "C:/Users/bingxxpc/AppData/Local/hermes/hermes-agent/venv/Scripts/pythonw.exe" \ -m hermes_cli.main gateway run# 启动 WebUI(新开终端)set HERMES_HOME=D:/opensrc/.hermesset HERMES_WEBUI_STATE_DIR=D:/opensrc/.hermes/webui"C:/Users/bingxxpc/AppData/Local/hermes/hermes-agent/venv/Scripts/pythonw.exe" \ D:/opensrc/github/hermes-webui/server.py/health 返回 200 即正常。
坑 5:弹窗停不下来
现象:浏览器不停弹"审批"卡片 + 提示音 + 通知,关一个又来一个,根本停不下。
根因:这是最迷惑的一个。gateway 重启时自动恢复了某个历史 agent 会话(引用旧 WSL 路径 /mnt/d/<项目目录>/...,Windows 上不存在),该会话循环调 terminal 工具并卡住一个未解决的 pending approval。前端 static/messages.js 收到 SSE approval 事件就弹窗,并持续轮询/api/approval/pending——只要网关内存里 pending 没清,就一遍遍弹。
真正根治需要两步:① 改 config.yaml 把 approvals.mode: manual 切 deny(阻止新 pending,但清不掉内存里已有的);② 必须重启 WebUI 进程(WebUI 内存里的 _pending 队列跟 gateway 内存是独立镜像,改库/改 config 都清不掉,只有进程销毁才清)。
此外,state.db 的 messages 表里残留了 147 条历史 cron 任务留下的 approval_pending:true 消息(active=1),浏览器重连 WebUI 时会把这些渲染成审批卡片——所以光重启 WebUI 一次还不够,得先把这 147 条库里的旧消息置为无效,彻底根治。
解法(终局版):
# 1. 备份 state.dbcp D:/opensrc/.hermes/state.db \ D:/opensrc/.hermes/state.db.bak-20260725# 2. 在 AppData venv 里跑,把 147 条残留消息清掉python -c "import sqlite3db = sqlite3.connect('D:/opensrc/.hermes/state.db')cur = db.cursor()cur.execute('''UPDATE messages SET content = REPLACE(content, '"approval_pending":true', '"approval_pending":false'), content = REPLACE(content, '"status":"pending_approval"', '"status":"denied"'), active = 0WHERE content LIKE '%pending_approval%' AND content LIKE '%approval_pending%' ''')print(f'Fixed {cur.rowcount} rows')db.commit(); db.close()"# 3. 强杀旧 WebUI + gatewaytaskkill /PID <webui_pid> /F /Ttaskkill /PID <gateway_pid> /F /Trm -f D:/opensrc/.hermes/gateway.pid D:/opensrc/.hermes/gateway.lock# 4. 干净重启HERMES_HOME=D:/opensrc/.hermes \ "C:/Users/bingxxpc/AppData/Local/hermes/hermes-agent/venv/Scripts/pythonw.exe" \ -m hermes_cli.main gateway run # 后台# 另开终端set HERMES_HOME=D:/opensrc/.hermesset HERMES_WEBUI_STATE_DIR=D:/opensrc/.hermes/webuipythonw D:/opensrc/github/hermes-webui/server.py # 后台弹窗消失后,若需恢复手动审批,再把 approvals.mode 改回 manual(前提是确认没有卡住的 pending 会话,否则会再弹)。
坑 6:hermes update 把 venv 依赖清没了
现象:gateway 启动直接崩:
ModuleNotFoundError: No module named 'concurrent_log_handler'根因:执行 hermes update 经 git 升级引擎源码时,更新过程会清掉 venv 的部分依赖。这不是你的代码问题,是 update 的副作用。rich、tzdata、concurrent_log_handler 都曾被清过。
解法:在唯一能跑的那个 venv 里,按报错补装:
"C:/Users/bingxxpc/AppData/Local/hermes/hermes-agent/venv/Scripts/python.exe" -m pip install "concurrent_log_handler" "rich>=14.3.3,<15" tzdata注意:
rich必须< 15,初装 15.0.0 会跟 hermes 的依赖约束冲突,装14.3.4才兼容。补装后重起 gateway 即可。
坑 7:历史会话标题全是 "Webui Session"
现象:WebUI 会话列表里,所有历史会话的标题都是 Webui Session,无法区分。
根因:state.db 的 sessions.title 列有 UNIQUE 约束。历史会话生成标题时与已有标题冲突,Hermes 没写入,保持 NULL。前端对 NULL 标题兜底显示占位词 Webui Session。全库 782 个会话的 title 都是 NULL。
解法:用 Hermes 自身的 title_from 逻辑批量回填——普通会话取首条 user 消息前 80 字,cron 会话用 Cron · <id>,冲突追加 · n 后缀满足 UNIQUE:
# 备份 state.db 先cp D:/opensrc/.hermes/state.db D:/opensrc/.hermes/state.db.bak-title-fill# 在 venv 里跑回填脚本(写一个 Python 脚本读 state.db → 写 title → commit)# 回填后 782 条全部有标题,NULL 归零回填后同时重启 gateway 和 WebUI,前端刷新才能读到新标题。
坑 8:WebUI 索引残缺,只重建出 4 条
现象:删掉残缺的 _index.json 后,WebUI 自动重建只出了 4 条会话(各 profile 各 1 条),明显不对。
根因:_index.json 是 WebUI 的会话索引缓存,它不直接查 state.db,而是读 webui/sessions/*.json 侧车文件。侧车文件虽然合并了 703 个,但索引本身还没完成全量扫描。4 条是默认 fallback——重建后需要时间让 WebUI 跑完全量扫描。
实际上 703 个文件里有 4 个是重复保存(同 session_id 多文件),按消息数择优去重后,最终 699 个唯一会话。等扫描完成(几分钟内),列表恢复正常。
如果删了
_index.json重启后还是 4 条,等一下,给 WebUI 几分钟跑完全量侧车扫描,不要反复重启——反复重启会打断扫描,一直停留在 fallback 状态。
避坑清单(直接抄)
① HERMES_HOME 未设置,读错家目录 写入用户环境变量 HERMES_HOME=D:\opensrc\.hermes
② rsync --exclude=webui/ 漏掉侧车 合并侧车,删 _index.json 触发重建
③ 各 profile webui_state/ 没迁移 逐个从 WSL 取 workspaces.json + last_workspace.txt
④ agent 源码改动后报 "Agent was updated" gateway + WebUI 都重启,清 gateway.pid / gateway.lock
⑤ 弹窗停不下来(pending approval) ① 库清残留消息 ② 重启 WebUI 进程(必须)
⑥ hermes update 清 venv 依赖 按报错在 venv 补装(rich<15,concurrent_log_handler)
⑦ 会话标题全是 "Webui Session" 回填 state.db.sessions.title,重启 gateway + WebUI
⑧ _index.json 只出 4 条 等 WebUI 全量扫描侧车完成(不重复重启)
结语
迁移这种事,难的从来不是"搬文件",而是语义对齐:哪个路径归哪个进程管、哪个文件归哪层展示、版本变化了哪些进程需要同步重启。
这套 Hermes 在原生 Windows 上跑了两天后,比 WSL 里还稳——没有了 WSL ↔ Windows 文件系统性能损耗,gRPC 调用也省了一层转发。
如果你也在折腾类似迁移,希望这篇能帮你省下至少半天。有其他坑欢迎在评论区补充 👇
夜雨聆风