nonebot-plugin-aigf-master 2.0.0b1__tar.gz → 2.0.0b2__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.
Files changed (36) hide show
  1. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/PKG-INFO +37 -43
  2. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/README.md +36 -42
  3. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/__init__.py +11 -66
  4. nonebot_plugin_aigf_master-2.0.0b2/nonebot_plugin_aigf_master/notice.py +233 -0
  5. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/session.py +10 -2
  6. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master.egg-info/PKG-INFO +37 -43
  7. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master.egg-info/SOURCES.txt +1 -0
  8. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/pyproject.toml +1 -1
  9. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/LICENSE +0 -0
  10. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/anime_recognizer.py +0 -0
  11. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/animetrace_client.py +0 -0
  12. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/api_hooks.py +0 -0
  13. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/cache_cleanup.py +0 -0
  14. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/command_learner.py +0 -0
  15. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/config.py +0 -0
  16. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/context_bus.py +0 -0
  17. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/image_gen_client.py +0 -0
  18. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/image_handler.py +0 -0
  19. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/llm_client.py +0 -0
  20. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/meme_store.py +0 -0
  21. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/memory_store.py +0 -0
  22. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/models.py +0 -0
  23. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/peer_client.py +0 -0
  24. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/plugin_discovery.py +0 -0
  25. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/plugin_invoker.py +0 -0
  26. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/preset_store.py +0 -0
  27. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/processor.py +0 -0
  28. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/prompt_builder.py +0 -0
  29. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/response_parser.py +0 -0
  30. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/search_client.py +0 -0
  31. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/unimsg.py +0 -0
  32. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/vlm_client.py +0 -0
  33. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master.egg-info/dependency_links.txt +0 -0
  34. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master.egg-info/requires.txt +0 -0
  35. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master.egg-info/top_level.txt +0 -0
  36. {nonebot_plugin_aigf_master-2.0.0b1 → nonebot_plugin_aigf_master-2.0.0b2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nonebot-plugin-aigf-master
3
- Version: 2.0.0b1
3
+ Version: 2.0.0b2
4
4
  Summary: 群聊特化LLM聊天机器人(增强版),具有记忆、表情包、跨插件调用、跨bot通信与跨适配器支持
5
5
  License-Expression: MIT
6
6
  Requires-Python: <4.0,>=3.10
@@ -67,7 +67,7 @@ Dynamic: license-file
67
67
  - ⏱️ **不完整消息检测**:纯 @ 消息或连续发送时自动延长等待时间
68
68
  - 🕐 **消息时间标注**:可选为 LLM 看到的消息加上 `[MM-DD HH:MM]` 时间前缀(`AIGFM_SHOW_MESSAGE_TIME`)
69
69
  - 📄 **完整消息类型**:合并聊天记录、小程序/卡片、XML,以及 QQ 表情/语音/视频/群文件/链接分享/推荐名片/位置/音乐/骰子等共 18 种消息段均可解析(纯此类消息不再被丢弃)
70
- - 🔔 **群系统通知**:戳一戳、禁言/解禁、新人进群、退群被踢、消息撤回会以 `[系统]` 消息进入 LLM 上下文
70
+ - 🔔 **群系统通知**:戳一戳、禁言/解禁、新人进群、退群被踢、消息撤回等会以 `[系统]` 消息进入 LLM 上下文(OneBot V11 文案最细,其它适配器按通知类别映射成中文,见「多适配器支持」)
71
71
  - 🔒 **强制 JSON 输出**:确保 LLM 始终返回正确格式
72
72
  - ⚡ **轻量高效**:单次 LLM 调用完成对话 + 记忆管理 + 命令学习
73
73
 
@@ -80,9 +80,9 @@ Dynamic: license-file
80
80
  >
81
81
  > 从 2.0 起插件还依赖 `nonebot-plugin-alconna` 与 `nonebot-plugin-uninfo`(跨适配器收发与会话/成员信息),用下面的方式安装时会自动装上。
82
82
 
83
- ### 安装 beta 版本(2.0.0b1)
83
+ ### 安装 beta 版本(2.0.0b2)
84
84
 
85
- `2.0.0b1` 是**预发布(pre-release)版本**,因此:
85
+ `2.0.0b2` 是**预发布(pre-release)版本**,因此:
86
86
 
87
87
  - `pip install nonebot-plugin-aigf-master` 装的仍是稳定版 **1.6.x**
88
88
  - `nb plugin install nonebot-plugin-aigf-master` 同样只装稳定版,而且 nb-cli **不支持指定版本**,所以 beta 只能用 pip 装
@@ -90,7 +90,7 @@ Dynamic: license-file
90
90
  要装 beta,**必须显式写出版本号**:
91
91
 
92
92
  ```bash
93
- pip install nonebot-plugin-aigf-master==2.0.0b1
93
+ pip install nonebot-plugin-aigf-master==2.0.0b2
94
94
  ```
95
95
 
96
96
  在 `pyproject.toml` 中添加:
@@ -250,42 +250,41 @@ AIGFM_PEER_CAPTURE_PLUGINS=[] # 要捕获输出的插件名列表,为
250
250
 
251
251
  ### 功能 × 适配器支持表(2026-09-25 实测)
252
252
 
253
- 测试方式:本机用 `nb`/pip 装上各适配器包后,为每个适配器**构造真实事件**(各适配器的消息/事件模型),跑本插件同一套功能路径——会话解析(uninfo)→ 消息渲染(uniseg)→ 回复发送(alconna exporter)→ 跨插件捕获(`on_calling_api`)→ 插件调用(复制真实事件 + 目标响应器实际收到命令)。bot 为记录型 mock(拦下所有 API 调用并断言调用的接口名),**未连真实平台**,因此"需要真实 API 才有数据"的项目另标 ⚠。
253
+ 为每个适配器**构造真实事件**跑同一套功能路径(会话解析 → 渲染 → 回复发送 → 跨插件捕获 → 插件调用),bot 是记录型 mock,**未连真实平台**。
254
254
 
255
- | 适配器 | 会话解析 | 收发/渲染 | 回复发送 | 私聊 | @ 按昵称 | 跨插件捕获 | 插件调用 | 群通知¹ | 跨 bot² |
255
+ | 适配器 | 会话解析 | 收发/渲染 | 回复发送 | 私聊 | @ 按昵称 | 跨插件捕获 | 插件调用 | 群通知 | 跨 bot |
256
256
  |---|---|---|---|---|---|---|---|---|---|
257
257
  | OneBot V11 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
258
- | OneBot V12 | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
259
- | Console | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
260
- | Satori | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ⚠ | ❌ | ✅ |
261
- | Telegram | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
262
- | Discord | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
263
- | QQ(频道/C2C/群) | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
264
- | Feishu | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
265
- | Milky | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
266
- | Mirai | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
267
- | Kaiheila(Kook) | ✅ | ✅ | ✅ | 未测³ | ⚠ | ✅ | ⚠ | ❌ | ✅ |
268
- | DoDo | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
269
- | Kritor | ✅ | ✅ | ✅ | 未测³ | ⚠ | ✅ | ✅ | ❌ | ✅ |
270
- | Mail | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
271
- | Minecraft | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
272
- | WXMP | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
273
- | EFChat | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
274
- | YunHu⁴ | ✅ | ✅ | ✅ | 未测³ | ⚠ | ✅ | ✅ | ❌ | ✅ |
275
- | bilibili Live⁴ | ✅ | ✅ | ✅ | ❌ 无好友私聊类型 | ⚠ | ✅ | ✅ | ❌ | ✅ |
276
-
277
- **读表说明**
278
-
279
- - **✅ 已实测通过**:会话键、渲染文本、发送接口、捕获入缓冲、目标插件确实收到了命令,逐项断言过。**本表 17 行在 Python 3.10 实测(167 项检查)**;**YunHu / bilibili Live 两行在 Python 3.14 实测**(3.10 装不上,见下),19 个适配器合计 **187 项检查、只有 2 项未通过**(Satori / Kaiheila 的插件调用)
280
- - **整套依赖栈在 Python 3.14 上跑通**:`nonebot2 2.5.0` + `nonebot-plugin-alconna 0.62.1` + `nonebot-plugin-uninfo 0.11.2` 在 **Python 3.14.3** 上通过了全部回归——主插件 150 项、peer 子插件 67 项、适配器矩阵 187 项(仅上述 2 项未通过)
281
- - **⚠ 需真机/特例**:
282
- - **@ 按昵称**:只有 OneBot V11 实测能按群昵称反查出用户 id(走 `get_group_member_list`);其它适配器需要 uninfo 的成员列表查询实现(多数适配器没有),因此**让 LLM 直接用用户 id 更稳**;渲染 @ 时昵称拿不到会回落成 id
283
- - **插件调用(Satori / Kaiheila)**:这两类适配器的事件结构特殊(Satori 的 `message` 是 `{id, content}` 结构体、Kaiheila 的消息在嵌套的 `event` 里),离线构造"uninfo 与 alconna 都认可"的事件未能复现,因此**未验证通过**;其余 14 个适配器已实测目标响应器收到命令。真机使用请以实际表现为准
284
- - **❌ 不支持**:群系统通知¹(戳一戳/禁言/进出群/撤回是 OneBot 专属事件类型)
285
- - **⚠ Python 版本要求**(上表 YunHu / bilibili Live):这两个适配器包**需要 Python ≥ 3.12**——`bilibili Live` 用了 `typing.TypedDict`(pydantic 在 Python < 3.12 直接拒绝),`YunHu` 还额外用到 `typing.NotRequired`(Python 3.11+),所以在 Python 3.10 的 bot 里**导入即失败**。上表这两行的 ✅ 是**在本机 Python 3.14.3 上实测的结果**:两个包都能导入,`alconna` 的 builder/exporter 与 `uninfo` 的 fetcher 三者齐备,会话解析/渲染/发送/捕获/插件调用全部通过(`bilibili Live` 的场景路径=房间号;`YunHu` 的群资料需要真实 API 才有昵称)。**要用这两个平台就把 bot 跑在 Python 3.12+/3.14 上**(本工作目录另建了参考环境 `.venv314`:Python 3.14.3 + `nonebot2 2.5.0` + `alconna 0.62.1` + `uninfo 0.11.2` + 全部 19 个适配器包,回归与矩阵都在里面跑过)
286
- - **Kook 注意**:社区包 `nonebot-adapter-kook` 的模块名是 `nonebot.adapters.kook`、`get_name()` 报 `Kook`,而 alconna/uninfo 期望的是 `nonebot.adapters.kaiheila`(`Kaiheila`)——**请安装 `nonebot-adapter-kaiheila`**,否则该平台等同于"不被支持"
287
- - **昵称/群名片**:取决于适配器的 uninfo fetcher 能取到什么(很多要真实 API 调用,离线 mock 拿不到),拿不到时回落成用户 id,功能不受影响
288
- - 适配器完全不被 alconna/uninfo 支持时,该会话的消息会被静默跳过(只留 debug 日志),不会报错、也不会影响其它适配器
258
+ | OneBot V12 | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
259
+ | Console | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
260
+ | Satori | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ⚠️ | ✅ | ✅ |
261
+ | Telegram | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
262
+ | Discord | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ⚠️ | ✅ |
263
+ | QQ(频道/C2C/群) | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
264
+ | Feishu | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
265
+ | Milky | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
266
+ | Mirai | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
267
+ | Kaiheila(Kook) | ✅ | ✅ | ✅ | 未测 | ⚠️ | ✅ | ⚠️ | ✅ | ✅ |
268
+ | DoDo | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
269
+ | Kritor | ✅ | ✅ | ✅ | 未测 | ⚠️ | ✅ | ✅ | ❌ | ✅ |
270
+ | Mail | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
271
+ | Minecraft | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
272
+ | WXMP | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
273
+ | EFChat | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
274
+ | YunHu | ✅ | ✅ | ✅ | 未测 | ⚠️ | ✅ | ✅ | ✅ | ✅ |
275
+ | bilibili Live | ✅ | ✅ | ✅ | ❌ 无好友私聊类型 | ⚠️ | ✅ | ✅ | ⚠️ | ✅ |
276
+
277
+ 图例:**✅ 已实测通过** | **⚠️ 已实现但未验证通过**(离线构造不出该适配器的事件,或需要真实 API / 真机)| **❌ 该适配器没有这类事件,或库层无法把该通知定位到群/频道会话(通知会被跳过)** | **未测** 本次未构造该场景。
278
+
279
+ **已知特例**
280
+
281
+ - **@ 按昵称**:只有 OneBot V11 能按群昵称反查用户 id,其余适配器请让 LLM 直接用用户 id
282
+ - **插件调用**:Satori / Kaiheila 的事件结构特殊,离线未验证通过;其余 14 个已实测目标响应器收到命令
283
+ - **群通知**:为每个适配器构造真实通知事件实测,**10 个通过**(OneBot V11/V12、Satori、Telegram、QQ、Milky、Mirai、Kaiheila、DoDo、YunHu);Discord / bilibili 未验证(离线 mock 不够 / 只有部分通知类在库的覆盖范围内);**Feishu、Kritor、EFChat** 的通知 uninfo 与 alconna 都无法定位到群/频道会话,**Minecraft、WXMP** 的通知事件没有群/频道字段(会话按私聊处理)——这几种会被跳过(库层限制,非本插件可修)
284
+ - **YunHu / bilibili Live**:需要把 bot 跑在 **Python ≥ 3.12**(3.10 里导入即失败;这两行的 ✅ 是在 3.14 实测的)
285
+ - **Kaiheila**:请装 `nonebot-adapter-kaiheila`(社区包 `nonebot-adapter-kook` 的模块名与 alconna/uninfo 期望的不一致)
286
+ - **跨 bot**:按 peer 0.4.0+ 的 `session` 协议可用;旧 peer(0.3.x)只有 OneBot V11 的会话能对上
287
+ - 适配器不被 alconna/uninfo 支持时,该会话的消息静默跳过(只留 debug 日志),不影响其它适配器
289
288
 
290
289
  <details>
291
290
  <summary>实测到的各适配器发送接口(供排查用)</summary>
@@ -311,11 +310,6 @@ AIGFM_PEER_CAPTURE_PLUGINS=[] # 要捕获输出的插件名列表,为
311
310
 
312
311
  </details>
313
312
 
314
- > ¹ 群系统通知 = 戳一戳/禁言/进出群/消息撤回。
315
- > ² 跨 bot = 子插件 [nonebot-plugin-aigfm-peer](https://github.com/Funny1Potato/nonebot-plugin-aigfm-peer) 的推送与远程调用。上表按 **peer 0.4.0+** 的协议标 ✅:推送与调用都会带 `session`(`适配器:会话id`)、主插件按会话键落位,因此各适配器都可用。**用旧 peer(0.3.x)时只有 OneBot V11 的会话能对上**(旧协议只带 `group_id`,按 `onebot11:{group_id}` 映射)。peer 侧捕获/推送与主插件侧落位各自实测过,端到端的「复制事件执行命令」在 OneBot V11 与 Console 上跑通。
316
- > ³ Kritor / Kaiheila 的私聊事件本次未构造,故未测;它们的私聊类型在 alconna/uninfo 里是有实现的。
317
- > ⁴ 这两个适配器**需要 Python ≥ 3.12**(3.10 环境里导入即失败),表里这两行是在 **Python 3.14.3** 下实测的结果,见下方说明。
318
-
319
313
  ## 🔌 跨插件感知
320
314
 
321
315
  ### 工作原理
@@ -45,7 +45,7 @@
45
45
  - ⏱️ **不完整消息检测**:纯 @ 消息或连续发送时自动延长等待时间
46
46
  - 🕐 **消息时间标注**:可选为 LLM 看到的消息加上 `[MM-DD HH:MM]` 时间前缀(`AIGFM_SHOW_MESSAGE_TIME`)
47
47
  - 📄 **完整消息类型**:合并聊天记录、小程序/卡片、XML,以及 QQ 表情/语音/视频/群文件/链接分享/推荐名片/位置/音乐/骰子等共 18 种消息段均可解析(纯此类消息不再被丢弃)
48
- - 🔔 **群系统通知**:戳一戳、禁言/解禁、新人进群、退群被踢、消息撤回会以 `[系统]` 消息进入 LLM 上下文
48
+ - 🔔 **群系统通知**:戳一戳、禁言/解禁、新人进群、退群被踢、消息撤回等会以 `[系统]` 消息进入 LLM 上下文(OneBot V11 文案最细,其它适配器按通知类别映射成中文,见「多适配器支持」)
49
49
  - 🔒 **强制 JSON 输出**:确保 LLM 始终返回正确格式
50
50
  - ⚡ **轻量高效**:单次 LLM 调用完成对话 + 记忆管理 + 命令学习
51
51
 
@@ -58,9 +58,9 @@
58
58
  >
59
59
  > 从 2.0 起插件还依赖 `nonebot-plugin-alconna` 与 `nonebot-plugin-uninfo`(跨适配器收发与会话/成员信息),用下面的方式安装时会自动装上。
60
60
 
61
- ### 安装 beta 版本(2.0.0b1)
61
+ ### 安装 beta 版本(2.0.0b2)
62
62
 
63
- `2.0.0b1` 是**预发布(pre-release)版本**,因此:
63
+ `2.0.0b2` 是**预发布(pre-release)版本**,因此:
64
64
 
65
65
  - `pip install nonebot-plugin-aigf-master` 装的仍是稳定版 **1.6.x**
66
66
  - `nb plugin install nonebot-plugin-aigf-master` 同样只装稳定版,而且 nb-cli **不支持指定版本**,所以 beta 只能用 pip 装
@@ -68,7 +68,7 @@
68
68
  要装 beta,**必须显式写出版本号**:
69
69
 
70
70
  ```bash
71
- pip install nonebot-plugin-aigf-master==2.0.0b1
71
+ pip install nonebot-plugin-aigf-master==2.0.0b2
72
72
  ```
73
73
 
74
74
  在 `pyproject.toml` 中添加:
@@ -228,42 +228,41 @@ AIGFM_PEER_CAPTURE_PLUGINS=[] # 要捕获输出的插件名列表,为
228
228
 
229
229
  ### 功能 × 适配器支持表(2026-09-25 实测)
230
230
 
231
- 测试方式:本机用 `nb`/pip 装上各适配器包后,为每个适配器**构造真实事件**(各适配器的消息/事件模型),跑本插件同一套功能路径——会话解析(uninfo)→ 消息渲染(uniseg)→ 回复发送(alconna exporter)→ 跨插件捕获(`on_calling_api`)→ 插件调用(复制真实事件 + 目标响应器实际收到命令)。bot 为记录型 mock(拦下所有 API 调用并断言调用的接口名),**未连真实平台**,因此"需要真实 API 才有数据"的项目另标 ⚠。
231
+ 为每个适配器**构造真实事件**跑同一套功能路径(会话解析 → 渲染 → 回复发送 → 跨插件捕获 → 插件调用),bot 是记录型 mock,**未连真实平台**。
232
232
 
233
- | 适配器 | 会话解析 | 收发/渲染 | 回复发送 | 私聊 | @ 按昵称 | 跨插件捕获 | 插件调用 | 群通知¹ | 跨 bot² |
233
+ | 适配器 | 会话解析 | 收发/渲染 | 回复发送 | 私聊 | @ 按昵称 | 跨插件捕获 | 插件调用 | 群通知 | 跨 bot |
234
234
  |---|---|---|---|---|---|---|---|---|---|
235
235
  | OneBot V11 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
236
- | OneBot V12 | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
237
- | Console | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
238
- | Satori | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ⚠ | ❌ | ✅ |
239
- | Telegram | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
240
- | Discord | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
241
- | QQ(频道/C2C/群) | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
242
- | Feishu | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
243
- | Milky | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
244
- | Mirai | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
245
- | Kaiheila(Kook) | ✅ | ✅ | ✅ | 未测³ | ⚠ | ✅ | ⚠ | ❌ | ✅ |
246
- | DoDo | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
247
- | Kritor | ✅ | ✅ | ✅ | 未测³ | ⚠ | ✅ | ✅ | ❌ | ✅ |
248
- | Mail | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
249
- | Minecraft | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
250
- | WXMP | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
251
- | EFChat | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
252
- | YunHu⁴ | ✅ | ✅ | ✅ | 未测³ | ⚠ | ✅ | ✅ | ❌ | ✅ |
253
- | bilibili Live⁴ | ✅ | ✅ | ✅ | ❌ 无好友私聊类型 | ⚠ | ✅ | ✅ | ❌ | ✅ |
254
-
255
- **读表说明**
256
-
257
- - **✅ 已实测通过**:会话键、渲染文本、发送接口、捕获入缓冲、目标插件确实收到了命令,逐项断言过。**本表 17 行在 Python 3.10 实测(167 项检查)**;**YunHu / bilibili Live 两行在 Python 3.14 实测**(3.10 装不上,见下),19 个适配器合计 **187 项检查、只有 2 项未通过**(Satori / Kaiheila 的插件调用)
258
- - **整套依赖栈在 Python 3.14 上跑通**:`nonebot2 2.5.0` + `nonebot-plugin-alconna 0.62.1` + `nonebot-plugin-uninfo 0.11.2` 在 **Python 3.14.3** 上通过了全部回归——主插件 150 项、peer 子插件 67 项、适配器矩阵 187 项(仅上述 2 项未通过)
259
- - **⚠ 需真机/特例**:
260
- - **@ 按昵称**:只有 OneBot V11 实测能按群昵称反查出用户 id(走 `get_group_member_list`);其它适配器需要 uninfo 的成员列表查询实现(多数适配器没有),因此**让 LLM 直接用用户 id 更稳**;渲染 @ 时昵称拿不到会回落成 id
261
- - **插件调用(Satori / Kaiheila)**:这两类适配器的事件结构特殊(Satori 的 `message` 是 `{id, content}` 结构体、Kaiheila 的消息在嵌套的 `event` 里),离线构造"uninfo 与 alconna 都认可"的事件未能复现,因此**未验证通过**;其余 14 个适配器已实测目标响应器收到命令。真机使用请以实际表现为准
262
- - **❌ 不支持**:群系统通知¹(戳一戳/禁言/进出群/撤回是 OneBot 专属事件类型)
263
- - **⚠ Python 版本要求**(上表 YunHu / bilibili Live):这两个适配器包**需要 Python ≥ 3.12**——`bilibili Live` 用了 `typing.TypedDict`(pydantic 在 Python < 3.12 直接拒绝),`YunHu` 还额外用到 `typing.NotRequired`(Python 3.11+),所以在 Python 3.10 的 bot 里**导入即失败**。上表这两行的 ✅ 是**在本机 Python 3.14.3 上实测的结果**:两个包都能导入,`alconna` 的 builder/exporter 与 `uninfo` 的 fetcher 三者齐备,会话解析/渲染/发送/捕获/插件调用全部通过(`bilibili Live` 的场景路径=房间号;`YunHu` 的群资料需要真实 API 才有昵称)。**要用这两个平台就把 bot 跑在 Python 3.12+/3.14 上**(本工作目录另建了参考环境 `.venv314`:Python 3.14.3 + `nonebot2 2.5.0` + `alconna 0.62.1` + `uninfo 0.11.2` + 全部 19 个适配器包,回归与矩阵都在里面跑过)
264
- - **Kook 注意**:社区包 `nonebot-adapter-kook` 的模块名是 `nonebot.adapters.kook`、`get_name()` 报 `Kook`,而 alconna/uninfo 期望的是 `nonebot.adapters.kaiheila`(`Kaiheila`)——**请安装 `nonebot-adapter-kaiheila`**,否则该平台等同于"不被支持"
265
- - **昵称/群名片**:取决于适配器的 uninfo fetcher 能取到什么(很多要真实 API 调用,离线 mock 拿不到),拿不到时回落成用户 id,功能不受影响
266
- - 适配器完全不被 alconna/uninfo 支持时,该会话的消息会被静默跳过(只留 debug 日志),不会报错、也不会影响其它适配器
236
+ | OneBot V12 | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
237
+ | Console | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
238
+ | Satori | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ⚠️ | ✅ | ✅ |
239
+ | Telegram | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
240
+ | Discord | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ⚠️ | ✅ |
241
+ | QQ(频道/C2C/群) | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
242
+ | Feishu | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
243
+ | Milky | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
244
+ | Mirai | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
245
+ | Kaiheila(Kook) | ✅ | ✅ | ✅ | 未测 | ⚠️ | ✅ | ⚠️ | ✅ | ✅ |
246
+ | DoDo | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
247
+ | Kritor | ✅ | ✅ | ✅ | 未测 | ⚠️ | ✅ | ✅ | ❌ | ✅ |
248
+ | Mail | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
249
+ | Minecraft | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
250
+ | WXMP | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
251
+ | EFChat | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
252
+ | YunHu | ✅ | ✅ | ✅ | 未测 | ⚠️ | ✅ | ✅ | ✅ | ✅ |
253
+ | bilibili Live | ✅ | ✅ | ✅ | ❌ 无好友私聊类型 | ⚠️ | ✅ | ✅ | ⚠️ | ✅ |
254
+
255
+ 图例:**✅ 已实测通过** | **⚠️ 已实现但未验证通过**(离线构造不出该适配器的事件,或需要真实 API / 真机)| **❌ 该适配器没有这类事件,或库层无法把该通知定位到群/频道会话(通知会被跳过)** | **未测** 本次未构造该场景。
256
+
257
+ **已知特例**
258
+
259
+ - **@ 按昵称**:只有 OneBot V11 能按群昵称反查用户 id,其余适配器请让 LLM 直接用用户 id
260
+ - **插件调用**:Satori / Kaiheila 的事件结构特殊,离线未验证通过;其余 14 个已实测目标响应器收到命令
261
+ - **群通知**:为每个适配器构造真实通知事件实测,**10 个通过**(OneBot V11/V12、Satori、Telegram、QQ、Milky、Mirai、Kaiheila、DoDo、YunHu);Discord / bilibili 未验证(离线 mock 不够 / 只有部分通知类在库的覆盖范围内);**Feishu、Kritor、EFChat** 的通知 uninfo 与 alconna 都无法定位到群/频道会话,**Minecraft、WXMP** 的通知事件没有群/频道字段(会话按私聊处理)——这几种会被跳过(库层限制,非本插件可修)
262
+ - **YunHu / bilibili Live**:需要把 bot 跑在 **Python ≥ 3.12**(3.10 里导入即失败;这两行的 ✅ 是在 3.14 实测的)
263
+ - **Kaiheila**:请装 `nonebot-adapter-kaiheila`(社区包 `nonebot-adapter-kook` 的模块名与 alconna/uninfo 期望的不一致)
264
+ - **跨 bot**:按 peer 0.4.0+ 的 `session` 协议可用;旧 peer(0.3.x)只有 OneBot V11 的会话能对上
265
+ - 适配器不被 alconna/uninfo 支持时,该会话的消息静默跳过(只留 debug 日志),不影响其它适配器
267
266
 
268
267
  <details>
269
268
  <summary>实测到的各适配器发送接口(供排查用)</summary>
@@ -289,11 +288,6 @@ AIGFM_PEER_CAPTURE_PLUGINS=[] # 要捕获输出的插件名列表,为
289
288
 
290
289
  </details>
291
290
 
292
- > ¹ 群系统通知 = 戳一戳/禁言/进出群/消息撤回。
293
- > ² 跨 bot = 子插件 [nonebot-plugin-aigfm-peer](https://github.com/Funny1Potato/nonebot-plugin-aigfm-peer) 的推送与远程调用。上表按 **peer 0.4.0+** 的协议标 ✅:推送与调用都会带 `session`(`适配器:会话id`)、主插件按会话键落位,因此各适配器都可用。**用旧 peer(0.3.x)时只有 OneBot V11 的会话能对上**(旧协议只带 `group_id`,按 `onebot11:{group_id}` 映射)。peer 侧捕获/推送与主插件侧落位各自实测过,端到端的「复制事件执行命令」在 OneBot V11 与 Console 上跑通。
294
- > ³ Kritor / Kaiheila 的私聊事件本次未构造,故未测;它们的私聊类型在 alconna/uninfo 里是有实现的。
295
- > ⁴ 这两个适配器**需要 Python ≥ 3.12**(3.10 环境里导入即失败),表里这两行是在 **Python 3.14.3** 下实测的结果,见下方说明。
296
-
297
291
  ## 🔌 跨插件感知
298
292
 
299
293
  ### 工作原理
@@ -15,10 +15,6 @@ import anyio
15
15
  import httpx
16
16
  from nonebot import get_driver, logger, on_message, on_notice, require
17
17
  from nonebot.adapters import Bot, Event
18
- from nonebot.adapters.onebot.v11 import (
19
- GroupBanNoticeEvent, GroupDecreaseNoticeEvent, GroupIncreaseNoticeEvent,
20
- GroupRecallNoticeEvent, PokeNotifyEvent,
21
- )
22
18
  from nonebot.permission import SUPERUSER
23
19
  from nonebot.plugin import PluginMetadata, inherit_supported_adapters
24
20
 
@@ -43,6 +39,7 @@ from .image_gen_client import ImageGenClient
43
39
  from .meme_store import MemeStore
44
40
  from .memory_store import MemoryStore
45
41
  from .models import ChatMessage
42
+ from .notice import is_group_notice, notice_text
46
43
  from .peer_client import PeerClient
47
44
  from .plugin_invoker import PluginInvoker
48
45
  from .preset_store import PresetStore
@@ -434,60 +431,6 @@ def _is_message(event: Event) -> bool:
434
431
  return True
435
432
 
436
433
 
437
- _NOTICE_TYPES = (
438
- PokeNotifyEvent, GroupBanNoticeEvent, GroupIncreaseNoticeEvent,
439
- GroupDecreaseNoticeEvent, GroupRecallNoticeEvent,
440
- )
441
-
442
-
443
- def _is_group_notice(event: Event) -> bool:
444
- """只收群内系统通知;PokeNotifyEvent 的 group_id 可能为空(私聊戳一戳),过滤掉
445
-
446
- 群 notice 是 onebot 专属事件类型,其它适配器暂不处理(没有统一的通用通知模型)。
447
- """
448
- if not isinstance(event, _NOTICE_TYPES):
449
- return False
450
- return bool(getattr(event, "group_id", None))
451
-
452
-
453
- async def _resolve_notice_text(bot: Bot, event: Event) -> str | None:
454
- """把群 notice 事件渲染成一行文本;昵称查询失败回落用户 id"""
455
- group_id = int(getattr(event, "group_id"))
456
- info = SessionInfo(
457
- key=session_key("onebot11", str(group_id)), slug="onebot11", scene_path=str(group_id),
458
- native_chat_id=str(group_id), adapter_name=bot.adapter.get_name(),
459
- )
460
-
461
- async def _name(user_id) -> str:
462
- from .session import display_user
463
- name = await display_user(bot, info, user_id)
464
- return name or str(user_id)
465
-
466
- if isinstance(event, PokeNotifyEvent):
467
- return f"{await _name(event.user_id)} 戳了戳 {await _name(event.target_id)}"
468
- if isinstance(event, GroupBanNoticeEvent):
469
- if event.sub_type == "self_ban":
470
- return f"我被禁言 {event.duration} 分钟"
471
- if event.sub_type == "self_lift_ban":
472
- return "我被解除禁言"
473
- if event.sub_type == "lift_ban":
474
- return f"{await _name(event.operator_id)} 解除了 {await _name(event.user_id)} 的禁言"
475
- return f"{await _name(event.operator_id)} 将 {await _name(event.user_id)} 禁言 {event.duration} 分钟"
476
- if isinstance(event, GroupIncreaseNoticeEvent):
477
- return f"{await _name(event.user_id)} 加入了群聊"
478
- if isinstance(event, GroupDecreaseNoticeEvent):
479
- if event.sub_type == "kick_me":
480
- return "我被移出了群聊"
481
- if event.sub_type == "kick":
482
- return f"{await _name(event.user_id)} 被 {await _name(event.operator_id)} 移出了群聊"
483
- return f"{await _name(event.user_id)} 退出了群聊"
484
- if isinstance(event, GroupRecallNoticeEvent):
485
- if event.user_id == event.operator_id:
486
- return f"{await _name(event.user_id)} 撤回了一条消息"
487
- return f"{await _name(event.operator_id)} 撤回了 {await _name(event.user_id)} 的一条消息"
488
- return None
489
-
490
-
491
434
  async def _command_session(bot: Bot, event: Event) -> SessionInfo | None:
492
435
  """管理命令用:解析当前会话(解析不出时给用户一句可读的提示)"""
493
436
  return await resolve_session(bot, event)
@@ -525,7 +468,9 @@ set_preset_cmd = on_alconna(_cmd("set_preset", Args["rest", MultiVar(str, "*")])
525
468
  reload_meme_cmd = on_alconna(_cmd("reload_meme", Args["rest", MultiVar(str, "*")]),
526
469
  aliases={"重载表情包"}, permission=SUPERUSER, priority=0, block=True)
527
470
  auto_chat = on_message(rule=_is_message, priority=1, block=False)
528
- group_notice = on_notice(rule=_is_group_notice, priority=1, block=False)
471
+ # 类型闸门是内核给的(Matcher.check_rule 按 event.get_type() == "notice" 过滤,见 notice.py 开头),
472
+ # 所以这里收的是所有适配器的通知事件;rule 只做「带会话线索」的廉价预筛
473
+ group_notice = on_notice(rule=is_group_notice, priority=1, block=False)
529
474
 
530
475
 
531
476
  @status_cmd.handle()
@@ -687,23 +632,23 @@ async def handle_auto_chat(bot: Bot, event: Event):
687
632
 
688
633
  @group_notice.handle()
689
634
  async def handle_group_notice(bot: Bot, event: Event):
690
- """群系统通知(戳一戳/禁言/进出群/消息撤回)→ 渲染后入缓冲区
635
+ """群/频道系统通知(戳一戳/禁言/进出群/消息撤回等)→ 渲染后入缓冲区
691
636
 
692
- notice 事件没有用户消息上下文:不更新 processor._current_user_id(避免污染工具调用身份),
637
+ 通知事件没有用户消息上下文:不更新 processor._current_user_id(避免污染工具调用身份),
693
638
  批处理触发依赖该会话最近一次真实用户消息(与 peer 推送的等待行为一致)
694
639
  """
695
- key = session_key("onebot11", str(getattr(event, "group_id")))
696
- if key not in _enabled_keys():
640
+ info = await resolve_session(bot, event)
641
+ if info is None or info.is_private or not is_enabled(info, plugin_config):
697
642
  return
698
643
  try:
699
- text = await _resolve_notice_text(bot, event)
644
+ text = await notice_text(bot, info, event)
700
645
  except Exception as e:
701
646
  logger.error(f"[通知] 事件渲染失败: {e}")
702
647
  return
703
648
  if not text:
704
649
  return
705
- logger.info(f"[通知] {key}: {text}")
706
- _add_notice_message(key, text)
650
+ logger.info(f"[通知] {info.key}: {text}")
651
+ _add_notice_message(info.key, text)
707
652
 
708
653
 
709
654
  # ========== 启动 ==========
@@ -0,0 +1,233 @@
1
+ """群/频道系统通知的渲染层
2
+
3
+ NoneBot 的 `on_notice` 已经按 `event.get_type() == "notice"` 过滤(内核
4
+ `Matcher.check_rule` 里就是 `event_type == (cls.type or event_type)`),所以事件类型闸门
5
+ 本身就是跨适配器的;留给本插件的是两件事:
6
+
7
+ 1. 这条通知属于哪个会话 —— 交给 `session.resolve_session`(uninfo → alconna Target 回落)
8
+ 2. 渲染成一行中文 —— OneBot V11 走下面这份最详细的分支;其它适配器用一张类别关键词表,
9
+ 映射不到才回落到适配器自带的事件描述/事件名。
10
+
11
+ 多数适配器的 `get_event_description()` 是整段 `model_dump` 的 JSON,直接进 prompt 会灌水,
12
+ 所以这里一律丢弃 dump 形态的描述(只有 kritor/minecraft/efchat 这类真正写了句子的才采用)。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from collections.abc import Mapping
19
+
20
+ from nonebot.adapters import Bot, Event
21
+ from nonebot.adapters.onebot.v11 import (
22
+ GroupBanNoticeEvent, GroupDecreaseNoticeEvent, GroupIncreaseNoticeEvent,
23
+ GroupRecallNoticeEvent, PokeNotifyEvent,
24
+ )
25
+
26
+ from .session import SessionInfo, display_user
27
+
28
+ # OneBot V11 有结构化字段(sub_type/operator_id/target_id),文案做得最细
29
+ _ONEBOT_NOTICE_TYPES = (
30
+ PokeNotifyEvent, GroupBanNoticeEvent, GroupIncreaseNoticeEvent,
31
+ GroupDecreaseNoticeEvent, GroupRecallNoticeEvent,
32
+ )
33
+
34
+ # 「会话线索」的字段/键名(比较前统一规范化:去掉非字母数字并小写,因此
35
+ # group_id / groupId / chat_id / chatId 都等价)。命中且取值非空即认为不是纯好友/私聊类通知。
36
+ _SCENE_FIELDS = frozenset({
37
+ "groupid", "channelid", "guildid", "chatid", "roomid",
38
+ "group", "channel", "guild", "chat", "room", "scene",
39
+ })
40
+
41
+ # 递归查找的深度与每个容器的取样条数(只为便宜预筛,不需要穷举)
42
+ _SCENE_DEPTH = 3
43
+ _SCENE_SAMPLE = 8
44
+
45
+ # 递归时跳过的字段:消息体/回复体又大又不会带会话线索
46
+ _SKIP_FIELDS = frozenset({"message", "original_message", "raw_message", "reply"})
47
+
48
+ # 通用主体 id 的候选路径(`event.get_user_id()` 拿不到时依次尝试;整数表示列表下标)。
49
+ # 列表型(Telegram 的 new_chat_members[0] / left_chat_member)要排在通用字段前,否则会拿错人
50
+ _SUBJECT_PATHS = (
51
+ ("new_chat_members", 0, "id"), ("left_chat_member", "id"),
52
+ ("user_id",), ("user", "id"), ("member", "id"), ("member", "user", "id"),
53
+ ("target_id",), ("operator_id",), ("operator", "id"), ("member_id",), ("uin",), ("from_id",),
54
+ )
55
+
56
+ # 类别关键词表:`type(event).__name__ + get_event_name()` 规范化(只留小写字母数字)后按序匹配,
57
+ # 关键词自身也已是规范化形态。命中的模板即渲染文案,`{who}` 换主体昵称(拿不到则留空)。
58
+ # 顺序有意义:消息撤回要排在「成员删除/配置变更」之前,bilibili 直播类要排在通用类和之前。
59
+ _NOTICE_RULES: tuple[tuple[tuple[str, ...], str], ...] = (
60
+ (("poke", "nudge"), "{who} 被戳了戳"),
61
+ (("recall", "messagedelete", "messagemdelete", "deleterecall"), "{who} 撤回了一条消息"),
62
+ (("userenter", "userenterv"), "{who} 进入了直播间"),
63
+ (("userfollow",), "{who} 关注了直播间"),
64
+ (("guardbuy", "sendgift", "specialgift"), "{who} 赠送了礼物"),
65
+ (("wholeban", "liftban", "unmute", "unban", "mute", "ban", "silent"), "{who} 的禁言状态发生了变化"),
66
+ (
67
+ (
68
+ "memberincrease", "memberadded", "memberadd", "memberjoin", "newchatmember",
69
+ "newmember", "groupjoin", "useradded", "join", "increase", "added", "enter",
70
+ ),
71
+ "{who} 加入了群聊",
72
+ ),
73
+ (
74
+ (
75
+ "memberdecrease", "memberremoved", "memberremove", "memberleave", "memberquit",
76
+ "memberkick", "leftchatmember", "groupmemberdeleted", "groupmemberwithdrawn",
77
+ "groupdecrease", "leave", "quit", "kick", "remove", "decrease", "deleted", "withdrawn",
78
+ ),
79
+ "{who} 离开了群聊",
80
+ ),
81
+ (("adminchange", "adminchanged", "permissionchange", "roleadd", "roledelete", "roleupdate"),
82
+ "{who} 的管理权限发生了变化"),
83
+ (("reaction", "reactmessage"), "{who} 对消息添加了表情回应"),
84
+ (("essence", "pin", "pinned"), "消息的置顶/精华状态发生了变化"),
85
+ (("groupnamechange", "roomchangeinfo", "chatphoto", "chattitle", "namechange",
86
+ "configupdated", "cardchanged", "specialtitle", "updated", "change"),
87
+ "会话信息发生了变化"),
88
+ )
89
+
90
+ # 描述形态的 JSON dump 一律不用(`str(model_dump(...))` / `str(self.dict())` 都以 `{` 开头)
91
+ _DESCRIPTION_MAX = 200
92
+
93
+
94
+ def is_group_notice(event: Event) -> bool:
95
+ """`on_notice` 的规则:只看事件里有没有会话线索,不发任何请求
96
+
97
+ 真正的「是否启用/是否私聊」判断在 handler 里做(需要 uninfo,可能有 API 调用)。
98
+ 这里必须**递归**找:多数适配器把会话 id 放在顶层(`group_id`/`channel_id`…),但
99
+ Feishu 在 `event.event.chat_id`、Milky 在 `data.group_id`、YunHu 在 `event.chatId`。
100
+ 放宽的代价只是多查一次 uninfo(handler 还会按 `is_private` 复核),漏判才会真的丢通知。
101
+ """
102
+ return _has_scene_hint(event, _SCENE_DEPTH)
103
+
104
+
105
+ def _has_scene_hint(value, depth: int) -> bool:
106
+ if depth < 0 or value is None or isinstance(value, (str, bytes, int, float, bool)):
107
+ return False
108
+ if isinstance(value, Mapping):
109
+ for key, item in value.items():
110
+ if _norm_key(key) in _SCENE_FIELDS and item:
111
+ return True
112
+ if _has_scene_hint(item, depth - 1):
113
+ return True
114
+ return False
115
+ if isinstance(value, (list, tuple, set, frozenset)):
116
+ return any(_has_scene_hint(item, depth - 1) for item in list(value)[:_SCENE_SAMPLE])
117
+ fields = getattr(value, "model_fields", None)
118
+ if fields is not None: # pydantic 模型(事件/嵌套模型)
119
+ for name in fields:
120
+ if name.startswith("_") or name in _SKIP_FIELDS:
121
+ continue
122
+ try:
123
+ item = getattr(value, name, None)
124
+ except Exception:
125
+ continue
126
+ if _norm_key(name) in _SCENE_FIELDS and item:
127
+ return True
128
+ if _has_scene_hint(item, depth - 1):
129
+ return True
130
+ extra = getattr(value, "__pydantic_extra__", None)
131
+ return bool(extra) and _has_scene_hint(extra, depth - 1)
132
+ return False
133
+
134
+
135
+ def _norm_key(name) -> str:
136
+ return re.sub(r"[^a-z0-9]", "", str(name).lower())
137
+
138
+
139
+ async def notice_text(bot: Bot, info: SessionInfo, event: Event) -> str | None:
140
+ """把一条通知渲染成一行文本;渲染不出返回 None(调用方跳过该条)"""
141
+ if isinstance(event, _ONEBOT_NOTICE_TYPES):
142
+ return await _onebot_notice_text(bot, info, event)
143
+ return await _generic_notice_text(bot, info, event)
144
+
145
+
146
+ async def _onebot_notice_text(bot: Bot, info: SessionInfo, event: Event) -> str | None:
147
+ async def _name(user_id) -> str:
148
+ name = await display_user(bot, info, user_id)
149
+ return name or str(user_id)
150
+
151
+ if isinstance(event, PokeNotifyEvent):
152
+ return f"{await _name(event.user_id)} 戳了戳 {await _name(event.target_id)}"
153
+ if isinstance(event, GroupBanNoticeEvent):
154
+ if event.sub_type == "self_ban":
155
+ return f"我被禁言 {event.duration} 分钟"
156
+ if event.sub_type == "self_lift_ban":
157
+ return "我被解除禁言"
158
+ if event.sub_type == "lift_ban":
159
+ return f"{await _name(event.operator_id)} 解除了 {await _name(event.user_id)} 的禁言"
160
+ return f"{await _name(event.operator_id)} 将 {await _name(event.user_id)} 禁言 {event.duration} 分钟"
161
+ if isinstance(event, GroupIncreaseNoticeEvent):
162
+ return f"{await _name(event.user_id)} 加入了群聊"
163
+ if isinstance(event, GroupDecreaseNoticeEvent):
164
+ if event.sub_type == "kick_me":
165
+ return "我被移出了群聊"
166
+ if event.sub_type == "kick":
167
+ return f"{await _name(event.user_id)} 被 {await _name(event.operator_id)} 移出了群聊"
168
+ return f"{await _name(event.user_id)} 退出了群聊"
169
+ if isinstance(event, GroupRecallNoticeEvent):
170
+ if event.user_id == event.operator_id:
171
+ return f"{await _name(event.user_id)} 撤回了一条消息"
172
+ return f"{await _name(event.operator_id)} 撤回了 {await _name(event.user_id)} 的一条消息"
173
+ return None
174
+
175
+
176
+ async def _generic_notice_text(bot: Bot, info: SessionInfo, event: Event) -> str | None:
177
+ event_name = ""
178
+ try:
179
+ event_name = str(event.get_event_name() or "")
180
+ except Exception:
181
+ pass
182
+ key = re.sub(r"[^a-z0-9]+", "", f"{type(event).__name__} {event_name}".lower())
183
+ for keywords, template in _NOTICE_RULES:
184
+ if any(kw in key for kw in keywords):
185
+ return _fill(template, await _subject_name(bot, info, event))
186
+ return _prose_description(event, event_name) or event_name or None
187
+
188
+
189
+ def _fill(template: str, who: str) -> str:
190
+ text = template.replace("{who}", who).strip()
191
+ return re.sub(r"\s{2,}", " ", text)
192
+
193
+
194
+ async def _subject_name(bot: Bot, info: SessionInfo, event: Event) -> str:
195
+ """通知主体(被禁言/被移出/撤回消息的那位)的显示名"""
196
+ uid = _subject_id(event)
197
+ if not uid:
198
+ return ""
199
+ return await display_user(bot, info, uid) or uid
200
+
201
+
202
+ def _subject_id(event: Event) -> str:
203
+ try:
204
+ uid = event.get_user_id()
205
+ except Exception:
206
+ uid = ""
207
+ if uid:
208
+ return str(uid)
209
+ for path in _SUBJECT_PATHS:
210
+ obj: object = event
211
+ for attr in path:
212
+ if isinstance(attr, int):
213
+ obj = obj[attr] if isinstance(obj, (list, tuple)) and len(obj) > attr else None
214
+ else:
215
+ obj = getattr(obj, attr, None)
216
+ if obj is None:
217
+ break
218
+ if obj not in (None, ""):
219
+ return str(obj)
220
+ return ""
221
+
222
+
223
+ def _prose_description(event: Event, event_name: str) -> str | None:
224
+ """适配器自带的事件描述;只有像句子的才用(dump 形态与空描述丢弃)"""
225
+ try:
226
+ desc = str(event.get_event_description() or "").strip()
227
+ except Exception:
228
+ return None
229
+ if not desc or desc == event_name or len(desc) > _DESCRIPTION_MAX:
230
+ return None
231
+ if desc.startswith("{") or desc.startswith("["):
232
+ return None
233
+ return desc
@@ -110,6 +110,14 @@ class SessionInfo:
110
110
  return int(self.native_chat_id) if self.native_chat_id.isdigit() else 0
111
111
 
112
112
 
113
+ def event_user_id(event: Event) -> str:
114
+ """事件主体 id;通知类事件的 `get_user_id()` 常常直接抛 ValueError(OneBot V12 / Telegram / EFChat 等)"""
115
+ try:
116
+ return str(event.get_user_id() or "")
117
+ except Exception:
118
+ return ""
119
+
120
+
113
121
  def _from_uninfo(bot: Bot, event: Event, sess) -> SessionInfo:
114
122
  scene = sess.scene
115
123
  member_scene = scene.parent or scene
@@ -134,7 +142,7 @@ def _from_uninfo(bot: Bot, event: Event, sess) -> SessionInfo:
134
142
  scene_type=scene.type,
135
143
  member_scene_type=member_scene.type,
136
144
  member_scene_id=str(member_scene.id),
137
- user_id=str(event.get_user_id() or user.id or ""),
145
+ user_id=event_user_id(event) or user.id or "",
138
146
  user_name=display_name(user),
139
147
  card=card,
140
148
  target=target,
@@ -173,7 +181,7 @@ async def resolve_session(bot: Bot, event: Event) -> SessionInfo | None:
173
181
  adapter_name=bot.adapter.get_name(),
174
182
  is_private=bool(target.private),
175
183
  is_channel=bool(target.channel),
176
- user_id=str(event.get_user_id() or ""),
184
+ user_id=event_user_id(event),
177
185
  target=target,
178
186
  )
179
187
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nonebot-plugin-aigf-master
3
- Version: 2.0.0b1
3
+ Version: 2.0.0b2
4
4
  Summary: 群聊特化LLM聊天机器人(增强版),具有记忆、表情包、跨插件调用、跨bot通信与跨适配器支持
5
5
  License-Expression: MIT
6
6
  Requires-Python: <4.0,>=3.10
@@ -67,7 +67,7 @@ Dynamic: license-file
67
67
  - ⏱️ **不完整消息检测**:纯 @ 消息或连续发送时自动延长等待时间
68
68
  - 🕐 **消息时间标注**:可选为 LLM 看到的消息加上 `[MM-DD HH:MM]` 时间前缀(`AIGFM_SHOW_MESSAGE_TIME`)
69
69
  - 📄 **完整消息类型**:合并聊天记录、小程序/卡片、XML,以及 QQ 表情/语音/视频/群文件/链接分享/推荐名片/位置/音乐/骰子等共 18 种消息段均可解析(纯此类消息不再被丢弃)
70
- - 🔔 **群系统通知**:戳一戳、禁言/解禁、新人进群、退群被踢、消息撤回会以 `[系统]` 消息进入 LLM 上下文
70
+ - 🔔 **群系统通知**:戳一戳、禁言/解禁、新人进群、退群被踢、消息撤回等会以 `[系统]` 消息进入 LLM 上下文(OneBot V11 文案最细,其它适配器按通知类别映射成中文,见「多适配器支持」)
71
71
  - 🔒 **强制 JSON 输出**:确保 LLM 始终返回正确格式
72
72
  - ⚡ **轻量高效**:单次 LLM 调用完成对话 + 记忆管理 + 命令学习
73
73
 
@@ -80,9 +80,9 @@ Dynamic: license-file
80
80
  >
81
81
  > 从 2.0 起插件还依赖 `nonebot-plugin-alconna` 与 `nonebot-plugin-uninfo`(跨适配器收发与会话/成员信息),用下面的方式安装时会自动装上。
82
82
 
83
- ### 安装 beta 版本(2.0.0b1)
83
+ ### 安装 beta 版本(2.0.0b2)
84
84
 
85
- `2.0.0b1` 是**预发布(pre-release)版本**,因此:
85
+ `2.0.0b2` 是**预发布(pre-release)版本**,因此:
86
86
 
87
87
  - `pip install nonebot-plugin-aigf-master` 装的仍是稳定版 **1.6.x**
88
88
  - `nb plugin install nonebot-plugin-aigf-master` 同样只装稳定版,而且 nb-cli **不支持指定版本**,所以 beta 只能用 pip 装
@@ -90,7 +90,7 @@ Dynamic: license-file
90
90
  要装 beta,**必须显式写出版本号**:
91
91
 
92
92
  ```bash
93
- pip install nonebot-plugin-aigf-master==2.0.0b1
93
+ pip install nonebot-plugin-aigf-master==2.0.0b2
94
94
  ```
95
95
 
96
96
  在 `pyproject.toml` 中添加:
@@ -250,42 +250,41 @@ AIGFM_PEER_CAPTURE_PLUGINS=[] # 要捕获输出的插件名列表,为
250
250
 
251
251
  ### 功能 × 适配器支持表(2026-09-25 实测)
252
252
 
253
- 测试方式:本机用 `nb`/pip 装上各适配器包后,为每个适配器**构造真实事件**(各适配器的消息/事件模型),跑本插件同一套功能路径——会话解析(uninfo)→ 消息渲染(uniseg)→ 回复发送(alconna exporter)→ 跨插件捕获(`on_calling_api`)→ 插件调用(复制真实事件 + 目标响应器实际收到命令)。bot 为记录型 mock(拦下所有 API 调用并断言调用的接口名),**未连真实平台**,因此"需要真实 API 才有数据"的项目另标 ⚠。
253
+ 为每个适配器**构造真实事件**跑同一套功能路径(会话解析 → 渲染 → 回复发送 → 跨插件捕获 → 插件调用),bot 是记录型 mock,**未连真实平台**。
254
254
 
255
- | 适配器 | 会话解析 | 收发/渲染 | 回复发送 | 私聊 | @ 按昵称 | 跨插件捕获 | 插件调用 | 群通知¹ | 跨 bot² |
255
+ | 适配器 | 会话解析 | 收发/渲染 | 回复发送 | 私聊 | @ 按昵称 | 跨插件捕获 | 插件调用 | 群通知 | 跨 bot |
256
256
  |---|---|---|---|---|---|---|---|---|---|
257
257
  | OneBot V11 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
258
- | OneBot V12 | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
259
- | Console | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
260
- | Satori | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ⚠ | ❌ | ✅ |
261
- | Telegram | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
262
- | Discord | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
263
- | QQ(频道/C2C/群) | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
264
- | Feishu | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
265
- | Milky | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
266
- | Mirai | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
267
- | Kaiheila(Kook) | ✅ | ✅ | ✅ | 未测³ | ⚠ | ✅ | ⚠ | ❌ | ✅ |
268
- | DoDo | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
269
- | Kritor | ✅ | ✅ | ✅ | 未测³ | ⚠ | ✅ | ✅ | ❌ | ✅ |
270
- | Mail | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
271
- | Minecraft | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
272
- | WXMP | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
273
- | EFChat | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ✅ | ❌ | ✅ |
274
- | YunHu⁴ | ✅ | ✅ | ✅ | 未测³ | ⚠ | ✅ | ✅ | ❌ | ✅ |
275
- | bilibili Live⁴ | ✅ | ✅ | ✅ | ❌ 无好友私聊类型 | ⚠ | ✅ | ✅ | ❌ | ✅ |
276
-
277
- **读表说明**
278
-
279
- - **✅ 已实测通过**:会话键、渲染文本、发送接口、捕获入缓冲、目标插件确实收到了命令,逐项断言过。**本表 17 行在 Python 3.10 实测(167 项检查)**;**YunHu / bilibili Live 两行在 Python 3.14 实测**(3.10 装不上,见下),19 个适配器合计 **187 项检查、只有 2 项未通过**(Satori / Kaiheila 的插件调用)
280
- - **整套依赖栈在 Python 3.14 上跑通**:`nonebot2 2.5.0` + `nonebot-plugin-alconna 0.62.1` + `nonebot-plugin-uninfo 0.11.2` 在 **Python 3.14.3** 上通过了全部回归——主插件 150 项、peer 子插件 67 项、适配器矩阵 187 项(仅上述 2 项未通过)
281
- - **⚠ 需真机/特例**:
282
- - **@ 按昵称**:只有 OneBot V11 实测能按群昵称反查出用户 id(走 `get_group_member_list`);其它适配器需要 uninfo 的成员列表查询实现(多数适配器没有),因此**让 LLM 直接用用户 id 更稳**;渲染 @ 时昵称拿不到会回落成 id
283
- - **插件调用(Satori / Kaiheila)**:这两类适配器的事件结构特殊(Satori 的 `message` 是 `{id, content}` 结构体、Kaiheila 的消息在嵌套的 `event` 里),离线构造"uninfo 与 alconna 都认可"的事件未能复现,因此**未验证通过**;其余 14 个适配器已实测目标响应器收到命令。真机使用请以实际表现为准
284
- - **❌ 不支持**:群系统通知¹(戳一戳/禁言/进出群/撤回是 OneBot 专属事件类型)
285
- - **⚠ Python 版本要求**(上表 YunHu / bilibili Live):这两个适配器包**需要 Python ≥ 3.12**——`bilibili Live` 用了 `typing.TypedDict`(pydantic 在 Python < 3.12 直接拒绝),`YunHu` 还额外用到 `typing.NotRequired`(Python 3.11+),所以在 Python 3.10 的 bot 里**导入即失败**。上表这两行的 ✅ 是**在本机 Python 3.14.3 上实测的结果**:两个包都能导入,`alconna` 的 builder/exporter 与 `uninfo` 的 fetcher 三者齐备,会话解析/渲染/发送/捕获/插件调用全部通过(`bilibili Live` 的场景路径=房间号;`YunHu` 的群资料需要真实 API 才有昵称)。**要用这两个平台就把 bot 跑在 Python 3.12+/3.14 上**(本工作目录另建了参考环境 `.venv314`:Python 3.14.3 + `nonebot2 2.5.0` + `alconna 0.62.1` + `uninfo 0.11.2` + 全部 19 个适配器包,回归与矩阵都在里面跑过)
286
- - **Kook 注意**:社区包 `nonebot-adapter-kook` 的模块名是 `nonebot.adapters.kook`、`get_name()` 报 `Kook`,而 alconna/uninfo 期望的是 `nonebot.adapters.kaiheila`(`Kaiheila`)——**请安装 `nonebot-adapter-kaiheila`**,否则该平台等同于"不被支持"
287
- - **昵称/群名片**:取决于适配器的 uninfo fetcher 能取到什么(很多要真实 API 调用,离线 mock 拿不到),拿不到时回落成用户 id,功能不受影响
288
- - 适配器完全不被 alconna/uninfo 支持时,该会话的消息会被静默跳过(只留 debug 日志),不会报错、也不会影响其它适配器
258
+ | OneBot V12 | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
259
+ | Console | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
260
+ | Satori | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ⚠️ | ✅ | ✅ |
261
+ | Telegram | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
262
+ | Discord | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ⚠️ | ✅ |
263
+ | QQ(频道/C2C/群) | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
264
+ | Feishu | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
265
+ | Milky | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
266
+ | Mirai | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
267
+ | Kaiheila(Kook) | ✅ | ✅ | ✅ | 未测 | ⚠️ | ✅ | ⚠️ | ✅ | ✅ |
268
+ | DoDo | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ |
269
+ | Kritor | ✅ | ✅ | ✅ | 未测 | ⚠️ | ✅ | ✅ | ❌ | ✅ |
270
+ | Mail | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
271
+ | Minecraft | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
272
+ | WXMP | ✅(仅私聊) | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
273
+ | EFChat | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ❌ | ✅ |
274
+ | YunHu | ✅ | ✅ | ✅ | 未测 | ⚠️ | ✅ | ✅ | ✅ | ✅ |
275
+ | bilibili Live | ✅ | ✅ | ✅ | ❌ 无好友私聊类型 | ⚠️ | ✅ | ✅ | ⚠️ | ✅ |
276
+
277
+ 图例:**✅ 已实测通过** | **⚠️ 已实现但未验证通过**(离线构造不出该适配器的事件,或需要真实 API / 真机)| **❌ 该适配器没有这类事件,或库层无法把该通知定位到群/频道会话(通知会被跳过)** | **未测** 本次未构造该场景。
278
+
279
+ **已知特例**
280
+
281
+ - **@ 按昵称**:只有 OneBot V11 能按群昵称反查用户 id,其余适配器请让 LLM 直接用用户 id
282
+ - **插件调用**:Satori / Kaiheila 的事件结构特殊,离线未验证通过;其余 14 个已实测目标响应器收到命令
283
+ - **群通知**:为每个适配器构造真实通知事件实测,**10 个通过**(OneBot V11/V12、Satori、Telegram、QQ、Milky、Mirai、Kaiheila、DoDo、YunHu);Discord / bilibili 未验证(离线 mock 不够 / 只有部分通知类在库的覆盖范围内);**Feishu、Kritor、EFChat** 的通知 uninfo 与 alconna 都无法定位到群/频道会话,**Minecraft、WXMP** 的通知事件没有群/频道字段(会话按私聊处理)——这几种会被跳过(库层限制,非本插件可修)
284
+ - **YunHu / bilibili Live**:需要把 bot 跑在 **Python ≥ 3.12**(3.10 里导入即失败;这两行的 ✅ 是在 3.14 实测的)
285
+ - **Kaiheila**:请装 `nonebot-adapter-kaiheila`(社区包 `nonebot-adapter-kook` 的模块名与 alconna/uninfo 期望的不一致)
286
+ - **跨 bot**:按 peer 0.4.0+ 的 `session` 协议可用;旧 peer(0.3.x)只有 OneBot V11 的会话能对上
287
+ - 适配器不被 alconna/uninfo 支持时,该会话的消息静默跳过(只留 debug 日志),不影响其它适配器
289
288
 
290
289
  <details>
291
290
  <summary>实测到的各适配器发送接口(供排查用)</summary>
@@ -311,11 +310,6 @@ AIGFM_PEER_CAPTURE_PLUGINS=[] # 要捕获输出的插件名列表,为
311
310
 
312
311
  </details>
313
312
 
314
- > ¹ 群系统通知 = 戳一戳/禁言/进出群/消息撤回。
315
- > ² 跨 bot = 子插件 [nonebot-plugin-aigfm-peer](https://github.com/Funny1Potato/nonebot-plugin-aigfm-peer) 的推送与远程调用。上表按 **peer 0.4.0+** 的协议标 ✅:推送与调用都会带 `session`(`适配器:会话id`)、主插件按会话键落位,因此各适配器都可用。**用旧 peer(0.3.x)时只有 OneBot V11 的会话能对上**(旧协议只带 `group_id`,按 `onebot11:{group_id}` 映射)。peer 侧捕获/推送与主插件侧落位各自实测过,端到端的「复制事件执行命令」在 OneBot V11 与 Console 上跑通。
316
- > ³ Kritor / Kaiheila 的私聊事件本次未构造,故未测;它们的私聊类型在 alconna/uninfo 里是有实现的。
317
- > ⁴ 这两个适配器**需要 Python ≥ 3.12**(3.10 环境里导入即失败),表里这两行是在 **Python 3.14.3** 下实测的结果,见下方说明。
318
-
319
313
  ## 🔌 跨插件感知
320
314
 
321
315
  ### 工作原理
@@ -15,6 +15,7 @@ nonebot_plugin_aigf_master/llm_client.py
15
15
  nonebot_plugin_aigf_master/meme_store.py
16
16
  nonebot_plugin_aigf_master/memory_store.py
17
17
  nonebot_plugin_aigf_master/models.py
18
+ nonebot_plugin_aigf_master/notice.py
18
19
  nonebot_plugin_aigf_master/peer_client.py
19
20
  nonebot_plugin_aigf_master/plugin_discovery.py
20
21
  nonebot_plugin_aigf_master/plugin_invoker.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nonebot-plugin-aigf-master"
7
- version = "2.0.0b1"
7
+ version = "2.0.0b2"
8
8
  description = "群聊特化LLM聊天机器人(增强版),具有记忆、表情包、跨插件调用、跨bot通信与跨适配器支持"
9
9
  readme = "README.md"
10
10
  license = "MIT"