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

@@ -0,0 +1,227 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { $t } from '@easyflow/locales';
import {
ElButton,
ElCard,
ElForm,
ElFormItem,
ElInputNumber,
ElMessage,
ElOption,
ElSelect,
ElSwitch,
} from 'element-plus';
import { api } from '#/api/request';
import { buildKnowledgePath } from '#/views/ai/documentCollection/share-path';
const props = defineProps({
knowledgeId: {
type: String,
required: true,
},
detailData: {
type: Object as any,
default: () => ({}),
},
requestClient: {
type: Object as any,
default: () => api,
},
endpointPrefix: {
type: String,
default: '',
},
});
const emit = defineEmits(['reload']);
const form = ref<any>({
knowledgeId: '',
vectorEmbedModelId: '',
rerankModelId: '',
rerankEnable: false,
docRecallMaxNum: 5,
simThreshold: 0.6,
rebuildVectors: false,
});
const embeddingModels = ref<any[]>([]);
const rerankModels = ref<any[]>([]);
const saving = ref(false);
const syncForm = () => {
const options = props.detailData?.options || {};
form.value = {
knowledgeId: props.knowledgeId,
vectorEmbedModelId: props.detailData?.vectorEmbedModelId || '',
rerankModelId: props.detailData?.rerankModelId || '',
rerankEnable:
options?.rerankEnable === undefined
? !!props.detailData?.rerankModelId
: !!options.rerankEnable,
docRecallMaxNum: Number(options?.docRecallMaxNum || 5),
simThreshold: Number(options?.simThreshold || 0.6),
rebuildVectors: false,
};
};
watch(
() => [props.detailData, props.knowledgeId],
() => {
syncForm();
},
{ immediate: true, deep: true },
);
const loadModels = async () => {
const [embeddingRes, rerankRes] = await Promise.all([
props.requestClient.get(
buildKnowledgePath(
props.endpointPrefix,
'/api/v1/documentCollection/modelList',
),
{ params: { modelType: 'embeddingModel' } },
),
props.requestClient.get(
buildKnowledgePath(
props.endpointPrefix,
'/api/v1/documentCollection/modelList',
),
{ params: { modelType: 'rerankModel' } },
),
]);
embeddingModels.value = embeddingRes?.data || [];
rerankModels.value = rerankRes?.data || [];
};
const handleSave = async () => {
saving.value = true;
try {
const res = await props.requestClient.post(
buildKnowledgePath(
props.endpointPrefix,
'/api/v1/documentCollection/shareConfigUpdate',
),
form.value,
);
if (res.errorCode === 0) {
ElMessage.success($t('message.saveOkMessage'));
emit('reload');
}
} finally {
saving.value = false;
}
};
onMounted(() => {
loadModels();
});
</script>
<template>
<ElCard shadow="never" class="share-config-card">
<template #header>
<div class="share-config-card__header">
<div>
<div class="share-config-card__title">受限配置</div>
<div class="share-config-card__desc">
仅开放模型切换检索参数与向量重建
</div>
</div>
<ElButton type="primary" :loading="saving" @click="handleSave">
{{ $t('button.save') }}
</ElButton>
</div>
</template>
<ElForm label-position="top" class="share-config-form">
<ElFormItem :label="$t('documentCollection.vectorEmbedLlmId')">
<ElSelect v-model="form.vectorEmbedModelId">
<ElOption
v-for="item in embeddingModels"
:key="item.id"
:label="item.title"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem :label="$t('documentCollection.rerankLlmId')">
<ElSelect v-model="form.rerankModelId" clearable>
<ElOption
v-for="item in rerankModels"
:key="item.id"
:label="item.title"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem :label="$t('documentCollection.rerankEnable')">
<ElSwitch v-model="form.rerankEnable" />
</ElFormItem>
<ElFormItem :label="$t('documentCollectionSearch.docRecallMaxNum.label')">
<ElInputNumber v-model="form.docRecallMaxNum" :min="1" :max="50" />
</ElFormItem>
<ElFormItem :label="$t('documentCollectionSearch.simThreshold.label')">
<ElInputNumber
v-model="form.simThreshold"
:min="0"
:max="1"
:step="0.01"
/>
</ElFormItem>
<ElFormItem label="保存后重建向量">
<ElSwitch v-model="form.rebuildVectors" />
</ElFormItem>
</ElForm>
</ElCard>
</template>
<style scoped>
.share-config-card {
border-radius: 16px;
}
.share-config-card__header {
display: flex;
gap: 16px;
align-items: center;
justify-content: space-between;
}
.share-config-card__title {
font-size: 16px;
font-weight: 600;
}
.share-config-card__desc {
margin-top: 4px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.share-config-form {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0 16px;
}
@media (max-width: 768px) {
.share-config-form {
grid-template-columns: 1fr;
}
.share-config-card__header {
flex-direction: column;
align-items: flex-start;
}
}
</style>