Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f4a31cf8c | |||
| 23549f13d6 | |||
| 869d11f9a6 | |||
| 02e73b82ee | |||
| f85f87f545 | |||
| 1fff5713f3 | |||
| 8453ec36f0 |
@@ -18,7 +18,8 @@
|
||||
|
||||
<a href="https://github.com/Soulter/AstrBot/blob/master/README_en.md">English</a> |
|
||||
<a href="https://github.com/Soulter/AstrBot/blob/master/README_ja.md">日本語</a> |
|
||||
<a href="https://astrbot.app/">查看文档</a> |
|
||||
<a href="https://astrbot.app/">文档</a> |
|
||||
<a href="https://blog.astrbot.app/">Blog</a> |
|
||||
<a href="https://github.com/Soulter/AstrBot/issues">问题提交</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.1.6"
|
||||
VERSION = "4.1.7"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
# 默认配置
|
||||
|
||||
@@ -51,24 +51,6 @@ def validate_config(
|
||||
def validate(data: dict, metadata: dict = schema, path=""):
|
||||
for key, value in data.items():
|
||||
if key not in metadata:
|
||||
# 无 schema 的配置项,执行类型猜测
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
data[key] = int(value)
|
||||
continue
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
data[key] = float(value)
|
||||
continue
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if value.lower() == "true":
|
||||
data[key] = True
|
||||
elif value.lower() == "false":
|
||||
data[key] = False
|
||||
continue
|
||||
meta = metadata[key]
|
||||
if "type" not in meta:
|
||||
@@ -127,12 +109,12 @@ def validate_config(
|
||||
)
|
||||
|
||||
if is_core:
|
||||
for key, group in schema.items():
|
||||
group_meta = group.get("metadata")
|
||||
if not group_meta:
|
||||
continue
|
||||
# logger.info(f"验证配置: 组 {key} ...")
|
||||
validate(data, group_meta, path=f"{key}.")
|
||||
meta_all = {
|
||||
**schema["platform_group"]["metadata"],
|
||||
**schema["provider_group"]["metadata"],
|
||||
**schema["misc_config_group"]["metadata"],
|
||||
}
|
||||
validate(data, meta_all)
|
||||
else:
|
||||
validate(data, schema)
|
||||
|
||||
@@ -142,6 +124,7 @@ def validate_config(
|
||||
def save_config(post_config: dict, config: AstrBotConfig, is_core: bool = False):
|
||||
"""验证并保存配置"""
|
||||
errors = None
|
||||
logger.info(f"Saving config, is_core={is_core}")
|
||||
try:
|
||||
if is_core:
|
||||
errors, post_config = validate_config(
|
||||
|
||||
@@ -169,15 +169,65 @@ class ConversationRoute(Route):
|
||||
"""删除对话"""
|
||||
try:
|
||||
data = await request.get_json()
|
||||
user_id = data.get("user_id")
|
||||
cid = data.get("cid")
|
||||
|
||||
if not user_id or not cid:
|
||||
return Response().error("缺少必要参数: user_id 和 cid").__dict__
|
||||
await self.core_lifecycle.conversation_manager.delete_conversation(
|
||||
unified_msg_origin=user_id, conversation_id=cid
|
||||
)
|
||||
return Response().ok({"message": "对话删除成功"}).__dict__
|
||||
# 检查是否是批量删除
|
||||
if "conversations" in data:
|
||||
# 批量删除
|
||||
conversations = data.get("conversations", [])
|
||||
if not conversations:
|
||||
return (
|
||||
Response().error("批量删除时conversations参数不能为空").__dict__
|
||||
)
|
||||
|
||||
deleted_count = 0
|
||||
failed_items = []
|
||||
|
||||
for conv in conversations:
|
||||
user_id = conv.get("user_id")
|
||||
cid = conv.get("cid")
|
||||
|
||||
if not user_id or not cid:
|
||||
failed_items.append(
|
||||
f"user_id:{user_id}, cid:{cid} - 缺少必要参数"
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
await self.core_lifecycle.conversation_manager.delete_conversation(
|
||||
unified_msg_origin=user_id, conversation_id=cid
|
||||
)
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
failed_items.append(f"user_id:{user_id}, cid:{cid} - {str(e)}")
|
||||
|
||||
message = f"成功删除 {deleted_count} 个对话"
|
||||
if failed_items:
|
||||
message += f",失败 {len(failed_items)} 个"
|
||||
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"message": message,
|
||||
"deleted_count": deleted_count,
|
||||
"failed_count": len(failed_items),
|
||||
"failed_items": failed_items,
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
else:
|
||||
# 单个删除
|
||||
user_id = data.get("user_id")
|
||||
cid = data.get("cid")
|
||||
|
||||
if not user_id or not cid:
|
||||
return Response().error("缺少必要参数: user_id 和 cid").__dict__
|
||||
|
||||
await self.core_lifecycle.conversation_manager.delete_conversation(
|
||||
unified_msg_origin=user_id, conversation_id=cid
|
||||
)
|
||||
return Response().ok({"message": "对话删除成功"}).__dict__
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"删除对话失败: {str(e)}\n{traceback.format_exc()}")
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# What's Changed
|
||||
|
||||
1. perf: 优化 WebChat 等组件的 UI 风格
|
||||
2. fix: 修复 4.1.6 版本可能无法点击更新按钮的问题
|
||||
3. fix: 修复更新开发版的时候,可能无法同时更新 WebUI 的问题
|
||||
4. feat: 支持在「对话数据」页批量删除对话
|
||||
5. fix: 修复部分错误地显示「格式校验未通过」的问题
|
||||
6. perf: WebChat 支持手动填写模型名称
|
||||
@@ -31,7 +31,7 @@
|
||||
<div v-else class="bot-message">
|
||||
<div v-if="isStreaming && index === messages.length - 1" style="width: 36px; height: 36px;">
|
||||
<v-progress-circular indeterminate size="28" width="2"
|
||||
style="margin-top: 16px;"></v-progress-circular>
|
||||
style="margin-top: 12px;"></v-progress-circular>
|
||||
</div>
|
||||
<v-avatar v-else class="bot-avatar" size="36">
|
||||
<span class="text-h2">✨</span>
|
||||
@@ -437,13 +437,13 @@ export default {
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 8px 16px;
|
||||
padding: 2px 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.user-bubble {
|
||||
color: var(--v-theme-primaryText);
|
||||
padding: 18px 20px;
|
||||
padding: 12px 18px;
|
||||
font-size: 15px;
|
||||
max-width: 60%;
|
||||
border-radius: 1.5rem;
|
||||
@@ -459,7 +459,7 @@ export default {
|
||||
.user-avatar,
|
||||
.bot-avatar {
|
||||
align-self: flex-start;
|
||||
margin-top: 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* 附件样式 */
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 选择提供商和模型按钮 -->
|
||||
<v-btn
|
||||
class="text-none"
|
||||
variant="tonal"
|
||||
rounded="xl"
|
||||
size="small"
|
||||
v-if="selectedProviderId && selectedModelName"
|
||||
@click="showDialog = true">
|
||||
<v-btn class="text-none" variant="tonal" rounded="xl" size="small"
|
||||
v-if="selectedProviderId && selectedModelName" @click="openDialog">
|
||||
{{ selectedProviderId }} / {{ selectedModelName }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
rounded="xl"
|
||||
size="small"
|
||||
v-else
|
||||
@click="showDialog = true">
|
||||
<v-btn variant="tonal" rounded="xl" size="small" v-else @click="openDialog">
|
||||
选择模型
|
||||
</v-btn>
|
||||
|
||||
@@ -33,16 +23,12 @@
|
||||
<h4>提供商</h4>
|
||||
</div>
|
||||
<v-list density="compact" nav class="provider-list">
|
||||
<v-list-item
|
||||
v-for="provider in providerConfigs"
|
||||
:key="provider.id"
|
||||
:value="provider.id"
|
||||
@click="selectProvider(provider)"
|
||||
:active="selectedProviderId === provider.id"
|
||||
rounded="lg"
|
||||
class="provider-item">
|
||||
<v-list-item v-for="provider in providerConfigs" :key="provider.id" :value="provider.id"
|
||||
@click="selectProvider(provider)" :active="tempSelectedProviderId === provider.id"
|
||||
rounded="lg" class="provider-item">
|
||||
<v-list-item-title>{{ provider.id }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="provider.api_base">{{ provider.api_base }}</v-list-item-subtitle>
|
||||
<v-list-item-subtitle v-if="provider.api_base">{{ provider.api_base
|
||||
}}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<div v-if="providerConfigs.length === 0" class="empty-state">
|
||||
@@ -55,33 +41,28 @@
|
||||
<div class="model-list-panel">
|
||||
<div class="panel-header">
|
||||
<h4>模型</h4>
|
||||
<v-btn
|
||||
v-if="selectedProviderId"
|
||||
icon="mdi-refresh"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="refreshModels"
|
||||
:loading="loadingModels">
|
||||
<v-btn v-if="tempSelectedProviderId" icon="mdi-refresh" size="small" variant="text"
|
||||
@click="refreshModels" :loading="loadingModels">
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-list density="compact" nav class="model-list" v-if="selectedProviderId">
|
||||
<v-list-item
|
||||
v-for="model in modelList"
|
||||
:key="model"
|
||||
:value="model"
|
||||
@click="selectModel(model)"
|
||||
:active="selectedModelName === model"
|
||||
rounded="lg"
|
||||
<v-list density="compact" nav class="model-list" v-if="tempSelectedProviderId">
|
||||
|
||||
<v-text-field v-model="tempSelectedModelName" placeholder="自定义模型" hide-details solo variant="outlined" density="compact" class="mb-2 mx-2"></v-text-field>
|
||||
|
||||
<v-list-item v-for="model in modelList" :key="model" :value="model"
|
||||
@click="selectModel(model)" :active="tempSelectedModelName === model" rounded="lg"
|
||||
class="model-item">
|
||||
<v-list-item-title>{{ model }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="model.description">{{ model.description }}</v-list-item-subtitle>
|
||||
<v-list-item-subtitle v-if="model.description">{{ model.description
|
||||
}}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<div v-else class="empty-state">
|
||||
<v-icon icon="mdi-robot-outline" size="large" color="grey-lighten-1"></v-icon>
|
||||
<div class="empty-text">请先选择提供商</div>
|
||||
</div>
|
||||
<div v-if="selectedProviderId && modelList.length === 0 && !loadingModels" class="empty-state">
|
||||
<div v-if="tempSelectedProviderId && modelList.length === 0 && !loadingModels"
|
||||
class="empty-state">
|
||||
<v-icon icon="mdi-robot-off-outline" size="large" color="grey-lighten-1"></v-icon>
|
||||
<div class="empty-text">该提供商暂无可用模型</div>
|
||||
</div>
|
||||
@@ -91,11 +72,8 @@
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="closeDialog" color="grey-darken-1">取消</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
@click="confirmSelection"
|
||||
color="primary"
|
||||
:disabled="!selectedProviderId || !selectedModelName">
|
||||
<v-btn text @click="confirmSelection" color="primary"
|
||||
:disabled="!tempSelectedProviderId || !tempSelectedModelName">
|
||||
确认选择
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -127,12 +105,17 @@ export default {
|
||||
modelList: [],
|
||||
selectedProviderId: '',
|
||||
selectedModelName: '',
|
||||
// 临时选择状态,用于对话框内的选择
|
||||
tempSelectedProviderId: '',
|
||||
tempSelectedModelName: '',
|
||||
loadingModels: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// 从localStorage加载保存的选择
|
||||
this.loadFromStorage();
|
||||
// 初始化临时选择
|
||||
this.resetTempSelection();
|
||||
// 获取提供商列表
|
||||
this.loadProviderConfigs();
|
||||
// 如果有保存的选择,加载对应的模型列表
|
||||
@@ -145,13 +128,13 @@ export default {
|
||||
loadFromStorage() {
|
||||
const savedProvider = localStorage.getItem('selectedProvider');
|
||||
const savedModel = localStorage.getItem('selectedModel');
|
||||
|
||||
|
||||
if (savedProvider) {
|
||||
this.selectedProviderId = savedProvider;
|
||||
} else if (this.initialProvider) {
|
||||
this.selectedProviderId = this.initialProvider;
|
||||
}
|
||||
|
||||
|
||||
if (savedModel) {
|
||||
this.selectedModelName = savedModel;
|
||||
} else if (this.initialModel) {
|
||||
@@ -215,36 +198,40 @@ export default {
|
||||
|
||||
// 选择提供商
|
||||
selectProvider(provider) {
|
||||
this.selectedProviderId = provider.id;
|
||||
this.selectedModelName = ''; // 清空已选择的模型
|
||||
this.tempSelectedProviderId = provider.id;
|
||||
this.tempSelectedModelName = ''; // 清空已选择的模型
|
||||
this.modelList = []; // 清空模型列表
|
||||
this.getProviderModels(provider.id); // 获取该提供商的模型列表
|
||||
},
|
||||
|
||||
// 选择模型
|
||||
selectModel(model) {
|
||||
this.selectedModelName = model;
|
||||
this.tempSelectedModelName = model;
|
||||
},
|
||||
|
||||
// 刷新模型列表
|
||||
refreshModels() {
|
||||
if (this.selectedProviderId) {
|
||||
this.getProviderModels(this.selectedProviderId);
|
||||
if (this.tempSelectedProviderId) {
|
||||
this.getProviderModels(this.tempSelectedProviderId);
|
||||
}
|
||||
},
|
||||
|
||||
// 确认选择
|
||||
confirmSelection() {
|
||||
if (this.selectedProviderId && this.selectedModelName) {
|
||||
if (this.tempSelectedProviderId && this.tempSelectedModelName) {
|
||||
// 将临时选择应用到正式选择
|
||||
this.selectedProviderId = this.tempSelectedProviderId;
|
||||
this.selectedModelName = this.tempSelectedModelName;
|
||||
|
||||
// 保存到localStorage
|
||||
this.saveToStorage();
|
||||
|
||||
|
||||
// 触发事件通知父组件
|
||||
this.$emit('selection-changed', {
|
||||
providerId: this.selectedProviderId,
|
||||
modelName: this.selectedModelName
|
||||
});
|
||||
|
||||
|
||||
this.closeDialog();
|
||||
}
|
||||
},
|
||||
@@ -252,6 +239,24 @@ export default {
|
||||
// 关闭对话框
|
||||
closeDialog() {
|
||||
this.showDialog = false;
|
||||
// 重置临时选择为当前选择
|
||||
this.resetTempSelection();
|
||||
},
|
||||
|
||||
// 重置临时选择
|
||||
resetTempSelection() {
|
||||
this.tempSelectedProviderId = this.selectedProviderId;
|
||||
this.tempSelectedModelName = this.selectedModelName;
|
||||
// 如果有临时选择的提供商,重新加载模型列表
|
||||
if (this.tempSelectedProviderId) {
|
||||
this.getProviderModels(this.tempSelectedProviderId);
|
||||
}
|
||||
},
|
||||
|
||||
// 打开对话框
|
||||
openDialog() {
|
||||
this.resetTempSelection();
|
||||
this.showDialog = true;
|
||||
},
|
||||
|
||||
// 公开方法:获取当前选择
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<v-dialog v-model="showDialog" max-width="900px" min-height="80%">
|
||||
<v-card class="platform-selection-dialog" :title="tm('dialog.addPlatform')">
|
||||
<v-card-text class="pa-4" style="overflow-y: auto;">
|
||||
<v-row style="padding: 0px 8px;">
|
||||
<v-col v-for="(template, name) in platformTemplates"
|
||||
:key="name" cols="12" sm="6" md="6">
|
||||
<v-card variant="outlined" hover class="platform-card" @click="selectTemplate(name)">
|
||||
<div class="platform-card-content">
|
||||
<div class="platform-card-text">
|
||||
<v-card-title class="platform-card-title">{{ tm('dialog.connectTitle', { name }) }}</v-card-title>
|
||||
<v-card-text class="text-caption text-medium-emphasis platform-card-description">
|
||||
{{ getPlatformDescription(template, name) }}
|
||||
</v-card-text>
|
||||
</div>
|
||||
<div class="platform-card-logo">
|
||||
<img :src="getPlatformIcon(template.type)" v-if="getPlatformIcon(template.type)" class="platform-logo-img">
|
||||
<div v-else class="platform-logo-fallback">
|
||||
{{ name[0].toUpperCase() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-if="Object.keys(platformTemplates).length === 0"
|
||||
cols="12">
|
||||
<v-alert type="info" variant="tonal">
|
||||
{{ tm('dialog.noTemplates') }}
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { getPlatformIcon, getPlatformDescription } from '@/utils/platformUtils';
|
||||
|
||||
export default {
|
||||
name: 'AddNewPlatform',
|
||||
emits: ['update:show', 'select-template'],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
metadata: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { tm } = useModuleI18n('features/platform');
|
||||
return { tm };
|
||||
},
|
||||
computed: {
|
||||
showDialog: {
|
||||
get() {
|
||||
return this.show;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:show', value);
|
||||
}
|
||||
},
|
||||
platformTemplates() {
|
||||
return this.metadata['platform_group']?.metadata?.platform?.config_template || {};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 从工具函数导入
|
||||
getPlatformIcon,
|
||||
getPlatformDescription,
|
||||
|
||||
selectTemplate(name) {
|
||||
this.$emit('select-template', name);
|
||||
this.closeDialog();
|
||||
},
|
||||
|
||||
closeDialog() {
|
||||
this.showDialog = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.platform-selection-dialog .v-card-title {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
.platform-card {
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.platform-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.05);
|
||||
border-color: var(--v-primary-base);
|
||||
}
|
||||
|
||||
.platform-card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.platform-card-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.platform-card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.platform-card-description {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.platform-card-logo {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.platform-logo-img {
|
||||
max-width: 60px;
|
||||
max-height: 60px;
|
||||
opacity: 0.6;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.platform-logo-fallback {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--v-primary-base);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
opacity: 0.3;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<v-dialog v-model="showDialog" max-width="1100px" min-height="95%">
|
||||
<v-card :title="tm('dialogs.addProvider.title')">
|
||||
<v-card-text style="overflow-y: auto;">
|
||||
<v-tabs v-model="activeProviderTab" grow>
|
||||
<v-tab value="chat_completion" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-message-text</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.basic') }}
|
||||
</v-tab>
|
||||
<v-tab value="speech_to_text" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-microphone-message</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.speechToText') }}
|
||||
</v-tab>
|
||||
<v-tab value="text_to_speech" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-volume-high</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.textToSpeech') }}
|
||||
</v-tab>
|
||||
<v-tab value="embedding" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-code-json</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.embedding') }}
|
||||
</v-tab>
|
||||
<v-tab value="rerank" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-compare-vertical</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.rerank') }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-window v-model="activeProviderTab" class="mt-4">
|
||||
<v-window-item
|
||||
v-for="tabType in ['chat_completion', 'speech_to_text', 'text_to_speech', 'embedding', 'rerank']"
|
||||
:key="tabType" :value="tabType">
|
||||
<v-row class="mt-1">
|
||||
<v-col v-for="(template, name) in getTemplatesByType(tabType)" :key="name" cols="12" sm="6"
|
||||
md="4">
|
||||
<v-card variant="outlined" hover class="provider-card"
|
||||
@click="selectProviderTemplate(name)">
|
||||
<div class="provider-card-content">
|
||||
<div class="provider-card-text">
|
||||
<v-card-title class="provider-card-title">接入 {{ name }}</v-card-title>
|
||||
<v-card-text
|
||||
class="text-caption text-medium-emphasis provider-card-description">
|
||||
{{ getProviderDescription(template, name) }}
|
||||
</v-card-text>
|
||||
</div>
|
||||
<div class="provider-card-logo">
|
||||
<img :src="getProviderIcon(template.provider)"
|
||||
v-if="getProviderIcon(template.provider)" class="provider-logo-img">
|
||||
<div v-else class="provider-logo-fallback">
|
||||
{{ name[0].toUpperCase() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col v-if="Object.keys(getTemplatesByType(tabType)).length === 0" cols="12">
|
||||
<v-alert type="info" variant="tonal">
|
||||
{{ tm('dialogs.addProvider.noTemplates', { type: getTabTypeName(tabType) }) }}
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="closeDialog">
|
||||
Close
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { getProviderIcon, getProviderDescription } from '@/utils/providerUtils';
|
||||
|
||||
export default {
|
||||
name: 'AddNewProvider',
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
metadata: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['update:show', 'select-template'],
|
||||
setup() {
|
||||
const { tm } = useModuleI18n('features/provider');
|
||||
return { tm };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeProviderTab: 'chat_completion'
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showDialog: {
|
||||
get() {
|
||||
return this.show;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:show', value);
|
||||
}
|
||||
},
|
||||
|
||||
// 翻译消息的计算属性
|
||||
messages() {
|
||||
return {
|
||||
tabTypes: {
|
||||
'chat_completion': this.tm('providers.tabs.chatCompletion'),
|
||||
'speech_to_text': this.tm('providers.tabs.speechToText'),
|
||||
'text_to_speech': this.tm('providers.tabs.textToSpeech'),
|
||||
'embedding': this.tm('providers.tabs.embedding'),
|
||||
'rerank': this.tm('providers.tabs.rerank')
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeDialog() {
|
||||
this.showDialog = false;
|
||||
},
|
||||
|
||||
// 按提供商类型获取模板列表
|
||||
getTemplatesByType(type) {
|
||||
const templates = this.metadata['provider_group']?.metadata?.provider?.config_template || {};
|
||||
const filtered = {};
|
||||
|
||||
for (const [name, template] of Object.entries(templates)) {
|
||||
if (template.provider_type === type) {
|
||||
filtered[name] = template;
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
},
|
||||
|
||||
// 从工具函数导入
|
||||
getProviderIcon,
|
||||
|
||||
// 获取Tab类型的中文名称
|
||||
getTabTypeName(tabType) {
|
||||
return this.messages.tabTypes[tabType] || tabType;
|
||||
},
|
||||
|
||||
// 获取提供商简介
|
||||
getProviderDescription(template, name) {
|
||||
return getProviderDescription(template, name, this.tm);
|
||||
},
|
||||
|
||||
// 选择提供商模板
|
||||
selectProviderTemplate(name) {
|
||||
this.$emit('select-template', name);
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.provider-card {
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.provider-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.05);
|
||||
border-color: var(--v-primary-base);
|
||||
}
|
||||
|
||||
.provider-card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.provider-card-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.provider-card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.provider-card-description {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.provider-card-logo {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.provider-logo-img {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
opacity: 0.6;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.provider-logo-fallback {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--v-primary-base);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
opacity: 0.3;
|
||||
}
|
||||
</style>
|
||||
@@ -12,6 +12,13 @@
|
||||
"title": "Conversation History",
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
"batch": {
|
||||
"deleteSelected": "Delete Selected ({count})"
|
||||
},
|
||||
"pagination": {
|
||||
"itemsPerPage": "Items per page",
|
||||
"showingItems": "Showing {start}-{end} of {total} items"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"title": "Conversation Title",
|
||||
@@ -61,6 +68,13 @@
|
||||
"message": "Are you sure you want to delete conversation {title}? This action cannot be undone.",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Delete"
|
||||
},
|
||||
"batchDelete": {
|
||||
"title": "Batch Delete Confirmation",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
@@ -72,6 +86,10 @@
|
||||
"historyError": "Failed to fetch conversation history",
|
||||
"historySaveSuccess": "Conversation history saved successfully",
|
||||
"historySaveError": "Failed to save conversation history",
|
||||
"invalidJson": "Invalid JSON format"
|
||||
"invalidJson": "Invalid JSON format",
|
||||
"noItemSelected": "Please select conversations to delete first",
|
||||
"batchDeleteSuccess": "Successfully deleted {count} conversations",
|
||||
"batchDeleteError": "Batch delete failed",
|
||||
"batchDeletePartial": "Delete completed: {deleted} successful, {failed} failed"
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,13 @@
|
||||
"title": "对话历史",
|
||||
"refresh": "刷新"
|
||||
},
|
||||
"batch": {
|
||||
"deleteSelected": "删除选中 ({count})"
|
||||
},
|
||||
"pagination": {
|
||||
"itemsPerPage": "每页",
|
||||
"showingItems": "显示 {start}-{end} 项,共 {total} 项"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"title": "对话标题",
|
||||
@@ -61,6 +68,13 @@
|
||||
"message": "确定要删除对话 {title} 吗?此操作不可恢复。",
|
||||
"cancel": "取消",
|
||||
"confirm": "删除"
|
||||
},
|
||||
"batchDelete": {
|
||||
"title": "批量删除确认",
|
||||
"message": "确定要删除选中的 {count} 个对话吗?此操作不可恢复,请谨慎操作!",
|
||||
"andMore": "等 {count} 个",
|
||||
"cancel": "取消",
|
||||
"confirm": "批量删除"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
@@ -72,6 +86,10 @@
|
||||
"historyError": "获取对话历史失败",
|
||||
"historySaveSuccess": "对话历史保存成功",
|
||||
"historySaveError": "对话历史保存失败",
|
||||
"invalidJson": "JSON格式无效"
|
||||
"invalidJson": "JSON格式无效",
|
||||
"noItemSelected": "请先选择要删除的对话",
|
||||
"batchDeleteSuccess": "成功删除 {count} 个对话",
|
||||
"batchDeleteError": "批量删除失败",
|
||||
"batchDeletePartial": "删除完成:成功 {deleted} 个,失败 {failed} 个"
|
||||
}
|
||||
}
|
||||
@@ -342,15 +342,10 @@ commonStore.getStartTime();
|
||||
<v-dialog v-model="updateStatusDialog" :width="$vuetify.display.smAndDown ? '100%' : '1200'"
|
||||
:fullscreen="$vuetify.display.xs">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-tooltip>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="small" @click="checkUpdate(); getReleases(); getDevCommits();" class="action-btn"
|
||||
color="var(--v-theme-surface)" variant="flat" rounded="sm" v-bind="props" icon>
|
||||
<v-icon>mdi-arrow-up-circle</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
{{ t('core.header.buttons.update') }}
|
||||
</v-tooltip>
|
||||
<v-btn size="small" @click="checkUpdate(); getReleases(); getDevCommits();" class="action-btn"
|
||||
color="var(--v-theme-surface)" variant="flat" rounded="sm" v-bind="props" icon>
|
||||
<v-icon>mdi-arrow-up-circle</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title class="mobile-card-title">
|
||||
@@ -473,7 +468,7 @@ commonStore.getStartTime();
|
||||
<h3 class="mb-4">{{ t('core.header.updateDialog.dashboardUpdate.title') }}</h3>
|
||||
<div class="mb-4">
|
||||
<small>{{ t('core.header.updateDialog.dashboardUpdate.currentVersion') }} {{ dashboardCurrentVersion
|
||||
}}</small>
|
||||
}}</small>
|
||||
<br>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 平台相关工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取平台图标
|
||||
* @param {string} name - 平台名称或类型
|
||||
* @returns {string|undefined} 图标URL
|
||||
*/
|
||||
export function getPlatformIcon(name) {
|
||||
if (name === 'aiocqhttp' || name === 'qq_official' || name === 'qq_official_webhook') {
|
||||
return new URL('@/assets/images/platform_logos/qq.png', import.meta.url).href
|
||||
} else if (name === 'wecom') {
|
||||
return new URL('@/assets/images/platform_logos/wecom.png', import.meta.url).href
|
||||
} else if (name === 'wechatpadpro' || name === 'weixin_official_account' || name === 'wechat') {
|
||||
return new URL('@/assets/images/platform_logos/wechat.png', import.meta.url).href
|
||||
} else if (name === 'lark') {
|
||||
return new URL('@/assets/images/platform_logos/lark.png', import.meta.url).href
|
||||
} else if (name === 'dingtalk') {
|
||||
return new URL('@/assets/images/platform_logos/dingtalk.svg', import.meta.url).href
|
||||
} else if (name === 'telegram') {
|
||||
return new URL('@/assets/images/platform_logos/telegram.svg', import.meta.url).href
|
||||
} else if (name === 'discord') {
|
||||
return new URL('@/assets/images/platform_logos/discord.svg', import.meta.url).href
|
||||
} else if (name === 'slack') {
|
||||
return new URL('@/assets/images/platform_logos/slack.svg', import.meta.url).href
|
||||
} else if (name === 'kook') {
|
||||
return new URL('@/assets/images/platform_logos/kook.png', import.meta.url).href
|
||||
} else if (name === 'vocechat') {
|
||||
return new URL('@/assets/images/platform_logos/vocechat.png', import.meta.url).href
|
||||
} else if (name === 'satori' || name === 'Satori') {
|
||||
return new URL('@/assets/images/platform_logos/satori.png', import.meta.url).href
|
||||
} else if (name === 'misskey') {
|
||||
return new URL('@/assets/images/platform_logos/misskey.png', import.meta.url).href
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台教程链接
|
||||
* @param {string} platformType - 平台类型
|
||||
* @returns {string} 教程链接
|
||||
*/
|
||||
export function getTutorialLink(platformType) {
|
||||
const tutorialMap = {
|
||||
"qq_official_webhook": "https://docs.astrbot.app/deploy/platform/qqofficial/webhook.html",
|
||||
"qq_official": "https://docs.astrbot.app/deploy/platform/qqofficial/websockets.html",
|
||||
"aiocqhttp": "https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html",
|
||||
"wecom": "https://docs.astrbot.app/deploy/platform/wecom.html",
|
||||
"lark": "https://docs.astrbot.app/deploy/platform/lark.html",
|
||||
"telegram": "https://docs.astrbot.app/deploy/platform/telegram.html",
|
||||
"dingtalk": "https://docs.astrbot.app/deploy/platform/dingtalk.html",
|
||||
"wechatpadpro": "https://docs.astrbot.app/deploy/platform/wechat/wechatpadpro.html",
|
||||
"weixin_official_account": "https://docs.astrbot.app/deploy/platform/weixin-official-account.html",
|
||||
"discord": "https://docs.astrbot.app/deploy/platform/discord.html",
|
||||
"slack": "https://docs.astrbot.app/deploy/platform/slack.html",
|
||||
"kook": "https://docs.astrbot.app/deploy/platform/kook.html",
|
||||
"vocechat": "https://docs.astrbot.app/deploy/platform/vocechat.html",
|
||||
"satori": "https://docs.astrbot.app/deploy/platform/satori/llonebot.html",
|
||||
"misskey": "https://docs.astrbot.app/deploy/platform/misskey.html",
|
||||
}
|
||||
return tutorialMap[platformType] || "https://docs.astrbot.app";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台描述
|
||||
* @param {Object} template - 平台模板
|
||||
* @param {string} name - 平台名称
|
||||
* @returns {string} 平台描述
|
||||
*/
|
||||
export function getPlatformDescription(template, name) {
|
||||
// special judge for community platforms
|
||||
if (name.includes('vocechat')) {
|
||||
return "由 @HikariFroya 提供。";
|
||||
} else if (name.includes('kook')) {
|
||||
return "由 @wuyan1003 提供。"
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 提供商相关的工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取提供商类型对应的图标
|
||||
* @param {string} type - 提供商类型
|
||||
* @returns {string} 图标 URL
|
||||
*/
|
||||
export function getProviderIcon(type) {
|
||||
const icons = {
|
||||
'openai': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg',
|
||||
'azure': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/azure.svg',
|
||||
'xai': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/xai.svg',
|
||||
'anthropic': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/anthropic.svg',
|
||||
'ollama': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama.svg',
|
||||
'google': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg',
|
||||
'deepseek': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek.svg',
|
||||
'modelscope': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/modelscope.svg',
|
||||
'zhipu': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/zhipu.svg',
|
||||
'siliconflow': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/siliconcloud.svg',
|
||||
'moonshot': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg',
|
||||
'ppio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ppio.svg',
|
||||
'dify': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dify-color.svg',
|
||||
'dashscope': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/alibabacloud-color.svg',
|
||||
'fastgpt': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/fastgpt-color.svg',
|
||||
'lm_studio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/lmstudio.svg',
|
||||
'fishaudio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/fishaudio.svg',
|
||||
'minimax': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax.svg',
|
||||
'302ai': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.53.0/files/icons/ai302-color.svg',
|
||||
'microsoft': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/microsoft.svg',
|
||||
'vllm': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/vllm.svg',
|
||||
};
|
||||
return icons[type] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提供商简介
|
||||
* @param {Object} template - 模板对象
|
||||
* @param {string} name - 提供商名称
|
||||
* @param {Function} tm - 翻译函数
|
||||
* @returns {string} 提供商描述
|
||||
*/
|
||||
export function getProviderDescription(template, name, tm) {
|
||||
if (name == 'OpenAI') {
|
||||
return tm('providers.description.openai', { type: template.type });
|
||||
} else if (name == 'vLLM Rerank') {
|
||||
return tm('providers.description.vllm_rerank', { type: template.type });
|
||||
}
|
||||
return tm('providers.description.default', { type: template.type });
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-combobox v-model="platformFilter" :label="tm('filters.platform')"
|
||||
:items="availablePlatforms" chips multiple clearable variant="solo-filled" flat
|
||||
density="compact" hide-details>
|
||||
density="compact" hide-details :disabled="loading">
|
||||
<template v-slot:selection="{ item }">
|
||||
<v-chip size="small" label>
|
||||
{{ item.title }}
|
||||
@@ -21,7 +21,8 @@
|
||||
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select v-model="messageTypeFilter" :label="tm('filters.type')" :items="messageTypeItems"
|
||||
chips multiple clearable variant="solo-filled" density="compact" hide-details flat>
|
||||
chips multiple clearable variant="solo-filled" density="compact" hide-details flat
|
||||
:disabled="loading">
|
||||
<template v-slot:selection="{ item }">
|
||||
<v-chip size="small" variant="solo-filled" label>
|
||||
{{ item.title }}
|
||||
@@ -33,22 +34,33 @@
|
||||
<v-col cols="12" sm="12" md="4">
|
||||
<v-text-field v-model="search" prepend-inner-icon="mdi-magnify"
|
||||
:label="tm('filters.search')" hide-details density="compact" variant="solo-filled" flat
|
||||
clearable></v-text-field>
|
||||
clearable :disabled="loading"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-btn color="primary" prepend-icon="mdi-refresh" variant="tonal" @click="fetchConversations"
|
||||
:loading="loading" size="small">
|
||||
:loading="loading" size="small" class="mr-2">
|
||||
{{ tm('history.refresh') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="selectedItems.length > 0"
|
||||
color="error"
|
||||
prepend-icon="mdi-delete"
|
||||
variant="tonal"
|
||||
@click="confirmBatchDelete"
|
||||
:disabled="loading"
|
||||
size="small">
|
||||
{{ tm('batch.deleteSelected', { count: selectedItems.length }) }}
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<v-data-table :headers="tableHeaders" :items="conversations" :loading="loading"
|
||||
style="font-size: 12px;" density="comfortable" hide-default-footer items-per-page="10"
|
||||
<v-data-table v-model="selectedItems" :headers="tableHeaders" :items="conversations"
|
||||
:loading="loading" style="font-size: 12px;" density="comfortable" hide-default-footer
|
||||
class="elevation-0" :items-per-page="pagination.page_size"
|
||||
:items-per-page-options="[10, 20, 50, 100]" @update:options="handleTableOptions">
|
||||
:items-per-page-options="pageSizeOptions" show-select return-object
|
||||
:disabled="loading" @update:options="handleTableOptions">
|
||||
<template v-slot:item.title="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ item.title || tm('status.noTitle') }}</span>
|
||||
@@ -82,15 +94,15 @@
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<div class="actions-wrapper">
|
||||
<v-btn icon variant="plain" size="x-small" class="action-button"
|
||||
@click="viewConversation(item)">
|
||||
@click="viewConversation(item)" :disabled="loading">
|
||||
<v-icon>mdi-eye</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon variant="plain" size="x-small" class="action-button"
|
||||
@click="editConversation(item)">
|
||||
@click="editConversation(item)" :disabled="loading">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon color="error" variant="plain" size="x-small" class="action-button"
|
||||
@click="confirmDeleteConversation(item)">
|
||||
@click="confirmDeleteConversation(item)" :disabled="loading">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -105,9 +117,25 @@
|
||||
</v-data-table>
|
||||
|
||||
<!-- 分页控制 -->
|
||||
<div class="d-flex justify-end">
|
||||
<div class="d-flex justify-center py-3">
|
||||
<!-- 每页大小选择器 -->
|
||||
<div class="d-flex justify-between align-center px-4 py-2 bg-grey-lighten-5">
|
||||
<div class="d-flex align-center">
|
||||
<span class="text-caption mr-2">{{ tm('pagination.itemsPerPage') }}:</span>
|
||||
<v-select v-model="pagination.page_size" :items="pageSizeOptions" variant="outlined"
|
||||
density="compact" hide-details style="max-width: 100px;"
|
||||
:disabled="loading" @update:model-value="onPageSizeChange"></v-select>
|
||||
</div>
|
||||
<div class="text-caption ml-4">
|
||||
{{ tm('pagination.showingItems', {
|
||||
start: Math.min((pagination.page - 1) * pagination.page_size + 1, pagination.total),
|
||||
end: Math.min(pagination.page * pagination.page_size, pagination.total),
|
||||
total: pagination.total
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
<v-pagination v-model="pagination.page" :length="pagination.total_pages" :disabled="loading"
|
||||
@update:model-value="fetchConversations" rounded="circle"></v-pagination>
|
||||
@update:model-value="fetchConversations" rounded="circle" :total-visible="7"></v-pagination>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -116,24 +144,20 @@
|
||||
<!-- 对话详情对话框 -->
|
||||
<v-dialog v-model="dialogView" max-width="900px" scrollable>
|
||||
<v-card class="conversation-detail-card">
|
||||
<v-card-title class="bg-primary text-white py-3 d-flex align-center">
|
||||
<v-icon color="white" class="me-2">mdi-eye</v-icon>
|
||||
<v-card-title class="ml-2 mt-2 d-flex align-center">
|
||||
<span class="text-truncate">{{ selectedConversation?.title || tm('status.noTitle') }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<div class="d-flex align-center" v-if="selectedConversation?.sessionInfo">
|
||||
<v-chip color="white" text-color="primary" size="small" class="mr-2">
|
||||
<v-chip text-color="primary" size="small" class="mr-2" rounded="md">
|
||||
{{ selectedConversation.sessionInfo.platform }}
|
||||
</v-chip>
|
||||
<v-chip color="white" text-color="secondary" size="small">
|
||||
<v-chip text-color="secondary" size="small" rounded="md">
|
||||
{{ getMessageTypeDisplay(selectedConversation.sessionInfo.messageType) }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text class="py-4">
|
||||
<v-card-text>
|
||||
<div class="mb-4 d-flex align-center">
|
||||
<v-btn color="secondary" variant="tonal" size="small" class="mr-2"
|
||||
@click="isEditingHistory = !isEditingHistory">
|
||||
@@ -168,16 +192,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 消息列表组件 -->
|
||||
<MessageList
|
||||
v-else
|
||||
:messages="formattedMessages"
|
||||
:isDark="false"
|
||||
/>
|
||||
<MessageList v-else :messages="formattedMessages" :isDark="false" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="closeHistoryDialog">
|
||||
@@ -227,7 +245,7 @@
|
||||
|
||||
<v-card-text class="py-4">
|
||||
<p>{{ tm('dialogs.delete.message', { title: selectedConversation?.title || tm('status.noTitle') })
|
||||
}}</p>
|
||||
}}</p>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
@@ -244,6 +262,48 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 批量删除确认对话框 -->
|
||||
<v-dialog v-model="dialogBatchDelete" max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title class="bg-error text-white py-3">
|
||||
<v-icon color="white" class="me-2">mdi-delete</v-icon>
|
||||
<span>{{ tm('dialogs.batchDelete.title') }}</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="py-4">
|
||||
<p class="mb-3">{{ tm('dialogs.batchDelete.message', { count: selectedItems.length }) }}</p>
|
||||
|
||||
<!-- 显示前几个要删除的对话 -->
|
||||
<div v-if="selectedItems.length > 0" class="mb-3">
|
||||
<v-chip v-for="(item, index) in selectedItems.slice(0, 5)" :key="`${item.user_id}-${item.cid}`"
|
||||
size="small" class="mr-1 mb-1" closable @click:close="removeFromSelection(item)"
|
||||
:disabled="loading">
|
||||
{{ item.title || tm('status.noTitle') }}
|
||||
</v-chip>
|
||||
<v-chip v-if="selectedItems.length > 5" size="small" class="mr-1 mb-1">
|
||||
{{ tm('dialogs.batchDelete.andMore', { count: selectedItems.length - 5 }) }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<v-alert type="warning" variant="tonal" class="mb-3">
|
||||
{{ tm('dialogs.batchDelete.warning') }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="dialogBatchDelete = false" :disabled="loading">
|
||||
{{ tm('dialogs.batchDelete.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn color="error" @click="batchDeleteConversations" :loading="loading">
|
||||
{{ tm('dialogs.batchDelete.confirm') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="messageType" v-model="showMessage" location="top">
|
||||
{{ message }}
|
||||
@@ -291,32 +351,13 @@ export default {
|
||||
conversations: [],
|
||||
search: '',
|
||||
headers: [],
|
||||
selectedItems: [], // 批量选择的项目
|
||||
|
||||
// 筛选条件
|
||||
platformFilter: [],
|
||||
messageTypeFilter: [],
|
||||
lastAppliedFilters: null, // 记录上次应用的筛选条件
|
||||
|
||||
// 平台颜色映射
|
||||
platformColors: {
|
||||
'telegram': 'blue-lighten-1',
|
||||
'qq_official': 'purple-lighten-1',
|
||||
'qq_official_webhook': 'purple-lighten-2',
|
||||
'aiocqhttp': 'deep-purple-lighten-1',
|
||||
'lark': 'cyan-darken-1',
|
||||
'wecom': 'green-darken-1',
|
||||
'dingtalk': 'blue-darken-2',
|
||||
'default': 'grey-lighten-1'
|
||||
},
|
||||
|
||||
// 消息类型颜色映射
|
||||
messageTypeColors: {
|
||||
'GroupMessage': 'green',
|
||||
'FriendMessage': 'blue',
|
||||
'GuildMessage': 'purple',
|
||||
'default': 'grey'
|
||||
},
|
||||
|
||||
// 分页数据
|
||||
pagination: {
|
||||
page: 1,
|
||||
@@ -324,11 +365,13 @@ export default {
|
||||
total: 0,
|
||||
total_pages: 0
|
||||
},
|
||||
pageSizeOptions: [10, 20, 50, 100], // 每页大小选项
|
||||
|
||||
// 对话框控制
|
||||
dialogView: false,
|
||||
dialogEdit: false,
|
||||
dialogDelete: false,
|
||||
dialogBatchDelete: false, // 批量删除对话框
|
||||
|
||||
// 选中的对话
|
||||
selectedConversation: null,
|
||||
@@ -340,11 +383,6 @@ export default {
|
||||
cid: '',
|
||||
title: ''
|
||||
},
|
||||
defaultItem: {
|
||||
user_id: '',
|
||||
cid: '',
|
||||
title: ''
|
||||
},
|
||||
|
||||
// 表单验证
|
||||
valid: true,
|
||||
@@ -431,17 +469,6 @@ export default {
|
||||
];
|
||||
},
|
||||
|
||||
// 筛选后的对话 - 现在只用于额外的客户端筛选(排除astrbot和webchat)
|
||||
filteredConversations() {
|
||||
return this.conversations.filter(conv => {
|
||||
// 排除 user_id 为 astrbot 或 platform 为 webchat 的对话
|
||||
if (conv.user_id === 'astrbot' || conv.sessionInfo?.platform === 'webchat') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
// 当前的筛选条件对象
|
||||
currentFilters() {
|
||||
const platforms = this.platformFilter.map(item =>
|
||||
@@ -790,6 +817,88 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.deleteError'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.selectedItems = this.selectedItems.filter(item =>
|
||||
!(item.user_id === this.selectedConversation.user_id && item.cid === this.selectedConversation.cid)
|
||||
);
|
||||
this.selectedConversation = null;
|
||||
}
|
||||
},
|
||||
|
||||
// 处理页面大小变更
|
||||
onPageSizeChange() {
|
||||
this.pagination.page = 1; // 重置到第一页
|
||||
this.fetchConversations();
|
||||
},
|
||||
|
||||
// 确认批量删除
|
||||
confirmBatchDelete() {
|
||||
if (this.selectedItems.length === 0) {
|
||||
this.showErrorMessage(this.tm('messages.noItemSelected'));
|
||||
return;
|
||||
}
|
||||
this.dialogBatchDelete = true;
|
||||
},
|
||||
|
||||
// 从选择中移除项目
|
||||
removeFromSelection(item) {
|
||||
const index = this.selectedItems.findIndex(selected =>
|
||||
selected.user_id === item.user_id && selected.cid === item.cid
|
||||
);
|
||||
if (index !== -1) {
|
||||
this.selectedItems.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
// 批量删除对话
|
||||
async batchDeleteConversations() {
|
||||
if (this.selectedItems.length === 0) {
|
||||
this.showErrorMessage(this.tm('messages.noItemSelected'));
|
||||
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/delete', {
|
||||
conversations: conversations
|
||||
});
|
||||
|
||||
if (response.data.status === "ok") {
|
||||
const result = response.data.data;
|
||||
this.dialogBatchDelete = false;
|
||||
this.selectedItems = []; // 清空选择
|
||||
|
||||
// 显示结果消息
|
||||
if (result.failed_count > 0) {
|
||||
this.showErrorMessage(
|
||||
this.tm('messages.batchDeletePartial', {
|
||||
deleted: result.deleted_count,
|
||||
failed: result.failed_count
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.showSuccessMessage(
|
||||
this.tm('messages.batchDeleteSuccess', {
|
||||
count: result.deleted_count
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
this.fetchConversations();
|
||||
} else {
|
||||
this.showErrorMessage(response.data.message || this.tm('messages.batchDeleteError'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量删除对话出错:', error);
|
||||
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.batchDeleteError'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
@@ -812,35 +921,6 @@ export default {
|
||||
}).format(date);
|
||||
},
|
||||
|
||||
// 格式化消息内容
|
||||
formatMessage(content) {
|
||||
|
||||
// content 可能是数组
|
||||
// [{"type": "image_url", "image_url": {"url": url_or_base64}}, {"type": "text", "text": "text"}]
|
||||
|
||||
let final_content = content;
|
||||
if (Array.isArray(content)) {
|
||||
// 处理数组内容
|
||||
final_content = content.map(item => {
|
||||
if (item.type === 'image_url') {
|
||||
return `<img src="${item.image_url.url}" alt="Image" />`;
|
||||
} else if (item.type === 'text') {
|
||||
return item.text;
|
||||
}
|
||||
return '';
|
||||
}).join('\n');
|
||||
} else if (typeof content === 'object') {
|
||||
// 处理对象内容
|
||||
final_content = Object.values(content).join('');
|
||||
} else if (typeof content === 'string') {
|
||||
// 处理字符串内容
|
||||
final_content = content;
|
||||
} else if (!final_content) return this.tm('status.emptyContent');
|
||||
|
||||
// 使用markdown-it处理,默认安全(html: false会禁用HTML标签)
|
||||
return md.render(final_content);
|
||||
},
|
||||
|
||||
// 显示成功消息
|
||||
showSuccessMessage(message) {
|
||||
this.message = message;
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
{{ tm('subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" @click="showAddPlatformDialog = true" rounded="xl" size="x-large">
|
||||
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" @click="showAddPlatformDialog = true"
|
||||
rounded="xl" size="x-large">
|
||||
{{ tm('addAdapter') }}
|
||||
</v-btn>
|
||||
</v-row>
|
||||
@@ -25,14 +26,9 @@
|
||||
|
||||
<v-row v-else>
|
||||
<v-col v-for="(platform, index) in config_data.platform || []" :key="index" cols="12" md="6" lg="4" xl="3">
|
||||
<item-card
|
||||
:item="platform"
|
||||
title-field="id"
|
||||
enabled-field="enable"
|
||||
:bglogo="getPlatformIcon(platform.type || platform.id)"
|
||||
@toggle-enabled="platformStatusChange"
|
||||
@delete="deletePlatform"
|
||||
@edit="editPlatform">
|
||||
<item-card :item="platform" title-field="id" enabled-field="enable"
|
||||
:bglogo="getPlatformIcon(platform.type || platform.id)" @toggle-enabled="platformStatusChange"
|
||||
@delete="deletePlatform" @edit="editPlatform">
|
||||
</item-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -61,59 +57,13 @@
|
||||
</v-container>
|
||||
|
||||
<!-- 添加平台适配器对话框 -->
|
||||
<v-dialog v-model="showAddPlatformDialog" max-width="900px" min-height="80%">
|
||||
<v-card class="platform-selection-dialog">
|
||||
<v-card-title class="bg-primary text-white py-3 px-4" style="display: flex; align-items: center;">
|
||||
<v-icon color="white" class="me-2">mdi-plus-circle</v-icon>
|
||||
<span>{{ tm('dialog.addPlatform') }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon variant="text" color="white" @click="showAddPlatformDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4" style="overflow-y: auto;">
|
||||
<v-row class="mt-1">
|
||||
<v-col v-for="(template, name) in metadata['platform_group']?.metadata?.platform?.config_template || {}"
|
||||
:key="name" cols="12" sm="6" md="6">
|
||||
<v-card variant="outlined" hover class="platform-card" @click="selectPlatformTemplate(name)">
|
||||
<div class="platform-card-content">
|
||||
<div class="platform-card-text">
|
||||
<v-card-title class="platform-card-title">{{ tm('dialog.connectTitle', { name }) }}</v-card-title>
|
||||
<v-card-text class="text-caption text-medium-emphasis platform-card-description">
|
||||
{{ getPlatformDescription(template, name) }}
|
||||
</v-card-text>
|
||||
</div>
|
||||
<div class="platform-card-logo">
|
||||
<img :src="getPlatformIcon(template.type)" v-if="getPlatformIcon(template.type)" class="platform-logo-img">
|
||||
<div v-else class="platform-logo-fallback">
|
||||
{{ name[0].toUpperCase() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-if="Object.keys(metadata['platform_group']?.metadata?.platform?.config_template || {}).length === 0"
|
||||
cols="12">
|
||||
<v-alert type="info" variant="tonal">
|
||||
{{ tm('dialog.noTemplates') }}
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<AddNewPlatform v-model:show="showAddPlatformDialog" :metadata="metadata"
|
||||
@select-template="selectPlatformTemplate" />
|
||||
|
||||
<!-- 配置对话框 -->
|
||||
<v-dialog v-model="showPlatformCfg" persistent width="900px" max-width="90%">
|
||||
<v-card>
|
||||
<v-card-title class="bg-primary text-white py-3">
|
||||
<v-icon color="white" class="me-2">{{ updatingMode ? 'mdi-pencil' : 'mdi-plus' }}</v-icon>
|
||||
<span>{{ updatingMode ? tm('dialog.edit') : tm('dialog.add') }} {{ newSelectedPlatformName }} {{
|
||||
tm('dialog.adapter') }}</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card
|
||||
:title="updatingMode ? tm('dialog.edit') : tm('dialog.add') + ` ${newSelectedPlatformName} ` + tm('dialog.adapter')">
|
||||
<v-card-text class="py-4">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
@@ -164,7 +114,7 @@
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" variant="text" @click="handleIdConflictConfirm(false)">{{ tm('dialog.idConflict.confirm')
|
||||
}}</v-btn>
|
||||
}}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -177,7 +127,9 @@
|
||||
</v-card-title>
|
||||
<v-card-text class="py-4">
|
||||
<p>{{ tm('dialog.securityWarning.aiocqhttpTokenMissing') }}</p>
|
||||
<span><a href="https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html#%E9%99%84%E5%BD%95-%E5%A2%9E%E5%BC%BA%E8%BF%9E%E6%8E%A5%E5%AE%89%E5%85%A8%E6%80%A7" target="_blank">{{ tm('dialog.securityWarning.learnMore') }}</a></span>
|
||||
<span><a
|
||||
href="https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html#%E9%99%84%E5%BD%95-%E5%A2%9E%E5%BC%BA%E8%BF%9E%E6%8E%A5%E5%AE%89%E5%85%A8%E6%80%A7"
|
||||
target="_blank">{{ tm('dialog.securityWarning.learnMore') }}</a></span>
|
||||
</v-card-text>
|
||||
<v-card-actions class="px-4 pb-4">
|
||||
<v-spacer></v-spacer>
|
||||
@@ -199,8 +151,10 @@ import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
|
||||
import ItemCard from '@/components/shared/ItemCard.vue';
|
||||
import AddNewPlatform from '@/components/platform/AddNewPlatform.vue';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import { getPlatformIcon, getTutorialLink } from '@/utils/platformUtils';
|
||||
|
||||
export default {
|
||||
name: 'PlatformPage',
|
||||
@@ -208,7 +162,8 @@ export default {
|
||||
AstrBotConfig,
|
||||
WaitingForRestart,
|
||||
ConsoleDisplayer,
|
||||
ItemCard
|
||||
ItemCard,
|
||||
AddNewPlatform
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
@@ -285,69 +240,14 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 从工具函数导入
|
||||
getPlatformIcon,
|
||||
|
||||
openTutorial() {
|
||||
const tutorialUrl = this.getTutorialLink(this.newSelectedPlatformConfig.type);
|
||||
const tutorialUrl = getTutorialLink(this.newSelectedPlatformConfig.type);
|
||||
window.open(tutorialUrl, '_blank');
|
||||
},
|
||||
|
||||
getPlatformIcon(name) {
|
||||
if (name === 'aiocqhttp' || name === 'qq_official' || name === 'qq_official_webhook') {
|
||||
return new URL('@/assets/images/platform_logos/qq.png', import.meta.url).href
|
||||
} else if (name === 'wecom') {
|
||||
return new URL('@/assets/images/platform_logos/wecom.png', import.meta.url).href
|
||||
} else if (name === 'wechatpadpro' || name === 'weixin_official_account' || name === 'wechat') {
|
||||
return new URL('@/assets/images/platform_logos/wechat.png', import.meta.url).href
|
||||
} else if (name === 'lark') {
|
||||
return new URL('@/assets/images/platform_logos/lark.png', import.meta.url).href
|
||||
} else if (name === 'dingtalk') {
|
||||
return new URL('@/assets/images/platform_logos/dingtalk.svg', import.meta.url).href
|
||||
} else if (name === 'telegram') {
|
||||
return new URL('@/assets/images/platform_logos/telegram.svg', import.meta.url).href
|
||||
} else if (name === 'discord') {
|
||||
return new URL('@/assets/images/platform_logos/discord.svg', import.meta.url).href
|
||||
} else if (name === 'slack') {
|
||||
return new URL('@/assets/images/platform_logos/slack.svg', import.meta.url).href
|
||||
} else if (name === 'kook') {
|
||||
return new URL('@/assets/images/platform_logos/kook.png', import.meta.url).href
|
||||
} else if (name === 'vocechat') {
|
||||
return new URL('@/assets/images/platform_logos/vocechat.png', import.meta.url).href
|
||||
} else if (name === 'satori' || name === 'Satori') {
|
||||
return new URL('@/assets/images/platform_logos/satori.png', import.meta.url).href
|
||||
} else if (name === 'misskey') {
|
||||
return new URL('@/assets/images/platform_logos/misskey.png', import.meta.url).href
|
||||
}
|
||||
},
|
||||
|
||||
getTutorialLink(platform_type) {
|
||||
let tutorial_map = {
|
||||
"qq_official_webhook": "https://docs.astrbot.app/deploy/platform/qqofficial/webhook.html",
|
||||
"qq_official": "https://docs.astrbot.app/deploy/platform/qqofficial/websockets.html",
|
||||
"aiocqhttp": "https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html",
|
||||
"wecom": "https://docs.astrbot.app/deploy/platform/wecom.html",
|
||||
"lark": "https://docs.astrbot.app/deploy/platform/lark.html",
|
||||
"telegram": "https://docs.astrbot.app/deploy/platform/telegram.html",
|
||||
"dingtalk": "https://docs.astrbot.app/deploy/platform/dingtalk.html",
|
||||
"wechatpadpro": "https://docs.astrbot.app/deploy/platform/wechat/wechatpadpro.html",
|
||||
"weixin_official_account": "https://docs.astrbot.app/deploy/platform/weixin-official-account.html",
|
||||
"discord": "https://docs.astrbot.app/deploy/platform/discord.html",
|
||||
"slack": "https://docs.astrbot.app/deploy/platform/slack.html",
|
||||
"kook": "https://docs.astrbot.app/deploy/platform/kook.html",
|
||||
"vocechat": "https://docs.astrbot.app/deploy/platform/vocechat.html",
|
||||
"satori": "https://docs.astrbot.app/deploy/platform/satori/llonebot.html",
|
||||
"misskey": "https://docs.astrbot.app/deploy/platform/misskey.html",
|
||||
}
|
||||
return tutorial_map[platform_type] || "https://docs.astrbot.app";
|
||||
},
|
||||
|
||||
getPlatformDescription(template, name) {
|
||||
// special judge for community platforms
|
||||
if (name.includes('vocechat')) {
|
||||
return "由 @HikariFroya 提供。";
|
||||
} else if (name.includes('kook')) {
|
||||
return "由 @wuyan1003 提供。"
|
||||
}
|
||||
},
|
||||
|
||||
getConfig() {
|
||||
axios.get('/api/config/get').then((res) => {
|
||||
this.config_data = res.data.data.config;
|
||||
@@ -358,7 +258,7 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
// 添加一个新方法来选择平台模板
|
||||
// 选择平台模板
|
||||
selectPlatformTemplate(name) {
|
||||
this.newSelectedPlatformName = name;
|
||||
this.showPlatformCfg = true;
|
||||
@@ -366,7 +266,6 @@ export default {
|
||||
this.newSelectedPlatformConfig = JSON.parse(JSON.stringify(
|
||||
this.metadata['platform_group']?.metadata?.platform?.config_template[name] || {}
|
||||
));
|
||||
this.showAddPlatformDialog = false;
|
||||
},
|
||||
|
||||
addFromDefaultConfigTmpl(index) {
|
||||
@@ -483,7 +382,7 @@ export default {
|
||||
this.oneBotEmptyTokenWarningResolve(continueWithWarning);
|
||||
this.oneBotEmptyTokenWarningResolve = null;
|
||||
}
|
||||
|
||||
|
||||
if (!continueWithWarning) {
|
||||
this.loading = false;
|
||||
}
|
||||
@@ -535,84 +434,4 @@ export default {
|
||||
padding: 20px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.platform-selection-dialog .v-card-title {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
.platform-card {
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.platform-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.05);
|
||||
border-color: var(--v-primary-base);
|
||||
}
|
||||
|
||||
.platform-card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.platform-card-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.platform-card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.platform-card-description {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.platform-card-logo {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.platform-logo-img {
|
||||
max-width: 60px;
|
||||
max-height: 60px;
|
||||
opacity: 0.6;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.platform-logo-fallback {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--v-primary-base);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
opacity: 0.3;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -155,86 +155,15 @@
|
||||
</v-container>
|
||||
|
||||
<!-- 添加提供商对话框 -->
|
||||
<v-dialog v-model="showAddProviderDialog" max-width="1100px" min-height="95%">
|
||||
<v-card class="provider-selection-dialog">
|
||||
<v-card-title class="bg-primary text-white py-3 px-4" style="display: flex; align-items: center;">
|
||||
<v-icon color="white" class="me-2">mdi-plus-circle</v-icon>
|
||||
<span>{{ tm('dialogs.addProvider.title') }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon variant="text" color="white" @click="showAddProviderDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4" style="overflow-y: auto;">
|
||||
<v-tabs v-model="activeProviderTab" grow slider-color="primary" bg-color="background">
|
||||
<v-tab value="chat_completion" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-message-text</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.basic') }}
|
||||
</v-tab>
|
||||
<v-tab value="speech_to_text" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-microphone-message</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.speechToText') }}
|
||||
</v-tab>
|
||||
<v-tab value="text_to_speech" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-volume-high</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.textToSpeech') }}
|
||||
</v-tab>
|
||||
<v-tab value="embedding" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-code-json</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.embedding') }}
|
||||
</v-tab>
|
||||
<v-tab value="rerank" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-compare-vertical</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.rerank') }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-window v-model="activeProviderTab" class="mt-4">
|
||||
<v-window-item v-for="tabType in ['chat_completion', 'speech_to_text', 'text_to_speech', 'embedding', 'rerank']"
|
||||
:key="tabType"
|
||||
:value="tabType">
|
||||
<v-row class="mt-1">
|
||||
<v-col v-for="(template, name) in getTemplatesByType(tabType)"
|
||||
:key="name"
|
||||
cols="12" sm="6" md="4">
|
||||
<v-card variant="outlined" hover class="provider-card" @click="selectProviderTemplate(name)">
|
||||
<div class="provider-card-content">
|
||||
<div class="provider-card-text">
|
||||
<v-card-title class="provider-card-title">接入 {{ name }}</v-card-title>
|
||||
<v-card-text class="text-caption text-medium-emphasis provider-card-description">
|
||||
{{ getProviderDescription(template, name) }}
|
||||
</v-card-text>
|
||||
</div>
|
||||
<div class="provider-card-logo">
|
||||
<img :src="getProviderIcon(template.provider)" v-if="getProviderIcon(template.provider)" class="provider-logo-img">
|
||||
<div v-else class="provider-logo-fallback">
|
||||
{{ name[0].toUpperCase() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col v-if="Object.keys(getTemplatesByType(tabType)).length === 0" cols="12">
|
||||
<v-alert type="info" variant="tonal">
|
||||
{{ tm('dialogs.addProvider.noTemplates', { type: getTabTypeName(tabType) }) }}
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<AddNewProvider
|
||||
v-model:show="showAddProviderDialog"
|
||||
:metadata="metadata"
|
||||
@select-template="selectProviderTemplate"
|
||||
/>
|
||||
|
||||
<!-- 配置对话框 -->
|
||||
<v-dialog v-model="showProviderCfg" width="900" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="bg-primary text-white py-3">
|
||||
<v-icon color="white" class="me-2">{{ updatingMode ? 'mdi-pencil' : 'mdi-plus' }}</v-icon>
|
||||
<span>{{ updatingMode ? tm('dialogs.config.editTitle') : tm('dialogs.config.addTitle') }} {{ newSelectedProviderName }} {{ tm('dialogs.config.provider') }}</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card :title="updatingMode ? tm('dialogs.config.editTitle') : tm('dialogs.config.addTitle') + ` ${newSelectedProviderName} ` + tm('dialogs.config.provider')">
|
||||
<v-card-text class="py-4">
|
||||
<AstrBotConfig
|
||||
:iterable="newSelectedProviderConfig"
|
||||
@@ -309,7 +238,9 @@ import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
|
||||
import ItemCard from '@/components/shared/ItemCard.vue';
|
||||
import AddNewProvider from '@/components/provider/AddNewProvider.vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { getProviderIcon } from '@/utils/providerUtils';
|
||||
|
||||
export default {
|
||||
name: 'ProviderPage',
|
||||
@@ -317,7 +248,8 @@ export default {
|
||||
AstrBotConfig,
|
||||
WaitingForRestart,
|
||||
ConsoleDisplayer,
|
||||
ItemCard
|
||||
ItemCard,
|
||||
AddNewProvider
|
||||
},
|
||||
setup() {
|
||||
const { tm } = useModuleI18n('features/provider');
|
||||
@@ -360,7 +292,6 @@ export default {
|
||||
|
||||
// 新增提供商对话框相关
|
||||
showAddProviderDialog: false,
|
||||
activeProviderTab: 'chat_completion',
|
||||
|
||||
// 添加提供商类型分类
|
||||
activeProviderTypeTab: 'all',
|
||||
@@ -474,6 +405,9 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
// 从工具函数导入
|
||||
getProviderIcon,
|
||||
|
||||
// 获取空列表文本
|
||||
getEmptyText() {
|
||||
if (this.activeProviderTypeTab === 'all') {
|
||||
@@ -483,63 +417,11 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
// 按提供商类型获取模板列表
|
||||
getTemplatesByType(type) {
|
||||
const templates = this.metadata['provider_group']?.metadata?.provider?.config_template || {};
|
||||
const filtered = {};
|
||||
|
||||
for (const [name, template] of Object.entries(templates)) {
|
||||
if (template.provider_type === type) {
|
||||
filtered[name] = template;
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
},
|
||||
|
||||
// 获取提供商类型对应的图标
|
||||
getProviderIcon(type) {
|
||||
const icons = {
|
||||
'openai': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg',
|
||||
'azure': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/azure.svg',
|
||||
'xai': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/xai.svg',
|
||||
'anthropic': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/anthropic.svg',
|
||||
'ollama': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama.svg',
|
||||
'google': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg',
|
||||
'deepseek': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek.svg',
|
||||
'modelscope': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/modelscope.svg',
|
||||
'zhipu': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/zhipu.svg',
|
||||
'siliconflow': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/siliconcloud.svg',
|
||||
'moonshot': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg',
|
||||
'ppio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ppio.svg',
|
||||
'dify': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dify-color.svg',
|
||||
'dashscope': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/alibabacloud-color.svg',
|
||||
'fastgpt': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/fastgpt-color.svg',
|
||||
'lm_studio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/lmstudio.svg',
|
||||
'fishaudio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/fishaudio.svg',
|
||||
'minimax': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax.svg',
|
||||
'302ai': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.53.0/files/icons/ai302-color.svg',
|
||||
'microsoft': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/microsoft.svg',
|
||||
'vllm': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/vllm.svg',
|
||||
};
|
||||
return icons[type] || '';
|
||||
},
|
||||
|
||||
// 获取Tab类型的中文名称
|
||||
getTabTypeName(tabType) {
|
||||
return this.messages.tabTypes[tabType] || tabType;
|
||||
},
|
||||
|
||||
// 获取提供商简介
|
||||
getProviderDescription(template, name) {
|
||||
if (name == 'OpenAI') {
|
||||
return this.tm('providers.description.openai', { type: template.type });
|
||||
} else if (name == 'vLLM Rerank') {
|
||||
return this.tm('providers.description.vllm_rerank', { type: template.type });
|
||||
}
|
||||
return this.tm('providers.description.default', { type: template.type });
|
||||
},
|
||||
|
||||
// 选择提供商模板
|
||||
selectProviderTemplate(name) {
|
||||
this.newSelectedProviderName = name;
|
||||
@@ -548,7 +430,6 @@ export default {
|
||||
this.newSelectedProviderConfig = JSON.parse(JSON.stringify(
|
||||
this.metadata['provider_group']?.metadata?.provider?.config_template[name] || {}
|
||||
));
|
||||
this.showAddProviderDialog = false;
|
||||
},
|
||||
|
||||
configExistingProvider(provider) {
|
||||
@@ -854,89 +735,6 @@ export default {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.provider-card {
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.provider-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.05);
|
||||
border-color: var(--v-primary-base);
|
||||
}
|
||||
|
||||
.provider-card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.provider-card-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.provider-card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.provider-card-description {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.provider-card-logo {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.provider-logo-img {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
opacity: 0.6;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.provider-logo-fallback {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--v-primary-base);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.v-tabs {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.v-window {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
height: 120px;
|
||||
overflow-y: auto;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.1.6"
|
||||
version = "4.1.7"
|
||||
description = "易上手的多平台 LLM 聊天机器人及开发框架"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Reference in New Issue
Block a user