mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-10-24 23:21:40 +08:00
750 lines
28 KiB
Vue
750 lines
28 KiB
Vue
<template>
|
||
<div class="provider-page">
|
||
<v-container fluid class="pa-0">
|
||
<!-- 页面标题 -->
|
||
<v-row>
|
||
<v-col cols="12">
|
||
<h1 class="text-h4 font-weight-bold mb-2">
|
||
<v-icon size="x-large" color="primary" class="me-2">mdi-creation</v-icon>服务提供商管理
|
||
</h1>
|
||
<p class="text-subtitle-1 text-medium-emphasis mb-4">
|
||
管理模型服务提供商
|
||
</p>
|
||
</v-col>
|
||
</v-row>
|
||
|
||
<!-- 服务提供商部分 -->
|
||
<v-card class="mb-6" elevation="2">
|
||
<v-card-title class="d-flex align-center py-3 px-4">
|
||
<v-icon color="primary" class="me-2">mdi-api</v-icon>
|
||
<span class="text-h6">服务提供商</span>
|
||
<v-chip color="info" size="small" class="ml-2">{{ config_data.provider?.length || 0 }}</v-chip>
|
||
<v-spacer></v-spacer>
|
||
<v-btn color="success" prepend-icon="mdi-cog" variant="tonal" class="me-2" @click="showSettingsDialog = true">
|
||
设置
|
||
</v-btn>
|
||
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" @click="showAddProviderDialog = true">
|
||
新增服务提供商
|
||
</v-btn>
|
||
</v-card-title>
|
||
|
||
<v-divider></v-divider>
|
||
|
||
<!-- 添加分类标签页 -->
|
||
<v-card-text class="px-4 pt-3 pb-0">
|
||
<v-tabs v-model="activeProviderTypeTab" bg-color="transparent">
|
||
<v-tab value="all" class="font-weight-medium px-3">
|
||
<v-icon start>mdi-filter-variant</v-icon>
|
||
全部
|
||
</v-tab>
|
||
<v-tab value="chat_completion" class="font-weight-medium px-3">
|
||
<v-icon start>mdi-message-text</v-icon>
|
||
基本对话
|
||
</v-tab>
|
||
<v-tab value="speech_to_text" class="font-weight-medium px-3">
|
||
<v-icon start>mdi-microphone-message</v-icon>
|
||
语音转文字
|
||
</v-tab>
|
||
<v-tab value="text_to_speech" class="font-weight-medium px-3">
|
||
<v-icon start>mdi-volume-high</v-icon>
|
||
文字转语音
|
||
</v-tab>
|
||
<v-tab value="embedding" class="font-weight-medium px-3">
|
||
<v-icon start>mdi-code-json</v-icon>
|
||
Embedding
|
||
</v-tab>
|
||
</v-tabs>
|
||
</v-card-text>
|
||
|
||
<v-card-text class="px-4 py-3">
|
||
<item-card-grid
|
||
:items="filteredProviders"
|
||
title-field="id"
|
||
enabled-field="enable"
|
||
empty-icon="mdi-api-off"
|
||
:empty-text="getEmptyText()"
|
||
@toggle-enabled="providerStatusChange"
|
||
@delete="deleteProvider"
|
||
@edit="configExistingProvider"
|
||
>
|
||
<template v-slot:item-details="{ item }">
|
||
<div class="d-flex align-center mb-2">
|
||
<v-icon size="small" color="grey" class="me-2">mdi-tag</v-icon>
|
||
<span class="text-caption text-medium-emphasis">
|
||
提供商类型:
|
||
<v-chip size="x-small" color="primary" class="ml-1">{{ item.type }}</v-chip>
|
||
</span>
|
||
</div>
|
||
<div v-if="item.api_base" class="d-flex align-center mb-2">
|
||
<v-icon size="small" color="grey" class="me-2">mdi-web</v-icon>
|
||
<span class="text-caption text-medium-emphasis text-truncate" :title="item.api_base">
|
||
API Base: {{ item.api_base }}
|
||
</span>
|
||
</div>
|
||
<div v-if="item.api_key" class="d-flex align-center">
|
||
<v-icon size="small" color="grey" class="me-2">mdi-key</v-icon>
|
||
<span class="text-caption text-medium-emphasis">API Key: ••••••••</span>
|
||
</div>
|
||
</template>
|
||
</item-card-grid>
|
||
</v-card-text>
|
||
</v-card>
|
||
|
||
<!-- 供应商状态部分 -->
|
||
<v-card class="mb-6" elevation="2">
|
||
<v-card-title class="d-flex align-center py-3 px-4">
|
||
<v-icon color="primary" class="me-2">mdi-heart-pulse</v-icon>
|
||
<span class="text-h6">供应商可用性</span>
|
||
<v-spacer></v-spacer>
|
||
<v-btn color="primary" variant="tonal" :loading="loadingStatus" @click="fetchProviderStatus">
|
||
<v-icon left>mdi-refresh</v-icon>
|
||
刷新状态
|
||
</v-btn>
|
||
</v-card-title>
|
||
<v-card-subtitle class="px-4 py-1 text-caption text-medium-emphasis">
|
||
通过测试模型对话可用性判断,可能产生API费用
|
||
</v-card-subtitle>
|
||
|
||
<v-divider></v-divider>
|
||
|
||
<v-card-text class="px-4 py-3">
|
||
<v-alert v-if="providerStatuses.length === 0" type="info" variant="tonal">
|
||
点击"刷新状态"按钮获取供应商可用性
|
||
</v-alert>
|
||
|
||
<v-container v-else class="pa-0">
|
||
<v-row>
|
||
<v-col v-for="status in providerStatuses" :key="status.id" cols="12" sm="6" md="4">
|
||
<v-card variant="outlined" class="status-card">
|
||
<v-card-item>
|
||
<v-icon :color="status.status === 'available' ? 'success' : 'error'" class="me-2">
|
||
{{ status.status === 'available' ? 'mdi-check-circle' : 'mdi-alert-circle' }}
|
||
</v-icon>
|
||
<span class="font-weight-bold">{{ status.id }}</span>
|
||
<v-chip :color="status.status === 'available' ? 'success' : 'error'" size="small" class="ml-2">
|
||
{{ status.status === 'available' ? '可用' : '不可用' }}
|
||
</v-chip>
|
||
</v-card-item>
|
||
<v-card-text v-if="status.status === 'unavailable'" class="text-caption text-medium-emphasis">
|
||
<span class="font-weight-bold">错误信息:</span> {{ status.error }}
|
||
</v-card-text>
|
||
</v-card>
|
||
</v-col>
|
||
</v-row>
|
||
</v-container>
|
||
</v-card-text>
|
||
</v-card>
|
||
|
||
<!-- 日志部分 -->
|
||
<v-card elevation="2">
|
||
<v-card-title class="d-flex align-center py-3 px-4">
|
||
<v-icon color="primary" class="me-2">mdi-console-line</v-icon>
|
||
<span class="text-h6">服务日志</span>
|
||
<v-spacer></v-spacer>
|
||
<v-btn variant="text" color="primary" @click="showConsole = !showConsole">
|
||
{{ showConsole ? '收起' : '展开' }}
|
||
<v-icon>{{ showConsole ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
|
||
</v-btn>
|
||
</v-card-title>
|
||
|
||
<v-divider></v-divider>
|
||
|
||
<v-expand-transition>
|
||
<v-card-text class="pa-0" v-if="showConsole">
|
||
<ConsoleDisplayer style="background-color: #1e1e1e; height: 300px; border-radius: 0"></ConsoleDisplayer>
|
||
</v-card-text>
|
||
</v-expand-transition>
|
||
</v-card>
|
||
</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>服务提供商</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>
|
||
基本
|
||
</v-tab>
|
||
<v-tab value="speech_to_text" class="font-weight-medium px-3">
|
||
<v-icon start>mdi-microphone-message</v-icon>
|
||
语音转文字
|
||
</v-tab>
|
||
<v-tab value="text_to_speech" class="font-weight-medium px-3">
|
||
<v-icon start>mdi-volume-high</v-icon>
|
||
文字转语音
|
||
</v-tab>
|
||
<v-tab value="embedding" class="font-weight-medium px-3">
|
||
<v-icon start>mdi-code-json</v-icon>
|
||
Embedding
|
||
</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']"
|
||
: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)">
|
||
<v-card-item>
|
||
<template v-slot:prepend>
|
||
<v-avatar color="primary" variant="tonal" class="mr-3">
|
||
<img :src="getProviderIcon(name)" v-if="getProviderIcon(name)" width="24" height="24">
|
||
<span v-else style="font-weight: 1000;">
|
||
{{ name[0].toUpperCase() }}
|
||
</span>
|
||
</v-avatar>
|
||
</template>
|
||
<v-card-title style="font-size: 15px;">{{ name }}</v-card-title>
|
||
</v-card-item>
|
||
<v-card-text class="text-caption text-medium-emphasis">
|
||
{{ getProviderDescription(template, name) }}
|
||
</v-card-text>
|
||
</v-card>
|
||
</v-col>
|
||
<v-col v-if="Object.keys(getTemplatesByType(tabType)).length === 0" cols="12">
|
||
<v-alert type="info" variant="tonal">
|
||
暂无{{ getTabTypeName(tabType) }}类型的提供商模板
|
||
</v-alert>
|
||
</v-col>
|
||
</v-row>
|
||
</v-window-item>
|
||
</v-window>
|
||
</v-card-text>
|
||
</v-card>
|
||
</v-dialog>
|
||
|
||
<!-- 配置对话框 -->
|
||
<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 ? '编辑' : '新增' }} {{ newSelectedProviderName }} 服务提供商</span>
|
||
</v-card-title>
|
||
|
||
<v-card-text class="py-4">
|
||
<AstrBotConfig
|
||
:iterable="newSelectedProviderConfig"
|
||
:metadata="metadata['provider_group']?.metadata"
|
||
metadataKey="provider"
|
||
/>
|
||
</v-card-text>
|
||
|
||
<v-divider></v-divider>
|
||
|
||
<v-card-actions class="pa-4">
|
||
<v-spacer></v-spacer>
|
||
<v-btn variant="text" @click="showProviderCfg = false" :disabled="loading">
|
||
取消
|
||
</v-btn>
|
||
<v-btn color="primary" @click="newProvider" :loading="loading">
|
||
保存
|
||
</v-btn>
|
||
</v-card-actions>
|
||
</v-card>
|
||
</v-dialog>
|
||
|
||
<!-- 设置对话框 -->
|
||
<v-dialog v-model="showSettingsDialog" max-width="600px">
|
||
<v-card>
|
||
<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-cog</v-icon>
|
||
<span>服务提供商设置</span>
|
||
<v-spacer></v-spacer>
|
||
<v-btn icon variant="text" color="white" @click="showSettingsDialog = false">
|
||
<v-icon>mdi-close</v-icon>
|
||
</v-btn>
|
||
</v-card-title>
|
||
|
||
<v-card-text class="pa-4">
|
||
<v-list>
|
||
<v-list-item>
|
||
<v-switch
|
||
style="padding: 12px;"
|
||
v-model="sessionSeparationEnabled"
|
||
color="primary"
|
||
:loading="sessionSettingLoading"
|
||
@change="updateSessionSeparation"
|
||
hide-details
|
||
>
|
||
<template v-slot:label>
|
||
<div>
|
||
<div class="text-subtitle-1">启用提供商会话隔离</div>
|
||
<div class="text-caption text-medium-emphasis">不同会话将可独立选择文本生成、TTS、STT 等服务提供商。</div>
|
||
</div>
|
||
</template>
|
||
</v-switch>
|
||
</v-list-item>
|
||
</v-list>
|
||
</v-card-text>
|
||
|
||
<v-card-actions class="pa-4">
|
||
<v-spacer></v-spacer>
|
||
<v-btn variant="text" @click="showSettingsDialog = false">
|
||
关闭
|
||
</v-btn>
|
||
</v-card-actions>
|
||
</v-card>
|
||
</v-dialog>
|
||
|
||
<!-- 消息提示 -->
|
||
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack"
|
||
location="top">
|
||
{{ save_message }}
|
||
</v-snackbar>
|
||
|
||
<WaitingForRestart ref="wfr"></WaitingForRestart>
|
||
|
||
<!-- Key为空的确认对话框 -->
|
||
<v-dialog v-model="showKeyConfirm" max-width="450" persistent>
|
||
<v-card>
|
||
<v-card-title class="text-h6 bg-error d-flex align-center">
|
||
<v-icon start class="me-2">mdi-alert-circle-outline</v-icon>
|
||
确认保存
|
||
</v-card-title>
|
||
<v-card-text class="py-4 text-body-1 text-medium-emphasis">
|
||
您没有填写 API Key,确定要保存吗?这可能会导致该服务提供商无法正常工作。
|
||
</v-card-text>
|
||
<v-card-actions>
|
||
<v-spacer></v-spacer>
|
||
<v-btn color="grey" variant="text" @click="handleKeyConfirm(false)">取消</v-btn>
|
||
<v-btn color="error" variant="flat" @click="handleKeyConfirm(true)">确定</v-btn>
|
||
</v-card-actions>
|
||
</v-card>
|
||
</v-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import axios from 'axios';
|
||
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
|
||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
|
||
import ItemCardGrid from '@/components/shared/ItemCardGrid.vue';
|
||
|
||
export default {
|
||
name: 'ProviderPage',
|
||
components: {
|
||
AstrBotConfig,
|
||
WaitingForRestart,
|
||
ConsoleDisplayer,
|
||
ItemCardGrid
|
||
},
|
||
data() {
|
||
return {
|
||
config_data: {},
|
||
fetched: false,
|
||
metadata: {},
|
||
showProviderCfg: false,
|
||
|
||
// 设置对话框相关
|
||
showSettingsDialog: false,
|
||
sessionSeparationEnabled: false,
|
||
sessionSettingLoading: false,
|
||
|
||
// Key确认对话框
|
||
showKeyConfirm: false,
|
||
keyConfirmResolve: null,
|
||
|
||
newSelectedProviderName: '',
|
||
newSelectedProviderConfig: {},
|
||
updatingMode: false,
|
||
|
||
loading: false,
|
||
|
||
save_message_snack: false,
|
||
save_message: "",
|
||
save_message_success: "success",
|
||
|
||
showConsole: false,
|
||
|
||
// 供应商状态相关
|
||
providerStatuses: [],
|
||
loadingStatus: false,
|
||
|
||
// 新增提供商对话框相关
|
||
showAddProviderDialog: false,
|
||
activeProviderTab: 'chat_completion',
|
||
|
||
// 添加提供商类型分类
|
||
activeProviderTypeTab: 'all',
|
||
|
||
// 兼容旧版本(< v3.5.11)的 mapping,用于映射到对应的提供商能力类型
|
||
oldVersionProviderTypeMapping: {
|
||
"openai_chat_completion": "chat_completion",
|
||
"anthropic_chat_completion": "chat_completion",
|
||
"googlegenai_chat_completion": "chat_completion",
|
||
"zhipu_chat_completion": "chat_completion",
|
||
"llm_tuner": "chat_completion",
|
||
"dify": "chat_completion",
|
||
"dashscope": "chat_completion",
|
||
"openai_whisper_api": "speech_to_text",
|
||
"openai_whisper_selfhost": "speech_to_text",
|
||
"sensevoice_stt_selfhost": "speech_to_text",
|
||
"openai_tts_api": "text_to_speech",
|
||
"edge_tts": "text_to_speech",
|
||
"gsvi_tts_api": "text_to_speech",
|
||
"fishaudio_tts_api": "text_to_speech",
|
||
"dashscope_tts": "text_to_speech",
|
||
"azure_tts": "text_to_speech",
|
||
"minimax_tts_api": "text_to_speech",
|
||
"volcengine_tts": "text_to_speech",
|
||
}
|
||
}
|
||
},
|
||
|
||
computed: {
|
||
// 根据选择的标签过滤提供商列表
|
||
filteredProviders() {
|
||
if (!this.config_data.provider || this.activeProviderTypeTab === 'all') {
|
||
return this.config_data.provider || [];
|
||
}
|
||
|
||
return this.config_data.provider.filter(provider => {
|
||
// 如果provider.provider_type已经存在,直接使用它
|
||
if (provider.provider_type) {
|
||
return provider.provider_type === this.activeProviderTypeTab;
|
||
}
|
||
|
||
// 否则使用映射关系
|
||
const mappedType = this.oldVersionProviderTypeMapping[provider.type];
|
||
return mappedType === this.activeProviderTypeTab;
|
||
});
|
||
}
|
||
},
|
||
|
||
mounted() {
|
||
this.getConfig();
|
||
this.getSessionSeparationStatus();
|
||
},
|
||
|
||
methods: {
|
||
getConfig() {
|
||
axios.get('/api/config/get').then((res) => {
|
||
this.config_data = res.data.data.config;
|
||
this.fetched = true
|
||
this.metadata = res.data.data.metadata;
|
||
}).catch((err) => {
|
||
this.showError(err.response?.data?.message || err.message);
|
||
});
|
||
},
|
||
|
||
// 获取空列表文本
|
||
getEmptyText() {
|
||
if (this.activeProviderTypeTab === 'all') {
|
||
return "暂无服务提供商,点击 新增服务提供商 添加";
|
||
} else {
|
||
return `暂无${this.getTabTypeName(this.activeProviderTypeTab)}类型的服务提供商,点击 新增服务提供商 添加`;
|
||
}
|
||
},
|
||
|
||
// 按提供商类型获取模板列表
|
||
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 OpenAI': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg',
|
||
'Whisper': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.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/anthropic.svg',
|
||
'Gemini': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg',
|
||
'Gemini(OpenAI兼容)': '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',
|
||
'智谱 AI': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/zhipu.svg',
|
||
'硅基流动': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/siliconcloud.svg',
|
||
'Kimi': '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',
|
||
'阿里云百炼': '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',
|
||
'Azure': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/azure.svg',
|
||
'MiniMax': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax.svg',
|
||
};
|
||
for (const key in icons) {
|
||
if (type.startsWith(key)) {
|
||
return icons[key];
|
||
}
|
||
}
|
||
return ''
|
||
},
|
||
|
||
// 获取Tab类型的中文名称
|
||
getTabTypeName(tabType) {
|
||
const names = {
|
||
'chat_completion': '基本对话',
|
||
'speech_to_text': '语音转文本',
|
||
'text_to_speech': '文本转语音',
|
||
'embedding': 'Embedding'
|
||
};
|
||
return names[tabType] || tabType;
|
||
},
|
||
|
||
// 获取提供商简介
|
||
getProviderDescription(template, name) {
|
||
if (name == 'OpenAI') {
|
||
return `${template.type} 服务提供商。同时也支持所有兼容 OpenAI API 的模型提供商。`;
|
||
}
|
||
return `${template.type} 服务提供商`;
|
||
},
|
||
|
||
// 选择提供商模板
|
||
selectProviderTemplate(name) {
|
||
this.newSelectedProviderName = name;
|
||
this.showProviderCfg = true;
|
||
this.updatingMode = false;
|
||
this.newSelectedProviderConfig = JSON.parse(JSON.stringify(
|
||
this.metadata['provider_group']?.metadata?.provider?.config_template[name] || {}
|
||
));
|
||
this.showAddProviderDialog = false;
|
||
},
|
||
|
||
// 废弃旧方法,保留为兼容
|
||
addFromDefaultConfigTmpl(index) {
|
||
this.selectProviderTemplate(index[0]);
|
||
},
|
||
|
||
configExistingProvider(provider) {
|
||
this.newSelectedProviderName = provider.id;
|
||
this.newSelectedProviderConfig = {};
|
||
|
||
// 比对默认配置模版,看看是否有更新
|
||
let templates = this.metadata['provider_group']?.metadata?.provider?.config_template || {};
|
||
let defaultConfig = {};
|
||
for (let key in templates) {
|
||
if (templates[key]?.type === provider.type) {
|
||
defaultConfig = templates[key];
|
||
break;
|
||
}
|
||
}
|
||
|
||
const mergeConfigWithOrder = (target, source, reference) => {
|
||
// 首先复制所有source中的属性到target
|
||
if (source && typeof source === 'object' && !Array.isArray(source)) {
|
||
for (let key in source) {
|
||
if (source.hasOwnProperty(key)) {
|
||
if (typeof source[key] === 'object' && source[key] !== null) {
|
||
target[key] = Array.isArray(source[key]) ? [...source[key]] : {...source[key]};
|
||
} else {
|
||
target[key] = source[key];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 然后根据reference的结构添加或覆盖属性
|
||
for (let key in reference) {
|
||
if (typeof reference[key] === 'object' && reference[key] !== null) {
|
||
if (!(key in target)) {
|
||
target[key] = Array.isArray(reference[key]) ? [] : {};
|
||
}
|
||
mergeConfigWithOrder(
|
||
target[key],
|
||
source && source[key] ? source[key] : {},
|
||
reference[key]
|
||
);
|
||
} else if (!(key in target)) {
|
||
// 只有当target中不存在该键时才从reference复制
|
||
target[key] = reference[key];
|
||
}
|
||
}
|
||
};
|
||
|
||
if (defaultConfig) {
|
||
mergeConfigWithOrder(this.newSelectedProviderConfig, provider, defaultConfig);
|
||
}
|
||
|
||
this.showProviderCfg = true;
|
||
this.updatingMode = true;
|
||
},
|
||
|
||
async newProvider() {
|
||
// 检查 key 是否为空
|
||
if (
|
||
'key' in this.newSelectedProviderConfig &&
|
||
(!this.newSelectedProviderConfig.key || this.newSelectedProviderConfig.key.length === 0)
|
||
) {
|
||
const confirmed = await this.confirmEmptyKey();
|
||
if (!confirmed) {
|
||
return; // 如果用户取消,则中止保存
|
||
}
|
||
}
|
||
|
||
this.loading = true;
|
||
try {
|
||
if (this.updatingMode) {
|
||
const res = await axios.post('/api/config/provider/update', {
|
||
id: this.newSelectedProviderName,
|
||
config: this.newSelectedProviderConfig
|
||
});
|
||
this.showSuccess(res.data.message || "更新成功!");
|
||
this.updatingMode = false;
|
||
} else {
|
||
const res = await axios.post('/api/config/provider/new', this.newSelectedProviderConfig);
|
||
this.showSuccess(res.data.message || "添加成功!");
|
||
}
|
||
this.showProviderCfg = false;
|
||
this.getConfig();
|
||
} catch (err) {
|
||
this.showError(err.response?.data?.message || err.message);
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
|
||
deleteProvider(provider) {
|
||
if (confirm(`确定要删除服务提供商 ${provider.id} 吗?`)) {
|
||
axios.post('/api/config/provider/delete', { id: provider.id }).then((res) => {
|
||
this.getConfig();
|
||
this.showSuccess(res.data.message || "删除成功!");
|
||
}).catch((err) => {
|
||
this.showError(err.response?.data?.message || err.message);
|
||
});
|
||
}
|
||
},
|
||
|
||
providerStatusChange(provider) {
|
||
provider.enable = !provider.enable; // 切换状态
|
||
|
||
axios.post('/api/config/provider/update', {
|
||
id: provider.id,
|
||
config: provider
|
||
}).then((res) => {
|
||
this.getConfig();
|
||
this.showSuccess(res.data.message || "状态更新成功!");
|
||
}).catch((err) => {
|
||
provider.enable = !provider.enable; // 发生错误时回滚状态
|
||
this.showError(err.response?.data?.message || err.message);
|
||
});
|
||
},
|
||
|
||
// 获取会话隔离配置状态
|
||
getSessionSeparationStatus() {
|
||
axios.get('/api/config/provider/get_session_seperate').then((res) => {
|
||
if (res.data && res.data.status === 'ok') {
|
||
this.sessionSeparationEnabled = res.data.data.enable;
|
||
}
|
||
}).catch((err) => {
|
||
this.showError(err.response?.data?.message || "获取会话隔离配置失败");
|
||
});
|
||
},
|
||
|
||
// 更新会话隔离配置
|
||
updateSessionSeparation() {
|
||
this.sessionSettingLoading = true;
|
||
axios.post('/api/config/provider/set_session_seperate', {
|
||
enable: this.sessionSeparationEnabled
|
||
}).then((res) => {
|
||
this.showSuccess(res.data.message || "会话隔离设置已更新");
|
||
this.sessionSettingLoading = false;
|
||
}).catch((err) => {
|
||
this.sessionSeparationEnabled = !this.sessionSeparationEnabled; // 发生错误时回滚状态
|
||
this.showError(err.response?.data?.message || err.message);
|
||
this.sessionSettingLoading = false;
|
||
});
|
||
},
|
||
|
||
showSuccess(message) {
|
||
this.save_message = message;
|
||
this.save_message_success = "success";
|
||
this.save_message_snack = true;
|
||
},
|
||
|
||
showError(message) {
|
||
this.save_message = message;
|
||
this.save_message_success = "error";
|
||
this.save_message_snack = true;
|
||
},
|
||
|
||
// 获取供应商状态
|
||
fetchProviderStatus() {
|
||
this.loadingStatus = true;
|
||
axios.get('/api/config/provider/check_status').then((res) => {
|
||
if (res.data && res.data.status === 'ok') {
|
||
this.providerStatuses = res.data.data || [];
|
||
} else {
|
||
this.showError(res.data?.message || "获取供应商状态失败");
|
||
}
|
||
this.loadingStatus = false;
|
||
}).catch((err) => {
|
||
this.loadingStatus = false;
|
||
this.showError(err.response?.data?.message || err.message);
|
||
});
|
||
},
|
||
|
||
confirmEmptyKey() {
|
||
this.showKeyConfirm = true;
|
||
return new Promise((resolve) => {
|
||
this.keyConfirmResolve = resolve;
|
||
});
|
||
},
|
||
|
||
handleKeyConfirm(confirmed) {
|
||
if (this.keyConfirmResolve) {
|
||
this.keyConfirmResolve(confirmed);
|
||
}
|
||
this.showKeyConfirm = false;
|
||
},
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.provider-page {
|
||
padding: 20px;
|
||
padding-top: 8px;
|
||
}
|
||
|
||
.provider-selection-dialog .v-card-title {
|
||
border-top-left-radius: 4px;
|
||
border-top-right-radius: 4px;
|
||
}
|
||
|
||
.provider-card {
|
||
transition: all 0.3s ease;
|
||
height: 100%;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.provider-card:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.05);
|
||
border-color: var(--v-primary-base);
|
||
}
|
||
|
||
.v-tabs {
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.v-window {
|
||
border-radius: 4px;
|
||
}
|
||
</style>
|