10 天以前 0f74d0721b7407709b6bdd45dc6b58970820536b
docs(knowledge-base): 更新知识库RAG向量检索功能前端联调文档

- 新增知识库文件向量记录表结构定义
- 添加知识库表文件数量和切片数量字段
- 完善文件上传、向量化状态查询、删除和重新处理接口说明
- 提供前端文件上传、列表展示和问答组件示例代码
- 优化文件上传流程,统一调用向量化触发接口
- 调整文件列表刷新逻辑,直接使用向量状态接口获取数据
- 修复文件删除时使用错误的ID字段问题
- 添加文件类型支持和向量化状态说明
- 补充业务流程和技术实现要点说明
已添加1个文件
已修改2个文件
690 ■■■■■ 文件已修改
doc/20260608_知识库RAG向量检索功能前端联调文档.md 598 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/knowledgeBase.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/knowledgeBase/index.vue 72 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260608_֪ʶ¿âRAGÏòÁ¿¼ìË÷¹¦ÄÜǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,598 @@
# çŸ¥è¯†åº“文件上传与RAG向量检索功能前端联调文档
## ä¸€ã€åŠŸèƒ½æ¦‚è¿°
知识库模块新增文件上传功能,上传的文件会实时进入向量库进行切片处理,配合AI模块实现RAG(检索增强生成)问答功能。
### åŠŸèƒ½æ¨¡å—
1. **知识库文件管理**:使用系统已有的附件管理机制上传文件
2. **向量检索处理**:文件内容自动切片并存入向量库(Pinecone)
3. **知识库问答**:基于上传文件内容进行智能问答
## äºŒã€æ•°æ®åº“变更
### 2.1 æ–°å¢žçŸ¥è¯†åº“文件向量记录表
```sql
-- çŸ¥è¯†åº“文件向量记录表(用于跟踪向量化状态)
CREATE TABLE IF NOT EXISTS knowledge_base_vector (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    knowledge_base_id BIGINT NOT NULL COMMENT '关联知识库ID',
    storage_blob_id BIGINT NOT NULL COMMENT '关联文件blob ID',
    file_name VARCHAR(255) NOT NULL COMMENT '文件名称',
    file_type VARCHAR(50) NOT NULL COMMENT '文件类型(docx/pdf/xlsx/txt等)',
    vector_status TINYINT DEFAULT 0 COMMENT '向量化状态: 0-待处理, 1-处理中, 2-已完成, 3-失败',
    vector_error VARCHAR(500) COMMENT '向量化失败原因',
    chunk_count INT DEFAULT 0 COMMENT '切片数量',
    namespace VARCHAR(100) COMMENT '向量命名空间',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    create_user INT COMMENT '创建人',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    update_user INT COMMENT '更新人',
    tenant_id BIGINT COMMENT '租户ID',
    dept_id BIGINT COMMENT '部门ID',
    INDEX idx_knowledge_base_id (knowledge_base_id),
    INDEX idx_storage_blob_id (storage_blob_id),
    INDEX idx_vector_status (vector_status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库文件向量记录表';
```
### 2.2 ä¿®æ”¹çŸ¥è¯†åº“表
```sql
-- çŸ¥è¯†åº“表增加字段
ALTER TABLE knowledge_base
ADD COLUMN file_count INT DEFAULT 0 COMMENT '文件数量',
ADD COLUMN total_chunk_count INT DEFAULT 0 COMMENT '总切片数量',
ADD COLUMN description VARCHAR(500) COMMENT '知识库描述';
```
## ä¸‰ã€æŽ¥å£è®¾è®¡
### 3.1 æ–‡ä»¶ä¸Šä¼ ï¼ˆä½¿ç”¨ç³»ç»Ÿå·²æœ‰æŽ¥å£ï¼‰
**接口地址**:`POST /common/upload`
**请求方式**:multipart/form-data
**请求参数**:
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| files | MultipartFile[] | æ˜¯ | ä¸Šä¼ çš„æ–‡ä»¶åˆ—表 |
**响应结果**:
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": [
    {
      "id": 123,
      "name": "操作手册.docx",
      "url": "/profile/upload/20260608/xxx.docx",
      "previewURL": "/common/preview/xxx?token=yyy",
      "downloadURL": "/common/download/xxx?token=yyy",
      "storageAttachmentId": null
    }
  ]
}
```
### 3.2 ä¿å­˜çŸ¥è¯†åº“文件关联(触发向量化)
**重要**:上传完成后,**必须**调用此接口来关联文件并触发向量化处理。
**接口地址**:`POST /knowledgeBase/file/save`
**请求参数**:
```json
{
  "knowledgeBaseId": 10,
  "storageBlobIds": [123, 124]
}
```
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| knowledgeBaseId | Long | æ˜¯ | çŸ¥è¯†åº“ID |
| storageBlobIds | List<Long> | æ˜¯ | ä¸Šä¼ è¿”回的文件blob ID列表 |
**响应结果**:
```json
{
  "code": 200,
  "msg": "操作成功"
}
```
**调用时机**:在 `/common/upload` ä¸Šä¼ æˆåŠŸåŽï¼Œç«‹å³è°ƒç”¨æ­¤æŽ¥å£ã€‚
**注意**:此接口会:
1. ä¿å­˜é™„件关联到 `storage_attachment` è¡¨
2. åˆ›å»ºå‘量记录到 `knowledge_base_vector` è¡¨
3. **异步触发向量化处理**(文件切片 â†’ å‘量嵌入 â†’ å­˜å…¥Pinecone)
---
### 3.3 æŸ¥è¯¢å‘量化状态(推荐使用)
**接口地址**:`GET /knowledgeBase/vector/status/{knowledgeBaseId}`
**响应结果**:
```json
{
  "code": 200,
  "data": [
    {
      "id": 1,
      "storageBlobId": 123,
      "fileName": "操作手册.docx",
      "fileType": "docx",
      "vectorStatus": 2,
      "chunkCount": 15,
      "namespace": "kb-10",
      "vectorError": null
    }
  ]
}
```
### 3.4 åˆ é™¤çŸ¥è¯†åº“文件
**接口地址**:`DELETE /knowledgeBase/file/delete`
**请求参数**:
```json
{
  "ids": [1, 2, 3]
}
```
**注意**:此接口会同时删除向量库中的相关数据。
**响应结果**:
```json
{
  "code": 200,
  "msg": "操作成功"
}
```
### 3.5 é‡æ–°å‘量化文件
**接口地址**:`POST /knowledgeBase/vector/reprocess/{vectorId}`
**响应结果**:
```json
{
  "code": 200,
  "msg": "已重新提交向量化任务"
}
```
### 3.6 çŸ¥è¯†åº“问答接口
**接口地址**:`POST /ai/knowledge/chat`(流式返回)
**请求参数**:
```json
{
  "knowledgeBaseId": 10,
  "memoryId": "session-xxx",
  "question": "如何操作审批流程?"
}
```
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| knowledgeBaseId | Long | æ˜¯ | çŸ¥è¯†åº“ID |
| memoryId | String | æ˜¯ | ä¼šè¯ID,用于保持上下文 |
| question | String | æ˜¯ | ç”¨æˆ·æé—®å†…容 |
**响应结果**(流式返回 `text/stream;charset=utf-8`):
```
根据知识库内容,审批流程的操作步骤如下:
1. ç™»å½•系统后进入审批管理模块...
```
### 3.7 çŸ¥è¯†åº“问答会话历史
**接口地址**:`GET /ai/knowledge/history/{memoryId}`
**响应结果**:
```json
{
  "code": 200,
  "data": [
    {
      "role": "user",
      "content": "如何操作审批流程?",
      "createTime": "2026-06-08 10:00:00"
    },
    {
      "role": "assistant",
      "content": "根据知识库内容...",
      "createTime": "2026-06-08 10:01:00"
    }
  ]
}
```
## å››ã€æ–‡ä»¶ç±»åž‹æ”¯æŒ
| æ–‡ä»¶ç±»åž‹ | æ‰©å±•名 | è¯´æ˜Ž |
|----------|--------|------|
| Word文档 | .docx | æ”¯æŒæ–‡æœ¬æå– |
| Excel表格 | .xlsx, .xls | æ”¯æŒè¡¨æ ¼å†…容提取 |
| PDF文档 | .pdf | æ”¯æŒPDF文本提取 |
| æ–‡æœ¬æ–‡ä»¶ | .txt, .md, .json, .csv | ç›´æŽ¥è¯»å–内容 |
| ä»£ç æ–‡ä»¶ | .java, .js, .vue, .html, .sql等 | ç›´æŽ¥è¯»å–内容 |
**文件大小限制**:单文件最大10MB(系统默认限制)
## äº”、向量化状态说明
| çŠ¶æ€å€¼ | çŠ¶æ€åç§° | è¯´æ˜Ž |
|--------|----------|------|
| 0 | å¾…处理 | æ–‡ä»¶å·²ä¸Šä¼ ï¼Œç­‰å¾…向量化处理 |
| 1 | å¤„理中 | æ­£åœ¨è¿›è¡Œå‘量切片处理 |
| 2 | å·²å®Œæˆ | å‘量化完成,可进行检索问答 |
| 3 | å¤±è´¥ | å‘量化失败,需重新处理 |
## å…­ã€å‰ç«¯ç»„件设计
### 6.1 æ–‡ä»¶ä¸Šä¼ ç»„件(使用系统已有上传)
```vue
<template>
  <div class="knowledge-file-upload">
    <!-- çŸ¥è¯†åº“选择 -->
    <el-select v-model="selectedKnowledgeBase" placeholder="选择知识库" style="width: 200px">
      <el-option
        v-for="kb in knowledgeBaseList"
        :key="kb.id"
        :label="kb.title"
        :value="kb.id"
      />
    </el-select>
    <!-- ä½¿ç”¨ç³»ç»Ÿå·²æœ‰ä¸Šä¼ ç»„ä»¶ -->
    <el-upload
      :action="uploadUrl"
      :headers="uploadHeaders"
      :on-success="handleUploadSuccess"
      :before-upload="beforeUpload"
      :accept="acceptTypes"
      :limit="10"
      :file-list="fileList"
      multiple
    >
      <el-button type="primary">点击上传</el-button>
      <template #tip>
        <div class="el-upload__tip">
          æ”¯æŒ docx、xlsx、pdf、txt ç­‰æ ¼å¼ï¼Œå•文件不超过10MB
        </div>
      </template>
    </el-upload>
  </div>
