瀏覽代碼

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

YunaiV 5 月之前
父節點
當前提交
1958c2bb9d

File diff suppressed because it is too large
+ 5733 - 347
pnpm-lock.yaml


+ 49 - 0
src/api/ai/knowledge/segment/index.ts

@@ -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(',') } 
+    })
+  }
+}

+ 111 - 200
src/views/ai/knowledge/document/form/ProcessStep.vue

@@ -1,235 +1,146 @@
 <template>
-  <div class="process-complete">
-    <div class="mb-20px">
-      <el-alert
-        title="处理说明"
-        type="info"
-        description="系统将对文档进行处理,包括文本提取、向量化等操作,处理完成后文档将被添加到知识库中。"
-        show-icon
-        :closable="false"
-      />
-    </div>
+  <div>
+    <!-- 文件处理列表 -->
+    <div class="mt-15px grid grid-cols-1 gap-2">
+      <div
+        v-for="(file, index) in modelValue.list"
+        :key="index"
+        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"
+      >
+        <!-- 文件图标和名称 -->
+        <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="mb-20px">
-      <el-card class="box-card">
-        <template #header>
-          <div class="card-header">
-            <span class="text-16px font-bold">文档信息</span>
-          </div>
-        </template>
-        <div class="document-info">
-          <div class="info-item">
-            <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 class="flex-1">
+          <el-progress
+            :percentage="file.progress || 0"
+            :stroke-width="10"
+            :status="isProcessComplete(file) ? 'success' : ''"
+          />
         </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 class="ml-10px text-[13px] text-[#606266]">
+          分段数量:{{ file.count ? file.count : '-' }}
         </div>
-      </el-card>
+      </div>
     </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 class="flex justify-end mt-20px">
+      <el-button
+        :type="allProcessComplete ? 'success' : 'primary'"
+        :disabled="!allProcessComplete"
+        @click="handleComplete"
+      >
+        完成
+      </el-button>
     </div>
   </div>
 </template>
 
-<script lang="ts" setup>
-import { PropType } from 'vue'
+<script setup lang="ts">
+import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
 
 const props = defineProps({
   modelValue: {
-    type: Object as PropType<any>,
+    type: Object,
     required: true
   }
 })
 
 const emit = defineEmits(['update:modelValue'])
+const parent = inject('parent') as any
+const pollingTimer = ref<number | null>(null) // 轮询定时器 ID,用于跟踪和清除轮询进程
 
-// 表单数据
-const modelData = computed({
-  get: () => props.modelValue,
-  set: (val) => emit('update:modelValue', val)
-})
+/** 判断文件处理是否完成 */
+const isProcessComplete = (file) => {
+  return file.progress === 100
+}
 
-// 处理选项
-const processOptions = ref({
-  mode: 1, // 1: 标准处理, 2: 高级处理
-  vectorModel: 'text-embedding-basic',
-  priority: 'medium'
+/** 判断所有文件是否都处理完成 */
+const allProcessComplete = computed(() => {
+  return props.modelValue.list.every((file) => isProcessComplete(file))
 })
 
-// 处理状态
-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 typeMap = {
-    pdf: 'PDF文档',
-    word: 'Word文档',
-    text: '文本文件',
-    url: '网页链接'
+/** 完成按钮点击事件处理 */
+const handleComplete = () => {
+  if (parent?.exposed?.handleBack) {
+    parent.exposed.handleBack()
   }
-  return typeMap[type] || '未知类型'
 }
 
-// 开始处理
-const handleStartProcess = () => {
-  isProcessing.value = true
-  processPercentage.value = 0
-  processStatus.value = '正在准备处理...'
-
-  // 模拟处理过程
-  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 getProcessList = async () => {
+  try {
+    // 1. 调用 API 获取处理进度
+    const documentIds = props.modelValue.list.filter((item) => item.id).map((item) => item.id)
+    if (documentIds.length === 0) {
+      return
     }
-
-    if (processPercentage.value >= 100) {
-      clearInterval(timer)
-      isProcessing.value = false
-      isProcessed.value = true
-      modelData.value.status = 2 // 已完成
+    const result = await KnowledgeSegmentApi.getKnowledgeSegmentProcessList(documentIds)
+
+    // 2.1更新进度
+    const updatedList = props.modelValue.list.map((file) => {
+      const processInfo = result.find((item) => item.documentId === file.id)
+      if (processInfo) {
+        // 计算进度百分比:已嵌入数量 / 总数量 * 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)
-}
-
-// 查看文档
-const handleViewDocument = () => {
-  // 跳转到文档详情页
-  console.log('查看文档:', modelData.value.id)
+  } catch (error) {
+    // 出错后也继续轮询
+    console.error('获取处理进度失败:', error)
+    pollingTimer.value = window.setTimeout(getProcessList, 5000)
+  }
 }
 
-// 表单校验
-const validate = () => {
-  return new Promise((resolve, reject) => {
-    if (modelData.value.status === 2) {
-      resolve(true)
-    } else {
-      reject(new Error('请先完成文档处理'))
-    }
+/** 组件挂载时开始轮询 */
+onMounted(() => {
+  // 1. 初始化进度为 0
+  const initialList = props.modelValue.list.map((file) => ({
+    ...file,
+    progress: 0
+  }))
+
+  emit('update:modelValue', {
+    ...props.modelValue,
+    list: initialList
   })
-}
 
-// 对外暴露方法
-defineExpose({
-  validate
+  // 2. 开始轮询获取进度
+  getProcessList()
 })
-</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;
-      }
-    }
+/** 组件卸载前清除轮询 */
+onBeforeUnmount(() => {
+  // 1. 清除定时器
+  if (pollingTimer.value) {
+    clearTimeout(pollingTimer.value)
+    pollingTimer.value = null
   }
-}
-</style>
+})
+</script>

+ 5 - 2
src/views/ai/knowledge/document/form/SplitStep.vue

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

+ 20 - 6
src/views/ai/knowledge/document/form/index.vue

@@ -89,16 +89,17 @@ const formData = ref({
   id: undefined, // 编辑的文档编号(documentId)
   segmentMaxTokens: 500, // 分段最大 token 数
   list: [] as Array<{
-    name: string
-    url: string
+    id: number // 文档编号
+    name: string // 文档名称
+    url: string // 文档 URL
     segments: Array<{
       content?: string
       contentLength?: number
       tokens?: number
     }>
-  }>, // 用于存储上传的文件列表
-  documentIds: [], // 最终提交的创建/修改的文档编号,用于 ProcessStep 组件的轮询
-  status: 0 // 0: 草稿, 1: 处理中, 2: 已完成
+    count?: number // 段落数量
+    process?: number // 处理进度
+  }> // 用于存储上传的文件列表
 }) // 表单数据
 
 provide('parent', getCurrentInstance()) // 提供 parent 给子组件使用
@@ -119,6 +120,7 @@ const initData = async () => {
     formData.value.segmentMaxTokens = document.segmentMaxTokens
     formData.value.list = [
       {
+        id: document.id,
         name: document.name,
         url: document.url,
         segments: []
@@ -139,6 +141,17 @@ const initData = async () => {
     ]
     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({
   goToNextStep,
-  goToPrevStep
+  goToPrevStep,
+  handleBack
 })
 </script>
 

Some files were not shown because too many files changed in this diff