【功能新增】AI:知识库文档上传:90%,ProcessStep 已完成

This commit is contained in:
YunaiV 2025-03-01 13:33:37 +08:00
parent 809fb9fdfc
commit 1958c2bb9d
5 changed files with 7298 additions and 6031 deletions

12928
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
import request from '@/config/axios'
// AI 知识库分片 VO
export interface AiKnowledgeSegmentRespVO {
id: number // 编号
documentId: number // 文档编号
knowledgeId: number // 知识库编号
vectorId: string // 向量库编号
content: string // 切片内容
contentLength: number // 切片内容长度
tokens: number // token 数量
retrievalCount: number // 召回次数
status: number // 文档状态
createTime: number // 创建时间
}
// AI 知识库分片 API
export const KnowledgeSegmentApi = {
// 查询知识库分片分页
getKnowledgeSegmentPage: async (params: any) => {
return await request.get({ url: `/ai/knowledge/segment/page`, params })
},
// 查询知识库分片详情
getKnowledgeSegment: async (id: number) => {
return await request.get({ url: `/ai/knowledge/segment/get?id=` + id })
},
// 删除知识库分片
deleteKnowledgeSegment: async (id: number) => {
return await request.delete({ url: `/ai/knowledge/segment/delete?id=` + id })
},
// 切片内容
splitContent: async (url: string, segmentMaxTokens: number) => {
return await request.get({
url: `/ai/knowledge/segment/split`,
params: { url, segmentMaxTokens }
})
},
// 获取文档处理列表
getKnowledgeSegmentProcessList: async (documentIds: number[]) => {
return await request.get({
url: `/ai/knowledge/segment/get-process-list`,
params: { documentIds: documentIds.join(',') }
})
}
}

View File

