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.
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/PKG-INFO +66 -17
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/README.md +65 -16
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/__init__.py +18 -69
- nonebot_plugin_aigf_master-2.0.0b2/nonebot_plugin_aigf_master/notice.py +233 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/plugin_invoker.py +139 -6
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/session.py +10 -2
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/unimsg.py +25 -7
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master.egg-info/PKG-INFO +66 -17
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master.egg-info/SOURCES.txt +1 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/pyproject.toml +1 -1
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/LICENSE +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/anime_recognizer.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/animetrace_client.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/api_hooks.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/cache_cleanup.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/command_learner.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/config.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/context_bus.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/image_gen_client.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/image_handler.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/llm_client.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/meme_store.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/memory_store.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/models.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/peer_client.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/plugin_discovery.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/preset_store.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/processor.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/prompt_builder.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/response_parser.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/search_client.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master/vlm_client.py +0 -0
- {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
- {nonebot_plugin_aigf_master-2.0.0b0 → nonebot_plugin_aigf_master-2.0.0b2}/nonebot_plugin_aigf_master.egg-info/requires.txt +0 -0
- {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
- {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.
|
|
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
|
-
- 🔔
|
|
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.
|
|
83
|
+
### 安装 beta 版本(2.0.0b2)
|
|
84
84
|
|
|
85
|
-
`2.0.
|
|
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.
|
|
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
|
-
###
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
|
256
|
-
|
|
257
|
-
|
|
|
258
|
-
|
|
|
259
|
-
|
|
|
260
|
-
|
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
- 🔔
|
|
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.
|
|
61
|
+
### 安装 beta 版本(2.0.0b2)
|
|
62
62
|
|
|
63
|
-
`2.0.
|
|
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.
|
|
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
|
-
###
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
|
234
|
-
|
|
235
|
-
|
|
|
236
|
-
|
|
|
237
|
-
|
|
|
238
|
-
|
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
#
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
637
|
+
通知事件没有用户消息上下文:不更新 processor._current_user_id(避免污染工具调用身份),
|
|
689
638
|
批处理触发依赖该会话最近一次真实用户消息(与 peer 推送的等待行为一致)
|
|
690
639
|
"""
|
|
691
|
-
|
|
692
|
-
if
|
|
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
|
|
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 = {
|
|
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"] =
|
|
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=
|
|
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=
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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.
|
|
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
|
-
- 🔔
|
|
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.
|
|
83
|
+
### 安装 beta 版本(2.0.0b2)
|
|
84
84
|
|
|
85
|
-
`2.0.
|
|
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.
|
|
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
|
-
###
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
|
256
|
-
|
|
257
|
-
|
|
|
258
|
-
|
|
|
259
|
-
|
|
|
260
|
-
|
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|