🤖HermesBlog
Hermes 功能详解 · 第9篇2026/8/9· Easy Understand Hermes Agent

第9篇:插件系统——无限扩展 Hermes

Hermes 功能详解第9篇:插件系统。不满足于内置功能?插件让 Hermes 能力无限扩展。

插件系统:给 Hermes 装 App

第9篇:插件系统——无限扩展 Hermes

嘿,朋友们!今天咱们来聊聊 Hermes 的插件系统——说白了,就是给 Hermes 装 App。不用改核心代码,丢个文件夹进去,它就能多出一堆新本事。

一个插件长啥样

在 ~/.hermes/plugins/ 下建个目录,放一个 plugin.yaml 加几个 Python 文件就行:

~/.hermes/plugins/my-plugin/
├── plugin.yaml      # 清单
├── __init__.py      # register() —— 把 schema 和 handler 接起来
├── schemas.py       # 工具 schema(LLM 看到的东西)
└── tools.py         # 工具 handler(被调用时真正跑的代码)

重启 Hermes,你的工具就和内置工具并排出现了,模型马上就能调用。想跟着完整例子一步步做,看官方那篇 Build a Hermes Plugin。

插件能干啥

在插件的 register(ctx) 函数里,这些 ctx.* API 都能用:

  • 加工具:ctx.register_tool(...)
  • 加钩子:ctx.register_hook("post_tool_call", callback)
  • 加斜杠命令:ctx.register_command(...),CLI 和网关会话里都能用 /名字(handler 跑在网关进程里:返回文本而不是 print(),用 agent.runtime_cwd.resolve_agent_cwd() 读会话工作目录,别用 os.getcwd();handler 可以声明 session_id、session_key、platform 或 source 作为关键字参数,Hermes 会自动传入,单参数 handler 照旧可用)
  • 加 CLI 子命令:ctx.register_cli_command(...),变成 hermes <插件> <子命令>
  • 注入消息:ctx.inject_message(content, role="user", session_key=... | origin=...),详见下面的「注入消息」一节
  • 发状态消息:handle = ctx.post_message(text),之后 handle.edit(text) 原地更新
  • 打包技能:ctx.register_skill(...),命名空间是 plugin:skill
  • 借用宿主 LLM:ctx.llm.complete(...) / ctx.llm.complete_structured(...),直接借用户当前在用的模型和鉴权跑一次性补全,还能带 JSON schema 校验
  • 调 MCP 工具:ctx.call_mcp(server, tool, arguments, timeout=30)

再往深了玩,插件还能注册一整个后端:

  • 网关平台(Discord、Telegram、IRC 等):ctx.register_platform(...)
  • 图像生成后端:ctx.register_image_gen_provider(provider)
  • 委派策略(每个 delegate_task 子代理怎么路由):ctx.register_delegation_provider(provider),由 delegation.backend 选
  • 视频生成后端:ctx.register_video_gen_provider(provider)
  • 上下文压缩引擎:ctx.register_context_engine(engine)
  • 终端执行后端(云沙箱):ctx.register_terminal_environment_provider(provider)
  • 电脑操作驱动(computer_use 工具背后的桌面控制):ctx.register_computer_use_provider(provider),由 computer_use.backend 选
  • 委派策略:在 plugins/delegation/<名字>/__init__.py 里注册(default 就是现在的路由方式)
  • 记忆后端:在 plugins/memory/<名字>/__init__.py 里继承 MemoryProvider(走的是另一套发现机制)
  • 推理后端(LLM 供应商):在 plugins/model-providers/<名字>/__init__.py 里 register_provider(ProviderProfile(...))(也是独立发现机制)

插件从哪来

Hermes 会在好几个地方找插件:

来源 路径 用途
内置 <repo>/plugins/ 随 Hermes 一起发布
用户 ~/.hermes/plugins/ 你自己的插件
项目 .hermes/plugins/ 网关不加载;见下文「项目本地插件」
pip hermes_agent.plugins entry_points 分发的包
Nix services.hermes-agent.extraPlugins / extraPythonPackages NixOS 声明式安装

同名冲突时,后面的来源会覆盖前面的——所以你自己写的同名插件能顶掉内置的。

插件默认是关着的

通用插件和用户装的后端默认不加载——Hermes 能发现它们(hermes plugins 和 /plugins 里看得到),但在你把名字加进 ~/.hermes/config.yaml 的 plugins.enabled 之前,带钩子或工具的东西一个都不会跑。这是为了防止第三方代码未经你同意就执行。

plugins:
  enabled:
    - my-tool-plugin
    - disk-cleanup
  disabled:       # 可选的拒绝名单,两边都出现时它赢
    - noisy-plugin

三种切换方式:

