乐于分享
好东西不私藏

把 AI 助手从 WSL 迁回原生 Windows,我踩了 8 个坑(附真实排障日志)

把 AI 助手从 WSL 迁回原生 Windows,我踩了 8 个坑(附真实排障日志)

一套 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.txt

Windows 侧(已有,待复用):

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%\hermesC:\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 工作空间全丢了

现象:切到 writersysops 等 profile,工作空间列表全空,切换就报路径不存在。

根因:迁移只覆盖了 default 数据,各 profile 的 webui_state/ 是独立目录——writer 的 webui_state 完全没搬过来,sysops 缺 workspaces.jsonqa-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 的副作用。richtzdataconcurrent_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<15concurrent_log_handler

⑦ 会话标题全是 "Webui Session" 回填 state.db.sessions.title,重启 gateway + WebUI

⑧ _index.json 只出 4 条 等 WebUI 全量扫描侧车完成(不重复重启)


结语

迁移这种事,难的从来不是"搬文件",而是语义对齐:哪个路径归哪个进程管、哪个文件归哪层展示、版本变化了哪些进程需要同步重启。

这套 Hermes 在原生 Windows 上跑了两天后,比 WSL 里还稳——没有了 WSL ↔ Windows 文件系统性能损耗,gRPC 调用也省了一层转发。

如果你也在折腾类似迁移,希望这篇能帮你省下至少半天。有其他坑欢迎在评论区补充 👇