nonebot-plugin-aigf-master 1.2.0__tar.gz → 1.3.0__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-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/PKG-INFO +16 -1
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/README.md +15 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/__init__.py +59 -9
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/config.py +8 -0
- nonebot_plugin_aigf_master-1.3.0/nonebot_plugin_aigf_master/image_gen_client.py +42 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/meme_store.py +12 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/processor.py +189 -1
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/prompt_builder.py +4 -3
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master.egg-info/PKG-INFO +16 -1
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master.egg-info/SOURCES.txt +1 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/pyproject.toml +1 -1
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/LICENSE +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/api_hooks.py +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/command_learner.py +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/context_bus.py +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/image_handler.py +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/llm_client.py +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/memory_store.py +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/models.py +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/peer_client.py +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/plugin_discovery.py +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/plugin_invoker.py +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/preset_store.py +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/response_parser.py +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/search_client.py +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master/vlm_client.py +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master.egg-info/dependency_links.txt +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master.egg-info/requires.txt +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/nonebot_plugin_aigf_master.egg-info/top_level.txt +0 -0
- {nonebot_plugin_aigf_master-1.2.0 → nonebot_plugin_aigf_master-1.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nonebot-plugin-aigf-master
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: 群聊特化LLM聊天机器人(增强版),具有记忆、表情包、跨插件调用和跨bot通信能力
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: <4.0,>=3.10
|
|
@@ -52,6 +52,9 @@ Dynamic: license-file
|
|
|
52
52
|
- 💬 **口语化回复**:像平时聊天一样自然,回复长度有变化,允许敷衍
|
|
53
53
|
- 📨 **消息合并**:同一用户连续消息自动合并,避免碎片化上下文
|
|
54
54
|
- ⏱️ **不完整消息检测**:纯 @ 消息或连续发送时自动延长等待时间
|
|
55
|
+
- 🕐 **消息时间标注**:可选为 LLM 看到的消息加上 `[MM-DD HH:MM]` 时间前缀(`AIGFM_SHOW_MESSAGE_TIME`)
|
|
56
|
+
- 📄 **完整消息类型**:合并聊天记录、小程序/卡片、XML 消息均可解析与捕获
|
|
57
|
+
- 🎨 **AI 生图**:LLM 生成图片自动发到群里,尺寸由 LLM 决定(程序限最大尺寸),支持参考图改绘(回复图片消息或从聊天记录选图)
|
|
55
58
|
- 🔒 **强制 JSON 输出**:确保 LLM 始终返回正确格式
|
|
56
59
|
- ⚡ **轻量高效**:单次 LLM 调用完成对话 + 记忆管理 + 命令学习
|
|
57
60
|
|
|
@@ -106,6 +109,14 @@ AIGFM_VLM_ENABLED=true # 是否启用VLM(仅 vlm 模式有效
|
|
|
106
109
|
AIGFM_VLM_MODEL="..." # VLM 模型名称(vlm 模式必填)
|
|
107
110
|
AIGFM_VLM_BASE_URL="https://..." # VLM API 地址
|
|
108
111
|
AIGFM_VLM_API_KEY="" # VLM API Key(为空时使用 chat 的 key)
|
|
112
|
+
|
|
113
|
+
# --- AI 生图 ---
|
|
114
|
+
AIGFM_IMAGE_GEN_ENABLED=false # 是否启用 AI 生图(默认 false)
|
|
115
|
+
AIGFM_IMAGE_GEN_MODEL="..." # 生图模型名称(豆包/Ark 示例: doubao-seedream-5-0-lite-260128)
|
|
116
|
+
AIGFM_IMAGE_GEN_BASE_URL="https://..." # 生图 API 地址(OpenAI 兼容 images API;豆包示例 https://ark.cn-beijing.volces.com/api/v3)
|
|
117
|
+
AIGFM_IMAGE_GEN_API_KEY="" # 生图 API Key(为空时使用 chat 的 key)
|
|
118
|
+
AIGFM_IMAGE_GEN_MAX_SIZE="1024x1024" # 生成最大尺寸限制(宽x高,LLM 可指定比例/尺寸,程序自动缩放到不超过该上限)
|
|
119
|
+
AIGFM_IMAGE_GEN_WATERMARK=false # 生图水印(参考豆包类服务,仅参考图时传入,默认 false)
|
|
109
120
|
```
|
|
110
121
|
|
|
111
122
|
### 可选
|
|
@@ -122,6 +133,7 @@ AIGFM_BATCH_TIMEOUT=30.0 # 距最后一条消息多少秒后触
|
|
|
122
133
|
AIGFM_INCOMPLETE_TIMEOUT=40.0 # 消息可能不完整时的等待时间/秒(默认 40.0;替代默认 30s 生效,非叠加)
|
|
123
134
|
AIGFM_RECENT_MESSAGES=20 # prompt 中包含的最近历史消息条数(默认 20,最大 50)
|
|
124
135
|
AIGFM_MERGE_WINDOW=5.0 # 消息合并时间窗口/秒(默认 5.0)
|
|
136
|
+
AIGFM_SHOW_MESSAGE_TIME=false # LLM 看到的消息是否带 [MM-DD HH:MM] 时间前缀(默认 false)
|
|
125
137
|
|
|
126
138
|
# --- LLM ---
|
|
127
139
|
AIGFM_LLM_JSON_MODE=true # 是否强制 LLM 输出 JSON(默认 true)
|
|
@@ -222,6 +234,9 @@ LLM 在聊天记录中看到:[插件名]: 插件的输出
|
|
|
222
234
|
|------|---------|
|
|
223
235
|
| 文本 | 直接以 `[来源]: '内容'` 进入上下文 |
|
|
224
236
|
| 图片 | 先经 VLM 转成文字描述,再以 `[图片] 描述` 进入上下文(`AIGFM_CAPTURE_IMAGES=false` 时不捕获) |
|
|
237
|
+
| 合并聊天记录 | 以 `[收到一条合并聊天记录]` 进入上下文 |
|
|
238
|
+
| 小程序/卡片 | 以 `[小程序/卡片: 标题, 描述]` 进入上下文 |
|
|
239
|
+
| XML | 以 `[收到一条XML消息]` 进入上下文 |
|
|
225
240
|
|
|
226
241
|
图片的来源字段依次支持:
|
|
227
242
|
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
- 💬 **口语化回复**:像平时聊天一样自然,回复长度有变化,允许敷衍
|
|
33
33
|
- 📨 **消息合并**:同一用户连续消息自动合并,避免碎片化上下文
|
|
34
34
|
- ⏱️ **不完整消息检测**:纯 @ 消息或连续发送时自动延长等待时间
|
|
35
|
+
- 🕐 **消息时间标注**:可选为 LLM 看到的消息加上 `[MM-DD HH:MM]` 时间前缀(`AIGFM_SHOW_MESSAGE_TIME`)
|
|
36
|
+
- 📄 **完整消息类型**:合并聊天记录、小程序/卡片、XML 消息均可解析与捕获
|
|
37
|
+
- 🎨 **AI 生图**:LLM 生成图片自动发到群里,尺寸由 LLM 决定(程序限最大尺寸),支持参考图改绘(回复图片消息或从聊天记录选图)
|
|
35
38
|
- 🔒 **强制 JSON 输出**:确保 LLM 始终返回正确格式
|
|
36
39
|
- ⚡ **轻量高效**:单次 LLM 调用完成对话 + 记忆管理 + 命令学习
|
|
37
40
|
|
|
@@ -86,6 +89,14 @@ AIGFM_VLM_ENABLED=true # 是否启用VLM(仅 vlm 模式有效
|
|
|
86
89
|
AIGFM_VLM_MODEL="..." # VLM 模型名称(vlm 模式必填)
|
|
87
90
|
AIGFM_VLM_BASE_URL="https://..." # VLM API 地址
|
|
88
91
|
AIGFM_VLM_API_KEY="" # VLM API Key(为空时使用 chat 的 key)
|
|
92
|
+
|
|
93
|
+
# --- AI 生图 ---
|
|
94
|
+
AIGFM_IMAGE_GEN_ENABLED=false # 是否启用 AI 生图(默认 false)
|
|
95
|
+
AIGFM_IMAGE_GEN_MODEL="..." # 生图模型名称(豆包/Ark 示例: doubao-seedream-5-0-lite-260128)
|
|
96
|
+
AIGFM_IMAGE_GEN_BASE_URL="https://..." # 生图 API 地址(OpenAI 兼容 images API;豆包示例 https://ark.cn-beijing.volces.com/api/v3)
|
|
97
|
+
AIGFM_IMAGE_GEN_API_KEY="" # 生图 API Key(为空时使用 chat 的 key)
|
|
98
|
+
AIGFM_IMAGE_GEN_MAX_SIZE="1024x1024" # 生成最大尺寸限制(宽x高,LLM 可指定比例/尺寸,程序自动缩放到不超过该上限)
|
|
99
|
+
AIGFM_IMAGE_GEN_WATERMARK=false # 生图水印(参考豆包类服务,仅参考图时传入,默认 false)
|
|
89
100
|
```
|
|
90
101
|
|
|
91
102
|
### 可选
|
|
@@ -102,6 +113,7 @@ AIGFM_BATCH_TIMEOUT=30.0 # 距最后一条消息多少秒后触
|
|
|
102
113
|
AIGFM_INCOMPLETE_TIMEOUT=40.0 # 消息可能不完整时的等待时间/秒(默认 40.0;替代默认 30s 生效,非叠加)
|
|
103
114
|
AIGFM_RECENT_MESSAGES=20 # prompt 中包含的最近历史消息条数(默认 20,最大 50)
|
|
104
115
|
AIGFM_MERGE_WINDOW=5.0 # 消息合并时间窗口/秒(默认 5.0)
|
|
116
|
+
AIGFM_SHOW_MESSAGE_TIME=false # LLM 看到的消息是否带 [MM-DD HH:MM] 时间前缀(默认 false)
|
|
105
117
|
|
|
106
118
|
# --- LLM ---
|
|
107
119
|
AIGFM_LLM_JSON_MODE=true # 是否强制 LLM 输出 JSON(默认 true)
|
|
@@ -202,6 +214,9 @@ LLM 在聊天记录中看到:[插件名]: 插件的输出
|
|
|
202
214
|
|------|---------|
|
|
203
215
|
| 文本 | 直接以 `[来源]: '内容'` 进入上下文 |
|
|
204
216
|
| 图片 | 先经 VLM 转成文字描述,再以 `[图片] 描述` 进入上下文(`AIGFM_CAPTURE_IMAGES=false` 时不捕获) |
|
|
217
|
+
| 合并聊天记录 | 以 `[收到一条合并聊天记录]` 进入上下文 |
|
|
218
|
+
| 小程序/卡片 | 以 `[小程序/卡片: 标题, 描述]` 进入上下文 |
|
|
219
|
+
| XML | 以 `[收到一条XML消息]` 进入上下文 |
|
|
205
220
|
|
|
206
221
|
图片的来源字段依次支持:
|
|
207
222
|
|
|
@@ -26,6 +26,7 @@ from .context_bus import ContextBus
|
|
|
26
26
|
from .command_learner import CommandLearner
|
|
27
27
|
from .llm_client import LLMClient
|
|
28
28
|
from .image_handler import ImageHandler
|
|
29
|
+
from .image_gen_client import ImageGenClient
|
|
29
30
|
from .meme_store import MemeStore
|
|
30
31
|
from .memory_store import MemoryStore
|
|
31
32
|
from .models import ChatMessage
|
|
@@ -60,6 +61,12 @@ _learner = CommandLearner(
|
|
|
60
61
|
min_confidence=plugin_config.aigfm_learn_min_confidence,
|
|
61
62
|
)
|
|
62
63
|
_image_handler = ImageHandler(_cache_dir)
|
|
64
|
+
_image_gen = ImageGenClient(
|
|
65
|
+
plugin_config.aigfm_image_gen_api_key or plugin_config.aigfm_llm_api_key,
|
|
66
|
+
plugin_config.aigfm_image_gen_model,
|
|
67
|
+
plugin_config.aigfm_image_gen_base_url,
|
|
68
|
+
_proxy,
|
|
69
|
+
) if plugin_config.aigfm_image_gen_enabled else None
|
|
63
70
|
_search = create_search_client(
|
|
64
71
|
plugin_config.aigfm_search_api, plugin_config.aigfm_search_api_key,
|
|
65
72
|
plugin_config.aigfm_openwebsearch_url,
|
|
@@ -78,6 +85,7 @@ def _get_processor(group_id: int) -> MessageProcessor:
|
|
|
78
85
|
context_bus=_bus, invoker=_invoker, learner=_learner,
|
|
79
86
|
search=_search, config=plugin_config, peer_client=_peer_client,
|
|
80
87
|
peer_scanned_commands=_peer_scanned_commands,
|
|
88
|
+
image_gen=_image_gen, image_handler=_image_handler,
|
|
81
89
|
)
|
|
82
90
|
return _processors[group_id]
|
|
83
91
|
|
|
@@ -186,15 +194,19 @@ async def _parse_message(bot: Bot, event: GroupMessageEvent, message: Message, b
|
|
|
186
194
|
elif seg.type == "reply":
|
|
187
195
|
has_non_at = True
|
|
188
196
|
try:
|
|
189
|
-
|
|
197
|
+
# OneBot v11 的 reply 段键名在不同实现/构造方式下可能是 message_id 或 id
|
|
198
|
+
reply_msg_id = seg.data.get("message_id") or seg.data.get("id")
|
|
190
199
|
if reply_msg_id:
|
|
191
200
|
original = await bot.get_msg(message_id=int(reply_msg_id))
|
|
192
201
|
if original and "message" in original:
|
|
193
|
-
replied_text =
|
|
202
|
+
replied_text = await _extract_reply_content(bot, original["message"], event.group_id)
|
|
194
203
|
replied_sender = original.get("sender", {}).get("nickname", "未知")
|
|
195
204
|
content += f"[回复 {replied_sender} 的消息: \"{replied_text}\"] "
|
|
196
|
-
|
|
197
|
-
|
|
205
|
+
else:
|
|
206
|
+
content += "[回复: (无法获取被回复消息)] "
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.warning(f"回复消息处理失败: {e}")
|
|
209
|
+
content += "[回复: (被回复内容获取失败)] "
|
|
198
210
|
elif seg.type == "forward":
|
|
199
211
|
has_non_at = True
|
|
200
212
|
content += "[收到一条合并聊天记录] "
|
|
@@ -208,7 +220,12 @@ async def _parse_message(bot: Bot, event: GroupMessageEvent, message: Message, b
|
|
|
208
220
|
return content.strip(), not has_non_at
|
|
209
221
|
|
|
210
222
|
|
|
211
|
-
def
|
|
223
|
+
async def _extract_reply_content(bot: Bot, message_content, group_id: int) -> str:
|
|
224
|
+
"""把被回复消息渲染为「原文 + 图片 id + VLM 描述」(完整输出,不截断)
|
|
225
|
+
|
|
226
|
+
图片段与普通图片消息同链路:下载 → 缓存(save_to_cache,保证 cache_id 进入本群缓存)→ VLM 描述。
|
|
227
|
+
这样 LLM 从回复段能看到图片 id 与内容,可作为生图参考图(reference_cache_id)。
|
|
228
|
+
"""
|
|
212
229
|
try:
|
|
213
230
|
if isinstance(message_content, str):
|
|
214
231
|
msg = OneBotMessage(message_content)
|
|
@@ -218,12 +235,45 @@ def _extract_reply_text(message_content) -> str:
|
|
|
218
235
|
if isinstance(seg, dict) and "type" in seg:
|
|
219
236
|
msg.append(MessageSegment(type=seg["type"], data=seg.get("data", {})))
|
|
220
237
|
else:
|
|
221
|
-
return str(message_content)[:50]
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
except Exception:
|
|
238
|
+
return str(message_content)[:50] # 非标准结构兜底
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.warning(f"被回复消息解析失败: {e}")
|
|
225
241
|
return "(无法获取)"
|
|
226
242
|
|
|
243
|
+
parts = []
|
|
244
|
+
for seg in msg:
|
|
245
|
+
if seg.type == "text":
|
|
246
|
+
text = seg.data.get("text", "")
|
|
247
|
+
if text.strip():
|
|
248
|
+
parts.append(text.strip())
|
|
249
|
+
elif seg.type in ("image", "emoji"):
|
|
250
|
+
_on_image_start(group_id)
|
|
251
|
+
try:
|
|
252
|
+
img = _extract_image_data(seg.data)
|
|
253
|
+
is_sticker = seg.data.get("sub_type") == 1
|
|
254
|
+
image_bytes = await _load_image_bytes(img)
|
|
255
|
+
image_base64 = base64.b64encode(image_bytes).decode()
|
|
256
|
+
if plugin_config.aigfm_image_mode == "llm":
|
|
257
|
+
cache_id = await _memes.save_to_cache(group_id, image_bytes, "", "")
|
|
258
|
+
parts.append(f"[发送了一张图片, id: {cache_id}]")
|
|
259
|
+
else:
|
|
260
|
+
desc = await _image_handler.describe(image_base64, is_sticker)
|
|
261
|
+
if desc:
|
|
262
|
+
cache_id = await _memes.save_to_cache(group_id, image_bytes, desc.description, desc.emotion)
|
|
263
|
+
if is_sticker:
|
|
264
|
+
parts.append(f"[发送了一张可能是表情包的图片, id: {cache_id}] [情感:{desc.emotion}] [内容:{desc.description}]")
|
|
265
|
+
else:
|
|
266
|
+
parts.append(f"[发送了一张图片, id: {cache_id}] [内容:{desc.description}]")
|
|
267
|
+
else:
|
|
268
|
+
parts.append("[发送了一张图片](识图失败)")
|
|
269
|
+
except Exception as e:
|
|
270
|
+
logger.error(f"被回复图片处理错误: {e}")
|
|
271
|
+
parts.append("[图片加载失败]")
|
|
272
|
+
finally:
|
|
273
|
+
_on_image_done(group_id)
|
|
274
|
+
text = " ".join(parts)
|
|
275
|
+
return text if text else "(非文字消息)"
|
|
276
|
+
|
|
227
277
|
|
|
228
278
|
# ========== 批量消息处理 ==========
|
|
229
279
|
|
|
@@ -21,6 +21,14 @@ class PluginConfig(BaseModel):
|
|
|
21
21
|
aigfm_vlm_model: str = Field("", description="VLM 模型名称")
|
|
22
22
|
aigfm_vlm_base_url: str = Field("", description="VLM API 地址")
|
|
23
23
|
aigfm_vlm_api_key: str = Field("", description="VLM API Key(为空时使用 llm_api_key)")
|
|
24
|
+
# AI 生图(OpenAI 兼容 images API)
|
|
25
|
+
aigfm_image_gen_enabled: bool = Field(False, description="是否启用 AI 生图")
|
|
26
|
+
aigfm_image_gen_model: str = Field("", description="生图模型名称")
|
|
27
|
+
aigfm_image_gen_base_url: str = Field("", description="生图 API 地址")
|
|
28
|
+
aigfm_image_gen_api_key: str = Field("", description="生图 API Key(为空时使用 llm_api_key)")
|
|
29
|
+
aigfm_image_gen_max_size: str = Field("1024x1024", description="生图最大尺寸限制(宽x高),LLM 可指定比例/尺寸,程序自动缩放到不超过该上限")
|
|
30
|
+
aigfm_image_gen_min_size: str = Field("", description="生图最小尺寸(宽x高),用于满足服务的最低像素要求(如豆包 Seedream 需 ≥ 1920x1920 即 3686400 像素);为空不限制,非空时按比例放大到不小于该面积(最小优先于最大)")
|
|
31
|
+
aigfm_image_gen_watermark: bool = Field(False, description="生图水印(参考豆包类服务,仅参考图路径传入 watermark 参数)")
|
|
24
32
|
|
|
25
33
|
# 群聊配置
|
|
26
34
|
aigfm_enabled_groups: list[int] = Field(default_factory=list, description="启用的群号列表")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""AI 生图客户端(OpenAI 兼容 images API)"""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
from openai import AsyncOpenAI
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ImageGenClient:
|
|
8
|
+
"""AI 生图客户端:调用 OpenAI 兼容的 images API(POST /v1/images/generations)"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, api_key: str, model: str, base_url: str,
|
|
11
|
+
proxy: str | None = None, timeout: int = 120):
|
|
12
|
+
http_client = httpx.AsyncClient(proxy=proxy) if proxy else None
|
|
13
|
+
self._client = AsyncOpenAI(api_key=api_key, base_url=base_url, http_client=http_client)
|
|
14
|
+
self._model = model
|
|
15
|
+
self._timeout = timeout
|
|
16
|
+
|
|
17
|
+
async def generate(self, prompt: str, size: str = "1024x1024",
|
|
18
|
+
reference_images: list[str] | None = None,
|
|
19
|
+
watermark: bool = False) -> dict | None:
|
|
20
|
+
"""生成图片
|
|
21
|
+
|
|
22
|
+
reference_images: 可选参考图列表(data URI:data:image/<小写格式>;base64,<b64>,或可访问 url)。
|
|
23
|
+
有参考图时经 extra_body.image 传给豆包类服务做图生图/改绘;无参考图走普通文生图。
|
|
24
|
+
|
|
25
|
+
返回 {"b64": "..."}(base64 图片数据)或 {"url": "..."}(网络地址);
|
|
26
|
+
无结果返回 None。异常由调用方捕获处理。
|
|
27
|
+
"""
|
|
28
|
+
kwargs: dict = dict(
|
|
29
|
+
model=self._model, prompt=prompt, n=1, size=size,
|
|
30
|
+
response_format="b64_json", timeout=self._timeout,
|
|
31
|
+
)
|
|
32
|
+
if reference_images:
|
|
33
|
+
kwargs["extra_body"] = {"image": reference_images, "watermark": watermark}
|
|
34
|
+
response = await self._client.images.generate(**kwargs)
|
|
35
|
+
if not response.data:
|
|
36
|
+
return None
|
|
37
|
+
data = response.data[0]
|
|
38
|
+
if getattr(data, "b64_json", None):
|
|
39
|
+
return {"b64": data.b64_json}
|
|
40
|
+
if getattr(data, "url", None):
|
|
41
|
+
return {"url": data.url}
|
|
42
|
+
return None
|
|
@@ -127,6 +127,18 @@ class MemeStore:
|
|
|
127
127
|
def get_cache_info(self, group_id: int, cache_id: str) -> dict | None:
|
|
128
128
|
return self._cache_index.get(group_id, {}).get(cache_id)
|
|
129
129
|
|
|
130
|
+
def get_cache_file(self, cache_id: str) -> str | None:
|
|
131
|
+
"""按 cache_id 从磁盘兜底查找缓存图片文件路径(clear_cache 只清索引不清文件)
|
|
132
|
+
|
|
133
|
+
返回首个存在的文件路径:sticker_cache/{cache_id}.{ext};找不到返回 None。
|
|
134
|
+
"""
|
|
135
|
+
if not cache_id:
|
|
136
|
+
return None
|
|
137
|
+
for p in self._cache_dir.glob(f"{cache_id}.*"):
|
|
138
|
+
if p.is_file():
|
|
139
|
+
return str(p)
|
|
140
|
+
return None
|
|
141
|
+
|
|
130
142
|
def clear_cache(self, group_id: int):
|
|
131
143
|
self._cache_index.pop(group_id, None)
|
|
132
144
|
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
"""消息处理编排器"""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
import base64
|
|
3
5
|
import random
|
|
4
6
|
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
5
8
|
|
|
9
|
+
import anyio
|
|
10
|
+
import httpx
|
|
6
11
|
from nonebot import get_driver, logger
|
|
12
|
+
from nonebot.adapters.onebot.v11 import Message, MessageSegment
|
|
7
13
|
|
|
14
|
+
from .command_learner import CommandLearner
|
|
8
15
|
from .config import PluginConfig
|
|
9
16
|
from .context_bus import ContextBus
|
|
10
|
-
from .
|
|
17
|
+
from .image_gen_client import ImageGenClient
|
|
18
|
+
from .image_handler import ImageHandler
|
|
11
19
|
from .llm_client import LLMClient
|
|
12
20
|
from .meme_store import MemeStore
|
|
13
21
|
from .memory_store import MemoryStore
|
|
@@ -34,6 +42,72 @@ def _command_head(command: str) -> str:
|
|
|
34
42
|
return command.split(None, 1)[0] if command else command
|
|
35
43
|
|
|
36
44
|
|
|
45
|
+
def _normalize_size(size_desc: str, max_size: str, min_size: str = "") -> str:
|
|
46
|
+
"""把 LLM 的尺寸描述(方向词或 WxH)缩放到不超过 max_size 的偶数尺寸
|
|
47
|
+
|
|
48
|
+
支持:square/方形/1:1、portrait/竖版/竖屏、landscape/横版/横屏、
|
|
49
|
+
16:9/宽幅/banner、4:3,或直接写 宽x高(如 768x1024)。
|
|
50
|
+
结果按比例缩放到不超过 max_size,并对齐到偶数像素。
|
|
51
|
+
min_size 非空时:若结果面积小于 min_size 面积(宽x高),按比例放大到不小于该面积(最小优先于最大,
|
|
52
|
+
服务于有最低像素要求的生图服务,如豆包 Seedream 需 ≥ 1920x1920 即 3686400 像素)。
|
|
53
|
+
"""
|
|
54
|
+
max_w, max_h = 1024, 1024
|
|
55
|
+
try:
|
|
56
|
+
mw, mh = max_size.lower().split("x")
|
|
57
|
+
max_w, max_h = int(mw), int(mh)
|
|
58
|
+
except (ValueError, AttributeError):
|
|
59
|
+
pass
|
|
60
|
+
max_w = max(max_w, 64)
|
|
61
|
+
max_h = max(max_h, 64)
|
|
62
|
+
|
|
63
|
+
min_pixels = 0
|
|
64
|
+
if min_size:
|
|
65
|
+
try:
|
|
66
|
+
min_w, min_h = min_size.lower().split("x")
|
|
67
|
+
min_pixels = int(min_w) * int(min_h)
|
|
68
|
+
except (ValueError, AttributeError):
|
|
69
|
+
min_pixels = 0
|
|
70
|
+
|
|
71
|
+
ratio = 1.0 # 宽/高
|
|
72
|
+
if size_desc:
|
|
73
|
+
s = size_desc.strip().lower()
|
|
74
|
+
if "x" in s:
|
|
75
|
+
try:
|
|
76
|
+
w, h = s.split("x")
|
|
77
|
+
ratio = int(w) / int(h)
|
|
78
|
+
except (ValueError, ZeroDivisionError):
|
|
79
|
+
ratio = 1.0
|
|
80
|
+
elif s in ("portrait", "竖版", "竖屏", "2:3"):
|
|
81
|
+
ratio = 2 / 3
|
|
82
|
+
elif s in ("landscape", "横版", "横屏", "3:2"):
|
|
83
|
+
ratio = 3 / 2
|
|
84
|
+
elif s in ("16:9", "宽幅", "banner"):
|
|
85
|
+
ratio = 16 / 9
|
|
86
|
+
elif s in ("4:3",):
|
|
87
|
+
ratio = 4 / 3
|
|
88
|
+
# square/方形/1:1 及其它未知描述 → ratio = 1.0
|
|
89
|
+
|
|
90
|
+
if ratio >= 1.0:
|
|
91
|
+
w, h = max_w, round(max_w / ratio)
|
|
92
|
+
else:
|
|
93
|
+
w, h = round(max_h * ratio), max_h
|
|
94
|
+
w -= w % 2
|
|
95
|
+
h -= h % 2
|
|
96
|
+
w = max(w, 64)
|
|
97
|
+
h = max(h, 64)
|
|
98
|
+
if min_pixels and w * h < min_pixels:
|
|
99
|
+
scale = (min_pixels / (w * h)) ** 0.5
|
|
100
|
+
w, h = round(w * scale), round(h * scale)
|
|
101
|
+
w -= w % 2
|
|
102
|
+
h -= h % 2
|
|
103
|
+
while w * h < min_pixels: # 偶数取整后仍可能略低于下限,循环补边
|
|
104
|
+
if w >= h:
|
|
105
|
+
w += 2
|
|
106
|
+
else:
|
|
107
|
+
h += 2
|
|
108
|
+
return f"{w}x{h}"
|
|
109
|
+
|
|
110
|
+
|
|
37
111
|
# 本插件自身的管理命令不允许被 LLM 代为执行:synthetic 事件携带真实触发用户的 user_id,
|
|
38
112
|
# 若该用户是 superuser,`/status`、`/reset` 等会被真的执行
|
|
39
113
|
SELF_PLUGIN_NAMES = {"nonebot_plugin_aigf_master", "nonebot-plugin-aigf-master"}
|
|
@@ -65,6 +139,8 @@ class MessageProcessor:
|
|
|
65
139
|
config: PluginConfig,
|
|
66
140
|
peer_client: PeerClient | None = None,
|
|
67
141
|
peer_scanned_commands: dict[str, list[dict]] | None = None,
|
|
142
|
+
image_gen: ImageGenClient | None = None,
|
|
143
|
+
image_handler: ImageHandler | None = None,
|
|
68
144
|
):
|
|
69
145
|
self.group_id = group_id
|
|
70
146
|
self.llm = llm
|
|
@@ -78,6 +154,9 @@ class MessageProcessor:
|
|
|
78
154
|
self.config = config
|
|
79
155
|
self.peer_client = peer_client
|
|
80
156
|
self.peer_scanned_commands = peer_scanned_commands if peer_scanned_commands is not None else {}
|
|
157
|
+
self.image_gen = image_gen
|
|
158
|
+
self.image_handler = image_handler
|
|
159
|
+
self._image_gen_tasks: set[asyncio.Task] = set()
|
|
81
160
|
self.bot_name = "小助手"
|
|
82
161
|
self.bot_role = "一个友好的群聊助手"
|
|
83
162
|
self.current_preset = ""
|
|
@@ -327,6 +406,18 @@ class MessageProcessor:
|
|
|
327
406
|
}, "required": ["bot", "command"]},
|
|
328
407
|
},
|
|
329
408
|
})
|
|
409
|
+
if self.config.aigfm_image_gen_enabled and self.image_gen:
|
|
410
|
+
tools.append({
|
|
411
|
+
"type": "function", "function": {
|
|
412
|
+
"name": "generate_image",
|
|
413
|
+
"description": "使用 AI 生图模型根据描述生成图片并自动发到群里(仅当用户明确要求生成/画图时才调用)",
|
|
414
|
+
"parameters": {"type": "object", "properties": {
|
|
415
|
+
"prompt": {"type": "string", "description": "详细的图片描述(画面内容、风格、色调、比例等),越具体生成效果越好"},
|
|
416
|
+
"size": {"type": "string", "description": "可选尺寸/比例:square/方形、portrait/竖版、landscape/横版、16:9/宽幅,或直接写宽x高(如 768x1024);默认方形,程序会自动限制在配置的最大尺寸内"},
|
|
417
|
+
"reference_cache_id": {"type": "string", "description": "可选参考图 id:聊天记录中出现的图片 id(如 [发送了一张图片, id: xxx] 或 [回复 xxx 的消息: \"[发送了一张图片, id: xxx]...\"] 中的 xxx);用户要求\"参考这张图/照着改/换成某种风格\"时填,不要编造不存在的 id,不需要时省略"}
|
|
418
|
+
}, "required": ["prompt"]},
|
|
419
|
+
},
|
|
420
|
+
})
|
|
330
421
|
|
|
331
422
|
async def handler(name: str, args: dict) -> str:
|
|
332
423
|
if name == "search_internet" and self.search:
|
|
@@ -386,6 +477,23 @@ class MessageProcessor:
|
|
|
386
477
|
content=f"已调用命令「{command}」({peer_name},user_id={uid})",
|
|
387
478
|
))
|
|
388
479
|
return result
|
|
480
|
+
elif name == "generate_image":
|
|
481
|
+
prompt = args.get("prompt", "").strip()
|
|
482
|
+
if not prompt:
|
|
483
|
+
return "缺少图片描述,请提供要生成的画面内容"
|
|
484
|
+
size_desc = args.get("size", "").strip()
|
|
485
|
+
reference_id = args.get("reference_cache_id", "").strip()
|
|
486
|
+
uid = args.get("user_id", self._current_user_id)
|
|
487
|
+
logger.info(f"[生图] 开始: prompt={prompt[:60]}, size={size_desc or '默认方形'}, reference={reference_id or '无'}, user_id={uid}")
|
|
488
|
+
# 防重复:立即写自述,后台完成后另写一条结果自述
|
|
489
|
+
self.recent_messages.append(ChatMessage(
|
|
490
|
+
time=datetime.now(), user_name=self.bot_name,
|
|
491
|
+
content=f"已开始生成图片「{prompt}」",
|
|
492
|
+
))
|
|
493
|
+
task = asyncio.create_task(self._generate_image_bg(prompt, size_desc, uid, reference_id))
|
|
494
|
+
self._image_gen_tasks.add(task)
|
|
495
|
+
task.add_done_callback(self._image_gen_tasks.discard)
|
|
496
|
+
return "图片生成任务已启动,完成后会自动发到群里;生成需要一点时间,期间请不要重复提交相同的生图请求"
|
|
389
497
|
return f"未知工具: {name}"
|
|
390
498
|
|
|
391
499
|
return await self.llm.chat_with_tools(
|
|
@@ -394,6 +502,86 @@ class MessageProcessor:
|
|
|
394
502
|
first_call_json=self.config.aigfm_llm_tools_json_strict,
|
|
395
503
|
)
|
|
396
504
|
|
|
505
|
+
async def _generate_image_bg(self, prompt: str, size_desc: str, uid: int, reference_id: str = ""):
|
|
506
|
+
"""后台生图任务:生成 → 发送到群 → VLM 描述 → 写自述(LLM 下一次感知)"""
|
|
507
|
+
try:
|
|
508
|
+
if not (self._bot and self.image_gen):
|
|
509
|
+
self.recent_messages.append(ChatMessage(
|
|
510
|
+
time=datetime.now(), user_name=self.bot_name,
|
|
511
|
+
content="图片生成失败:生图功能未就绪",
|
|
512
|
+
))
|
|
513
|
+
return
|
|
514
|
+
size = _normalize_size(size_desc, self.config.aigfm_image_gen_max_size,
|
|
515
|
+
self.config.aigfm_image_gen_min_size)
|
|
516
|
+
# 参考图:先查本群缓存索引,未命中则按 id 从磁盘兜底(clear_cache 只清索引不清文件)
|
|
517
|
+
reference_images: list[str] | None = None
|
|
518
|
+
reference_note = ""
|
|
519
|
+
if reference_id:
|
|
520
|
+
ref_path = None
|
|
521
|
+
info = self.memes.get_cache_info(int(self.group_id), reference_id)
|
|
522
|
+
if info and info.get("path") and Path(info["path"]).is_file():
|
|
523
|
+
ref_path = info["path"]
|
|
524
|
+
else:
|
|
525
|
+
disk_path = self.memes.get_cache_file(reference_id)
|
|
526
|
+
if disk_path:
|
|
527
|
+
ref_path = disk_path
|
|
528
|
+
if ref_path:
|
|
529
|
+
async with await anyio.open_file(ref_path, "rb") as f:
|
|
530
|
+
ref_bytes = await f.read()
|
|
531
|
+
ext = Path(ref_path).suffix.lstrip(".").lower() or "png"
|
|
532
|
+
reference_images = [f"data:image/{ext};base64,{base64.b64encode(ref_bytes).decode()}"]
|
|
533
|
+
else:
|
|
534
|
+
logger.warning(f"[生图] 参考图不可用: {reference_id},改为普通生成")
|
|
535
|
+
reference_note = "(参考图不可用,已改为普通生成)"
|
|
536
|
+
result = await self.image_gen.generate(
|
|
537
|
+
prompt, size,
|
|
538
|
+
reference_images=reference_images,
|
|
539
|
+
watermark=self.config.aigfm_image_gen_watermark,
|
|
540
|
+
)
|
|
541
|
+
if not result:
|
|
542
|
+
self.recent_messages.append(ChatMessage(
|
|
543
|
+
time=datetime.now(), user_name=self.bot_name,
|
|
544
|
+
content=f"图片生成失败:服务未返回图片(「{prompt}」)",
|
|
545
|
+
))
|
|
546
|
+
return
|
|
547
|
+
# 取图片数据(仅返回 url 时下载)
|
|
548
|
+
b64 = result.get("b64")
|
|
549
|
+
if not b64 and result.get("url"):
|
|
550
|
+
async with httpx.AsyncClient() as client:
|
|
551
|
+
resp = await client.get(result["url"], timeout=30)
|
|
552
|
+
resp.raise_for_status()
|
|
553
|
+
b64 = base64.b64encode(resp.content).decode()
|
|
554
|
+
if not b64:
|
|
555
|
+
self.recent_messages.append(ChatMessage(
|
|
556
|
+
time=datetime.now(), user_name=self.bot_name,
|
|
557
|
+
content="图片生成失败:无法获取图片数据",
|
|
558
|
+
))
|
|
559
|
+
return
|
|
560
|
+
await self._bot.send_msg(
|
|
561
|
+
message_type="group", group_id=int(self.group_id),
|
|
562
|
+
message=Message(MessageSegment.image(f"base64://{b64}")),
|
|
563
|
+
)
|
|
564
|
+
# VLM 描述(本插件自己发的图不会被钩子捕获,需主动描述让 LLM 感知结果)
|
|
565
|
+
desc_text = ""
|
|
566
|
+
if self.image_handler:
|
|
567
|
+
try:
|
|
568
|
+
info = await self.image_handler.describe(b64, is_sticker=False)
|
|
569
|
+
if info and info.description:
|
|
570
|
+
desc_text = f",内容描述: {info.description}"
|
|
571
|
+
except Exception:
|
|
572
|
+
desc_text = ""
|
|
573
|
+
logger.success(f"[生图] 已生成并发送: {prompt[:60]} ({size})")
|
|
574
|
+
self.recent_messages.append(ChatMessage(
|
|
575
|
+
time=datetime.now(), user_name=self.bot_name,
|
|
576
|
+
content=f"图片已生成:「{prompt}」({size}){desc_text}{reference_note}",
|
|
577
|
+
))
|
|
578
|
+
except Exception as e:
|
|
579
|
+
logger.error(f"[生图] 失败: {e}")
|
|
580
|
+
self.recent_messages.append(ChatMessage(
|
|
581
|
+
time=datetime.now(), user_name=self.bot_name,
|
|
582
|
+
content=f"图片生成失败:{e}",
|
|
583
|
+
))
|
|
584
|
+
|
|
397
585
|
async def _save_memes(self, save_meme: list, cached_stickers: list[dict]):
|
|
398
586
|
current_ids = {s["id"] for s in cached_stickers}
|
|
399
587
|
for item in save_meme if isinstance(save_meme, list) else [save_meme]:
|
|
@@ -274,11 +274,12 @@ def build_prompt(
|
|
|
274
274
|
```json
|
|
275
275
|
{short_term_str}
|
|
276
276
|
```
|
|
277
|
-
|
|
278
|
-
- modify
|
|
279
|
-
- delete
|
|
277
|
+
这是你对**近期对话**的临时记忆,需要频繁维护:对话有新进展→修改;话题结束或被取代→删除;重要且长期有效的信息→**提升到长期记忆**(群友的个人信息写入群友信息,群规/重大事件写入长期记忆),写入后从短期记忆删除。
|
|
278
|
+
- modify:对话有新进展时直接更新已有条目(优先使用,如约定的时间/地点变了就改写原条目)
|
|
279
|
+
- delete:已结束的话题、已解决的问题、被新对话取代的旧要点(积极使用)
|
|
280
280
|
- add:仅当列表中没有相关内容时才添加
|
|
281
281
|
index 对应上面数组的下标(从 0 开始)。
|
|
282
|
+
短期记忆是临时的:保留当前话题的要点即可(建议不超过 8 条),超过时删除最旧或最不相关的,让列表跟随对话流动。
|
|
282
283
|
值得记:本群近期的话题、群友的临时约定(如"明天一起开黑")、本次对话中值得留存的要点。
|
|
283
284
|
|
|
284
285
|
### 长期记忆
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nonebot-plugin-aigf-master
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: 群聊特化LLM聊天机器人(增强版),具有记忆、表情包、跨插件调用和跨bot通信能力
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: <4.0,>=3.10
|
|
@@ -52,6 +52,9 @@ Dynamic: license-file
|
|
|
52
52
|
- 💬 **口语化回复**:像平时聊天一样自然,回复长度有变化,允许敷衍
|
|
53
53
|
- 📨 **消息合并**:同一用户连续消息自动合并,避免碎片化上下文
|
|
54
54
|
- ⏱️ **不完整消息检测**:纯 @ 消息或连续发送时自动延长等待时间
|
|
55
|
+
- 🕐 **消息时间标注**:可选为 LLM 看到的消息加上 `[MM-DD HH:MM]` 时间前缀(`AIGFM_SHOW_MESSAGE_TIME`)
|
|
56
|
+
- 📄 **完整消息类型**:合并聊天记录、小程序/卡片、XML 消息均可解析与捕获
|
|
57
|
+
- 🎨 **AI 生图**:LLM 生成图片自动发到群里,尺寸由 LLM 决定(程序限最大尺寸),支持参考图改绘(回复图片消息或从聊天记录选图)
|
|
55
58
|
- 🔒 **强制 JSON 输出**:确保 LLM 始终返回正确格式
|
|
56
59
|
- ⚡ **轻量高效**:单次 LLM 调用完成对话 + 记忆管理 + 命令学习
|
|
57
60
|
|
|
@@ -106,6 +109,14 @@ AIGFM_VLM_ENABLED=true # 是否启用VLM(仅 vlm 模式有效
|
|
|
106
109
|
AIGFM_VLM_MODEL="..." # VLM 模型名称(vlm 模式必填)
|
|
107
110
|
AIGFM_VLM_BASE_URL="https://..." # VLM API 地址
|
|
108
111
|
AIGFM_VLM_API_KEY="" # VLM API Key(为空时使用 chat 的 key)
|
|
112
|
+
|
|
113
|
+
# --- AI 生图 ---
|
|
114
|
+
AIGFM_IMAGE_GEN_ENABLED=false # 是否启用 AI 生图(默认 false)
|
|
115
|
+
AIGFM_IMAGE_GEN_MODEL="..." # 生图模型名称(豆包/Ark 示例: doubao-seedream-5-0-lite-260128)
|
|
116
|
+
AIGFM_IMAGE_GEN_BASE_URL="https://..." # 生图 API 地址(OpenAI 兼容 images API;豆包示例 https://ark.cn-beijing.volces.com/api/v3)
|
|
117
|
+
AIGFM_IMAGE_GEN_API_KEY="" # 生图 API Key(为空时使用 chat 的 key)
|
|
118
|
+
AIGFM_IMAGE_GEN_MAX_SIZE="1024x1024" # 生成最大尺寸限制(宽x高,LLM 可指定比例/尺寸,程序自动缩放到不超过该上限)
|
|
119
|
+
AIGFM_IMAGE_GEN_WATERMARK=false # 生图水印(参考豆包类服务,仅参考图时传入,默认 false)
|
|
109
120
|
```
|
|
110
121
|
|
|
111
122
|
### 可选
|
|
@@ -122,6 +133,7 @@ AIGFM_BATCH_TIMEOUT=30.0 # 距最后一条消息多少秒后触
|
|
|
122
133
|
AIGFM_INCOMPLETE_TIMEOUT=40.0 # 消息可能不完整时的等待时间/秒(默认 40.0;替代默认 30s 生效,非叠加)
|
|
123
134
|
AIGFM_RECENT_MESSAGES=20 # prompt 中包含的最近历史消息条数(默认 20,最大 50)
|
|
124
135
|
AIGFM_MERGE_WINDOW=5.0 # 消息合并时间窗口/秒(默认 5.0)
|
|
136
|
+
AIGFM_SHOW_MESSAGE_TIME=false # LLM 看到的消息是否带 [MM-DD HH:MM] 时间前缀(默认 false)
|
|
125
137
|
|
|
126
138
|
# --- LLM ---
|
|
127
139
|
AIGFM_LLM_JSON_MODE=true # 是否强制 LLM 输出 JSON(默认 true)
|
|
@@ -222,6 +234,9 @@ LLM 在聊天记录中看到:[插件名]: 插件的输出
|
|
|
222
234
|
|------|---------|
|
|
223
235
|
| 文本 | 直接以 `[来源]: '内容'` 进入上下文 |
|
|
224
236
|
| 图片 | 先经 VLM 转成文字描述,再以 `[图片] 描述` 进入上下文(`AIGFM_CAPTURE_IMAGES=false` 时不捕获) |
|
|
237
|
+
| 合并聊天记录 | 以 `[收到一条合并聊天记录]` 进入上下文 |
|
|
238
|
+
| 小程序/卡片 | 以 `[小程序/卡片: 标题, 描述]` 进入上下文 |
|
|
239
|
+
| XML | 以 `[收到一条XML消息]` 进入上下文 |
|
|
225
240
|
|
|
226
241
|
图片的来源字段依次支持:
|
|
227
242
|
|
|
@@ -6,6 +6,7 @@ nonebot_plugin_aigf_master/api_hooks.py
|
|
|
6
6
|
nonebot_plugin_aigf_master/command_learner.py
|
|
7
7
|
nonebot_plugin_aigf_master/config.py
|
|
8
8
|
nonebot_plugin_aigf_master/context_bus.py
|
|
9
|
+
nonebot_plugin_aigf_master/image_gen_client.py
|
|
9
10
|
nonebot_plugin_aigf_master/image_handler.py
|
|
10
11
|
nonebot_plugin_aigf_master/llm_client.py
|
|
11
12
|
nonebot_plugin_aigf_master/meme_store.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
|