hermes plugins                    # 交互式开关(空格勾选/取消)
hermes plugins enable <名字>      # 加进允许名单
hermes plugins disable <名字>     # 从允许名单移除并加进 disabled

如果你保存过工具选择(用过 hermes tools,也就是 platform_toolsets 里显式列了工具集),那禁用某个插件时,它的工具集也会从这些保存的列表里被拿掉;重新启用时再放回去。CLI 和 Desktop/TUI/dashboard 上的开关行为一致。没保存过选择的话,这些列表不动:插件的工具集默认就是开着的。

hermes plugins install owner/repo 装完后会问你 Enable 'name' now? [y/N],默认是不启用。脚本化安装可以用 --enable 或 --no-enable 跳过提问。如果装的是记忆后端(__init__.py 里注册了 MemoryProvider 的插件),问的会变成 Use 'name' as the memory provider now?:选是(或 --enable)会去设 memory.provider——这是唯一能激活某个记忆后端的开关;选否就留给 hermes memory setup 去处理。

想装得可复现,可以钉死一个完整的 commit(标签、分支、缩写 SHA 都不收):

hermes plugins install owner/repo --ref 0123456789abcdef0123456789abcdef01234567

批量更新也有对应命令:hermes plugins update 不带名字时会跑一遍检查并列出能更新的插件(目录安装的重新钉到审核过的目录 commit,跟踪的 Git 安装则拉取),跳过哪些、为什么跳过都会点名(已钉住、需要修复、状态未知,或 Hermes 不升级的 pip 分发)。终端里问一次,然后逐个走和 hermes plugins update <名字> 一样的路径,所以新能力、放宽的目录钉和扫描结果仍会逐个插件提示。一个失败不影响其余,最后汇总更新了什么、哪些已是最新、哪些失败及原因(有失败则退出码 1)。没有终端时只打印列表、什么都不更新,除非加 --yes;加了 --yes 后同意类提示按非交互规则失败关闭(新能力不授予、放宽的目录钉不应用)。hermes plugins update <名字> --force 则可以在不问问题的情况下接受 caution 级安全扫描结论。

钉住之后 hermes plugins update 不会动它,要换得显式用 --force --ref <新commit> 重新装。更新前 Hermes 会用同一套规则扫描新代码,所以更新里带进风险代码的插件会在运行前被标出来:Git 更新拿到 caution 结论会问 Update anyway? [y/N],hermes plugins update <名字> --force 可以不问直接接受;目录更新是移到审核过的钉,caution 结论不再追问。Desktop 会在对话框里显示同样的结论和 Update anyway 按钮,Git 安装的插件在上次检查发现新 commit 时也会显示 Update 按钮——网关每 plugins.auto_update_check_hours(默认 24 小时)跑一次检查,hermes plugins check-updates 可以按需触发。桌面端也有对应的 Pin to commit 输入框(Capabilities → Plugins → Install from Git,填完整的 40 位 SHA),Installed 列表里会给钉住的 agent 插件显示 pinned @ <sha8> 徽章。注意这只保证 agent 插件被钉住,不保证独立的桌面端插件安装也被钉住。

顺带说一句,hermes://plugin/install?catalog=<名字> 这种深链会拿名字去查实时目录(就是 Capabilities → Plugins 选择器用的那份数据),然后弹出和 App 内一样的已审核目录条目对话框,装的就是它钉住的那个 commit。

强制重装会保留你的文件

用 --force --ref <新commit> 从同一个来源重装,效果类似升级:插件代码被换掉,但你的文件还在——没被 git 跟踪的、被 git 忽略的文件原地不动,被跟踪文件上的改动会被拷到 ~/.hermes/plugins-backup/<名字>-<sha>/ 里留底。如果换成另一个来源重装,那就是全新开始;想把插件彻底重置,先 hermes plugins remove 再装。

装私有仓库的插件

hermes plugins install 克隆时不会弹用户名密码,所以私有仓库得让 Hermes 自己找得到凭据。每次克隆、--ref 拉取和 hermes plugins update 都会先匿名试一次——公开仓库永远看不到你的凭据,所以过期的 token 也不会把公开安装搞坏。只有远端拒绝匿名访问时,Hermes 才去找凭据。对 https:// 来源,按顺序试:

  1. .env 里的 GITHUB_TOKEN 或 GH_TOKEN(仅 GitHub)
  2. gh CLI 的登录态(gh auth login,仅 GitHub)
  3. 你的 git credential helper(git credential fill)——GitLab、Bitbucket、自建服务器只要存过凭据都能用

凭据只在那次安装或更新时作为一次性 HTTP header 发出去,绝不会写进插件的 .git/config 或安装元数据。SSH 来源(git@host:owner/repo.git)还是走 ssh-agent。

