5 小时以前 f2d004a07d198d6d483f93228005506ae5c70ed2
feat(collaborativeApproval): 添加知识库RAG向量检索问答功能

- 新增知识库文件管理和向量化处理功能
- 实现文件上传、向量化状态监控和重新处理机制
- 集成知识库问答功能,支持流式AI对话
- 添加知识库文件数量和切片数量统计
- 支持多种文件格式(txt、md、docx、xlsx、xls、pdf)
- 实现向量数据库集成和智能检索功能
- 优化知识库管理界面和用户体验
- 完善相关API接口和错误处理机制
已添加1个文件
已修改3个文件
1366 ■■■■■ 文件已修改
doc/知识库模块前端实现文档.md 1212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/knowledgeBase.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/knowledgeBase/index.vue 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/֪ʶ¿âÄ£¿éǰ¶ËʵÏÖÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1212 @@
# çŸ¥è¯†åº“模块完整前端实现文档
## ä¸€ã€æ¨¡å—概述
知识库模块是一个集成了RAG(检索增强生成)技术的智能知识管理系统,支持:
- **知识库CRUD管理** - åˆ›å»ºã€ç¼–辑、删除、查询知识库
- **文件上传与向量化** - æ”¯æŒå¤šç§æ–‡ä»¶æ ¼å¼,自动进行向量切片处理
- **智能问答** - åŸºäºŽä¸Šä¼ æ–‡ä»¶å†…容进行AI问答
- **文件管理** - æŸ¥çœ‹æ–‡ä»¶å‘量化状态,支持重新处理和删除
### æŠ€æœ¯æž¶æž„
- **前端框架**: Vue 3 + Composition API
- **UI组件库**: Element Plus
- **向量数据库**: Pinecone
- **AI模型**: é˜¿é‡Œäº‘通义千问
- **文件处理**: æ”¯æŒdocx、xlsx、pdf、txt、md等格式
---
## äºŒã€æ–‡ä»¶ç»“æž„
```
src/
├── api/
│   â””── collaborativeApproval/
│       â””── knowledgeBase.js          # API接口封装
├── views/
│   â””── collaborativeApproval/
│       â””── knowledgeBase/
│           â””── index.vue              # ä¸»é¡µé¢ç»„ä»¶
└── components/
    â”œâ”€â”€ PIMTable/
    â”‚   â””── PIMTable.vue              # è¡¨æ ¼ç»„ä»¶
    â””── Dialog/
        â””── FormDialog.vue             # å¼¹çª—组件
```
---
## ä¸‰ã€API接口定义
### 3.1 æ–‡ä»¶ä½ç½®
`src/api/collaborativeApproval/knowledgeBase.js`
### 3.2 å®Œæ•´æŽ¥å£åˆ—表
```javascript
import request from "@/utils/request";
import { getToken } from '@/utils/auth';
// 1. æŸ¥è¯¢çŸ¥è¯†åº“列表(分页)
export function listKnowledgeBase(query) {
  return request({
    url: "/knowledgeBase/getList",
    method: "get",
    params: query,
  });
}
// 2. æ–°å¢žçŸ¥è¯†åº“
export function addKnowledgeBase(data) {
  return request({
    url: "/knowledgeBase/add",
    method: "post",
    data: data,
  });
}
// 3. ä¿®æ”¹çŸ¥è¯†åº“
export function updateKnowledgeBase(data) {
  return request({
    url: "/knowledgeBase/update",
    method: "post",
    data: data,
  });
}
// 4. åˆ é™¤çŸ¥è¯†åº“
export function delKnowledgeBase(query) {
  return request({
    url: "/knowledgeBase/delete",
    method: "delete",
    data: query,
  });
}
// 5. æŸ¥è¯¢çŸ¥è¯†åº“文件向量化状态(包含文件列表)
export function getVectorStatus(knowledgeBaseId) {
  return request({
    url: `/knowledgeBase/vector/status/${knowledgeBaseId}`,
    method: "get",
  });
}
// 6. ä¿å­˜çŸ¥è¯†åº“文件关联(触发向量化)
export function saveKnowledgeBaseFiles(data) {
  return request({
    url: "/knowledgeBase/file/save",
    method: "post",
    data,
  });
}
// 7. åˆ é™¤çŸ¥è¯†åº“文件
export function deleteKnowledgeBaseFile(ids) {
  return request({
    url: "/knowledgeBase/file/delete",
    method: "delete",
    data: ids,
  });
}
// 8. é‡æ–°å‘量化文件
export function reprocessVector(vectorId) {
  return request({
    url: `/knowledgeBase/vector/reprocess/${vectorId}`,
    method: "post",
  });
}
// 9. çŸ¥è¯†åº“问答(流式)
export function knowledgeChat(data, onMessage) {
  const token = getToken();
  return fetch(import.meta.env.VITE_APP_BASE_API + '/ai/knowledge/chat', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + token
    },
    body: JSON.stringify(data)
  });
}
// 10. æŸ¥è¯¢çŸ¥è¯†åº“问答历史
export function getKnowledgeHistory(memoryId) {
  return request({
    url: `/ai/knowledge/history/${memoryId}`,
    method: "get",
  });
}
```
### 3.3 æŽ¥å£å‚数说明
#### çŸ¥è¯†åº“列表查询
```javascript
// è¯·æ±‚参数
{
  current: 1,        // å½“前页码
  size: 20,          // æ¯é¡µæ¡æ•°
  title: "",         // çŸ¥è¯†æ ‡é¢˜(可选)
  type: ""           // çŸ¥è¯†ç±»åž‹(可选)
}
// å“åº”数据
{
  code: 200,
  data: {
    total: 100,
    records: [
      {
        id: 1,
        title: "操作手册",
        type: "guide",
        scenario: "系统操作指导",
        efficiency: "high",
        problem: "用户不会操作系统",
        solution: "按照操作手册执行...",
        keyPoints: "步骤1,步骤2,步骤3",
        creator: "张三",
        usageCount: 10,
        fileCount: 3,           // æ–‡ä»¶æ•°é‡
        totalChunkCount: 45,    // æ€»åˆ‡ç‰‡æ•°é‡
        createTime: "2026-06-08 10:00:00"
      }
    ]
  }
}
```
#### ä¿å­˜æ–‡ä»¶å…³è”
```javascript
// è¯·æ±‚参数
{
  knowledgeBaseId: 10,         // çŸ¥è¯†åº“ID
  storageBlobIds: [123, 124]   // ä¸Šä¼ æ–‡ä»¶è¿”回的blob ID列表
}
// å“åº”数据
{
  code: 200,
  msg: "操作成功"
}
```
#### å‘量化状态查询
```javascript
// å“åº”数据
{
  code: 200,
  data: [
    {
      id: 1,
      storageBlobId: 123,
      fileName: "操作手册.docx",
      fileType: "docx",
      vectorStatus: 2,         // 0-待处理,1-处理中,2-已完成,3-失败
      chunkCount: 15,          // åˆ‡ç‰‡æ•°é‡
      namespace: "kb-10",
      vectorError: null,
      createTime: "2026-06-08 10:00:00"
    }
  ]
}
```
#### çŸ¥è¯†åº“问答
```javascript
// è¯·æ±‚参数
{
  knowledgeBaseId: 10,
  memoryId: "session-xxx",     // ä¼šè¯ID,用于保持上下文
  question: "如何操作审批流程?"
}
// å“åº”(流式返回 text/stream;charset=utf-8)
// æ ¹æ®çŸ¥è¯†åº“内容,审批流程的操作步骤如下:
// 1. ç™»å½•系统后进入审批管理模块...
```
---
## å››ã€æ ¸å¿ƒç»„件实现
### 4.1 ä¸»é¡µé¢ç»“æž„
页面采用Tab页签布局,包含两个主要功能模块:
- **知识库管理** - çŸ¥è¯†åº“CRUD操作
- **知识库问答** - åŸºäºŽRAG的智能问答
### 4.2 æ•°æ®æ¨¡åž‹å®šä¹‰
```javascript
// å“åº”式数据
const data = reactive({
  // æœç´¢è¡¨å•
  searchForm: {
    title: "",
    type: "",
  },
  // åˆ†é¡µé…ç½®
  page: {
    current: 1,
    size: 20,
    total: 0,
  },
  // è¡¨æ ¼æ•°æ®
  tableData: [],
  tableLoading: false,
  selectedIds: [],
  // çŸ¥è¯†åº“表单
  form: {
    title: "",
    type: "",
    scenario: "",
    efficiency: "",
    problem: "",
    solution: "",
    keyPoints: "",
    creator: "",
    usageCount: 0
  },
  // å¼¹çª—控制
  dialogVisible: false,
  dialogTitle: "",
  dialogType: "add",  // add or edit
  viewDialogVisible: false,
  currentKnowledge: {},
  // æ–‡ä»¶ç®¡ç†
  filesDialogVisible: false,
  currentKnowledgeBase: null,
  fileList: [],
  uploadedBlobIds: [],
  savingFiles: false,
  // çŸ¥è¯†åº“问答
  chatDialogVisible: false,
  messages: [],
  inputQuestion: "",
  chatLoading: false,
  memoryId: ""
});
```
### 4.3 è¡¨æ ¼åˆ—配置
```javascript
const tableColumn = ref([
  {
    label: "知识标题",
    prop: "title",
    showOverflowTooltip: true,
  },
  {
    label: "知识类型",
    prop: "type",
    dataType: "tag",
    formatData: (params) => getKnowledgeTypeLabel(params),
    formatType: (params) => getKnowledgeTypeTagType(params)
  },
  {
    label: "适用场景",
    prop: "scenario",
    width: 150,
    showOverflowTooltip: true,
  },
  {
    label: "解决效率",
    prop: "efficiency",
    dataType: "tag",
    formatData: (params) => {
      const efficiencyMap = {
        high: "显著提升",
        medium: "一般提升",
        low: "轻微提升"
      };
      return efficiencyMap[params] || params;
    },
    formatType: (params) => {
      const typeMap = {
        high: "success",
        medium: "warning",
        low: "info"
      };
      return typeMap[params] || "info";
    }
  },
  {
    label: "文件数量",
    prop: "fileCount",
    width: 100,
    align: "center"
  },
  {
    label: "切片数量",
    prop: "totalChunkCount",
    width: 100,
    align: "center"
  },
  {
    label: "使用次数",
    prop: "usageCount",
    width: 100,
    align: "center"
  },
  {
    label: "创建人",
    prop: "creator",
    width: 120,
  },
  {
    label: "创建时间",
    prop: "createTime",
    width: 180,
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 280,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => openForm("edit", row)
      },
      {
        name: "文件",
        type: "text",
        clickFun: (row) => openFilesDialog(row)
      },
      {
        name: "问答",
        type: "text",
        clickFun: (row) => openChatDialog(row)
      },
      {
        name: "详情",
        type: "text",
        clickFun: (row) => viewKnowledge(row)
      }
    ]
  }
]);
```
---
## äº”、核心业务逻辑
### 5.1 æ–‡ä»¶ä¸Šä¼ ä¸Žå‘量化流程
#### æµç¨‹å›¾
```
用户点击"上传文件"
    â†“
选择文件(支持多选)
    â†“
前端校验文件类型和大小
    â†“
调用 /common/upload ä¸Šä¼ æ–‡ä»¶
    â†“
获取 storageBlobId åˆ—表
    â†“
用户点击"保存文件关联"
    â†“
调用 /knowledgeBase/file/save
    â†“
后端创建向量记录 + å¼‚步触发向量化
    â†“
前端延迟1秒刷新文件列表
    â†“
显示向量化状态(待处理→处理中→已完成)
```
#### ä»£ç å®žçް
```vue
<template>
  <div class="file-manager">
    <!-- æ–‡ä»¶ä¸Šä¼  -->
    <div class="upload-section">
      <el-upload
        :action="uploadUrl"
        :headers="uploadHeaders"
        :on-success="handleUploadSuccess"
        :on-error="handleUploadError"
        :before-upload="beforeUpload"
        multiple
        :show-file-list="false"
        accept=".txt,.md,.docx,.xlsx,.xls,.pdf"
      >
        <el-button type="primary">上传文件</el-button>
      </el-upload>
      <el-button
        type="success"
        @click="saveFiles"
        :disabled="uploadedBlobIds.length === 0"
        :loading="savingFiles"
      >
        ä¿å­˜æ–‡ä»¶å…³è”
      </el-button>
    </div>
    <!-- æ–‡ä»¶åˆ—表 -->
    <el-table :data="fileList" style="margin-top: 20px" border>
      <el-table-column prop="fileName" label="文件名" />
      <el-table-column prop="fileType" label="文件类型" width="100" />
      <el-table-column label="向量化状态" width="120">
        <template #default="{ row }">
          <el-tag :type="getStatusType(row.vectorStatus)">
            {{ getStatusText(row.vectorStatus) }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="chunkCount" label="切片数" width="100" />
      <el-table-column label="操作" width="150">
        <template #default="{ row }">
          <el-button
            v-if="row.vectorStatus === 3"
            type="text"
            @click="reprocessFile(row)"
          >
            é‡æ–°å¤„理
          </el-button>
          <el-button type="text" @click="deleteFile(row)" style="color: #f56c6c">
            åˆ é™¤
          </el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>
<script setup>
import { getToken } from "@/utils/auth";
// æ–‡ä»¶ä¸Šä¼ é…ç½®
const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/common/upload";
const uploadHeaders = { Authorization: "Bearer " + getToken() };
const uploadedBlobIds = ref([]);
const fileList = ref([]);
// ä¸Šä¼ å‰æ ¡éªŒ
const beforeUpload = (file) => {
  const allowedTypes = ['.txt', '.md', '.docx', '.xlsx', '.xls', '.pdf'];
  const fileName = file.name.toLowerCase();
  const isAllowed = allowedTypes.some(type => fileName.endsWith(type));
  if (!isAllowed) {
    ElMessage.error('只支持 txt、md、docx、xlsx、xls、pdf æ ¼å¼çš„æ–‡ä»¶');
    return false;
  }
  const isLt50M = file.size / 1024 / 1024 < 50;
  if (!isLt50M) {
    ElMessage.error('文件大小不能超过 50MB');
    return false;
  }
  return true;
};
// ä¸Šä¼ æˆåŠŸ
const handleUploadSuccess = (response, file) => {
  if (response.code === 200) {
    uploadedBlobIds.value.push(response.data.id);
    ElMessage.success(`文件 ${file.name} ä¸Šä¼ æˆåŠŸ`);
  } else {
    ElMessage.error(response.msg || "上传失败");
  }
};
// ä¿å­˜æ–‡ä»¶å…³è”
const saveFiles = async () => {
  if (uploadedBlobIds.value.length === 0) {
    ElMessage.warning("请先上传文件");
    return;
  }
  savingFiles.value = true;
  try {
    await saveKnowledgeBaseFiles({
      knowledgeBaseId: currentKnowledgeBase.value.id,
      storageBlobIds: uploadedBlobIds.value
    });
    ElMessage.success("文件关联保存成功,正在后台处理向量化");
    uploadedBlobIds.value = [];
    // å»¶è¿Ÿåˆ·æ–°æ–‡ä»¶åˆ—表
    setTimeout(() => {
      loadFileList();
    }, 1000);
  } catch (error) {
    console.error("保存文件关联失败:", error);
    ElMessage.error("保存文件关联失败");
  } finally {
    savingFiles.value = false;
  }
};
// åŠ è½½æ–‡ä»¶åˆ—è¡¨
const loadFileList = async () => {
  if (!currentKnowledgeBase.value?.id) return;
  try {
    const res = await getVectorStatus(currentKnowledgeBase.value.id);
    fileList.value = res.data || [];
  } catch (error) {
    console.error("加载文件列表失败:", error);
    ElMessage.error("加载文件列表失败");
  }
};
// çŠ¶æ€æ˜ å°„
const getStatusText = (status) => {
  const map = {
    0: '待处理',
    1: '处理中',
    2: '已完成',
    3: '失败'
  };
  return map[status] || '未知';
};
const getStatusType = (status) => {
  const map = {
    0: 'info',
    1: 'warning',
    2: 'success',
    3: 'danger'
  };
  return map[status] || 'info';
};
// é‡æ–°å¤„理向量化的文件
const reprocessFile = async (row) => {
  try {
    await reprocessVector(row.id);
    ElMessage.success("已重新提交向量化任务");
    setTimeout(() => {
      loadFileList();
    }, 1000);
  } catch (error) {
    console.error("重新处理失败:", error);
    ElMessage.error("重新处理失败");
  }
};
// åˆ é™¤æ–‡ä»¶
const deleteFile = async (row) => {
  try {
    await ElMessageBox.confirm(
      "确定要删除该文件吗?删除后将无法恢复向量数据",
      "删除确认",
      {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      }
    );
    await deleteKnowledgeBaseFiles([row.id]);
    ElMessage.success("删除成功");
    loadFileList();
  } catch (error) {
    if (error !== 'cancel') {
      console.error("删除文件失败:", error);
      ElMessage.error("删除文件失败");
    }
  }
};
</script>
```
### 5.2 çŸ¥è¯†åº“问答流程
#### æµç¨‹å›¾
```
用户选择知识库
    â†“
输入问题并提交
    â†“
前端生成memoryId(用于会话上下文)
    â†“
调用 /ai/knowledge/chat (流式接口)
    â†“
后端处理:
  - å¯¹é—®é¢˜è¿›è¡Œå‘量化
  - åœ¨Pinecone中检索相关切片
  - æž„建上下文Prompt
  - è°ƒç”¨LLM生成回答
    â†“
流式返回AI回答
    â†“
前端实时显示回答内容
    â†“
自动滚动到底部
```
#### ä»£ç å®žçް
```vue
<template>
  <div class="knowledge-chat">
    <div class="chat-header">
      <el-tag type="success">当前知识库: {{ currentKnowledgeBase?.title }}</el-tag>
    </div>
    <!-- å¯¹è¯åŒºåŸŸ -->
    <div class="chat-messages" ref="chatMessagesRef">
      <div
        v-for="(msg, index) in messages"
        :key="index"
        :class="['message', msg.role]"
      >
        <div class="message-role">{{ msg.role === 'user' ? '我' : 'AI助手' }}</div>
        <div class="message-content">{{ msg.content }}</div>
      </div>
      <div v-if="chatLoading" class="message assistant">
        <div class="message-role">AI助手</div>
        <div class="message-content typing">正在思考中...</div>
      </div>
    </div>
    <!-- è¾“入框 -->
    <div class="chat-input">
      <el-input
        v-model="inputQuestion"
        placeholder="请输入问题,按回车发送"
        @keyup.enter="sendMessage"
        :disabled="chatLoading"
      >
        <template #append>
          <el-button @click="sendMessage" :loading="chatLoading">发送</el-button>
        </template>
      </el-input>
    </div>
  </div>
</template>
<script setup>
import { nextTick } from 'vue';
import { getToken } from "@/utils/auth";
const messages = ref([]);
const inputQuestion = ref("");
const chatLoading = ref(false);
const memoryId = ref("");
const chatMessagesRef = ref();
// æ‰“开问答弹窗
const openChatDialog = (row) => {
  currentKnowledgeBase.value = row;
  chatDialogVisible.value = true;
  memoryId.value = crypto.randomUUID(); // ç”Ÿæˆå”¯ä¸€ä¼šè¯ID
  messages.value = [];
  inputQuestion.value = "";
};
// å‘送消息
const sendMessage = async () => {
  if (!inputQuestion.value.trim()) {
    ElMessage.warning("请输入问题");
    return;
  }
  const question = inputQuestion.value.trim();
  // æ·»åŠ ç”¨æˆ·æ¶ˆæ¯
  messages.value.push({
    role: 'user',
    content: question
  });
  inputQuestion.value = "";
  chatLoading.value = true;
  // æ»šåŠ¨åˆ°åº•éƒ¨
  await nextTick();
  scrollToBottom();
  try {
    // æµå¼è¯·æ±‚
    const response = await fetch('/api/ai/knowledge/chat', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + getToken()
      },
      body: JSON.stringify({
        knowledgeBaseId: currentKnowledgeBase.value.id,
        memoryId: memoryId.value,
        question: question
      })
    });
    if (!response.ok) {
      throw new Error('请求失败');
    }
    // å¤„理SSE流式响应
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let aiContent = '';
    messages.value.push({ role: 'assistant', content: '' });
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const text = decoder.decode(value);
      aiContent += text;
      messages.value[messages.value.length - 1].content = aiContent;
      // æ»šåŠ¨åˆ°åº•éƒ¨
      await nextTick();
      scrollToBottom();
    }
  } catch (error) {
    console.error("问答请求失败:", error);
    ElMessage.error("问答请求失败,请稍后重试");
    messages.value.push({
      role: 'assistant',
      content: '抱歉,发生了错误,请稍后重试'
    });
  } finally {
    chatLoading.value = false;
  }
};
// æ»šåŠ¨åˆ°åº•éƒ¨
const scrollToBottom = () => {
  if (chatMessagesRef.value) {
    chatMessagesRef.value.scrollTop = chatMessagesRef.value.scrollHeight;
  }
};
</script>
<style scoped>
.knowledge-chat {
  display: flex;
  flex-direction: column;
  height: 500px;
}
.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  background: #f5f7fa;
  border-radius: 8px;
  margin-bottom: 16px;
}
.message {
  margin-bottom: 16px;
  max-width: 80%;
}
.message.user {
  margin-left: auto;
  text-align: right;
}
.message.assistant {
  margin-right: auto;
}
.message-role {
  font-size: 12px;
  color: #909399;
  margin-bottom: 4px;
}
.message-content {
  display: inline-block;
  padding: 10px 14px;
  border-radius: 8px;
  line-height: 1.6;
  word-wrap: break-word;
  white-space: pre-wrap;
}
.message.user .message-content {
  background: #409eff;
  color: white;
}
.message.assistant .message-content {
  background: white;
  color: #303133;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.typing {
  animation: typing 1.5s infinite;
}
@keyframes typing {
  0%, 50%, 100% { opacity: 1; }
  25%, 75% { opacity: 0.5; }
}
</style>
```
---
## å…­ã€å…³é”®å®žçŽ°ç»†èŠ‚
### 6.1 æ–‡ä»¶ä¸Šä¼ é…ç½®
```javascript
// ä¸Šä¼ åœ°å€
const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/common/upload";
// è¯·æ±‚头(必须携带Token)
const uploadHeaders = {
  Authorization: "Bearer " + getToken()
};
// æ”¯æŒçš„æ–‡ä»¶ç±»åž‹
const acceptTypes = '.txt,.md,.docx,.xlsx,.xls,.pdf';
// æ–‡ä»¶å¤§å°é™åˆ¶
const maxSize = 50 * 1024 * 1024; // 50MB
```
### 6.2 å‘量化状态轮询
```javascript
// å¼€å§‹è½®è¯¢å‘量化状态
const startVectorStatusPolling = () => {
  const timer = setInterval(async () => {
    const res = await getVectorStatus(currentKnowledgeBase.value.id);
    const hasProcessing = res.data.some(item => item.vectorStatus === 1);
    if (!hasProcessing) {
      clearInterval(timer);
    }
    fileList.value = res.data;
  }, 3000); // æ¯3秒轮询一次
};
```
### 6.3 æµå¼å“åº”处理
```javascript
// ä½¿ç”¨ Fetch API å¤„理流式响应
const response = await fetch('/api/ai/knowledge/chat', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + getToken()
  },
  body: JSON.stringify({
    knowledgeBaseId: 10,
    memoryId: "session-xxx",
    question: "问题内容"
  })
});
// èŽ·å–å¯è¯»æµ
const reader = response.body.getReader();
const decoder = new TextDecoder();
// é€å—读取数据
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  const text = decoder.decode(value);
  // å¤„理文本块
  processText(text);
}
```
### 6.4 ä¼šè¯ç®¡ç†
```javascript
// ç”Ÿæˆå”¯ä¸€ä¼šè¯ID
const memoryId = crypto.randomUUID();
// æˆ–使用时间戳
const memoryId = 'kb-chat-' + Date.now();
// ä¼šè¯ID用于:
// 1. ä¿æŒå¯¹è¯ä¸Šä¸‹æ–‡
// 2. æ”¯æŒå¤šè½®å¯¹è¯
// 3. æŸ¥è¯¢åŽ†å²è®°å½•
```
---
## ä¸ƒã€çŠ¶æ€ç®¡ç†
### 7.1 å‘量化状态定义
| çŠ¶æ€å€¼ | çŠ¶æ€åç§° | è¯´æ˜Ž | æ ‡ç­¾é¢œè‰² |
|--------|----------|------|----------|
| 0 | å¾…处理 | æ–‡ä»¶å·²ä¸Šä¼ ,等待向量化处理 | info(灰色) |
| 1 | å¤„理中 | æ­£åœ¨è¿›è¡Œå‘量切片处理 | warning(橙色) |
| 2 | å·²å®Œæˆ | å‘量化完成,可进行检索问答 | success(绿色) |
| 3 | å¤±è´¥ | å‘量化失败,需重新处理 | danger(红色) |
### 7.2 çŸ¥è¯†ç±»åž‹é…ç½®
```javascript
// ä½¿ç”¨å­—典配置知识类型
const { knowledge_type } = proxy.useDict("knowledge_type");
// ç¤ºä¾‹æ•°æ®
const knowledgeTypeOptions = [
  { value: 'contract', label: '合同知识', elTagType: 'success' },
  { value: 'approval', label: '审批流程', elTagType: 'warning' },
  { value: 'solution', label: '解决方案', elTagType: 'primary' },
  { value: 'experience', label: '经验分享', elTagType: 'info' },
  { value: 'guide', label: '操作指南', elTagType: 'danger' }
];
```
### 7.3 è§£å†³æ•ˆçŽ‡æ˜ å°„
```javascript
const efficiencyMap = {
  high: { label: '显著提升', color: 'success', score: 40, time: '2-3天' },
  medium: { label: '一般提升', color: 'warning', score: 25, time: '1-2天' },
  low: { label: '轻微提升', color: 'info', score: 15, time: '0.5-1天' }
};
```
---
## å…«ã€æ ·å¼è®¾è®¡
### 8.1 æ–‡ä»¶ç®¡ç†æ ·å¼
```css
.file-manager {
  padding: 20px 0;
}
.upload-section {
  display: flex;
  align-items: center;
  gap: 10px;
}
```
### 8.2 é—®ç­”界面样式
```css
.knowledge-chat {
  display: flex;
  flex-direction: column;
  height: 500px;
}
.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  background: #f5f7fa;
  border-radius: 8px;
  margin-bottom: 16px;
}
.message {
  margin-bottom: 16px;
  max-width: 80%;
}
.message.user {
  margin-left: auto;
  text-align: right;
}
.message.assistant {
  margin-right: auto;
}
.message-content {
  display: inline-block;
  padding: 10px 14px;
  border-radius: 8px;
  line-height: 1.6;
  word-wrap: break-word;
  white-space: pre-wrap;
}
.message.user .message-content {
  background: #409eff;
  color: white;
}
.message.assistant .message-content {
  background: white;
  color: #303133;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
```
---
## ä¹ã€æ³¨æ„äº‹é¡¹
### 9.1 æ–‡ä»¶ä¸Šä¼ 
1. **必须调用保存接口**: ä¸Šä¼ æˆåŠŸåŽå¿…é¡»è°ƒç”¨ `/knowledgeBase/file/save` æ‰èƒ½è§¦å‘向量化
2. **文件类型限制**: åªæ”¯æŒ txt、md、docx、xlsx、xls、pdf æ ¼å¼
3. **文件大小限制**: å•文件最大 50MB
4. **异步处理**: å‘量化是异步处理,不会阻塞用户操作
### 9.2 å‘量化状态
1. **状态轮询**: å»ºè®®æ¯3-5秒轮询一次状态
2. **停止轮询**: å½“所有文件状态都不是"处理中"时停止轮询
3. **失败处理**: çŠ¶æ€ä¸º"失败"时可点击"重新处理"按钮
### 9.3 çŸ¥è¯†åº“问答
1. **会话ID**: æ¯æ¬¡æ‰“开问答弹窗需要生成新的 memoryId
2. **流式处理**: ä½¿ç”¨ Fetch API å¤„理流式响应,不支持 axios
3. **错误处理**: éœ€è¦å¤„理网络错误和AI响应错误
4. **自动滚动**: æ¯æ¬¡æ”¶åˆ°æ–°æ¶ˆæ¯è‡ªåŠ¨æ»šåŠ¨åˆ°åº•éƒ¨
### 9.4 æ•°æ®ä¸€è‡´æ€§
1. **删除知识库**: éœ€è¦åŒæ—¶åˆ é™¤å…³è”的文件和向量数据
2. **删除文件**: åˆ é™¤æ–‡ä»¶æ—¶åŒæ­¥åˆ é™¤å‘量库中的相关切片
3. **刷新列表**: æ–‡ä»¶æ“ä½œåŽéœ€è¦åˆ·æ–°çŸ¥è¯†åº“列表,更新文件数量和切片数量
---
## åã€æµ‹è¯•检查清单
### 10.1 çŸ¥è¯†åº“管理
- [ ] æ–°å¢žçŸ¥è¯†åº“成功
- [ ] ç¼–辑知识库成功
- [ ] åˆ é™¤çŸ¥è¯†åº“成功(单个/批量)
- [ ] æœç´¢åŠŸèƒ½æ­£å¸¸(按标题、类型)
- [ ] åˆ†é¡µåŠŸèƒ½æ­£å¸¸
- [ ] å¯¼å‡ºåŠŸèƒ½æ­£å¸¸
### 10.2 æ–‡ä»¶ä¸Šä¼ 
- [ ] å•文件上传成功
- [ ] å¤šæ–‡ä»¶ä¸Šä¼ æˆåŠŸ
- [ ] æ–‡ä»¶ç±»åž‹æ ¡éªŒæ­£å¸¸
- [ ] æ–‡ä»¶å¤§å°æ ¡éªŒæ­£å¸¸
- [ ] ä¿å­˜æ–‡ä»¶å…³è”成功
- [ ] å‘量化状态正确显示
### 10.3 æ–‡ä»¶ç®¡ç†
- [ ] æŸ¥çœ‹æ–‡ä»¶åˆ—表正常
- [ ] å‘量化状态轮询正常
- [ ] é‡æ–°å¤„理失败文件成功
- [ ] åˆ é™¤æ–‡ä»¶æˆåŠŸ
- [ ] æ–‡ä»¶é¢„览/下载正常
### 10.4 çŸ¥è¯†åº“问答
- [ ] é—®ç­”弹窗打开正常
- [ ] å‘送问题成功
- [ ] æµå¼å“åº”显示正常
- [ ] å¤šè½®å¯¹è¯æ­£å¸¸
- [ ] è‡ªåŠ¨æ»šåŠ¨åˆ°åº•éƒ¨
- [ ] é”™è¯¯å¤„理正常
---
## åä¸€ã€å¸¸è§é—®é¢˜
### Q1: æ–‡ä»¶ä¸Šä¼ åŽæ²¡æœ‰è§¦å‘向量化?
**A**: æ£€æŸ¥æ˜¯å¦è°ƒç”¨äº† `/knowledgeBase/file/save` æŽ¥å£ã€‚上传成功后必须调用此接口才能触发向量化。
### Q2: å‘量化状态一直是"处理中"?
**A**: å¯èƒ½åŽŸå› :
1. åŽå°æœåŠ¡æœªå¯åŠ¨
2. Embedding模型调用失败
3. Pinecone连接失败
建议查看后台日志排查问题。
### Q3: é—®ç­”返回空内容?
**A**: å¯èƒ½åŽŸå› :
1. çŸ¥è¯†åº“中没有文件
2. æ–‡ä»¶æœªå®Œæˆå‘量化
3. æ£€ç´¢ç›¸ä¼¼åº¦ä½ŽäºŽé˜ˆå€¼
建议检查文件数量和向量化状态。
### Q4: æµå¼å“åº”显示乱码?
**A**: ç¡®ä¿è¯·æ±‚头包含正确的编码设置:
```javascript
headers: {
  'Content-Type': 'application/json'
}
```
### Q5: å¦‚何调试流式接口?
**A**: ä½¿ç”¨æµè§ˆå™¨å¼€å‘者工具:
1. æ‰“å¼€ Network æ ‡ç­¾
2. æ‰¾åˆ° `/ai/knowledge/chat` è¯·æ±‚
3. æŸ¥çœ‹ Response æ ‡ç­¾,可以看到流式返回的内容
---
## åäºŒã€ä¼˜åŒ–建议
### 12.1 æ€§èƒ½ä¼˜åŒ–
1. **虚拟滚动**: æ¶ˆæ¯åˆ—表超过100条时使用虚拟滚动
2. **防抖节流**: æœç´¢è¾“入使用防抖,状态轮询使用节流
3. **懒加载**: æ–‡ä»¶åˆ—表使用懒加载
### 12.2 ç”¨æˆ·ä½“验优化
1. **进度提示**: å‘量化时显示进度条
2. **快捷键**: æ”¯æŒå¿«æ·é”®æ“ä½œ(如 Ctrl+Enter å‘送)
3. **历史记录**: æ”¯æŒæŸ¥çœ‹åŽ†å²é—®ç­”è®°å½•
4. **导出对话**: æ”¯æŒå¯¼å‡ºå¯¹è¯å†…容
### 12.3 åŠŸèƒ½æ‰©å±•
1. **文件预览**: æ”¯æŒåœ¨çº¿é¢„览文件内容
2. **批量操作**: æ”¯æŒæ‰¹é‡åˆ é™¤ã€æ‰¹é‡é‡æ–°å¤„理
3. **向量化配置**: å…è®¸ç”¨æˆ·é…ç½®åˆ‡ç‰‡å¤§å°ã€é‡å å¤§å°
4. **相似度阈值**: å…è®¸ç”¨æˆ·è°ƒæ•´æ£€ç´¢ç›¸ä¼¼åº¦é˜ˆå€¼
---
## åä¸‰ã€æ›´æ–°æ—¥å¿—
### v1.0.0 (2026-06-08)
- âœ… å®ŒæˆçŸ¥è¯†åº“CRUD功能
- âœ… å®Œæˆæ–‡ä»¶ä¸Šä¼ ä¸Žå‘量化功能
- âœ… å®ŒæˆçŸ¥è¯†åº“问答功能
- âœ… å®Œæˆæ–‡ä»¶ç®¡ç†åŠŸèƒ½
- âœ… å®Œæˆå‘量化状态显示
### v1.1.0 (计划中)
- ðŸ”² æ·»åŠ åŽ†å²è®°å½•æŸ¥è¯¢
- ðŸ”² æ·»åŠ æ‰¹é‡æ“ä½œåŠŸèƒ½
- ðŸ”² æ·»åŠ æ–‡ä»¶é¢„è§ˆåŠŸèƒ½
- ðŸ”² ä¼˜åŒ–向量化进度显示
src/api/collaborativeApproval/knowledgeBase.js
@@ -1,4 +1,5 @@
import request from "@/utils/request";
import { getToken } from '@/utils/auth';
// æŸ¥è¯¢çŸ¥è¯†åº“列表
export function listKnowledgeBase(query) {
@@ -87,20 +88,24 @@
  });
}
// èŽ·å–çŸ¥è¯†åº“åˆ—è¡¨(问答用)
export function getKnowledgeBaseListForChat() {
// æŸ¥è¯¢çŸ¥è¯†åº“问答历史
export function getKnowledgeHistory(memoryId) {
  return request({
    url: "/ai/knowledge/list",
    url: `/ai/knowledge/history/${memoryId}`,
    method: "get",
  });
}
// çŸ¥è¯†åº“问答(流式)
export async function knowledgeChat(data) {
  const response = await fetch("/api/ai/knowledge/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  const token = getToken();
  const response = await fetch(import.meta.env.VITE_APP_BASE_API + '/ai/knowledge/chat', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + token
    },
    body: JSON.stringify(data)
  });
  return response.body;
}
src/views/collaborativeApproval/knowledgeBase/index.vue
@@ -259,6 +259,22 @@
          >
            ä¿å­˜æ–‡ä»¶å…³è”
          </el-button>
          <el-button
            v-if="uploadedBlobIds.length > 0"
            type="text"
            @click="clearUploadedFiles"
            style="margin-left: 10px"
          >
            æ¸…空待保存列表
          </el-button>
        </div>
        <!-- å¾…保存的文件列表 -->
        <div v-if="uploadedBlobIds.length > 0" class="uploaded-list">
          <div class="uploaded-tip">
            <el-icon style="color: #409eff"><InfoFilled /></el-icon>
            <span>已上传 {{ uploadedBlobIds.length }} ä¸ªæ–‡ä»¶,请点击"保存文件关联"按钮触发向量化处理</span>
          </div>
        </div>
        <!-- æ–‡ä»¶åˆ—表与向量化状态 -->
