装了插件不用重启网关:Hermes 现在会给它当场接线


你在 Telegram 群里给自己的机器人装了个插件,插件带一个新的斜杠命令。你兴冲冲地在群里敲下去——机器人像没听见。翻文档,答案是“重启网关”。可重启意味着正在流式输出的回答断掉、跑着的后台任务没了、所有会话都要重来一遍。为了一个斜杠命令,代价太大了。

所以大多数人就这么算了:等下次重启吧。最近这次改动(#119266,修的是 #87770)把这件事改了——网关还在跑的时候装插件,它的平台处理器会当场接线。

为什么以前必须重启

问题出在“什么时候去发现插件”这件事上:

  • discover_plugins() 在已经发现过之后会直接短路返回,除非显式传 force=True。
  • 运行中安装的几条路径都没有传这个参数。更糟的是,tools/mcp_tool_config._portable_mcp_servers 用的是非强制调用——所以你在运行中装完插件再执行 mcp.reload,重新加载的其实是旧的 server 集合。
  • TUI 的 plugins.manage install 调用完安装函数就返回了,压根没有重新扫描。
  • 平台适配器(Telegram、Slack 等)的处理器注册表是在启动时建好的,之后不会再看一眼新插件。

于是结果就是:插件文件在硬盘上了,配置也开了,但这个正在运行的进程对它一无所知。

一张激活清单:现在生效的 vs 下个会话的

现在每条运行中加载的路径都是一次真实的强制重扫,并且从重扫内部发出一个回调(PluginManager.on_plugin_loaded),每个新加载的插件带一份激活摘要。看着像这样:

{"name": "late-mcp", "key": "late-mcp",
 "activated_now": {"gateway_commands": ["late"], "callbacks": ["telegram"]},
 "deferred": {"tools": ["late_tool"], "prompt": ["late.section"], "mcp_servers": ["worker"]}}

翻译成人话:

类别 什么时候生效 具体是什么
网关斜杠命令 立刻 插件注册的命令名
网关变换钩子 立刻 transform_llm_output、transform_tool_result、transform_terminal_output、pre_gateway_dispatch、gateway_platform_event、pre_command
其他钩子 立刻 其余钩子名
平台回调 立刻 register_platform_handler 工厂所在的平台,以及 Slack action id
工具 下个会话 工具名
系统提示段落 下个会话 段落 id
便携 MCP server 一次 mcp.reload 或下个会话 用 mcp.json 里的原始名字

三个“立刻”里最实用的是平台回调:它意味着插件注册的按钮回调、入站消息变换,不用重启就能用。

接线为什么不会重复、也不会被核心处理器抢走

“立刻生效”听起来简单,做起来有两个坑:

第一个坑:重复注册。 强制重载会交回全新的函数对象,所以不能靠函数身份去判断“这个装过了”。办法是给每次接线打上 (插件, 工厂限定名) 的键——同一个插件同一个工厂只接一次。这就是为什么那个方法叫幂等接线(rewire_plugin_handlers()):它重新读一遍工厂注册表,只跑那些还没接到活客户端上的工厂。

第二个坑:顺序。 Telegram 那边有个细节很值得说:新加的处理器会被提到核心的通配处理器前面。原因是 PTB 每个分组只派发第一个匹配的处理器,而核心注册了一个通配的 filters.COMMAND——如果新处理器排在后面,你的斜杠命令永远轮不到。Slack 那边则是按 AsyncApp 去重 register_slack_action_handler。

网关运行时会按它服务的每个 profile 在启动时订阅这个事件,收到之后在事件循环上给每个活着的适配器重新接线。这一切都发生在网关进程内部,不在安装它的那个进程里。

每个安装入口都会说实话

不管是 CLI、桌面 App、TUI 还是 dashboard 的 REST 安装,装完之后都会明确告诉你“现在生效了什么、什么要等”:

hermes plugins install late-mcp --enable

命令会先通知正在运行的网关(控制套接字上的 reload-plugins 动词,让它对指定的 home 强制重扫),然后打印上面那张“立即/延后”的清单。桌面和 TUI 的 plugins.manage install/toggle/update 则返回 activation 和 gateway_reloaded 两个字段;restart_required 只有在没有任何网关应答的时候才是 true。

这里有个很实在的设计取向:宁可不做,也不假装做到。 所以:

  • 插件中途被加载,它的工具和系统提示段落不会在本会话里生效。原因和 /skills install 一样——正在跑的会话,提示词和工具 schema 要保持缓存稳定,中途变脸会让缓存失效。
  • 便携 MCP server 要等 mcp.reload。
  • 禁用不撤销接线。 运行中禁用一个插件,它已经接好的处理器会一直留到网关重启。所有界面都会这么告诉你,而不是让你以为立刻就干净了。

用起来是什么样

一个典型流程:

# 1. 装并启用(会通知正在跑的网关重扫)
hermes plugins install late-mcp --enable

# 2. 输出里会写清楚:gateway_commands: ["late"] 已经生效
#    这时候在 Telegram 群里敲 /late 就能用了,不用重启

# 3. 如果这个插件还带了 MCP server,让它也连上
#    (通过 mcp.reload 或直接开新会话)

需要注意的一点:你的网关必须是在插件所在的那个 home 上服务的。 事件是按 profile 传播的,一个没服务这个 profile 的网关不会替它接线。多 profile / 多网关部署下,这一点比单机更容易踩到——相关背景可以看一主机一网关。

边界与注意事项

  • 这一版只覆盖处理器。 工具、系统提示段落、便携 MCP server 都不在“立刻生效”范围内,理由分别是提示词缓存和连接生命周期。
  • 没有反向操作。 接线是单向的:装上立刻生效,禁用了要等重启才真正干净。升级插件同理。
  • 插件目录本身的可信度没变。 这是运行时的接线机制,不是信任机制的松动;想了解插件从哪来、怎么审的,看插件目录的信任模型和桌面插件 SDK。
  • 终端后端类插件是另一条线,见终端后端插件。

小结

“装完重启”这种要求,在个人玩具上可以忍,在跑着真事的网关上就是一道硬门槛——它会让升级永远排在凌晨。现在这道门槛拆了一半:该立刻生效的(平台处理器、斜杠命令、变换钩子)立刻生效,该等下一轮的(工具、提示段落、MCP 连接)明确说等下一轮,禁用不撤销也如实告知。 剩下那一半——工具和提示段落——短期内不会动,因为它踩的是提示词缓存这条底线。