refactor: 解耦 PlatformPage 和 ProviderPage 的部分组件

This commit is contained in:
Soulter
2025-09-23 15:31:16 +08:00
parent 8453ec36f0
commit 1fff5713f3
6 changed files with 573 additions and 419 deletions
@@ -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>
+78
View File
@@ -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 '';
}
+51
View File
@@ -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 });
}
+23 -204
View File
@@ -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>
+13 -215
View File
@@ -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;