@@ -273,6 +289,12 @@
            </template>
          </el-table-column>
          <el-table-column prop="chunkCount" label="切片数" width="100" align="center" />
          <el-table-column label="错误信息" width="200" show-overflow-tooltip>
            <template #default="{ row }">
              <span v-if="row.vectorError" style="color: #f56c6c">{{ row.vectorError }}</span>
              <span v-else style="color: #909399">-</span>
            </template>
          </el-table-column>
          <el-table-column prop="createTime" label="上传时间" width="180" />
          <el-table-column label="操作" width="150" align="center">
            <template #default="{ row }">
@@ -326,7 +348,7 @@
        <div class="chat-input">
          <el-input
            v-model="inputQuestion"
            placeholder="请输入问题,按回车发送"
            placeholder="请输入问题,按回车发送(Ctrl+Enter快捷发送)"
            @keyup.enter="sendMessage"
            :disabled="chatLoading"
          >
@@ -334,6 +356,9 @@
              <el-button @click="sendMessage" :loading="chatLoading">发送</el-button>
            </template>
          </el-input>
          <div class="chat-actions">
            <el-button type="text" size="small" @click="clearMessages">清空对话</el-button>
          </div>
        </div>
      </div>
    </FormDialog>
@@ -341,7 +366,7 @@
</template>
<script setup>
import { Search } from "@element-plus/icons-vue";
import { Search, InfoFilled } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, toRefs, getCurrentInstance, computed, watch, nextTick } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
@@ -412,6 +437,7 @@
  fileList: [],
  uploadedBlobIds: [],
  savingFiles: false,
  vectorStatusTimer: null, // å‘量化状态轮询定时器
  chatDialogVisible: false,
  messages: [],
  inputQuestion: "",
