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