nonebot-plugin-hermes 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/PKG-INFO +21 -10
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/README.md +20 -9
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/__init__.py +1 -1
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/config.py +11 -8
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/core/hermes_client.py +118 -7
- nonebot_plugin_hermes-0.2.2/nonebot_plugin_hermes/core/inflight.py +71 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/core/prompt_builder.py +40 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/handlers/commands.py +28 -20
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/handlers/message.py +288 -8
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/mcp/__init__.py +6 -2
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes.egg-info/PKG-INFO +21 -10
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes.egg-info/SOURCES.txt +3 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/pyproject.toml +1 -1
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/tests/test_hermes_client_structured.py +119 -2
- nonebot_plugin_hermes-0.2.2/tests/test_inflight.py +91 -0
- nonebot_plugin_hermes-0.2.2/tests/test_message_handler_coalesce.py +543 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/tests/test_prompt_builder.py +65 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/hermes_install_skill.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/core/__init__.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/core/active_session.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/core/bot_registry.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/core/message_buffer.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/core/outbound.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/core/session.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/handlers/__init__.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/mcp/auth.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/mcp/server.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/mcp/tools/__init__.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/mcp/tools/get_recent_messages.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/mcp/tools/list_active_sessions.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/mcp/tools/push_message.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/scripts/__init__.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/scripts/install_skill.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/skill/SKILL.md +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/skill/__init__.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/tasks/__init__.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/tasks/expire_active_sessions.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes/utils.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes.egg-info/dependency_links.txt +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes.egg-info/entry_points.txt +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes.egg-info/requires.txt +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/nonebot_plugin_hermes.egg-info/top_level.txt +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/setup.cfg +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/tests/test_active_session.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/tests/test_bot_registry.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/tests/test_mcp_auth.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/tests/test_mcp_push_message.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/tests/test_mcp_read_tools.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/tests/test_message_buffer.py +0 -0
- {nonebot_plugin_hermes-0.2.0 → nonebot_plugin_hermes-0.2.2}/tests/test_session_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nonebot-plugin-hermes
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: NoneBot plugin for Hermes Agent — multi-platform AI chatbot via Hermes API Server
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/gsskk/nonebot-plugin-hermes
|
|
@@ -222,24 +222,35 @@ platform_toolsets:
|
|
|
222
222
|
在 `.env` 中:
|
|
223
223
|
|
|
224
224
|
```env
|
|
225
|
-
HERMES_PERCEPTION_ENABLED=true # 活跃态依赖消息缓冲做上下文
|
|
226
225
|
HERMES_ACTIVE_SESSION_ENABLED=true
|
|
227
226
|
HERMES_MCP_ENABLED=true
|
|
228
227
|
```
|
|
229
228
|
|
|
229
|
+
> 启用 `HERMES_ACTIVE_SESSION_ENABLED` 时被动感知会自动开启(消息缓冲是活跃态的依赖),无需再单独设置 `HERMES_PERCEPTION_ENABLED`。后者只在 active=false 的群聊里有意义——给 @bot 那一刻的 LLM 注入旁观历史。
|
|
230
|
+
|
|
230
231
|
重启后 bot 会:
|
|
231
232
|
|
|
232
233
|
- 监听 `127.0.0.1:8643` 暴露 MCP 工具:`push_message` / `list_active_sessions` / `get_recent_messages`
|
|
233
234
|
- 在 @bot 触发后进入 reactive 模式,5 分钟内对群消息做 should_reply 决策(每次插话续期)
|
|
234
235
|
|
|
235
|
-
> ⚠️
|
|
236
|
+
> ⚠️ **安全注意 ——`HERMES_MCP_HOST` 默认 `127.0.0.1`(loopback)。** 改成监听公网 / 局域网地址在技术上完全可行,但安全后果是:`push_message` 工具能让 bot 往群里发任意内容,而当前防御仅有 Bearer token(明文 HTTP 传输,且与 `HERMES_API_KEY` 同钥匙)。改之前请配套上反向代理(TLS 终结) + 来源 IP ACL,否则任何能 reach 该端口的进程一旦拿到 token 就可以冒名发送。
|
|
236
237
|
|
|
237
238
|
### 把插件能力告诉 Hermes Agent
|
|
238
239
|
|
|
239
|
-
插件自带一份 `SKILL.md`(reactive 决策契约 +
|
|
240
|
+
插件自带一份 `SKILL.md`(reactive 决策契约 + 反向通道用法)。在 bot 项目目录下任选一种执行(都是把 SKILL.md 装到 `~/.hermes/skills/nonebot-bridge/`):
|
|
240
241
|
|
|
241
242
|
```bash
|
|
242
|
-
|
|
243
|
+
# 用 uv 管理依赖
|
|
244
|
+
uv run hermes-install-skill
|
|
245
|
+
|
|
246
|
+
# 或者 bot 项目用普通 venv
|
|
247
|
+
.venv/bin/hermes-install-skill
|
|
248
|
+
|
|
249
|
+
# 或者已激活虚拟环境
|
|
250
|
+
hermes-install-skill
|
|
251
|
+
|
|
252
|
+
# 备用入口(任何能 import nonebot-plugin-hermes 的环境)
|
|
253
|
+
python -m hermes_install_skill
|
|
243
254
|
```
|
|
244
255
|
|
|
245
256
|
然后在 `~/.hermes/config.yaml` 注册插件 MCP server,把 `<HERMES_API_KEY>` 替换为你前面生成的同一把密钥(用于双向鉴权):
|
|
@@ -251,7 +262,7 @@ mcp_servers:
|
|
|
251
262
|
headers: { Authorization: "Bearer <HERMES_API_KEY>" }
|
|
252
263
|
```
|
|
253
264
|
|
|
254
|
-
后续插件 SKILL.md
|
|
265
|
+
后续插件 SKILL.md 升级时,用上面同样的入口加 `--force` 重装,例如 `uv run hermes-install-skill --force` 或 `.venv/bin/hermes-install-skill --force`。
|
|
255
266
|
|
|
256
267
|
## 命令
|
|
257
268
|
|
|
@@ -260,7 +271,7 @@ mcp_servers:
|
|
|
260
271
|
| `/clear` | 重置对话,开始新会话 |
|
|
261
272
|
| `/ping` | 检查 Hermes Agent 连接状态 |
|
|
262
273
|
| `/help` | 显示帮助信息 |
|
|
263
|
-
| `/hermes-status` | 打印 M1 运行时状态(MCP / 活跃 sessions / buffer / registry
|
|
274
|
+
| `/hermes-status` | 打印 M1 运行时状态(MCP / 活跃 sessions / buffer / registry)。**需在 `HERMES_ADMIN_USERS` 显式授权 `adapter:user_id`**;非管理员调用时静默无响应,且 `/help` 输出里也不出现该命令 |
|
|
264
275
|
|
|
265
276
|
## 配置项
|
|
266
277
|
|
|
@@ -276,10 +287,11 @@ mcp_servers:
|
|
|
276
287
|
| `HERMES_PRIVATE_TRIGGER` | `all` | 私聊触发方式: `all` / `allowlist` |
|
|
277
288
|
| `HERMES_ALLOW_USERS` | `[]` | 允许私聊的用户 ID 列表 (`allowlist` 模式) |
|
|
278
289
|
| `HERMES_ALLOW_GROUPS` | `[]` | 允许响应的群组 ID 列表(空为全部允许) |
|
|
290
|
+
| `HERMES_ADMIN_USERS` | `[]` | 管理员白名单,格式 `["telegram:<user_id>", "onebotv11:<user_id>"]`。**默认空集 = deny by default**;`/hermes-status` 等敏感命令必须命中此列表才执行 |
|
|
279
291
|
| `HERMES_SESSION_SHARE_GROUP` | `false` | 群内是否共享同一个 session |
|
|
280
292
|
| `HERMES_MAX_LENGTH` | `4000` | 单条回复最大长度(超出后截断) |
|
|
281
293
|
| `HERMES_IGNORE_PREFIX` | `["."]` | 以这些字符开头的消息不触发回复 |
|
|
282
|
-
| `HERMES_PERCEPTION_ENABLED` | `false` |
|
|
294
|
+
| `HERMES_PERCEPTION_ENABLED` | `false` | 群聊 + active_session=false 下,是否在 @bot 时给 LLM 注入旁观历史。**`HERMES_ACTIVE_SESSION_ENABLED=true` 时自动隐含为 on,本开关无效**。私聊永远不注入(Hermes session 已覆盖) |
|
|
283
295
|
| `HERMES_PERCEPTION_BUFFER` | `10` | 被动感知缓存的历史消息数量 |
|
|
284
296
|
| `HERMES_PERCEPTION_TEXT_LENGTH` | `200` | 被动感知单条历史消息最大长度 |
|
|
285
297
|
| `HERMES_PERCEPTION_IMAGE_MODE` | `placeholder` | 历史图片模式: `placeholder`(纯文本占位,推荐) / `inline_labeled`(带标签随多模态发送,适合跨图诉求) / `none`(不提) |
|
|
@@ -289,10 +301,9 @@ mcp_servers:
|
|
|
289
301
|
| `HERMES_BUFFER_PER_GROUP_CAP` | `200` | MessageBuffer 每群最近消息上限 |
|
|
290
302
|
| `HERMES_BUFFER_TOTAL_GROUPS_CAP` | `50` | MessageBuffer 跨群总容量(LRU 驱逐) |
|
|
291
303
|
| `HERMES_MCP_ENABLED` | `false` | 启动内嵌 FastMCP server(M1 反向通道) |
|
|
292
|
-
| `HERMES_MCP_HOST` | `127.0.0.1` | MCP server
|
|
304
|
+
| `HERMES_MCP_HOST` | `127.0.0.1` | MCP server 绑定地址。改成公开地址前请阅读上文「群活跃态 + 反向通道」节的安全注意 |
|
|
293
305
|
| `HERMES_MCP_PORT` | `8643` | MCP server 绑定端口 |
|
|
294
306
|
| `HERMES_MCP_RECENT_LIMIT_MAX` | `50` | `get_recent_messages` 工具单次最大返回条数 |
|
|
295
|
-
| `HERMES_STRUCTURED_PATH` | `prompt` | reactive 结构化输出路径: `prompt`(JSON5 解析) / `tools`(OpenAI tool_choice) |
|
|
296
307
|
|
|
297
308
|
## 限制
|
|
298
309
|
|
|
@@ -203,24 +203,35 @@ platform_toolsets:
|
|
|
203
203
|
在 `.env` 中:
|
|
204
204
|
|
|
205
205
|
```env
|
|
206
|
-
HERMES_PERCEPTION_ENABLED=true # 活跃态依赖消息缓冲做上下文
|
|
207
206
|
HERMES_ACTIVE_SESSION_ENABLED=true
|
|
208
207
|
HERMES_MCP_ENABLED=true
|
|
209
208
|
```
|
|
210
209
|
|
|
210
|
+
> 启用 `HERMES_ACTIVE_SESSION_ENABLED` 时被动感知会自动开启(消息缓冲是活跃态的依赖),无需再单独设置 `HERMES_PERCEPTION_ENABLED`。后者只在 active=false 的群聊里有意义——给 @bot 那一刻的 LLM 注入旁观历史。
|
|
211
|
+
|
|
211
212
|
重启后 bot 会:
|
|
212
213
|
|
|
213
214
|
- 监听 `127.0.0.1:8643` 暴露 MCP 工具:`push_message` / `list_active_sessions` / `get_recent_messages`
|
|
214
215
|
- 在 @bot 触发后进入 reactive 模式,5 分钟内对群消息做 should_reply 决策(每次插话续期)
|
|
215
216
|
|
|
216
|
-
> ⚠️
|
|
217
|
+
> ⚠️ **安全注意 ——`HERMES_MCP_HOST` 默认 `127.0.0.1`(loopback)。** 改成监听公网 / 局域网地址在技术上完全可行,但安全后果是:`push_message` 工具能让 bot 往群里发任意内容,而当前防御仅有 Bearer token(明文 HTTP 传输,且与 `HERMES_API_KEY` 同钥匙)。改之前请配套上反向代理(TLS 终结) + 来源 IP ACL,否则任何能 reach 该端口的进程一旦拿到 token 就可以冒名发送。
|
|
217
218
|
|
|
218
219
|
### 把插件能力告诉 Hermes Agent
|
|
219
220
|
|
|
220
|
-
插件自带一份 `SKILL.md`(reactive 决策契约 +
|
|
221
|
+
插件自带一份 `SKILL.md`(reactive 决策契约 + 反向通道用法)。在 bot 项目目录下任选一种执行(都是把 SKILL.md 装到 `~/.hermes/skills/nonebot-bridge/`):
|
|
221
222
|
|
|
222
223
|
```bash
|
|
223
|
-
|
|
224
|
+
# 用 uv 管理依赖
|
|
225
|
+
uv run hermes-install-skill
|
|
226
|
+
|
|
227
|
+
# 或者 bot 项目用普通 venv
|
|
228
|
+
.venv/bin/hermes-install-skill
|
|
229
|
+
|
|
230
|
+
# 或者已激活虚拟环境
|
|
231
|
+
hermes-install-skill
|
|
232
|
+
|
|
233
|
+
# 备用入口(任何能 import nonebot-plugin-hermes 的环境)
|
|
234
|
+
python -m hermes_install_skill
|
|
224
235
|
```
|
|
225
236
|
|
|
226
237
|
然后在 `~/.hermes/config.yaml` 注册插件 MCP server,把 `<HERMES_API_KEY>` 替换为你前面生成的同一把密钥(用于双向鉴权):
|
|
@@ -232,7 +243,7 @@ mcp_servers:
|
|
|
232
243
|
headers: { Authorization: "Bearer <HERMES_API_KEY>" }
|
|
233
244
|
```
|
|
234
245
|
|
|
235
|
-
后续插件 SKILL.md
|
|
246
|
+
后续插件 SKILL.md 升级时,用上面同样的入口加 `--force` 重装,例如 `uv run hermes-install-skill --force` 或 `.venv/bin/hermes-install-skill --force`。
|
|
236
247
|
|
|
237
248
|
## 命令
|
|
238
249
|
|
|
@@ -241,7 +252,7 @@ mcp_servers:
|
|
|
241
252
|
| `/clear` | 重置对话,开始新会话 |
|
|
242
253
|
| `/ping` | 检查 Hermes Agent 连接状态 |
|
|
243
254
|
| `/help` | 显示帮助信息 |
|
|
244
|
-
| `/hermes-status` | 打印 M1 运行时状态(MCP / 活跃 sessions / buffer / registry
|
|
255
|
+
| `/hermes-status` | 打印 M1 运行时状态(MCP / 活跃 sessions / buffer / registry)。**需在 `HERMES_ADMIN_USERS` 显式授权 `adapter:user_id`**;非管理员调用时静默无响应,且 `/help` 输出里也不出现该命令 |
|
|
245
256
|
|
|
246
257
|
## 配置项
|
|
247
258
|
|
|
@@ -257,10 +268,11 @@ mcp_servers:
|
|
|
257
268
|
| `HERMES_PRIVATE_TRIGGER` | `all` | 私聊触发方式: `all` / `allowlist` |
|
|
258
269
|
| `HERMES_ALLOW_USERS` | `[]` | 允许私聊的用户 ID 列表 (`allowlist` 模式) |
|
|
259
270
|
| `HERMES_ALLOW_GROUPS` | `[]` | 允许响应的群组 ID 列表(空为全部允许) |
|
|
271
|
+
| `HERMES_ADMIN_USERS` | `[]` | 管理员白名单,格式 `["telegram:<user_id>", "onebotv11:<user_id>"]`。**默认空集 = deny by default**;`/hermes-status` 等敏感命令必须命中此列表才执行 |
|
|
260
272
|
| `HERMES_SESSION_SHARE_GROUP` | `false` | 群内是否共享同一个 session |
|
|
261
273
|
| `HERMES_MAX_LENGTH` | `4000` | 单条回复最大长度(超出后截断) |
|
|
262
274
|
| `HERMES_IGNORE_PREFIX` | `["."]` | 以这些字符开头的消息不触发回复 |
|
|
263
|
-
| `HERMES_PERCEPTION_ENABLED` | `false` |
|
|
275
|
+
| `HERMES_PERCEPTION_ENABLED` | `false` | 群聊 + active_session=false 下,是否在 @bot 时给 LLM 注入旁观历史。**`HERMES_ACTIVE_SESSION_ENABLED=true` 时自动隐含为 on,本开关无效**。私聊永远不注入(Hermes session 已覆盖) |
|
|
264
276
|
| `HERMES_PERCEPTION_BUFFER` | `10` | 被动感知缓存的历史消息数量 |
|
|
265
277
|
| `HERMES_PERCEPTION_TEXT_LENGTH` | `200` | 被动感知单条历史消息最大长度 |
|
|
266
278
|
| `HERMES_PERCEPTION_IMAGE_MODE` | `placeholder` | 历史图片模式: `placeholder`(纯文本占位,推荐) / `inline_labeled`(带标签随多模态发送,适合跨图诉求) / `none`(不提) |
|
|
@@ -270,10 +282,9 @@ mcp_servers:
|
|
|
270
282
|
| `HERMES_BUFFER_PER_GROUP_CAP` | `200` | MessageBuffer 每群最近消息上限 |
|
|
271
283
|
| `HERMES_BUFFER_TOTAL_GROUPS_CAP` | `50` | MessageBuffer 跨群总容量(LRU 驱逐) |
|
|
272
284
|
| `HERMES_MCP_ENABLED` | `false` | 启动内嵌 FastMCP server(M1 反向通道) |
|
|
273
|
-
| `HERMES_MCP_HOST` | `127.0.0.1` | MCP server
|
|
285
|
+
| `HERMES_MCP_HOST` | `127.0.0.1` | MCP server 绑定地址。改成公开地址前请阅读上文「群活跃态 + 反向通道」节的安全注意 |
|
|
274
286
|
| `HERMES_MCP_PORT` | `8643` | MCP server 绑定端口 |
|
|
275
287
|
| `HERMES_MCP_RECENT_LIMIT_MAX` | `50` | `get_recent_messages` 工具单次最大返回条数 |
|
|
276
|
-
| `HERMES_STRUCTURED_PATH` | `prompt` | reactive 结构化输出路径: `prompt`(JSON5 解析) / `tools`(OpenAI tool_choice) |
|
|
277
288
|
|
|
278
289
|
## 限制
|
|
279
290
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
所有配置项通过 NoneBot 的 .env 文件读取,前缀为 HERMES_。
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import Set
|
|
8
8
|
|
|
9
9
|
from nonebot import get_plugin_config
|
|
10
10
|
from pydantic import BaseModel, Field
|
|
@@ -37,6 +37,12 @@ class Config(BaseModel):
|
|
|
37
37
|
hermes_allow_groups: Set[str] = set()
|
|
38
38
|
"""允许响应的群组 ID(空 = 全部允许)"""
|
|
39
39
|
|
|
40
|
+
hermes_admin_users: Set[str] = set()
|
|
41
|
+
"""管理员白名单(adapter+user 复合 ID)。用于 /hermes-status 等敏感命令。
|
|
42
|
+
格式:`{adapter}:{user_id}`,adapter 取小写 + 去空格点(同 get_adapter_name)。
|
|
43
|
+
例:["telegram:7055555877", "onebotv11:12345678"]
|
|
44
|
+
空集 = 不允许任何人使用敏感命令(默认 deny)。"""
|
|
45
|
+
|
|
40
46
|
# --- 会话 ---
|
|
41
47
|
hermes_session_share_group: bool = False
|
|
42
48
|
"""群内是否共享同一个 session(False = 每人独立)"""
|
|
@@ -88,7 +94,10 @@ class Config(BaseModel):
|
|
|
88
94
|
"""是否启动内嵌 FastMCP server(False 时 Hermes 反向调用全失败,出向不影响)"""
|
|
89
95
|
|
|
90
96
|
hermes_mcp_host: str = "127.0.0.1"
|
|
91
|
-
"""MCP server bind host
|
|
97
|
+
"""MCP server bind host. 默认 loopback (127.0.0.1)。
|
|
98
|
+
改监听公网/局域网在技术上可行,但安全代价:push_message 能让 bot 往群里发
|
|
99
|
+
任意内容,前端防御只有 Bearer token(明文 HTTP,且与 HERMES_API_KEY 同
|
|
100
|
+
钥匙)。要改请配套反向代理 + TLS + 来源 IP ACL。"""
|
|
92
101
|
|
|
93
102
|
hermes_mcp_port: int = 8643
|
|
94
103
|
"""MCP server bind port"""
|
|
@@ -97,11 +106,5 @@ class Config(BaseModel):
|
|
|
97
106
|
"""get_recent_messages 工具单次返回上限。最小 1——0/负值会让工具静默返空,
|
|
98
107
|
Pydantic 在启动期校验防 misconfig。"""
|
|
99
108
|
|
|
100
|
-
# --- M1: 结构化输出路径(由 P0-spike 决定) ---
|
|
101
|
-
hermes_structured_path: Literal["tools", "prompt"] = "prompt"
|
|
102
|
-
"""tools = 路径 A(tools+tool_choice);prompt = 路径 B(JSON5)。
|
|
103
|
-
Task 3 spike (2026-05-09) 结论:Hermes 不透传 tools/tool_choice 给底层 LLM,
|
|
104
|
-
必须用 prompt 强约束 + JSON5 容错解析。"""
|
|
105
|
-
|
|
106
109
|
|
|
107
110
|
plugin_config = get_plugin_config(Config)
|
|
@@ -7,6 +7,7 @@ M1-mem 路径 B(P0-spike 决策):tools/tool_choice 被 Hermes 吞掉,改用 syst
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import json
|
|
10
11
|
import re
|
|
11
12
|
from dataclasses import dataclass, field
|
|
12
13
|
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
|
@@ -23,6 +24,16 @@ UserContent = Union[str, List[Dict[str, Any]]]
|
|
|
23
24
|
_MD_IMAGE_PATTERN = re.compile(r"!\[([^\]]*)\]\(([^)]+)\)")
|
|
24
25
|
_MEDIA_TAG_PATTERN = re.compile(r"MEDIA:(\S+)")
|
|
25
26
|
|
|
27
|
+
# Hermes apiserver 把 provider 端错误包成 502, body 形如:
|
|
28
|
+
# {"error": {"message": "Error code: 400 - {'error': {'message': '...'}}"}}
|
|
29
|
+
# 把外层 502 当真因显示会误导, 因此抠出内层 status 与 reason。
|
|
30
|
+
_INNER_STATUS_RE = re.compile(r"Error code:\s*(\d+)")
|
|
31
|
+
_VISION_UNSUPPORTED_RE = re.compile(
|
|
32
|
+
r"unknown variant\s+[`'\"]image_url|image_url.*not.*support|does not support.*image|"
|
|
33
|
+
r"input_image.*not.*support|multimodal.*not.*support",
|
|
34
|
+
re.IGNORECASE,
|
|
35
|
+
)
|
|
36
|
+
|
|
26
37
|
# 提取首个 {...} 块。
|
|
27
38
|
# 当前正则只支持嵌套一层(`\{[^{}]*\}` 出现在外层 `\{...\}` 中)。
|
|
28
39
|
# M1 submit_decision schema 全平,够用。如未来 schema 加 nested object,
|
|
@@ -36,10 +47,101 @@ _DECISION_HINT = (
|
|
|
36
47
|
" reply_text (string, optional, required when should_reply=true)\n"
|
|
37
48
|
" topic_hint (string, optional)\n"
|
|
38
49
|
" should_exit_active (boolean, optional)\n"
|
|
39
|
-
"Output ONLY the JSON object, no preamble, no postscript, no markdown fences
|
|
50
|
+
"Output ONLY the JSON object, no preamble, no postscript, no markdown fences.\n"
|
|
51
|
+
"All string values MUST be single-line; escape line breaks inside strings as \\n (no raw newlines)."
|
|
40
52
|
)
|
|
41
53
|
|
|
42
54
|
|
|
55
|
+
def _escape_raw_newlines_in_strings(s: str) -> str:
|
|
56
|
+
"""把 JSON 字符串字面量内部的裸 \\n/\\r/\\t 转义掉,让 json5 能解析。
|
|
57
|
+
|
|
58
|
+
LLM 经常在 reply_text 里嵌真换行(段落分隔),JSON5 字符串不允许;首发 json5
|
|
59
|
+
抛 `Unexpected "\\n"` 后我们走这一遍状态机重试一次。状态:跟踪 " / ' 进出
|
|
60
|
+
string、`\\X` 整对透传(不参与 quote 计数),quoted 区里把裸控制字符替换。
|
|
61
|
+
"""
|
|
62
|
+
out: List[str] = []
|
|
63
|
+
in_string = False
|
|
64
|
+
quote = ""
|
|
65
|
+
i = 0
|
|
66
|
+
n = len(s)
|
|
67
|
+
while i < n:
|
|
68
|
+
c = s[i]
|
|
69
|
+
if not in_string:
|
|
70
|
+
if c == '"' or c == "'":
|
|
71
|
+
in_string = True
|
|
72
|
+
quote = c
|
|
73
|
+
out.append(c)
|
|
74
|
+
i += 1
|
|
75
|
+
continue
|
|
76
|
+
# 在 string 内
|
|
77
|
+
if c == "\\" and i + 1 < n:
|
|
78
|
+
# 转义序列整对透传(含 \" / \' / \\ / \n 等),不参与 quote 计数
|
|
79
|
+
out.append(s[i : i + 2])
|
|
80
|
+
i += 2
|
|
81
|
+
continue
|
|
82
|
+
if c == quote:
|
|
83
|
+
in_string = False
|
|
84
|
+
quote = ""
|
|
85
|
+
out.append(c)
|
|
86
|
+
i += 1
|
|
87
|
+
continue
|
|
88
|
+
if c == "\n":
|
|
89
|
+
out.append("\\n")
|
|
90
|
+
elif c == "\r":
|
|
91
|
+
out.append("\\r")
|
|
92
|
+
elif c == "\t":
|
|
93
|
+
out.append("\\t")
|
|
94
|
+
else:
|
|
95
|
+
out.append(c)
|
|
96
|
+
i += 1
|
|
97
|
+
return "".join(out)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _summarize_error_body(body: str) -> Tuple[str, Optional[int]]:
|
|
101
|
+
"""从 Hermes 错误响应体里抠出可读 reason 与内层 status。
|
|
102
|
+
|
|
103
|
+
Hermes apiserver 习惯把 provider 端错误外包成 502, body 是 JSON, `error.message`
|
|
104
|
+
形如 `"Error code: 400 - {...}"`。直接把 200 字符 body 倒进日志噪音大、
|
|
105
|
+
用户看到只剩外层 502 完全不知道发生了什么, 所以这里先剥一层。
|
|
106
|
+
|
|
107
|
+
返回 (reason_snippet, inner_status_or_None);body 不可解析时退化到 body 截断。
|
|
108
|
+
"""
|
|
109
|
+
if not body:
|
|
110
|
+
return "(empty body)", None
|
|
111
|
+
raw = body.strip()
|
|
112
|
+
try:
|
|
113
|
+
parsed = json.loads(raw)
|
|
114
|
+
except (json.JSONDecodeError, ValueError):
|
|
115
|
+
return raw[:200], None
|
|
116
|
+
err = parsed.get("error") if isinstance(parsed, dict) else None
|
|
117
|
+
if isinstance(err, dict):
|
|
118
|
+
msg = str(err.get("message") or err).strip()
|
|
119
|
+
elif isinstance(err, str):
|
|
120
|
+
msg = err.strip()
|
|
121
|
+
else:
|
|
122
|
+
msg = raw[:200]
|
|
123
|
+
inner_status: Optional[int] = None
|
|
124
|
+
m = _INNER_STATUS_RE.search(msg)
|
|
125
|
+
if m:
|
|
126
|
+
try:
|
|
127
|
+
inner_status = int(m.group(1))
|
|
128
|
+
except ValueError:
|
|
129
|
+
inner_status = None
|
|
130
|
+
return msg[:300], inner_status
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _user_facing_error(reason: str) -> str:
|
|
134
|
+
"""把 _summarize_error_body 的 reason 翻译成给群里发的简短提示。
|
|
135
|
+
|
|
136
|
+
命中已知模式(目前: 图片输入被非 vision 模型拒)就给精准提示;否则带 reason 片段
|
|
137
|
+
让用户能直接看到真因, 不再只露一个误导性的 502。
|
|
138
|
+
"""
|
|
139
|
+
if _VISION_UNSUPPORTED_RE.search(reason):
|
|
140
|
+
return "⚠️ 当前主模型不支持图片识别,请改用文字提问或换用 vision 模型"
|
|
141
|
+
snippet = reason.strip().splitlines()[0][:140] if reason.strip() else "未知错误"
|
|
142
|
+
return f"⚠️ AI 服务异常: {snippet}"
|
|
143
|
+
|
|
144
|
+
|
|
43
145
|
def extract_response_media(text: str) -> Tuple[str, List[str]]:
|
|
44
146
|
"""从 Hermes 回复中提取 markdown 图片 / MEDIA: 标签 URL,返回 (清洗后文本, URL 列表)。"""
|
|
45
147
|
media_urls: List[str] = []
|
|
@@ -55,16 +157,24 @@ def extract_response_media(text: str) -> Tuple[str, List[str]]:
|
|
|
55
157
|
|
|
56
158
|
|
|
57
159
|
def _try_parse_first_json_block(text: str) -> Optional[Dict[str, Any]]:
|
|
58
|
-
"""从模型回复中提取首个 {...} 块并 JSON5 解析。失败返回 None,调用方记 parse_failed。
|
|
160
|
+
"""从模型回复中提取首个 {...} 块并 JSON5 解析。失败返回 None,调用方记 parse_failed。
|
|
161
|
+
|
|
162
|
+
两段式回退:json5 首发失败 → 走 _escape_raw_newlines_in_strings 把字符串内
|
|
163
|
+
的裸控制字符转义后重试。两次都失败才返回 None。
|
|
164
|
+
"""
|
|
59
165
|
if not text:
|
|
60
166
|
return None
|
|
61
167
|
m = _FIRST_JSON_BLOCK.search(text)
|
|
62
168
|
if not m:
|
|
63
169
|
return None
|
|
170
|
+
candidate = m.group(0)
|
|
64
171
|
try:
|
|
65
|
-
parsed = json5.loads(
|
|
172
|
+
parsed = json5.loads(candidate)
|
|
66
173
|
except Exception:
|
|
67
|
-
|
|
174
|
+
try:
|
|
175
|
+
parsed = json5.loads(_escape_raw_newlines_in_strings(candidate))
|
|
176
|
+
except Exception:
|
|
177
|
+
return None
|
|
68
178
|
if not isinstance(parsed, dict):
|
|
69
179
|
return None
|
|
70
180
|
return parsed
|
|
@@ -212,10 +322,11 @@ class HermesClient:
|
|
|
212
322
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
213
323
|
resp = await client.post(url, json=payload, headers=headers)
|
|
214
324
|
if resp.status_code != 200:
|
|
215
|
-
|
|
216
|
-
|
|
325
|
+
reason, inner_status = _summarize_error_body(resp.text)
|
|
326
|
+
inner_tag = f" inner={inner_status}" if inner_status else ""
|
|
327
|
+
logger.error(f"[HERMES] upstream HTTP {resp.status_code}{inner_tag}: {reason}")
|
|
217
328
|
return ChatResult(
|
|
218
|
-
raw_text=
|
|
329
|
+
raw_text=_user_facing_error(reason),
|
|
219
330
|
parse_failed=True,
|
|
220
331
|
is_transport_error=True,
|
|
221
332
|
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""In-flight 调用追踪 + coalesce 重燃支持。
|
|
2
|
+
|
|
3
|
+
修同一 (adapter, group_id|user_id) 上事件 task 并发调 chat() 的 bug:
|
|
4
|
+
in-flight 时新消息只更新 pending 单元,等当前一发完成后再合并跑一次。
|
|
5
|
+
|
|
6
|
+
线程安全:**否**。预设单线程 asyncio 事件循环,与 ActiveSessionManager 一致。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Dict, Literal, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
from .message_buffer import BufferedMessage
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Refire 链最大深度。超过则丢 pending、warn,等下一个新触发。
|
|
18
|
+
# 一次 burst 最多产出 1(主回) + MAX_REFIRE_DEPTH(链尾)= 4 发回复。
|
|
19
|
+
MAX_REFIRE_DEPTH = 3
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class InflightSlot:
|
|
24
|
+
started_at: int
|
|
25
|
+
pending: Optional[BufferedMessage] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InflightRegistry:
|
|
29
|
+
"""per-target 非阻塞 busy 标记 + pending 单元。
|
|
30
|
+
|
|
31
|
+
Key 约定:
|
|
32
|
+
- 群: ("adapter", "group:" + group_id)
|
|
33
|
+
- 私聊: ("adapter", "private:" + user_id)
|
|
34
|
+
|
|
35
|
+
不持有任何 asyncio.Task 引用 —— 重燃由 caller 用 create_task 自己接手,
|
|
36
|
+
registry 只负责「现在有没有人在跑」+「跑完后是否要再跑一次」两个状态。
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
self._slots: Dict[Tuple[str, str], InflightSlot] = {}
|
|
41
|
+
|
|
42
|
+
def try_enter(
|
|
43
|
+
self,
|
|
44
|
+
key: Tuple[str, str],
|
|
45
|
+
current_msg: BufferedMessage,
|
|
46
|
+
now_ms: int,
|
|
47
|
+
) -> Literal["entered", "pending_set"]:
|
|
48
|
+
"""无 slot → 占位 started_at=now_ms,返回 'entered'。
|
|
49
|
+
有 slot → 把 current_msg 写进 pending(覆盖旧 pending),返回 'pending_set'。
|
|
50
|
+
"""
|
|
51
|
+
slot = self._slots.get(key)
|
|
52
|
+
if slot is None:
|
|
53
|
+
self._slots[key] = InflightSlot(started_at=now_ms)
|
|
54
|
+
return "entered"
|
|
55
|
+
slot.pending = current_msg
|
|
56
|
+
return "pending_set"
|
|
57
|
+
|
|
58
|
+
def take_pending(self, key: Tuple[str, str]) -> Optional[BufferedMessage]:
|
|
59
|
+
"""Destructive read。无 slot 或 pending 为 None 都返回 None。"""
|
|
60
|
+
slot = self._slots.get(key)
|
|
61
|
+
if slot is None:
|
|
62
|
+
return None
|
|
63
|
+
msg = slot.pending
|
|
64
|
+
slot.pending = None
|
|
65
|
+
return msg
|
|
66
|
+
|
|
67
|
+
def exit(self, key: Tuple[str, str]) -> None:
|
|
68
|
+
"""释放 slot。pending 仍在的话由调用方自行先 take_pending。
|
|
69
|
+
slot 不存在则 no-op。
|
|
70
|
+
"""
|
|
71
|
+
self._slots.pop(key, None)
|
|
@@ -75,6 +75,46 @@ def build_reactive_system_prompt(
|
|
|
75
75
|
return "\n".join(runtime_lines) + "\n\n" + decision_block
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def build_passive_system_prompt(
|
|
79
|
+
*,
|
|
80
|
+
adapter: str,
|
|
81
|
+
is_private: bool,
|
|
82
|
+
user_id: str,
|
|
83
|
+
group_id: Optional[str],
|
|
84
|
+
recent_messages: Sequence[BufferedMessage],
|
|
85
|
+
) -> str:
|
|
86
|
+
"""passive 路径的 system prompt:Message Context 头(对齐 hermes_client.chat
|
|
87
|
+
默认拼装)+ 可选 <recent_messages> 块。
|
|
88
|
+
|
|
89
|
+
用于补回 0.1.6 在群聊 + active_session=false 默认配置下的「旁观历史注入」:
|
|
90
|
+
@bot 那一刻让 LLM 看到群里其他人之前在聊什么(Hermes 自身只记得 user↔bot
|
|
91
|
+
来回,看不到群里旁观对话)。
|
|
92
|
+
|
|
93
|
+
私聊调用方应传空 recent(0.1.6 起私聊就不跑 perception——1:1 没有旁观第三方)。
|
|
94
|
+
|
|
95
|
+
注:Message Context 5 行必须与 hermes_client.chat 的默认拼装保持一致,
|
|
96
|
+
任一处改格式记得同步另一处(测试 test_passive_prompt_* 是约束)。
|
|
97
|
+
"""
|
|
98
|
+
ctx_lines = [f"Platform: {adapter or 'unknown'}"]
|
|
99
|
+
ctx_lines.append("Chat Type: " + ("Private" if is_private else "Group"))
|
|
100
|
+
if user_id:
|
|
101
|
+
ctx_lines.append(f"User ID: {user_id}")
|
|
102
|
+
if not is_private and group_id:
|
|
103
|
+
ctx_lines.append(f"Group ID: {group_id}")
|
|
104
|
+
sp = "Message Context:\n" + "\n".join(ctx_lines)
|
|
105
|
+
|
|
106
|
+
if not recent_messages:
|
|
107
|
+
return sp
|
|
108
|
+
|
|
109
|
+
history_lines = ["<recent_messages>"]
|
|
110
|
+
for m in reversed(list(recent_messages)):
|
|
111
|
+
prefix = "[bot] " if m.is_bot else ""
|
|
112
|
+
speaker = m.nickname or m.user_id
|
|
113
|
+
history_lines.append(f"{prefix}{speaker}: {m.content}")
|
|
114
|
+
history_lines.append("</recent_messages>")
|
|
115
|
+
return sp + "\n\n" + "\n".join(history_lines)
|
|
116
|
+
|
|
117
|
+
|
|
78
118
|
def build_reactive_user_content(
|
|
79
119
|
*,
|
|
80
120
|
recent_messages: Sequence[BufferedMessage],
|
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import time
|
|
10
10
|
|
|
11
11
|
import nonebot_plugin_alconna as alconna
|
|
12
|
-
from nonebot import on_command
|
|
12
|
+
from nonebot import logger, on_command
|
|
13
13
|
from nonebot.adapters import Bot, Event
|
|
14
14
|
from nonebot.matcher import Matcher
|
|
15
15
|
|
|
@@ -80,27 +80,26 @@ async def handle_help(bot: Bot, event: Event, matcher: Matcher):
|
|
|
80
80
|
if not check_isolation(event, target):
|
|
81
81
|
matcher.skip()
|
|
82
82
|
|
|
83
|
+
# 是否管理员决定要不要把 /hermes-status 暴露给当前用户
|
|
84
|
+
adapter_name = get_adapter_name(target)
|
|
85
|
+
user_id = event.get_user_id() or ""
|
|
86
|
+
is_admin = f"{adapter_name}:{user_id}" in plugin_config.hermes_admin_users
|
|
87
|
+
|
|
83
88
|
if target.private:
|
|
84
|
-
|
|
85
|
-
"🤖 Hermes Agent 帮助\n\n"
|
|
86
|
-
"直接发送消息即可与 AI 对话。\n\n"
|
|
87
|
-
"命令:\n"
|
|
88
|
-
"/clear - 重置对话\n"
|
|
89
|
-
"/ping - 检查连接状态\n"
|
|
90
|
-
"/hermes-status - 查看插件运行时状态\n"
|
|
91
|
-
"/help - 显示本帮助"
|
|
92
|
-
)
|
|
89
|
+
intro = "🤖 Hermes Agent 帮助\n\n直接发送消息即可与 AI 对话。\n\n命令:\n"
|
|
93
90
|
else:
|
|
94
|
-
|
|
95
|
-
"🤖 Hermes Agent 帮助\n\n"
|
|
96
|
-
"@我 发送消息即可与 AI 对话。\n\n"
|
|
97
|
-
"命令:\n"
|
|
98
|
-
"/clear - 重置对话\n"
|
|
99
|
-
"/ping - 检查连接状态\n"
|
|
100
|
-
"/hermes-status - 查看插件运行时状态\n"
|
|
101
|
-
"/help - 显示本帮助"
|
|
102
|
-
)
|
|
91
|
+
intro = "🤖 Hermes Agent 帮助\n\n@我 发送消息即可与 AI 对话。\n\n命令:\n"
|
|
103
92
|
|
|
93
|
+
lines = [
|
|
94
|
+
"/clear - 重置对话",
|
|
95
|
+
"/ping - 检查连接状态",
|
|
96
|
+
"/help - 显示本帮助",
|
|
97
|
+
]
|
|
98
|
+
if is_admin:
|
|
99
|
+
# 管理员才看见运行时状态命令,普通用户视角下此命令"不存在"
|
|
100
|
+
lines.append("/hermes-status - 查看插件运行时状态(管理员)")
|
|
101
|
+
|
|
102
|
+
help_text = intro + "\n".join(lines)
|
|
104
103
|
await alconna.UniMessage(help_text).send(target=target, bot=bot)
|
|
105
104
|
|
|
106
105
|
|
|
@@ -115,6 +114,16 @@ async def handle_status(bot: Bot, event: Event, matcher: Matcher):
|
|
|
115
114
|
if not check_isolation(event, target):
|
|
116
115
|
matcher.skip()
|
|
117
116
|
|
|
117
|
+
# /hermes-status 暴露内部运行时(活跃群、buffer 内容、bot 路由),
|
|
118
|
+
# 应限制在管理员白名单内。**隐身策略**:对非管理员完全静默,不发"未授权"
|
|
119
|
+
# 提示——避免把命令存在性暴露给一般用户。空集 = deny by default。
|
|
120
|
+
adapter_name = get_adapter_name(target)
|
|
121
|
+
user_id = event.get_user_id() or ""
|
|
122
|
+
admin_key = f"{adapter_name}:{user_id}"
|
|
123
|
+
if admin_key not in plugin_config.hermes_admin_users:
|
|
124
|
+
logger.debug(f"[HERMES] /hermes-status silent skip for {admin_key}")
|
|
125
|
+
return # block=True 已阻断后续 matcher,直接 return 即静默
|
|
126
|
+
|
|
118
127
|
now_ms = int(time.time() * 1000)
|
|
119
128
|
|
|
120
129
|
# MCP 状态
|
|
@@ -159,7 +168,6 @@ async def handle_status(bot: Bot, event: Event, matcher: Matcher):
|
|
|
159
168
|
"🔍 Hermes Plugin M1-mem 状态",
|
|
160
169
|
f"MCP: {mcp_line}",
|
|
161
170
|
f"active_session: {active_line}",
|
|
162
|
-
f"structured_path: {plugin_config.hermes_structured_path}",
|
|
163
171
|
f"hermes_api: {plugin_config.hermes_api_url}",
|
|
164
172
|
"",
|
|
165
173
|
f"📊 ActiveSessions: {active_count} 个活跃",
|