feat: 添加会话知识库配置的 API 接口,支持获取、设置和删除会话配置,优化知识库选择界面

This commit is contained in:
lxfight
2025-10-25 13:52:57 +08:00
parent 594ccff9c8
commit 6cb5527894
5 changed files with 297 additions and 46 deletions

View File

@@ -11,27 +11,71 @@ async def inject_kb_context(
"""inject knowledge base context into the provider request
Args:
umo: Unique message object (session ID)
p_ctx: Pipeline context
req: Provider request
"""
from astrbot.core import sp
kb_mgr = p_ctx.plugin_manager.context.kb_manager
kb_names = p_ctx.astrbot_config.get("kb_names", [])
# 1. 优先读取会话级配置
session_config = await sp.session_get(umo, "kb_config", default={})
if session_config and "kb_ids" in session_config:
# 使用会话级配置
kb_ids = session_config.get("kb_ids", [])
# 如果配置为空列表,明确表示不使用知识库
if not kb_ids:
logger.info(f"[KB注入] 会话 {umo} 已配置为不使用知识库")
return
top_k = session_config.get("top_k", 5)
# 将 kb_ids 转换为 kb_names
kb_names = []
invalid_kb_ids = []
for kb_id in kb_ids:
kb_helper = await kb_mgr.get_kb(kb_id)
if kb_helper:
kb_names.append(kb_helper.kb.kb_name)
else:
logger.warning(f"[KB注入] 知识库不存在或未加载: {kb_id}")
invalid_kb_ids.append(kb_id)
if invalid_kb_ids:
logger.warning(f"[KB注入] 会话 {umo} 配置的以下知识库无效: {invalid_kb_ids}")
if not kb_names:
logger.warning(f"[KB注入] 会话 {umo} 配置的所有知识库都无效,跳过知识库上下文注入")
return
logger.debug(f"[KB注入] 使用会话级配置,知识库数量: {len(kb_names)}")
else:
# 回退到全局配置
kb_names = p_ctx.astrbot_config.get("kb_names", [])
top_k = p_ctx.astrbot_config.get("kb_final_top_k", 5)
logger.debug(f"[KB注入] 使用全局配置,知识库数量: {len(kb_names)}")
top_k_fusion = p_ctx.astrbot_config.get("kb_fusion_top_k", 20)
top_k = p_ctx.astrbot_config.get("kb_final_top_k", 5)
if not kb_names:
return
logger.debug(f"[KB注入] 开始检索知识库,数量: {len(kb_names)}, top_k={top_k}")
kb_context = await kb_mgr.retrieve(
query=req.prompt,
kb_names=kb_names,
top_k_fusion=top_k_fusion,
top_m_final=top_k,
)
if not kb_context:
return
formatted = kb_context.get("context_text", "") if kb_context else ""
formatted = kb_context.get("context_text", "")
if formatted:
results = kb_context.get("results", [])
logger.debug(f"知识库上下文注入:请求注入了 {len(results)} 条相关知识块")
logger.debug(f"[KB注入]会话 {umo} 注入了 {len(results)} 条相关知识块")
req.system_prompt = f"{formatted}\n\n{req.system_prompt or ''}"

View File

@@ -55,6 +55,10 @@ class KnowledgeBaseRoute(Route):
# "/kb/media/delete": ("POST", self.delete_media),
# 检索
"/kb/retrieve": ("POST", self.retrieve),
# 会话知识库配置
"/kb/session/config/get": ("GET", self.get_session_kb_config),
"/kb/session/config/set": ("POST", self.set_session_kb_config),
"/kb/session/config/delete": ("POST", self.delete_session_kb_config),
}
self.register_routes()
@@ -864,3 +868,156 @@ class KnowledgeBaseRoute(Route):
logger.error(f"检索失败: {e}")
logger.error(traceback.format_exc())
return Response().error(f"检索失败: {str(e)}").__dict__
# ===== 会话知识库配置 API =====
async def get_session_kb_config(self):
"""获取会话的知识库配置
Query 参数:
- session_id: 会话 ID (必填)
返回:
- kb_ids: 知识库 ID 列表
- top_k: 返回结果数量
- enable_rerank: 是否启用重排序
"""
try:
from astrbot.core import sp
session_id = request.args.get("session_id")
if not session_id:
return Response().error("缺少参数 session_id").__dict__
# 从 SharedPreferences 获取配置
config = await sp.session_get(session_id, "kb_config", default={})
logger.debug(f"[KB配置] 读取到配置: session_id={session_id}")
# 如果没有配置,返回默认值
if not config:
config = {
"kb_ids": [],
"top_k": 5,
"enable_rerank": True
}
return Response().ok(config).__dict__
except Exception as e:
logger.error(f"[KB配置] 获取配置时出错: {e}", exc_info=True)
return Response().error(f"获取会话知识库配置失败: {str(e)}").__dict__
async def set_session_kb_config(self):
"""设置会话的知识库配置
Body:
- scope: 配置范围 (目前只支持 "session")
- scope_id: 会话 ID (必填)
- kb_ids: 知识库 ID 列表 (必填)
- top_k: 返回结果数量 (可选, 默认 5)
- enable_rerank: 是否启用重排序 (可选, 默认 true)
"""
try:
from astrbot.core import sp
data = await request.json
scope = data.get("scope")
scope_id = data.get("scope_id")
kb_ids = data.get("kb_ids", [])
top_k = data.get("top_k", 5)
enable_rerank = data.get("enable_rerank", True)
# 验证参数
if scope != "session":
return Response().error("目前仅支持 session 范围的配置").__dict__
if not scope_id:
return Response().error("缺少参数 scope_id").__dict__
if not isinstance(kb_ids, list):
return Response().error("kb_ids 必须是列表").__dict__
# 验证知识库是否存在
kb_mgr = self._get_kb_manager()
invalid_ids = []
valid_ids = []
for kb_id in kb_ids:
kb_helper = await kb_mgr.get_kb(kb_id)
if kb_helper:
valid_ids.append(kb_id)
else:
invalid_ids.append(kb_id)
logger.warning(f"[KB配置] 知识库不存在: {kb_id}")
if invalid_ids:
logger.warning(f"[KB配置] 以下知识库ID无效: {invalid_ids}")
# 允许保存空列表,表示明确不使用任何知识库
if kb_ids and not valid_ids:
# 只有当用户提供了 kb_ids 但全部无效时才报错
return Response().error(f"所有提供的知识库ID都无效: {kb_ids}").__dict__
# 如果 kb_ids 为空列表,表示用户想清空配置
if not kb_ids:
valid_ids = []
# 构建配置对象只保存有效的ID
config = {
"kb_ids": valid_ids,
"top_k": top_k,
"enable_rerank": enable_rerank
}
# 保存到 SharedPreferences
await sp.session_put(scope_id, "kb_config", config)
# 立即验证是否保存成功
verify_config = await sp.session_get(scope_id, "kb_config", default={})
if verify_config == config:
return Response().ok(
{"valid_ids": valid_ids, "invalid_ids": invalid_ids},
"保存知识库配置成功"
).__dict__
else:
logger.error(f"[KB配置] 配置保存失败,验证不匹配")
return Response().error("配置保存失败").__dict__
except Exception as e:
logger.error(f"[KB配置] 设置配置时出错: {e}", exc_info=True)
return Response().error(f"设置会话知识库配置失败: {str(e)}").__dict__
async def delete_session_kb_config(self):
"""删除会话的知识库配置
Body:
- scope: 配置范围 (目前只支持 "session")
- scope_id: 会话 ID (必填)
"""
try:
from astrbot.core import sp
data = await request.json
scope = data.get("scope")
scope_id = data.get("scope_id")
# 验证参数
if scope != "session":
return Response().error("目前仅支持 session 范围的配置").__dict__
if not scope_id:
return Response().error("缺少参数 scope_id").__dict__
# 从 SharedPreferences 删除配置
await sp.session_remove(scope_id, "kb_config")
return Response().ok(message="删除知识库配置成功").__dict__
except Exception as e:
logger.error(f"删除会话知识库配置失败: {e}")
logger.error(traceback.format_exc())
return Response().error(f"删除会话知识库配置失败: {str(e)}").__dict__

