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;升级流程指南里讲的是另一套东西。