AstrBot 4.8.0__py3-none-any.whl → 4.9.1__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_tool_exec.py +5 -1
- astrbot/core/config/astrbot_config.py +4 -0
- astrbot/core/config/default.py +72 -1
- astrbot/core/config/i18n_utils.py +1 -0
- astrbot/core/core_lifecycle.py +1 -1
- astrbot/core/db/__init__.py +2 -3
- 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 +4 -3
- 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/star_request.py +1 -2
- astrbot/core/pipeline/process_stage/stage.py +1 -1
- astrbot/core/pipeline/respond/stage.py +8 -2
- astrbot/core/pipeline/result_decorate/stage.py +89 -22
- 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 +4 -0
- astrbot/core/platform/platform.py +11 -3
- 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 +9 -5
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +24 -16
- 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 +52 -24
- astrbot/core/platform/sources/discord/discord_platform_event.py +29 -8
- astrbot/core/platform/sources/lark/lark_adapter.py +183 -20
- 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 +2 -3
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +62 -18
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +13 -7
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +5 -3
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +2 -1
- astrbot/core/platform/sources/slack/client.py +9 -2
- astrbot/core/platform/sources/slack/slack_adapter.py +15 -9
- astrbot/core/platform/sources/slack/slack_event.py +8 -7
- astrbot/core/platform/sources/telegram/tg_adapter.py +1 -1
- astrbot/core/platform/sources/telegram/tg_event.py +23 -27
- astrbot/core/platform/sources/webchat/webchat_adapter.py +2 -2
- astrbot/core/platform/sources/webchat/webchat_event.py +2 -2
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +26 -9
- astrbot/core/platform/sources/wecom/wecom_adapter.py +25 -28
- astrbot/core/platform/sources/wecom/wecom_event.py +2 -2
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +3 -3
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +30 -25
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +10 -7
- 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 +1 -1
- 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 +1 -1
- astrbot/core/utils/version_comparator.py +6 -3
- astrbot/core/utils/webhook_utils.py +19 -0
- astrbot/dashboard/routes/chat.py +14 -9
- astrbot/dashboard/routes/config.py +10 -20
- astrbot/dashboard/routes/conversation.py +91 -1
- astrbot/dashboard/routes/knowledge_base.py +253 -78
- astrbot/dashboard/routes/log.py +13 -8
- astrbot/dashboard/routes/platform.py +1 -1
- astrbot/dashboard/routes/plugin.py +113 -52
- astrbot/dashboard/routes/route.py +2 -0
- astrbot/dashboard/server.py +6 -3
- {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/METADATA +9 -1
- {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/RECORD +106 -105
- {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/WHEEL +0 -0
- {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/entry_points.txt +0 -0
- {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -20,7 +20,7 @@ class FontManager:
|
|
|
20
20
|
_font_cache = {}
|
|
21
21
|
|
|
22
22
|
@classmethod
|
|
23
|
-
def get_font(cls, size: int) -> ImageFont.FreeTypeFont:
|
|
23
|
+
def get_font(cls, size: int) -> ImageFont.FreeTypeFont|ImageFont.ImageFont:
|
|
24
24
|
"""获取指定大小的字体,优先从缓存获取"""
|
|
25
25
|
if size in cls._font_cache:
|
|
26
26
|
return cls._font_cache[size]
|
|
@@ -66,23 +66,17 @@ class TextMeasurer:
|
|
|
66
66
|
"""测量文本尺寸的工具类"""
|
|
67
67
|
|
|
68
68
|
@staticmethod
|
|
69
|
-
def get_text_size(text: str, font: ImageFont.FreeTypeFont) ->
|
|
69
|
+
def get_text_size(text: str, font: ImageFont.FreeTypeFont|ImageFont.ImageFont) -> tuple[int, int]:
|
|
70
70
|
"""获取文本的尺寸"""
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if hasattr(font, "getbbox")
|
|
76
|
-
else font.getsize(text)
|
|
77
|
-
)
|
|
78
|
-
except Exception:
|
|
79
|
-
# 兼容旧版本
|
|
80
|
-
return font.getsize(text)
|
|
71
|
+
|
|
72
|
+
# 依赖库Pillow>=11.2.1,不再需要考虑<9.0.0
|
|
73
|
+
left, top, right, bottom = font.getbbox("Hello world")
|
|
74
|
+
return int(right - left), int(bottom - top)
|
|
81
75
|
|
|
82
76
|
@staticmethod
|
|
83
77
|
def split_text_to_fit_width(
|
|
84
|
-
text: str, font: ImageFont.FreeTypeFont, max_width: int
|
|
85
|
-
) ->
|
|
78
|
+
text: str, font: ImageFont.FreeTypeFont|ImageFont.ImageFont, max_width: int
|
|
79
|
+
) -> list[str]:
|
|
86
80
|
"""将文本拆分为多行,确保每行不超过指定宽度"""
|
|
87
81
|
lines = []
|
|
88
82
|
if not text:
|
|
@@ -126,7 +120,7 @@ class MarkdownElement(ABC):
|
|
|
126
120
|
def render(
|
|
127
121
|
self,
|
|
128
122
|
image: Image.Image,
|
|
129
|
-
draw: ImageDraw.
|
|
123
|
+
draw: ImageDraw.ImageDraw,
|
|
130
124
|
x: int,
|
|
131
125
|
y: int,
|
|
132
126
|
image_width: int,
|
|
@@ -152,7 +146,7 @@ class TextElement(MarkdownElement):
|
|
|
152
146
|
def render(
|
|
153
147
|
self,
|
|
154
148
|
image: Image.Image,
|
|
155
|
-
draw: ImageDraw.
|
|
149
|
+
draw: ImageDraw.ImageDraw,
|
|
156
150
|
x: int,
|
|
157
151
|
y: int,
|
|
158
152
|
image_width: int,
|
|
@@ -186,7 +180,7 @@ class BoldTextElement(MarkdownElement):
|
|
|
186
180
|
def render(
|
|
187
181
|
self,
|
|
188
182
|
image: Image.Image,
|
|
189
|
-
draw: ImageDraw.
|
|
183
|
+
draw: ImageDraw.ImageDraw,
|
|
190
184
|
x: int,
|
|
191
185
|
y: int,
|
|
192
186
|
image_width: int,
|
|
@@ -251,7 +245,7 @@ class ItalicTextElement(MarkdownElement):
|
|
|
251
245
|
def render(
|
|
252
246
|
self,
|
|
253
247
|
image: Image.Image,
|
|
254
|
-
draw: ImageDraw.
|
|
248
|
+
draw: ImageDraw.ImageDraw,
|
|
255
249
|
x: int,
|
|
256
250
|
y: int,
|
|
257
251
|
image_width: int,
|
|
@@ -299,7 +293,7 @@ class ItalicTextElement(MarkdownElement):
|
|
|
299
293
|
# 倾斜变换,使用仿射变换实现斜体效果
|
|
300
294
|
# 变换矩阵: [1, 0.2, 0, 0, 1, 0]
|
|
301
295
|
italic_img = text_img.transform(
|
|
302
|
-
text_img.size, Image.AFFINE, (1, 0.2, 0, 0, 1, 0), Image.BICUBIC
|
|
296
|
+
text_img.size, Image.Transform.AFFINE, (1, 0.2, 0, 0, 1, 0), Image.Resampling.BICUBIC
|
|
303
297
|
)
|
|
304
298
|
|
|
305
299
|
# 粘贴到原图像
|
|
@@ -331,7 +325,7 @@ class UnderlineTextElement(MarkdownElement):
|
|
|
331
325
|
def render(
|
|
332
326
|
self,
|
|
333
327
|
image: Image.Image,
|
|
334
|
-
draw: ImageDraw.
|
|
328
|
+
draw: ImageDraw.ImageDraw,
|
|
335
329
|
x: int,
|
|
336
330
|
y: int,
|
|
337
331
|
image_width: int,
|
|
@@ -371,7 +365,7 @@ class StrikethroughTextElement(MarkdownElement):
|
|
|
371
365
|
def render(
|
|
372
366
|
self,
|
|
373
367
|
image: Image.Image,
|
|
374
|
-
draw: ImageDraw.
|
|
368
|
+
draw: ImageDraw.ImageDraw,
|
|
375
369
|
x: int,
|
|
376
370
|
y: int,
|
|
377
371
|
image_width: int,
|
|
@@ -422,7 +416,7 @@ class HeaderElement(MarkdownElement):
|
|
|
422
416
|
def render(
|
|
423
417
|
self,
|
|
424
418
|
image: Image.Image,
|
|
425
|
-
draw: ImageDraw.
|
|
419
|
+
draw: ImageDraw.ImageDraw,
|
|
426
420
|
x: int,
|
|
427
421
|
y: int,
|
|
428
422
|
image_width: int,
|
|
@@ -458,7 +452,7 @@ class QuoteElement(MarkdownElement):
|
|
|
458
452
|
def render(
|
|
459
453
|
self,
|
|
460
454
|
image: Image.Image,
|
|
461
|
-
draw: ImageDraw.
|
|
455
|
+
draw: ImageDraw.ImageDraw,
|
|
462
456
|
x: int,
|
|
463
457
|
y: int,
|
|
464
458
|
image_width: int,
|
|
@@ -502,7 +496,7 @@ class ListItemElement(MarkdownElement):
|
|
|
502
496
|
def render(
|
|
503
497
|
self,
|
|
504
498
|
image: Image.Image,
|
|
505
|
-
draw: ImageDraw.
|
|
499
|
+
draw: ImageDraw.ImageDraw,
|
|
506
500
|
x: int,
|
|
507
501
|
y: int,
|
|
508
502
|
image_width: int,
|
|
@@ -532,7 +526,7 @@ class ListItemElement(MarkdownElement):
|
|
|
532
526
|
class CodeBlockElement(MarkdownElement):
|
|
533
527
|
"""代码块元素"""
|
|
534
528
|
|
|
535
|
-
def __init__(self, content:
|
|
529
|
+
def __init__(self, content: list[str]):
|
|
536
530
|
super().__init__("\n".join(content))
|
|
537
531
|
|
|
538
532
|
def calculate_height(self, image_width: int, font_size: int) -> int:
|
|
@@ -552,7 +546,7 @@ class CodeBlockElement(MarkdownElement):
|
|
|
552
546
|
def render(
|
|
553
547
|
self,
|
|
554
548
|
image: Image.Image,
|
|
555
|
-
draw: ImageDraw.
|
|
549
|
+
draw: ImageDraw.ImageDraw,
|
|
556
550
|
x: int,
|
|
557
551
|
y: int,
|
|
558
552
|
image_width: int,
|
|
@@ -595,7 +589,7 @@ class InlineCodeElement(MarkdownElement):
|
|
|
595
589
|
def render(
|
|
596
590
|
self,
|
|
597
591
|
image: Image.Image,
|
|
598
|
-
draw: ImageDraw.
|
|
592
|
+
draw: ImageDraw.ImageDraw,
|
|
599
593
|
x: int,
|
|
600
594
|
y: int,
|
|
601
595
|
image_width: int,
|
|
@@ -667,7 +661,7 @@ class ImageElement(MarkdownElement):
|
|
|
667
661
|
def render(
|
|
668
662
|
self,
|
|
669
663
|
image: Image.Image,
|
|
670
|
-
draw: ImageDraw.
|
|
664
|
+
draw: ImageDraw.ImageDraw,
|
|
671
665
|
x: int,
|
|
672
666
|
y: int,
|
|
673
667
|
image_width: int,
|
|
@@ -686,7 +680,7 @@ class ImageElement(MarkdownElement):
|
|
|
686
680
|
if pasted_image.width > max_width:
|
|
687
681
|
ratio = max_width / pasted_image.width
|
|
688
682
|
new_size = (int(max_width), int(pasted_image.height * ratio))
|
|
689
|
-
pasted_image = pasted_image.resize(new_size, Image.LANCZOS)
|
|
683
|
+
pasted_image = pasted_image.resize(new_size, Image.Resampling.LANCZOS)
|
|
690
684
|
|
|
691
685
|
# 计算居中位置
|
|
692
686
|
paste_x = x + (image_width - pasted_image.width) // 2 - 10
|
|
@@ -705,7 +699,7 @@ class MarkdownParser:
|
|
|
705
699
|
"""Markdown解析器,将文本解析为元素"""
|
|
706
700
|
|
|
707
701
|
@staticmethod
|
|
708
|
-
async def parse(text: str) ->
|
|
702
|
+
async def parse(text: str) -> list[MarkdownElement]:
|
|
709
703
|
elements = []
|
|
710
704
|
lines = text.split("\n")
|
|
711
705
|
|
|
@@ -847,7 +841,7 @@ class MarkdownRenderer:
|
|
|
847
841
|
self,
|
|
848
842
|
font_size: int = 26,
|
|
849
843
|
width: int = 800,
|
|
850
|
-
bg_color:
|
|
844
|
+
bg_color: tuple[int, int, int] = (255, 255, 255),
|
|
851
845
|
):
|
|
852
846
|
self.font_size = font_size
|
|
853
847
|
self.width = width
|
|
@@ -68,7 +68,7 @@ async def convert_to_pcm_wav(input_path: str, output_path: str) -> str:
|
|
|
68
68
|
from pyffmpeg import FFmpeg
|
|
69
69
|
|
|
70
70
|
ff = FFmpeg()
|
|
71
|
-
ff.convert(
|
|
71
|
+
ff.convert(input_file=input_path, output_file=output_path)
|
|
72
72
|
except Exception as e:
|
|
73
73
|
logger.debug(f"pyffmpeg 转换失败: {e}, 尝试使用 ffmpeg 命令行进行转换")
|
|
74
74
|
|
|
@@ -60,9 +60,12 @@ class VersionComparator:
|
|
|
60
60
|
return -1
|
|
61
61
|
if isinstance(p1, str) and isinstance(p2, int):
|
|
62
62
|
return 1
|
|
63
|
-
if
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
if isinstance(p1, int) and isinstance(p2, int):
|
|
64
|
+
if p1 > p2:
|
|
65
|
+
return 1
|
|
66
|
+
if p1 < p2:
|
|
67
|
+
return -1
|
|
68
|
+
if isinstance(p1, str) and isinstance(p2, str):
|
|
66
69
|
if p1 > p2:
|
|
67
70
|
return 1
|
|
68
71
|
if p1 < p2:
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
1
3
|
from astrbot.core import astrbot_config, logger
|
|
4
|
+
from astrbot.core.config.default import WEBHOOK_SUPPORTED_PLATFORMS
|
|
2
5
|
|
|
3
6
|
|
|
4
7
|
def _get_callback_api_base() -> str:
|
|
@@ -45,3 +48,19 @@ def log_webhook_info(platform_name: str, webhook_uuid: str):
|
|
|
45
48
|
"====================\n"
|
|
46
49
|
)
|
|
47
50
|
logger.info(display_log)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def ensure_platform_webhook_config(platform_cfg: dict) -> bool:
|
|
54
|
+
"""为支持统一 webhook 的平台自动生成 webhook_uuid
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
platform_cfg (dict): 平台配置字典
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
bool: 如果生成了 webhook_uuid 则返回 True,否则返回 False
|
|
61
|
+
"""
|
|
62
|
+
pt = platform_cfg.get("type", "")
|
|
63
|
+
if pt in WEBHOOK_SUPPORTED_PLATFORMS and not platform_cfg.get("webhook_uuid"):
|
|
64
|
+
platform_cfg["webhook_uuid"] = uuid.uuid4().hex[:16]
|
|
65
|
+
return True
|
|
66
|
+
return False
|
astrbot/dashboard/routes/chat.py
CHANGED
|
@@ -4,7 +4,9 @@ import mimetypes
|
|
|
4
4
|
import os
|
|
5
5
|
import uuid
|
|
6
6
|
from contextlib import asynccontextmanager
|
|
7
|
+
from typing import cast
|
|
7
8
|
|
|
9
|
+
from quart import Response as QuartResponse
|
|
8
10
|
from quart import g, make_response, request, send_file
|
|
9
11
|
|
|
10
12
|
from astrbot.core import logger
|
|
@@ -424,16 +426,19 @@ class ChatRoute(Route):
|
|
|
424
426
|
sender_name=username,
|
|
425
427
|
)
|
|
426
428
|
|
|
427
|
-
response =
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
+
),
|
|
435
440
|
)
|
|
436
|
-
response.timeout = None # fix SSE auto disconnect issue
|
|
441
|
+
response.timeout = None # fix SSE auto disconnect issue
|
|
437
442
|
return response
|
|
438
443
|
|
|
439
444
|
async def delete_webchat_session(self):
|
|
@@ -2,7 +2,7 @@ import asyncio
|
|
|
2
2
|
import inspect
|
|
3
3
|
import os
|
|
4
4
|
import traceback
|
|
5
|
-
import
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
7
|
from quart import request
|
|
8
8
|
|
|
@@ -14,7 +14,6 @@ from astrbot.core.config.default import (
|
|
|
14
14
|
CONFIG_METADATA_3_SYSTEM,
|
|
15
15
|
DEFAULT_CONFIG,
|
|
16
16
|
DEFAULT_VALUE_MAP,
|
|
17
|
-
WEBHOOK_SUPPORTED_PLATFORMS,
|
|
18
17
|
)
|
|
19
18
|
from astrbot.core.config.i18n_utils import ConfigMetadataI18n
|
|
20
19
|
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
|
@@ -22,11 +21,12 @@ from astrbot.core.platform.register import platform_cls_map, platform_registry
|
|
|
22
21
|
from astrbot.core.provider import Provider
|
|
23
22
|
from astrbot.core.provider.register import provider_registry
|
|
24
23
|
from astrbot.core.star.star import star_registry
|
|
24
|
+
from astrbot.core.utils.webhook_utils import ensure_platform_webhook_config
|
|
25
25
|
|
|
26
26
|
from .route import Response, Route, RouteContext
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def try_cast(value:
|
|
29
|
+
def try_cast(value: Any, type_: str):
|
|
30
30
|
if type_ == "int":
|
|
31
31
|
try:
|
|
32
32
|
return int(value)
|
|
@@ -505,9 +505,9 @@ class ConfigRoute(Route):
|
|
|
505
505
|
if not isinstance(inst, EmbeddingProvider):
|
|
506
506
|
return Response().error("提供商不是 EmbeddingProvider 类型").__dict__
|
|
507
507
|
|
|
508
|
-
|
|
509
|
-
if
|
|
510
|
-
await
|
|
508
|
+
init_fn = getattr(inst, "initialize", None)
|
|
509
|
+
if inspect.iscoroutinefunction(init_fn):
|
|
510
|
+
await init_fn()
|
|
511
511
|
|
|
512
512
|
# 获取嵌入向量维度
|
|
513
513
|
vec = await inst.get_embedding("echo")
|
|
@@ -558,13 +558,8 @@ class ConfigRoute(Route):
|
|
|
558
558
|
async def post_new_platform(self):
|
|
559
559
|
new_platform_config = await request.json
|
|
560
560
|
|
|
561
|
-
# 如果是支持统一 webhook
|
|
562
|
-
|
|
563
|
-
if platform_type in WEBHOOK_SUPPORTED_PLATFORMS:
|
|
564
|
-
if new_platform_config.get("unified_webhook_mode", False):
|
|
565
|
-
# 如果没有 webhook_uuid 或为空,自动生成
|
|
566
|
-
if not new_platform_config.get("webhook_uuid"):
|
|
567
|
-
new_platform_config["webhook_uuid"] = uuid.uuid4().hex[:16]
|
|
561
|
+
# 如果是支持统一 webhook 模式的平台,生成 webhook_uuid
|
|
562
|
+
ensure_platform_webhook_config(new_platform_config)
|
|
568
563
|
|
|
569
564
|
self.config["platform"].append(new_platform_config)
|
|
570
565
|
try:
|
|
@@ -596,12 +591,7 @@ class ConfigRoute(Route):
|
|
|
596
591
|
return Response().error("参数错误").__dict__
|
|
597
592
|
|
|
598
593
|
# 如果是支持统一 webhook 模式的平台,且启用了统一 webhook 模式,确保有 webhook_uuid
|
|
599
|
-
|
|
600
|
-
if platform_type in WEBHOOK_SUPPORTED_PLATFORMS:
|
|
601
|
-
if new_config.get("unified_webhook_mode", False):
|
|
602
|
-
# 如果没有 webhook_uuid 或为空,自动生成
|
|
603
|
-
if not new_config.get("webhook_uuid"):
|
|
604
|
-
new_config["webhook_uuid"] = uuid.uuid4().hex
|
|
594
|
+
ensure_platform_webhook_config(new_config)
|
|
605
595
|
|
|
606
596
|
for i, platform in enumerate(self.config["platform"]):
|
|
607
597
|
if platform["id"] == platform_id:
|
|
@@ -777,7 +767,7 @@ class ConfigRoute(Route):
|
|
|
777
767
|
return {"metadata": CONFIG_METADATA_2, "config": config}
|
|
778
768
|
|
|
779
769
|
async def _get_plugin_config(self, plugin_name: str):
|
|
780
|
-
ret = {"metadata": None, "config": None}
|
|
770
|
+
ret: dict = {"metadata": None, "config": None}
|
|
781
771
|
|
|
782
772
|
for plugin_md in star_registry:
|
|
783
773
|
if plugin_md.name == plugin_name:
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import traceback
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from io import BytesIO
|
|
3
5
|
|
|
4
|
-
from quart import request
|
|
6
|
+
from quart import request, send_file
|
|
5
7
|
|
|
6
8
|
from astrbot.core import logger
|
|
7
9
|
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
|
@@ -30,6 +32,7 @@ class ConversationRoute(Route):
|
|
|
30
32
|
"POST",
|
|
31
33
|
self.update_history,
|
|
32
34
|
),
|
|
35
|
+
"/conversation/export": ("POST", self.export_conversations),
|
|
33
36
|
}
|
|
34
37
|
self.db_helper = db_helper
|
|
35
38
|
self.conv_mgr = core_lifecycle.conversation_manager
|
|
@@ -283,3 +286,90 @@ class ConversationRoute(Route):
|
|
|
283
286
|
except Exception as e:
|
|
284
287
|
logger.error(f"更新对话历史失败: {e!s}\n{traceback.format_exc()}")
|
|
285
288
|
return Response().error(f"更新对话历史失败: {e!s}").__dict__
|
|
289
|
+
|
|
290
|
+
async def export_conversations(self):
|
|
291
|
+
"""批量导出对话为 JSONL 格式"""
|
|
292
|
+
try:
|
|
293
|
+
data = await request.get_json()
|
|
294
|
+
conversations_to_export = data.get("conversations", [])
|
|
295
|
+
|
|
296
|
+
if not conversations_to_export:
|
|
297
|
+
return Response().error("导出列表不能为空").__dict__
|
|
298
|
+
|
|
299
|
+
# 收集所有对话的内容
|
|
300
|
+
jsonl_lines = []
|
|
301
|
+
exported_count = 0
|
|
302
|
+
failed_items = []
|
|
303
|
+
|
|
304
|
+
for conv_info in conversations_to_export:
|
|
305
|
+
user_id = conv_info.get("user_id")
|
|
306
|
+
cid = conv_info.get("cid")
|
|
307
|
+
|
|
308
|
+
if not user_id or not cid:
|
|
309
|
+
failed_items.append(
|
|
310
|
+
f"user_id:{user_id}, cid:{cid} - 缺少必要参数",
|
|
311
|
+
)
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
conversation = await self.conv_mgr.get_conversation(
|
|
316
|
+
unified_msg_origin=user_id,
|
|
317
|
+
conversation_id=cid,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
if not conversation:
|
|
321
|
+
failed_items.append(
|
|
322
|
+
f"user_id:{user_id}, cid:{cid} - 对话不存在"
|
|
323
|
+
)
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
# 解析对话内容 (history is always a JSON string from _convert_conv_from_v2_to_v1)
|
|
327
|
+
content = json.loads(conversation.history)
|
|
328
|
+
|
|
329
|
+
# 创建导出记录
|
|
330
|
+
export_record = {
|
|
331
|
+
"cid": cid,
|
|
332
|
+
"user_id": user_id,
|
|
333
|
+
"platform_id": conversation.platform_id,
|
|
334
|
+
"title": conversation.title,
|
|
335
|
+
"persona_id": conversation.persona_id,
|
|
336
|
+
"created_at": conversation.created_at,
|
|
337
|
+
"updated_at": conversation.updated_at,
|
|
338
|
+
"content": content,
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# 将记录转换为 JSON 字符串并添加到 JSONL
|
|
342
|
+
jsonl_lines.append(json.dumps(export_record, ensure_ascii=False))
|
|
343
|
+
exported_count += 1
|
|
344
|
+
|
|
345
|
+
except Exception as e:
|
|
346
|
+
failed_items.append(f"user_id:{user_id}, cid:{cid} - {e!s}")
|
|
347
|
+
logger.error(
|
|
348
|
+
f"导出对话失败: user_id={user_id}, cid={cid}, error={e!s}"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if exported_count == 0:
|
|
352
|
+
return Response().error("没有成功导出任何对话").__dict__
|
|
353
|
+
|
|
354
|
+
# 创建 JSONL 内容
|
|
355
|
+
jsonl_content = "\n".join(jsonl_lines)
|
|
356
|
+
|
|
357
|
+
# 创建一个内存文件对象
|
|
358
|
+
file_obj = BytesIO(jsonl_content.encode("utf-8"))
|
|
359
|
+
file_obj.seek(0)
|
|
360
|
+
|
|
361
|
+
# 生成文件名
|
|
362
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
363
|
+
filename = f"astrbot_conversations_export_{timestamp}.jsonl"
|
|
364
|
+
|
|
365
|
+
# 返回文件流
|
|
366
|
+
return await send_file(
|
|
367
|
+
file_obj,
|
|
368
|
+
mimetype="application/jsonl",
|
|
369
|
+
as_attachment=True,
|
|
370
|
+
attachment_filename=filename,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
logger.error(f"批量导出对话失败: {e!s}\n{traceback.format_exc()}")
|
|
375
|
+
return Response().error(f"批量导出对话失败: {e!s}").__dict__
|