Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2f928a7e5 | |||
| b8e4068c75 | |||
| 0916177a57 | |||
| 02cd5e396b | |||
| 56673ad78f | |||
| 9a4d05e2b6 | |||
| c3f45449e8 | |||
| 65da469deb | |||
| 16df64c405 | |||
| 6b73b19e54 |
@@ -243,4 +243,10 @@ pre-commit install
|
||||
|
||||
</details>
|
||||
|
||||
<div align="center">
|
||||
|
||||
_私は、高性能ですから!_
|
||||
|
||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||
</div
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.9.0"
|
||||
__version__ = "4.9.2"
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.9.0"
|
||||
VERSION = "4.9.2"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||
@@ -108,6 +108,7 @@ DEFAULT_CONFIG = {
|
||||
"provider_id": "",
|
||||
"dual_output": False,
|
||||
"use_file_service": False,
|
||||
"trigger_probability": 1.0,
|
||||
},
|
||||
"provider_ltm_settings": {
|
||||
"group_icl_enable": False,
|
||||
@@ -2209,6 +2210,9 @@ CONFIG_METADATA_2 = {
|
||||
"use_file_service": {
|
||||
"type": "bool",
|
||||
},
|
||||
"trigger_probability": {
|
||||
"type": "float",
|
||||
},
|
||||
},
|
||||
},
|
||||
"provider_ltm_settings": {
|
||||
@@ -2419,6 +2423,14 @@ CONFIG_METADATA_3 = {
|
||||
"provider_tts_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"provider_tts_settings.trigger_probability": {
|
||||
"description": "TTS 触发概率",
|
||||
"type": "float",
|
||||
"slider": {"min": 0, "max": 1, "step": 0.05},
|
||||
"condition": {
|
||||
"provider_tts_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"provider_settings.image_caption_prompt": {
|
||||
"description": "图片转述提示词",
|
||||
"type": "text",
|
||||
@@ -2986,6 +2998,7 @@ CONFIG_METADATA_3 = {
|
||||
"description": "回复概率",
|
||||
"type": "float",
|
||||
"hint": "0.0-1.0 之间的数值",
|
||||
"slider": {"min": 0, "max": 1, "step": 0.05},
|
||||
"condition": {
|
||||
"provider_ltm_settings.active_reply.enable": True,
|
||||
},
|
||||
|
||||
@@ -79,6 +79,7 @@ class ConfigMetadataI18n:
|
||||
"_special",
|
||||
"invisible",
|
||||
"options",
|
||||
"slider",
|
||||
]:
|
||||
if attr in field_data:
|
||||
field_result[attr] = field_data[attr]
|
||||
|
||||
@@ -158,7 +158,11 @@ class RespondStage(Stage):
|
||||
result = event.get_result()
|
||||
if result is None:
|
||||
return
|
||||
if event.get_extra("_streaming_finished", False):
|
||||
# prevent some plugin make result content type to LLM_RESULT after streaming finished, lead to send again
|
||||
return
|
||||
if result.result_content_type == ResultContentType.STREAMING_FINISH:
|
||||
event.set_extra("_streaming_finished", True)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
@@ -42,6 +43,18 @@ class ResultDecorateStage(Stage):
|
||||
"forward_threshold"
|
||||
]
|
||||
|
||||
trigger_probability = ctx.astrbot_config["provider_tts_settings"].get(
|
||||
"trigger_probability",
|
||||
1,
|
||||
)
|
||||
try:
|
||||
self.tts_trigger_probability = max(
|
||||
0.0,
|
||||
min(float(trigger_probability), 1.0),
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
self.tts_trigger_probability = 1.0
|
||||
|
||||
# 分段回复
|
||||
self.words_count_threshold = int(
|
||||
ctx.astrbot_config["platform_settings"]["segmented_reply"][
|
||||
@@ -246,7 +259,14 @@ class ResultDecorateStage(Stage):
|
||||
and result.is_llm_result()
|
||||
and SessionServiceManager.should_process_tts_request(event)
|
||||
):
|
||||
if not tts_provider:
|
||||
should_tts = self.tts_trigger_probability >= 1.0 or (
|
||||
self.tts_trigger_probability > 0.0
|
||||
and random.random() <= self.tts_trigger_probability
|
||||
)
|
||||
|
||||
if not should_tts:
|
||||
logger.debug("跳过 TTS:触发概率未命中。")
|
||||
elif not tts_provider:
|
||||
logger.warning(
|
||||
f"会话 {event.unified_msg_origin} 未配置文本转语音模型。",
|
||||
)
|
||||
|
||||
@@ -81,7 +81,12 @@ class LarkPlatformAdapter(Platform):
|
||||
)
|
||||
|
||||
self.lark_api = (
|
||||
lark.Client.builder().app_id(self.appid).app_secret(self.appsecret).build()
|
||||
lark.Client.builder()
|
||||
.app_id(self.appid)
|
||||
.app_secret(self.appsecret)
|
||||
.log_level(lark.LogLevel.ERROR)
|
||||
.domain(self.domain)
|
||||
.build()
|
||||
)
|
||||
|
||||
self.webhook_server = None
|
||||
|
||||
@@ -2,15 +2,19 @@ from astrbot.core import html_renderer
|
||||
from astrbot.core.provider import Provider
|
||||
from astrbot.core.star.star_tools import StarTools
|
||||
from astrbot.core.utils.command_parser import CommandParserMixin
|
||||
from astrbot.core.utils.plugin_kv_store import PluginKVStoreMixin
|
||||
|
||||
from .context import Context
|
||||
from .star import StarMetadata, star_map, star_registry
|
||||
from .star_manager import PluginManager
|
||||
|
||||
|
||||
class Star(CommandParserMixin):
|
||||
class Star(CommandParserMixin, PluginKVStoreMixin):
|
||||
"""所有插件(Star)的父类,所有插件都应该继承于这个类"""
|
||||
|
||||
author: str
|
||||
name: str
|
||||
|
||||
def __init__(self, context: Context, config: dict | None = None):
|
||||
StarTools.initialize(context)
|
||||
self.context = context
|
||||
|
||||
@@ -467,6 +467,18 @@ class PluginManager:
|
||||
metadata.star_cls = metadata.star_cls_type(
|
||||
context=self.context,
|
||||
)
|
||||
|
||||
p_name = (metadata.name or "unknown").lower().replace("/", "_")
|
||||
p_author = (
|
||||
(metadata.author or "unknown").lower().replace("/", "_")
|
||||
)
|
||||
setattr(metadata.star_cls, "name", p_name)
|
||||
setattr(metadata.star_cls, "author", p_author)
|
||||
setattr(
|
||||
metadata.star_cls,
|
||||
"plugin_id",
|
||||
f"{p_author}/{p_name}",
|
||||
)
|
||||
else:
|
||||
logger.info(f"插件 {metadata.name} 已被禁用。")
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
from typing import TypeVar
|
||||
|
||||
from astrbot.core import sp
|
||||
|
||||
SUPPORTED_VALUE_TYPES = int | float | str | bytes | bool | dict | list | None
|
||||
_VT = TypeVar("_VT")
|
||||
|
||||
|
||||
class PluginKVStoreMixin:
|
||||
"""为插件提供键值存储功能的 Mixin 类"""
|
||||
|
||||
plugin_id: str
|
||||
|
||||
async def put_kv_data(
|
||||
self,
|
||||
key: str,
|
||||
value: SUPPORTED_VALUE_TYPES,
|
||||
) -> None:
|
||||
"""为指定插件存储一个键值对"""
|
||||
await sp.put_async("plugin", self.plugin_id, key, value)
|
||||
|
||||
async def get_kv_data(self, key: str, default: _VT) -> _VT | None:
|
||||
"""获取指定插件存储的键值对"""
|
||||
return await sp.get_async("plugin", self.plugin_id, key, default)
|
||||
|
||||
async def delete_kv_data(self, key: str) -> None:
|
||||
"""删除指定插件存储的键值对"""
|
||||
await sp.remove_async("plugin", self.plugin_id, key)
|
||||
@@ -1,7 +1,9 @@
|
||||
import json
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
|
||||
from quart import request
|
||||
from quart import request, send_file
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
@@ -30,6 +32,7 @@ class ConversationRoute(Route):
|
||||
"POST",
|
||||
self.update_history,
|
||||
),
|
||||
"/conversation/export": ("POST", self.export_conversations),
|
||||
}
|
||||
self.db_helper = db_helper
|
||||
self.conv_mgr = core_lifecycle.conversation_manager
|
||||
@@ -283,3 +286,90 @@ class ConversationRoute(Route):
|
||||
except Exception as e:
|
||||
logger.error(f"更新对话历史失败: {e!s}\n{traceback.format_exc()}")
|
||||
return Response().error(f"更新对话历史失败: {e!s}").__dict__
|
||||
|
||||
async def export_conversations(self):
|
||||
"""批量导出对话为 JSONL 格式"""
|
||||
try:
|
||||
data = await request.get_json()
|
||||
conversations_to_export = data.get("conversations", [])
|
||||
|
||||
if not conversations_to_export:
|
||||
return Response().error("导出列表不能为空").__dict__
|
||||
|
||||
# 收集所有对话的内容
|
||||
jsonl_lines = []
|
||||
exported_count = 0
|
||||
failed_items = []
|
||||
|
||||
for conv_info in conversations_to_export:
|
||||
user_id = conv_info.get("user_id")
|
||||
cid = conv_info.get("cid")
|
||||
|
||||
if not user_id or not cid:
|
||||
failed_items.append(
|
||||
f"user_id:{user_id}, cid:{cid} - 缺少必要参数",
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
conversation = await self.conv_mgr.get_conversation(
|
||||
unified_msg_origin=user_id,
|
||||
conversation_id=cid,
|
||||
)
|
||||
|
||||
if not conversation:
|
||||
failed_items.append(
|
||||
f"user_id:{user_id}, cid:{cid} - 对话不存在"
|
||||
)
|
||||
continue
|
||||
|
||||
# 解析对话内容 (history is always a JSON string from _convert_conv_from_v2_to_v1)
|
||||
content = json.loads(conversation.history)
|
||||
|
||||
# 创建导出记录
|
||||
export_record = {
|
||||
"cid": cid,
|
||||
"user_id": user_id,
|
||||
"platform_id": conversation.platform_id,
|
||||
"title": conversation.title,
|
||||
"persona_id": conversation.persona_id,
|
||||
"created_at": conversation.created_at,
|
||||
"updated_at": conversation.updated_at,
|
||||
"content": content,
|
||||
}
|
||||
|
||||
# 将记录转换为 JSON 字符串并添加到 JSONL
|
||||
jsonl_lines.append(json.dumps(export_record, ensure_ascii=False))
|
||||
exported_count += 1
|
||||
|
||||
except Exception as e:
|
||||
failed_items.append(f"user_id:{user_id}, cid:{cid} - {e!s}")
|
||||
logger.error(
|
||||
f"导出对话失败: user_id={user_id}, cid={cid}, error={e!s}"
|
||||
)
|
||||
|
||||
if exported_count == 0:
|
||||
return Response().error("没有成功导出任何对话").__dict__
|
||||
|
||||
# 创建 JSONL 内容
|
||||
jsonl_content = "\n".join(jsonl_lines)
|
||||
|
||||
# 创建一个内存文件对象
|
||||
file_obj = BytesIO(jsonl_content.encode("utf-8"))
|
||||
file_obj.seek(0)
|
||||
|
||||
# 生成文件名
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"astrbot_conversations_export_{timestamp}.jsonl"
|
||||
|
||||
# 返回文件流
|
||||
return await send_file(
|
||||
file_obj,
|
||||
mimetype="application/jsonl",
|
||||
as_attachment=True,
|
||||
attachment_filename=filename,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"批量导出对话失败: {e!s}\n{traceback.format_exc()}")
|
||||
return Response().error(f"批量导出对话失败: {e!s}").__dict__
|
||||
|
||||
@@ -124,7 +124,11 @@ class PluginRoute(Route):
|
||||
session.get(url) as response,
|
||||
):
|
||||
if response.status == 200:
|
||||
remote_data = await response.json()
|
||||
try:
|
||||
remote_data = await response.json()
|
||||
except aiohttp.ContentTypeError:
|
||||
remote_text = await response.text()
|
||||
remote_data = json.loads(remote_text)
|
||||
|
||||
# 检查远程数据是否为空
|
||||
if not remote_data or (
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
## What's Changed
|
||||
|
||||
-
|
||||
@@ -0,0 +1,17 @@
|
||||
## What's Changed
|
||||
|
||||
### 修复
|
||||
|
||||
- 企业自部署飞书(自定义 domain)可以接收消息但无法发送消息的问题。
|
||||
- 安装插件 Dialog 的深色样式问题。
|
||||
|
||||
### 优化
|
||||
|
||||
- 避免某些插件在流式响应结束后重d复发送消息的问题。
|
||||
|
||||
### 新增
|
||||
|
||||
- 支持在对话管理批量导出对话轨迹数据为 `jsonl` 格式文件。入口:WebUI -> 对话管理 -> 批量选中 -> 导出。
|
||||
- 支持对 TTS(文本转语音)设置概率触发。
|
||||
- (插件开发)支持在 schema 中对 float 和 int 类型设置 `slider` 滑块控件。例如 `slider: {min: 0, max: 1, step: 0.1}`。
|
||||
- (插件开发)支持 key-value 存储功能。例如使用 `await self.put_kv_data("key", value)`, `await self.get_kv_data("key", default_value)` 和 `await self.delete_kv_data("key")`。
|
||||
@@ -304,16 +304,32 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
<!-- Numeric input -->
|
||||
<v-text-field
|
||||
<!-- Numeric input with optional slider -->
|
||||
<div
|
||||
v-else-if="(metadata[metadataKey].items[key]?.type === 'int' || metadata[metadataKey].items[key]?.type === 'float') && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[key]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
type="number"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
class="d-flex align-center gap-3"
|
||||
>
|
||||
<v-slider
|
||||
v-if="metadata[metadataKey].items[key]?.slider"
|
||||
v-model.number="iterable[key]"
|
||||
:min="metadata[metadataKey].items[key]?.slider?.min ?? 0"
|
||||
:max="metadata[metadataKey].items[key]?.slider?.max ?? 100"
|
||||
:step="metadata[metadataKey].items[key]?.slider?.step ?? 1"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="flex-grow-1"
|
||||
></v-slider>
|
||||
<v-text-field
|
||||
v-model.number="iterable[key]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
type="number"
|
||||
hide-details
|
||||
style="max-width: 140px;"
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<!-- Text area -->
|
||||
<v-textarea
|
||||
@@ -413,16 +429,32 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
<!-- Numeric input -->
|
||||
<v-text-field
|
||||
<!-- Numeric input with optional slider -->
|
||||
<div
|
||||
v-else-if="(metadata[metadataKey]?.type === 'int' || metadata[metadataKey]?.type === 'float') && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[metadataKey]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
type="number"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
class="d-flex align-center gap-3"
|
||||
>
|
||||
<v-slider
|
||||
v-if="metadata[metadataKey]?.slider"
|
||||
v-model.number="iterable[metadataKey]"
|
||||
:min="metadata[metadataKey]?.slider?.min ?? 0"
|
||||
:max="metadata[metadataKey]?.slider?.max ?? 100"
|
||||
:step="metadata[metadataKey]?.slider?.step ?? 1"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="flex-grow-1"
|
||||
></v-slider>
|
||||
<v-text-field
|
||||
v-model.number="iterable[metadataKey]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
type="number"
|
||||
hide-details
|
||||
style="max-width: 140px;"
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<!-- Text area -->
|
||||
<v-textarea
|
||||
|
||||
@@ -245,10 +245,29 @@ function getSpecialSubtype(value) {
|
||||
<v-text-field v-else-if="itemMeta?.type === 'string'" v-model="createSelectorModel(itemKey).value"
|
||||
density="compact" variant="outlined" class="config-field" hide-details></v-text-field>
|
||||
|
||||
<!-- Numeric input for JSON selector -->
|
||||
<v-text-field v-else-if="itemMeta?.type === 'int' || itemMeta?.type === 'float'"
|
||||
v-model="createSelectorModel(itemKey).value" density="compact" variant="outlined" class="config-field"
|
||||
type="number" hide-details></v-text-field>
|
||||
<!-- Numeric input with optional slider for JSON selector -->
|
||||
<div v-else-if="itemMeta?.type === 'int' || itemMeta?.type === 'float'" class="d-flex align-center gap-3">
|
||||
<v-slider
|
||||
v-if="itemMeta?.slider"
|
||||
v-model.number="createSelectorModel(itemKey).value"
|
||||
:min="itemMeta?.slider?.min ?? 0"
|
||||
:max="itemMeta?.slider?.max ?? 100"
|
||||
:step="itemMeta?.slider?.step ?? 1"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
style="flex: 3"
|
||||
></v-slider>
|
||||
<v-text-field
|
||||
v-model.number="createSelectorModel(itemKey).value"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
style="flex: 2"
|
||||
type="number"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<!-- Text area for JSON selector -->
|
||||
<v-textarea v-else-if="itemMeta?.type === 'text'" v-model="createSelectorModel(itemKey).value"
|
||||
|
||||
@@ -115,7 +115,7 @@ const _show = computed({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog v-model="_show" width="800" persistent>
|
||||
<v-dialog v-model="_show" width="800">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span class="text-h5">{{ t('core.common.readme.title') }}</span>
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
},
|
||||
"provider_id": {
|
||||
"description": "Default Text-to-Speech Model"
|
||||
},
|
||||
"trigger_probability": {
|
||||
"description": "TTS Trigger Probability"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
"batch": {
|
||||
"deleteSelected": "Delete Selected ({count})"
|
||||
"deleteSelected": "Delete Selected ({count})",
|
||||
"exportSelected": "Export Selected ({count})"
|
||||
},
|
||||
"pagination": {
|
||||
"itemsPerPage": "Items per page",
|
||||
@@ -76,7 +77,8 @@
|
||||
"message": "Are you sure you want to delete the selected {count} conversations? This action cannot be undone, please proceed with caution!",
|
||||
"andMore": "and {count} more",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Batch Delete"
|
||||
"confirm": "Batch Delete",
|
||||
"warning": "Warning: This action cannot be undone!"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
@@ -92,6 +94,9 @@
|
||||
"noItemSelected": "Please select conversations to delete first",
|
||||
"batchDeleteSuccess": "Successfully deleted {count} conversations",
|
||||
"batchDeleteError": "Batch delete failed",
|
||||
"batchDeletePartial": "Delete completed: {deleted} successful, {failed} failed"
|
||||
"batchDeletePartial": "Delete completed: {deleted} successful, {failed} failed",
|
||||
"exportSuccess": "Export successful",
|
||||
"exportError": "Export failed",
|
||||
"noItemSelectedForExport": "Please select conversations to export first"
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,9 @@
|
||||
},
|
||||
"provider_id": {
|
||||
"description": "默认文本转语音模型"
|
||||
},
|
||||
"trigger_probability": {
|
||||
"description": "TTS 触发概率"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"refresh": "刷新"
|
||||
},
|
||||
"batch": {
|
||||
"deleteSelected": "删除选中 ({count})"
|
||||
"deleteSelected": "删除选中 ({count})",
|
||||
"exportSelected": "导出选中 ({count})"
|
||||
},
|
||||
"pagination": {
|
||||
"itemsPerPage": "每页",
|
||||
@@ -76,7 +77,8 @@
|
||||
"message": "确定要删除选中的 {count} 个对话吗?此操作不可恢复,请谨慎操作!",
|
||||
"andMore": "等 {count} 个",
|
||||
"cancel": "取消",
|
||||
"confirm": "批量删除"
|
||||
"confirm": "批量删除",
|
||||
"warning": "警告:此操作不可撤销!"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
@@ -92,6 +94,9 @@
|
||||
"noItemSelected": "请先选择要删除的对话",
|
||||
"batchDeleteSuccess": "成功删除 {count} 个对话",
|
||||
"batchDeleteError": "批量删除失败",
|
||||
"batchDeletePartial": "删除完成:成功 {deleted} 个,失败 {failed} 个"
|
||||
"batchDeletePartial": "删除完成:成功 {deleted} 个,失败 {failed} 个",
|
||||
"exportSuccess": "导出成功",
|
||||
"exportError": "导出失败",
|
||||
"noItemSelectedForExport": "请先选择要导出的对话"
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,17 @@
|
||||
:loading="loading" size="small" class="mr-2">
|
||||
{{ tm('history.refresh') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="selectedItems.length > 0"
|
||||
color="success"
|
||||
prepend-icon="mdi-download"
|
||||
variant="tonal"
|
||||
@click="exportConversations"
|
||||
:disabled="loading"
|
||||
size="small"
|
||||
class="mr-2">
|
||||
{{ tm('batch.exportSelected', { count: selectedItems.length }) }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="selectedItems.length > 0"
|
||||
color="error"
|
||||
@@ -910,6 +921,53 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
// 导出选中的对话
|
||||
async exportConversations() {
|
||||
if (this.selectedItems.length === 0) {
|
||||
this.showErrorMessage(this.tm('messages.noItemSelectedForExport'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
// 准备导出的数据
|
||||
const conversations = this.selectedItems.map(item => ({
|
||||
user_id: item.user_id,
|
||||
cid: item.cid
|
||||
}));
|
||||
|
||||
const response = await axios.post('/api/conversation/export', {
|
||||
conversations: conversations
|
||||
}, {
|
||||
responseType: 'blob' // 重要:告诉 axios 响应是一个 blob
|
||||
});
|
||||
|
||||
// 创建一个下载链接
|
||||
const url = window.URL.createObjectURL(response.data);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
|
||||
// 生成文件名(使用时间戳)
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
||||
const filename = `conversations_export_${timestamp}.jsonl`;
|
||||
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// 清理
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
this.showSuccessMessage(this.tm('messages.exportSuccess'));
|
||||
} catch (error) {
|
||||
console.error(this.tm('messages.exportError'), error);
|
||||
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.exportError'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化时间戳
|
||||
formatTimestamp(timestamp) {
|
||||
if (!timestamp) return this.tm('status.unknown');
|
||||
|
||||
@@ -1568,7 +1568,7 @@ watch(marketSearch, (newVal) => {
|
||||
|
||||
<!-- 上传插件对话框 -->
|
||||
<v-dialog v-model="dialog" width="500">
|
||||
<div class="v-card v-theme--PurpleThemeDark v-card--density-default rounded-lg v-card--variant-elevated">
|
||||
<div class="v-card v-card--density-default rounded-lg v-card--variant-elevated">
|
||||
<div class="v-card__loader">
|
||||
<v-progress-linear :indeterminate="loading_" color="primary" height="2" :active="loading_"></v-progress-linear>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.9.0"
|
||||
version = "4.9.2"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Reference in New Issue
Block a user