mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-20 02:55:08 +08:00
feat: 添加会话知识库配置的 API 接口,支持获取、设置和删除会话配置,优化知识库选择界面
This commit is contained in:
@@ -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 ''}"
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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?"
|
||||
|
||||
@@ -93,6 +93,9 @@
|
||||
"clearFailed": "清除知识库配置失败",
|
||||
"clearConfirm": "确定要清除此会话的知识库配置吗?"
|
||||
},
|
||||
"list": {
|
||||
"documents": "篇文档"
|
||||
},
|
||||
"deleteConfirm": {
|
||||
"message": "确定要删除会话 {sessionName} 吗?",
|
||||
"warning": "此操作将永久删除本次会话的「全部对话记录」与「偏好设置」(插件对会话的关联数据除外),且无法恢复。确认继续?"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user