nonebot-plugin-aigf-master 2.0.0b2__tar.gz → 2.0.0b3__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.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/PKG-INFO +14 -9
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/README.md +13 -8
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/__init__.py +2 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/memory_store.py +48 -1
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/processor.py +34 -3
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master.egg-info/PKG-INFO +14 -9
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/pyproject.toml +1 -1
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/LICENSE +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/anime_recognizer.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/animetrace_client.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/api_hooks.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/cache_cleanup.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/command_learner.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/config.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/context_bus.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/image_gen_client.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/image_handler.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/llm_client.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/meme_store.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/models.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/notice.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/peer_client.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/plugin_discovery.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/plugin_invoker.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/preset_store.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/prompt_builder.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/response_parser.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/search_client.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/session.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/unimsg.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/vlm_client.py +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master.egg-info/SOURCES.txt +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master.egg-info/dependency_links.txt +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master.egg-info/requires.txt +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master.egg-info/top_level.txt +0 -0
- {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/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.0b3
|
|
4
4
|
Summary: 群聊特化LLM聊天机器人(增强版),具有记忆、表情包、跨插件调用、跨bot通信与跨适配器支持
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: <4.0,>=3.10
|
|
@@ -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.0b3)
|
|
84
84
|
|
|
85
|
-
`2.0.
|
|
85
|
+
`2.0.0b3` 是**预发布(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.0b3
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
在 `pyproject.toml` 中添加:
|
|
@@ -115,8 +115,8 @@ plugins = ["nonebot-plugin-aigf-master"]
|
|
|
115
115
|
AIGFM_LLM_API_KEY="sk-xxxxxxxxxxxx" # LLM API Key
|
|
116
116
|
AIGFM_LLM_BASE_URL="https://api.deepseek.com" # LLM API 地址
|
|
117
117
|
AIGFM_LLM_MODEL="deepseek-v4-flash" # LLM 模型名称
|
|
118
|
-
AIGFM_ENABLED_GROUPS=[
|
|
119
|
-
AIGFM_ENABLED_PRIVATE=[]
|
|
118
|
+
AIGFM_ENABLED_GROUPS=["onebot11:123456789", "console:console-chat"] # 启用的群/频道会话(`适配器:会话id`;也可只写数字,按 OneBot V11 识别)
|
|
119
|
+
AIGFM_ENABLED_PRIVATE=["onebot11:20001"] # 启用的私聊会话(同上写法;留空则私聊不响应)
|
|
120
120
|
|
|
121
121
|
# --- 图片理解(VLM) ---
|
|
122
122
|
AIGFM_IMAGE_MODE="vlm" # 图片模式: vlm / llm(默认 vlm)
|
|
@@ -234,7 +234,7 @@ AIGFM_PEER_CAPTURE_PLUGINS=[] # 要捕获输出的插件名列表,为
|
|
|
234
234
|
- 纯 @ 消息或检测到同一用户连续发送时,以及调用的插件返回图片时,等待时间会用 `AIGFM_INCOMPLETE_TIMEOUT`(默认 40 秒)**替代**默认的 30 秒(非叠加)
|
|
235
235
|
- **有图片正在解析时整批推迟触发**:等到 VLM 描述填好后随**同一批**发给 LLM,不会把没有描述的空消息发出去;"图片进入解析"这一特例最长等待 `AIGFM_BATCH_TIMEOUT + AIGFM_INCOMPLETE_TIMEOUT`(默认 70 秒)后仍会放行,此时该图片以 `[图片解析中]` 进入上下文
|
|
236
236
|
- 消息合并窗口内的同一用户连续消息会被合并为一条
|
|
237
|
-
- 每次处理时,LLM 收到最近 **20 条**(可配置)历史聊天记录 + 四层记忆 + 预设 + 表情包列表 +
|
|
237
|
+
- 每次处理时,LLM 收到最近 **20 条**(可配置)历史聊天记录 + 四层记忆 + 预设 + 表情包列表 + 其它插件响应;本批消息只出现在「新消息」段,同一条插件响应也只注入一次(不会在历史与新消息里重复)。历史聊天记录会落盘(`memory/<群号>/recent.json`),**机器人重启后仍能读回**
|
|
238
238
|
- LLM 一次调用同时完成:回复决策 + 记忆管理 + 表情包选择 + 命令学习
|
|
239
239
|
|
|
240
240
|
## 🧩 多适配器支持
|
|
@@ -243,9 +243,9 @@ AIGFM_PEER_CAPTURE_PLUGINS=[] # 要捕获输出的插件名列表,为
|
|
|
243
243
|
|
|
244
244
|
### 会话与启用清单
|
|
245
245
|
|
|
246
|
-
- 会话以 **`适配器:会话id`** 为键,例如 `onebot11:
|
|
246
|
+
- 会话以 **`适配器:会话id`** 为键,例如 `onebot11:123456789`(群)、`console:console-chat`(频道)、`onebot11:20001`(私聊)
|
|
247
247
|
- `AIGFM_ENABLED_GROUPS` 管群/频道会话、`AIGFM_ENABLED_PRIVATE` 管私聊会话;条目可写带前缀的完整键,也可以只写 id(**按 OneBot V11 识别**,旧配置无需改动)
|
|
248
|
-
- 落盘目录名不能含 `:`,因此磁盘上写作 `
|
|
248
|
+
- 落盘目录名不能含 `:`,因此磁盘上写作 `onebot11_123456789`;`memory/` 下旧版裸数字目录会在启动时**一次性自动迁移**(目标已存在则跳过,幂等)
|
|
249
249
|
- 非启用会话不发请求、不入缓冲:插件只会为启用的会话解析成员信息与图片
|
|
250
250
|
|
|
251
251
|
### 功能 × 适配器支持表(2026-09-25 实测)
|
|
@@ -590,6 +590,10 @@ LLM 可以:
|
|
|
590
590
|
|
|
591
591
|
存储在 `<插件数据目录>/memory/<群号>/culture.json`,记录梗、网络用语、流行语。LLM 主动学习和存储,根据聊天内容自动匹配。
|
|
592
592
|
|
|
593
|
+
### 最近聊天记录
|
|
594
|
+
|
|
595
|
+
上面四层记忆之外的**滚动窗口**:最近 50 条消息(含机器人自己的回复与插件响应)会落盘在 `<插件数据目录>/memory/<群号>/recent.json`,**机器人重启后会读回**,因此刚重启的会话也能接上之前的上下文(注入 prompt 的条数由 `AIGFM_RECENT_MESSAGES` 控制,默认 20)。`/reset` 会连这份记录一起清空。
|
|
596
|
+
|
|
593
597
|
## 🖼️ 表情包功能
|
|
594
598
|
|
|
595
599
|
### 工作原理
|
|
@@ -766,6 +770,7 @@ LLM 支持以下回复类型:
|
|
|
766
770
|
short_term.json # 短期记忆
|
|
767
771
|
long_term.json # 长期记忆
|
|
768
772
|
culture.json # 文化记忆
|
|
773
|
+
recent.json # 最近的聊天记录(重启后仍可读)
|
|
769
774
|
friends/
|
|
770
775
|
{user_id}.json # 群友信息
|
|
771
776
|
memes/
|
|
@@ -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.0b3)
|
|
62
62
|
|
|
63
|
-
`2.0.
|
|
63
|
+
`2.0.0b3` 是**预发布(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.0b3
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
在 `pyproject.toml` 中添加:
|
|
@@ -93,8 +93,8 @@ plugins = ["nonebot-plugin-aigf-master"]
|
|
|
93
93
|
AIGFM_LLM_API_KEY="sk-xxxxxxxxxxxx" # LLM API Key
|
|
94
94
|
AIGFM_LLM_BASE_URL="https://api.deepseek.com" # LLM API 地址
|
|
95
95
|
AIGFM_LLM_MODEL="deepseek-v4-flash" # LLM 模型名称
|
|
96
|
-
AIGFM_ENABLED_GROUPS=[
|
|
97
|
-
AIGFM_ENABLED_PRIVATE=[]
|
|
96
|
+
AIGFM_ENABLED_GROUPS=["onebot11:123456789", "console:console-chat"] # 启用的群/频道会话(`适配器:会话id`;也可只写数字,按 OneBot V11 识别)
|
|
97
|
+
AIGFM_ENABLED_PRIVATE=["onebot11:20001"] # 启用的私聊会话(同上写法;留空则私聊不响应)
|
|
98
98
|
|
|
99
99
|
# --- 图片理解(VLM) ---
|
|
100
100
|
AIGFM_IMAGE_MODE="vlm" # 图片模式: vlm / llm(默认 vlm)
|
|
@@ -212,7 +212,7 @@ AIGFM_PEER_CAPTURE_PLUGINS=[] # 要捕获输出的插件名列表,为
|
|
|
212
212
|
- 纯 @ 消息或检测到同一用户连续发送时,以及调用的插件返回图片时,等待时间会用 `AIGFM_INCOMPLETE_TIMEOUT`(默认 40 秒)**替代**默认的 30 秒(非叠加)
|
|
213
213
|
- **有图片正在解析时整批推迟触发**:等到 VLM 描述填好后随**同一批**发给 LLM,不会把没有描述的空消息发出去;"图片进入解析"这一特例最长等待 `AIGFM_BATCH_TIMEOUT + AIGFM_INCOMPLETE_TIMEOUT`(默认 70 秒)后仍会放行,此时该图片以 `[图片解析中]` 进入上下文
|
|
214
214
|
- 消息合并窗口内的同一用户连续消息会被合并为一条
|
|
215
|
-
- 每次处理时,LLM 收到最近 **20 条**(可配置)历史聊天记录 + 四层记忆 + 预设 + 表情包列表 +
|
|
215
|
+
- 每次处理时,LLM 收到最近 **20 条**(可配置)历史聊天记录 + 四层记忆 + 预设 + 表情包列表 + 其它插件响应;本批消息只出现在「新消息」段,同一条插件响应也只注入一次(不会在历史与新消息里重复)。历史聊天记录会落盘(`memory/<群号>/recent.json`),**机器人重启后仍能读回**
|
|
216
216
|
- LLM 一次调用同时完成:回复决策 + 记忆管理 + 表情包选择 + 命令学习
|
|
217
217
|
|
|
218
218
|
## 🧩 多适配器支持
|
|
@@ -221,9 +221,9 @@ AIGFM_PEER_CAPTURE_PLUGINS=[] # 要捕获输出的插件名列表,为
|
|
|
221
221
|
|
|
222
222
|
### 会话与启用清单
|
|
223
223
|
|
|
224
|
-
- 会话以 **`适配器:会话id`** 为键,例如 `onebot11:
|
|
224
|
+
- 会话以 **`适配器:会话id`** 为键,例如 `onebot11:123456789`(群)、`console:console-chat`(频道)、`onebot11:20001`(私聊)
|
|
225
225
|
- `AIGFM_ENABLED_GROUPS` 管群/频道会话、`AIGFM_ENABLED_PRIVATE` 管私聊会话;条目可写带前缀的完整键,也可以只写 id(**按 OneBot V11 识别**,旧配置无需改动)
|
|
226
|
-
- 落盘目录名不能含 `:`,因此磁盘上写作 `
|
|
226
|
+
- 落盘目录名不能含 `:`,因此磁盘上写作 `onebot11_123456789`;`memory/` 下旧版裸数字目录会在启动时**一次性自动迁移**(目标已存在则跳过,幂等)
|
|
227
227
|
- 非启用会话不发请求、不入缓冲:插件只会为启用的会话解析成员信息与图片
|
|
228
228
|
|
|
229
229
|
### 功能 × 适配器支持表(2026-09-25 实测)
|
|
@@ -568,6 +568,10 @@ LLM 可以:
|
|
|
568
568
|
|
|
569
569
|
存储在 `<插件数据目录>/memory/<群号>/culture.json`,记录梗、网络用语、流行语。LLM 主动学习和存储,根据聊天内容自动匹配。
|
|
570
570
|
|
|
571
|
+
### 最近聊天记录
|
|
572
|
+
|
|
573
|
+
上面四层记忆之外的**滚动窗口**:最近 50 条消息(含机器人自己的回复与插件响应)会落盘在 `<插件数据目录>/memory/<群号>/recent.json`,**机器人重启后会读回**,因此刚重启的会话也能接上之前的上下文(注入 prompt 的条数由 `AIGFM_RECENT_MESSAGES` 控制,默认 20)。`/reset` 会连这份记录一起清空。
|
|
574
|
+
|
|
571
575
|
## 🖼️ 表情包功能
|
|
572
576
|
|
|
573
577
|
### 工作原理
|
|
@@ -744,6 +748,7 @@ LLM 支持以下回复类型:
|
|
|
744
748
|
short_term.json # 短期记忆
|
|
745
749
|
long_term.json # 长期记忆
|
|
746
750
|
culture.json # 文化记忆
|
|
751
|
+
recent.json # 最近的聊天记录(重启后仍可读)
|
|
747
752
|
friends/
|
|
748
753
|
{user_id}.json # 群友信息
|
|
749
754
|
memes/
|
|
@@ -507,6 +507,8 @@ async def _(bot: Bot, event: Event):
|
|
|
507
507
|
processor.bot_name = "小助手"
|
|
508
508
|
processor.bot_role = "一个友好的群聊助手"
|
|
509
509
|
processor.recent_messages.clear()
|
|
510
|
+
# 落盘的聊天记录一起清,否则重置后重启又会把旧历史读回来
|
|
511
|
+
await processor.memory.clear_recent()
|
|
510
512
|
# 插件响应另有一份存在 ContextBus 里,不清会跨 reset 残留进后续 prompt
|
|
511
513
|
processor.context_bus.clear(session.key)
|
|
512
514
|
# 图片缓存索引随聊天记录一起清(历史 id 已不在 prompt 中,留着也无可收藏的上下文)
|
|
@@ -10,7 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
import anyio
|
|
11
11
|
from nonebot import logger
|
|
12
12
|
|
|
13
|
-
from .models import MemoryOps
|
|
13
|
+
from .models import ChatMessage, MemoryOps
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class MemoryStore:
|
|
@@ -59,6 +59,53 @@ class MemoryStore:
|
|
|
59
59
|
async with await anyio.open_file(path, "w", encoding="utf-8") as f:
|
|
60
60
|
await f.write(json.dumps({"facts": facts}, ensure_ascii=False, indent=2))
|
|
61
61
|
|
|
62
|
+
# ========== 最近的聊天记录(重启后仍可读) ==========
|
|
63
|
+
|
|
64
|
+
async def load_recent(self) -> list[ChatMessage]:
|
|
65
|
+
"""读回落盘的聊天记录;文件缺失/损坏时返回空列表"""
|
|
66
|
+
path = self._group_dir / "recent.json"
|
|
67
|
+
if not path.exists():
|
|
68
|
+
return []
|
|
69
|
+
try:
|
|
70
|
+
async with await anyio.open_file(path, encoding="utf-8") as f:
|
|
71
|
+
raw = json.loads(await f.read()).get("messages", [])
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"加载聊天记录失败: {e}")
|
|
74
|
+
return []
|
|
75
|
+
messages = []
|
|
76
|
+
for item in raw if isinstance(raw, list) else []:
|
|
77
|
+
try:
|
|
78
|
+
messages.append(ChatMessage(
|
|
79
|
+
time=datetime.fromisoformat(item["time"]),
|
|
80
|
+
user_name=item.get("user_name", ""),
|
|
81
|
+
content=item.get("content", ""),
|
|
82
|
+
user_id=str(item.get("user_id", "")),
|
|
83
|
+
is_at_only=bool(item.get("is_at_only", False)),
|
|
84
|
+
))
|
|
85
|
+
except Exception:
|
|
86
|
+
continue # 单条坏数据丢弃,不影响其余记录
|
|
87
|
+
return messages
|
|
88
|
+
|
|
89
|
+
async def save_recent(self, messages: list[ChatMessage]):
|
|
90
|
+
path = self._group_dir / "recent.json"
|
|
91
|
+
data = {"messages": [
|
|
92
|
+
{
|
|
93
|
+
"time": m.time.isoformat(), "user_name": m.user_name, "content": m.content,
|
|
94
|
+
"user_id": m.user_id, "is_at_only": m.is_at_only,
|
|
95
|
+
}
|
|
96
|
+
for m in messages
|
|
97
|
+
]}
|
|
98
|
+
async with await anyio.open_file(path, "w", encoding="utf-8") as f:
|
|
99
|
+
await f.write(json.dumps(data, ensure_ascii=False, indent=2))
|
|
100
|
+
|
|
101
|
+
async def clear_recent(self):
|
|
102
|
+
path = self._group_dir / "recent.json"
|
|
103
|
+
try:
|
|
104
|
+
if path.exists():
|
|
105
|
+
path.unlink()
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error(f"清除聊天记录失败: {e}")
|
|
108
|
+
|
|
62
109
|
# ========== 群友信息 ==========
|
|
63
110
|
|
|
64
111
|
async def load_friend(self, user_id: str) -> dict | None:
|
|
@@ -146,6 +146,9 @@ def _parts_key(parts: list[dict] | None) -> str:
|
|
|
146
146
|
# 调用台账注入 prompt 时最多展示的条数(比对的台账条数由 aigfm_invoke_dedup_records 控制)
|
|
147
147
|
_LEDGER_DISPLAY = 10
|
|
148
148
|
|
|
149
|
+
# 「最近的聊天记录」在内存与落盘文件中保留的条数上限(注入 prompt 的条数另由 aigfm_recent_messages 控制)
|
|
150
|
+
_RECENT_KEEP = 50
|
|
151
|
+
|
|
149
152
|
|
|
150
153
|
class MessageProcessor:
|
|
151
154
|
"""消息处理编排器,协调所有组件"""
|
|
@@ -186,6 +189,8 @@ class MessageProcessor:
|
|
|
186
189
|
self.bot_role = "一个友好的群聊助手"
|
|
187
190
|
self.current_preset = ""
|
|
188
191
|
self.recent_messages: list[ChatMessage] = []
|
|
192
|
+
# 「最近的聊天记录」在首次处理时从 memory/{会话}/recent.json 读回(重启后不丢历史)
|
|
193
|
+
self._recent_loaded = False
|
|
189
194
|
# 代为执行过的命令台账:既注入 prompt 让模型「看得见自己刚调过什么」,
|
|
190
195
|
# 也作为「最近调用过相同命令」去重的比对依据(见 _find_recent_same)
|
|
191
196
|
self._invoke_log: list[_Invocation] = []
|
|
@@ -325,10 +330,22 @@ class MessageProcessor:
|
|
|
325
330
|
for msg in messages:
|
|
326
331
|
logger.debug(f"[处理] 消息: [{msg.user_name}] {msg.content[:80]}")
|
|
327
332
|
|
|
328
|
-
#
|
|
333
|
+
# 更新最近消息(首次处理时先读回上次落盘的记录,这样重启后 LLM 仍能看到历史)
|
|
334
|
+
if not self._recent_loaded:
|
|
335
|
+
self._recent_loaded = True
|
|
336
|
+
try:
|
|
337
|
+
restored = await self.memory.load_recent()
|
|
338
|
+
except Exception as e:
|
|
339
|
+
logger.error(f"[记忆] 加载聊天记录失败: {e}")
|
|
340
|
+
restored = []
|
|
341
|
+
if restored:
|
|
342
|
+
self.recent_messages = restored + self.recent_messages
|
|
343
|
+
logger.info(f"[记忆] {self.session_key} 恢复聊天记录 {len(restored)} 条")
|
|
329
344
|
self.recent_messages.extend(messages)
|
|
330
|
-
if len(self.recent_messages) >
|
|
331
|
-
self.recent_messages = self.recent_messages[-
|
|
345
|
+
if len(self.recent_messages) > _RECENT_KEEP:
|
|
346
|
+
self.recent_messages = self.recent_messages[-_RECENT_KEEP:]
|
|
347
|
+
# 先落盘一次:后面任何提前 return(没回复/解析失败)都不影响历史的保存
|
|
348
|
+
await self._persist_recent()
|
|
332
349
|
|
|
333
350
|
# 加载记忆
|
|
334
351
|
short_term = await self.memory.load_short_term()
|
|
@@ -512,6 +529,7 @@ class MessageProcessor:
|
|
|
512
529
|
|
|
513
530
|
# 更新最近消息
|
|
514
531
|
self._record_bot_reply(reply_segments)
|
|
532
|
+
await self._persist_recent()
|
|
515
533
|
|
|
516
534
|
# 消耗社交能量
|
|
517
535
|
text_length = sum(len(s.content) for s in reply_segments if s.type == "text")
|
|
@@ -783,6 +801,19 @@ class MessageProcessor:
|
|
|
783
801
|
# 索引常驻后 LLM 会引用历史 id,被上限淘汰或不存在时在此暴露,避免静默失败无从排查
|
|
784
802
|
logger.warning(f"[表情包收藏] 跳过: id={cache_id} 不在缓存索引中(已淘汰或不存在)")
|
|
785
803
|
|
|
804
|
+
async def _persist_recent(self):
|
|
805
|
+
"""把「最近的聊天记录」落盘,让重启后的会话仍带着上下文(条数与内存里一致)
|
|
806
|
+
|
|
807
|
+
落盘前后都按 `_RECENT_KEEP` 截断:批次内还会追加机器人的回复/自述等,
|
|
808
|
+
不在这里截一次的话内存与文件都会慢慢超过上限。
|
|
809
|
+
"""
|
|
810
|
+
if len(self.recent_messages) > _RECENT_KEEP:
|
|
811
|
+
self.recent_messages = self.recent_messages[-_RECENT_KEEP:]
|
|
812
|
+
try:
|
|
813
|
+
await self.memory.save_recent(self.recent_messages)
|
|
814
|
+
except Exception as e:
|
|
815
|
+
logger.error(f"[记忆] 保存聊天记录失败: {e}")
|
|
816
|
+
|
|
786
817
|
def _record_bot_reply(self, segments: list[ReplySegment]):
|
|
787
818
|
for seg in segments:
|
|
788
819
|
if seg.type == "text":
|
|
@@ -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.0b3
|
|
4
4
|
Summary: 群聊特化LLM聊天机器人(增强版),具有记忆、表情包、跨插件调用、跨bot通信与跨适配器支持
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: <4.0,>=3.10
|
|
@@ -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.0b3)
|
|
84
84
|
|
|
85
|
-
`2.0.
|
|
85
|
+
`2.0.0b3` 是**预发布(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.0b3
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
在 `pyproject.toml` 中添加:
|
|
@@ -115,8 +115,8 @@ plugins = ["nonebot-plugin-aigf-master"]
|
|
|
115
115
|
AIGFM_LLM_API_KEY="sk-xxxxxxxxxxxx" # LLM API Key
|
|
116
116
|
AIGFM_LLM_BASE_URL="https://api.deepseek.com" # LLM API 地址
|
|
117
117
|
AIGFM_LLM_MODEL="deepseek-v4-flash" # LLM 模型名称
|
|
118
|
-
AIGFM_ENABLED_GROUPS=[
|
|
119
|
-
AIGFM_ENABLED_PRIVATE=[]
|
|
118
|
+
AIGFM_ENABLED_GROUPS=["onebot11:123456789", "console:console-chat"] # 启用的群/频道会话(`适配器:会话id`;也可只写数字,按 OneBot V11 识别)
|
|
119
|
+
AIGFM_ENABLED_PRIVATE=["onebot11:20001"] # 启用的私聊会话(同上写法;留空则私聊不响应)
|
|
120
120
|
|
|
121
121
|
# --- 图片理解(VLM) ---
|
|
122
122
|
AIGFM_IMAGE_MODE="vlm" # 图片模式: vlm / llm(默认 vlm)
|
|
@@ -234,7 +234,7 @@ AIGFM_PEER_CAPTURE_PLUGINS=[] # 要捕获输出的插件名列表,为
|
|
|
234
234
|
- 纯 @ 消息或检测到同一用户连续发送时,以及调用的插件返回图片时,等待时间会用 `AIGFM_INCOMPLETE_TIMEOUT`(默认 40 秒)**替代**默认的 30 秒(非叠加)
|
|
235
235
|
- **有图片正在解析时整批推迟触发**:等到 VLM 描述填好后随**同一批**发给 LLM,不会把没有描述的空消息发出去;"图片进入解析"这一特例最长等待 `AIGFM_BATCH_TIMEOUT + AIGFM_INCOMPLETE_TIMEOUT`(默认 70 秒)后仍会放行,此时该图片以 `[图片解析中]` 进入上下文
|
|
236
236
|
- 消息合并窗口内的同一用户连续消息会被合并为一条
|
|
237
|
-
- 每次处理时,LLM 收到最近 **20 条**(可配置)历史聊天记录 + 四层记忆 + 预设 + 表情包列表 +
|
|
237
|
+
- 每次处理时,LLM 收到最近 **20 条**(可配置)历史聊天记录 + 四层记忆 + 预设 + 表情包列表 + 其它插件响应;本批消息只出现在「新消息」段,同一条插件响应也只注入一次(不会在历史与新消息里重复)。历史聊天记录会落盘(`memory/<群号>/recent.json`),**机器人重启后仍能读回**
|
|
238
238
|
- LLM 一次调用同时完成:回复决策 + 记忆管理 + 表情包选择 + 命令学习
|
|
239
239
|
|
|
240
240
|
## 🧩 多适配器支持
|
|
@@ -243,9 +243,9 @@ AIGFM_PEER_CAPTURE_PLUGINS=[] # 要捕获输出的插件名列表,为
|
|
|
243
243
|
|
|
244
244
|
### 会话与启用清单
|
|
245
245
|
|
|
246
|
-
- 会话以 **`适配器:会话id`** 为键,例如 `onebot11:
|
|
246
|
+
- 会话以 **`适配器:会话id`** 为键,例如 `onebot11:123456789`(群)、`console:console-chat`(频道)、`onebot11:20001`(私聊)
|
|
247
247
|
- `AIGFM_ENABLED_GROUPS` 管群/频道会话、`AIGFM_ENABLED_PRIVATE` 管私聊会话;条目可写带前缀的完整键,也可以只写 id(**按 OneBot V11 识别**,旧配置无需改动)
|
|
248
|
-
- 落盘目录名不能含 `:`,因此磁盘上写作 `
|
|
248
|
+
- 落盘目录名不能含 `:`,因此磁盘上写作 `onebot11_123456789`;`memory/` 下旧版裸数字目录会在启动时**一次性自动迁移**(目标已存在则跳过,幂等)
|
|
249
249
|
- 非启用会话不发请求、不入缓冲:插件只会为启用的会话解析成员信息与图片
|
|
250
250
|
|
|
251
251
|
### 功能 × 适配器支持表(2026-09-25 实测)
|
|
@@ -590,6 +590,10 @@ LLM 可以:
|
|
|
590
590
|
|
|
591
591
|
存储在 `<插件数据目录>/memory/<群号>/culture.json`,记录梗、网络用语、流行语。LLM 主动学习和存储,根据聊天内容自动匹配。
|
|
592
592
|
|
|
593
|
+
### 最近聊天记录
|
|
594
|
+
|
|
595
|
+
上面四层记忆之外的**滚动窗口**:最近 50 条消息(含机器人自己的回复与插件响应)会落盘在 `<插件数据目录>/memory/<群号>/recent.json`,**机器人重启后会读回**,因此刚重启的会话也能接上之前的上下文(注入 prompt 的条数由 `AIGFM_RECENT_MESSAGES` 控制,默认 20)。`/reset` 会连这份记录一起清空。
|
|
596
|
+
|
|
593
597
|
## 🖼️ 表情包功能
|
|
594
598
|
|
|
595
599
|
### 工作原理
|
|
@@ -766,6 +770,7 @@ LLM 支持以下回复类型:
|
|
|
766
770
|
short_term.json # 短期记忆
|
|
767
771
|
long_term.json # 长期记忆
|
|
768
772
|
culture.json # 文化记忆
|
|
773
|
+
recent.json # 最近的聊天记录(重启后仍可读)
|
|
769
774
|
friends/
|
|
770
775
|
{user_id}.json # 群友信息
|
|
771
776
|
memes/
|
|
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
|
|
File without changes
|