feat: 添加知识库配置功能,支持会话管理中的知识库选择与设置

This commit is contained in:
lxfight
2025-10-20 21:46:39 +08:00
parent 333bf56ddc
commit c56edb4da6
5 changed files with 446 additions and 4 deletions
@@ -30,6 +30,7 @@
"ttsProvider": "TTS Provider",
"llmStatus": "LLM Status",
"ttsStatus": "TTS Status",
"knowledgeBase": "Knowledge Base",
"pluginManagement": "Plugin Management",
"actions": "Actions"
}
@@ -67,6 +68,31 @@
"fullSessionId": "Full Session ID",
"hint": "Custom names help you easily identify sessions. The small information icon (!) will show the actual UMO when hovering."
},
"knowledgeBase": {
"title": "Knowledge Base Configuration",
"configure": "Configure",
"selectKB": "Select Knowledge Bases",
"selectMultiple": "You can select multiple knowledge bases",
"noKBAvailable": "No knowledge bases available",
"noKBDesc": "No knowledge bases have been created yet",
"createKB": "Create Knowledge Base",
"advancedSettings": "Advanced Settings",
"topK": "Result Count",
"topKHint": "Number of results to retrieve from knowledge base",
"enableRerank": "Enable Reranking",
"enableRerankHint": "Use reranking model to improve retrieval quality",
"clearConfig": "Clear Configuration",
"save": "Save",
"cancel": "Cancel",
"loading": "Loading knowledge base configuration...",
"description": "Configure knowledge bases for this session. The session will use configured knowledge bases to enhance conversation context.",
"saveSuccess": "Knowledge base configuration saved successfully",
"saveFailed": "Failed to save knowledge base configuration",
"loadFailed": "Failed to load knowledge base configuration",
"clearSuccess": "Knowledge base configuration cleared",
"clearFailed": "Failed to clear knowledge base configuration",
"clearConfirm": "Are you sure you want to clear the knowledge base configuration for this session?"
},
"deleteConfirm": {
"message": "Are you sure you want to delete session {sessionName}?",
"warning": "This action will permanently delete all chat history and preference settings for this session (except for data linked via plugins), and this cannot be undone. Continue?"
@@ -30,6 +30,7 @@
"ttsProvider": "语音合成模型",
"llmStatus": "启用 LLM",
"ttsStatus": "启用 TTS",
"knowledgeBase": "知识库配置",
"pluginManagement": "插件管理",
"actions": "操作"
}
@@ -67,6 +68,31 @@
"fullSessionId": "完整会话ID",
"hint": "自定义名称帮助您轻松识别会话。当设置了自定义名称时,会显示一个小感叹号标识(!),鼠标悬停时会显示实际的UMO。"
},
"knowledgeBase": {
"title": "知识库配置",
"configure": "配置",
"selectKB": "选择知识库",
"selectMultiple": "可以选择多个知识库",
"noKBAvailable": "暂无可用的知识库",
"noKBDesc": "目前没有创建任何知识库",
"createKB": "创建知识库",
"advancedSettings": "高级配置",
"topK": "返回结果数量",
"topKHint": "从知识库检索的结果数量",
"enableRerank": "启用重排序",
"enableRerankHint": "使用重排序模型提高检索质量",
"clearConfig": "清除配置",
"save": "保存",
"cancel": "取消",
"loading": "加载知识库配置中...",
"description": "为此会话配置使用的知识库。会话将使用配置的知识库来增强对话上下文。",
"saveSuccess": "知识库配置保存成功",
"saveFailed": "保存知识库配置失败",
"loadFailed": "加载知识库配置失败",
"clearSuccess": "知识库配置已清除",
"clearFailed": "清除知识库配置失败",
"clearConfirm": "确定要清除此会话的知识库配置吗?"
},
"deleteConfirm": {
"message": "确定要删除会话 {sessionName} 吗?",
"warning": "此操作将永久删除本次会话的「全部对话记录」与「偏好设置」(插件对会话的关联数据除外),且无法恢复。确认继续?"
+244 -1
View File
@@ -142,6 +142,14 @@
</v-checkbox>
</template>
<!-- 知识库配置 -->
<template v-slot:item.knowledge_base="{ item }">
<v-btn size="x-small" variant="tonal" color="info" @click="openKBManager(item)"
:loading="item.loadingKB" :disabled="!item.session_enabled">
{{ tm('knowledgeBase.configure') }}
</v-btn>
</template>
<!-- 插件管理 -->
<template v-slot:item.plugins="{ item }">
<v-btn size="x-small" variant="tonal" color="primary" @click="openPluginManager(item)"
@@ -335,6 +343,123 @@
</v-card>
</v-dialog>
<!-- 知识库配置对话框 -->
<v-dialog v-model="kbDialog" max-width="800" min-height="60%">
<v-card v-if="selectedSessionForKB">
<v-card-title class="bg-primary text-white py-3 px-4" style="display: flex; align-items: center;">
<span>{{ tm('knowledgeBase.title') }} - {{ selectedSessionForKB.session_name }}</span>
<v-spacer></v-spacer>
<v-btn icon variant="text" color="white" @click="kbDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text v-if="!loadingKBConfig">
<div style="padding: 16px;">
<v-alert type="info" variant="tonal" class="mb-4">
{{ tm('knowledgeBase.description') }}
</v-alert>
<!-- 知识库选择 -->
<v-select
v-model="sessionKBConfig.kb_ids"
:items="availableKBs"
item-title="kb_name"
item-value="kb_id"
:label="tm('knowledgeBase.selectKB')"
multiple
chips
closable-chips
variant="outlined"
class="mb-4"
:hint="tm('knowledgeBase.selectMultiple')"
persistent-hint
>
<template v-slot:chip="{ item }">
<v-chip>
<span class="mr-1">{{ item.raw.emoji }}</span>
{{ item.raw.kb_name }}
</v-chip>
</template>
<template v-slot:item="{ item, props }">
<v-list-item v-bind="props">
<template v-slot:prepend>
<span style="font-size: 20px; margin-right: 8px;">{{ item.raw.emoji }}</span>
</template>
<v-list-item-title>{{ item.raw.kb_name }}</v-list-item-title>
<v-list-item-subtitle>
{{ item.raw.description || tm('knowledgeBase.noKBDesc') }} - {{ item.raw.doc_count }} {{ tm('list.documents', { count: item.raw.doc_count }) }}
</v-list-item-subtitle>
</v-list-item>
</template>
</v-select>
<!-- 高级配置 -->
<v-expansion-panels class="mb-4">
<v-expansion-panel>
<v-expansion-panel-title>
<v-icon class="mr-2">mdi-cog</v-icon>
{{ tm('knowledgeBase.advancedSettings') }}
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-row>
<v-col cols="12" md="6">
<v-text-field
v-model.number="sessionKBConfig.top_k"
:label="tm('knowledgeBase.topK')"
type="number"
variant="outlined"
density="comfortable"
:hint="tm('knowledgeBase.topKHint')"
persistent-hint
/>
</v-col>
<v-col cols="12" md="6">
<v-checkbox
v-model="sessionKBConfig.enable_rerank"
:label="tm('knowledgeBase.enableRerank')"
color="primary"
:hint="tm('knowledgeBase.enableRerankHint')"
persistent-hint
/>
</v-col>
</v-row>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<div v-if="availableKBs.length === 0" class="text-center py-8">
<v-icon size="64" color="grey-lighten-2">mdi-database-off</v-icon>
<p class="mt-4 text-medium-emphasis">{{ tm('knowledgeBase.noKBAvailable') }}</p>
<v-btn color="primary" variant="tonal" class="mt-2" @click="goToKBPage">
{{ tm('knowledgeBase.createKB') }}
</v-btn>
</div>
</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('knowledgeBase.loading') }}</div>
</v-card-text>
<v-divider />
<v-card-actions class="pa-4">
<v-btn variant="text" @click="clearKBConfig" :disabled="savingKBConfig || loadingKBConfig">
{{ tm('knowledgeBase.clearConfig') }}
</v-btn>
<v-spacer />
<v-btn variant="text" @click="kbDialog = false" :disabled="savingKBConfig">
{{ tm('knowledgeBase.cancel') }}
</v-btn>
<v-btn color="primary" variant="tonal" @click="saveKBConfig" :loading="savingKBConfig">
{{ tm('knowledgeBase.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 提示信息 -->
<v-snackbar v-model="snackbar" :timeout="3000" elevation="24" :color="snackbarColor" location="top">
{{ snackbarText }}
@@ -399,6 +524,18 @@ export default {
newSessionName: '',
nameEditLoading: false,
// 知识库管理
kbDialog: false,
selectedSessionForKB: null,
sessionKBConfig: {
kb_ids: [],
top_k: 5,
enable_rerank: true
},
availableKBs: [],
loadingKBConfig: false,
savingKBConfig: false,
// 提示信息
snackbar: false,
snackbarText: '',
@@ -432,6 +569,7 @@ export default {
{ title: this.tm('table.headers.ttsProvider'), key: 'tts_provider', sortable: false, minWidth: '200px' },
{ title: this.tm('table.headers.llmStatus'), key: 'llm_enabled', sortable: false, minWidth: '120px' },
{ title: this.tm('table.headers.ttsStatus'), key: 'tts_enabled', sortable: false, minWidth: '120px' },
{ title: this.tm('table.headers.knowledgeBase'), key: 'knowledge_base', sortable: false, minWidth: '150px' },
{ title: this.tm('table.headers.pluginManagement'), key: 'plugins', sortable: false, minWidth: '120px' },
{ title: this.tm('table.headers.actions'), key: 'actions', sortable: false, minWidth: '100px' },
]
@@ -503,7 +641,8 @@ export default {
...session,
updating: false, // 添加更新状态标志
loadingPlugins: false, // 添加插件加载状态标志
deleting: false // 添加删除状态标志
deleting: false, // 添加删除状态标志
loadingKB: false // 添加知识库加载状态标志
}));
this.availablePersonas = data.available_personas;
this.availableChatProviders = data.available_chat_providers;
@@ -964,6 +1103,110 @@ export default {
this.currentPage = 1; // 重置到第一页
this.loadSessions();
},
// 知识库配置相关方法
async openKBManager(session) {
this.selectedSessionForKB = session;
this.kbDialog = true;
this.loadingKBConfig = true;
try {
// 加载可用的知识库列表
const kbListResponse = await axios.get('/api/kb/list');
if (kbListResponse.data.status === 'ok') {
this.availableKBs = kbListResponse.data.data.items;
}
// 加载当前会话的知识库配置
const configResponse = await axios.get('/api/kb/session/config/get', {
params: { session_id: session.session_id }
});
if (configResponse.data.status === 'ok') {
const config = configResponse.data.data;
this.sessionKBConfig = {
kb_ids: config.kb_ids || [],
top_k: config.top_k || 5,
enable_rerank: config.enable_rerank !== false
};
} else {
// 如果没有配置,使用默认值
this.sessionKBConfig = {
kb_ids: [],
top_k: 5,
enable_rerank: true
};
}
} catch (error) {
console.error('加载知识库配置失败:', error);
this.showError(this.tm('knowledgeBase.loadFailed'));
} finally {
this.loadingKBConfig = false;
}
},
async saveKBConfig() {
if (!this.selectedSessionForKB) return;
this.savingKBConfig = true;
try {
const response = await axios.post('/api/kb/session/config/set', {
scope: 'session',
scope_id: this.selectedSessionForKB.session_id,
kb_ids: this.sessionKBConfig.kb_ids,
top_k: this.sessionKBConfig.top_k,
enable_rerank: this.sessionKBConfig.enable_rerank
});
if (response.data.status === 'ok') {
this.showSuccess(this.tm('knowledgeBase.saveSuccess'));
this.kbDialog = false;
} else {
this.showError(response.data.message || this.tm('knowledgeBase.saveFailed'));
}
} catch (error) {
console.error('保存知识库配置失败:', error);
this.showError(error.response?.data?.message || this.tm('knowledgeBase.saveFailed'));
} finally {
this.savingKBConfig = false;
}
},
async clearKBConfig() {
if (!this.selectedSessionForKB) return;
if (!confirm(this.tm('knowledgeBase.clearConfirm'))) {
return;
}
this.savingKBConfig = true;
try {
const response = await axios.post('/api/kb/session/config/delete', {
scope: 'session',
scope_id: this.selectedSessionForKB.session_id
});
if (response.data.status === 'ok') {
this.showSuccess(this.tm('knowledgeBase.clearSuccess'));
this.sessionKBConfig = {
kb_ids: [],
top_k: 5,
enable_rerank: true
};
} else {
this.showError(response.data.message || this.tm('knowledgeBase.clearFailed'));
}
} catch (error) {
console.error('清除知识库配置失败:', error);
this.showError(error.response?.data?.message || this.tm('knowledgeBase.clearFailed'));
} finally {
this.savingKBConfig = false;
}
},
goToKBPage() {
this.$router.push('/knowledge-base');
},
},
}
</script>
@@ -152,6 +152,7 @@
:label="t('create.embeddingModelLabel')"
variant="outlined"
class="mb-4"
@update:model-value="handleEmbeddingProviderChange"
>
<template #item="{ props, item }">
<v-list-item v-bind="props">
@@ -165,6 +166,10 @@
</template>
</v-select>
<v-alert type="warning" variant="tonal" density="compact" class="mb-4" v-if="editingKB && showEmbeddingWarning">
<strong>注意:</strong> 修改嵌入模型会导致现有的向量数据失效,建议重新上传文档。不同的嵌入模型生成的向量不兼容,可能导致检索结果不准确。
</v-alert>
<v-select
v-model="formData.rerank_provider_id"
:items="rerankProviders"
@@ -272,6 +277,39 @@
<v-snackbar v-model="snackbar.show" :color="snackbar.color">
{{ snackbar.text }}
</v-snackbar>
<!-- Embedding Provider 修改确认对话框 -->
<v-dialog v-model="embeddingChangeDialog" max-width="500px" persistent>
<v-card>
<v-card-title class="bg-warning text-white">
<v-icon class="mr-2">mdi-alert</v-icon>
确认修改嵌入模型
</v-card-title>
<v-card-text class="pa-6">
<v-alert type="warning" variant="tonal" class="mb-4">
<strong>警告:</strong> 修改嵌入模型将导致以下影响:
</v-alert>
<ul class="text-body-2">
<li>现有的向量数据将失效</li>
<li>检索功能可能无法正常工作</li>
<li>建议删除现有文档后重新上传</li>
<li>不同嵌入模型生成的向量不兼容</li>
</ul>
<div class="mt-4 text-body-2">
您确定要将嵌入模型从 <strong>{{ originalEmbeddingProvider }}</strong> 修改为 <strong>{{ pendingEmbeddingProvider }}</strong> 吗?
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer />
<v-btn variant="text" @click="cancelEmbeddingChange">
取消
</v-btn>
<v-btn color="warning" variant="elevated" @click="confirmEmbeddingChange">
确认修改
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
@@ -291,6 +329,10 @@ const deleting = ref(false)
const kbList = ref<any[]>([])
const embeddingProviders = ref<any[]>([])
const rerankProviders = ref<any[]>([])
const originalEmbeddingProvider = ref<string | null>(null)
const showEmbeddingWarning = ref(false)
const embeddingChangeDialog = ref(false)
const pendingEmbeddingProvider = ref<string | null>(null)
// 对话框
const showCreateDialog = ref(false)
@@ -386,6 +428,7 @@ const navigateToDetail = (kbId: string) => {
// 编辑知识库
const editKB = (kb: any) => {
editingKB.value = kb
originalEmbeddingProvider.value = kb.embedding_provider_id
formData.value = {
kb_name: kb.kb_name,
description: kb.description || '',
@@ -396,6 +439,39 @@ const editKB = (kb: any) => {
showCreateDialog.value = true
}
// 处理 embedding provider 变更
const handleEmbeddingProviderChange = (newValue: string | null) => {
// 检测是否修改了embedding provider
if (newValue && originalEmbeddingProvider.value && newValue !== originalEmbeddingProvider.value) {
// 显示二次确认对话框
showEmbeddingWarning.value = true
pendingEmbeddingProvider.value = newValue
embeddingChangeDialog.value = true
} else {
showEmbeddingWarning.value = false
}
}
// 确认修改 embedding provider
const confirmEmbeddingChange = () => {
if (pendingEmbeddingProvider.value) {
formData.value.embedding_provider_id = pendingEmbeddingProvider.value
// 更新原始值,这样下次比较时不会重复弹窗
originalEmbeddingProvider.value = pendingEmbeddingProvider.value
}
embeddingChangeDialog.value = false
showEmbeddingWarning.value = true
}
// 取消修改 embedding provider
const cancelEmbeddingChange = () => {
// 恢复到原始值
formData.value.embedding_provider_id = originalEmbeddingProvider.value
embeddingChangeDialog.value = false
showEmbeddingWarning.value = false
pendingEmbeddingProvider.value = null
}
// 确认删除
const confirmDelete = (kb: any) => {
deleteTarget.value = kb
@@ -481,6 +557,9 @@ const submitForm = async () => {
const closeCreateDialog = () => {
showCreateDialog.value = false
editingKB.value = null
originalEmbeddingProvider.value = null
showEmbeddingWarning.value = false
pendingEmbeddingProvider.value = null
formData.value = {
kb_name: '',
description: '',
@@ -86,9 +86,7 @@
:label="t('settings.embeddingProvider')"
variant="outlined"
density="comfortable"
disabled
hint="嵌入模型创建后不可修改"
persistent-hint
@update:model-value="handleEmbeddingProviderChange"
/>
</v-col>
<v-col cols="12" md="6">
@@ -108,6 +106,10 @@
<v-alert type="info" variant="tonal" class="mt-4">
{{ t('settings.tips') }}
</v-alert>
<v-alert type="warning" variant="tonal" class="mt-4" v-if="showEmbeddingWarning">
<strong>注意:</strong> 修改嵌入模型会导致现有的向量数据失效,建议重新上传文档。不同的嵌入模型生成的向量不兼容,可能导致检索结果不准确。
</v-alert>
</v-form>
</v-card-text>
@@ -131,6 +133,39 @@
<v-snackbar v-model="snackbar.show" :color="snackbar.color">
{{ snackbar.text }}
</v-snackbar>
<!-- Embedding Provider修改确认对话框 -->
<v-dialog v-model="embeddingChangeDialog" max-width="500px" persistent>
<v-card>
<v-card-title class="bg-warning text-white">
<v-icon class="mr-2">mdi-alert</v-icon>
确认修改嵌入模型
</v-card-title>
<v-card-text class="pa-6">
<v-alert type="warning" variant="tonal" class="mb-4">
<strong>警告:</strong> 修改嵌入模型将导致以下影响:
</v-alert>
<ul class="text-body-2">
<li>现有的向量数据将失效</li>
<li>检索功能可能无法正常工作</li>
<li>建议删除现有文档后重新上传</li>
<li>不同嵌入模型生成的向量不兼容</li>
</ul>
<div class="mt-4 text-body-2">
您确定要将嵌入模型从 <strong>{{ originalEmbeddingProvider }}</strong> 修改为 <strong>{{ pendingEmbeddingProvider }}</strong> 吗?
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer />
<v-btn variant="text" @click="cancelEmbeddingChange">
取消
</v-btn>
<v-btn color="warning" variant="elevated" @click="confirmEmbeddingChange">
确认修改
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
@@ -152,6 +187,10 @@ const saving = ref(false)
const formRef = ref()
const embeddingProviders = ref<any[]>([])
const rerankProviders = ref<any[]>([])
const originalEmbeddingProvider = ref('')
const showEmbeddingWarning = ref(false)
const embeddingChangeDialog = ref(false)
const pendingEmbeddingProvider = ref('')
const snackbar = ref({
show: false,
@@ -190,6 +229,8 @@ watch(() => props.kb, (kb) => {
embedding_provider_id: kb.embedding_provider_id || '',
rerank_provider_id: kb.rerank_provider_id || ''
}
// 保存原始的embedding provider
originalEmbeddingProvider.value = kb.embedding_provider_id || ''
}
}, { immediate: true })
@@ -212,6 +253,33 @@ const loadProviders = async () => {
}
}
// 处理embedding provider变更
const handleEmbeddingProviderChange = (newValue: string) => {
if (newValue && newValue !== originalEmbeddingProvider.value) {
// 显示警告并需要确认
showEmbeddingWarning.value = true
pendingEmbeddingProvider.value = newValue
embeddingChangeDialog.value = true
} else {
showEmbeddingWarning.value = false
}
}
// 确认修改embedding provider
const confirmEmbeddingChange = () => {
formData.value.embedding_provider_id = pendingEmbeddingProvider.value
embeddingChangeDialog.value = false
showEmbeddingWarning.value = true
}
// 取消修改embedding provider
const cancelEmbeddingChange = () => {
formData.value.embedding_provider_id = originalEmbeddingProvider.value
embeddingChangeDialog.value = false
showEmbeddingWarning.value = false
pendingEmbeddingProvider.value = ''
}
// 保存设置
const saveSettings = async () => {
const { valid } = await formRef.value.validate()