@ -1,235 +1,146 @@
<template> <template>
<div class="process-complete"> <div>
<div class="mb-20px"> <!-- 文件处理列表 -->
<el-alert <div class="mt-15px grid grid-cols-1 gap-2">
title="处理说明" <div
type="info" v-for="(file, index) in modelValue.list"
description="系统将对文档进行处理,包括文本提取、向量化等操作,处理完成后文档将被添加到知识库中。" :key="index"
show-icon class="flex items-center py-4px px-12px border-l-4 border-l-[#409eff] rounded-sm shadow-sm hover:bg-[#ecf5ff] transition-all duration-300"
:closable="false" >
/> <!-- 文件图标和名称 -->
<div class="flex items-center min-w-[200px] mr-10px">
<Icon icon="ep:document" class="mr-8px text-[#409eff]" />
<span class="text-[13px] text-[#303133] break-all">{{ file.name }}</span>
</div>
<!-- 处理进度 -->
<div class="flex-1">
<el-progress
:percentage="file.progress || 0"
:stroke-width="10"
:status="isProcessComplete(file) ? 'success' : ''"
/>
</div>
<!-- 分段数量 -->
<div class="ml-10px text-[13px] text-[#606266]">
分段数量{{ file.count ? file.count : '-' }}
</div>
</div>
</div> </div>
<div class="mb-20px"> <!-- 底部完成按钮 -->
<el-card class="box-card"> <div class="flex justify-end mt-20px">
<template #header> <el-button
<div class="card-header"> :type="allProcessComplete ? 'success' : 'primary'"
<span class="text-16px font-bold">文档信息</span> :disabled="!allProcessComplete"
</div> @click="handleComplete"
</template> >
<div class="document-info"> 完成
<div class="info-item"> </el-button>
<span class="label">文档名称</span>
<span class="value">{{ modelData.name }}</span>
</div>
<div class="info-item">
<span class="label">知识库</span>
<span class="value">{{ getKnowledgeBaseName(modelData.knowledgeBaseId) }}</span>
</div>
<div class="info-item">
<span class="label">文档类型</span>
<span class="value">{{ getDocumentTypeName(modelData.documentType) }}</span>
</div>
<div class="info-item">
<span class="label">段落数量</span>
<span class="value">{{ modelData.segments.length }}</span>
</div>
</div>
</el-card>
</div>
<div class="mb-20px">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span class="text-16px font-bold">处理选项</span>
</div>
</template>
<div class="process-options">
<el-form :model="processOptions" label-width="120px">
<el-form-item label="处理模式">
<el-radio-group v-model="processOptions.mode">
<el-radio :label="1">标准处理</el-radio>
<el-radio :label="2">高级处理</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="向量模型" v-if="processOptions.mode === 2">
<el-select v-model="processOptions.vectorModel" placeholder="请选择向量模型">
<el-option label="文本嵌入模型-基础版" value="text-embedding-basic" />
<el-option label="文本嵌入模型-高级版" value="text-embedding-advanced" />
<el-option label="多模态嵌入模型" value="multimodal-embedding" />
</el-select>
</el-form-item>
<el-form-item label="处理优先级" v-if="processOptions.mode === 2">
<el-select v-model="processOptions.priority" placeholder="请选择处理优先级">
<el-option label="低" value="low" />
<el-option label="中" value="medium" />
<el-option label="高" value="high" />
</el-select>
</el-form-item>
</el-form>
</div>
</el-card>
</div>
<div class="mb-20px">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span class="text-16px font-bold">处理状态</span>
</div>
</template>
<div class="process-status">
<div v-if="!isProcessing && !isProcessed">
<el-empty description="尚未开始处理" />
<div class="flex justify-center mt-20px">
<el-button type="primary" @click="handleStartProcess">开始处理</el-button>
</div>
</div>
<div v-else-if="isProcessing">
<div class="flex flex-col items-center">
<el-progress type="circle" :percentage="processPercentage" />
<div class="mt-10px">{{ processStatus }}</div>
</div>
</div>
<div v-else>
<div class="flex items-center justify-center">
<el-result icon="success" title="处理完成" sub-title="文档已成功处理并添加到知识库中">
<template #extra>
<el-button type="primary" @click="handleViewDocument">查看文档</el-button>
</template>
</el-result>
</div>
</div>
</div>
</el-card>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script setup lang="ts">
import { PropType } from 'vue' import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Object as PropType<any>, type: Object,
required: true required: true
} }
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const parent = inject('parent') as any
const pollingTimer = ref<number | null>(null) // ID
// /** 判断文件处理是否完成 */
const modelData = computed({ const isProcessComplete = (file) => {
get: () => props.modelValue, return file.progress === 100
set: (val) => emit('update:modelValue', val)
})
//
const processOptions = ref({
mode: 1, // 1: , 2:
vectorModel: 'text-embedding-basic',
priority: 'medium'
})
//
const isProcessing = ref(false)
const isProcessed = ref(false)
const processPercentage = ref(0)
const processStatus = ref('正在准备处理...')
//
const knowledgeBaseList = [
{ id: 1, name: '产品知识库' },
{ id: 2, name: '技术文档库' },
{ id: 3, name: '客户服务知识库' }
]
//
const getKnowledgeBaseName = (id) => {
const base = knowledgeBaseList.find((item) => item.id === id)
return base ? base.name : '未知知识库'
} }
// /** 判断所有文件是否都处理完成 */
const getDocumentTypeName = (type) => { const allProcessComplete = computed(() => {
const typeMap = { return props.modelValue.list.every((file) => isProcessComplete(file))
pdf: 'PDF文档', })
word: 'Word文档',
text: '文本文件', /** 完成按钮点击事件处理 */
url: '网页链接' const handleComplete = () => {
if (parent?.exposed?.handleBack) {
parent.exposed.handleBack()
} }
return typeMap[type] || '未知类型'
} }
// /** 获取文件处理进度 */
const handleStartProcess = () => { const getProcessList = async () => {
isProcessing.value = true try {
processPercentage.value = 0 // 1. API
processStatus.value = '正在准备处理...' const documentIds = props.modelValue.list.filter((item) => item.id).map((item) => item.id)
if (documentIds.length === 0) {
// return
const timer = setInterval(() => {
processPercentage.value += 10
if (processPercentage.value < 30) {
processStatus.value = '正在提取文本内容...'
} else if (processPercentage.value < 60) {
processStatus.value = '正在进行向量化处理...'
} else if (processPercentage.value < 90) {
processStatus.value = '正在写入知识库...'
} else {
processStatus.value = '处理完成,正在整理结果...'
} }
const result = await KnowledgeSegmentApi.getKnowledgeSegmentProcessList(documentIds)
if (processPercentage.value >= 100) { // 2.1
clearInterval(timer) const updatedList = props.modelValue.list.map((file) => {
isProcessing.value = false const processInfo = result.find((item) => item.documentId === file.id)
isProcessed.value = true if (processInfo) {
modelData.value.status = 2 // // / * 100
const progress =
processInfo.embeddingCount && processInfo.count
? Math.floor((processInfo.embeddingCount / processInfo.count) * 100)
: 0
return {
...file,
progress: progress,
count: processInfo.count || 0
}
}
return file
})
// 2.2
emit('update:modelValue', {
...props.modelValue,
list: updatedList
})
// 3.
if (!updatedList.every((file) => isProcessComplete(file))) {
pollingTimer.value = window.setTimeout(getProcessList, 3000)
} }
}, 500) } catch (error) {
//
console.error('获取处理进度失败:', error)
pollingTimer.value = window.setTimeout(getProcessList, 5000)
}
} }
// /** 组件挂载时开始轮询 */
const handleViewDocument = () => { onMounted(() => {
// // 1. 0
console.log('查看文档:', modelData.value.id) const initialList = props.modelValue.list.map((file) => ({
} ...file,
progress: 0
}))
// emit('update:modelValue', {
const validate = () => { ...props.modelValue,
return new Promise((resolve, reject) => { list: initialList
if (modelData.value.status === 2) {
resolve(true)
} else {
reject(new Error('请先完成文档处理'))
}
}) })
}
// // 2.
defineExpose({ getProcessList()
validate })
/** 组件卸载前清除轮询 */
onBeforeUnmount(() => {
// 1.
if (pollingTimer.value) {
clearTimeout(pollingTimer.value)
pollingTimer.value = null
}
}) })
</script> </script>
<style lang="scss" scoped>
.process-complete {
.document-info {
.info-item {
margin-bottom: 10px;
display: flex;
.label {
width: 100px;
color: #606266;
}
.value {
font-weight: bold;
}
}
}
}
</style>