如果 token 过期了,hermes doctor 会把配置好的 GITHUB_TOKEN/GH_TOKEN 发到 api.github.com 验证(在 API Connectivity 一节),被 GitHub 拒绝时会直接点名是哪个变量、哪个 .env 文件里带着这个过期 token,方便你删掉或换掉。

哪些东西不受允许名单管

有几类插件会绕过 plugins.enabled——它们是 Hermes 内置能力的一部分,默认关掉会坏事儿:

  • 内置平台插件(plugins/platforms/ 下的 IRC、Teams 等):自动加载,具体频道由 gateway.platforms.<名字>.enabled 控制
  • 内置后端(plugins/image_gen/ 等):自动加载,用 <类别>.provider 选
  • 记忆后端:全部被发现,同时只有一个生效,由 memory.provider 选
  • 上下文引擎:同上,由 context.engine 选
  • 模型供应商:内置的全部注册,用 --provider 或 config 一次选一个
  • pip 装的 backend 插件:还是要 plugins.enabled 显式开启
  • 用户装的平台插件(~/.hermes/plugins/platforms/):也要显式开启,第三方网关适配器得你点头

一句话:内置的“开箱即用”基础设施自动加载,第三方的东西一律要你同意。

网关事件钩子不走允许名单

有个容易搞混的地方:plugins.enabled 只管插件,不管网关事件钩子。放在 ~/.hermes/hooks/<名字>/ 下的钩子不是插件,既不受 plugins.enabled 也不受 plugins.disabled 约束。这个目录靠“位置”来信任——只要某个子目录里有合法的 HOOK.yaml + handler.py,网关启动时就会导入它,把文件放进去本身就是“我同意”。

注入消息

插件可以用 ctx.inject_message() 往 CLI 会话、已知的网关会话,或者某个聊天里新建的网关会话里塞消息:

# 为某个聊天/线程启动(或继续)会话,比如插件刚开的一个 Discord 论坛帖
ctx.inject_message(
    "Review the attached spec and post a plan here.",
    origin={
        "platform": "discord",
        "chat_id": "1290000000000000000",   # 论坛帖(线程)id
        "chat_type": "thread",
        "thread_id": "1290000000000000000",
        "user_id": "123456789012345678",    # 这一轮以谁的身份跑;必须过网关的允许名单
        "user_name": "Desk Orchestrator",
    },
)

签名: ctx.inject_message(content: str, role: str = "user", *, session_key: str | None = None, origin: Mapping[str, Any] | None = None) -> bool

  • 用 session_key 时,Hermes 复用那个会话里存的平台、聊天、线程、profile 和对话历史。插件不能通过 session_key 改这条路由。
  • session_key 和 origin 都没给(或都给了)、权限没授予、或者没有活着的宿主能接这个请求时,返回 False。异步接受之后才发现会话 key 未知或无法路由的,会写进网关日志。
  • 用 origin 启动会话(仅网关模式):origin 的字段形状和存储的会话 origin 一样(SessionSource.to_dict())——platform 和 chat_id 必填,chat_type、thread_id、user_id、user_name、chat_name 等可选。还没有会话时用它,比如机器人自己发的论坛帖——Discord 这类平台不会把它当入站消息回传给机器人。
  • 会话建在插件自己所属的 profile 里(就是加载它的那个插件管理器所在的 profile),走正常会话路径,全新提示词、空历史,回复投递到那个聊天/线程。如果该 profile 里这个聊天已经有会话了,文本会像人类消息一样并进去(忙会话队列照常生效),所以同线程里的人类追问会接着同一个会话。
  • 插件永远不能指向别的 profile。origin 里写了不同的 profile、网关自己的 profile 路由把这个聊天分给了别的 profile、插件的 profile 不由这个网关服务、或者那个 profile 没有 platform 对应的已连接适配器时,返回 False(原因写进网关日志)——绝不会借用别的 profile 的机器人。多路复用网关上,这一轮跑在插件 profile 的家目录和密钥下。
  • 授权不会被绕过:origin 里的 user 必须过网关当前针对该 profile 的允许名单、配对或 allow-all 配置,和 session_key 路径是同一套检查。挑一个已授权的 user_id,否则请求会在派发时被丢弃并记日志。
  • 文本规则一样:不能是斜杠命令、审批或提示词解析。下面这个权限是从插件自己 profile 的 config.yaml 里读的。

只给你信任的插件开网关注入。Hermes 会检查这个宿主 API 权限,把它限制在已有会话路由或插件自己 profile 里的新会话,并施加当前授权——但 Python 插件是进程内跑的,这个设置不是沙箱。

让插件跑在独立进程里(plugins.isolation)

默认情况下,第三方 Python 插件和以前一样,是直接 import 进 Hermes 进程的。把 plugins.isolation 设成 host,它们就会被挪进一个插件宿主:每个 profile 一个独立的 Python 进程,按需启动,只 import 这个 profile 里用户装的插件,通过一条私有管道和 Hermes 通信。

