mirror of
https://gitee.com/myxzgzs/boyue-ui-admin-vue3
synced 2025-08-08 16:32:43 +08:00
【功能新增】AI:知识库文档上传:90%,ProcessStep 已完成
This commit is contained in:
parent
809fb9fdfc
commit
1958c2bb9d
12928
pnpm-lock.yaml
generated
12928
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
49
src/api/ai/knowledge/segment/index.ts
Normal file
49
src/api/ai/knowledge/segment/index.ts
Normal 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(',') }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
|
||||||
|
@ -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]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 进入下一步
|
// 进入下一步
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user