</template>
<script setup>
import { ref } from 'vue'
import { getToken } from '@/utils/auth'
import request from '@/utils/request'
const uploadUrl = '/common/upload'
const uploadHeaders = { Authorization: 'Bearer ' + getToken() }
const acceptTypes = '.docx,.xlsx,.xls,.pdf,.txt,.md,.json,.csv'
const uploadedBlobs = ref([])
const beforeUpload = (file) => {
  const maxSize = 10 * 1024 * 1024
  if (file.size > maxSize) {
    ElMessage.error('文件大小不能超过10MB')
    return false
  }
  return true
}
// ä¸Šä¼ æˆåŠŸåŽä¿å­˜æ–‡ä»¶å…³è”å¹¶è§¦å‘å‘é‡åŒ–
const handleUploadSuccess = async (response, file) => {
  if (response.code === 200) {
    uploadedBlobs.value.push(...response.data)
    // è°ƒç”¨çŸ¥è¯†åº“文件保存接口,关联文件并触发向量化
    await saveKnowledgeBaseFiles()
    ElMessage.success('文件上传成功,正在处理向量化...')
    refreshVectorStatus()
  } else {
    ElMessage.error(response.msg)
  }
}
// ä¿å­˜æ–‡ä»¶å…³è”到知识库并触发向量化
const saveKnowledgeBaseFiles = async () => {
  await request.post('/knowledgeBase/file/save', {
    knowledgeBaseId: selectedKnowledgeBase.value,
    storageBlobIds: uploadedBlobs.value.map(b => b.id)
  })
  uploadedBlobs.value = []
}
</script>
```
### 6.2 æ–‡ä»¶åˆ—表组件
```vue
<template>
  <el-table :data="fileList" v-loading="loading">
    <el-table-column prop="name" label="文件名" width="200" />
    <el-table-column prop="fileType" label="类型" width="80">
      <template #default="{ row }">
        {{ getFileType(row.name) }}
      </template>
    </el-table-column>
    <el-table-column prop="vectorStatus" label="向量化状态" width="120">
      <template #default="{ row }">
        <el-tag :type="getVectorStatusType(row.vectorStatus)">
          {{ getVectorStatusText(row.vectorStatus) }}
        </el-tag>
      </template>
    </el-table-column>
    <el-table-column prop="chunkCount" label="切片数" width="80" />
    <el-table-column prop="createTime" label="上传时间" width="160" />
    <el-table-column label="操作" width="180">
      <template #default="{ row }">
        <el-button type="primary" size="small" link @click="previewFile(row)">预览</el-button>
        <el-button type="primary" size="small" link @click="downloadFile(row)">下载</el-button>
        <el-button
          v-if="row.vectorStatus === 3"
          type="warning"
          size="small"
          link
          @click="revectorFile(row)"
        >重新处理</el-button>
        <el-button type="danger" size="small" link @click="deleteFile(row)">删除</el-button>
      </template>
    </el-table-column>
  </el-table>
