Skip to main content

nonebot-plugin-hermes

中文文档 | English

Hermes Agent 的 NoneBot2 插件,通过 Hermes API Server 实现多平台 AI 聊天机器人。

支持的平台

通过 NoneBot adapter 机制,本插件自动支持:

  • ✅ OneBot v11(NapCatQQ、LLOneBot、go-cqhttp 等)
  • ✅ OneBot v12
  • ✅ QQ Official Bot
  • ✅ Kook(开黑啦)
  • ✅ Discord
  • ✅ Telegram
  • ✅ 飞书
  • ✅ 其他 nonebot-plugin-alconna 支持的平台

工作原理

用户消息 → NoneBot Adapter → nonebot-plugin-hermes
  → POST /v1/chat/completions (Hermes API Server)
  → 解析回复 → UniMessage.send() → NoneBot Adapter → 用户

功能

  • ✅ 私聊 / 群聊对话
  • ✅ 多轮上下文记忆(基于 Hermes Session)
  • ✅ 群聊 @触发 / 关键词触发 / 全部触发
  • 引用消息提取:自动提取被回复消息中的文本和图片作为 AI 上下文
  • 被动感知 (Chat Awareness):在群聊中默默记录最近对话,为下次触发提供完整背景
  • ✅ 图片接收(通过 vision 发给 AI)
  • ✅ 图片发送(解析 AI 回复中的 markdown 图片)
  • ✅ 会话生命周期由 Hermes Agent 管理
  • ✅ 白名单(群/用户级别)
  • ✅ 内置命令(/clear /ping /help /hermes-status
  • 🧪 群活跃态 (M1, 实验性):@bot 后 5 分钟内主动监听群对话,由 Hermes 通过结构化决策判断是否插话
  • 🧪 反向通道 (M1, 实验性):内嵌本地 MCP server,让 Hermes 主动 push 消息进群(延迟回复 / 异步通知)
  • 🧪 历史图片召回 (0.3+, 实验性):SQLite 持久化消息日志 + 文件系统图字节缓存 + MCP 工具 get_message_images,让 Hermes 在用户说"上图"/"刚才那张"时按消息 id 精确取回历史图字节
  • 🧪 OneBot v11 Notice 触发 (0.3.3+, 实验性):戳一戳作为第二种 @ 等价触发;有人入群时让 Hermes 自决要不要欢迎(noop 合法,不做模板欢迎语)
  • 🧪 消息段感知扩展 (0.3.4+, 实验性):语音/视频/QQ 表情/sticker 占位文本注入 LLM 视野;sticker 自动跳过 vision API。OneBot v11 NapCat 显式 @ 时贴 emoji 回执(HERMES_ACK_FEEDBACK_ENABLED=true)
  • 合并转发处理 (0.4.0+):群里收到合并转发消息时展开为有限长度摘要;bot 自身长回复在 OneBot v11 群里转为合并转发避免截断

快速开始

1. 前置条件

  • 已安装并运行 Hermes Agent,且 API Server 已启用
  • 已安装 NoneBot2 和对应平台的 adapter

2. 启用 Hermes API Server

~/.hermes/.env 中添加配置:

# 启用 API Server 并指定端口
API_SERVER_ENABLED=true
API_SERVER_PORT=8642
# 如果 NoneBot 和 Hermes 不在同一台机器上,需要监听所有 IP:
# API_SERVER_HOST=0.0.0.0

设置 API Key(必须,用于会话保持):

# 生成密钥
python3 -c "import secrets; print(secrets.token_hex(32))"
# 或 openssl rand -hex 32

# 写入 Hermes 环境配置
echo 'API_SERVER_KEY=your-generated-key' | tee -a ~/.hermes/.env

Note: 不设置 API_SERVER_KEY 会导致 Session 续接被拒绝,每次对话无法保持上下文。

启动 Hermes Gateway:

hermes gateway

3. 安装插件

方式 A:使用 nb-cli 安装(推荐)

nb plugin install nonebot-plugin-hermes

方式 B:使用 pip / uv 安装

pip install nonebot-plugin-hermes
# 或 uv add nonebot-plugin-hermes

pyproject.toml 中添加插件(如果是 nb-cli 安装会自动添加):

[tool.nonebot]
plugins = ["nonebot_plugin_hermes"]

新建 NoneBot 项目的完整步骤

pip install nb-cli
nb create          # 创建项目,选择 fastapi 驱动器
nb plugin install nonebot-adapter-onebot  # 安装对应平台的适配器,例如 OneBot
nb plugin install nonebot-plugin-hermes   # 安装 Hermes 插件

4. 配置

复制示例配置:

cp .env.example .env

编辑 .env,主要配置:

# OneBot 正向 WebSocket
ONEBOT_WS_URLS=["ws://127.0.0.1:3001"]

# Hermes API
HERMES_API_URL=http://127.0.0.1:8642
HERMES_API_KEY=

# 群聊触发
HERMES_GROUP_TRIGGER=at

5. 运行

nb run

可用的 AI 工具

本插件通过 Hermes 的 api_server 平台通信,默认使用 hermes-api-server 工具集:

工具类别 包含的工具
Web 搜索与提取 web_search, web_extract
终端与进程 terminal, process
文件操作 read_file, write_file, patch, search_files
视觉与图片生成 vision_analyze, image_generate
浏览器自动化 browser_navigate, browser_snapshot
规划与记忆 todo, memory, session_search
代码执行与委托 execute_code, delegate_task
定时任务 cronjob
智能家居 ha_list_entities, ha_get_state

🔒 安全最佳实践:限制 API Server 工具集

默认的 hermes-api-server 工具集包含 terminalexecute_code 等危险工具。针对不同的部署环境,强烈建议配置不同的受限工具集,特别是在公共群聊中,必须禁止文件读写(file 工具)以防敏感信息泄露或被植入后门。

~/.hermes/config.yaml 中配置 platform_toolsets

platform_toolsets:
  # 其他平台保持默认
  cli: [hermes-cli]
  telegram: [hermes-telegram]

  # API Server 根据部署场景选择工具集 (见下方推荐)
  api_server: [web]

推荐的部署安全级别:

部署场景 推荐配置 包含的工具集 说明
🔴 公共群聊 (极简防刷) [web] web (联网搜索) 对外公开机器人的最稳妥配置。 杜绝文件操作,同时避免画图/识图带来的高昂 API 费用和合规封号风险。
🟠 公共群聊 (含多媒体) [safe] 搜索 + 识图 + 画图 等同于 [web, vision, image_gen]。增加了视觉能力,但需注意防范 API 被刷或恶意图片封号的风险。
🟡 内部/信任群聊 (受限读写) [web, vision, image_gen, memory, session_search] 搜索 + 多媒体 + 记忆 适合公司内部群或好友群。允许发图画图、保留跨会话记忆,但依然严格禁止文件读写。
🟢 站长私聊 (高级管理) [web, file, vision, image_gen, skills, todo, memory, session_search] 包含文件读写、技能管理等 适合机器人主人的私聊。有文件读写能力,可通过群白名单机制将其他群屏蔽。
💀 危险/开发环境 (完全信任) [hermes-api-server] 包含终端、代码执行等全部工具 (默认)仅限开发者自己在安全的隔离环境使用。

[!WARNING] 关于 memorysession_search 的跨群隐私泄露风险: Hermes Agent 的底层数据库是全局共享的(无平台/群组隔离)。如果在多群共用的 Agent 上开启这两个工具,A群的成员可以搜到B群的聊天记录,甚至你的私人终端/私聊记录。若看重隐私隔离,多群共用时请勿包含 memorysession_search。普通的上下文多轮对话由临时 Session 维护,不受关闭这两个工具的影响。

🆔 用户身份与元数据注入

本插件会自动向 Hermes API 注入以下元数据,使后端 LLM 具备环境感知能力:

  • 用户标识 (user_id): 用户的平台 ID(如 QQ 号)。
  • 群组标识 (group_id): 消息来源群号(私聊则为空)。
  • 适配器名称 (adapter_name): 消息来源平台(如 OneBot V11, Discord, Telegram 等)。
  • 私聊状态 (is_private): 当前是否为私聊环境。

后端 Prompt 可以通过这些信息实现个性化称呼或针对特定平台的功能逻辑。

🔄 会话轮换(0.4.5+)

插件用 X-Hermes-Session-Id 请求头维持会话连续性,key 由 {adapter}+{private|group}+{ids} 派生(/clear 递增 -gN)。但这个 id 不是永久不变的:Hermes 自动压缩上下文时会轮换会话 —— 旧 id 被置为 end_reason='compression' 并关闭,新建一个 continuation 子会话,新 id 通过 响应头 X-Hermes-Session-Id 回传。

插件从 0.4.5 起采纳这个回传值,并把 internal_id → session key 映射持久化到消息库同目录的 session_keys.db(/clear 的 generation 也一并持久化,重启不再复活被清掉的会话)。

0.1.0 ~ 0.4.4 不读这个响应头,后果是每轮都把会话钉回已关闭的父会话:读还能跟随 tip,写全部 失败,而且每压缩一次就再分叉一个兄弟快照。live 子会话超过一个后,Hermes 的 find_live_compression_child() 判定歧义并 fail-closed,该会话从此永久写不进去。旧版本的 Hermes 容忍往已关闭会话追加,所以这个问题长期无声;上游 2026-07-23 的 fix(compression): recover rotated session lineage 之后变成硬失败,日志会刷 Session '…' is closed by compression。存量损坏用 hermes-repair-sessions 修。

🧠 长期记忆作用域(0.5.0+,默认关)

Hermes 侧的长期记忆(memory provider,目前是 Honcho)默认不按群区分。插件不告诉它 "这段对话属于谁"时,它按自己的兜底策略给记忆命名,两种兜底都有问题:

  • 全局 / 按目录策略 → 所有群共写一份记忆,bot 在 A 群知道的事会在 B 群说出来;
  • 按会话策略 → 以 Hermes 会话 id 为记忆键,而这个 id 每次自动压缩都会轮换(见上一节), 于是记忆每压缩一次换一本,长期什么都攒不下来。

开启本功能后,插件用 X-Hermes-Session-Key 请求头显式告诉上游记忆该记在谁名下。这个头与 X-Hermes-Session-Id 是两个独立维度:后者管"接哪段对话历史",会随 /clear 和自动压缩轮换; 前者管"记忆记在谁名下",跨 /clear、跨压缩恒定不变。

HERMES_HONCHO_ENABLED=true
# 群记忆按群还是按人:false = 一个群一份,群成员共享;true = 群内每人一份
HERMES_GROUP_SESSIONS_PER_USER=false

部署 Honcho 的 compose 与配置示例见 honcho/ 目录(含成本说明与验证方法)。

前置条件(缺任一条,功能静默无效):

  1. Hermes 端已配好 memory provider(hermes memory setup)。Honcho 本身要么用 Honcho Cloud (按量计费),要么自托管一套 Postgres + pgvector + FastAPI 服务,不是加个开关就有的东西。
  2. 插件配了 HERMES_API_KEY。上游对这个头要求鉴权,没 key 时插件不发头并在启动日志 WARN。
  3. Hermes 端的 ~/.hermes/honcho.json 不能有 peerName 这个键。它有值时所有群共用一个 memory peer,画像层(representation / peer card)会跨群共享,隔离只做到对话记录那一层。 hermes memory setup 向导会把它默认成当前用户名且不接受留空,跑完要手动删掉。 详见 honcho/README.md

切换代价:开启后记忆作用域改名,此前累积在旧作用域下的记忆不再被读到。数据仍在 Hermes 侧,关掉开关即回原状。另外记忆需要累积,头一两周体感不明显。

记忆 key 默认形如 agent:main:nonebot-{adapter}:group:{group_id},对齐 Hermes 原生 adapter 的 命名格式,nonebot- 前缀用于防止与 Hermes 原生 adapter 写进同一个 workspace 时撞名。三个模板 (群共享 / 群按人 / 私聊)都可通过 HERMES_*_SESSION_KEY_FORMAT 覆盖,一般不需要。

[!NOTE] 本开关只隔离记忆session_search 工具搜的是整个 state.db,不分群(见上文 "限制 API Server 工具集"的警告),要一并堵住需在 Hermes 端的 platform_toolsets.api_server 里移除该工具。终端 / 文件类工具的工作区同理不受本开关影响。

群活跃态 + 反向通道(M1,实验性)

启用后,bot 在被 @ 之后会进入 5 分钟"活跃窗口"——期间能听到所有群消息(无需再 @),由 Hermes Agent 通过结构化决策(should_reply / should_exit_active)自行判断是否插话。同时插件起一个本地 MCP server,让 Hermes 可以主动 push 消息进群(延迟回复、异步通知等)。

启用

.env 中:

HERMES_ACTIVE_SESSION_ENABLED=true
HERMES_MCP_ENABLED=true

启用 HERMES_ACTIVE_SESSION_ENABLED 时被动感知会自动开启(消息缓冲是活跃态的依赖),无需再单独设置 HERMES_PERCEPTION_ENABLED。后者只在 active=false 的群聊里有意义——给 @bot 那一刻的 LLM 注入旁观历史。

重启后 bot 会:

  • 监听 127.0.0.1:8643 暴露 MCP 工具:push_message / list_active_sessions / get_recent_messages / get_message_images
  • 在 @bot 触发后进入 reactive 模式,5 分钟内对群消息做 should_reply 决策(每次插话续期)
  • 把每条群消息持久化到 SQLite(默认走 nonebot-plugin-localstore,通常 ~/.local/share/nonebot2/nonebot_plugin_hermes/messages.db)并分配稳定 msg_id;<recent_messages> prompt 块的每条历史前缀变成 [m:<id>],Hermes 凭此 id 调 get_message_images 取回历史图字节

⚠️ 安全注意 ——HERMES_MCP_HOST 默认 127.0.0.1(loopback)。 改成监听公网 / 局域网地址在技术上完全可行,但安全后果是:push_message 工具能让 bot 往群里发任意内容,而当前防御仅有 Bearer token(明文 HTTP 传输,且与 HERMES_API_KEY 同钥匙)。改之前请配套上反向代理(TLS 终结) + 来源 IP ACL,否则任何能 reach 该端口的进程一旦拿到 token 就可以冒名发送。

把插件能力告诉 Hermes Agent

插件自带一份 SKILL.md(reactive 决策契约 + 反向通道用法),要装到 Hermes 那台<HERMES_HOME>/skills/nonebot-bridge/(默认 ~/.hermes/skills/nonebot-bridge/)—— 装错机器的话 Hermes 读不到,skill 不生效。安装位置跟随 HERMES_HOME(0.5.1+),所以多 profile 部署下按 profile 分别装:HERMES_HOME=~/.hermes/profiles/<name> hermes-install-skill

bot 与 Hermes 同机时,在 bot 项目目录下任选一种执行:

# 用 uv 管理依赖
uv run hermes-install-skill

# 或者 bot 项目用普通 venv
.venv/bin/hermes-install-skill

# 或者已激活虚拟环境
hermes-install-skill

# 备用入口(任何能 import nonebot-plugin-hermes 的环境)
python -m hermes_install_skill

分机部署时,Hermes 那台一般没装插件,不必为此装一遍 —— 脚本只用标准库,克隆仓库直接跑即可 (它按相对路径读同仓库的 nonebot_plugin_hermes/skill/SKILL.md,所以要整个仓库目录,不能只拷单文件):

git clone https://github.com/gsskk/nonebot-plugin-hermes.git
cd nonebot-plugin-hermes
python3 hermes_install_skill.py

(也可以在 bot 那台跑完再把 ~/.hermes/skills/nonebot-bridge/SKILL.md 拷过去,效果一样。)

然后在 ~/.hermes/config.yaml 注册插件 MCP server,把 <HERMES_API_KEY> 替换为你前面生成的同一把密钥(用于双向鉴权):

mcp_servers:
  nonebot-bridge:
    url: http://127.0.0.1:8643/mcp
    headers: { Authorization: "Bearer <HERMES_API_KEY>" }

后续插件 SKILL.md 升级时,用上面同样的入口加 --force 重装,例如 uv run hermes-install-skill --force.venv/bin/hermes-install-skill --force

按群路由到不同 Hermes 接入点(0.5.1+,默认关)

让指定的群走各自的 Hermes profile,从而拥有该群独占的工具集、模型和文件工作区。

先确认你要的是不是这个:只想让 bot 别把 A 群的事在 B 群说出来,用上文的 HERMES_HONCHO_ENABLED 就够了 —— 单进程、不改部署、不用为每个群维护一份 profile。本节解决的是 另一件事:A 群只能查资料、B 群能跑代码这种按群给不同能力。它顺带也隔离了记忆(profile 各有 自己的 state.db),代价是每多一个接入点,Hermes 侧就多一份要维护的 HERMES_HOME

配置

# 键是 {adapter}:{group_id};未列出的群和所有私聊走默认的 HERMES_API_URL
HERMES_GROUP_ENDPOINTS='{"onebotv11:12345": {"url": "http://127.0.0.1:8642/p/team-a", "key": "<team-a 的 API_SERVER_KEY>"}}'

两种部署形态共用同一个 url 字段:

形态 Hermes 侧 url
多路复用(推荐) hermes config set gateway.multiplex_profiles true 后重启 gateway http://host:8642/p/<profile>
独立进程 每个 profile 各起一个 api server http://host:8643(各自端口)

key 留空会沿用全局 HERMES_API_KEY,timeout 留空沿用 HERMES_API_TIMEOUT

[!WARNING] profile 名必须全小写(合法字符 [a-z0-9][a-z0-9_-]{0,63})。hermes profile create TeamA 会把名字 归一化成小写再落盘(profiles/teama/),但 URL 前缀不做归一化 —— 上游只 strip() 后直接和目录名 集合比对,所以 /p/TeamA/ 对着 profiles/teama/ 会直接 404。名字里想分词就用 -_

[!IMPORTANT] 指向命名 profile 时 key 必填,而且必须与默认 profile 的不同、不短于 16 字符。 三个原因: 上游校验的是该 profile 自己的 API_SERVER_KEY(沿用全局 key 必然 401);它是反向通道认身份的 依据(见下);而且它是**"忘开 gateway.multiplex_profiles"唯一的告警器** —— 多路复用关闭时上游会 静默忽略 /p/<profile>/ 前缀、把请求当默认 profile 处理,此时只有 key 不匹配才会报 401, 否则你会看到"一切正常"但零隔离。

老的默认接入点不会失效

打开多路复用后,原来那个监听器仍由默认 profile 持有:不带前缀的老 URL、老 key 继续可用, API_SERVER_KEY 放在 systemd Environment= / docker environment: 里也照样读得到(上游对默认 profile 的凭证读取保留了 os.environ 回落)。要迁的只有新增的命名 profile,它的 key 必须放在 该 profile 自己的 .env 里。

Hermes 侧要做的事

一次性(在默认 profile 上做,它才是多路复用器):

hermes config set gateway.multiplex_profiles true
hermes gateway restart

多路复用打开后,不要再为次级 profile 单独 hermes gateway start,也不要在次级 profile 的 config.yaml 里启用 api_server —— 端口绑定类平台留在默认 profile,次级 profile 靠 /p/<profile>/ 前缀被访问。反过来,如果你选的是"独立进程"形态,那就别开多路复用。

hermes profile create 结尾打印的 Next steps 里那条 <name> gateway start 是写给默认的 "一进程一 profile"部署的,多路复用下别执行(<name> setup 要执行,<name> chat 可以用来 验证 key 配好了)。上游确实有守卫会拒绝它并让你改用默认 profile 的 hermes gateway restart, 但那个守卫有两个前提、不能当保险:它要默认 gateway 正在运行才探测得到(默认 gateway 停着时 这条命令会成功起一个独立进程,之后拉起多路复用器就双绑:同一 bot token 两个 poller、端口冲突), 而且要该 profile 在 multiplex_profile_allowlist 的服务范围内,被排除掉时守卫直接放行。

hermes config set gateway.multiplex_profiles true 可能会打印一条 not a recognized config key 并建议你改成 gateway.multiplex_profile_allowlist —— 别照着改。这个键运行时确实会被读 (gateway/config.py 里有专门认它的分支,注释直接点名这条命令),告警只是上游 CLI 的键表没登记 嵌套形式;想消掉就用顶层形式 hermes config set multiplex_profiles true(等价)或加 --forcemultiplex_profile_allowlist 是另一件事:哪些命名 profile 被多路复用器服务,留空不配 = 全部服务, 设成 [](或写错类型 fail-safe 成 [])则只服务默认 profile,你的 /p/team-a/ 会 404。

改完重启,然后用这条确认前缀真的生效(唯一的 ground truth):

curl -s -o /dev/null -w '%{http_code}\n' \
  -H "Authorization: Bearer <team-a 自己的 API_SERVER_KEY>" \
  http://<hermes-host>:8642/p/team-a/v1/models
# 200 = 生效;401 = 前缀被静默忽略(当默认 profile 处理了,等于没开);
# 404 = 前缀被拒(profile 不存在,或被 allowlist 排除)

每个新接入点各一次:

export TEAM_HOME=~/.hermes/profiles/team-a

hermes profile create team-a                       # 独立 state.db / 记忆 / skills / config.yaml
team-a setup                                       # 给它自己的大模型 API key(见下方 WARNING,别跳过)
echo "API_SERVER_KEY=$(openssl rand -hex 32)" >> $TEAM_HOME/.env   # 必须与默认 profile 不同

# 这个群能用什么能力 —— 本功能唯一不可替代的价值就在这一步。
# 编辑 $TEAM_HOME/config.yaml 的 platform_toolsets.api_server,工具集选法见上文
# 「限制 API Server 工具集」那张表。

HERMES_HOME=$TEAM_HOME hermes-install-skill       # skill 按 profile 分别装

# 反向通道:只给需要它的 profile 配。**两种部署形态写法不同**,见下面
# 「反向通道自动跟着收敛」小节 —— 多路复用下 Bearer 必须配在默认 profile 上,
# 这里写的 header 不会生效。独立进程形态才是下面这条:
HERMES_HOME=$TEAM_HOME hermes mcp add nonebot --url http://<bot>:8643/mcp
#   Bearer 填上面那把 API_SERVER_KEY

两边唯一需要一致的值就是这把 API_SERVER_KEY:插件侧写进 HERMES_GROUP_ENDPOINTS[...].key, Hermes 侧既是它的 API_SERVER_KEY 也是它的 MCP token。轮换一次改两处、覆盖两个方向。

hermes profile create 还会在 ~/.local/bin/<name> 生成一个 wrapper(内容是 exec hermes -p <name> "$@"),于是:

  • profile 名会变成一个 shell 命令。 保留名只有 hermes / test / tmp / root / sudo, 像 webtopdocker 这种照样能建,并且会在 PATH 里抢在原命令前面 —— 起名前先 command -v <name> 看一眼。
  • 之后该 profile 的 hermes 子命令可以直接 team-a config set … / team-a mcp add …,不必写 HERMES_HOME=…。但 hermes-install-skill本插件的独立 CLI,不走这个 wrapper,仍要带 HERMES_HOME=

[!WARNING] 多路复用下必须给命名 profile 自己的 LLM provider key —— 就是大模型厂商那把 API key (ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY / NOUS_API_KEY / GEMINI_API_KEY 之类),没有它 agent 连一次推理都发不出去。注意它与本节另外两把 key 是三件 不同的东西:

这把 key 谁验它 放哪
LLM provider key(如 ANTHROPIC_API_KEY) 大模型厂商 profiles/<name>/.env
API_SERVER_KEY Hermes 自己的 api_server 入站鉴权 profiles/<name>/.env
插件的 HERMES_API_KEY / 条目 key 同上,是插件出向呈上的那一把 bot 的 .env

hermes profile create 结尾会提示"否则会继承你 shell 环境里的 key" —— 那只在单 profile 部署 成立。多路复用打开后,凭证读取以 profile 的 secret scope 为权威且不回落 os.environ (全局豁免表里只有 PATH / HOME / API_SERVER_HOST|PORT|ENABLED 这类部署项,没有任何 API key), 所以 .env 空着的 profile 里 agent 一次都跑不起来。跑 <name> setup,或把 key 直接写进 profiles/<name>/.env同一条规则适用于该 profile 用到的所有凭证,不止 LLM —— 搜索(EXA_API_KEY 等)、图片生成、memory provider 的 key 也都要在它自己的 .env 里。

反向通道自动跟着收敛

反向通道(push_message / get_recent_messages / get_message_images / list_active_sessions) 没有第二张 token 表:调用方呈上哪个接入点的 key,就只能操作该接入点名下的群;呈上全局 HERMES_API_KEY 则只能操作补集(不在路由表里、或条目没有自己 key 的那些群);两者都不是就 401。 不配路由表时补集 = 全部群,行为与 0.5.0 完全一致。

也就是说:想被保护的群必须进路由表并指向一个命名 profile。留在补集里的群,补集那把 key 的持有者 (默认 profile)照样能读能推。

[!IMPORTANT] 多路复用下,"哪个 profile 有反向通道"可以按 profile 控;"它呈哪把 token"不行。 上游把这件事 分成两层:

读谁的 config 时机
连接(进程里有没有这个 MCP client) 默认 profileconfig.yaml gateway 启动一次,注册表全进程共享
可用性(agent 拿不拿到这些工具) 被路由到的那个 profileconfig.yaml 每请求读

所以多路复用下:

  • Bearer 必须配在默认 profile 上。命名 profile 里 hermes mcp add 写的 url / headers 不会生效 —— 同名 server 全进程共用默认 profile 建的那一个连接。
  • 反过来,要不要给某个 profile 反向通道是它自己说了算:它的 platform_toolsets.api_server 列出该 server 名 = 打开;放特殊哨兵 no_mcp = 该 profile 完全没有 MCP 工具;两者都不写则它的 MCP 名单为空,同样拿不到。这一层每请求读、改完不用重启。
  • 照最直觉的配法(默认 profile 一个 nonebot server),scope 不随 profile 变:插件看到的永远 是那把共享 token,通常就是全局 HERMES_API_KEY(scope = 补集),于是进了路由表的群谁都推不进去 —— 会被拒并留 WARNING(fail-closed,但等于那些群没有反向通道)。

想在多路复用下让 token 也按接入点分开,可以做到 —— MCP 工具名按 server 名 namespace (mcp__<server>__<tool>),所以同一个 URL 可以用不同名字连多次:

# ~/.hermes/config.yaml(默认 profile —— 连接与 token 都由它建立)
mcp_servers:
  nonebot-team-a:
    url: http://<bot>:8643/mcp
    headers: { Authorization: "Bearer <team-a  API_SERVER_KEY>" }
  nonebot-lab:
    url: http://<bot>:8643/mcp
    headers: { Authorization: "Bearer <lab  API_SERVER_KEY>" }
# ~/.hermes/profiles/team-a/config.yaml —— 只声明属于自己的那个名字
mcp_servers:
  nonebot-team-a: { url: http://<bot>:8643/mcp }   # url/headers 不生效,声明名字即开启

这样 team-a 的 agent 只看到 mcp__nonebot_team_a__*,发出的请求带 team-a 那把 token,插件的 scope 判定就按接入点成立了。默认 profile 自己也要显式 allowlist,否则它会拿到全部 server 名、 也就拿到了操作别人群的能力:

# ~/.hermes/config.yaml
platform_toolsets:
  api_server: [<你原来那些工具集>, nonebot-default]   # 只列自己那个名字

三个代价:默认 profile 的 config.yaml 里握着全部接入点的 token(所有连接都由它建,所以默认 profile 必须可信 + 受限工具集,它的 agent 能读文件就能拿到全部 token);每个 server 名一条常驻 连接;名字两处必须一致,拼错就是那个 profile 静默没有工具。Bearer 若写成 ${MCP_*_API_KEY} 引用形式,变量必须在默认 profile 的 .env 里 —— 插值发生在启动建连时的默认作用域下。

启动期 validate_endpoints() 会对"路由表里有 /p/<profile> 形式的 url 且 HERMES_MCP_ENABLED=true"这个组合告警,提醒你按上面这样配。

被拒时 bot 侧会留一条 WARNING,写清调用方属于哪个接入点、它的范围、被拒的目标 —— 排查 "某个群的反向推送忽然不工作"时先看这条。

运维成本与已知限制

  • 每个 profile 是一份完整的 HERMES_HOME:hermes-repair-sessions 要按 profile 分别跑, skill 升级要按 profile 分别装。
  • 改动某个群的路由条目后要对该群 /clear:session 血缘不带接入点维度,旧 session id 在新 profile 里不存在,上游会静默开一段新会话,同名 session 分居两份 state.db。
  • /ping 只探当前会话自己的接入点(它对普通用户开放,不能列别群的路由键);逐接入点体检在 管理员命令 /hermes-status 里。
  • 启动期的上游能力探测(/v1/capabilities)只探默认接入点,命名 profile 的 Hermes 版本偏旧不会告警。
  • 启动日志会对路由表里"永远匹配不上的键 / 非 http(s) 地址 / 缺 key / key 过短 / 同一接入点多把 key" 逐条 WARN。
  • session_search 仍是另一条跨群通道:分了 profile 自然分开;不分 profile 又想堵,在该 profile 的 platform_toolsets.api_server 里移除该工具。

历史图片召回(0.3+,实验性)

在 0.3 起,消息感知 + 反向通道一起开启时,bot 自动启用一条"按消息 id 精确召回历史图"的通路。典型场景:

T0    用户 A:  [图片]                    ← 仅文字描述,bot 看到 [图片] 占位
T+5s  用户 B:  @bot 评价下上图
                ↓
                Hermes 看到 prompt 里 [m:1234] A: [图片]
                Hermes 调 get_recent_messages → 知道 m:1234 有图(image_count=1)
                Hermes 调 get_message_images([1234]) → 拿到字节
                下一轮 LLM 真的看到那张图,回复正常

技术细节:

  • 持久化:消息进 SQLite,路径由 nonebot-plugin-localstore 管理(默认 ~/.local/share/nonebot2/nonebot_plugin_hermes/messages.db,可被 LOCALSTORE_* env vars 整体重定向);自增 id 即 [m:<id>] 前缀的 N
  • 字节缓存:perception 看到图后异步抓 URL → 落到 localstore 管理的 cache dir(默认 ~/.cache/nonebot2/nonebot_plugin_hermes/images/<sha256>.<ext>),LRU 按 atime 淘汰,默认 200MB 上限
  • 失败降级:URL 短效过期 / 缓存被淘汰 / 消息已过 30 天保留期 → MCP 工具返回 available: false,Hermes 礼貌告知用户图不可用,不崩
  • 保留窗口:消息 30 天或 10 万条上限(谁先到),整点 :37 后台 vacuum

如果你的 Hermes 后端模型偏弱、识别 [m:<id>] 约定不稳,bot 行为退化为今天的"看不到上图"——无 regression。

命令

命令 说明
/clear 重置对话,开始新会话
/ping 检查 Hermes Agent 连接状态
/help 显示帮助信息
/hermes-status 打印 M1 运行时状态(MCP / 活跃 sessions / buffer / registry)。需在 HERMES_ADMIN_USERS 显式授权 adapter:user_id;非管理员调用时静默无响应,且 /help 输出里也不出现该命令

命令行工具

命令 在哪台跑 说明
hermes-install-skill --force Hermes 那台 SKILL.md 装到 ~/.hermes/skills/nonebot-bridge/(覆盖已装版本要带 --force)
hermes-purge-media bot 那台 清理插件消息库(messages.db)里内联的 base64 图片字节。默认只报告,--apply 写回,--vacuum 收缩文件
hermes-repair-sessions Hermes 那台 解开 Hermes state.db 里被 compression 血缘歧义卡死的会话。默认只报告,--apply 备份后修复

「在哪台跑」取决于工具动的是谁的数据,不是谁装了插件。bot 与 Hermes 同机时三个命令都能直接敲; 分机部署时 Hermes 那台通常并没有装本插件,此时不需要为了跑工具去装一遍——三个脚本都是仓库 根目录下的单文件、只用标准库、也不 import 本包:

git clone https://github.com/gsskk/nonebot-plugin-hermes.git
cd nonebot-plugin-hermes
python3 hermes_repair_sessions.py            # 与 hermes-repair-sessions 完全等价

(只拷单个文件过去跑也行;hermes-install-skill 例外——它要读同仓库里的 nonebot_plugin_hermes/skill/SKILL.md,得带上仓库目录。)

hermes-purge-media 用于清理历史遗留:早期版本会把 agent 回复里 api_server 内联的 data:image/…;base64,… 整段存进消息库,单条可达 MB 级。当前版本写入端与渲染端都已挡住, 这个命令只把存量字节清出去。

bot 那台(插件装在那儿)执行:

uv run hermes-purge-media                    # 只报告:每群命中数、最大行、可回收字节
uv run hermes-purge-media --apply --vacuum   # 清理并收缩文件

bot 项目用普通 venv 就换成 .venv/bin/hermes-purge-media;已激活虚拟环境时可直接敲 hermes-purge-media。裸命令只在虚拟环境已激活时才在 PATH 上。

不删消息,只把图片 payload 换成 [图片] 占位;幂等,可反复运行。--vacuum 需要排它锁, 拿不到时停掉 bot 再跑。

hermes-repair-sessions 修的是 Hermes 侧 state.db 的会话血缘。症状是 Hermes 日志反复刷:

Session '…' is closed by compression; adopt its live continuation before appending messages
compression skipped: … no unique live child could be adopted

成因见上文「会话轮换」:0.1.0 ~ 0.4.4 的插件不采纳轮换后的 session id,每压缩一次就从同一个 已关闭的父会话再分叉一个快照子会话;live 子会话超过一个后上游判定歧义并 fail-closed,该会话 从此写不进去 —— 对话记录冻结,上下文还会无限膨胀(压缩永远跑不完)。

Hermes 那台执行。那台一般没装插件,所以下面直接用单文件形式:

systemctl stop hermes-gateway     # 修复期间要拿写锁,跑着的 agent 也可能持有旧会话状态

git clone https://github.com/gsskk/nonebot-plugin-hermes.git
cd nonebot-plugin-hermes
python3 hermes_repair_sessions.py            # 只报告:哪些会话卡住、会动哪些行
python3 hermes_repair_sessions.py --apply    # 整库备份后:重开父会话 + 退休快照子会话

systemctl start hermes-gateway

与 bot 同机、插件已装时,把这两行换成 uv run hermes-repair-sessions [--apply] 即可。

不删任何消息行。先把插件升级到 0.4.5+ 并重启,再停掉 gateway 跑修复 —— 否则下一次压缩 会把父会话再次关闭,几轮之内又卡回去。某个子会话若是被真正续写过的 continuation (消息跨度远超一次批量写入),脚本会跳过该会话并报告,交给人判断。

配置项

所有配置项通过 .env 文件设置,参见 .env.example 中的详细注释。

配置项 默认值 说明
HERMES_API_URL http://127.0.0.1:8642 Hermes API Server 地址
HERMES_API_KEY (空) API 密钥(建议设置以启用会话持久化)
HERMES_API_TIMEOUT 300 API 请求超时时间(秒)
HERMES_GROUP_ENDPOINTS {} 按群路由的接入点表,键 {adapter}:{group_id},值 {"url": …, "key": …, "timeout": …}。空表 = 全部走 HERMES_API_URL。也是反向通道 scope 的唯一来源,详见上文「按群路由到不同 Hermes 接入点」
HERMES_GROUP_TRIGGER at 群聊触发方式: at / all / keyword
HERMES_KEYWORDS ["/ai"] keyword 模式下的触发关键词
HERMES_PRIVATE_TRIGGER all 私聊触发方式: all / allowlist
HERMES_ALLOW_USERS [] 允许私聊的用户 ID 列表 (allowlist 模式)
HERMES_ALLOW_GROUPS [] 允许响应的群组 ID 列表(空为全部允许)
HERMES_ADMIN_USERS [] 管理员白名单,格式 ["telegram:<user_id>", "onebotv11:<user_id>"]默认空集 = deny by default;/hermes-status 等敏感命令必须命中此列表才执行
HERMES_SESSION_SHARE_GROUP false 群内是否共享同一个 session
HERMES_HONCHO_ENABLED false 发送 X-Hermes-Session-Key,按群/私聊隔离 Hermes 侧长期记忆并让记忆不随压缩轮换重置。需要上游配了 memory provider + 本插件配了 HERMES_API_KEY,详见上文「长期记忆作用域」
HERMES_GROUP_SESSIONS_PER_USER false 群记忆按群还是按人。false = 一个群一份(成员共享);true = 群内每人一份
HERMES_GROUP_SESSION_KEY_FORMAT agent:main:nonebot-{adapter}:group:{group_id} 群共享记忆 key 模板
HERMES_GROUP_PER_USER_SESSION_KEY_FORMAT agent:main:nonebot-{adapter}:group:{group_id}:{user_id} 群按人记忆 key 模板
HERMES_PRIVATE_SESSION_KEY_FORMAT agent:main:nonebot-{adapter}:dm:{user_id} 私聊记忆 key 模板
HERMES_MAX_LENGTH 4000 单条回复最大长度(超出后截断)
HERMES_IGNORE_PREFIX ["."] 以这些字符开头的消息不触发回复
HERMES_PERCEPTION_ENABLED false 群聊 + active_session=false 下,是否在 @bot 时给 LLM 注入旁观历史。HERMES_ACTIVE_SESSION_ENABLED=true 时自动隐含为 on,本开关无效。私聊永远不注入(Hermes session 已覆盖)
HERMES_PERCEPTION_BUFFER 10 被动感知缓存的历史消息数量
HERMES_PERCEPTION_TEXT_LENGTH 200 被动感知单条历史消息最大长度
HERMES_PERCEPTION_IMAGE_MODE placeholder ⚠️ 0.3 起弃用——历史图召回改走 get_message_images MCP 工具。本配置当前仅控制 [图片] 文本占位是否出现(none=不加占位;其他值=加占位)。inline_labeled 行为已被 MCP 工具流取代,设为该值与 placeholder 等效
HERMES_ACTIVE_SESSION_ENABLED false 启用群活跃态(M1)。false 时退化为 v0.1.6 等价行为
HERMES_ACTIVE_SESSION_TTL_SEC 300 活跃窗口 TTL(秒),每次插话滑动续期
HERMES_ACTIVE_SWEEP_INTERVAL_SEC 30 活跃态过期清扫 cron 频率(秒)
HERMES_POKE_TRIGGER_ENABLED false OneBot v11:被戳一戳时触发对话(私聊 / 群都生效,等价于被 @)。其他适配器静默忽略
HERMES_GREET_ON_JOIN false OneBot v11:有人加入群且 HERMES_ACTIVE_SESSION_ENABLED=true 时,触发一次 reactive turn 让 Hermes 自决是否欢迎(noop 是合法返回)。active 关时不触发
HERMES_ACK_FEEDBACK_ENABLED false 用户消息上显示 ack 回执(B-0 实装 OneBot v11 NapCat emoji)。B-0.5 规划扩 TG/Discord 私聊 typing
HERMES_ACK_EMOJI_ID 341 B-0 OneBot v11 路径下贴的 QQ 表情 id(默认 341 = /打招呼;373 /忙 = 打字动物;129 /挥手 = 经典挥手)
HERMES_BUFFER_PER_GROUP_CAP 200 ⚠️ 0.3 起空转——MessageBuffer 改为 SQLite 后端,无内存 per-group 上限;消息淘汰由 HERMES_STORAGE_MESSAGE_* 控制。下一个 major 版本会移除
HERMES_BUFFER_TOTAL_GROUPS_CAP 50 ⚠️ 0.3 起空转——同上,SQLite 后端无 LRU,改为 retention + 行数上限
HERMES_MCP_ENABLED false 启动内嵌 FastMCP server(M1 反向通道)
HERMES_MCP_HOST 127.0.0.1 MCP server 绑定地址。改成公开地址前请阅读上文「群活跃态 + 反向通道」节的安全注意
HERMES_MCP_PORT 8643 MCP server 绑定端口
HERMES_MCP_RECENT_LIMIT_MAX 50 get_recent_messages 工具单次最大返回条数
HERMES_STORAGE_DB_PATH (空) SQLite 消息日志路径。空值走 nonebot-plugin-localstore 的 plugin_data_dir(通常 ~/.local/share/nonebot2/nonebot_plugin_hermes/messages.db),也可被 LOCALSTORE_* env vars 重定向
HERMES_STORAGE_MESSAGE_RETENTION_DAYS 30 消息日志保留天数,vacuum cron 删超龄行
HERMES_STORAGE_MESSAGE_MAX_ROWS 100000 消息日志总行数硬上限,超出按 ts 老到新删
HERMES_IMAGE_CACHE_DIR (空) 图字节缓存目录。空值走 localstore 的 plugin_cache_dir(通常 ~/.cache/nonebot2/nonebot_plugin_hermes/images/)
HERMES_IMAGE_CACHE_QUOTA_MB 200 图缓存总体积上限(MB),vacuum 时按 atime 老到新淘汰
HERMES_IMAGE_FETCH_TIMEOUT_S 10 单图 HTTP 抓取超时秒数
HERMES_IMAGE_FETCH_MAX_ATTEMPTS 2 单图总尝试次数(1=不重试,2=一次重试,以此类推)

Busy notice(显式 @ 被 plumbing 丢单时的可见信号)

_refire 链触顶 MAX_REFIRE_DEPTH=3(同群短时间内塞了 ≥ 4 条 explicit @ 而上游 Hermes 跟不上)时,最新一条 explicit @ 会被 plumbing 丢掉。此时插件会在那条原消息上贴 HERMES_BUSY_EMOJI_ID(默认 97 = QQ 经典表情 /擦汗),不撤销,作为"我看见了但确实忙不过来"的视觉信号。

与 ack-feedback emoji(HERMES_ACK_EMOJI_ID,默认 341 /打招呼)是不同语义:

  • ack-feedback:chat() 期间常驻,完成后撤销,表示"工作中"
  • busy notice:depth-cap 触顶时常驻,不撤销,表示"工作不下去"

默认值刻意取互相区分明显的表情;改默认值前请验证 OneBot 实现端的 emoji_id 映射表。

仅 OneBot v11 群聊路径生效;其它 adapter(Telegram / Discord)或 msg_id 缺失时降级为 WARN 日志,不会文本兜底,避免在 burst 上下文里加噪声。

同样有一类失败路径有 user-visible 兜底:上游 Hermes 5xx / 网络断时,refire 路径上的 explicit @ 会发 HERMES_TRANSPORT_ERROR_FALLBACK_TEXT(默认"嗯…我这边遇到点状况,稍后再问一次")。设为空串可关闭文本兜底。

限制

由于通过 HTTP API 与 Hermes 通信(而非原生 Gateway Adapter),以下功能不可用:

  • ❌ 追问用户(clarify 工具)
  • ❌ 跨平台发消息(send_message 工具)
  • ❌ 语音合成发送(text_to_speech 工具)
  • ❌ 危险命令审批按钮
  • ❌ Cron 定时主动推送
  • ❌ 中断正在运行的 Agent

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nonebot_plugin_hermes-0.5.1.tar.gz (260.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nonebot_plugin_hermes-0.5.1-py3-none-any.whl (151.4 kB view details)

Uploaded Python 3

File details

Details for the file nonebot_plugin_hermes-0.5.1.tar.gz.

File metadata

  • Download URL: nonebot_plugin_hermes-0.5.1.tar.gz
  • Upload date:
  • Size: 260.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for nonebot_plugin_hermes-0.5.1.tar.gz
Algorithm Hash digest
SHA256 76375e6e759a6196758baaed5b491a25ba265f83e1c66f7880e6182f47f1a60f
MD5 368a5cdd232c5814b894244920f07df1
BLAKE2b-256 6dce769a8ba37d731ada5af2a1c0f1b03f7d2dde8956c457188183624e667c5a

See more details on using hashes here.

Provenance

The following attestation bundles were made for nonebot_plugin_hermes-0.5.1.tar.gz:

Publisher: pypi-publish.yml on gsskk/nonebot-plugin-hermes

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nonebot_plugin_hermes-0.5.1-py3-none-any.whl.

File metadata

File hashes

Hashes for nonebot_plugin_hermes-0.5.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d1ce29d700986c2abe7344e3324ceca0d89ec3924aefe90f3183e2f0e8514cb1
MD5 b4744ee402cc1a7a24042093884e3603
BLAKE2b-256 c4a49c69200148917c49cfd42d256d5bfb633f0b1573d4a2c05406c62589120e

See more details on using hashes here.

Provenance

The following attestation bundles were made for nonebot_plugin_hermes-0.5.1-py3-none-any.whl:

Publisher: pypi-publish.yml on gsskk/nonebot-plugin-hermes

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page