AstrBot 4.7.4__py3-none-any.whl → 4.9.0__py3-none-any.whl
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.
- astrbot/cli/__init__.py +1 -1
- astrbot/core/agent/runners/tool_loop_agent_runner.py +0 -1
- astrbot/core/agent/tool.py +7 -2
- astrbot/core/astr_agent_run_util.py +15 -1
- astrbot/core/astr_agent_tool_exec.py +5 -1
- astrbot/core/config/astrbot_config.py +4 -0
- astrbot/core/config/default.py +116 -1
- astrbot/core/core_lifecycle.py +1 -1
- astrbot/core/db/__init__.py +32 -4
- astrbot/core/db/migration/migra_3_to_4.py +2 -0
- astrbot/core/db/migration/sqlite_v3.py +6 -4
- astrbot/core/db/po.py +16 -15
- astrbot/core/db/sqlite.py +56 -1
- astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +2 -0
- astrbot/core/event_bus.py +6 -1
- astrbot/core/knowledge_base/retrieval/manager.py +5 -1
- astrbot/core/log.py +2 -1
- astrbot/core/message/components.py +9 -3
- astrbot/core/persona_mgr.py +2 -2
- astrbot/core/pipeline/content_safety_check/stage.py +1 -1
- astrbot/core/pipeline/context_utils.py +2 -1
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +1 -1
- astrbot/core/pipeline/process_stage/method/star_request.py +1 -2
- astrbot/core/pipeline/process_stage/stage.py +1 -1
- astrbot/core/pipeline/respond/stage.py +4 -2
- astrbot/core/pipeline/result_decorate/stage.py +68 -21
- astrbot/core/pipeline/scheduler.py +5 -1
- astrbot/core/pipeline/waking_check/stage.py +10 -0
- astrbot/core/platform/astr_message_event.py +5 -3
- astrbot/core/platform/astrbot_message.py +2 -2
- astrbot/core/platform/manager.py +71 -9
- astrbot/core/platform/platform.py +109 -4
- astrbot/core/platform/platform_metadata.py +1 -1
- astrbot/core/platform/register.py +1 -0
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +8 -6
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +13 -8
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +28 -22
- astrbot/core/platform/sources/dingtalk/dingtalk_event.py +5 -2
- astrbot/core/platform/sources/discord/client.py +16 -4
- astrbot/core/platform/sources/discord/components.py +2 -2
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +53 -26
- astrbot/core/platform/sources/discord/discord_platform_event.py +29 -8
- astrbot/core/platform/sources/lark/lark_adapter.py +178 -22
- astrbot/core/platform/sources/lark/lark_event.py +39 -4
- astrbot/core/platform/sources/lark/server.py +206 -0
- astrbot/core/platform/sources/misskey/misskey_adapter.py +3 -5
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +64 -18
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +14 -10
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -11
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +15 -2
- astrbot/core/platform/sources/satori/satori_adapter.py +1 -2
- astrbot/core/platform/sources/slack/client.py +58 -40
- astrbot/core/platform/sources/slack/slack_adapter.py +36 -16
- astrbot/core/platform/sources/slack/slack_event.py +11 -10
- astrbot/core/platform/sources/telegram/tg_adapter.py +2 -3
- astrbot/core/platform/sources/telegram/tg_event.py +23 -27
- astrbot/core/platform/sources/webchat/webchat_adapter.py +97 -31
- astrbot/core/platform/sources/webchat/webchat_event.py +35 -35
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +27 -11
- astrbot/core/platform/sources/wecom/wecom_adapter.py +75 -36
- astrbot/core/platform/sources/wecom/wecom_event.py +3 -3
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +26 -9
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +3 -3
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +27 -5
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +81 -35
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +11 -8
- astrbot/core/platform_message_history_mgr.py +3 -3
- astrbot/core/provider/func_tool_manager.py +3 -3
- astrbot/core/provider/manager.py +130 -74
- astrbot/core/provider/provider.py +12 -1
- astrbot/core/provider/sources/azure_tts_source.py +31 -9
- astrbot/core/provider/sources/bailian_rerank_source.py +4 -0
- astrbot/core/provider/sources/dashscope_tts.py +3 -2
- astrbot/core/provider/sources/edge_tts_source.py +1 -1
- astrbot/core/provider/sources/fishaudio_tts_api_source.py +5 -4
- astrbot/core/provider/sources/gemini_embedding_source.py +15 -5
- astrbot/core/provider/sources/gemini_source.py +12 -10
- astrbot/core/provider/sources/minimax_tts_api_source.py +4 -2
- astrbot/core/provider/sources/openai_embedding_source.py +2 -2
- astrbot/core/provider/sources/openai_source.py +4 -0
- astrbot/core/provider/sources/sensevoice_selfhosted_source.py +5 -2
- astrbot/core/provider/sources/vllm_rerank_source.py +1 -0
- astrbot/core/provider/sources/whisper_api_source.py +44 -12
- astrbot/core/provider/sources/whisper_selfhosted_source.py +6 -2
- astrbot/core/provider/sources/xinference_rerank_source.py +10 -2
- astrbot/core/star/context.py +2 -2
- astrbot/core/star/register/star_handler.py +22 -5
- astrbot/core/star/star_handler.py +85 -4
- astrbot/core/updator.py +3 -3
- astrbot/core/utils/io.py +1 -1
- astrbot/core/utils/session_waiter.py +17 -10
- astrbot/core/utils/shared_preferences.py +32 -0
- astrbot/core/utils/t2i/__init__.py +2 -2
- astrbot/core/utils/t2i/local_strategy.py +25 -31
- astrbot/core/utils/tencent_record_helper.py +2 -2
- astrbot/core/utils/version_comparator.py +6 -3
- astrbot/core/utils/webhook_utils.py +66 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/chat.py +311 -76
- astrbot/dashboard/routes/config.py +14 -5
- astrbot/dashboard/routes/knowledge_base.py +254 -79
- astrbot/dashboard/routes/log.py +13 -8
- astrbot/dashboard/routes/platform.py +100 -0
- astrbot/dashboard/routes/plugin.py +108 -51
- astrbot/dashboard/routes/route.py +2 -0
- astrbot/dashboard/server.py +9 -4
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/METADATA +50 -37
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/RECORD +111 -108
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/WHEEL +0 -0
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/entry_points.txt +0 -0
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/licenses/LICENSE +0 -0
astrbot/dashboard/routes/chat.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
|
+
import mimetypes
|
|
3
4
|
import os
|
|
4
5
|
import uuid
|
|
5
6
|
from contextlib import asynccontextmanager
|
|
7
|
+
from typing import cast
|
|
6
8
|
|
|
7
9
|
from quart import Response as QuartResponse
|
|
8
|
-
from quart import g, make_response, request
|
|
10
|
+
from quart import g, make_response, request, send_file
|
|
9
11
|
|
|
10
12
|
from astrbot.core import logger
|
|
11
13
|
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
|
@@ -44,7 +46,7 @@ class ChatRoute(Route):
|
|
|
44
46
|
self.update_session_display_name,
|
|
45
47
|
),
|
|
46
48
|
"/chat/get_file": ("GET", self.get_file),
|
|
47
|
-
"/chat/
|
|
49
|
+
"/chat/get_attachment": ("GET", self.get_attachment),
|
|
48
50
|
"/chat/post_file": ("POST", self.post_file),
|
|
49
51
|
}
|
|
50
52
|
self.core_lifecycle = core_lifecycle
|
|
@@ -73,52 +75,184 @@ class ChatRoute(Route):
|
|
|
73
75
|
if not real_file_path.startswith(real_imgs_dir):
|
|
74
76
|
return Response().error("Invalid file path").__dict__
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return QuartResponse(f.read(), mimetype="image/jpeg")
|
|
83
|
-
return QuartResponse(f.read())
|
|
78
|
+
filename_ext = os.path.splitext(filename)[1].lower()
|
|
79
|
+
if filename_ext == ".wav":
|
|
80
|
+
return await send_file(real_file_path, mimetype="audio/wav")
|
|
81
|
+
if filename_ext[1:] in self.supported_imgs:
|
|
82
|
+
return await send_file(real_file_path, mimetype="image/jpeg")
|
|
83
|
+
return await send_file(real_file_path)
|
|
84
84
|
|
|
85
85
|
except (FileNotFoundError, OSError):
|
|
86
86
|
return Response().error("File access error").__dict__
|
|
87
87
|
|
|
88
|
-
async def
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
async def get_attachment(self):
|
|
89
|
+
"""Get attachment file by attachment_id."""
|
|
90
|
+
attachment_id = request.args.get("attachment_id")
|
|
91
|
+
if not attachment_id:
|
|
92
|
+
return Response().error("Missing key: attachment_id").__dict__
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
try:
|
|
95
|
+
attachment = await self.db.get_attachment_by_id(attachment_id)
|
|
96
|
+
if not attachment:
|
|
97
|
+
return Response().error("Attachment not found").__dict__
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
file_path = attachment.path
|
|
100
|
+
real_file_path = os.path.realpath(file_path)
|
|
101
|
+
|
|
102
|
+
return await send_file(real_file_path, mimetype=attachment.mime_type)
|
|
103
|
+
|
|
104
|
+
except (FileNotFoundError, OSError):
|
|
105
|
+
return Response().error("File access error").__dict__
|
|
99
106
|
|
|
100
107
|
async def post_file(self):
|
|
108
|
+
"""Upload a file and create an attachment record, return attachment_id."""
|
|
101
109
|
post_data = await request.files
|
|
102
110
|
if "file" not in post_data:
|
|
103
111
|
return Response().error("Missing key: file").__dict__
|
|
104
112
|
|
|
105
113
|
file = post_data["file"]
|
|
106
|
-
filename = f"{uuid.uuid4()!s}"
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
filename = file.filename or f"{uuid.uuid4()!s}"
|
|
115
|
+
content_type = file.content_type or "application/octet-stream"
|
|
116
|
+
|
|
117
|
+
# 根据 content_type 判断文件类型并添加扩展名
|
|
118
|
+
if content_type.startswith("image"):
|
|
119
|
+
attach_type = "image"
|
|
120
|
+
elif content_type.startswith("audio"):
|
|
121
|
+
attach_type = "record"
|
|
122
|
+
elif content_type.startswith("video"):
|
|
123
|
+
attach_type = "video"
|
|
124
|
+
else:
|
|
125
|
+
attach_type = "file"
|
|
110
126
|
|
|
111
127
|
path = os.path.join(self.imgs_dir, filename)
|
|
112
128
|
await file.save(path)
|
|
113
129
|
|
|
114
|
-
|
|
130
|
+
# 创建 attachment 记录
|
|
131
|
+
attachment = await self.db.insert_attachment(
|
|
132
|
+
path=path,
|
|
133
|
+
type=attach_type,
|
|
134
|
+
mime_type=content_type,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if not attachment:
|
|
138
|
+
return Response().error("Failed to create attachment").__dict__
|
|
139
|
+
|
|
140
|
+
filename = os.path.basename(attachment.path)
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
Response()
|
|
144
|
+
.ok(
|
|
145
|
+
data={
|
|
146
|
+
"attachment_id": attachment.attachment_id,
|
|
147
|
+
"filename": filename,
|
|
148
|
+
"type": attach_type,
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
.__dict__
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
async def _build_user_message_parts(self, message: str | list) -> list[dict]:
|
|
155
|
+
"""构建用户消息的部分列表
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
message: 文本消息 (str) 或消息段列表 (list)
|
|
159
|
+
"""
|
|
160
|
+
parts = []
|
|
161
|
+
|
|
162
|
+
if isinstance(message, list):
|
|
163
|
+
for part in message:
|
|
164
|
+
part_type = part.get("type")
|
|
165
|
+
if part_type == "plain":
|
|
166
|
+
parts.append({"type": "plain", "text": part.get("text", "")})
|
|
167
|
+
elif part_type == "reply":
|
|
168
|
+
parts.append(
|
|
169
|
+
{"type": "reply", "message_id": part.get("message_id")}
|
|
170
|
+
)
|
|
171
|
+
elif attachment_id := part.get("attachment_id"):
|
|
172
|
+
attachment = await self.db.get_attachment_by_id(attachment_id)
|
|
173
|
+
if attachment:
|
|
174
|
+
parts.append(
|
|
175
|
+
{
|
|
176
|
+
"type": attachment.type,
|
|
177
|
+
"attachment_id": attachment.attachment_id,
|
|
178
|
+
"filename": os.path.basename(attachment.path),
|
|
179
|
+
"path": attachment.path, # will be deleted
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
return parts
|
|
183
|
+
|
|
184
|
+
if message:
|
|
185
|
+
parts.append({"type": "plain", "text": message})
|
|
186
|
+
|
|
187
|
+
return parts
|
|
188
|
+
|
|
189
|
+
async def _create_attachment_from_file(
|
|
190
|
+
self, filename: str, attach_type: str
|
|
191
|
+
) -> dict | None:
|
|
192
|
+
"""从本地文件创建 attachment 并返回消息部分
|
|
193
|
+
|
|
194
|
+
用于处理 bot 回复中的媒体文件
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
filename: 存储的文件名
|
|
198
|
+
attach_type: 附件类型 (image, record, file, video)
|
|
199
|
+
"""
|
|
200
|
+
file_path = os.path.join(self.imgs_dir, os.path.basename(filename))
|
|
201
|
+
if not os.path.exists(file_path):
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
# guess mime type
|
|
205
|
+
mime_type, _ = mimetypes.guess_type(filename)
|
|
206
|
+
if not mime_type:
|
|
207
|
+
mime_type = "application/octet-stream"
|
|
208
|
+
|
|
209
|
+
# insert attachment
|
|
210
|
+
attachment = await self.db.insert_attachment(
|
|
211
|
+
path=file_path,
|
|
212
|
+
type=attach_type,
|
|
213
|
+
mime_type=mime_type,
|
|
214
|
+
)
|
|
215
|
+
if not attachment:
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
"type": attach_type,
|
|
220
|
+
"attachment_id": attachment.attachment_id,
|
|
221
|
+
"filename": os.path.basename(file_path),
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async def _save_bot_message(
|
|
225
|
+
self,
|
|
226
|
+
webchat_conv_id: str,
|
|
227
|
+
text: str,
|
|
228
|
+
media_parts: list,
|
|
229
|
+
reasoning: str,
|
|
230
|
+
):
|
|
231
|
+
"""保存 bot 消息到历史记录,返回保存的记录"""
|
|
232
|
+
bot_message_parts = []
|
|
233
|
+
if text:
|
|
234
|
+
bot_message_parts.append({"type": "plain", "text": text})
|
|
235
|
+
bot_message_parts.extend(media_parts)
|
|
236
|
+
|
|
237
|
+
new_his = {"type": "bot", "message": bot_message_parts}
|
|
238
|
+
if reasoning:
|
|
239
|
+
new_his["reasoning"] = reasoning
|
|
240
|
+
|
|
241
|
+
record = await self.platform_history_mgr.insert(
|
|
242
|
+
platform_id="webchat",
|
|
243
|
+
user_id=webchat_conv_id,
|
|
244
|
+
content=new_his,
|
|
245
|
+
sender_id="bot",
|
|
246
|
+
sender_name="bot",
|
|
247
|
+
)
|
|
248
|
+
return record
|
|
115
249
|
|
|
116
250
|
async def chat(self):
|
|
117
251
|
username = g.get("username", "guest")
|
|
118
252
|
|
|
119
253
|
post_data = await request.json
|
|
120
|
-
if "message" not in post_data and "
|
|
121
|
-
return Response().error("Missing key: message or
|
|
254
|
+
if "message" not in post_data and "files" not in post_data:
|
|
255
|
+
return Response().error("Missing key: message or files").__dict__
|
|
122
256
|
|
|
123
257
|
if "session_id" not in post_data and "conversation_id" not in post_data:
|
|
124
258
|
return (
|
|
@@ -126,44 +260,40 @@ class ChatRoute(Route):
|
|
|
126
260
|
)
|
|
127
261
|
|
|
128
262
|
message = post_data["message"]
|
|
129
|
-
# conversation_id = post_data["conversation_id"]
|
|
130
263
|
session_id = post_data.get("session_id", post_data.get("conversation_id"))
|
|
131
|
-
image_url = post_data.get("image_url")
|
|
132
|
-
audio_url = post_data.get("audio_url")
|
|
133
264
|
selected_provider = post_data.get("selected_provider")
|
|
134
265
|
selected_model = post_data.get("selected_model")
|
|
135
|
-
enable_streaming = post_data.get("enable_streaming", True)
|
|
266
|
+
enable_streaming = post_data.get("enable_streaming", True)
|
|
136
267
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.
|
|
141
|
-
|
|
268
|
+
# 检查消息是否为空
|
|
269
|
+
if isinstance(message, list):
|
|
270
|
+
has_content = any(
|
|
271
|
+
part.get("type") in ("plain", "image", "record", "file", "video")
|
|
272
|
+
for part in message
|
|
142
273
|
)
|
|
274
|
+
if not has_content:
|
|
275
|
+
return (
|
|
276
|
+
Response()
|
|
277
|
+
.error("Message content is empty (reply only is not allowed)")
|
|
278
|
+
.__dict__
|
|
279
|
+
)
|
|
280
|
+
elif not message:
|
|
281
|
+
return Response().error("Message are both empty").__dict__
|
|
282
|
+
|
|
143
283
|
if not session_id:
|
|
144
284
|
return Response().error("session_id is empty").__dict__
|
|
145
285
|
|
|
146
|
-
# 追加用户消息
|
|
147
286
|
webchat_conv_id = session_id
|
|
148
|
-
|
|
149
|
-
# 获取会话特定的队列
|
|
150
287
|
back_queue = webchat_queue_mgr.get_or_create_back_queue(webchat_conv_id)
|
|
151
288
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
new_his["image_url"] = image_url
|
|
155
|
-
if audio_url:
|
|
156
|
-
new_his["audio_url"] = audio_url
|
|
157
|
-
await self.platform_history_mgr.insert(
|
|
158
|
-
platform_id="webchat",
|
|
159
|
-
user_id=webchat_conv_id,
|
|
160
|
-
content=new_his,
|
|
161
|
-
sender_id=username,
|
|
162
|
-
sender_name=username,
|
|
163
|
-
)
|
|
289
|
+
# 构建用户消息段(包含 path 用于传递给 adapter)
|
|
290
|
+
message_parts = await self._build_user_message_parts(message)
|
|
164
291
|
|
|
165
292
|
async def stream():
|
|
166
293
|
client_disconnected = False
|
|
294
|
+
accumulated_parts = []
|
|
295
|
+
accumulated_text = ""
|
|
296
|
+
accumulated_reasoning = ""
|
|
167
297
|
|
|
168
298
|
try:
|
|
169
299
|
async with track_conversation(self.running_convs, webchat_conv_id):
|
|
@@ -182,16 +312,17 @@ class ChatRoute(Route):
|
|
|
182
312
|
continue
|
|
183
313
|
|
|
184
314
|
result_text = result["data"]
|
|
185
|
-
|
|
315
|
+
msg_type = result.get("type")
|
|
186
316
|
streaming = result.get("streaming", False)
|
|
187
317
|
|
|
318
|
+
# 发送 SSE 数据
|
|
188
319
|
try:
|
|
189
320
|
if not client_disconnected:
|
|
190
321
|
yield f"data: {json.dumps(result, ensure_ascii=False)}\n\n"
|
|
191
322
|
except Exception as e:
|
|
192
323
|
if not client_disconnected:
|
|
193
324
|
logger.debug(
|
|
194
|
-
f"[WebChat] 用户 {username} 断开聊天长连接。 {e}"
|
|
325
|
+
f"[WebChat] 用户 {username} 断开聊天长连接。 {e}"
|
|
195
326
|
)
|
|
196
327
|
client_disconnected = True
|
|
197
328
|
|
|
@@ -202,24 +333,68 @@ class ChatRoute(Route):
|
|
|
202
333
|
logger.debug(f"[WebChat] 用户 {username} 断开聊天长连接。")
|
|
203
334
|
client_disconnected = True
|
|
204
335
|
|
|
205
|
-
|
|
336
|
+
# 累积消息部分
|
|
337
|
+
if msg_type == "plain":
|
|
338
|
+
chain_type = result.get("chain_type", "normal")
|
|
339
|
+
if chain_type == "reasoning":
|
|
340
|
+
accumulated_reasoning += result_text
|
|
341
|
+
else:
|
|
342
|
+
accumulated_text += result_text
|
|
343
|
+
elif msg_type == "image":
|
|
344
|
+
filename = result_text.replace("[IMAGE]", "")
|
|
345
|
+
part = await self._create_attachment_from_file(
|
|
346
|
+
filename, "image"
|
|
347
|
+
)
|
|
348
|
+
if part:
|
|
349
|
+
accumulated_parts.append(part)
|
|
350
|
+
elif msg_type == "record":
|
|
351
|
+
filename = result_text.replace("[RECORD]", "")
|
|
352
|
+
part = await self._create_attachment_from_file(
|
|
353
|
+
filename, "record"
|
|
354
|
+
)
|
|
355
|
+
if part:
|
|
356
|
+
accumulated_parts.append(part)
|
|
357
|
+
elif msg_type == "file":
|
|
358
|
+
# 格式: [FILE]filename
|
|
359
|
+
filename = result_text.replace("[FILE]", "")
|
|
360
|
+
part = await self._create_attachment_from_file(
|
|
361
|
+
filename, "file"
|
|
362
|
+
)
|
|
363
|
+
if part:
|
|
364
|
+
accumulated_parts.append(part)
|
|
365
|
+
|
|
366
|
+
# 消息结束处理
|
|
367
|
+
if msg_type == "end":
|
|
206
368
|
break
|
|
207
369
|
elif (
|
|
208
|
-
(streaming and
|
|
370
|
+
(streaming and msg_type == "complete")
|
|
209
371
|
or not streaming
|
|
210
|
-
or
|
|
372
|
+
or msg_type == "break"
|
|
211
373
|
):
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
platform_id="webchat",
|
|
218
|
-
user_id=webchat_conv_id,
|
|
219
|
-
content=new_his,
|
|
220
|
-
sender_id="bot",
|
|
221
|
-
sender_name="bot",
|
|
374
|
+
saved_record = await self._save_bot_message(
|
|
375
|
+
webchat_conv_id,
|
|
376
|
+
accumulated_text,
|
|
377
|
+
accumulated_parts,
|
|
378
|
+
accumulated_reasoning,
|
|
222
379
|
)
|
|
380
|
+
# 发送保存的消息信息给前端
|
|
381
|
+
if saved_record and not client_disconnected:
|
|
382
|
+
saved_info = {
|
|
383
|
+
"type": "message_saved",
|
|
384
|
+
"data": {
|
|
385
|
+
"id": saved_record.id,
|
|
386
|
+
"created_at": saved_record.created_at.astimezone().isoformat(),
|
|
387
|
+
},
|
|
388
|
+
}
|
|
389
|
+
try:
|
|
390
|
+
yield f"data: {json.dumps(saved_info, ensure_ascii=False)}\n\n"
|
|
391
|
+
except Exception:
|
|
392
|
+
pass
|
|
393
|
+
# 重置累积变量 (对于 break 后的下一段消息)
|
|
394
|
+
if msg_type == "break":
|
|
395
|
+
accumulated_parts = []
|
|
396
|
+
accumulated_text = ""
|
|
397
|
+
accumulated_reasoning = ""
|
|
223
398
|
except BaseException as e:
|
|
224
399
|
logger.exception(f"WebChat stream unexpected error: {e}", exc_info=True)
|
|
225
400
|
|
|
@@ -230,9 +405,7 @@ class ChatRoute(Route):
|
|
|
230
405
|
username,
|
|
231
406
|
webchat_conv_id,
|
|
232
407
|
{
|
|
233
|
-
"message":
|
|
234
|
-
"image_url": image_url, # list
|
|
235
|
-
"audio_url": audio_url,
|
|
408
|
+
"message": message_parts,
|
|
236
409
|
"selected_provider": selected_provider,
|
|
237
410
|
"selected_model": selected_model,
|
|
238
411
|
"enable_streaming": enable_streaming,
|
|
@@ -240,14 +413,30 @@ class ChatRoute(Route):
|
|
|
240
413
|
),
|
|
241
414
|
)
|
|
242
415
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
{
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
416
|
+
message_parts_for_storage = []
|
|
417
|
+
for part in message_parts:
|
|
418
|
+
part_copy = {k: v for k, v in part.items() if k != "path"}
|
|
419
|
+
message_parts_for_storage.append(part_copy)
|
|
420
|
+
|
|
421
|
+
await self.platform_history_mgr.insert(
|
|
422
|
+
platform_id="webchat",
|
|
423
|
+
user_id=webchat_conv_id,
|
|
424
|
+
content={"type": "user", "message": message_parts_for_storage},
|
|
425
|
+
sender_id=username,
|
|
426
|
+
sender_name=username,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
response = cast(
|
|
430
|
+
QuartResponse,
|
|
431
|
+
await make_response(
|
|
432
|
+
stream(),
|
|
433
|
+
{
|
|
434
|
+
"Content-Type": "text/event-stream",
|
|
435
|
+
"Cache-Control": "no-cache",
|
|
436
|
+
"Transfer-Encoding": "chunked",
|
|
437
|
+
"Connection": "keep-alive",
|
|
438
|
+
},
|
|
439
|
+
),
|
|
251
440
|
)
|
|
252
441
|
response.timeout = None # fix SSE auto disconnect issue
|
|
253
442
|
return response
|
|
@@ -271,6 +460,17 @@ class ChatRoute(Route):
|
|
|
271
460
|
unified_msg_origin = f"{session.platform_id}:{message_type}:{session.platform_id}!{username}!{session_id}"
|
|
272
461
|
await self.conv_mgr.delete_conversations_by_user_id(unified_msg_origin)
|
|
273
462
|
|
|
463
|
+
# 获取消息历史中的所有附件 ID 并删除附件
|
|
464
|
+
history_list = await self.platform_history_mgr.get(
|
|
465
|
+
platform_id=session.platform_id,
|
|
466
|
+
user_id=session_id,
|
|
467
|
+
page=1,
|
|
468
|
+
page_size=100000, # 获取足够多的记录
|
|
469
|
+
)
|
|
470
|
+
attachment_ids = self._extract_attachment_ids(history_list)
|
|
471
|
+
if attachment_ids:
|
|
472
|
+
await self._delete_attachments(attachment_ids)
|
|
473
|
+
|
|
274
474
|
# 删除消息历史
|
|
275
475
|
await self.platform_history_mgr.delete(
|
|
276
476
|
platform_id=session.platform_id,
|
|
@@ -297,6 +497,41 @@ class ChatRoute(Route):
|
|
|
297
497
|
|
|
298
498
|
return Response().ok().__dict__
|
|
299
499
|
|
|
500
|
+
def _extract_attachment_ids(self, history_list) -> list[str]:
|
|
501
|
+
"""从消息历史中提取所有 attachment_id"""
|
|
502
|
+
attachment_ids = []
|
|
503
|
+
for history in history_list:
|
|
504
|
+
content = history.content
|
|
505
|
+
if not content or "message" not in content:
|
|
506
|
+
continue
|
|
507
|
+
message_parts = content.get("message", [])
|
|
508
|
+
for part in message_parts:
|
|
509
|
+
if isinstance(part, dict) and "attachment_id" in part:
|
|
510
|
+
attachment_ids.append(part["attachment_id"])
|
|
511
|
+
return attachment_ids
|
|
512
|
+
|
|
513
|
+
async def _delete_attachments(self, attachment_ids: list[str]):
|
|
514
|
+
"""删除附件(包括数据库记录和磁盘文件)"""
|
|
515
|
+
try:
|
|
516
|
+
attachments = await self.db.get_attachments(attachment_ids)
|
|
517
|
+
for attachment in attachments:
|
|
518
|
+
if not os.path.exists(attachment.path):
|
|
519
|
+
continue
|
|
520
|
+
try:
|
|
521
|
+
os.remove(attachment.path)
|
|
522
|
+
except OSError as e:
|
|
523
|
+
logger.warning(
|
|
524
|
+
f"Failed to delete attachment file {attachment.path}: {e}"
|
|
525
|
+
)
|
|
526
|
+
except Exception as e:
|
|
527
|
+
logger.warning(f"Failed to get attachments: {e}")
|
|
528
|
+
|
|
529
|
+
# 批量删除数据库记录
|
|
530
|
+
try:
|
|
531
|
+
await self.db.delete_attachments(attachment_ids)
|
|
532
|
+
except Exception as e:
|
|
533
|
+
logger.warning(f"Failed to delete attachments: {e}")
|
|
534
|
+
|
|
300
535
|
async def new_session(self):
|
|
301
536
|
"""Create a new Platform session (default: webchat)."""
|
|
302
537
|
username = g.get("username", "guest")
|
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
import inspect
|
|
3
3
|
import os
|
|
4
4
|
import traceback
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
7
|
from quart import request
|
|
7
8
|
|
|
@@ -20,11 +21,12 @@ from astrbot.core.platform.register import platform_cls_map, platform_registry
|
|
|
20
21
|
from astrbot.core.provider import Provider
|
|
21
22
|
from astrbot.core.provider.register import provider_registry
|
|
22
23
|
from astrbot.core.star.star import star_registry
|
|
24
|
+
from astrbot.core.utils.webhook_utils import ensure_platform_webhook_config
|
|
23
25
|
|
|
24
26
|
from .route import Response, Route, RouteContext
|
|
25
27
|
|
|
26
28
|
|
|
27
|
-
def try_cast(value:
|
|
29
|
+
def try_cast(value: Any, type_: str):
|
|
28
30
|
if type_ == "int":
|
|
29
31
|
try:
|
|
30
32
|
return int(value)
|
|
@@ -503,9 +505,9 @@ class ConfigRoute(Route):
|
|
|
503
505
|
if not isinstance(inst, EmbeddingProvider):
|
|
504
506
|
return Response().error("提供商不是 EmbeddingProvider 类型").__dict__
|
|
505
507
|
|
|
506
|
-
|
|
507
|
-
if
|
|
508
|
-
await
|
|
508
|
+
init_fn = getattr(inst, "initialize", None)
|
|
509
|
+
if inspect.iscoroutinefunction(init_fn):
|
|
510
|
+
await init_fn()
|
|
509
511
|
|
|
510
512
|
# 获取嵌入向量维度
|
|
511
513
|
vec = await inst.get_embedding("echo")
|
|
@@ -555,6 +557,10 @@ class ConfigRoute(Route):
|
|
|
555
557
|
|
|
556
558
|
async def post_new_platform(self):
|
|
557
559
|
new_platform_config = await request.json
|
|
560
|
+
|
|
561
|
+
# 如果是支持统一 webhook 模式的平台,生成 webhook_uuid
|
|
562
|
+
ensure_platform_webhook_config(new_platform_config)
|
|
563
|
+
|
|
558
564
|
self.config["platform"].append(new_platform_config)
|
|
559
565
|
try:
|
|
560
566
|
save_config(self.config, self.config, is_core=True)
|
|
@@ -584,6 +590,9 @@ class ConfigRoute(Route):
|
|
|
584
590
|
if not platform_id or not new_config:
|
|
585
591
|
return Response().error("参数错误").__dict__
|
|
586
592
|
|
|
593
|
+
# 如果是支持统一 webhook 模式的平台,且启用了统一 webhook 模式,确保有 webhook_uuid
|
|
594
|
+
ensure_platform_webhook_config(new_config)
|
|
595
|
+
|
|
587
596
|
for i, platform in enumerate(self.config["platform"]):
|
|
588
597
|
if platform["id"] == platform_id:
|
|
589
598
|
self.config["platform"][i] = new_config
|
|
@@ -758,7 +767,7 @@ class ConfigRoute(Route):
|
|
|
758
767
|
return {"metadata": CONFIG_METADATA_2, "config": config}
|
|
759
768
|
|
|
760
769
|
async def _get_plugin_config(self, plugin_name: str):
|
|
761
|
-
ret = {"metadata": None, "config": None}
|
|
770
|
+
ret: dict = {"metadata": None, "config": None}
|
|
762
771
|
|
|
763
772
|
for plugin_md in star_registry:
|
|
764
773
|
if plugin_md.name == plugin_name:
|