@@ -436,6 +462,7 @@
  fileList,
  uploadedBlobIds,
  savingFiles,
  vectorStatusTimer,
  chatDialogVisible,
  messages,
  inputQuestion,
@@ -872,9 +899,45 @@
  try {
    const res = await getVectorStatus(currentKnowledgeBase.value.id);
    fileList.value = res.data || [];
    // æ£€æŸ¥æ˜¯å¦æœ‰å¤„理中的文件,如果有则启动轮询
    const hasProcessing = res.data.some(item => item.vectorStatus === 1);
    if (hasProcessing && !vectorStatusTimer.value) {
      startVectorStatusPolling();
    } else if (!hasProcessing && vectorStatusTimer.value) {
      stopVectorStatusPolling();
    }
  } catch (error) {
    console.error("加载文件列表失败:", error);
    ElMessage.error("加载文件列表失败");
  }
};
// å¼€å§‹è½®è¯¢å‘量化状态
const startVectorStatusPolling = () => {
  vectorStatusTimer.value = setInterval(async () => {
    try {
      const res = await getVectorStatus(currentKnowledgeBase.value.id);
      fileList.value = res.data || [];
      // æ£€æŸ¥æ˜¯å¦è¿˜æœ‰å¤„理中的文件
      const hasProcessing = res.data.some(item => item.vectorStatus === 1);
      if (!hasProcessing) {
        stopVectorStatusPolling();
        ElMessage.success("所有文件向量化处理完成");
      }
    } catch (error) {
      console.error("轮询向量化状态失败:", error);
      stopVectorStatusPolling();
    }
  }, 3000); // æ¯3秒轮询一次
};
// åœæ­¢è½®è¯¢å‘量化状态
const stopVectorStatusPolling = () => {
  if (vectorStatusTimer.value) {
    clearInterval(vectorStatusTimer.value);
    vectorStatusTimer.value = null;
  }
};
@@ -957,6 +1020,12 @@
  }
};
// æ¸…空待保存的文件列表
const clearUploadedFiles = () => {
  uploadedBlobIds.value = [];
  ElMessage.success("已清空待保存文件列表");
};
// åˆ é™¤æ–‡ä»¶
const deleteFile = async (row) => {
  try {
@@ -1009,7 +1078,8 @@
  currentKnowledgeBase.value = null;
  fileList.value = [];
  uploadedBlobIds.value = [];
  getList(); // åˆ·æ–°ä¸»åˆ—表,更新文件数量
  stopVectorStatusPolling(); // åœæ­¢è½®è¯¢
  getList(); // åˆ·æ–°ä¸»åˆ—表,更新文件数量
};
// ============ çŸ¥è¯†åº“问答相关 ============
@@ -1027,6 +1097,11 @@
const sendMessage = async () => {
  if (!inputQuestion.value.trim()) {
    ElMessage.warning("请输入问题");
    return;
  }
  if (!currentKnowledgeBase.value?.id) {
    ElMessage.error("知识库信息异常");
    return;
  }
@@ -1061,7 +1136,8 @@
    });
    if (!response.ok) {
      throw new Error('请求失败');
      const errorText = await response.text();
      throw new Error(errorText || '请求失败');
    }
    // å¤„理SSE流式响应