</template>
<script setup>
import request from '@/utils/request'
const getFileType = (name) => {
  return name?.split('.').pop() || ''
}
const getVectorStatusText = (status) => {
  const statusMap = { 0: '待处理', 1: '处理中', 2: '已完成', 3: '失败' }
  return statusMap[status] || '未知'
}
const getVectorStatusType = (status) => {
  const typeMap = { 0: 'info', 1: 'warning', 2: 'success', 3: 'danger' }
  return typeMap[status] || 'info'
}
const previewFile = (row) => {
  window.open(row.previewURL, '_blank')
}
const downloadFile = (row) => {
  window.open(row.downloadURL, '_blank')
}
const deleteFile = async (row) => {
  await request.delete('/knowledgeBase/file/delete', { data: [row.id] })
  ElMessage.success('删除成功')
  refreshFileList()
}
const revectorFile = async (row) => {
  await request.post(`/knowledgeBase/vector/reprocess/${row.vectorId}`)
  ElMessage.success('已重新提交向量化任务')
  refreshVectorStatus()
}
</script>
```
### 6.3 çŸ¥è¯†åº“问答组件
```vue
<template>
  <div class="knowledge-chat">
    <!-- çŸ¥è¯†åº“选择 -->
    <div class="kb-selector">
      <el-select v-model="selectedKnowledgeBase" placeholder="选择知识库">
        <el-option v-for="kb in knowledgeBaseList" :key="kb.id" :label="kb.title" :value="kb.id" />
      </el-select>
    </div>
    <!-- èŠå¤©åŒºåŸŸ -->
    <div class="chat-container">
      <div class="message-list" ref="messageList">
        <div v-for="msg in messages" :key="msg.id" :class="['message', msg.role]">
          <div class="message-content">{{ msg.content }}</div>
        </div>
        <div v-if="streamingContent" class="message assistant">
          <div class="message-content">{{ streamingContent }}</div>
        </div>
      </div>
      <!-- è¾“入区域 -->
      <div class="input-area">
        <el-input
          v-model="question"
          placeholder="输入问题,基于知识库内容回答..."
          @keyup.enter="sendQuestion"
        />
        <el-button type="primary" @click="sendQuestion" :loading="sending">发送</el-button>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