View File

@@ -93,6 +93,9 @@
"clearFailed": "Failed to clear knowledge base configuration",
"clearConfirm": "Are you sure you want to clear the knowledge base configuration for this session?"
},
"list": {
"documents": "documents"
},
"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?"

View File

@@ -93,6 +93,9 @@
"clearFailed": "清除知识库配置失败",
"clearConfirm": "确定要清除此会话的知识库配置吗?"
},
"list": {
"documents": "篇文档"
},
"deleteConfirm": {
"message": "确定要删除会话 {sessionName} 吗?",
"warning": "此操作将永久删除本次会话的「全部对话记录」与「偏好设置」(插件对会话的关联数据除外),且无法恢复。确认继续?"

View File

@@ -344,12 +344,12 @@
</v-dialog>
<!-- 知识库配置对话框 -->
<v-dialog v-model="kbDialog" max-width="800" min-height="60%">
<v-dialog v-model="kbDialog" max-width="800" min-height="60%" @update:model-value="(val) => { if (!val) closeKBDialog(); }">
<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-btn icon variant="text" color="white" @click="closeKBDialog()">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
@@ -360,39 +360,41 @@
{{ 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>
<!-- 知识库选择区域 -->
<div class="mb-4">
<div class="text-subtitle-2 mb-2">{{ tm('knowledgeBase.selectKB') }}</div>
<v-card variant="outlined" class="pa-3">
<div v-if="availableKBs.length === 0" class="text-body-2 text-medium-emphasis">
{{ tm('knowledgeBase.noKBAvailable') || '暂无可用知识库' }}
</div>
<div v-else>
<v-checkbox
v-for="kb in availableKBs"
:key="kb.kb_id"
:value="kb.kb_id"
v-model="sessionKBConfig.kb_ids"
hide-details
density="compact"
class="mb-1"
>
<template v-slot:label>
<div class="d-flex align-center">
<span style="font-size: 18px; margin-right: 8px;">{{ kb.emoji }}</span>
<div>
<div class="text-body-2">{{ kb.kb_name }}</div>
<div class="text-caption text-medium-emphasis">
{{ kb.description || tm('knowledgeBase.noKBDesc') }} - {{ kb.doc_count }} {{ tm('list.documents', { count: kb.doc_count }) }}
</div>
</div>
</div>
</template>
</v-checkbox>
</div>
</v-card>
<div class="text-caption text-medium-emphasis mt-2">
{{ tm('knowledgeBase.selectMultiple') }}
</div>
</div>
<!-- 高级配置 -->
<v-expansion-panels class="mb-4">
@@ -450,7 +452,7 @@
{{ tm('knowledgeBase.clearConfig') }}
</v-btn>
<v-spacer />
<v-btn variant="text" @click="kbDialog = false" :disabled="savingKBConfig">
<v-btn variant="text" @click="closeKBDialog()" :disabled="savingKBConfig">
{{ tm('knowledgeBase.cancel') }}
</v-btn>
<v-btn color="primary" variant="tonal" @click="saveKBConfig" :loading="savingKBConfig">
@@ -1107,6 +1109,14 @@ export default {
// 知识库配置相关方法
async openKBManager(session) {
this.selectedSessionForKB = session;
// ✅ 先重置配置为默认值,避免数据残留
this.sessionKBConfig = {
kb_ids: [],
top_k: 5,
enable_rerank: true
};
this.kbDialog = true;
this.loadingKBConfig = true;
@@ -1124,13 +1134,20 @@ export default {
if (configResponse.data.status === 'ok') {
const config = configResponse.data.data;
// 初始化配置
this.sessionKBConfig = {
kb_ids: config.kb_ids || [],
kb_ids: [],
top_k: config.top_k || 5,
enable_rerank: config.enable_rerank !== false
};
// 如果有配置的 kb_ids进行清理后赋值
if (config.kb_ids && Array.isArray(config.kb_ids)) {
this.sessionKBConfig.kb_ids = config.kb_ids.filter(id => id != null && id !== '');
}
} else {
// 如果没有配置,使用默认值
// 保持默认值(已在开头设置)
this.sessionKBConfig = {
kb_ids: [],
top_k: 5,
@@ -1148,19 +1165,33 @@ export default {
async saveKBConfig() {
if (!this.selectedSessionForKB) return;
// 确保 kb_ids 是一个干净的数组
const cleanKbIds = Array.isArray(this.sessionKBConfig.kb_ids)
? this.sessionKBConfig.kb_ids.filter(id => id != null && id !== '')
: [];
this.savingKBConfig = true;
try {
const response = await axios.post('/api/kb/session/config/set', {
const payload = {
scope: 'session',
scope_id: this.selectedSessionForKB.session_id,
kb_ids: this.sessionKBConfig.kb_ids,
kb_ids: cleanKbIds, // 使用清理后的数组
top_k: this.sessionKBConfig.top_k,
enable_rerank: this.sessionKBConfig.enable_rerank
});
};
const response = await axios.post('/api/kb/session/config/set', payload);
if (response.data.status === 'ok') {
this.showSuccess(this.tm('knowledgeBase.saveSuccess'));
this.kbDialog = false;
// ✅ 保存成功后清理数据
this.sessionKBConfig = {
kb_ids: [],
top_k: 5,
enable_rerank: true
};
this.selectedSessionForKB = null;
} else {
this.showError(response.data.message || this.tm('knowledgeBase.saveFailed'));
}
@@ -1172,6 +1203,19 @@ export default {
}
},
// 关闭知识库配置对话框并清理数据
closeKBDialog() {
this.kbDialog = false;
// 清理数据,避免下次打开时出现旧数据
this.sessionKBConfig = {
kb_ids: [],
top_k: 5,
enable_rerank: true
};
this.selectedSessionForKB = null;
this.availableKBs = [];
},
async clearKBConfig() {
if (!this.selectedSessionForKB) return;
@@ -1213,7 +1257,7 @@ export default {
<style scoped>
.v-data-table>>>.v-data-table__td {
.v-data-table :deep(.v-data-table__td) {
padding: 8px 16px !important;
vertical-align: middle !important;
}