AstrBot 4.7.3__py3-none-any.whl → 4.8.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/message.py +21 -5
- astrbot/core/astr_agent_run_util.py +15 -1
- astrbot/core/config/default.py +113 -1
- astrbot/core/db/__init__.py +30 -1
- astrbot/core/db/sqlite.py +55 -1
- astrbot/core/message/components.py +6 -1
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +64 -5
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +1 -1
- astrbot/core/platform/manager.py +67 -9
- astrbot/core/platform/platform.py +99 -2
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +19 -5
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +5 -7
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +1 -2
- astrbot/core/platform/sources/lark/lark_adapter.py +1 -3
- astrbot/core/platform/sources/misskey/misskey_adapter.py +1 -2
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +2 -0
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +1 -3
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +32 -9
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +13 -1
- astrbot/core/platform/sources/satori/satori_adapter.py +1 -2
- astrbot/core/platform/sources/slack/client.py +50 -39
- astrbot/core/platform/sources/slack/slack_adapter.py +21 -7
- astrbot/core/platform/sources/slack/slack_event.py +3 -3
- astrbot/core/platform/sources/telegram/tg_adapter.py +4 -3
- astrbot/core/platform/sources/webchat/webchat_adapter.py +95 -29
- astrbot/core/platform/sources/webchat/webchat_event.py +33 -33
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +1 -2
- astrbot/core/platform/sources/wecom/wecom_adapter.py +51 -9
- astrbot/core/platform/sources/wecom/wecom_event.py +1 -1
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +26 -9
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +27 -5
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +52 -11
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +1 -1
- astrbot/core/platform_message_history_mgr.py +3 -3
- astrbot/core/provider/provider.py +35 -0
- astrbot/core/provider/sources/whisper_api_source.py +43 -11
- astrbot/core/utils/file_extract.py +23 -0
- astrbot/core/utils/tencent_record_helper.py +1 -1
- astrbot/core/utils/webhook_utils.py +47 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/chat.py +300 -70
- astrbot/dashboard/routes/config.py +32 -165
- astrbot/dashboard/routes/knowledge_base.py +1 -1
- astrbot/dashboard/routes/platform.py +100 -0
- astrbot/dashboard/routes/plugin.py +65 -6
- astrbot/dashboard/server.py +3 -1
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/METADATA +48 -37
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/RECORD +52 -49
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/WHEEL +0 -0
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/entry_points.txt +0 -0
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/licenses/LICENSE +0 -0
astrbot/dashboard/routes/chat.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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
|
|
6
7
|
|
|
7
|
-
from quart import
|
|
8
|
-
from quart import g, make_response, request
|
|
8
|
+
from quart import g, make_response, request, send_file
|
|
9
9
|
|
|
10
10
|
from astrbot.core import logger
|
|
11
11
|
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
|
@@ -44,7 +44,7 @@ class ChatRoute(Route):
|
|
|
44
44
|
self.update_session_display_name,
|
|
45
45
|
),
|
|
46
46
|
"/chat/get_file": ("GET", self.get_file),
|
|
47
|
-
"/chat/
|
|
47
|
+
"/chat/get_attachment": ("GET", self.get_attachment),
|
|
48
48
|
"/chat/post_file": ("POST", self.post_file),
|
|
49
49
|
}
|
|
50
50
|
self.core_lifecycle = core_lifecycle
|
|
@@ -73,52 +73,184 @@ class ChatRoute(Route):
|
|
|
73
73
|
if not real_file_path.startswith(real_imgs_dir):
|
|
74
74
|
return Response().error("Invalid file path").__dict__
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return QuartResponse(f.read(), mimetype="image/jpeg")
|
|
83
|
-
return QuartResponse(f.read())
|
|
76
|
+
filename_ext = os.path.splitext(filename)[1].lower()
|
|
77
|
+
if filename_ext == ".wav":
|
|
78
|
+
return await send_file(real_file_path, mimetype="audio/wav")
|
|
79
|
+
if filename_ext[1:] in self.supported_imgs:
|
|
80
|
+
return await send_file(real_file_path, mimetype="image/jpeg")
|
|
81
|
+
return await send_file(real_file_path)
|
|
84
82
|
|
|
85
83
|
except (FileNotFoundError, OSError):
|
|
86
84
|
return Response().error("File access error").__dict__
|
|
87
85
|
|
|
88
|
-
async def
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
async def get_attachment(self):
|
|
87
|
+
"""Get attachment file by attachment_id."""
|
|
88
|
+
attachment_id = request.args.get("attachment_id")
|
|
89
|
+
if not attachment_id:
|
|
90
|
+
return Response().error("Missing key: attachment_id").__dict__
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
try:
|
|
93
|
+
attachment = await self.db.get_attachment_by_id(attachment_id)
|
|
94
|
+
if not attachment:
|
|
95
|
+
return Response().error("Attachment not found").__dict__
|
|
97
96
|
|
|
98
|
-
|
|
97
|
+
file_path = attachment.path
|
|
98
|
+
real_file_path = os.path.realpath(file_path)
|
|
99
|
+
|
|
100
|
+
return await send_file(real_file_path, mimetype=attachment.mime_type)
|
|
101
|
+
|
|
102
|
+
except (FileNotFoundError, OSError):
|
|
103
|
+
return Response().error("File access error").__dict__
|
|
99
104
|
|
|
100
105
|
async def post_file(self):
|
|
106
|
+
"""Upload a file and create an attachment record, return attachment_id."""
|
|
101
107
|
post_data = await request.files
|
|
102
108
|
if "file" not in post_data:
|
|
103
109
|
return Response().error("Missing key: file").__dict__
|
|
104
110
|
|
|
105
111
|
file = post_data["file"]
|
|
106
|
-
filename = f"{uuid.uuid4()!s}"
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
filename = file.filename or f"{uuid.uuid4()!s}"
|
|
113
|
+
content_type = file.content_type or "application/octet-stream"
|
|
114
|
+
|
|
115
|
+
# 根据 content_type 判断文件类型并添加扩展名
|
|
116
|
+
if content_type.startswith("image"):
|
|
117
|
+
attach_type = "image"
|
|
118
|
+
elif content_type.startswith("audio"):
|
|
119
|
+
attach_type = "record"
|
|
120
|
+
elif content_type.startswith("video"):
|
|
121
|
+
attach_type = "video"
|
|
122
|
+
else:
|
|
123
|
+
attach_type = "file"
|
|
110
124
|
|
|
111
125
|
path = os.path.join(self.imgs_dir, filename)
|
|
112
126
|
await file.save(path)
|
|
113
127
|
|
|
114
|
-
|
|
128
|
+
# 创建 attachment 记录
|
|
129
|
+
attachment = await self.db.insert_attachment(
|
|
130
|
+
path=path,
|
|
131
|
+
type=attach_type,
|
|
132
|
+
mime_type=content_type,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if not attachment:
|
|
136
|
+
return Response().error("Failed to create attachment").__dict__
|
|
137
|
+
|
|
138
|
+
filename = os.path.basename(attachment.path)
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
Response()
|
|
142
|
+
.ok(
|
|
143
|
+
data={
|
|
144
|
+
"attachment_id": attachment.attachment_id,
|
|
145
|
+
"filename": filename,
|
|
146
|
+
"type": attach_type,
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
.__dict__
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
async def _build_user_message_parts(self, message: str | list) -> list[dict]:
|
|
153
|
+
"""构建用户消息的部分列表
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
message: 文本消息 (str) 或消息段列表 (list)
|
|
157
|
+
"""
|
|
158
|
+
parts = []
|
|
159
|
+
|
|
160
|
+
if isinstance(message, list):
|
|
161
|
+
for part in message:
|
|
162
|
+
part_type = part.get("type")
|
|
163
|
+
if part_type == "plain":
|
|
164
|
+
parts.append({"type": "plain", "text": part.get("text", "")})
|
|
165
|
+
elif part_type == "reply":
|
|
166
|
+
parts.append(
|
|
167
|
+
{"type": "reply", "message_id": part.get("message_id")}
|
|
168
|
+
)
|
|
169
|
+
elif attachment_id := part.get("attachment_id"):
|
|
170
|
+
attachment = await self.db.get_attachment_by_id(attachment_id)
|
|
171
|
+
if attachment:
|
|
172
|
+
parts.append(
|
|
173
|
+
{
|
|
174
|
+
"type": attachment.type,
|
|
175
|
+
"attachment_id": attachment.attachment_id,
|
|
176
|
+
"filename": os.path.basename(attachment.path),
|
|
177
|
+
"path": attachment.path, # will be deleted
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
return parts
|
|
181
|
+
|
|
182
|
+
if message:
|
|
183
|
+
parts.append({"type": "plain", "text": message})
|
|
184
|
+
|
|
185
|
+
return parts
|
|
186
|
+
|
|
187
|
+
async def _create_attachment_from_file(
|
|
188
|
+
self, filename: str, attach_type: str
|
|
189
|
+
) -> dict | None:
|
|
190
|
+
"""从本地文件创建 attachment 并返回消息部分
|
|
191
|
+
|
|
192
|
+
用于处理 bot 回复中的媒体文件
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
filename: 存储的文件名
|
|
196
|
+
attach_type: 附件类型 (image, record, file, video)
|
|
197
|
+
"""
|
|
198
|
+
file_path = os.path.join(self.imgs_dir, os.path.basename(filename))
|
|
199
|
+
if not os.path.exists(file_path):
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
# guess mime type
|
|
203
|
+
mime_type, _ = mimetypes.guess_type(filename)
|
|
204
|
+
if not mime_type:
|
|
205
|
+
mime_type = "application/octet-stream"
|
|
206
|
+
|
|
207
|
+
# insert attachment
|
|
208
|
+
attachment = await self.db.insert_attachment(
|
|
209
|
+
path=file_path,
|
|
210
|
+
type=attach_type,
|
|
211
|
+
mime_type=mime_type,
|
|
212
|
+
)
|
|
213
|
+
if not attachment:
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
"type": attach_type,
|
|
218
|
+
"attachment_id": attachment.attachment_id,
|
|
219
|
+
"filename": os.path.basename(file_path),
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async def _save_bot_message(
|
|
223
|
+
self,
|
|
224
|
+
webchat_conv_id: str,
|
|
225
|
+
text: str,
|
|
226
|
+
media_parts: list,
|
|
227
|
+
reasoning: str,
|
|
228
|
+
):
|
|
229
|
+
"""保存 bot 消息到历史记录,返回保存的记录"""
|
|
230
|
+
bot_message_parts = []
|
|
231
|
+
if text:
|
|
232
|
+
bot_message_parts.append({"type": "plain", "text": text})
|
|
233
|
+
bot_message_parts.extend(media_parts)
|
|
234
|
+
|
|
235
|
+
new_his = {"type": "bot", "message": bot_message_parts}
|
|
236
|
+
if reasoning:
|
|
237
|
+
new_his["reasoning"] = reasoning
|
|
238
|
+
|
|
239
|
+
record = await self.platform_history_mgr.insert(
|
|
240
|
+
platform_id="webchat",
|
|
241
|
+
user_id=webchat_conv_id,
|
|
242
|
+
content=new_his,
|
|
243
|
+
sender_id="bot",
|
|
244
|
+
sender_name="bot",
|
|
245
|
+
)
|
|
246
|
+
return record
|
|
115
247
|
|
|
116
248
|
async def chat(self):
|
|
117
249
|
username = g.get("username", "guest")
|
|
118
250
|
|
|
119
251
|
post_data = await request.json
|
|
120
|
-
if "message" not in post_data and "
|
|
121
|
-
return Response().error("Missing key: message or
|
|
252
|
+
if "message" not in post_data and "files" not in post_data:
|
|
253
|
+
return Response().error("Missing key: message or files").__dict__
|
|
122
254
|
|
|
123
255
|
if "session_id" not in post_data and "conversation_id" not in post_data:
|
|
124
256
|
return (
|
|
@@ -126,44 +258,40 @@ class ChatRoute(Route):
|
|
|
126
258
|
)
|
|
127
259
|
|
|
128
260
|
message = post_data["message"]
|
|
129
|
-
# conversation_id = post_data["conversation_id"]
|
|
130
261
|
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
262
|
selected_provider = post_data.get("selected_provider")
|
|
134
263
|
selected_model = post_data.get("selected_model")
|
|
135
|
-
enable_streaming = post_data.get("enable_streaming", True)
|
|
264
|
+
enable_streaming = post_data.get("enable_streaming", True)
|
|
136
265
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.
|
|
141
|
-
|
|
266
|
+
# 检查消息是否为空
|
|
267
|
+
if isinstance(message, list):
|
|
268
|
+
has_content = any(
|
|
269
|
+
part.get("type") in ("plain", "image", "record", "file", "video")
|
|
270
|
+
for part in message
|
|
142
271
|
)
|
|
272
|
+
if not has_content:
|
|
273
|
+
return (
|
|
274
|
+
Response()
|
|
275
|
+
.error("Message content is empty (reply only is not allowed)")
|
|
276
|
+
.__dict__
|
|
277
|
+
)
|
|
278
|
+
elif not message:
|
|
279
|
+
return Response().error("Message are both empty").__dict__
|
|
280
|
+
|
|
143
281
|
if not session_id:
|
|
144
282
|
return Response().error("session_id is empty").__dict__
|
|
145
283
|
|
|
146
|
-
# 追加用户消息
|
|
147
284
|
webchat_conv_id = session_id
|
|
148
|
-
|
|
149
|
-
# 获取会话特定的队列
|
|
150
285
|
back_queue = webchat_queue_mgr.get_or_create_back_queue(webchat_conv_id)
|
|
151
286
|
|
|
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
|
-
)
|
|
287
|
+
# 构建用户消息段(包含 path 用于传递给 adapter)
|
|
288
|
+
message_parts = await self._build_user_message_parts(message)
|
|
164
289
|
|
|
165
290
|
async def stream():
|
|
166
291
|
client_disconnected = False
|
|
292
|
+
accumulated_parts = []
|
|
293
|
+
accumulated_text = ""
|
|
294
|
+
accumulated_reasoning = ""
|
|
167
295
|
|
|
168
296
|
try:
|
|
169
297
|
async with track_conversation(self.running_convs, webchat_conv_id):
|
|
@@ -182,16 +310,17 @@ class ChatRoute(Route):
|
|
|
182
310
|
continue
|
|
183
311
|
|
|
184
312
|
result_text = result["data"]
|
|
185
|
-
|
|
313
|
+
msg_type = result.get("type")
|
|
186
314
|
streaming = result.get("streaming", False)
|
|
187
315
|
|
|
316
|
+
# 发送 SSE 数据
|
|
188
317
|
try:
|
|
189
318
|
if not client_disconnected:
|
|
190
319
|
yield f"data: {json.dumps(result, ensure_ascii=False)}\n\n"
|
|
191
320
|
except Exception as e:
|
|
192
321
|
if not client_disconnected:
|
|
193
322
|
logger.debug(
|
|
194
|
-
f"[WebChat] 用户 {username} 断开聊天长连接。 {e}"
|
|
323
|
+
f"[WebChat] 用户 {username} 断开聊天长连接。 {e}"
|
|
195
324
|
)
|
|
196
325
|
client_disconnected = True
|
|
197
326
|
|
|
@@ -202,24 +331,68 @@ class ChatRoute(Route):
|
|
|
202
331
|
logger.debug(f"[WebChat] 用户 {username} 断开聊天长连接。")
|
|
203
332
|
client_disconnected = True
|
|
204
333
|
|
|
205
|
-
|
|
334
|
+
# 累积消息部分
|
|
335
|
+
if msg_type == "plain":
|
|
336
|
+
chain_type = result.get("chain_type", "normal")
|
|
337
|
+
if chain_type == "reasoning":
|
|
338
|
+
accumulated_reasoning += result_text
|
|
339
|
+
else:
|
|
340
|
+
accumulated_text += result_text
|
|
341
|
+
elif msg_type == "image":
|
|
342
|
+
filename = result_text.replace("[IMAGE]", "")
|
|
343
|
+
part = await self._create_attachment_from_file(
|
|
344
|
+
filename, "image"
|
|
345
|
+
)
|
|
346
|
+
if part:
|
|
347
|
+
accumulated_parts.append(part)
|
|
348
|
+
elif msg_type == "record":
|
|
349
|
+
filename = result_text.replace("[RECORD]", "")
|
|
350
|
+
part = await self._create_attachment_from_file(
|
|
351
|
+
filename, "record"
|
|
352
|
+
)
|
|
353
|
+
if part:
|
|
354
|
+
accumulated_parts.append(part)
|
|
355
|
+
elif msg_type == "file":
|
|
356
|
+
# 格式: [FILE]filename
|
|
357
|
+
filename = result_text.replace("[FILE]", "")
|
|
358
|
+
part = await self._create_attachment_from_file(
|
|
359
|
+
filename, "file"
|
|
360
|
+
)
|
|
361
|
+
if part:
|
|
362
|
+
accumulated_parts.append(part)
|
|
363
|
+
|
|
364
|
+
# 消息结束处理
|
|
365
|
+
if msg_type == "end":
|
|
206
366
|
break
|
|
207
367
|
elif (
|
|
208
|
-
(streaming and
|
|
368
|
+
(streaming and msg_type == "complete")
|
|
209
369
|
or not streaming
|
|
210
|
-
or
|
|
370
|
+
or msg_type == "break"
|
|
211
371
|
):
|
|
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",
|
|
372
|
+
saved_record = await self._save_bot_message(
|
|
373
|
+
webchat_conv_id,
|
|
374
|
+
accumulated_text,
|
|
375
|
+
accumulated_parts,
|
|
376
|
+
accumulated_reasoning,
|
|
222
377
|
)
|
|
378
|
+
# 发送保存的消息信息给前端
|
|
379
|
+
if saved_record and not client_disconnected:
|
|
380
|
+
saved_info = {
|
|
381
|
+
"type": "message_saved",
|
|
382
|
+
"data": {
|
|
383
|
+
"id": saved_record.id,
|
|
384
|
+
"created_at": saved_record.created_at.astimezone().isoformat(),
|
|
385
|
+
},
|
|
386
|
+
}
|
|
387
|
+
try:
|
|
388
|
+
yield f"data: {json.dumps(saved_info, ensure_ascii=False)}\n\n"
|
|
389
|
+
except Exception:
|
|
390
|
+
pass
|
|
391
|
+
# 重置累积变量 (对于 break 后的下一段消息)
|
|
392
|
+
if msg_type == "break":
|
|
393
|
+
accumulated_parts = []
|
|
394
|
+
accumulated_text = ""
|
|
395
|
+
accumulated_reasoning = ""
|
|
223
396
|
except BaseException as e:
|
|
224
397
|
logger.exception(f"WebChat stream unexpected error: {e}", exc_info=True)
|
|
225
398
|
|
|
@@ -230,9 +403,7 @@ class ChatRoute(Route):
|
|
|
230
403
|
username,
|
|
231
404
|
webchat_conv_id,
|
|
232
405
|
{
|
|
233
|
-
"message":
|
|
234
|
-
"image_url": image_url, # list
|
|
235
|
-
"audio_url": audio_url,
|
|
406
|
+
"message": message_parts,
|
|
236
407
|
"selected_provider": selected_provider,
|
|
237
408
|
"selected_model": selected_model,
|
|
238
409
|
"enable_streaming": enable_streaming,
|
|
@@ -240,6 +411,19 @@ class ChatRoute(Route):
|
|
|
240
411
|
),
|
|
241
412
|
)
|
|
242
413
|
|
|
414
|
+
message_parts_for_storage = []
|
|
415
|
+
for part in message_parts:
|
|
416
|
+
part_copy = {k: v for k, v in part.items() if k != "path"}
|
|
417
|
+
message_parts_for_storage.append(part_copy)
|
|
418
|
+
|
|
419
|
+
await self.platform_history_mgr.insert(
|
|
420
|
+
platform_id="webchat",
|
|
421
|
+
user_id=webchat_conv_id,
|
|
422
|
+
content={"type": "user", "message": message_parts_for_storage},
|
|
423
|
+
sender_id=username,
|
|
424
|
+
sender_name=username,
|
|
425
|
+
)
|
|
426
|
+
|
|
243
427
|
response = await make_response(
|
|
244
428
|
stream(),
|
|
245
429
|
{
|
|
@@ -249,7 +433,7 @@ class ChatRoute(Route):
|
|
|
249
433
|
"Connection": "keep-alive",
|
|
250
434
|
},
|
|
251
435
|
)
|
|
252
|
-
response.timeout = None # fix SSE auto disconnect issue
|
|
436
|
+
response.timeout = None # fix SSE auto disconnect issue # pyright: ignore[reportAttributeAccessIssue]
|
|
253
437
|
return response
|
|
254
438
|
|
|
255
439
|
async def delete_webchat_session(self):
|
|
@@ -271,6 +455,17 @@ class ChatRoute(Route):
|
|
|
271
455
|
unified_msg_origin = f"{session.platform_id}:{message_type}:{session.platform_id}!{username}!{session_id}"
|
|
272
456
|
await self.conv_mgr.delete_conversations_by_user_id(unified_msg_origin)
|
|
273
457
|
|
|
458
|
+
# 获取消息历史中的所有附件 ID 并删除附件
|
|
459
|
+
history_list = await self.platform_history_mgr.get(
|
|
460
|
+
platform_id=session.platform_id,
|
|
461
|
+
user_id=session_id,
|
|
462
|
+
page=1,
|
|
463
|
+
page_size=100000, # 获取足够多的记录
|
|
464
|
+
)
|
|
465
|
+
attachment_ids = self._extract_attachment_ids(history_list)
|
|
466
|
+
if attachment_ids:
|
|
467
|
+
await self._delete_attachments(attachment_ids)
|
|
468
|
+
|
|
274
469
|
# 删除消息历史
|
|
275
470
|
await self.platform_history_mgr.delete(
|
|
276
471
|
platform_id=session.platform_id,
|
|
@@ -297,6 +492,41 @@ class ChatRoute(Route):
|
|
|
297
492
|
|
|
298
493
|
return Response().ok().__dict__
|
|
299
494
|
|
|
495
|
+
def _extract_attachment_ids(self, history_list) -> list[str]:
|
|
496
|
+
"""从消息历史中提取所有 attachment_id"""
|
|
497
|
+
attachment_ids = []
|
|
498
|
+
for history in history_list:
|
|
499
|
+
content = history.content
|
|
500
|
+
if not content or "message" not in content:
|
|
501
|
+
continue
|
|
502
|
+
message_parts = content.get("message", [])
|
|
503
|
+
for part in message_parts:
|
|
504
|
+
if isinstance(part, dict) and "attachment_id" in part:
|
|
505
|
+
attachment_ids.append(part["attachment_id"])
|
|
506
|
+
return attachment_ids
|
|
507
|
+
|
|
508
|
+
async def _delete_attachments(self, attachment_ids: list[str]):
|
|
509
|
+
"""删除附件(包括数据库记录和磁盘文件)"""
|
|
510
|
+
try:
|
|
511
|
+
attachments = await self.db.get_attachments(attachment_ids)
|
|
512
|
+
for attachment in attachments:
|
|
513
|
+
if not os.path.exists(attachment.path):
|
|
514
|
+
continue
|
|
515
|
+
try:
|
|
516
|
+
os.remove(attachment.path)
|
|
517
|
+
except OSError as e:
|
|
518
|
+
logger.warning(
|
|
519
|
+
f"Failed to delete attachment file {attachment.path}: {e}"
|
|
520
|
+
)
|
|
521
|
+
except Exception as e:
|
|
522
|
+
logger.warning(f"Failed to get attachments: {e}")
|
|
523
|
+
|
|
524
|
+
# 批量删除数据库记录
|
|
525
|
+
try:
|
|
526
|
+
await self.db.delete_attachments(attachment_ids)
|
|
527
|
+
except Exception as e:
|
|
528
|
+
logger.warning(f"Failed to delete attachments: {e}")
|
|
529
|
+
|
|
300
530
|
async def new_session(self):
|
|
301
531
|
"""Create a new Platform session (default: webchat)."""
|
|
302
532
|
username = g.get("username", "guest")
|