import { getToken } from '@/utils/auth'
const selectedKnowledgeBase = ref(null)
const question = ref('')
const messages = ref([])
const sending = ref(false)
const streamingContent = ref('')
const memoryId = ref('kb-chat-' + Date.now())
const sendQuestion = async () => {
  if (!question.value.trim() || !selectedKnowledgeBase.value) return
  sending.value = true
  streamingContent.value = ''
  messages.value.push({ id: Date.now(), role: 'user', content: question.value })
  try {
    const response = await fetch('/ai/knowledge/chat', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: 'Bearer ' + getToken()
      },
      body: JSON.stringify({
        knowledgeBaseId: selectedKnowledgeBase.value,
        memoryId: memoryId.value,
        question: question.value
      })
    })
    // æµå¼è¯»å–响应
    const reader = response.body.getReader()
    const decoder = new TextDecoder()
    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      streamingContent.value += decoder.decode(value, { stream: true })
      // æ»šåŠ¨åˆ°åº•éƒ¨
      nextTick(() => {
        const list = document.querySelector('.message-list')
        if (list) list.scrollTop = list.scrollHeight
      })
    }
    messages.value.push({ id: Date.now(), role: 'assistant', content: streamingContent.value })
    streamingContent.value = ''
  } catch (error) {
    ElMessage.error('问答请求失败')
  } finally {
    sending.value = false
    question.value = ''
  }
}
</script>
<style scoped>
.knowledge-chat {
  height: 100%;
  display: flex;
  flex-direction: column;
}
.kb-selector {
  padding: 10px;
  border-bottom: 1px solid #eee;
}
.chat-container {
  flex: 1;
  display: flex;
  flex-direction: column;
}
.message-list {
  flex: 1;
  overflow-y: auto;
  padding: 10px;
}
.message {
  margin-bottom: 10px;
  padding: 10px;
  border-radius: 8px;
}
.message.user {
  background: #e6f7ff;
  text-align: right;
}
.message.assistant {
  background: #f5f5f5;
}
.input-area {
  padding: 10px;
  display: flex;
  gap: 10px;
}
</style>
```
## ä¸ƒã€ä¸šåŠ¡æµç¨‹
### 7.1 æ–‡ä»¶ä¸Šä¼ æµç¨‹ï¼ˆé‡è¦ï¼‰
```
前端调用 /common/upload ä¸Šä¼ æ–‡ä»¶
    â†“
