Hermes 自带一个包管理器:pm doctor、repair、gc 到底在修什么

某个早上 hermes 起不来了,终端里一行 ModuleNotFoundError;你昨天只是装了个插件。或者换个方向:同一份配置在笔记本上好好的,到了台式机就提示某个工具不存在。你打开 pip list 想看个究竟,结果发现有三套 Python 环境,谁也不认识谁。
这就是 Hermes 为什么要自带一个包管理器。hermes pm 管的不是 Hermes 这个应用本身(那是 hermes update 的活),而是它脚下那层东西:工具二进制和Python 依赖环境。搞清楚这条分界线,后面所有命令就都有了位置。
它到底管什么
按官方文档的说法,PM 负责三件事:把固定版本的工具二进制装上并放进 PATH;给 Hermes 准备受管的 Python 环境;把“声明了什么依赖”和“实际装了什么”之间的差异讲清楚。
配套的文件各管一段,混淆它们是最常见的理解错误:
| 文件 | 作用 |
|---|---|
pm/lock.json |
受管工具的精确版本、按目标平台的下载地址、SHA-256 哈希 |
pm/pyproject.toml + pm/uv.lock |
依赖管理器自己那份 Python 依赖与锁定结果 |
pyproject.toml + uv.lock |
Hermes 主项目的依赖、extras、平台标记 |
工具 store 的 facts.json |
已安装的工具条目、身份、环境导出、实际文件摘要 |
每次安装的 facts.json |
选中的 Python 环境、输入戳、启用的 extras |
锁文件里有条目,不等于装上了。 这是整个设计里最容易被忽略的一句:hermes pm doctor 的工作就是拿实际状态去和锁对比,并且校验真实的文件字节。
四个日常命令
hermes pm status # 上一份同步/更新回执(JSON),没有就直说没有
hermes pm doctor # 工具身份、文件、摘要是否与锁一致
hermes pm install # 装必需工具 + Python,再同步 all extra
hermes pm repair # 重建并校验 Python 依赖环境
status 是最省时间的入口。它输出的是最近一次同步或更新的回执,长这样(本机真实输出,已截断):
{
"exit_code": 0,
"feature_list": null,
"finished_at": "2026-10-21T16:57:52.054592+00:00",
"kind": "plugin-check",
"outcome": "ok",
"plugin_checks": [],
"refusal": null,
"schema": 1,
"steps": [],
"update_id": null,
"venv_rebuild": null,
"warnings": []
}
outcome、refusal、warnings、steps 这几个字段就够你判断上一步是成功、被拒绝还是部分完成。
install 值得单独说一句:不带参数的 pm install 会装必需工具 + Python,把工具放进 PATH,然后同步 all extra,还会顺带装默认的可选工具(agent-browser 和 Chromium、cua-driver)。这两个下载失败只会打警告,不会让整次安装失败——因为绝大多数人只是想用 CLI,不需要浏览器或桌面控制。
如果你当初用 --skip-browser / --skip-computer-use 跳过了它们,这个选择会被记下来:之后的不带参数 install 和 hermes update 都会继续跳过。想反悔,直接 hermes pm install agent-browser 点名装一次就行。
修一条坏掉的依赖链
repair 和 gc 解决的是两类完全不同的问题,别用错。
pm repair:按记录重放当前选中那一代环境的依赖集合,校验通过之后再切换过去。关键在于它不解析插件配置——所以你那个把依赖搞坏的插件配置,不会被再读一遍。它也不会改版本号、extras 或插件配置。如果记录读不出来、或者保存的锁丢了,它会直接失败,而不是给你一个“缩水版”环境。
启动时如果发现依赖不完整,Hermes 也会自己请求一次 repair,但自动尝试次数是有上限的;手动 pm repair 不受这个上限限制。
pm gc:清掉没人引用的东西——工具 store 里孤立条目、可以清的下载残片、没在用的租约式 Python 环境,以及已经被删除的 checkout 留下的 installs/INSTALL_KEY/ 状态。它会保守地留下看不见的 checkout(比如容器里看到宿主机安装的那种情况),以及任何还持有安装锁或环境租约的状态。
一句话记法:坏了用 repair,多了用 gc。
锁、三天规则和镜像
Hermes 依赖的每个 Python 包,在被写进 uv.lock 的时候必须至少发布满三天(uv 的 exclude-newer,CI 的锁检查会强制这一点)。目的很直接:刚发布、可能被投毒的版本进不了你的锁文件。
实际影响是:hermes pm lock 是应用这条检查的那一步,而在不提供上传时间的镜像上它会直接停下,报 has no publish time。遇到这个报错,那一步换回官方源 https://pypi.org/simple 就好,日常安装仍然可以走镜像。
镜像本身可以配:pip.conf 里的 index-url,或者 UV_INDEX_URL 环境变量,Hermes 会把它传给 uv。
下载会重试,但不会重试坏哈希
这套重试策略挺实用,值得记:
- 每个探测或传输最多 4 次尝试,退避 1、2、4 秒;服务器给
Retry-After可以延长,最多 30 秒。 - 会重试:408、429、500、502、503、504,以及连接重置、超时、临时 DNS 失败、响应体中断。
- 断点续传会保留已下载的字节;服务器不支持 range 才重新拉。
- 不会重试:哈希不匹配、证书失败、本地文件系统错误这类永久性失败。
所以看到“下载失败”重试三次都没好,先别怀疑网络,先看日志里的失败原因是不是永久性的。
维护者命令,以及那个新参数
下面这些偏开发/打包用途,普通用户一般用不到:
| 命令 | 作用 |
|---|---|
pm lock |
从 pyproject.toml 重新锁定 uv.lock;锁是新的就什么都不写 |
pm lock --bump NAME VERSION |
解析并哈希目标产物,把工具版本写进 pm/lock.json |
pm update [names...] |
查上游版本、改工具版本、装变动的工具 |
pm update --check |
只查不写;退出码 1 可能表示“有更新”,要看输出区分 |
pm update --target TARGET |
为指定目标平台解析版本 |
pm update --uv / --npm |
顺带刷新 Python / npm 的依赖解析 |
pm update --termux [--check] |
重新固定滚动池里已退役的 termux 归档 |
pm bundle --out DIR |
暂存一份可移植负载(源码快照 + 工具 + 依赖),不是签名的安装包 |
新增的 pm update --skip-target TARGET 解决的是一个很具体的痛点:上游还没跟上时,你不想让某个目标平台的钉子被刷新。比如 termux 的池子还在追赶,你希望整轮更新跳过 linux-arm64-bionic——加这个参数(可重复)就不会动它,而后面的运行仍然会把这个包报告为“当前”。
排障速查
按现象对号入座,比从头读文档快:
- 某个工具不见了或版本不对 → 先看
pm doctor,然后在可写安装上显式pm install那个包。 - 依赖冲突 → 看
pm status,先把插件声明的依赖改对,再重试。 - Python 依赖坏了 →
pm repair,然后重启 Hermes。 - 装了新环境但没生效 → 重启对应的 Hermes 进程。不要往正在运行的进程里塞第二份 site-packages。
- 磁盘被旧环境吃掉 →
pm gc;磁盘告急还可以看我们写的 低盘自动清理。 - 包名写错 →
pm install收的是包名,不是 Python extra 名,也不是 pip 的name==version写法。
最后一句
hermes pm 的设计取向很明显:宁可失败,也不给你一个悄悄缩水过的环境。写错的成本表会让服务器不加载、读不出的记录会让 repair 直接失败、退役的 termux 归档会让更新报错——都是为了让你看见问题,而不是让问题变成一周后某个更难查的故障。
如果你只是想升级 Hermes 本体,那不需要碰 pm;升级流程指南里讲的是另一套东西。