| <template> |
| <div class="session-management-page"> |
| <v-container fluid class="pa-0"> |
| <v-card flat> |
| <v-card-title class="d-flex align-center py-3 px-4"> |
| <span class="text-h4">{{ tm('customRules.title') }}</span> |
| <v-btn icon="mdi-information-outline" size="small" variant="text" href="https://astrbot.app/use/custom-rules.html" target="_blank"></v-btn> |
| <v-chip size="small" class="ml-1">{{ totalItems }} {{ tm('customRules.rulesCount') }}</v-chip> |
| <v-row class="me-4 ms-4" dense> |
| <v-text-field v-model="searchQuery" prepend-inner-icon="mdi-magnify" :label="tm('search.placeholder')" |
| hide-details clearable variant="solo-filled" flat class="me-4" density="compact"></v-text-field> |
| </v-row> |
| <v-btn v-if="selectedItems.length > 0" color="error" prepend-icon="mdi-delete" variant="tonal" |
| @click="confirmBatchDelete" class="mr-2" size="small"> |
| {{ tm('buttons.batchDelete') }} ({{ selectedItems.length }}) |
| </v-btn> |
| <v-btn color="success" prepend-icon="mdi-plus" variant="tonal" @click="openAddRuleDialog" class="mr-2" |
| size="small"> |
| {{ tm('buttons.addRule') }} |
| </v-btn> |
| <v-btn color="primary" prepend-icon="mdi-refresh" variant="tonal" @click="refreshData" :loading="loading" |
| size="small"> |
| {{ tm('buttons.refresh') }} |
| </v-btn> |
| </v-card-title> |
| |
| <v-divider></v-divider> |
| |
| <v-card-text class="pa-0"> |
| <v-data-table-server :headers="headers" :items="filteredRulesList" :loading="loading" |
| :items-length="totalItems" v-model:items-per-page="itemsPerPage" v-model:page="currentPage" |
| @update:options="onTableOptionsUpdate" class="elevation-0" style="font-size: 12px;" v-model="selectedItems" |
| show-select item-value="umo" return-object> |
| |
| |
| <template v-slot:item.umo_info="{ item }"> |
| <div> |
| <div class="d-flex align-center"> |
| <v-chip size="x-small" :color="getPlatformColor(item.platform)" class="mr-2"> |
| {{ item.platform || 'unknown' }} |
| </v-chip> |
| <span class="text-truncate" style="max-width: 300px;">{{ item.umo }}</span> |
| <div class="d-flex align-center" v-if="item.rules?.session_service_config?.custom_name || true"> |
| <span class="ml-2" style="color: gray; font-size: 10px;" |
| v-if="item.rules?.session_service_config?.custom_name"> |
| ({{ item.rules?.session_service_config?.custom_name }}) |
| </span> |
| <v-btn icon size="x-small" variant="text" class="ml-1" @click.stop="openQuickEditName(item)"> |
| <v-icon size="small" color="grey">mdi-pencil-outline</v-icon> |
| <v-tooltip activator="parent" location="top">{{ tm('buttons.editCustomName') }}</v-tooltip> |
| </v-btn> |
| </div> |
| <v-tooltip location="top"> |
| <template v-slot:activator="{ props }"> |
| <v-icon v-bind="props" size="small" class="ml-1">mdi-information-outline</v-icon> |
| </template> |
| <div> |
| <p>UMO: {{ item.umo }}</p> |
| <p v-if="item.platform">平台: {{ item.platform }}</p> |
| <p v-if="item.message_type">消息类型: {{ item.message_type }}</p> |
| <p v-if="item.session_id">会话 ID: {{ item.session_id }}</p> |
| </div> |
| </v-tooltip> |
| </div> |
| |
| </div> |
| </template> |
| |
| |
| <template v-slot:item.rules_overview="{ item }"> |
| <div class="d-flex flex-wrap ga-1"> |
| <v-chip v-if="item.rules.session_service_config" size="x-small" color="primary" variant="outlined"> |
| {{ tm('customRules.serviceConfig') }} |
| </v-chip> |
| <v-chip v-if="item.rules.session_plugin_config" size="x-small" color="secondary" variant="outlined"> |
| {{ tm('customRules.pluginConfig') }} |
| </v-chip> |
| <v-chip v-if="item.rules.kb_config" size="x-small" color="info" variant="outlined"> |
| {{ tm('customRules.kbConfig') }} |
| </v-chip> |
| <v-chip v-if="hasProviderConfig(item.rules)" size="x-small" color="warning" variant="outlined"> |
| {{ tm('customRules.providerConfig') }} |
| </v-chip> |
| </div> |
| </template> |
| |
| |
| <template v-slot:item.actions="{ item }"> |
| <v-btn size="small" variant="tonal" color="primary" @click="openRuleEditor(item)" class="mr-1"> |
| <v-icon>mdi-pencil</v-icon> |
| <v-tooltip activator="parent" location="top">{{ tm('buttons.editRule') }}</v-tooltip> |
| </v-btn> |
| <v-btn size="small" variant="tonal" color="error" @click="confirmDeleteRules(item)"> |
| <v-icon>mdi-delete</v-icon> |
| <v-tooltip activator="parent" location="top">{{ tm('buttons.deleteAllRules') }}</v-tooltip> |
| </v-btn> |
| </template> |
| |
| |
| <template v-slot:no-data> |
| <div class="text-center py-8"> |
| <v-icon size="64" color="grey-400">mdi-file-document-edit-outline</v-icon> |
| <div class="text-h6 mt-4 text-grey-600">{{ tm('customRules.noRules') }}</div> |
| <div class="text-body-2 text-grey-500">{{ tm('customRules.noRulesDesc') }}</div> |
| <v-btn color="primary" variant="tonal" class="mt-4" @click="openAddRuleDialog"> |
| <v-icon start>mdi-plus</v-icon> |
| {{ tm('buttons.addRule') }} |
| </v-btn> |
| </div> |
| </template> |
| </v-data-table-server> |
| </v-card-text> |
| </v-card> |
| |
| <v-card flat class="mt-4"> |
| <v-card-title class="d-flex align-center py-3 px-4"> |
| <span class="text-h6">{{ tm('batchOperations.title') }}</span> |
| <v-chip size="small" class="ml-2" color="info" variant="outlined"> |
| {{ tm('batchOperations.hint') }} |
| </v-chip> |
| </v-card-title> |
| <v-card-text> |
| <v-row dense> |
| <v-col cols="12" md="6" lg="3"> |
| <v-select v-model="batchScope" :items="batchScopeOptions" item-title="label" item-value="value" |
| :label="tm('batchOperations.scope')" hide-details variant="solo-filled" flat density="comfortable"> |
| </v-select> |
| </v-col> |
| <v-col cols="12" md="6" lg="3"> |
| <v-select v-model="batchLlmStatus" :items="statusOptions" item-title="label" item-value="value" |
| :label="tm('batchOperations.llmStatus')" hide-details clearable variant="solo-filled" flat density="comfortable"> |
| </v-select> |
| </v-col> |
| <v-col cols="12" md="6" lg="3"> |
| <v-select v-model="batchTtsStatus" :items="statusOptions" item-title="label" item-value="value" |
| :label="tm('batchOperations.ttsStatus')" hide-details clearable variant="solo-filled" flat density="comfortable"> |
| </v-select> |
| </v-col> |
| <v-col cols="12" md="6" lg="3"> |
| <v-select v-model="batchChatProvider" :items="chatProviderOptions" item-title="label" item-value="value" |
| :label="tm('batchOperations.chatProvider')" hide-details clearable variant="solo-filled" flat density="comfortable"> |
| </v-select> |
| </v-col> |
| </v-row> |
| <v-row dense class="mt-3"> |
| <v-col cols="12" class="d-flex justify-end"> |
| <v-btn color="primary" variant="tonal" size="large" @click="applyBatchChanges" |
| :disabled="!canApplyBatch" :loading="batchUpdating" prepend-icon="mdi-check-all"> |
| {{ tm('batchOperations.apply') }} |
| </v-btn> |
| </v-col> |
| </v-row> |
| </v-card-text> |
| </v-card> |
| |
| |
| <v-card flat class="mt-4"> |
| <v-card-title class="d-flex align-center py-3 px-4"> |
| <span class="text-h6">分组管理</span> |
| <v-chip size="small" class="ml-2" color="secondary" variant="outlined"> |
| {{ groups.length }} 个分组 |
| </v-chip> |
| <v-spacer></v-spacer> |
| <v-btn v-if="selectedItems.length > 0 && groups.length > 0" color="info" variant="tonal" size="small" class="mr-2"> |
| <v-icon start>mdi-folder-plus</v-icon> |
| 添加到分组 |
| <v-menu activator="parent"> |
| <v-list density="compact"> |
| <v-list-item v-for="g in groups" :key="g.id" @click="addSelectedToGroup(g.id)"> |
| <v-list-item-title>{{ g.name }} ({{ g.umo_count }})</v-list-item-title> |
| </v-list-item> |
| </v-list> |
| </v-menu> |
| </v-btn> |
| <v-btn color="success" variant="tonal" size="small" @click="openCreateGroupDialog" prepend-icon="mdi-folder-plus"> |
| 新建分组 |
| </v-btn> |
| </v-card-title> |
| <v-card-text v-if="groups.length > 0"> |
| <v-row dense> |
| <v-col v-for="group in groups" :key="group.id" cols="12" sm="6" md="4" lg="3"> |
| <v-card variant="outlined" class="pa-3"> |
| <div class="d-flex align-center justify-space-between"> |
| <div> |
| <div class="font-weight-bold">{{ group.name }}</div> |
| <div class="text-caption text-grey">{{ group.umo_count }} 个会话</div> |
| </div> |
| <div> |
| <v-btn icon size="small" variant="text" @click="openEditGroupDialog(group)"> |
| <v-icon size="small">mdi-pencil</v-icon> |
| </v-btn> |
| <v-btn icon size="small" variant="text" color="error" @click="deleteGroup(group)"> |
| <v-icon size="small">mdi-delete</v-icon> |
| </v-btn> |
| </div> |
| </div> |
| </v-card> |
| </v-col> |
| </v-row> |
| </v-card-text> |
| <v-card-text v-else class="text-center text-grey py-6"> |
| 暂无分组,点击「新建分组」创建 |
| </v-card-text> |
| </v-card> |
| |
| |
| <v-dialog v-model="groupDialog" max-width="800" @after-enter="loadAvailableUmos"> |
| <v-card> |
| <v-card-title class="py-3 px-4"> |
| {{ groupDialogMode === 'create' ? '新建分组' : '编辑分组' }} |
| </v-card-title> |
| <v-card-text> |
| <v-text-field v-model="editingGroup.name" label="分组名称" variant="outlined" hide-details class="mb-4"></v-text-field> |
| <v-row dense> |
| |
| <v-col cols="5"> |
| <div class="text-subtitle-2 mb-2">可选会话 ({{ unselectedUmos.length }})</div> |
| <v-text-field v-model="groupMemberSearch" placeholder="搜索..." variant="outlined" density="compact" hide-details class="mb-2" clearable prepend-inner-icon="mdi-magnify"></v-text-field> |
| <v-list density="compact" class="transfer-list" lines="one"> |
| <v-list-item v-for="umo in filteredUnselectedUmos" :key="umo" @click="addToGroup(umo)" class="transfer-item"> |
| <template v-slot:prepend> |
| <v-icon size="small" color="grey">mdi-plus</v-icon> |
| </template> |
| <v-list-item-title class="text-caption">{{ formatUmoShort(umo) }}</v-list-item-title> |
| </v-list-item> |
| <v-list-item v-if="filteredUnselectedUmos.length === 0 && !loadingUmos"> |
| <v-list-item-title class="text-caption text-grey text-center">无匹配项</v-list-item-title> |
| </v-list-item> |
| <v-list-item v-if="loadingUmos"> |
| <v-list-item-title class="text-center"><v-progress-circular indeterminate size="20"></v-progress-circular></v-list-item-title> |
| </v-list-item> |
| </v-list> |
| </v-col> |
| |
| <v-col cols="2" class="d-flex flex-column align-center justify-center"> |
| <v-btn icon size="small" variant="tonal" color="primary" class="mb-2" @click="addAllToGroup" :disabled="unselectedUmos.length === 0"> |
| <v-icon>mdi-chevron-double-right</v-icon> |
| </v-btn> |
| <v-btn icon size="small" variant="tonal" color="error" @click="removeAllFromGroup" :disabled="editingGroup.umos.length === 0"> |
| <v-icon>mdi-chevron-double-left</v-icon> |
| </v-btn> |
| </v-col> |
| |
| <v-col cols="5"> |
| <div class="text-subtitle-2 mb-2">已选会话 ({{ editingGroup.umos.length }})</div> |
| <v-text-field v-model="groupSelectedSearch" placeholder="搜索..." variant="outlined" density="compact" hide-details class="mb-2" clearable prepend-inner-icon="mdi-magnify"></v-text-field> |
| <v-list density="compact" class="transfer-list" lines="one"> |
| <v-list-item v-for="umo in filteredSelectedUmos" :key="umo" @click="removeFromGroup(umo)" class="transfer-item"> |
| <template v-slot:prepend> |
| <v-icon size="small" color="error">mdi-minus</v-icon> |
| </template> |
| <v-list-item-title class="text-caption">{{ formatUmoShort(umo) }}</v-list-item-title> |
| </v-list-item> |
| <v-list-item v-if="editingGroup.umos.length === 0"> |
| <v-list-item-title class="text-caption text-grey text-center">暂无成员</v-list-item-title> |
| </v-list-item> |
| </v-list> |
| </v-col> |
| </v-row> |
| </v-card-text> |
| <v-card-actions class="px-4 pb-4"> |
| <v-spacer></v-spacer> |
| <v-btn variant="text" @click="groupDialog = false">取消</v-btn> |
| <v-btn color="primary" variant="tonal" @click="saveGroup">保存</v-btn> |
| </v-card-actions> |
| </v-card> |
| </v-dialog> |
| |
| |
| <v-dialog v-model="addRuleDialog" max-width="600"> |
| <v-card> |
| <v-card-title class="py-3 px-4" style="display: flex; align-items: center;"> |
| <span>{{ tm('addRule.title') }}</span> |
| <v-spacer></v-spacer> |
| <v-btn icon variant="text" @click="addRuleDialog = false"> |
| <v-icon>mdi-close</v-icon> |
| </v-btn> |
| </v-card-title> |
| |
| <v-card-text class="pa-4"> |
| <v-alert type="info" variant="tonal" class="mb-4"> |
| {{ tm('addRule.description') }} |
| </v-alert> |
| |
| <v-autocomplete v-model="selectedNewUmo" :items="availableUmos" :loading="loadingUmos" |
| :label="tm('addRule.selectUmo')" variant="outlined" clearable :no-data-text="tm('addRule.noUmos')" /> |
| </v-card-text> |
| |
| <v-card-actions class="px-4 pb-4"> |
| <v-spacer></v-spacer> |
| <v-btn variant="text" @click="addRuleDialog = false">{{ tm('buttons.cancel') }}</v-btn> |
| <v-btn color="primary" variant="tonal" @click="createNewRule" :disabled="!selectedNewUmo"> |
| {{ tm('buttons.next') }} |
| </v-btn> |
| </v-card-actions> |
| </v-card> |
| </v-dialog> |
| |
| |
| <v-dialog v-model="ruleDialog" max-width="550" scrollable> |
| <v-card v-if="selectedUmo" class="d-flex flex-column" height="600"> |
| <v-card-title class="py-3 px-6 d-flex align-center border-b"> |
| <span>{{ tm('ruleEditor.title') }}</span> |
| <v-chip size="x-small" class="ml-2 font-weight-regular" variant="outlined"> |
| {{ selectedUmo.umo }} |
| </v-chip> |
| <v-spacer></v-spacer> |
| <v-btn icon="mdi-close" variant="text" @click="closeRuleEditor"></v-btn> |
| </v-card-title> |
| |
| <v-card-text class="pa-0 overflow-y-auto"> |
| <div class="px-6 py-4"> |
| |
| <div class="d-flex align-center mb-4"> |
| <h3 class="font-weight-bold mb-0">{{ tm('ruleEditor.serviceConfig.title') }}</h3> |
| </div> |
| |
| <v-row dense> |
| <v-col cols="12"> |
| <v-checkbox v-model="serviceConfig.session_enabled" |
| :label="tm('ruleEditor.serviceConfig.sessionEnabled')" color="success" hide-details class="mb-2" /> |
| </v-col> |
| <v-col cols="12" md="6"> |
| <v-checkbox v-model="serviceConfig.llm_enabled" :label="tm('ruleEditor.serviceConfig.llmEnabled')" |
| color="primary" hide-details /> |
| </v-col> |
| <v-col cols="12" md="6"> |
| <v-checkbox v-model="serviceConfig.tts_enabled" :label="tm('ruleEditor.serviceConfig.ttsEnabled')" |
| color="secondary" hide-details /> |
| </v-col> |
| <v-col cols="12" class="mt-2"> |
| <v-text-field v-model="serviceConfig.custom_name" :label="tm('ruleEditor.serviceConfig.customName')" |
| variant="outlined" hide-details clearable /> |
| </v-col> |
| </v-row> |
| |
| <div class="d-flex justify-end mt-4"> |
| <v-btn color="primary" variant="tonal" size="small" @click="saveServiceConfig" :loading="saving" |
| prepend-icon="mdi-content-save"> |
| {{ tm('buttons.save') }} |
| </v-btn> |
| </div> |
| |
| |
| <div class="d-flex align-center mb-4 mt-4"> |
| <h3 class="font-weight-bold mb-0">{{ tm('ruleEditor.providerConfig.title') }}</h3> |
| </div> |
| |
| <v-row dense> |
| <v-col cols="12"> |
| <v-select v-model="providerConfig.chat_completion" :items="chatProviderOptions" item-title="label" |
| item-value="value" :label="tm('ruleEditor.providerConfig.chatProvider')" variant="outlined" |
| hide-details class="mb-2" /> |
| </v-col> |
| <v-col cols="12"> |
| <v-select v-model="providerConfig.speech_to_text" :items="sttProviderOptions" item-title="label" |
| item-value="value" :label="tm('ruleEditor.providerConfig.sttProvider')" variant="outlined" |
| hide-details :disabled="availableSttProviders.length === 0" class="mb-2" /> |
| </v-col> |
| <v-col cols="12"> |
| <v-select v-model="providerConfig.text_to_speech" :items="ttsProviderOptions" item-title="label" |
| item-value="value" :label="tm('ruleEditor.providerConfig.ttsProvider')" variant="outlined" |
| hide-details :disabled="availableTtsProviders.length === 0" /> |
| </v-col> |
| </v-row> |
| |
| <div class="d-flex justify-end mt-4"> |
| <v-btn color="primary" variant="tonal" size="small" @click="saveProviderConfig" :loading="saving" |
| prepend-icon="mdi-content-save"> |
| {{ tm('buttons.save') }} |
| </v-btn> |
| </div> |
| |
| |
| <div class="d-flex align-center mb-4 mt-4"> |
| <h3 class="font-weight-bold mb-0">{{ tm('ruleEditor.personaConfig.title') }}</h3> |
| </div> |
| |
| <v-row dense> |
| <v-col cols="12"> |
| <v-select v-model="serviceConfig.persona_id" :items="personaOptions" item-title="label" |
| item-value="value" :label="tm('ruleEditor.personaConfig.selectPersona')" variant="outlined" |
| hide-details clearable /> |
| </v-col> |
| <v-col cols="12"> |
| <v-alert type="info" variant="tonal" class="mt-2" icon="mdi-information-outline"> |
| {{ tm('ruleEditor.personaConfig.hint') }} |
| </v-alert> |
| </v-col> |
| </v-row> |
| |
| <div class="d-flex justify-end mt-4"> |
| <v-btn color="primary" variant="tonal" size="small" @click="saveServiceConfig" :loading="saving" |
| prepend-icon="mdi-content-save"> |
| {{ tm('buttons.save') }} |
| </v-btn> |
| </div> |
| |
| |
| <div class="d-flex align-center mb-4 mt-4"> |
| <h3 class="font-weight-bold mb-0">{{ tm('ruleEditor.pluginConfig.title') }}</h3> |
| </div> |
| |
| <v-row dense> |
| <v-col cols="12"> |
| <v-select v-model="pluginConfig.disabled_plugins" :items="pluginOptions" item-title="label" |
| item-value="value" :label="tm('ruleEditor.pluginConfig.disabledPlugins')" variant="outlined" |
| hide-details multiple chips closable-chips clearable /> |
| </v-col> |
| <v-col cols="12"> |
| <v-alert type="info" variant="tonal" class="mt-2" icon="mdi-information-outline"> |
| {{ tm('ruleEditor.pluginConfig.hint') }} |
| </v-alert> |
| </v-col> |
| </v-row> |
| |
| <div class="d-flex justify-end mt-4"> |
| <v-btn color="primary" variant="tonal" size="small" @click="savePluginConfig" :loading="saving" |
| prepend-icon="mdi-content-save"> |
| {{ tm('buttons.save') }} |
| </v-btn> |
| </div> |
| |
| |
| <div class="d-flex align-center mb-4 mt-4"> |
| <h3 class="font-weight-bold mb-0">{{ tm('ruleEditor.kbConfig.title') }}</h3> |
| </div> |
| |
| <v-row dense> |
| <v-col cols="12"> |
| <v-select v-model="kbConfig.kb_ids" :items="kbOptions" item-title="label" item-value="value" :disabled="availableKbs.length === 0" |
| :label="tm('ruleEditor.kbConfig.selectKbs')" variant="outlined" hide-details multiple chips |
| closable-chips clearable /> |
| </v-col> |
| <v-col cols="12" md="6"> |
| <v-text-field v-model.number="kbConfig.top_k" :label="tm('ruleEditor.kbConfig.topK')" |
| variant="outlined" hide-details type="number" min="1" max="20" class="mt-3"/> |
| </v-col> |
| <v-col cols="12" md="6"> |
| <v-checkbox v-model="kbConfig.enable_rerank" :label="tm('ruleEditor.kbConfig.enableRerank')" |
| color="primary" hide-details class="mt-3"/> |
| </v-col> |
| </v-row> |
| |
| <div class="d-flex justify-end mt-4"> |
| <v-btn color="primary" variant="tonal" size="small" @click="saveKbConfig" :loading="saving" |
| prepend-icon="mdi-content-save"> |
| {{ tm('buttons.save') }} |
| </v-btn> |
| </div> |
| </div> |
| </v-card-text> |
| </v-card> |
| </v-dialog> |
| |
| |
| <v-dialog v-model="deleteDialog" max-width="400"> |
| <v-card> |
| <v-card-title class="text-h6">{{ tm('deleteConfirm.title') }}</v-card-title> |
| <v-card-text> |
| {{ tm('deleteConfirm.message') }} |
| <br><br> |
| <code>{{ deleteTarget?.umo }}</code> |
| </v-card-text> |
| <v-card-actions> |
| <v-spacer></v-spacer> |
| <v-btn variant="text" @click="deleteDialog = false">{{ tm('buttons.cancel') }}</v-btn> |
| <v-btn color="error" variant="tonal" @click="deleteAllRules" :loading="deleting">{{ tm('buttons.delete') |
| }}</v-btn> |
| </v-card-actions> |
| </v-card> |
| </v-dialog> |
| |
| |
| <v-dialog v-model="batchDeleteDialog" max-width="500"> |
| <v-card> |
| <v-card-title class="text-h6">{{ tm('batchDeleteConfirm.title') }}</v-card-title> |
| <v-card-text> |
| {{ tm('batchDeleteConfirm.message', { count: selectedItems.length }) }} |
| <div class="mt-3" style="max-height: 200px; overflow-y: auto;"> |
| <v-chip v-for="item in selectedItems" :key="item.umo" size="small" class="ma-1" variant="outlined"> |
| {{ item.rules?.session_service_config?.custom_name || item.umo }} |
| </v-chip> |
| </div> |
| </v-card-text> |
| <v-card-actions> |
| <v-spacer></v-spacer> |
| <v-btn variant="text" @click="batchDeleteDialog = false">{{ tm('buttons.cancel') }}</v-btn> |
| <v-btn color="error" variant="tonal" @click="batchDeleteRules" :loading="deleting"> |
| {{ tm('buttons.delete') }} |
| </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-dialog v-model="quickEditNameDialog" max-width="400"> |
| <v-card> |
| <v-card-title class="py-3 px-4">{{ tm('quickEditName.title') }}</v-card-title> |
| <v-card-text class="pa-4"> |
| <v-text-field v-model="quickEditNameValue" :label="tm('ruleEditor.serviceConfig.customName')" |
| variant="outlined" hide-details clearable autofocus @keyup.enter="saveQuickEditName" /> |
| </v-card-text> |
| <v-card-actions class="px-4 pb-4"> |
| <v-spacer></v-spacer> |
| <v-btn variant="text" @click="quickEditNameDialog = false">{{ tm('buttons.cancel') }}</v-btn> |
| <v-btn color="primary" variant="tonal" @click="saveQuickEditName" :loading="saving"> |
| {{ tm('buttons.save') }} |
| </v-btn> |
| </v-card-actions> |
| </v-card> |
| </v-dialog> |
| </v-container> |
| </div> |
| </template> |
| |
| <script> |
| import axios from 'axios' |
| import { useI18n, useModuleI18n } from '@/i18n/composables' |
| import { |
| askForConfirmation as askForConfirmationDialog, |
| useConfirmDialog |
| } from '@/utils/confirmDialog' |
| |
| export default { |
| name: 'SessionManagementPage', |
| setup() { |
| const { t } = useI18n() |
| const { tm } = useModuleI18n('features/session-management') |
| const confirmDialog = useConfirmDialog() |
| |
| return { |
| t, |
| tm, |
| confirmDialog |
| } |
| }, |
| data() { |
| return { |
| loading: false, |
| saving: false, |
| deleting: false, |
| loadingUmos: false, |
| rulesList: [], |
| searchQuery: '', |
| |
| |
| currentPage: 1, |
| itemsPerPage: 10, |
| totalItems: 0, |
| searchTimeout: null, |
| |
| |
| availablePersonas: [], |
| availableChatProviders: [], |
| availableSttProviders: [], |
| availableTtsProviders: [], |
| availablePlugins: [], |
| availableKbs: [], |
| |
| |
| addRuleDialog: false, |
| availableUmos: [], |
| selectedNewUmo: null, |
| |
| |
| ruleDialog: false, |
| selectedUmo: null, |
| editingRules: {}, |
| |
| |
| serviceConfig: { |
| session_enabled: true, |
| llm_enabled: true, |
| tts_enabled: true, |
| custom_name: '', |
| persona_id: null, |
| }, |
| |
| |
| providerConfig: { |
| chat_completion: null, |
| speech_to_text: null, |
| text_to_speech: null, |
| }, |
| |
| |
| pluginConfig: { |
| enabled_plugins: [], |
| disabled_plugins: [], |
| }, |
| |
| |
| kbConfig: { |
| kb_ids: [], |
| top_k: 5, |
| enable_rerank: true, |
| }, |
| |
| |
| deleteDialog: false, |
| deleteTarget: null, |
| |
| |
| selectedItems: [], |
| batchDeleteDialog: false, |
| |
| |
| quickEditNameDialog: false, |
| quickEditNameTarget: null, |
| quickEditNameValue: '', |
| |
| batchScope: 'selected', |
| batchGroupId: null, |
| batchLlmStatus: null, |
| batchTtsStatus: null, |
| batchChatProvider: null, |
| batchTtsProvider: null, |
| batchUpdating: false, |
| |
| |
| groups: [], |
| groupsLoading: false, |
| groupDialog: false, |
| groupDialogMode: 'create', |
| editingGroup: { |
| id: null, |
| name: '', |
| umos: [], |
| }, |
| groupMemberDialog: false, |
| groupMemberTarget: null, |
| groupMemberSearch: '', |
| groupSelectedSearch: '', |
| |
| |
| snackbar: false, |
| snackbarText: '', |
| snackbarColor: 'success', |
| } |
| }, |
| |
| computed: { |
| headers() { |
| return [ |
| { title: this.tm('table.headers.umoInfo'), key: 'umo_info', sortable: false, minWidth: '300px' }, |
| { title: this.tm('table.headers.rulesOverview'), key: 'rules_overview', sortable: false, minWidth: '250px' }, |
| { title: this.tm('table.headers.actions'), key: 'actions', sortable: false, minWidth: '150px' }, |
| ] |
| }, |
| |
| filteredRulesList() { |
| |
| return this.rulesList |
| }, |
| |
| personaOptions() { |
| return [ |
| { label: this.tm('persona.none'), value: null }, |
| ...this.availablePersonas.map(p => ({ |
| label: p.name, |
| value: p.name |
| })) |
| ] |
| }, |
| |
| chatProviderOptions() { |
| return [ |
| { label: this.tm('provider.followConfig'), value: null }, |
| ...this.availableChatProviders.map(p => ({ |
| label: `${p.name} (${p.model})`, |
| value: p.id |
| })) |
| ] |
| }, |
| |
| sttProviderOptions() { |
| return [ |
| { label: this.tm('provider.followConfig'), value: null }, |
| ...this.availableSttProviders.map(p => ({ |
| label: `${p.name} (${p.model})`, |
| value: p.id |
| })) |
| ] |
| }, |
| |
| ttsProviderOptions() { |
| return [ |
| { label: this.tm('provider.followConfig'), value: null }, |
| ...this.availableTtsProviders.map(p => ({ |
| label: `${p.name} (${p.model})`, |
| value: p.id |
| })) |
| ] |
| }, |
| |
| pluginOptions() { |
| return this.availablePlugins.map(p => ({ |
| label: p.display_name || p.name, |
| value: p.name |
| })) |
| }, |
| |
| kbOptions() { |
| return this.availableKbs.map(kb => ({ |
| label: `${kb.emoji || '📚'} ${kb.kb_name}`, |
| value: kb.kb_id |
| })) |
| }, |
| batchScopeOptions() { |
| const options = [ |
| { label: this.tm('batchOperations.scopeSelected'), value: 'selected' }, |
| { label: this.tm('batchOperations.scopeAll'), value: 'all' }, |
| { label: this.tm('batchOperations.scopeGroup'), value: 'group' }, |
| { label: this.tm('batchOperations.scopePrivate'), value: 'private' }, |
| ] |
| |
| if (this.groups.length > 0) { |
| options.push({ label: '── 自定义分组 ──', value: '_divider', disabled: true }) |
| this.groups.forEach(g => { |
| options.push({ label: `📁 ${g.name} (${g.umo_count})`, value: `custom_group:${g.id}` }) |
| }) |
| } |
| return options |
| }, |
| |
| groupOptions() { |
| return this.groups.map(g => ({ |
| label: `${g.name} (${g.umo_count} 个会话)`, |
| value: g.id |
| })) |
| }, |
| |
| statusOptions() { |
| return [ |
| { label: this.tm('status.enabled'), value: true }, |
| { label: this.tm('status.disabled'), value: false }, |
| ] |
| }, |
| |
| canApplyBatch() { |
| const hasChanges = this.batchLlmStatus !== null || this.batchTtsStatus !== null || |
| this.batchChatProvider !== null || this.batchTtsProvider !== null |
| if (this.batchScope === 'selected') { |
| return hasChanges && this.selectedItems.length > 0 |
| } |
| return hasChanges |
| }, |
| |
| |
| unselectedUmos() { |
| const selected = new Set(this.editingGroup.umos || []) |
| return this.availableUmos.filter(u => !selected.has(u)) |
| }, |
| |
| |
| filteredUnselectedUmos() { |
| if (!this.groupMemberSearch) return this.unselectedUmos |
| const search = this.groupMemberSearch.toLowerCase() |
| return this.unselectedUmos.filter(u => u.toLowerCase().includes(search)) |
| }, |
| |
| |
| filteredSelectedUmos() { |
| if (!this.groupSelectedSearch) return this.editingGroup.umos || [] |
| const search = this.groupSelectedSearch.toLowerCase() |
| return (this.editingGroup.umos || []).filter(u => u.toLowerCase().includes(search)) |
| }, |
| }, |
| |
| watch: { |
| searchQuery: { |
| handler() { |
| |
| if (this.searchTimeout) { |
| clearTimeout(this.searchTimeout) |
| } |
| this.searchTimeout = setTimeout(() => { |
| this.onSearchChange() |
| }, 300) |
| } |
| } |
| }, |
| |
| mounted() { |
| this.loadData() |
| this.loadGroups() |
| }, |
| |
| beforeUnmount() { |
| if (this.searchTimeout) { |
| clearTimeout(this.searchTimeout) |
| } |
| }, |
| |
| methods: { |
| async loadData() { |
| this.loading = true |
| try { |
| const response = await axios.get('/api/session/list-rule', { |
| params: { |
| page: this.currentPage, |
| page_size: this.itemsPerPage, |
| search: this.searchQuery || '' |
| } |
| }) |
| if (response.data.status === 'ok') { |
| const data = response.data.data |
| this.rulesList = data.rules |
| this.totalItems = data.total |
| this.availablePersonas = data.available_personas |
| this.availableChatProviders = data.available_chat_providers |
| this.availableSttProviders = data.available_stt_providers |
| this.availableTtsProviders = data.available_tts_providers |
| this.availablePlugins = data.available_plugins || [] |
| this.availableKbs = data.available_kbs || [] |
| } else { |
| this.showError(response.data.message || this.tm('messages.loadError')) |
| } |
| } catch (error) { |
| this.showError(error.response?.data?.message || this.tm('messages.loadError')) |
| } |
| this.loading = false |
| }, |
| |
| onTableOptionsUpdate(options) { |
| |
| this.currentPage = options.page |
| this.itemsPerPage = options.itemsPerPage |
| this.loadData() |
| }, |
| |
| onSearchChange() { |
| |
| this.currentPage = 1 |
| this.loadData() |
| }, |
| |
| async loadUmos() { |
| this.loadingUmos = true |
| try { |
| const response = await axios.get('/api/session/active-umos') |
| if (response.data.status === 'ok') { |
| |
| const existingUmos = new Set(this.rulesList.map(r => r.umo)) |
| this.availableUmos = response.data.data.umos.filter(umo => !existingUmos.has(umo)) |
| } |
| } catch (error) { |
| this.showError(error.response?.data?.message || this.tm('messages.loadError')) |
| } |
| this.loadingUmos = false |
| }, |
| |
| async refreshData() { |
| await this.loadData() |
| this.showSuccess(this.tm('messages.refreshSuccess')) |
| }, |
| |
| hasProviderConfig(rules) { |
| return rules && ( |
| rules['provider_perf_chat_completion'] || |
| rules['provider_perf_speech_to_text'] || |
| rules['provider_perf_text_to_speech'] |
| ) |
| }, |
| |
| async openAddRuleDialog() { |
| this.addRuleDialog = true |
| this.selectedNewUmo = null |
| await this.loadUmos() |
| }, |
| |
| createNewRule() { |
| if (!this.selectedNewUmo) return |
| |
| |
| const newItem = { |
| umo: this.selectedNewUmo, |
| rules: {}, |
| } |
| |
| const parts = this.selectedNewUmo.split(':') |
| if (parts.length >= 3) { |
| newItem.platform = parts[0] |
| newItem.message_type = parts[1] |
| newItem.session_id = parts[2] |
| } |
| |
| this.addRuleDialog = false |
| this.openRuleEditor(newItem) |
| }, |
| |
| openRuleEditor(item) { |
| this.selectedUmo = item |
| this.editingRules = item.rules || {} |
| |
| |
| const svcConfig = this.editingRules.session_service_config || {} |
| this.serviceConfig = { |
| session_enabled: svcConfig.session_enabled !== false, |
| llm_enabled: svcConfig.llm_enabled !== false, |
| tts_enabled: svcConfig.tts_enabled !== false, |
| custom_name: svcConfig.custom_name || '', |
| persona_id: svcConfig.persona_id || null, |
| } |
| |
| |
| this.providerConfig = { |
| chat_completion: this.editingRules['provider_perf_chat_completion'] || null, |
| speech_to_text: this.editingRules['provider_perf_speech_to_text'] || null, |
| text_to_speech: this.editingRules['provider_perf_text_to_speech'] || null, |
| } |
| |
| |
| const pluginCfg = this.editingRules.session_plugin_config || {} |
| this.pluginConfig = { |
| enabled_plugins: pluginCfg.enabled_plugins || [], |
| disabled_plugins: pluginCfg.disabled_plugins || [], |
| } |
| |
| |
| const kbCfg = this.editingRules.kb_config || {} |
| this.kbConfig = { |
| kb_ids: kbCfg.kb_ids || [], |
| top_k: kbCfg.top_k ?? 5, |
| enable_rerank: kbCfg.enable_rerank !== false, |
| } |
| |
| this.ruleDialog = true |
| }, |
| |
| closeRuleEditor() { |
| this.ruleDialog = false |
| this.selectedUmo = null |
| this.editingRules = {} |
| }, |
| |
| async saveServiceConfig() { |
| if (!this.selectedUmo) return |
| |
| this.saving = true |
| try { |
| const config = { ...this.serviceConfig } |
| |
| if (!config.custom_name) delete config.custom_name |
| if (config.persona_id === null) delete config.persona_id |
| |
| const response = await axios.post('/api/session/update-rule', { |
| umo: this.selectedUmo.umo, |
| rule_key: 'session_service_config', |
| rule_value: config |
| }) |
| |
| if (response.data.status === 'ok') { |
| this.showSuccess(this.tm('messages.saveSuccess')) |
| this.editingRules.session_service_config = config |
| |
| |
| let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo) |
| if (item) { |
| item.rules = { ...item.rules, session_service_config: config } |
| } else { |
| |
| this.rulesList.push({ |
| umo: this.selectedUmo.umo, |
| platform: this.selectedUmo.platform, |
| message_type: this.selectedUmo.message_type, |
| session_id: this.selectedUmo.session_id, |
| rules: { session_service_config: config } |
| }) |
| } |
| } else { |
| this.showError(response.data.message || this.tm('messages.saveError')) |
| } |
| } catch (error) { |
| this.showError(error.response?.data?.message || this.tm('messages.saveError')) |
| } |
| this.saving = false |
| }, |
| |
| async saveProviderConfig() { |
| if (!this.selectedUmo) return |
| |
| this.saving = true |
| try { |
| const updateTasks = [] |
| const deleteTasks = [] |
| const providerTypes = ['chat_completion', 'speech_to_text', 'text_to_speech'] |
| |
| for (const type of providerTypes) { |
| const value = this.providerConfig[type] |
| if (value) { |
| |
| updateTasks.push( |
| axios.post('/api/session/update-rule', { |
| umo: this.selectedUmo.umo, |
| rule_key: `provider_perf_${type}`, |
| rule_value: value |
| }) |
| ) |
| } else if (this.editingRules[`provider_perf_${type}`]) { |
| |
| deleteTasks.push( |
| axios.post('/api/session/delete-rule', { |
| umo: this.selectedUmo.umo, |
| rule_key: `provider_perf_${type}` |
| }) |
| ) |
| } |
| } |
| |
| const allTasks = [...updateTasks, ...deleteTasks] |
| if (allTasks.length > 0) { |
| await Promise.all(allTasks) |
| this.showSuccess(this.tm('messages.saveSuccess')) |
| |
| |
| let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo) |
| if (!item) { |
| item = { |
| umo: this.selectedUmo.umo, |
| platform: this.selectedUmo.platform, |
| message_type: this.selectedUmo.message_type, |
| session_id: this.selectedUmo.session_id, |
| rules: {} |
| } |
| this.rulesList.push(item) |
| } |
| for (const type of providerTypes) { |
| if (this.providerConfig[type]) { |
| item.rules[`provider_perf_${type}`] = this.providerConfig[type] |
| this.editingRules[`provider_perf_${type}`] = this.providerConfig[type] |
| } else { |
| |
| delete item.rules[`provider_perf_${type}`] |
| delete this.editingRules[`provider_perf_${type}`] |
| } |
| } |
| } else { |
| this.showSuccess(this.tm('messages.noChanges')) |
| } |
| } catch (error) { |
| this.showError(error.response?.data?.message || this.tm('messages.saveError')) |
| } |
| this.saving = false |
| }, |
| |
| async savePluginConfig() { |
| if (!this.selectedUmo) return |
| |
| this.saving = true |
| try { |
| const config = { |
| enabled_plugins: this.pluginConfig.enabled_plugins, |
| disabled_plugins: this.pluginConfig.disabled_plugins, |
| } |
| |
| |
| if (config.enabled_plugins.length === 0 && config.disabled_plugins.length === 0) { |
| if (this.editingRules.session_plugin_config) { |
| await axios.post('/api/session/delete-rule', { |
| umo: this.selectedUmo.umo, |
| rule_key: 'session_plugin_config' |
| }) |
| delete this.editingRules.session_plugin_config |
| let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo) |
| if (item) delete item.rules.session_plugin_config |
| } |
| this.showSuccess(this.tm('messages.saveSuccess')) |
| } else { |
| const response = await axios.post('/api/session/update-rule', { |
| umo: this.selectedUmo.umo, |
| rule_key: 'session_plugin_config', |
| rule_value: config |
| }) |
| |
| if (response.data.status === 'ok') { |
| this.showSuccess(this.tm('messages.saveSuccess')) |
| this.editingRules.session_plugin_config = config |
| |
| let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo) |
| if (item) { |
| item.rules.session_plugin_config = config |
| } else { |
| this.rulesList.push({ |
| umo: this.selectedUmo.umo, |
| platform: this.selectedUmo.platform, |
| message_type: this.selectedUmo.message_type, |
| session_id: this.selectedUmo.session_id, |
| rules: { session_plugin_config: config } |
| }) |
| } |
| } else { |
| this.showError(response.data.message || this.tm('messages.saveError')) |
| } |
| } |
| } catch (error) { |
| this.showError(error.response?.data?.message || this.tm('messages.saveError')) |
| } |
| this.saving = false |
| }, |
| |
| async saveKbConfig() { |
| if (!this.selectedUmo) return |
| |
| this.saving = true |
| try { |
| const config = { |
| kb_ids: this.kbConfig.kb_ids, |
| top_k: this.kbConfig.top_k, |
| enable_rerank: this.kbConfig.enable_rerank, |
| } |
| |
| |
| if (config.kb_ids.length === 0) { |
| if (this.editingRules.kb_config) { |
| await axios.post('/api/session/delete-rule', { |
| umo: this.selectedUmo.umo, |
| rule_key: 'kb_config' |
| }) |
| delete this.editingRules.kb_config |
| let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo) |
| if (item) delete item.rules.kb_config |
| } |
| this.showSuccess(this.tm('messages.saveSuccess')) |
| } else { |
| const response = await axios.post('/api/session/update-rule', { |
| umo: this.selectedUmo.umo, |
| rule_key: 'kb_config', |
| rule_value: config |
| }) |
| |
| if (response.data.status === 'ok') { |
| this.showSuccess(this.tm('messages.saveSuccess')) |
| this.editingRules.kb_config = config |
| |
| let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo) |
| if (item) { |
| item.rules.kb_config = config |
| } else { |
| this.rulesList.push({ |
| umo: this.selectedUmo.umo, |
| platform: this.selectedUmo.platform, |
| message_type: this.selectedUmo.message_type, |
| session_id: this.selectedUmo.session_id, |
| rules: { kb_config: config } |
| }) |
| } |
| } else { |
| this.showError(response.data.message || this.tm('messages.saveError')) |
| } |
| } |
| } catch (error) { |
| this.showError(error.response?.data?.message || this.tm('messages.saveError')) |
| } |
| this.saving = false |
| }, |
| |
| confirmDeleteRules(item) { |
| this.deleteTarget = item |
| this.deleteDialog = true |
| }, |
| |
| async deleteAllRules() { |
| if (!this.deleteTarget) return |
| |
| this.deleting = true |
| try { |
| const response = await axios.post('/api/session/delete-rule', { |
| umo: this.deleteTarget.umo |
| }) |
| |
| if (response.data.status === 'ok') { |
| this.showSuccess(this.tm('messages.deleteSuccess')) |
| |
| const index = this.rulesList.findIndex(u => u.umo === this.deleteTarget.umo) |
| if (index > -1) { |
| this.rulesList.splice(index, 1) |
| } |
| this.deleteDialog = false |
| this.deleteTarget = null |
| |
| await this.loadData() |
| } else { |
| this.showError(response.data.message || this.tm('messages.deleteError')) |
| } |
| } catch (error) { |
| this.showError(error.response?.data?.message || this.tm('messages.deleteError')) |
| } |
| this.deleting = false |
| }, |
| |
| confirmBatchDelete() { |
| if (this.selectedItems.length === 0) return |
| this.batchDeleteDialog = true |
| }, |
| |
| async batchDeleteRules() { |
| if (this.selectedItems.length === 0) return |
| |
| this.deleting = true |
| try { |
| const umos = this.selectedItems.map(item => item.umo) |
| const response = await axios.post('/api/session/batch-delete-rule', { |
| umos: umos |
| }) |
| |
| if (response.data.status === 'ok') { |
| const data = response.data.data |
| this.showSuccess(data.message || this.tm('messages.batchDeleteSuccess')) |
| this.batchDeleteDialog = false |
| this.selectedItems = [] |
| |
| await this.loadData() |
| } else { |
| this.showError(response.data.message || this.tm('messages.batchDeleteError')) |
| } |
| } catch (error) { |
| this.showError(error.response?.data?.message || this.tm('messages.batchDeleteError')) |
| } |
| this.deleting = false |
| }, |
| |
| getPlatformColor(platform) { |
| const colors = { |
| 'aiocqhttp': 'blue', |
| 'qq_official': 'purple', |
| 'telegram': 'light-blue', |
| 'discord': 'indigo', |
| 'webchat': 'orange', |
| '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 |
| }, |
| |
| openQuickEditName(item) { |
| this.quickEditNameTarget = item |
| this.quickEditNameValue = item.rules?.session_service_config?.custom_name || '' |
| this.quickEditNameDialog = true |
| }, |
| |
| async saveQuickEditName() { |
| if (!this.quickEditNameTarget) return |
| |
| this.saving = true |
| try { |
| |
| const existingConfig = this.quickEditNameTarget.rules?.session_service_config || {} |
| const config = { |
| session_enabled: existingConfig.session_enabled !== false, |
| llm_enabled: existingConfig.llm_enabled !== false, |
| tts_enabled: existingConfig.tts_enabled !== false, |
| ...existingConfig, |
| } |
| |
| |
| if (this.quickEditNameValue) { |
| config.custom_name = this.quickEditNameValue |
| } else { |
| delete config.custom_name |
| } |
| |
| const response = await axios.post('/api/session/update-rule', { |
| umo: this.quickEditNameTarget.umo, |
| rule_key: 'session_service_config', |
| rule_value: config |
| }) |
| |
| if (response.data.status === 'ok') { |
| this.showSuccess(this.tm('messages.saveSuccess')) |
| |
| |
| let item = this.rulesList.find(u => u.umo === this.quickEditNameTarget.umo) |
| if (item) { |
| if (!item.rules) item.rules = {} |
| item.rules.session_service_config = config |
| } else { |
| |
| const parts = this.quickEditNameTarget.umo.split(':') |
| this.rulesList.push({ |
| umo: this.quickEditNameTarget.umo, |
| platform: parts[0] || '', |
| message_type: parts[1] || '', |
| session_id: parts[2] || '', |
| rules: { session_service_config: config } |
| }) |
| } |
| |
| this.quickEditNameDialog = false |
| this.quickEditNameTarget = null |
| this.quickEditNameValue = '' |
| } else { |
| this.showError(response.data.message || this.tm('messages.saveError')) |
| } |
| } catch (error) { |
| this.showError(error.response?.data?.message || this.tm('messages.saveError')) |
| } |
| this.saving = false |
| }, |
| |
| async applyBatchChanges() { |
| this.batchUpdating = true |
| try { |
| let scope = this.batchScope |
| let groupId = null |
| let umos = [] |
| |
| |
| if (scope.startsWith('custom_group:')) { |
| groupId = scope.split(':')[1] |
| scope = 'custom_group' |
| } |
| |
| if (scope === 'selected') { |
| umos = this.selectedItems.map(item => item.umo) |
| if (umos.length === 0) { |
| this.showError('请先选择要操作的会话') |
| this.batchUpdating = false |
| return |
| } |
| } |
| |
| const tasks = [] |
| |
| if (this.batchLlmStatus !== null || this.batchTtsStatus !== null) { |
| const serviceData = { scope, umos, group_id: groupId } |
| if (this.batchLlmStatus !== null) { |
| serviceData.llm_enabled = this.batchLlmStatus |
| } |
| if (this.batchTtsStatus !== null) { |
| serviceData.tts_enabled = this.batchTtsStatus |
| } |
| tasks.push(axios.post('/api/session/batch-update-service', serviceData)) |
| } |
| |
| if (this.batchChatProvider !== null) { |
| tasks.push(axios.post('/api/session/batch-update-provider', { |
| scope, |
| umos, |
| group_id: groupId, |
| provider_type: 'chat_completion', |
| provider_id: this.batchChatProvider || null |
| })) |
| } |
| |
| if (this.batchTtsProvider !== null) { |
| tasks.push(axios.post('/api/session/batch-update-provider', { |
| scope, |
| umos, |
| group_id: groupId, |
| provider_type: 'text_to_speech', |
| provider_id: this.batchTtsProvider || null |
| })) |
| } |
| |
| if (tasks.length === 0) { |
| this.showError('请至少选择一项要修改的配置') |
| this.batchUpdating = false |
| return |
| } |
| |
| const results = await Promise.all(tasks) |
| const allOk = results.every(r => r.data.status === 'ok') |
| |
| if (allOk) { |
| this.showSuccess('批量更新成功') |
| this.batchLlmStatus = null |
| this.batchTtsStatus = null |
| this.batchChatProvider = null |
| this.batchTtsProvider = null |
| await this.loadData() |
| } else { |
| this.showError('部分更新失败') |
| } |
| } catch (error) { |
| this.showError(error.response?.data?.message || '批量更新失败') |
| } |
| this.batchUpdating = false |
| }, |
| |
| |
| |
| async loadGroups() { |
| this.groupsLoading = true |
| try { |
| const response = await axios.get('/api/session/groups') |
| if (response.data.status === 'ok') { |
| this.groups = response.data.data.groups || [] |
| } |
| } catch (error) { |
| console.error('加载分组失败:', error) |
| } |
| this.groupsLoading = false |
| }, |
| |
| async loadAvailableUmos() { |
| if (this.availableUmos.length > 0) return |
| this.loadingUmos = true |
| try { |
| const response = await axios.get('/api/session/active-umos') |
| if (response.data.status === 'ok') { |
| this.availableUmos = response.data.data.umos || [] |
| } |
| } catch (error) { |
| console.error('加载会话列表失败:', error) |
| } |
| this.loadingUmos = false |
| }, |
| |
| openCreateGroupDialog() { |
| this.groupDialogMode = 'create' |
| this.editingGroup = { id: null, name: '', umos: [] } |
| this.groupMemberSearch = '' |
| this.groupSelectedSearch = '' |
| this.groupDialog = true |
| }, |
| |
| openEditGroupDialog(group) { |
| this.groupDialogMode = 'edit' |
| this.editingGroup = { ...group, umos: [...(group.umos || [])] } |
| this.groupMemberSearch = '' |
| this.groupSelectedSearch = '' |
| this.groupDialog = true |
| }, |
| |
| |
| addToGroup(umo) { |
| if (!this.editingGroup.umos.includes(umo)) { |
| this.editingGroup.umos.push(umo) |
| } |
| }, |
| |
| removeFromGroup(umo) { |
| const idx = this.editingGroup.umos.indexOf(umo) |
| if (idx > -1) { |
| this.editingGroup.umos.splice(idx, 1) |
| } |
| }, |
| |
| addAllToGroup() { |
| this.unselectedUmos.forEach(umo => { |
| if (!this.editingGroup.umos.includes(umo)) { |
| this.editingGroup.umos.push(umo) |
| } |
| }) |
| }, |
| |
| removeAllFromGroup() { |
| this.editingGroup.umos = [] |
| }, |
| |
| formatUmoShort(umo) { |
| |
| const parts = umo.split(':') |
| if (parts.length >= 3) { |
| return `${parts[0]}:${parts[2]}` |
| } |
| return umo |
| }, |
| |
| async saveGroup() { |
| if (!this.editingGroup.name.trim()) { |
| this.showError('分组名称不能为空') |
| return |
| } |
| |
| try { |
| let response |
| if (this.groupDialogMode === 'create') { |
| response = await axios.post('/api/session/group/create', { |
| name: this.editingGroup.name, |
| umos: this.editingGroup.umos |
| }) |
| } else { |
| response = await axios.post('/api/session/group/update', { |
| id: this.editingGroup.id, |
| name: this.editingGroup.name, |
| umos: this.editingGroup.umos |
| }) |
| } |
| |
| if (response.data.status === 'ok') { |
| this.showSuccess(response.data.data.message) |
| this.groupDialog = false |
| await this.loadGroups() |
| } else { |
| this.showError(response.data.message) |
| } |
| } catch (error) { |
| this.showError(error.response?.data?.message || '保存分组失败') |
| } |
| }, |
| |
| async deleteGroup(group) { |
| const message = `确定要删除分组 "${group.name}" 吗?` |
| if (!(await askForConfirmationDialog(message, this.confirmDialog))) return |
| |
| try { |
| const response = await axios.post('/api/session/group/delete', { id: group.id }) |
| if (response.data.status === 'ok') { |
| this.showSuccess(response.data.data.message) |
| await this.loadGroups() |
| } else { |
| this.showError(response.data.message) |
| } |
| } catch (error) { |
| this.showError(error.response?.data?.message || '删除分组失败') |
| } |
| }, |
| |
| openGroupMemberDialog(group) { |
| this.groupMemberTarget = { ...group } |
| this.groupMemberDialog = true |
| }, |
| |
| async addSelectedToGroup(groupId) { |
| if (this.selectedItems.length === 0) { |
| this.showError('请先选择要添加的会话') |
| return |
| } |
| |
| try { |
| const response = await axios.post('/api/session/group/update', { |
| id: groupId, |
| add_umos: this.selectedItems.map(item => item.umo) |
| }) |
| if (response.data.status === 'ok') { |
| this.showSuccess(`已添加 ${this.selectedItems.length} 个会话到分组`) |
| await this.loadGroups() |
| } else { |
| this.showError(response.data.message) |
| } |
| } catch (error) { |
| this.showError(error.response?.data?.message || '添加失败') |
| } |
| }, |
| }, |
| } |
| </script> |
| |
| <style scoped> |
| .v-data-table :deep(.v-data-table__td) { |
| padding: 8px 16px !important; |
| vertical-align: middle !important; |
| } |
| |
| code { |
| background-color: rgba(0, 0, 0, 0.05); |
| padding: 2px 6px; |
| border-radius: 4px; |
| font-size: 12px; |
| } |
| |
| .transfer-list { |
| max-height: 280px; |
| overflow-y: auto; |
| border: 1px solid rgba(0, 0, 0, 0.12); |
| border-radius: 4px; |
| } |
| |
| .transfer-item { |
| cursor: pointer; |
| transition: background-color 0.15s; |
| } |
| |
| .transfer-item:hover { |
| background-color: rgba(0, 0, 0, 0.04); |
| } |
| </style> |
| |