获取 StorageBlobVO åˆ—表(包含blobId、预览URL、下载URL)
    â†“
调用 /knowledgeBase/file/save å…³è”文件到知识库
    â†“
后端创建向量记录 â†’ å¼‚步触发向量化处理
    â†“
异步任务:提取文件文本 â†’ åˆ‡ç‰‡ â†’ è°ƒç”¨Embedding模型生成向量 â†’ å­˜å…¥Pinecone
    â†“
更新 knowledge_base_vector è¡¨çŠ¶æ€ä¸ºå·²å®Œæˆ
```
**关键点**:
- å¿…须调用 `/knowledgeBase/file/save` æŽ¥å£æ‰èƒ½è§¦å‘向量化
- å‘量化是异步处理的,不会阻塞请求
- å‰ç«¯å¯é€šè¿‡è½®è¯¢ `/knowledgeBase/vector/status/{knowledgeBaseId}` æŸ¥çœ‹å¤„理进度
### 7.2 çŸ¥è¯†åº“问答流程
```
用户提问 â†’ è°ƒç”¨ Embedding æ¨¡åž‹å¯¹é—®é¢˜å‘量化
    â†“
在 Pinecone ä¸­æ£€ç´¢ï¼ˆå‘½åç©ºé—´: kb-{knowledgeBaseId})
    â†“
