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.
Files changed (36) hide show
  1. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/PKG-INFO +14 -9
  2. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/README.md +13 -8
  3. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/__init__.py +2 -0
  4. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/memory_store.py +48 -1
  5. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/processor.py +34 -3
  6. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master.egg-info/PKG-INFO +14 -9
  7. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/pyproject.toml +1 -1
  8. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/LICENSE +0 -0
  9. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/anime_recognizer.py +0 -0
  10. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/animetrace_client.py +0 -0
  11. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/api_hooks.py +0 -0
  12. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/cache_cleanup.py +0 -0
  13. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/command_learner.py +0 -0
  14. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/config.py +0 -0
  15. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/context_bus.py +0 -0
  16. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/image_gen_client.py +0 -0
  17. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/image_handler.py +0 -0
  18. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/llm_client.py +0 -0
  19. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/meme_store.py +0 -0
  20. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/models.py +0 -0
  21. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/notice.py +0 -0
  22. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/peer_client.py +0 -0
  23. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/plugin_discovery.py +0 -0
  24. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/plugin_invoker.py +0 -0
  25. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/preset_store.py +0 -0
  26. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/prompt_builder.py +0 -0
  27. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/response_parser.py +0 -0
  28. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/search_client.py +0 -0
  29. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/session.py +0 -0
  30. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/unimsg.py +0 -0
  31. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master/vlm_client.py +0 -0
  32. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master.egg-info/SOURCES.txt +0 -0
  33. {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
  34. {nonebot_plugin_aigf_master-2.0.0b2 → nonebot_plugin_aigf_master-2.0.0b3}/nonebot_plugin_aigf_master.egg-info/requires.txt +0 -0
  35. {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
  36. {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.0b2
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.0b2)
83
+ ### 安装 beta 版本(2.0.0b3)
84
84
 
85
- `2.0.0b2` 是**预发布(pre-release)版本**,因此:
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.0b2
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=[123456, 789012] # 启用的群/频道会话(可写 `适配器:会话id`,只写数字按 OneBot V11 识别)
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:617770183`(群)、`console:console-chat`(频道)、`onebot11:20001`(私聊)
246
+ - 会话以 **`适配器:会话id`** 为键,例如 `onebot11:123456789`(群)、`console:console-chat`(频道)、`onebot11:20001`(私聊)
247
247
  - `AIGFM_ENABLED_GROUPS` 管群/频道会话、`AIGFM_ENABLED_PRIVATE` 管私聊会话;条目可写带前缀的完整键,也可以只写 id(**按 OneBot V11 识别**,旧配置无需改动)
248
- - 落盘目录名不能含 `:`,因此磁盘上写作 `onebot11_617770183`;`memory/` 下旧版裸数字目录会在启动时**一次性自动迁移**(目标已存在则跳过,幂等)
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.0b2)
61
+ ### 安装 beta 版本(2.0.0b3)
62
62
 
63
- `2.0.0b2` 是**预发布(pre-release)版本**,因此:
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.0b2
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=[123456, 789012] # 启用的群/频道会话(可写 `适配器:会话id`,只写数字按 OneBot V11 识别)
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:617770183`(群)、`console:console-chat`(频道)、`onebot11:20001`(私聊)
224
+ - 会话以 **`适配器:会话id`** 为键,例如 `onebot11:123456789`(群)、`console:console-chat`(频道)、`onebot11:20001`(私聊)
225
225
  - `AIGFM_ENABLED_GROUPS` 管群/频道会话、`AIGFM_ENABLED_PRIVATE` 管私聊会话;条目可写带前缀的完整键,也可以只写 id(**按 OneBot V11 识别**,旧配置无需改动)
226
- - 落盘目录名不能含 `:`,因此磁盘上写作 `onebot11_617770183`;`memory/` 下旧版裸数字目录会在启动时**一次性自动迁移**(目标已存在则跳过,幂等)
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) > 50:
331
- self.recent_messages = self.recent_messages[-50:]
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.0b2
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.0b2)
83
+ ### 安装 beta 版本(2.0.0b3)
84
84
 
85
- `2.0.0b2` 是**预发布(pre-release)版本**,因此:
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.0b2
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=[123456, 789012] # 启用的群/频道会话(可写 `适配器:会话id`,只写数字按 OneBot V11 识别)
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:617770183`(群)、`console:console-chat`(频道)、`onebot11:20001`(私聊)
246
+ - 会话以 **`适配器:会话id`** 为键,例如 `onebot11:123456789`(群)、`console:console-chat`(频道)、`onebot11:20001`(私聊)
247
247
  - `AIGFM_ENABLED_GROUPS` 管群/频道会话、`AIGFM_ENABLED_PRIVATE` 管私聊会话;条目可写带前缀的完整键,也可以只写 id(**按 OneBot V11 识别**,旧配置无需改动)
248
- - 落盘目录名不能含 `:`,因此磁盘上写作 `onebot11_617770183`;`memory/` 下旧版裸数字目录会在启动时**一次性自动迁移**(目标已存在则跳过,幂等)
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/
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nonebot-plugin-aigf-master"
7
- version = "2.0.0b2"
7
+ version = "2.0.0b3"
8
8
  description = "群聊特化LLM聊天机器人(增强版),具有记忆、表情包、跨插件调用、跨bot通信与跨适配器支持"
9
9
  readme = "README.md"
10
10
  license = "MIT"