插件突然加载不了?旧 import 路径的兼容窗口在 9 月 14 日关了

你自己写的小插件用了一个月,一直好好的。今天 hermes update 跑完,重启网关后发现机器人不说话——hermes plugins list 里那个插件显示未加载,日志里只有孤零零一行 ImportError。你没有改过任何一行代码。
这不是插件系统的偶发故障,而是一次计划内的收尾:兼容层到期了。下面说清楚它为什么存在、现在删掉了什么、以及怎么把插件改回来。
一、9 月的模块拆分动了“内部路径”
2026 年 9 月,Hermes 做了一次大规模模块拆分(PR #102117):原来的大文件被切成 <stem>_<topic> 这样的兄弟模块。对使用者来说几乎无感,但对从内部路径 import 的插件来说,原来的名字不在它原来的位置了。
官方在这里表过态,而且态度很明确:内部 import 路径从来不属于插件契约。文档里那条 Native plugin compatibility contract 保护的是一组“行为”——plugin.yaml、register(ctx)、hook 负载、provider 接口——而不是某一个 Python 模块的位置。
但拆分会立刻打破现存的插件,所以官方补了一层临时的兼容垫片,并公开宣布寿命到 2026-09-14。它做的事比想象中多:
- 往受影响模块尾部追加了 328 个
PLUGIN-COMPAT块; - 通过懒加载的
__getattr__指针表重新暴露 1148 个被移动过的公开名字(懒加载是为了不引入 import 环); - 恢复了 592 个原来由那些模块顺带暴露的第三方 / 标准库名字;
- 把 290 个因为“没人用”而被删掉的公开定义按其拆分前原文恢复;
- 另外 41 个被恢复的私有助手、17 个被恢复的 import,只是为了支撑上面这些定义;
- 还有 3 个整个被删掉的模块用 stub 转出到替代位置;
- 34 个名字无法恢复(比如泄漏出来的循环变量),只是列出让你知道。
插件作者当年有一个专门的自查工具:hermes plugins compat <你的插件路径>。它会逐行打印 文件:行号、旧路径 → 新路径,只要还剩一处没改就退出码 1。运行期也会警告——第一次通过垫片解析某个名字时抛一次 HermesPluginCompatWarning(一个 FutureWarning),每个进程每个名字只报一次。用户那边还有一个应急开关 plugins.allow_deprecated_imports: true,让来不及改的插件继续加载。
二、现在被删掉的是什么
兼容层在 2026-09-28 的 PR #126164 里正式移除:360 个文件,新增 38 行、删除 27749 行。删掉的东西包括:
- 全部 328 个
PLUGIN-COMPAT块,以及三个 re-export stub 模块; - 它的配套工具:
COMPAT_MANIFEST.md、compat_manifest.json、scripts/check_compat_pointers.py及其 CI 检查步骤; - 所有提示界面:CLI 的黄色 / 红色横幅、
hermes plugins compat子命令、hermes doctor里的那一节、更新后的提示、Desktop 的一次性弹窗、加载器里的“先跳过再 import”逻辑; - 应急开关本身:
plugins.allow_deprecated_imports已不存在,写了也不会生效。
唯一被刻意保留的是三个惰性 stub。理由很务实:一次“旧代码正在跑的更新”可能在切换 checkout 之后懒加载这些模块,如果直接删干净,更新进程本身会崩。所以 hermes_cli/plugin_compat.py 里剩三个不出声的函数(compat_report 返回空字典、removal_in_effect 返回真、summary_lines 返回空列表),专门给这种时序兜底。
三、你的插件现在会怎样
一句话:还在 import 旧路径的插件加载不了,报的是 ImportError,原因会显示在 hermes plugins list 里。它不是被“禁用”,而是根本没跑起来,所以你在群里看到的现象就是机器人沉默。
要修,两种思路,按优先级:
- 优先用
ctx和记录在案的 ABC。 大多数“我需要 import 一个内部函数”的诉求,其实文档里的PluginContext方法和 provider 抽象基类都已经覆盖了——这样写出来的插件不会因为一次代码重排就坏掉。 - 实在要 import,就从现在定义它的那个模块 import。 完整的“旧模块 → 新位置”对照表仍然可查,它就是兼容层最后一个版本里的那份清单:COMPAT_MANIFEST.md(commit 5912ed81ed9)。按表改 import,然后重启网关看是否加载成功。
有一条容易踩的边界:私有名字从来没有被恢复过。如果你的插件 import 了 _foo 这类下划线开头的名字,或者直接 monkeypatch 了某个内部接缝,那它不在兼容层的覆盖范围内——即便你当初没看到警告,它也可能已经坏了。这种情况只能换到公开等价物,或者换到新模块。
四、怎么定位“到底是哪一行坏了”
hermes plugins list # 每个插件的加载状态与原因
hermes plugins show <name> # 单个插件的细节(含 emits / listens)
hermes plugins doctor <路径> # 校验插件目录本身
最直接的验证方式是绕开 Hermes,用同一个 Python 环境 import 一遍:
python -c "import your_plugin"
报出的 ModuleNotFoundError: No module named 'agent.tools'(或类似的名字)就是你要改的那一行。改完记得重启网关——插件是在进程启动时加载的。
五、这次变动没有碰下面的东西
如果你的插件只用文档里的表面能力,那这次移除对你毫无影响:
plugin.yaml与register(ctx):文档化的PluginContext方法不会被删除或改名,新参数一律可选、有默认值、并且倾向 keyword-only;已有返回字段不会被移除或悄悄改类型。- Hook 负载:新增数据只以 keyword 字段出现,从不改变已有字段的含义或位置。老回调收到它声明的字段,带
**kwargs的回调收到完整负载——所以新插件最好都写**kwargs。 - 清单文件:未知的
plugin.yaml字段会被忽略,旧版本 Hermes 也能加载带新元数据的插件。 - Provider 接口:新方法都有默认实现,新增的上下文只在签名检查通过时才传入。
- 弃用政策仍然是四条硬条件:有替代方案和迁移说明、每进程最多警告一次并给出最早移除版本、旧行为至少支撑两个后续 minor 版本、整个窗口内都有行为级兼容测试。
简单说:插件契约是行为,不是文件路径。你按文档写,就不会被下一次重构波及。
如果你刚开始写插件,可以从我们整理的插件目录(Plugin Catalog)指南看起;想把插件用到 memory provider 上,内存提供者迁移记录解释了 9 月拆分的另一半故事。命令行细节参照 hermes plugins 命令参考,需要运行时改挂载的进阶做法见插件热重挂载指南。