feat: 收口知识库分享链路

- 新增 shareKey 单参数 URL 分享页与失效页

- 新增知识库分享后端鉴权、审计与迁移脚本

- 在访问令牌中增加知识库分享授权入口
This commit is contained in:
2026-04-13 14:44:31 +08:00
parent 8cfe5400fe
commit 31a755a8bc
57 changed files with 5158 additions and 143 deletions

View File

@@ -1,12 +1,13 @@
<script setup lang="ts">
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { $t } from '@easyflow/locales';
import { ElButton, ElInput, ElMessage, ElOption, ElSelect } from 'element-plus';
import { ElButton, ElInput, ElMessage } from 'element-plus';
import { api } from '#/api/request';
import PreviewSearchKnowledge from '#/views/ai/documentCollection/PreviewSearchKnowledge.vue';
import { buildKnowledgePath } from '#/views/ai/documentCollection/share-path';
type RetrievalMode = 'HYBRID' | 'KEYWORD' | 'VECTOR';
@@ -24,6 +25,14 @@ const props = defineProps({
type: String,
required: true,
},
requestClient: {
type: Object as any,
default: () => api,
},
endpointPrefix: {
type: String,
default: '',
},
});
const searchDataList = ref<SearchResultItem[]>([]);
@@ -31,11 +40,29 @@ const keyword = ref('');
const retrievalMode = ref<RetrievalMode>('HYBRID');
const previewSearchKnowledgeRef = ref();
const retrievalModeOptions = [
{ label: $t('bot.retrievalModes.hybrid'), value: 'HYBRID' },
{ label: $t('bot.retrievalModes.vector'), value: 'VECTOR' },
{ label: $t('bot.retrievalModes.keyword'), value: 'KEYWORD' },
];
const retrievalModeDescriptions = computed(() => [
{
key: 'HYBRID' as RetrievalMode,
label: $t('bot.retrievalModes.hybrid'),
description: $t(
'documentCollectionSearch.retrievalModeDescriptions.hybrid',
),
},
{
key: 'VECTOR' as RetrievalMode,
label: $t('bot.retrievalModes.vector'),
description: $t(
'documentCollectionSearch.retrievalModeDescriptions.vector',
),
},
{
key: 'KEYWORD' as RetrievalMode,
label: $t('bot.retrievalModes.keyword'),
description: $t(
'documentCollectionSearch.retrievalModeDescriptions.keyword',
),
},
]);
const handleSearch = () => {
if (!keyword.value) {
@@ -43,14 +70,20 @@ const handleSearch = () => {
return;
}
previewSearchKnowledgeRef.value.loadingContent(true);
api
.get('/api/v1/documentCollection/search', {
params: {
knowledgeId: props.knowledgeId,
keyword: keyword.value,
retrievalMode: retrievalMode.value,
props.requestClient
.get(
buildKnowledgePath(
props.endpointPrefix,
'/api/v1/documentCollection/search',
),
{
params: {
knowledgeId: props.knowledgeId,
keyword: keyword.value,
retrievalMode: retrievalMode.value,
},
},
})
)
.then((res) => {
searchDataList.value = res.data;
})
@@ -62,6 +95,10 @@ const handleSearch = () => {
previewSearchKnowledgeRef.value.loadingContent(false);
});
};
const handleRetrievalModeChange = (mode: RetrievalMode) => {
retrievalMode.value = mode;
};
</script>
<template>
@@ -72,23 +109,35 @@ const handleSearch = () => {
:placeholder="$t('common.searchPlaceholder')"
@keyup.enter="handleSearch"
/>
<ElSelect v-model="retrievalMode" class="retrieval-select">
<ElOption
v-for="item in retrievalModeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
<ElButton type="primary" @click="handleSearch">
{{ $t('button.query') }}
</ElButton>
</div>
<div class="search-hint">
<div class="search-hint__title">
{{ $t('documentCollectionSearch.retrievalModeTitle') }}
</div>
<div class="search-hint__list">
<button
v-for="item in retrievalModeDescriptions"
:key="item.key"
type="button"
class="search-hint__item"
:class="{ 'is-active': retrievalMode === item.key }"
@click="handleRetrievalModeChange(item.key)"
>
<div class="search-hint__label">{{ item.label }}</div>
<div class="search-hint__desc">{{ item.description }}</div>
</button>
</div>
</div>
<div class="search-result">
<PreviewSearchKnowledge
ref="previewSearchKnowledgeRef"
:data="searchDataList"
:retrieval-mode="retrievalMode"
ref="previewSearchKnowledgeRef"
/>
</div>
</div>
@@ -99,7 +148,6 @@ const handleSearch = () => {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: 0 0 20px;
}
@@ -107,16 +155,84 @@ const handleSearch = () => {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
}
.retrieval-select {
flex-shrink: 0;
width: 180px;
.search-input :deep(.el-input) {
flex: 1;
}
.search-hint {
display: grid;
gap: 12px;
margin-top: 16px;
padding: 18px 20px;
background: var(--el-fill-color-blank);
border: 1px solid var(--el-border-color-lighter);
border-radius: 16px;
}
.search-hint__title {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.search-hint__list {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.search-hint__item {
display: grid;
gap: 6px;
min-width: 0;
padding: 14px 16px;
text-align: left;
cursor: pointer;
background: var(--el-fill-color-light);
border: 1px solid transparent;
border-radius: 14px;
transition:
border-color 0.2s ease,
background-color 0.2s ease,
box-shadow 0.2s ease;
}
.search-hint__item.is-active {
background: var(--el-color-primary-light-9);
border-color: var(--el-color-primary-light-7);
box-shadow: 0 6px 18px rgb(64 158 255 / 10%);
}
.search-hint__item:focus-visible {
outline: 2px solid var(--el-color-primary-light-5);
outline-offset: 2px;
}
.search-hint__label {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.search-hint__desc {
font-size: 13px;
line-height: 1.6;
color: var(--el-text-color-secondary);
}
.search-result {
flex: 1;
padding-top: 20px;
}
@media (max-width: 960px) {
.search-hint__list {
grid-template-columns: 1fr;
}
.search-input {
flex-wrap: wrap;
}
}
</style>