nonebot-plugin-aigf-master 2.0.0b0__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.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/PKG-INFO +66 -17
  2. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/README.md +65 -16
  3. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/__init__.py +18 -69
  4. nonebot_plugin_aigf_master-2.0.0b2/nonebot_plugin_aigf_master/notice.py +233 -0
  5. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/plugin_invoker.py +139 -6
  6. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/session.py +10 -2
  7. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/unimsg.py +25 -7
  8. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master.egg-info/PKG-INFO +66 -17
  9. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master.egg-info/SOURCES.txt +1 -0
  10. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/pyproject.toml +1 -1
  11. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/LICENSE +0 -0
  12. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/anime_recognizer.py +0 -0
  13. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/animetrace_client.py +0 -0
  14. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/api_hooks.py +0 -0
  15. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/cache_cleanup.py +0 -0
  16. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/command_learner.py +0 -0
  17. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/config.py +0 -0
  18. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/context_bus.py +0 -0
  19. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/image_gen_client.py +0 -0
  20. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/image_handler.py +0 -0
  21. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/llm_client.py +0 -0
  22. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/meme_store.py +0 -0
  23. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/memory_store.py +0 -0
  24. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/models.py +0 -0
  25. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/peer_client.py +0 -0
  26. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/plugin_discovery.py +0 -0
  27. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/preset_store.py +0 -0
  28. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/processor.py +0 -0
  29. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/prompt_builder.py +0 -0
  30. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/response_parser.py +0 -0
  31. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/search_client.py +0 -0
  32. {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/vlm_client.py +0 -0
  33. {nonebot_plugin_aigf_master-2.0.0b0 → 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.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master.egg-info/requires.txt +0 -0
  35. {nonebot_plugin_aigf_master-2.0.0b0 → 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.0b0 → 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.0b0
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.0b0)
83
+ ### 安装 beta 版本(2.0.0b2)
84
84
 
85
- `2.0.0b0` 是**预发布(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.0b0
93
+ pip install nonebot-plugin-aigf-master==2.0.0b2
94
94
  ```
95
95
 
96
96
  在 `pyproject.toml` 中添加:
@@ -248,18 +248,67 @@ AIGFM_PEER_CAPTURE_PLUGINS=[] # 要捕获输出的插件名列表,为
248
248
  - 落盘目录名不能含 `:`,因此磁盘上写作 `onebot11_617770183`;`memory/` 下旧版裸数字目录会在启动时**一次性自动迁移**(目标已存在则跳过,幂等)
249
249
  - 非启用会话不发请求、不入缓冲:插件只会为启用的会话解析成员信息与图片
250
250
 
251
- ### 各能力在非 OneBot 适配器上的边界
252
-
253
- | 能力 | 其它适配器 | 说明 |
254
- |---|---|---|
255
- | 聊天、记忆、表情包、生图、命令学习、管理命令 | ✅ 可用 | `status` / `reset` / `set_role` / `presets` / `set_preset` / `reload_meme` 已改为 alconna 响应器,各适配器通用 |
256
- | 跨插件感知(捕获其它插件的输出) | ✅ 一般可用 | 依赖 NoneBot 基类的 `on_calling_api` 钩子(所有适配器);会话取自当前事件,消息取自适配器消息对象,取不到就静默跳过 |
257
- | 插件调用(`invoke_plugin`) | ⚠️ 尽力支持 | 通过复制该会话最近一条真实事件来投递命令;事件类型、消息类都由适配器自己决定,不需 onebot 专属类型。目标插件若依赖适配器特有能力(例如 OneBot 的 `get_msg`)仍可能失败 |
258
- | 群系统通知(戳一戳/禁言/进出群/撤回) | ❌ 仅 OneBot | 这些是 OneBot 专属事件类型,其它适配器没有统一的通知模型,不处理 |
259
- | 跨 bot 通信(子插件 `/peer/capture`) | ❌ 仅 OneBot | 其它 bot 都是同号 OneBot 实例,推送按 `onebot11:` 会话映射 |
260
- | 群名片记录 | ✅ 有则记录 | 成员信息来自 uninfo,适配器不提供名片时该项为空 |
261
-
262
- > 适配器完全不被 alconna/uninfo 支持时,该会话的消息会被静默跳过(只留 debug 日志),不会报错、也不会影响其它适配器。
251
+ ### 功能 × 适配器支持表(2026-09-25 实测)
252
+
253
+ 为每个适配器**构造真实事件**跑同一套功能路径(会话解析 → 渲染 → 回复发送 → 跨插件捕获 → 插件调用),bot 是记录型 mock,**未连真实平台**。
254
+
255
+ | 适配器 | 会话解析 | 收发/渲染 | 回复发送 | 私聊 | @ 按昵称 | 跨插件捕获 | 插件调用 | 群通知 | 跨 bot |
256
+ |---|---|---|---|---|---|---|---|---|---|
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
+ 图例:**✅ 已实测通过** | **⚠️ 已实现但未验证通过**(离线构造不出该适配器的事件,或需要真实 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 日志),不影响其它适配器
288
+
289
+ <details>
290
+ <summary>实测到的各适配器发送接口(供排查用)</summary>
291
+
292
+ | 适配器 | 回复发送最终调用的接口 |
293
+ |---|---|
294
+ | OneBot V11 / V12 | `send_msg` / `send_message` |
295
+ | Console | `send_message`(私聊 `send_private_message`) |
296
+ | Satori | `send_message`(私聊 `send_private_message`) |
297
+ | Telegram | `send_to` |
298
+ | Discord | `send_to` |
299
+ | QQ | `send_to_group` / `send_to_channel` / `send_to_c2c` |
300
+ | Feishu | `send_msg` |
301
+ | Milky | `send_group_message` / `send_private_message` |
302
+ | Mirai | `send_group_message` / `send_friend_message` |
303
+ | Kaiheila | `send_msg`(按 channel/private 区分) |
304
+ | DoDo | `send_to_channel` / `send_to_personal` |
305
+ | Kritor | `send_message` / `send_channel_message` |
306
+ | Mail | `send_to` |
307
+ | Minecraft | `send_msg` |
308
+ | WXMP | `send_custom_message` |
309
+ | EFChat | `send_chat_message` |
310
+
311
+ </details>
263
312
 
264
313
  ## 🔌 跨插件感知
265
314
 
@@ -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.0b0)
61
+ ### 安装 beta 版本(2.0.0b2)
62
62
 
63
- `2.0.0b0` 是**预发布(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.0b0
71
+ pip install nonebot-plugin-aigf-master==2.0.0b2
72
72
  ```
73
73
 
74
74
  在 `pyproject.toml` 中添加:
@@ -226,18 +226,67 @@ AIGFM_PEER_CAPTURE_PLUGINS=[] # 要捕获输出的插件名列表,为
226
226
  - 落盘目录名不能含 `:`,因此磁盘上写作 `onebot11_617770183`;`memory/` 下旧版裸数字目录会在启动时**一次性自动迁移**(目标已存在则跳过,幂等)
227
227
  - 非启用会话不发请求、不入缓冲:插件只会为启用的会话解析成员信息与图片
228
228
 
229
- ### 各能力在非 OneBot 适配器上的边界
230
-
231
- | 能力 | 其它适配器 | 说明 |
232
- |---|---|---|
233
- | 聊天、记忆、表情包、生图、命令学习、管理命令 | ✅ 可用 | `status` / `reset` / `set_role` / `presets` / `set_preset` / `reload_meme` 已改为 alconna 响应器,各适配器通用 |
234
- | 跨插件感知(捕获其它插件的输出) | ✅ 一般可用 | 依赖 NoneBot 基类的 `on_calling_api` 钩子(所有适配器);会话取自当前事件,消息取自适配器消息对象,取不到就静默跳过 |
235
- | 插件调用(`invoke_plugin`) | ⚠️ 尽力支持 | 通过复制该会话最近一条真实事件来投递命令;事件类型、消息类都由适配器自己决定,不需 onebot 专属类型。目标插件若依赖适配器特有能力(例如 OneBot 的 `get_msg`)仍可能失败 |
236
- | 群系统通知(戳一戳/禁言/进出群/撤回) | ❌ 仅 OneBot | 这些是 OneBot 专属事件类型,其它适配器没有统一的通知模型,不处理 |
237
- | 跨 bot 通信(子插件 `/peer/capture`) | ❌ 仅 OneBot | 其它 bot 都是同号 OneBot 实例,推送按 `onebot11:` 会话映射 |
238
- | 群名片记录 | ✅ 有则记录 | 成员信息来自 uninfo,适配器不提供名片时该项为空 |
239
-
240
- > 适配器完全不被 alconna/uninfo 支持时,该会话的消息会被静默跳过(只留 debug 日志),不会报错、也不会影响其它适配器。
229
+ ### 功能 × 适配器支持表(2026-09-25 实测)
230
+
231
+ 为每个适配器**构造真实事件**跑同一套功能路径(会话解析 → 渲染 → 回复发送 → 跨插件捕获 → 插件调用),bot 是记录型 mock,**未连真实平台**。
232
+
233
+ | 适配器 | 会话解析 | 收发/渲染 | 回复发送 | 私聊 | @ 按昵称 | 跨插件捕获 | 插件调用 | 群通知 | 跨 bot |
234
+ |---|---|---|---|---|---|---|---|---|---|
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
+ 图例:**✅ 已实测通过** | **⚠️ 已实现但未验证通过**(离线构造不出该适配器的事件,或需要真实 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 日志),不影响其它适配器
266
+
267
+ <details>
268
+ <summary>实测到的各适配器发送接口(供排查用)</summary>
269
+
270
+ | 适配器 | 回复发送最终调用的接口 |
271
+ |---|---|
272
+ | OneBot V11 / V12 | `send_msg` / `send_message` |
273
+ | Console | `send_message`(私聊 `send_private_message`) |
274
+ | Satori | `send_message`(私聊 `send_private_message`) |
275
+ | Telegram | `send_to` |
276
+ | Discord | `send_to` |
277
+ | QQ | `send_to_group` / `send_to_channel` / `send_to_c2c` |
278
+ | Feishu | `send_msg` |
279
+ | Milky | `send_group_message` / `send_private_message` |
280
+ | Mirai | `send_group_message` / `send_friend_message` |
281
+ | Kaiheila | `send_msg`(按 channel/private 区分) |
282
+ | DoDo | `send_to_channel` / `send_to_personal` |
283
+ | Kritor | `send_message` / `send_channel_message` |
284
+ | Mail | `send_to` |
285
+ | Minecraft | `send_msg` |
286
+ | WXMP | `send_custom_message` |
287
+ | EFChat | `send_chat_message` |
288
+
289
+ </details>
241
290
 
242
291
  ## 🔌 跨插件感知
243
292
 
@@ -15,12 +15,8 @@ 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
- from nonebot.plugin import PluginMetadata
19
+ from nonebot.plugin import PluginMetadata, inherit_supported_adapters
24
20
 
25
21
  # require 必须先于任何 import(否则库会以「先被 import」的方式装载,require 会判定它不是插件)
26
22
  require("nonebot_plugin_localstore")
@@ -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
@@ -58,8 +55,12 @@ __plugin_meta__ = PluginMetadata(
58
55
  name="AI群友(增强版)", description="群聊特化LLM聊天机器人(增强版),具备表情包管理、记忆存储、联网搜索、跨插件感知、插件调用、命令学习等能力。",
59
56
  usage="群聊特化LLM聊天机器人", type="application",
60
57
  config=PluginConfig,
61
- # 通用适配器支持:收发走 alconna、会话/成员信息走 uninfo,两者不支持的适配器会静默跳过
62
- supported_adapters=None,
58
+ # 收发走 alconna 的 uniseg、会话/成员信息走 uninfo,因此只声明两者共同支持的适配器
59
+ # (两者都不支持的适配器上本插件无法工作;inherit_supported_adapters 取交集,需在上面的 require 之后调用)
60
+ supported_adapters=inherit_supported_adapters(
61
+ "nonebot_plugin_alconna",
62
+ "nonebot_plugin_uninfo",
63
+ ),
63
64
  homepage="https://github.com/Funny1Potato/nonebot-plugin-aigf-master",
64
65
  extra={"author": "Funny1Potato"},
65
66
  )
@@ -430,60 +431,6 @@ def _is_message(event: Event) -> bool:
430
431
  return True
431
432
 
432
433
 
433
- _NOTICE_TYPES = (
434
- PokeNotifyEvent, GroupBanNoticeEvent, GroupIncreaseNoticeEvent,
435
- GroupDecreaseNoticeEvent, GroupRecallNoticeEvent,
436
- )
437
-
438
-
439
- def _is_group_notice(event: Event) -> bool:
440
- """只收群内系统通知;PokeNotifyEvent 的 group_id 可能为空(私聊戳一戳),过滤掉
441
-
442
- 群 notice 是 onebot 专属事件类型,其它适配器暂不处理(没有统一的通用通知模型)。
443
- """
444
- if not isinstance(event, _NOTICE_TYPES):
445
- return False
446
- return bool(getattr(event, "group_id", None))
447
-
448
-
449
- async def _resolve_notice_text(bot: Bot, event: Event) -> str | None:
450
- """把群 notice 事件渲染成一行文本;昵称查询失败回落用户 id"""
451
- group_id = int(getattr(event, "group_id"))
452
- info = SessionInfo(
453
- key=session_key("onebot11", str(group_id)), slug="onebot11", scene_path=str(group_id),
454
- native_chat_id=str(group_id), adapter_name=bot.adapter.get_name(),
455
- )
456
-
457
- async def _name(user_id) -> str:
458
- from .session import display_user
459
- name = await display_user(bot, info, user_id)
460
- return name or str(user_id)
461
-
462
- if isinstance(event, PokeNotifyEvent):
463
- return f"{await _name(event.user_id)} 戳了戳 {await _name(event.target_id)}"
464
- if isinstance(event, GroupBanNoticeEvent):
465
- if event.sub_type == "self_ban":
466
- return f"我被禁言 {event.duration} 分钟"
467
- if event.sub_type == "self_lift_ban":
468
- return "我被解除禁言"
469
- if event.sub_type == "lift_ban":
470
- return f"{await _name(event.operator_id)} 解除了 {await _name(event.user_id)} 的禁言"
471
- return f"{await _name(event.operator_id)} 将 {await _name(event.user_id)} 禁言 {event.duration} 分钟"
472
- if isinstance(event, GroupIncreaseNoticeEvent):
473
- return f"{await _name(event.user_id)} 加入了群聊"
474
- if isinstance(event, GroupDecreaseNoticeEvent):
475
- if event.sub_type == "kick_me":
476
- return "我被移出了群聊"
477
- if event.sub_type == "kick":
478
- return f"{await _name(event.user_id)} 被 {await _name(event.operator_id)} 移出了群聊"
479
- return f"{await _name(event.user_id)} 退出了群聊"
480
- if isinstance(event, GroupRecallNoticeEvent):
481
- if event.user_id == event.operator_id:
482
- return f"{await _name(event.user_id)} 撤回了一条消息"
483
- return f"{await _name(event.operator_id)} 撤回了 {await _name(event.user_id)} 的一条消息"
484
- return None
485
-
486
-
487
434
  async def _command_session(bot: Bot, event: Event) -> SessionInfo | None:
488
435
  """管理命令用:解析当前会话(解析不出时给用户一句可读的提示)"""
489
436
  return await resolve_session(bot, event)
@@ -521,7 +468,9 @@ set_preset_cmd = on_alconna(_cmd("set_preset", Args["rest", MultiVar(str, "*")])
521
468
  reload_meme_cmd = on_alconna(_cmd("reload_meme", Args["rest", MultiVar(str, "*")]),
522
469
  aliases={"重载表情包"}, permission=SUPERUSER, priority=0, block=True)
523
470
  auto_chat = on_message(rule=_is_message, priority=1, block=False)
524
- 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)
525
474
 
526
475
 
527
476
  @status_cmd.handle()
@@ -683,23 +632,23 @@ async def handle_auto_chat(bot: Bot, event: Event):
683
632
 
684
633
  @group_notice.handle()
685
634
  async def handle_group_notice(bot: Bot, event: Event):
686
- """群系统通知(戳一戳/禁言/进出群/消息撤回)→ 渲染后入缓冲区
635
+ """群/频道系统通知(戳一戳/禁言/进出群/消息撤回等)→ 渲染后入缓冲区
687
636
 
688
- notice 事件没有用户消息上下文:不更新 processor._current_user_id(避免污染工具调用身份),
637
+ 通知事件没有用户消息上下文:不更新 processor._current_user_id(避免污染工具调用身份),
689
638
  批处理触发依赖该会话最近一次真实用户消息(与 peer 推送的等待行为一致)
690
639
  """
691
- key = session_key("onebot11", str(getattr(event, "group_id")))
692
- 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):
693
642
  return
694
643
  try:
695
- text = await _resolve_notice_text(bot, event)
644
+ text = await notice_text(bot, info, event)
696
645
  except Exception as e:
697
646
  logger.error(f"[通知] 事件渲染失败: {e}")
698
647
  return
699
648
  if not text:
700
649
  return
701
- logger.info(f"[通知] {key}: {text}")
702
- _add_notice_message(key, text)
650
+ logger.info(f"[通知] {info.key}: {text}")
651
+ _add_notice_message(info.key, text)
703
652
 
704
653
 
705
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
@@ -6,10 +6,13 @@
6
6
  """
7
7
 
8
8
  import asyncio
9
+ from copy import deepcopy
9
10
  from typing import Literal
11
+ from collections.abc import MutableMapping
10
12
 
11
13
  from nonebot import get_driver, logger
12
14
  from nonebot.message import handle_event
15
+ from nonebot_plugin_alconna import UniMessage
13
16
  from nonebot_plugin_uninfo import get_interface
14
17
 
15
18
  from .session import SessionInfo, display_name
@@ -100,6 +103,129 @@ async def _sender_update(bot, session: SessionInfo | None, user_id) -> dict:
100
103
  return update
101
104
 
102
105
 
106
+ def _can_hold_message(template, message, expected_text: str) -> bool:
107
+ """事件的 `message` 字段能否直接放本适配器的 Message
108
+
109
+ 多数适配器(onebot/telegram/discord…)可以;但 **Satori** 这类事件的 `message` 是 `{id, content}`
110
+ 结构体,真正的消息由 `get_message()` 解析出来——硬塞 Message 进去会把事件弄坏
111
+ (`get_message()` 报错、`get_message_id()` 取不到 id),目标插件也就读不到命令。
112
+
113
+ 做法是**实测**:塞进副本后能取到非空 `get_message()` 就认(拿不准时保留原行为、由文本替换兜底)。
114
+ """
115
+ try:
116
+ probe = template.model_copy(update={"message": message})
117
+ text = str(probe.get_message())
118
+ except Exception as e:
119
+ logger.debug(f"[Invoker] message 字段不接受统一消息,改用文本替换: {e}")
120
+ return False
121
+ if not text.strip():
122
+ return False
123
+ # 能读到新内容才认为放得下:纯文本比对(at/图片段在 extract 里看不到)或整串比对
124
+ return expected_text.strip() in text or str(message) in text
125
+
126
+
127
+ def _plain_incoming(bot, event) -> str:
128
+ """取事件里原来的纯文本(用于「把旧文本替换成命令文本」)
129
+
130
+ 先用 alconna 的通用层解析(各适配器都可靠),拿不到再退回适配器自己的 `extract_plain_text()`
131
+ —— 部分适配器的 `extract_plain_text()` 会返回空串(实测 Satori),直接用它会让替换失效。
132
+ """
133
+ try:
134
+ unimsg = UniMessage.of(event.get_message(), bot=bot)
135
+ text = unimsg.extract_plain_text().strip()
136
+ if not text:
137
+ # 个别适配器(Satori)的 extract_plain_text 取不到文本,直接拼 Text 段
138
+ text = "".join(str(getattr(seg, "text", "") or "") for seg in unimsg).strip()
139
+ if text:
140
+ return text
141
+ except Exception as e:
142
+ logger.debug(f"[Invoker] 通用层取原文失败: {e}")
143
+ try:
144
+ return event.get_message().extract_plain_text().strip()
145
+ except Exception:
146
+ return ""
147
+
148
+
149
+ def _is_patchable(value) -> bool:
150
+ """能继续往里找文本字段的容器:映射 / 列表 / pydantic 模型"""
151
+ return (isinstance(value, (MutableMapping, list))
152
+ or bool(getattr(type(value), "model_fields", None)))
153
+
154
+
155
+ def _patch_text_fields(event, old_text: str, new_text: str) -> None:
156
+ """把事件里出现的旧消息文本替换成新文本(递归进嵌套模型/字典/列表,先复制再改以免污染原事件)
157
+
158
+ 消息文本不总在统一的 `message` 字段里:discord 用 `content`、dodo 用 `message_body`、
159
+ feishu 在嵌套的 `event.event.message.content`、**Satori 的 `message` 是 `{id, content}` 结构体**。
160
+ 只替换 `message` 的话,目标插件 `get_message()` 读到的仍是原来那条用户消息,命令永远匹配不上。
161
+ 这里采用「旧文本出现在哪个字符串槽位就替换哪个」的通用兜底(不做适配器特判)。
162
+ """
163
+ if not old_text:
164
+ return
165
+
166
+ def visit(parent, name, value, depth: int, setter) -> None:
167
+ if isinstance(value, str):
168
+ if old_text in value:
169
+ try:
170
+ setter(name, value.replace(old_text, new_text))
171
+ except Exception as e:
172
+ logger.debug(f"[Invoker] 替换文本字段 {name} 失败: {e}")
173
+ return
174
+ if not _is_patchable(value):
175
+ return
176
+ if getattr(type(parent), "model_fields", None):
177
+ # 从模型往下走时复制一份,避免改到用于下次调用的模板事件
178
+ try:
179
+ copied = deepcopy(value)
180
+ setter(name, copied)
181
+ value = copied
182
+ except Exception:
183
+ pass
184
+ walk(value, depth + 1)
185
+
186
+ def walk(obj, depth: int = 0) -> None:
187
+ if depth > 4:
188
+ return
189
+ fields = list(getattr(type(obj), "model_fields", None) or [])
190
+ if fields:
191
+ for name in fields:
192
+ try:
193
+ value = getattr(obj, name)
194
+ except Exception:
195
+ continue
196
+ visit(obj, name, value, depth, lambda n, v, o=obj: setattr(o, n, v))
197
+ elif isinstance(obj, MutableMapping):
198
+ for name in list(obj.keys()):
199
+ visit(obj, name, obj.get(name), depth,
200
+ lambda n, v: obj.__setitem__(n, v))
201
+ elif isinstance(obj, list):
202
+ for item in obj:
203
+ if _is_patchable(item):
204
+ walk(item, depth + 1)
205
+
206
+ walk(event)
207
+
208
+
209
+ def _reset_message_cache(event) -> None:
210
+ """清掉适配器「懒加载的消息缓存」私有属性(如 Discord 的 `_message` / `_original_message`)
211
+
212
+ Discord 的 `get_message()` 是 `if not hasattr(self, "_message"): self._message = ...` —— 结果会
213
+ 缓存在私有属性里,而 `model_copy` 会把这份缓存一起带过来。不清理的话,即使我们换掉了消息体,
214
+ 目标插件调用 `get_message()` 读到的仍是**原来那条消息**,命令永远匹配不上。
215
+ 只动「名字里带 message 的私有属性」,不碰任何协议字段。
216
+ """
217
+ for holder in (getattr(event, "__dict__", None), getattr(event, "__pydantic_private__", None)):
218
+ if not isinstance(holder, dict):
219
+ continue
220
+ stale = [k for k in holder
221
+ if k.startswith("_") and not k.startswith("__") and "message" in k.lower()]
222
+ for key in stale:
223
+ try:
224
+ holder.pop(key, None)
225
+ except Exception as e:
226
+ logger.debug(f"[Invoker] 清理消息缓存 {key} 失败: {e}")
227
+
228
+
103
229
  class PluginInvoker:
104
230
  """调用本实例的其它插件
105
231
 
@@ -165,13 +291,16 @@ class PluginInvoker:
165
291
  unimsg = messages[0] if messages else None
166
292
  message = await unimsg.export(bot=bot) if unimsg is not None else bot.adapter.get_message_class()()
167
293
 
168
- update: dict = {"message": message}
294
+ update: dict = {}
295
+ command_text = plain_text(unimsg) if unimsg is not None else command
296
+ if _can_hold_message(template, message, command_text):
297
+ update["message"] = message
298
+ # 部分适配器(onebot 等)另有 original_message(@ 机器人时保留的原文),一并换掉,
299
+ # 否则 alconna 的 use_origin 路径仍会读到原来的用户消息
300
+ if hasattr(template, "original_message"):
301
+ update["original_message"] = message
169
302
  if hasattr(template, "raw_message"):
170
- update["raw_message"] = plain_text(unimsg) if unimsg is not None else command
171
- # 部分适配器(onebot 等)另有 original_message(@ 机器人时保留的原文),一并换掉,
172
- # 否则 alconna 的 use_origin 路径仍会读到原来的用户消息
173
- if hasattr(template, "original_message"):
174
- update["original_message"] = message
303
+ update["raw_message"] = command_text
175
304
  # 合成消息没有引用关系:清掉被复制消息的 reply,避免目标插件以为它在回复某条消息
176
305
  if hasattr(template, "reply"):
177
306
  update["reply"] = None
@@ -193,6 +322,10 @@ class PluginInvoker:
193
322
  setattr(event, SYNTHETIC_FLAG, True)
194
323
  except Exception:
195
324
  pass
325
+ # 消息文本不总在 `message` 字段里(见 _patch_text_fields),补齐后才对得上目标插件
326
+ old_text = _plain_incoming(bot, template)
327
+ _patch_text_fields(event, old_text, plain_text(unimsg) if unimsg is not None else command)
328
+ _reset_message_cache(event)
196
329
  _refresh_uniseg_cache(event, bot)
197
330
  logger.debug(f"[Invoker] 创建 synthetic event: command={command}, session="
198
331
  f"{getattr(session, 'key', '')}, user={user_id}, parts={parts}")
@@ -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
 
@@ -255,15 +255,33 @@ async def build_unimsg(bot: Bot, event: Event) -> UniMessage | None:
255
255
 
256
256
 
257
257
  def _sender_label(sender) -> str:
258
- """从 sender(对象或 dict)里取「昵称(用户id)」标签"""
258
+ """从 sender(对象或 dict)里取「昵称(用户id)」标签
259
+
260
+ 只接受**字符串**取值:部分适配器(如 Kaiheila)原始回复段上的 sender 是对象/方法甚至占位值,
261
+ 直接 `value.strip()` 会抛 TypeError 把整条消息的渲染打挂(实测踩过)。
262
+ """
259
263
  if sender is None:
260
264
  return ""
261
- if isinstance(sender, dict):
262
- nick = (sender.get("nickname") or sender.get("name") or "").strip()
263
- uid = str(sender.get("user_id") or "")
264
- else:
265
- nick = (getattr(sender, "nickname", None) or getattr(sender, "name", None) or "").strip()
266
- uid = str(getattr(sender, "user_id", "") or "")
265
+
266
+ def pick(obj, name: str) -> str:
267
+ try:
268
+ value = obj.get(name) if isinstance(obj, dict) else getattr(obj, name, None)
269
+ except Exception:
270
+ return ""
271
+ return value.strip() if isinstance(value, str) else ""
272
+
273
+ def pick_id(obj, name: str) -> str:
274
+ """用户 id 可能是 int(onebot)也可能是 str(多数平台),只拒掉非标量"""
275
+ try:
276
+ value = obj.get(name) if isinstance(obj, dict) else getattr(obj, name, None)
277
+ except Exception:
278
+ return ""
279
+ if isinstance(value, bool) or not isinstance(value, (str, int)):
280
+ return ""
281
+ return str(value).strip()
282
+
283
+ nick = pick(sender, "nickname") or pick(sender, "name")
284
+ uid = pick_id(sender, "user_id")
267
285
  if uid and nick:
268
286
  return f"{nick}({uid})"
269
287
  return nick or uid
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nonebot-plugin-aigf-master
3
- Version: 2.0.0b0
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.0b0)
83
+ ### 安装 beta 版本(2.0.0b2)
84
84
 
85
- `2.0.0b0` 是**预发布(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.0b0
93
+ pip install nonebot-plugin-aigf-master==2.0.0b2
94
94
  ```
95
95
 
96
96
  在 `pyproject.toml` 中添加:
@@ -248,18 +248,67 @@ AIGFM_PEER_CAPTURE_PLUGINS=[] # 要捕获输出的插件名列表,为
248
248
  - 落盘目录名不能含 `:`,因此磁盘上写作 `onebot11_617770183`;`memory/` 下旧版裸数字目录会在启动时**一次性自动迁移**(目标已存在则跳过,幂等)
249
249
  - 非启用会话不发请求、不入缓冲:插件只会为启用的会话解析成员信息与图片
250
250
 
251
- ### 各能力在非 OneBot 适配器上的边界
252
-
253
- | 能力 | 其它适配器 | 说明 |
254
- |---|---|---|
255
- | 聊天、记忆、表情包、生图、命令学习、管理命令 | ✅ 可用 | `status` / `reset` / `set_role` / `presets` / `set_preset` / `reload_meme` 已改为 alconna 响应器,各适配器通用 |
256
- | 跨插件感知(捕获其它插件的输出) | ✅ 一般可用 | 依赖 NoneBot 基类的 `on_calling_api` 钩子(所有适配器);会话取自当前事件,消息取自适配器消息对象,取不到就静默跳过 |
257
- | 插件调用(`invoke_plugin`) | ⚠️ 尽力支持 | 通过复制该会话最近一条真实事件来投递命令;事件类型、消息类都由适配器自己决定,不需 onebot 专属类型。目标插件若依赖适配器特有能力(例如 OneBot 的 `get_msg`)仍可能失败 |
258
- | 群系统通知(戳一戳/禁言/进出群/撤回) | ❌ 仅 OneBot | 这些是 OneBot 专属事件类型,其它适配器没有统一的通知模型,不处理 |
259
- | 跨 bot 通信(子插件 `/peer/capture`) | ❌ 仅 OneBot | 其它 bot 都是同号 OneBot 实例,推送按 `onebot11:` 会话映射 |
260
- | 群名片记录 | ✅ 有则记录 | 成员信息来自 uninfo,适配器不提供名片时该项为空 |
261
-
262
- > 适配器完全不被 alconna/uninfo 支持时,该会话的消息会被静默跳过(只留 debug 日志),不会报错、也不会影响其它适配器。
251
+ ### 功能 × 适配器支持表(2026-09-25 实测)
252
+
253
+ 为每个适配器**构造真实事件**跑同一套功能路径(会话解析 → 渲染 → 回复发送 → 跨插件捕获 → 插件调用),bot 是记录型 mock,**未连真实平台**。
254
+
255
+ | 适配器 | 会话解析 | 收发/渲染 | 回复发送 | 私聊 | @ 按昵称 | 跨插件捕获 | 插件调用 | 群通知 | 跨 bot |
256
+ |---|---|---|---|---|---|---|---|---|---|
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
+ 图例:**✅ 已实测通过** | **⚠️ 已实现但未验证通过**(离线构造不出该适配器的事件,或需要真实 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 日志),不影响其它适配器
288
+
289
+ <details>
290
+ <summary>实测到的各适配器发送接口(供排查用)</summary>
291
+
292
+ | 适配器 | 回复发送最终调用的接口 |
293
+ |---|---|
294
+ | OneBot V11 / V12 | `send_msg` / `send_message` |
295
+ | Console | `send_message`(私聊 `send_private_message`) |
296
+ | Satori | `send_message`(私聊 `send_private_message`) |
297
+ | Telegram | `send_to` |
298
+ | Discord | `send_to` |
299
+ | QQ | `send_to_group` / `send_to_channel` / `send_to_c2c` |
300
+ | Feishu | `send_msg` |
301
+ | Milky | `send_group_message` / `send_private_message` |
302
+ | Mirai | `send_group_message` / `send_friend_message` |
303
+ | Kaiheila | `send_msg`(按 channel/private 区分) |
304
+ | DoDo | `send_to_channel` / `send_to_personal` |
305
+ | Kritor | `send_message` / `send_channel_message` |
306
+ | Mail | `send_to` |
307
+ | Minecraft | `send_msg` |
308
+ | WXMP | `send_custom_message` |
309
+ | EFChat | `send_chat_message` |
310
+
311
+ </details>
263
312
 
264
313
  ## 🔌 跨插件感知
265
314
 
@@ -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.0b0"
7
+ version = "2.0.0b2"
8
8
  description = "群聊特化LLM聊天机器人(增强版),具有记忆、表情包、跨插件调用、跨bot通信与跨适配器支持"
9
9
  readme = "README.md"
10
10
  license = "MIT"