feat: 收敛知识库检索调度与评分语义

- 固定 rag.engine 与 Milvus 配置,补齐启动期检索基础设施校验

- 支持调用方配置 retrievalMode,并统一知识库检索入口与结果来源展示

- 修正关键词检索 knowledgeId 过滤、混合检索评分归一化与本地 ES 默认配置
This commit is contained in:
2026-04-05 20:23:05 +08:00
parent 2592a1f09d
commit b5dd427920
41 changed files with 1260 additions and 600 deletions

View File

@@ -18,6 +18,7 @@ import {
import { useDebounceFn } from '@vueuse/core';
import {
ElAlert,
ElAvatar,
ElButton,
ElCol,
ElCollapse,
@@ -26,6 +27,7 @@ import {
ElInput,
ElInputNumber,
ElMessage,
ElMessageBox,
ElOption,
ElRow,
ElSelect,
@@ -63,6 +65,13 @@ interface ApiKeyOption {
label: string;
}
type RetrievalMode = 'HYBRID' | 'KEYWORD' | 'VECTOR';
interface BotKnowledgeBindingItem {
knowledgeId: string;
retrievalMode: RetrievalMode;
}
const props = defineProps<{
bot?: BotInfo;
hasSavePermission?: boolean;
@@ -70,6 +79,7 @@ const props = defineProps<{
const botStore = useBotStore();
const route = useRoute();
const botId = ref<string>((route.params.id as string) || '');
const fallbackAvatarUrl = `${import.meta.env.BASE_URL || '/'}favicon.svg`;
const options = ref<AiLlm[]>([]);
const selectedId = ref<string>('');
const llmConfig = ref({
@@ -170,11 +180,11 @@ const iframeCodeHighlighted = computed(() => {
}
const escaped = escapeHtml(iframeCode.value);
return escaped
.replace(
.replaceAll(
/(&lt;\/?)([a-zA-Z][\w-]*)/g,
'$1<span class="hljs-name">$2</span>',
)
.replace(
.replaceAll(
/([:@a-zA-Z_][\w:-]*)=(&quot;.*?&quot;)/g,
'<span class="hljs-attr">$1</span>=<span class="hljs-string">$2</span>',
);
@@ -202,6 +212,32 @@ const mcpToolData = ref<any>([]);
const workflowData = ref<any[]>([]);
const knowledgeData = ref<any[]>([]);
const pluginToolData = ref<any[]>([]);
const retrievalModeOptions = computed(() => [
{ label: $t('bot.retrievalModes.hybrid'), value: 'HYBRID' },
{ label: $t('bot.retrievalModes.vector'), value: 'VECTOR' },
{ label: $t('bot.retrievalModes.keyword'), value: 'KEYWORD' },
]);
const normalizeRetrievalMode = (value?: string): RetrievalMode => {
if (value === 'VECTOR' || value === 'KEYWORD' || value === 'HYBRID') {
return value;
}
return 'HYBRID';
};
const buildKnowledgeBindingsPayload = (
selectedIds: Array<number | string>,
): BotKnowledgeBindingItem[] => {
return [...new Set((selectedIds || []).map(String).filter(Boolean))].map(
(knowledgeId) => {
const existing = knowledgeData.value.find(
(item) => String(item.id) === knowledgeId,
);
return {
knowledgeId,
retrievalMode: normalizeRetrievalMode(existing?.retrievalMode),
};
},
);
};
const getAiBotPluginToolList = async () => {
api
.post('/api/v1/pluginItem/tool/list', { botId: botId.value })
@@ -226,6 +262,7 @@ const getAiBotKnowledgeList = async () => {
knowledgeData.value = res.data.map((item: any) => {
return {
recordId: item.id,
retrievalMode: normalizeRetrievalMode(item.options?.retrievalMode),
...item.knowledge,
};
});
@@ -419,7 +456,7 @@ const copyText = async (value: string) => {
textarea.style.opacity = '0';
textarea.style.pointerEvents = 'none';
document.body.appendChild(textarea);
document.body.append(textarea);
const selection = document.getSelection();
const previousRange =
@@ -430,7 +467,7 @@ const copyText = async (value: string) => {
textarea.setSelectionRange(0, textarea.value.length);
const copied = document.execCommand('copy');
document.body.removeChild(textarea);
textarea.remove();
if (selection) {
selection.removeAllRanges();
if (previousRange) {
@@ -543,7 +580,7 @@ const confirmUpdateAiBotKnowledge = (data: any) => {
api
.post('/api/v1/botKnowledge/updateBotKnowledgeIds', {
botId: botId.value,
knowledgeIds: data,
knowledgeBindings: buildKnowledgeBindingsPayload(data),
})
.then((res) => {
if (res.errorCode === 0) {
@@ -555,6 +592,31 @@ const confirmUpdateAiBotKnowledge = (data: any) => {
});
};
const updateKnowledgeBindings = (bindings: BotKnowledgeBindingItem[]) => {
return api.post('/api/v1/botKnowledge/updateBotKnowledgeIds', {
botId: botId.value,
knowledgeBindings: bindings,
});
};
const handleKnowledgeRetrievalModeChange = async (
item: any,
value: RetrievalMode | string,
) => {
item.retrievalMode = normalizeRetrievalMode(value);
const bindings = knowledgeData.value.map((knowledgeItem: any) => ({
knowledgeId: String(knowledgeItem.id),
retrievalMode: normalizeRetrievalMode(knowledgeItem.retrievalMode),
}));
const res = await updateKnowledgeBindings(bindings);
if (res.errorCode === 0) {
ElMessage.success($t('message.updateOkMessage'));
getAiBotKnowledgeList();
return;
}
ElMessage.error(res.message || $t('message.updateFailMessage'));
};
const confirmUpdateAiBotWorkflow = (data: any) => {
api
.post('/api/v1/botWorkflow/updateBotWorkflowIds', {
@@ -616,7 +678,20 @@ const deleteBotMcpTool = (item: any) => {
});
};
const deleteKnowledge = (item: any) => {
const deleteKnowledge = async (item: any) => {
try {
await ElMessageBox.confirm(
$t('message.deleteAlert'),
$t('message.noticeTitle'),
{
confirmButtonText: $t('button.confirm'),
cancelButtonText: $t('button.cancel'),
type: 'warning',
},
);
} catch {
return;
}
api
.post('/api/v1/botKnowledge/remove', {
id: item.recordId,
@@ -840,10 +915,10 @@ const handleBasicInfoChange = async (
<div class="bot-avatar-panel">
<span class="bot-avatar-label">{{ $t('common.avatar') }}</span>
<div
:class="[
'bot-avatar-upload-wrap',
!hasSavePermission || updatingBotIcon ? 'is-disabled' : '',
]"
class="bot-avatar-upload-wrap"
:class="
!hasSavePermission || updatingBotIcon ? 'is-disabled' : ''
"
>
<UploadAvatar
v-if="botInfo"
@@ -1105,7 +1180,53 @@ const handleBasicInfoChange = async (
</div>
</div>
</template>
<CollapseViewItem :data="knowledgeData" @delete="deleteKnowledge" />
<div class="knowledge-binding-list">
<div
v-for="item in knowledgeData"
:key="item.recordId"
class="knowledge-binding-item"
>
<div class="knowledge-binding-main">
<ElAvatar
:src="item.icon || fallbackAvatarUrl"
shape="circle"
/>
<div class="knowledge-binding-content">
<div class="knowledge-binding-title">
{{ item.title }}
</div>
<div class="knowledge-binding-description">
{{ item.description }}
</div>
</div>
</div>
<div class="knowledge-binding-actions" @click.stop>
<ElSelect
:model-value="item.retrievalMode"
class="knowledge-binding-select"
size="small"
@change="
(value) => handleKnowledgeRetrievalModeChange(item, value)
"
>
<ElOption
v-for="option in retrievalModeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</ElSelect>
<ElIcon
class="knowledge-binding-delete"
color="var(--el-color-danger)"
size="20px"
@click="deleteKnowledge(item)"
>
<Delete />
</ElIcon>
</div>
</div>
</div>
</ElCollapseItem>
<ElCollapseItem>
<template #title>
@@ -1779,4 +1900,64 @@ const handleBasicInfoChange = async (
height: 50px;
border-radius: 50%;
}
.knowledge-binding-list {
display: flex;
flex-direction: column;
gap: 12px;
padding: 10px;
background-color: var(--bot-collapse-itme-back);
}
.knowledge-binding-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px;
border-radius: 8px;
background-color: hsl(var(--background));
}
.knowledge-binding-main {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
flex: 1;
}
.knowledge-binding-content {
min-width: 0;
}
.knowledge-binding-title {
font-size: 12px;
font-weight: 500;
line-height: 24px;
color: var(--el-text-color-primary);
}
.knowledge-binding-description {
font-size: 12px;
line-height: 20px;
color: var(--el-text-color-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.knowledge-binding-actions {
display: flex;
align-items: center;
gap: 12px;
}
.knowledge-binding-select {
width: 140px;
}
.knowledge-binding-delete {
cursor: pointer;
}
</style>