plugins:
  isolation: host        # 默认是 in_process
  host:
    launcher: []         # 可选,给宿主进程加 argv 前缀,比如套个沙箱运行器

插件本身不用改:拿到的 ctx 一样,注册工具、钩子、斜杠命令、技能和各种 provider(图像/视频生成、网页搜索、浏览器、TTS/STT、记忆、上下文引擎、模型供应商 profile)的方式也完全一样,Hermes 这边会注册对应的入口转发到宿主。Dashboard 的插件 API 也由宿主提供。内置插件仍然跑在进程内。

切到 host 模式后,变化在这几点:

  • 不共享解释器:插件的模块永远不会进 Hermes 进程,所以它读不到别的 profile 在内存里的数据,也改不了 Hermes 内部。多路复用网关下,每个 profile 有自己的宿主,只带这个 profile 的环境变量和密钥启动。
  • 崩溃被隔离:插件崩了或退出,死的是它的宿主,不是 Hermes;正在进行的调用会返回一个工具错误,Hermes 重启宿主并重新加载插件(重试次数有上限)。
  • 少数接口必须在进程内,这类插件会带着明确原因加载失败,而不是硬上:网关平台适配器(register_platform)、审批传输、Telegram/平台 handler、自己建 SDK 客户端的模型供应商 profile(create_client)、终端后端(后端的命令会带着这个原因失败,singularity 插件就是其中之一)、流式 Dashboard 端点,以及会 monkeypatch Hermes 模块的插件。这些得用 isolation: in_process 跑。

给共享部署上锁:plugins.isolation 就是普通的 profile 配置,谁能改 profile 的 config.yaml 谁就能关掉它。如果这些 profile 属于你要互相隔离的人,就把它钉在托管作用域里;托管值会压过每个 profile 自己的配置,hermes config set 也拒绝改它:

# /etc/hermes/config.yaml(root 所有,机器上每个 profile 都读它)
plugins:
  isolation: host
  host:
    launcher: [...]      # 用了沙箱运行器的话,这里也一并钉住

顺便把 agent 的终端也放到隔离后端上(Docker、SSH 之类),这样 agent 自己碰不到操作员的文件。

hermes plugins validate <dir> 和 hermes plugins show <名字> 会告诉你某个插件是不是跑在宿主里,不是的话原因是什么。就目前插件目录的情况看,348 个条目里有 299 个不用改就能跑在宿主里。

小细节,大用心

  • 插件可以注册 hermes_cli.plugins.VALID_HOOKS 里那一长串生命周期事件,包括 post_tool_call、post_llm_call、pre_api_request、post_api_request、api_request_error、pre_auxiliary_call、post_auxiliary_call、on_stream_start、on_stream_delta、on_stream_end、on_interim_message、on_session_start、on_session_end、on_session_finalize、on_session_reset、agent_loop_stopped、on_skill_lifecycle、subagent_start、subagent_stop、pre_approval_request、post_approval_response、on_human_input_request、on_human_input_resolved、pre_command、kanban_task_claimed、kanban_task_completed、kanban_task_blocked 等,具体时机和返回值看 Event Hooks 目录
  • 插件安装时可以调两个超时参数:clone_timeout_seconds(每次 Git clone/fetch/checkout 的截止时间,默认 300,超过 3600 会被夹到 3600;owner/repo/path/to/plugin 这种子目录安装只下载那个文件夹的文件)和 load_timeout_seconds(单个插件 import + register() 的截止时间,默认 10,设 0 关闭,超过 600 会被夹到 600;超时的插件会被跳过并报 load timed out after Ns,其余插件继续加载,卡住的线程被丢弃)
  • 项目本地插件(./.hermes/plugins/)网关不会加载:所有会话(hermes、hermes --tui、Desktop、消息网关、cron)都跑在同一个网关里,插件对整个进程只发现一次,从不从你启动会话的目录里找。所以 HERMES_ENABLE_PROJECT_PLUGINS=true 对会话没有效果——在网关机器上带它启动 CLI/TUI 会话、且该目录的 ./.hermes/plugins/ 里有插件时,Hermes 会在会话开头打一条警告,并往 gateway.log 写同样一行。要用插件就装成用户插件(~/.hermes/plugins/ 或 hermes plugins install),再 hermes plugins enable <名字>
  • 模型看到的工具描述写在 schema["description"] 里;ctx.register_tool(description=...) 那个是注册表元数据,两处都写的话记得保持同步,模型只认 schema 里的

插件配好了,你的 Hermes 才算真正长出了自己的样子。下篇咱们聊聊怎么让它更懂你!

📖 官方文档

本文根据 Hermes Agent 官方文档编写,原文见:官方文档 › user-guide/features/plugins