获取相关切片文本 â†’ ä½œä¸ºä¸Šä¸‹æ–‡ + ç”¨æˆ·é—®é¢˜å‘ç»™ AI æ¨¡åž‹
    â†“
AI æµå¼ç”Ÿæˆå›žç­” â†’ è¿”回前端
```
## å…«ã€æŠ€æœ¯å®žçŽ°è¦ç‚¹
### 8.1 æ–‡æœ¬åˆ‡ç‰‡ç­–ç•¥
- **切片大小**:默认每片 500 å­—符
- **重叠大小**:默认 100 å­—符重叠,保证语义连贯
- **切片元数据**:包含文件ID、知识库ID、切片索引
### 8.2 å‘量命名空间
每个知识库使用独立命名空间:`kb-{knowledgeBaseId}`
## ä¹ã€æ³¨æ„äº‹é¡¹
1. **文件上传必须调用 `/knowledgeBase/file/save` è§¦å‘向量化**
2. åˆ é™¤æ–‡ä»¶æ—¶åŒæ­¥åˆ é™¤å‘量库中的相关切片
3. å¤§æ–‡ä»¶å‘量化可能耗时较长,前端需轮询状态或显示进度
4. çŸ¥è¯†åº“问答依赖向量检索质量,建议优化切片策略
5. ä¸åŒçŸ¥è¯†åº“使用不同命名空间,避免数据混淆
## åã€é”™è¯¯ç è¯´æ˜Ž
| é”™è¯¯ç  | è¯´æ˜Ž |
|--------|------|
| 40001 | æ–‡ä»¶ç±»åž‹ä¸æ”¯æŒ |
| 40002 | æ–‡ä»¶å¤§å°è¶…出限制 |
| 40003 | çŸ¥è¯†åº“不存在 |
| 50001 | æ–‡ä»¶ä¸Šä¼ å¤±è´¥ |
| 50002 | æ–‡ä»¶å†…容提取失败 |
| 50003 | å‘量化处理失败 |
| 50004 | å‘量检索失败 |
src/api/collaborativeApproval/knowledgeBase.js
@@ -54,7 +54,7 @@
  });
}
// æŸ¥è¯¢çŸ¥è¯†åº“文件向量化状态
// æŸ¥è¯¢çŸ¥è¯†åº“文件向量化状态(包含文件列表)
export function getVectorStatus(knowledgeBaseId) {
  return request({
    url: `/knowledgeBase/vector/status/${knowledgeBaseId}`,
@@ -62,6 +62,24 @@
  });
}
// ä¿å­˜çŸ¥è¯†åº“文件关联(触发向量化)
export function saveKnowledgeBaseFiles(data) {
  return request({
    url: "/knowledgeBase/file/save",
    method: "post",
    data,
  });
}
// åˆ é™¤çŸ¥è¯†åº“文件
export function deleteKnowledgeBaseFile(ids) {
  return request({
    url: "/knowledgeBase/file/delete",
    method: "delete",
    data: ids,
  });
}
// é‡æ–°å‘量化文件
export function reprocessVector(vectorId) {
  return request({
src/views/collaborativeApproval/knowledgeBase/index.vue
@@ -256,6 +256,7 @@
              :action="uploadUrl"
              :headers="uploadHeaders"
              :on-success="handleUploadSuccess"
              :on-change="handleFileChange"
              :before-upload="beforeUpload"
              :accept="acceptTypes"
              :file-list="uploadFileList"
@@ -398,9 +399,10 @@
  updateKnowledgeBase,
  getVectorStatus,
  reprocessVector,
  knowledgeChat
  knowledgeChat,
  saveKnowledgeBaseFiles,
  deleteKnowledgeBaseFile
} from "@/api/collaborativeApproval/knowledgeBase.js";
import { attachmentList, createAttachment, deleteAttachment } from "@/api/basicData/storageAttachment.js";
import useUserStore from '@/store/modules/user';
import { userListNoPageByTenantId } from '@/api/system/user.js';
import { getToken } from '@/utils/auth';
@@ -874,33 +876,17 @@
  refreshFileList();
};
// åˆ·æ–°æ–‡ä»¶åˆ—表
// åˆ·æ–°æ–‡ä»¶åˆ—表(直接使用向量状态接口)
const refreshFileList = async () => {
  if (!selectedKnowledgeBaseId.value) return;
  fileLoading.value = true;
  try {
    // èŽ·å–é™„ä»¶åˆ—è¡¨
    const attachmentRes = await attachmentList({
      recordType: 'knowledge_base',
      recordId: selectedKnowledgeBaseId.value,
      application: 'rag_file'
    });
    // èŽ·å–å‘é‡åŒ–çŠ¶æ€
    const vectorRes = await getVectorStatus(selectedKnowledgeBaseId.value);
    // åˆå¹¶æ•°æ®
    const vectorMap = {};
    (vectorRes.data || []).forEach(v => {
      vectorMap[v.storageBlobId] = v;
    });
    fileList.value = (attachmentRes.data || []).map(file => ({
      ...file,
      vectorStatus: vectorMap[file.storageBlobId]?.vectorStatus ?? 0,
      chunkCount: vectorMap[file.storageBlobId]?.chunkCount ?? 0,
      vectorId: vectorMap[file.storageBlobId]?.id
    const res = await getVectorStatus(selectedKnowledgeBaseId.value);
    fileList.value = (res.data || []).map(item => ({
      ...item,
      name: item.fileName,
      vectorId: item.id
    }));
  } catch (error) {
    console.error('获取文件列表失败:', error);
@@ -924,23 +910,35 @@
const handleUploadSuccess = async (response, file) => {
  if (response.code === 200) {
    uploadedBlobs.value.push(...response.data);
    await saveAttachment();
    ElMessage.success('文件上传成功,正在处理向量化...');
    uploadedBlobs.value = [];
    refreshFileList();
  } else {
    ElMessage.error(response.msg || '上传失败');
  }
};
// ä¿å­˜é™„件关联到知识库
const saveAttachment = async () => {
  await createAttachment({
    recordType: 'knowledge_base',
    recordId: selectedKnowledgeBaseId.value,
    application: 'rag_file',
    storageBlobDTOs: uploadedBlobs.value.map(b => b.id)
  });
// ä¸Šä¼ æ–‡ä»¶å˜åŒ–处理(用于检测所有文件上传完成)
const handleFileChange = (file, fileList) => {
  // å½“文件状态变化且没有正在上传的文件时,统一提交
  if (file.status === 'success' && fileList.every(f => f.status === 'success' || f.status === 'fail')) {
    if (uploadedBlobs.value.length > 0) {
      saveKnowledgeBaseFilesAndRefresh();
    }
  }
};
// ä¿å­˜çŸ¥è¯†åº“文件关联并触发向量化
const saveKnowledgeBaseFilesAndRefresh = async () => {
  try {
    await saveKnowledgeBaseFiles({
      knowledgeBaseId: selectedKnowledgeBaseId.value,
      storageBlobIds: uploadedBlobs.value.map(b => b.id)
    });
    ElMessage.success('文件上传成功,正在处理向量化...');
  } catch (error) {
    ElMessage.error('保存文件关联失败');
  } finally {
    uploadedBlobs.value = [];
    refreshFileList();
  }
};
// èŽ·å–æ–‡ä»¶ç±»åž‹
@@ -987,7 +985,7 @@
      type: 'warning'
    });
    await deleteAttachment([row.id]);
    await deleteKnowledgeBaseFile([row.id]);
    ElMessage.success('删除成功');
    refreshFileList();
  } catch (error) {