View File

@ -188,13 +188,13 @@ const handleSave = async () => {
try { try {
if (modelData.value.id) { if (modelData.value.id) {
// //
modelData.value.ids = await KnowledgeDocumentApi.updateKnowledgeDocument({ await KnowledgeDocumentApi.updateKnowledgeDocument({
id: modelData.value.id, id: modelData.value.id,
segmentMaxTokens: modelData.value.segmentMaxTokens segmentMaxTokens: modelData.value.segmentMaxTokens
}) })
} else { } else {
// //
modelData.value.ids = await KnowledgeDocumentApi.createKnowledgeDocumentList({ const data = await KnowledgeDocumentApi.createKnowledgeDocumentList({
knowledgeId: modelData.value.knowledgeId, knowledgeId: modelData.value.knowledgeId,
segmentMaxTokens: modelData.value.segmentMaxTokens, segmentMaxTokens: modelData.value.segmentMaxTokens,
list: modelData.value.list.map((item: any) => ({ list: modelData.value.list.map((item: any) => ({
@ -202,6 +202,9 @@ const handleSave = async () => {
url: item.url url: item.url
})) }))
}) })
modelData.value.list.forEach((document: any, index: number) => {
document.id = data[index]
})
} }
// //

View File

@ -89,16 +89,17 @@ const formData = ref({
id: undefined, // (documentId) id: undefined, // (documentId)
segmentMaxTokens: 500, // token segmentMaxTokens: 500, // token
list: [] as Array<{ list: [] as Array<{
name: string id: number //
url: string name: string //
url: string // URL
segments: Array<{ segments: Array<{
content?: string content?: string
contentLength?: number contentLength?: number
tokens?: number tokens?: number
}> }>
}>, // count?: number //
documentIds: [], // / ProcessStep process?: number //
status: 0 // 0: 稿, 1: , 2: }> //
}) // }) //
provide('parent', getCurrentInstance()) // parent 使 provide('parent', getCurrentInstance()) // parent 使
@ -119,6 +120,7 @@ const initData = async () => {
formData.value.segmentMaxTokens = document.segmentMaxTokens formData.value.segmentMaxTokens = document.segmentMaxTokens
formData.value.list = [ formData.value.list = [
{ {
id: document.id,
name: document.name, name: document.name,
url: document.url, url: document.url,
segments: [] segments: []
@ -139,6 +141,17 @@ const initData = async () => {
] ]
goToNextStep() goToNextStep()
} }
if (false) {
formData.value.list = [
{
id: 1,
name: '项目说明文档.pdf',
url: 'https://static.iocoder.cn/README_yudao.md',
segments: []
}
]
goToNextStep()
}
} }
/** 切换到下一步 */ /** 切换到下一步 */
@ -179,7 +192,8 @@ onBeforeUnmount(() => {
/** 暴露方法给子组件使用 */ /** 暴露方法给子组件使用 */
defineExpose({ defineExpose({
goToNextStep, goToNextStep,
goToPrevStep goToPrevStep,
handleBack
}) })
</script> </script>