Files
AstrBot/dashboard/src/views/SessionManagementPage.vue

1084 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="session-management-page">
<v-container fluid class="pa-0">
<v-row class="d-flex justify-space-between align-center px-4 py-3 pb-8">
<div>
<h1 class="text-h1 font-weight-bold mb-2" style="display: flex; align-items: center;">
<v-icon color="black" class="me-2">mdi-account-group</v-icon>{{ tm('title') }}
</h1>
<p class="text-subtitle-1 text-medium-emphasis mb-4">
{{ subtitle }}
</p>
</div>
<v-btn
color="primary"
prepend-icon="mdi-refresh"
variant="tonal"
@click="refreshSessions"
:loading="loading"
rounded="xl"
size="x-large"
>
{{ tm('buttons.refresh') }}
</v-btn>
</v-row>
<v-card elevation="0" rounded="12" class="session-card">
<v-card-title class="bg-primary text-white py-3 px-4 session-card-title">
<v-icon color="white" class="me-2">mdi-account-group</v-icon>
<span class="text-h4 mr-4">{{ tm('sessions.activeSessions') }}</span>
<v-chip color="white" text-color="primary" small>
{{ sessions.length }} {{ tm('sessions.sessionCount') }}
</v-chip>
</v-card-title>
<v-card-text class="pa-0">
<!-- 搜索栏 -->
<v-toolbar flat class="px-4">
<v-text-field
v-model="searchQuery"
prepend-inner-icon="mdi-magnify"
:label="tm('search.placeholder')"
hide-details
clearable
variant="outlined"
class="me-4"
density="compact"
></v-text-field>
<v-select
v-model="filterPlatform"
:items="platformOptions"
:label="tm('search.platformFilter')"
hide-details
clearable
variant="outlined"
class="me-4"
style="max-width: 150px;"
density="compact"
></v-select>
</v-toolbar>
<v-divider></v-divider>
<!-- 会话列表 -->
<v-data-table
:headers="headers"
:items="filteredSessions"
:loading="loading"
:items-per-page="itemsPerPage"
class="elevation-0"
>
<!-- 会话信息 -->
<template v-slot:item.session_info="{ item }">
<div class="py-2">
<div class="d-flex align-center">
<div class="flex-grow-1">
<div class="font-weight-medium d-flex align-center">
<v-tooltip
location="top"
>
<template v-slot:activator="{ props: tooltipProps }">
<span v-bind="tooltipProps">{{ item.session_name }}</span>
</template>
使用 /sid 指令可查看会话 ID
</v-tooltip>
<v-tooltip
v-if="item.session_name !== item.session_raw_name"
activator="parent"
location="top"
>
<span class="text-caption">实际UMO: {{ item.session_raw_name }}</span>
</v-tooltip>
<v-icon
v-if="item.session_name !== item.session_raw_name"
size="12"
color="warning"
class="ml-1"
>
mdi-information-outline
</v-icon>
</div>
<div class="text-caption text-grey-600">
<v-chip
:color="getPlatformColor(item.platform)"
size="x-small"
class="me-1"
>
{{ item.platform }}
</v-chip>
{{ item.message_type }}
</div>
</div>
<v-btn
icon
size="small"
variant="text"
color="primary"
@click="openNameEditor(item)"
:loading="item.updating"
>
<v-icon size="16">mdi-pencil</v-icon>
<v-tooltip activator="parent" location="top">
{{ tm('buttons.editName') }}
</v-tooltip>
</v-btn>
</div>
</div>
</template>
<!-- 人格 -->
<template v-slot:item.persona="{ item }">
<v-select
:model-value="item.persona_id || ''"
:items="personaOptions"
item-title="label"
item-value="value"
hide-details
density="compact"
variant="outlined"
@update:model-value="(value) => updatePersona(item, value)"
:loading="item.updating"
:disabled="!item.session_enabled"
>
<template v-slot:selection="{ item: selection }">
<v-chip
size="small"
:color="selection.raw.value === '[%None]' ? 'grey' : 'primary'"
>
{{ selection.raw.label }}
</v-chip>
</template>
</v-select>
</template>
<!-- Chat Provider -->
<template v-slot:item.chat_provider="{ item }">
<v-select
:model-value="item.chat_provider_id || ''"
:items="chatProviderOptions"
item-title="label"
item-value="value"
hide-details
density="compact"
variant="outlined"
@update:model-value="(value) => updateProvider(item, value, 'chat_completion')"
:loading="item.updating"
:disabled="!item.session_enabled"
>
<template v-slot:selection="{ item: selection }">
<v-chip size="small" color="success">
{{ selection.raw.label }}
</v-chip>
</template>
</v-select>
</template>
<!-- STT Provider -->
<template v-slot:item.stt_provider="{ item }">
<v-select
:model-value="item.stt_provider_id || ''"
:items="sttProviderOptions"
item-title="label"
item-value="value"
hide-details
density="compact"
variant="outlined"
@update:model-value="(value) => updateProvider(item, value, 'speech_to_text')"
:loading="item.updating"
:disabled="sttProviderOptions.length === 0 || !item.session_enabled"
>
<template v-slot:selection="{ item: selection }">
<v-chip size="small" color="info">
{{ selection.raw.label }}
</v-chip>
</template>
</v-select>
</template>
<!-- TTS Provider -->
<template v-slot:item.tts_provider="{ item }">
<v-select
:model-value="item.tts_provider_id || ''"
:items="ttsProviderOptions"
item-title="label"
item-value="value"
hide-details
density="compact"
variant="outlined"
@update:model-value="(value) => updateProvider(item, value, 'text_to_speech')"
:loading="item.updating"
:disabled="ttsProviderOptions.length === 0 || !item.session_enabled"
>
<template v-slot:selection="{ item: selection }">
<v-chip size="small" color="warning">
{{ selection.raw.label }}
</v-chip>
</template>
</v-select> </template>
<!-- 会话启停 -->
<template v-slot:item.session_enabled="{ item }">
<v-switch
:model-value="item.session_enabled"
@update:model-value="(value) => updateSessionStatus(item, value)"
:loading="item.updating"
hide-details
density="compact"
color="success"
inset
>
</v-switch>
</template>
<!-- LLM启停 -->
<template v-slot:item.llm_enabled="{ item }">
<v-switch
:model-value="item.llm_enabled"
@update:model-value="(value) => updateLLM(item, value)"
:loading="item.updating"
:disabled="!item.session_enabled"
hide-details
density="compact"
color="primary"
inset
>
</v-switch>
</template>
<!-- TTS启停 -->
<template v-slot:item.tts_enabled="{ item }">
<v-switch
:model-value="item.tts_enabled"
@update:model-value="(value) => updateTTS(item, value)"
:loading="item.updating"
:disabled="!item.session_enabled"
hide-details
density="compact"
color="secondary"
inset
>
</v-switch>
</template>
<!-- 插件管理 -->
<template v-slot:item.plugins="{ item }">
<v-btn
size="small"
variant="outlined"
color="primary"
@click="openPluginManager(item)"
:loading="item.loadingPlugins"
:disabled="!item.session_enabled"
>
{{ tm('buttons.edit') }}
</v-btn>
</template>
<!-- 空状态 -->
<template v-slot:no-data>
<div class="text-center py-8">
<v-icon size="64" color="grey-400">mdi-account-group-outline</v-icon>
<div class="text-h6 mt-4 text-grey-600">{{ tm('sessions.noActiveSessions') }}</div>
<div class="text-body-2 text-grey-500">{{ tm('sessions.noActiveSessionsDesc') }}</div>
</div>
</template>
</v-data-table>
</v-card-text>
</v-card>
<!-- 批量操作面板 -->
<v-card elevation="0" class="mt-4 batch-operations-card" rounded="12">
<v-card-title class="bg-secondary text-white py-3 px-4">
<v-icon color="white" class="me-2">mdi-cog-outline</v-icon>
<span class="text-h4">{{ tm('batchOperations.title') }}</span>
</v-card-title>
<v-card-text>
<div style="padding: 16px;">
<v-row>
<v-col cols="12" md="6" lg="3" v-if="availablePersonas.length > 0">
<v-select
v-model="batchPersona"
:items="personaOptions"
item-title="label"
item-value="value"
:label="tm('batchOperations.setPersona')"
hide-details
clearable
variant="outlined"
density="comfortable"
class="batch-select"
></v-select>
</v-col>
<v-col cols="12" md="6" lg="3" v-if="availableChatProviders.length > 0">
<v-select
v-model="batchChatProvider"
:items="chatProviderOptions"
item-title="label"
item-value="value"
:label="tm('batchOperations.setChatProvider')"
hide-details
clearable
variant="outlined"
density="comfortable"
class="batch-select"
></v-select>
</v-col>
<v-col cols="12" md="6" lg="3">
<v-select
v-model="batchSttProvider"
:items="sttProviderOptions"
item-title="label"
item-value="value"
:label="tm('batchOperations.setSttProvider')"
hide-details
clearable
variant="outlined"
density="comfortable"
class="batch-select"
:disabled="availableSttProviders.length === 0"
:placeholder="availableSttProviders.length === 0 ? tm('batchOperations.noSttProvider') : ''"
></v-select>
</v-col>
<v-col cols="12" md="6" lg="3">
<v-select
v-model="batchTtsProvider"
:items="ttsProviderOptions"
item-title="label"
item-value="value"
:label="tm('batchOperations.setTtsProvider')"
hide-details
clearable
variant="outlined"
density="comfortable"
class="batch-select"
:disabled="availableTtsProviders.length === 0"
:placeholder="availableTtsProviders.length === 0 ? tm('batchOperations.noTtsProvider') : ''"
></v-select>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="6" lg="3">
<v-select
v-model="batchLlmStatus"
:items="[{label: tm('status.enabled'), value: true}, {label: tm('status.disabled'), value: false}]"
item-title="label"
item-value="value"
:label="tm('batchOperations.setLlmStatus')"
hide-details
clearable
variant="outlined"
density="comfortable"
class="batch-select"
></v-select>
</v-col>
<v-col cols="12" md="6" lg="3">
<v-select
v-model="batchTtsStatus"
:items="[{label: tm('status.enabled'), value: true}, {label: tm('status.disabled'), value: false}]"
item-title="label"
item-value="value"
:label="tm('batchOperations.setTtsStatus')"
hide-details
clearable
variant="outlined"
density="comfortable"
class="batch-select"
></v-select>
</v-col>
</v-row>
<div class="d-flex justify-end align-center mt-8">
<v-btn
color="primary"
variant="tonal"
size="large"
rounded="lg"
@click="applyBatchChanges"
:disabled="!batchPersona && !batchChatProvider && !batchSttProvider && !batchTtsProvider && batchLlmStatus === null && batchTtsStatus === null"
:loading="batchUpdating"
class="me-3"
>
<v-icon start>mdi-check-all</v-icon>
{{ tm('buttons.apply') }}
</v-btn>
</div>
</div>
</v-card-text>
</v-card>
<!-- 插件管理对话框 -->
<v-dialog v-model="pluginDialog" max-width="800" min-height="80%">
<v-card v-if="selectedSessionForPlugin">
<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-puzzle</v-icon>
<span>{{ tm('pluginManagement.title') }} - {{ selectedSessionForPlugin.session_name }}</span>
<v-spacer></v-spacer>
<v-btn icon variant="text" color="white" @click="pluginDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text v-if="!loadingPlugins">
<div style="padding-left: 16px; padding-right: 16px;">
<div v-if="sessionPlugins.length === 0" class="text-center py-8">
<v-icon size="64" color="grey-400">mdi-puzzle-outline</v-icon>
<div class="text-h6 mt-4 text-grey-600">{{ tm('pluginManagement.noPlugins') }}</div>
<div class="text-body-2 text-grey-500">{{ tm('pluginManagement.noPluginsDesc') }}</div>
</div>
<v-list v-else>
<v-list-item
v-for="plugin in sessionPlugins"
:key="plugin.name"
class="px-0"
>
<template v-slot:prepend>
<v-icon :color="plugin.enabled ? 'success' : 'grey'">
{{ plugin.enabled ? 'mdi-check-circle' : 'mdi-circle-outline' }}
</v-icon>
</template>
<v-list-item-title class="font-weight-medium">
{{ plugin.name }}
</v-list-item-title>
<v-list-item-subtitle>
{{ tm('pluginManagement.author') }}: {{ plugin.author }}
</v-list-item-subtitle>
<template v-slot:append>
<v-switch
:model-value="plugin.enabled"
hide-details
color="primary"
@update:model-value="(value) => togglePlugin(plugin, value)"
:loading="plugin.updating"
></v-switch>
</template>
</v-list-item>
</v-list>
</div>
</v-card-text>
<v-card-text v-else class="text-center py-8">
<v-progress-circular indeterminate color="primary" size="48"></v-progress-circular>
<div class="text-body-1 mt-4">{{ tm('pluginManagement.loading') }}</div>
</v-card-text>
</v-card>
</v-dialog>
<!-- 会话命名编辑对话框 -->
<v-dialog v-model="nameEditDialog" max-width="500" min-height="60%">
<v-card v-if="selectedSessionForName">
<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-rename-box</v-icon>
<span>{{ tm('nameEditor.title') }}</span>
<v-spacer></v-spacer>
<v-btn icon variant="text" color="white" @click="nameEditDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<div style="padding-left: 16px; padding-right: 16px;">
<v-text-field
v-model="newSessionName"
:label="tm('nameEditor.customName')"
:placeholder="tm('nameEditor.placeholder')"
variant="outlined"
hide-details="auto"
clearable
class="mb-4"
@keyup.enter="saveSessionName"
></v-text-field>
<div class="text-caption text-grey-600 mb-2">
{{ tm('nameEditor.originalName') }}: {{ selectedSessionForName.session_raw_name }}
</div>
<div class="text-caption text-grey-600 mb-2">
{{ tm('nameEditor.fullSessionId') }}: {{ selectedSessionForName.session_id }}
</div>
<v-alert
variant="tonal"
type="info"
density="compact"
class="mb-4"
>
{{ tm('nameEditor.hint') }}
</v-alert>
</div>
</v-card-text>
<v-card-actions class="px-4 pb-4">
<v-spacer></v-spacer>
<v-btn
color="grey"
variant="text"
@click="nameEditDialog = false"
:disabled="nameEditLoading"
>
{{ tm('buttons.cancel') }}
</v-btn>
<v-btn
color="primary"
variant="tonal"
@click="saveSessionName"
:loading="nameEditLoading"
>
{{ tm('buttons.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 提示信息 -->
<v-snackbar v-model="snackbar" :timeout="3000" elevation="24" :color="snackbarColor" location="top">
{{ snackbarText }}
</v-snackbar>
</v-container>
</div>
</template>
<script>
import axios from 'axios'
import { useI18n, useModuleI18n } from '@/i18n/composables'
export default {
name: 'SessionManagementPage',
setup() {
const { t } = useI18n()
const { tm } = useModuleI18n('features/session-management')
return {
t,
tm
}
},
data() {
return {
loading: false,
sessions: [],
searchQuery: '',
filterPlatform: null,
// 分页相关
itemsPerPage: 10,
// 可用选项
availablePersonas: [],
availableChatProviders: [],
availableSttProviders: [],
availableTtsProviders: [],
// 批量操作
batchPersona: null,
batchChatProvider: null,
batchSttProvider: null,
batchTtsProvider: null,
batchLlmStatus: null,
batchTtsStatus: null,
batchUpdating: false,
// 插件管理
pluginDialog: false,
selectedSessionForPlugin: null,
sessionPlugins: [],
loadingPlugins: false,
// 会话命名编辑器
nameEditDialog: false,
selectedSessionForName: null,
newSessionName: '',
nameEditLoading: false,
// 提示信息
snackbar: false,
snackbarText: '',
snackbarColor: 'success',
}
},
computed: {
// 安全访问翻译的计算属性
messages() {
return {
updateSuccess: this.tm('messages.updateSuccess'),
addSuccess: this.tm('messages.addSuccess'),
deleteSuccess: this.tm('messages.deleteSuccess'),
statusUpdateSuccess: this.tm('messages.statusUpdateSuccess'),
deleteConfirm: this.tm('messages.deleteConfirm')
};
},
subtitle() {
return this.tm('subtitle') || '管理所有活跃的会话配置人格、LLM提供商和插件';
},
headers() {
return [
{ title: this.tm('table.headers.sessionStatus'), key: 'session_enabled', sortable: false, width: '120px' },
{ title: this.tm('table.headers.sessionInfo'), key: 'session_info', sortable: false, width: '200px' },
{ title: this.tm('table.headers.persona'), key: 'persona', sortable: false, width: '180px' },
{ title: this.tm('table.headers.chatProvider'), key: 'chat_provider', sortable: false, width: '180px' },
{ title: this.tm('table.headers.sttProvider'), key: 'stt_provider', sortable: false, width: '150px' },
{ title: this.tm('table.headers.ttsProvider'), key: 'tts_provider', sortable: false, width: '150px' },
{ title: this.tm('table.headers.llmStatus'), key: 'llm_enabled', sortable: false, width: '120px' },
{ title: this.tm('table.headers.ttsStatus'), key: 'tts_enabled', sortable: false, width: '120px' },
{ title: this.tm('table.headers.pluginManagement'), key: 'plugins', sortable: false, width: '120px' },
]
},
// 懒加载过滤会话 - 使用客户端分页
filteredSessions() {
let filtered = this.sessions;
// 搜索筛选
if (this.searchQuery) {
const query = this.searchQuery.toLowerCase();
filtered = filtered.filter(session =>
session.session_name.toLowerCase().includes(query) ||
session.platform.toLowerCase().includes(query) ||
session.persona_name?.toLowerCase().includes(query) ||
session.chat_provider_name?.toLowerCase().includes(query)
);
}
// 平台筛选
if (this.filterPlatform) {
filtered = filtered.filter(session => session.platform === this.filterPlatform);
}
return filtered;
},
platformOptions() {
const platforms = [...new Set(this.sessions.map(s => s.platform))];
return platforms.map(p => ({ title: p, value: p }));
},
personaOptions() {
const options = [
{ label: this.tm('persona.none'), value: '[%None]' },
...this.availablePersonas.map(p => ({
label: p.name,
value: p.name
}))
];
return options;
},
chatProviderOptions() {
return this.availableChatProviders.map(p => ({
label: `${p.name} (${p.model})`,
value: p.id
}));
},
sttProviderOptions() {
return this.availableSttProviders.map(p => ({
label: `${p.name} (${p.model})`,
value: p.id
}));
},
ttsProviderOptions() {
return this.availableTtsProviders.map(p => ({
label: `${p.name} (${p.model})`,
value: p.id
}));
},
},
mounted() {
this.loadSessions();
},
methods: {
async loadSessions() {
this.loading = true;
try {
const response = await axios.get('/api/session/list');
if (response.data.status === 'ok') {
const data = response.data.data;
this.sessions = data.sessions.map(session => ({
...session,
updating: false, // 添加更新状态标志
loadingPlugins: false // 添加插件加载状态标志
}));
this.availablePersonas = data.available_personas;
this.availableChatProviders = data.available_chat_providers;
this.availableSttProviders = data.available_stt_providers;
this.availableTtsProviders = data.available_tts_providers;
} else {
this.showError(response.data.message || this.tm('messages.loadSessionsError'));
}
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.loadSessionsError'));
}
this.loading = false;
},
async refreshSessions() {
await this.loadSessions();
this.showSuccess(this.tm('messages.refreshSuccess'));
},
async updatePersona(session, personaName) {
session.updating = true;
try {
const response = await axios.post('/api/session/update_persona', {
session_id: session.session_id,
persona_name: personaName
});
if (response.data.status === 'ok') {
session.persona_id = personaName;
session.persona_name = personaName === '[%None]' ? this.tm('persona.none') :
this.availablePersonas.find(p => p.name === personaName)?.name || personaName;
this.showSuccess(this.tm('messages.personaUpdateSuccess'));
} else {
this.showError(response.data.message || this.tm('messages.personaUpdateError'));
}
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.personaUpdateError'));
}
session.updating = false;
},
async updateProvider(session, providerId, providerType) {
session.updating = true;
try {
const response = await axios.post('/api/session/update_provider', {
session_id: session.session_id,
provider_id: providerId,
provider_type: providerType
});
if (response.data.status === 'ok') {
// 更新本地数据
if (providerType === 'chat_completion') {
session.chat_provider_id = providerId;
const provider = this.availableChatProviders.find(p => p.id === providerId);
session.chat_provider_name = provider?.name || providerId;
} else if (providerType === 'speech_to_text') {
session.stt_provider_id = providerId;
const provider = this.availableSttProviders.find(p => p.id === providerId);
session.stt_provider_name = provider?.name || providerId;
} else if (providerType === 'text_to_speech') {
session.tts_provider_id = providerId;
const provider = this.availableTtsProviders.find(p => p.id === providerId);
session.tts_provider_name = provider?.name || providerId;
}
this.showSuccess(this.tm('messages.providerUpdateSuccess'));
} else {
this.showError(response.data.message || this.tm('messages.providerUpdateError'));
}
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.providerUpdateError'));
} session.updating = false;
},
async updateSessionStatus(session, enabled) {
session.updating = true;
try {
const response = await axios.post('/api/session/update_status', {
session_id: session.session_id,
session_enabled: enabled
});
if (response.data.status === 'ok') {
session.session_enabled = enabled;
this.showSuccess(this.tm('messages.sessionStatusSuccess', { status: enabled ? this.tm('status.enabled') : this.tm('status.disabled') }));
} else {
this.showError(response.data.message || this.tm('messages.statusUpdateError'));
}
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.statusUpdateError'));
}
session.updating = false;
},
async updateLLM(session, enabled) {
session.updating = true;
try {
const response = await axios.post('/api/session/update_llm', {
session_id: session.session_id,
enabled: enabled
});
if (response.data.status === 'ok') {
session.llm_enabled = enabled;
this.showSuccess(this.tm('messages.llmStatusSuccess', { status: enabled ? this.tm('status.enabled') : this.tm('status.disabled') }));
} else {
this.showError(response.data.message || this.tm('messages.statusUpdateError'));
}
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.statusUpdateError'));
}
session.updating = false;
},
async updateTTS(session, enabled) {
session.updating = true;
try {
const response = await axios.post('/api/session/update_tts', {
session_id: session.session_id,
enabled: enabled
});
if (response.data.status === 'ok') {
session.tts_enabled = enabled;
this.showSuccess(this.tm('messages.ttsStatusSuccess', { status: enabled ? this.tm('status.enabled') : this.tm('status.disabled') }));
} else {
this.showError(response.data.message || this.tm('messages.statusUpdateError'));
}
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.statusUpdateError'));
}
session.updating = false;
},
async applyBatchChanges() {
if (!this.batchPersona && !this.batchChatProvider && !this.batchSttProvider && !this.batchTtsProvider && this.batchLlmStatus === null && this.batchTtsStatus === null) {
return;
}
this.batchUpdating = true;
let successCount = 0;
let errorCount = 0;
// 使用过滤后的会话数据进行批量操作
for (const session of this.filteredSessions) {
try {
// 批量更新人格
if (this.batchPersona) {
await this.updatePersona(session, this.batchPersona);
successCount++;
}
// 批量更新 Chat Provider
if (this.batchChatProvider) {
await this.updateProvider(session, this.batchChatProvider, 'chat_completion');
successCount++;
}
// 批量更新 STT Provider
if (this.batchSttProvider) {
await this.updateProvider(session, this.batchSttProvider, 'speech_to_text');
successCount++;
}
// 批量更新 TTS Provider
if (this.batchTtsProvider) {
await this.updateProvider(session, this.batchTtsProvider, 'text_to_speech');
successCount++;
}
// 批量更新 LLM 状态
if (this.batchLlmStatus !== null) {
await this.updateLLM(session, this.batchLlmStatus);
successCount++;
}
// 批量更新 TTS 状态
if (this.batchTtsStatus !== null) {
await this.updateTTS(session, this.batchTtsStatus);
successCount++;
}
} catch (error) {
errorCount++;
}
}
this.batchUpdating = false;
if (errorCount === 0) {
this.showSuccess(this.tm('messages.batchUpdateSuccess', { count: successCount }));
} else {
this.showError(this.tm('messages.batchUpdatePartial', { success: successCount, error: errorCount }));
}
// 清空批量设置
this.batchPersona = null;
this.batchChatProvider = null;
this.batchSttProvider = null;
this.batchTtsProvider = null;
this.batchLlmStatus = null;
this.batchTtsStatus = null;
},
async openPluginManager(session) {
this.selectedSessionForPlugin = session;
this.pluginDialog = true;
this.loadingPlugins = true;
this.sessionPlugins = [];
try {
const response = await axios.get('/api/session/plugins', {
params: { session_id: session.session_id }
});
if (response.data.status === 'ok') {
this.sessionPlugins = response.data.data.plugins.map(plugin => ({
...plugin,
updating: false
}));
} else {
this.showError(response.data.message || this.tm('messages.loadPluginsError'));
}
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.loadPluginsError'));
}
this.loadingPlugins = false;
},
async togglePlugin(plugin, enabled) {
plugin.updating = true;
try {
const response = await axios.post('/api/session/update_plugin', {
session_id: this.selectedSessionForPlugin.session_id,
plugin_name: plugin.name,
enabled: enabled
});
if (response.data.status === 'ok') {
plugin.enabled = enabled;
this.showSuccess(this.tm('messages.pluginStatusSuccess', {
name: plugin.name,
status: enabled ? this.tm('status.enabled') : this.tm('status.disabled')
}));
} else {
this.showError(response.data.message || this.tm('messages.pluginStatusError'));
}
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.pluginStatusError'));
}
plugin.updating = false;
},
openNameEditor(session) {
this.selectedSessionForName = session;
this.newSessionName = session.session_name === session.session_raw_name ? '' : session.session_name;
this.nameEditDialog = true;
},
async saveSessionName() {
if (!this.selectedSessionForName) return;
this.nameEditLoading = true;
try {
const response = await axios.post('/api/session/update_name', {
session_id: this.selectedSessionForName.session_id,
custom_name: this.newSessionName || ''
});
if (response.data.status === 'ok') {
// 更新本地数据
this.selectedSessionForName.session_name = response.data.data.display_name;
this.showSuccess(response.data.data.message || this.tm('messages.nameUpdateSuccess'));
this.nameEditDialog = false;
} else {
this.showError(response.data.message || this.tm('messages.nameUpdateError'));
}
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.nameUpdateError'));
}
this.nameEditLoading = false;
},
getPlatformColor(platform) {
const colors = {
'aiocqhttp': 'blue',
'wechatpadpro': 'green',
'qq_official': 'purple',
'telegram': 'light-blue',
'discord': 'indigo',
'default': 'grey'
};
return colors[platform] || colors.default;
},
showSuccess(message) {
this.snackbarText = message;
this.snackbarColor = 'success';
this.snackbar = true;
},
showError(message) {
this.snackbarText = message;
this.snackbarColor = 'error';
this.snackbar = true;
},
},
}
</script>
<style scoped>
.session-management-page {
padding: 20px;
padding-top: 8px;
}
.v-data-table >>> .v-data-table__td {
padding: 8px 16px !important;
vertical-align: middle !important;
}
.v-data-table >>> .v-data-table__th {
vertical-align: middle !important;
}
.v-select >>> .v-field__input {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.session-card {
border-radius: 12px !important;
overflow: hidden;
}
.session-card-title {
border-top-left-radius: 12px !important;
border-top-right-radius: 12px !important;
}
/* 对话框标题栏样式 */
.v-card .v-card-title {
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
/* 表格行悬停效果 */
.v-data-table >>> .v-data-table__tr:hover {
background-color: rgba(var(--v-theme-primary), 0.02) !important;
}
/* 工具栏样式改进 */
.v-toolbar {
background-color: rgba(var(--v-theme-surface), 0.8) !important;
}
/* 卡片内边距改进 */
.v-card-text {
padding: 16px 0 !important;
}
</style>