@@ -1083,16 +1159,40 @@
      await nextTick();
      scrollToBottom();
    }
    // å¦‚æžœAI返回空内容,显示提示
    if (!aiContent.trim()) {
      messages.value[messages.value.length - 1].content = '抱歉,知识库中未找到相关内容,请尝试其他问题。';
    }
  } catch (error) {
    console.error("问答请求失败:", error);
    ElMessage.error("问答请求失败,请稍后重试");
    ElMessage.error("问答请求失败,请稍后重试");
    messages.value.push({
      role: 'assistant',
      content: '抱歉,发生了错误,请稍后重试'
      content: '抱歉,发生了错误,请稍后重试'
    });
  } finally {
    chatLoading.value = false;
  }
};
// æ¸…空对话
const clearMessages = () => {
  ElMessageBox.confirm(
    "确定要清空所有对话记录吗?",
    "清空确认",
    {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning"
    }
  ).then(() => {
    messages.value = [];
    memoryId.value = crypto.randomUUID(); // é‡æ–°ç”Ÿæˆä¼šè¯ID
    ElMessage.success("对话已清空");
  }).catch(() => {
    // ç”¨æˆ·å–消
  });
};
// æ»šåŠ¨åˆ°åº•éƒ¨
@@ -1195,6 +1295,22 @@
  align-items: center;
}
.uploaded-list {
  margin-top: 16px;
  padding: 12px;
  background: #f0f9ff;
  border-radius: 6px;
  border: 1px solid #b3d8ff;
}
.uploaded-tip {
  display: flex;
  align-items: center;
  gap: 8px;
  color: #409eff;
  font-size: 14px;
}
/* çŸ¥è¯†åº“问答样式 */
.knowledge-chat {
  display: flex;
@@ -1272,4 +1388,9 @@
  margin-top: auto;
}
.chat-actions {
  margin-top: 8px;
  text-align: right;
}
</style>
vite.config.js
@@ -8,7 +8,7 @@
  const { VITE_APP_ENV } = env;
  const baseUrl =
      env.VITE_APP_ENV === "development"
          ? "http://1.15.17.182:9048"
          ? "http://localhost:7005"
          : env.VITE_BASE_API;
  const javaUrl =
      env.VITE_APP_ENV === "development"