From 00eb23cf54fa7b492ff13c0521a45506fb1f57ca Mon Sep 17 00:00:00 2001 From: "DESKTOP-72TV0V4\\caoxiaozhu" Date: Sun, 8 Mar 2026 23:02:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=20Knowledge=20?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=92=8C=E9=9C=80=E6=B1=82=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增强知识库前端交互 - 更新知识库 API 需求文档 - 添加 TODO 待办事项 Co-Authored-By: Claude Opus 4.6 --- team-require/api/knowledge-api.md | 9 ++ team-require/web/knowledge-base-api.md | 25 ++++- team-require/web/todo-2026-3-8.md | 20 +++- web/src/views/Knowledge.vue | 134 ++++++++++++++++++------ web/src/views/knowledge/useKnowledge.ts | 13 +++ 5 files changed, 161 insertions(+), 40 deletions(-) diff --git a/team-require/api/knowledge-api.md b/team-require/api/knowledge-api.md index 962a23f..593351f 100644 --- a/team-require/api/knowledge-api.md +++ b/team-require/api/knowledge-api.md @@ -28,6 +28,13 @@ Content-Type: application/json | - docling_url | String | 条件 | Docling URL(engine=docling 时必填) | | - enable_pdf | Boolean | 否 | 是否启用 PDF 解析 | | - pandoc | Boolean | 否 | 是否启用 Pandoc | +| storage_config | Object | 否 | 存储配置,不传则使用全局配置 | +| - type | String | 否 | 存储模式:local / minio | +| - endpoint | String | 否 | MinIO endpoint | +| - bucket | String | 否 | MinIO bucket | +| - access_key | String | 否 | MinIO access key | +| - secret_key | String | 否 | MinIO secret key | +| - use_ssl | Boolean | 否 | MinIO 是否使用 SSL | **响应** @@ -149,6 +156,8 @@ GET /api/knowledge/:id/documents "id": "doc_001", "knowledge_base_id": "kb_001", "name": "产品手册_v2.0.pdf", + "file_key": "abc123.pdf", + "file_url": "http://localhost:8082/files/abc123.pdf", "file_size": 2516582, "status": "parsed", "chunk_count": 156, diff --git a/team-require/web/knowledge-base-api.md b/team-require/web/knowledge-base-api.md index ec6d4a1..e3306e7 100644 --- a/team-require/web/knowledge-base-api.md +++ b/team-require/web/knowledge-base-api.md @@ -29,9 +29,16 @@ Content-Type: application/json | - docling_url | String | 条件必填 | Docling 服务 URL(engine=docling 时必填) | | - enable_pdf | Boolean | 否 | 是否启用 PDF 解析(默认 true) | | - pandoc | Boolean | 否 | 是否启用 Pandoc(默认 true) | +| storage_config | Object | 否 | 存储配置(默认 local) | +| - type | String | 是 | 存储类型:local / minio / s3 | +| - endpoint | String | 否 | MinIO Endpoint(如 minio:9000) | +| - access_key_id | String | 否 | MinIO Access Key ID | +| - secret_access_key | String | 否 | MinIO Secret Access Key | +| - bucket | String | 否 | MinIO Bucket 名称 | **请求示例** +本地存储: ```json { "name": "产品文档知识库", @@ -42,12 +49,14 @@ Content-Type: application/json "engine": "markitdown", "enable_pdf": true, "pandoc": true + }, + "storage_config": { + "type": "local" } } ``` -或使用 Docling: - +使用 Docling + MinIO: ```json { "name": "产品文档知识库", @@ -59,6 +68,13 @@ Content-Type: application/json "docling_url": "http://localhost:8501", "enable_pdf": true, "pandoc": true + }, + "storage_config": { + "type": "minio", + "endpoint": "localhost:9000", + "access_key_id": "minioadmin", + "secret_access_key": "minioadmin", + "bucket": "x-agents" } } ``` @@ -204,7 +220,10 @@ GET /api/knowledge/:id/documents "data": [ { "id": "doc_001", + "knowledge_base_id": "kb_001", "name": "产品手册_v2.0.pdf", + "file_key": "abc123.pdf", + "file_url": "http://localhost:8082/files/abc123.pdf", "file_size": 2516582, "status": "parsed", "chunk_count": 156, @@ -334,6 +353,7 @@ GET /api/knowledge/:id/documents/:doc_id/preview | llm_model_id | String | LLM 模型 ID | | embedding_model_id | String | Embedding 模型 ID | | parsing_config | JSON | 解析配置 | +| storage_config | JSON | 存储配置(包含 type, endpoint, access_key_id, secret_access_key, bucket) | | status | String | 状态:active / inactive | | document_count | Integer | 文档数量 | | chunk_count | Integer | 切片数量 | @@ -348,6 +368,7 @@ GET /api/knowledge/:id/documents/:doc_id/preview | knowledge_base_id | String | 知识库 ID | | name | String | 文档名称 | | file_key | String | 文件存储 key | +| file_url | String | 文件访问 URL(本地路径或 MinIO 预签名 URL) | | file_size | BigInteger | 文件大小 | | status | String | 状态:parsing / parsed / failed | | chunk_count | Integer | 切片数量 | diff --git a/team-require/web/todo-2026-3-8.md b/team-require/web/todo-2026-3-8.md index 9e275f3..7555832 100644 --- a/team-require/web/todo-2026-3-8.md +++ b/team-require/web/todo-2026-3-8.md @@ -4,12 +4,26 @@ ### 2026-03-08 -- [ ] **知识库(Knowledge Base)API** - 后端待实现 +- [x] **知识库(Knowledge Base)API** - 后端已完成 ✔ - 创建知识库、获取列表、获取详情、删除 - 上传文档、删除文档、重新解析 - 获取文档预览内容 - 详细需求:[knowledge-base-api.md](./knowledge-base-api.md) +- [x] **编辑时正确处理 sub_tables** - 后端已完成 ✔ + - 问题:取消选中 1 个表后保存,再次进入仍显示 2 个表 + - 详细需求:[sub-tables-edit.md](./sub-tables-edit.md) + +- [x] **知识库存储配置 (MinIO/S3)** - 后端已完成 ✔ + - 前端已完成:添加 storage_config 参数传递 + - 后端已完成:KnowledgeBase 模型添加 storage_config 字段 + - 上传文件时使用知识库的 storage_config,而非全局配置 + - 详细需求:[knowledge-base-api.md](./knowledge-base-api.md) + +- [x] **文档列表返回 file_url** - 后端已完成 ✔ + - 问题:重新进入知识库后 PDF 无法预览 + - 已确认:API 返回的 file_url 字段有值 + --- ### 2026-03-07 @@ -26,10 +40,6 @@ - 问题:用户从 2 个子表修改为 1 个后,Tables 列没有更新 - 详细需求:[table-count-update-edit.md](./table-count-update-edit.md) -- [ ] **编辑时正确处理 sub_tables** - 后端待实现 - - 问题:取消选中 1 个表后保存,再次进入仍显示 2 个表 - - 详细需求:[sub-tables-edit.md](./sub-tables-edit.md) - --- > 需求完成后请完成者打 ✔ diff --git a/web/src/views/Knowledge.vue b/web/src/views/Knowledge.vue index 691e579..7e32700 100644 --- a/web/src/views/Knowledge.vue +++ b/web/src/views/Knowledge.vue @@ -48,7 +48,18 @@ const step3Valid = computed(() => { } return true }) -const step4Valid = computed(() => true) // Storage - 暂时默认通过 +const step4Valid = computed(() => { + // Local 存储不需要额外配置 + if (storageConfig.value.type === 'local') { + return true + } + // MinIO 存储需要填写所有字段 + if (storageConfig.value.type === 'minio') { + return !!(storageConfig.value.endpoint && storageConfig.value.accessKeyId && storageConfig.value.secretAccessKey && storageConfig.value.bucket) + } + // S3 暂时默认通过 + return true +}) // 获取当前步骤是否有效 const isCurrentStepValid = computed(() => { @@ -123,11 +134,29 @@ const knowledgeDocuments = ref([]) // 知识库文档列表 const loadingDocuments = ref(false) const fileInput = ref(null) const uploading = ref(false) -const previewUrl = ref('') // 文档预览URL +const previewUrl = ref('') // 文档预览URL (blob URL) +const previewDownloadUrl = ref('') // 原始下载链接 const loadingPreview = ref(false) const previewPage = ref(1) // 当前页码 const previewTotalPages = ref(1) // 总页数 +// 使用代理接口加载PDF +const loadPdfWithProxy = async (doc: any): Promise => { + if (!selectedKnowledge.value || !doc.file_key) { + return '' + } + + try { + const { getFileProxyUrl } = await import('./knowledge/useKnowledge') + const proxyUrl = getFileProxyUrl(selectedKnowledge.value.id, doc.file_key) + console.log('Using proxy URL for PDF:', proxyUrl) + return proxyUrl + } catch (error) { + console.error('Failed to get proxy URL:', error) + return '' + } +} + const newKbForm = ref({ name: '', description: '', @@ -151,6 +180,10 @@ const parsingConfig = ref({ // Storage 配置 const storageConfig = ref({ type: 'local', + endpoint: '', + accessKeyId: '', + secretAccessKey: '', + bucket: '', }) const openCreateDialog = () => { @@ -197,6 +230,13 @@ const createKnowledgeBase = async () => { docling_url: parsingConfig.value.engine === 'docling' ? parsingConfig.value.doclingUrl : undefined, enable_pdf: parsingConfig.value.enablePdf, pandoc: parsingConfig.value.pandoc, + }, + storage_config: { + type: storageConfig.value.type, + endpoint: storageConfig.value.type === 'minio' ? storageConfig.value.endpoint : undefined, + access_key_id: storageConfig.value.type === 'minio' ? storageConfig.value.accessKeyId : undefined, + secret_access_key: storageConfig.value.type === 'minio' ? storageConfig.value.secretAccessKey : undefined, + bucket: storageConfig.value.type === 'minio' ? storageConfig.value.bucket : undefined, } }) @@ -297,17 +337,16 @@ const enterKnowledge = async (kb: any) => { selectedKnowledge.value = kb selectedFile.value = null previewUrl.value = '' + previewDownloadUrl.value = '' // 获取文档列表 loadingDocuments.value = true try { const docs = await fetchKnowledgeDocuments(kb.id, fileFilter.value) - console.log('Fetched documents:', docs) knowledgeDocuments.value = docs // 自动选中第一个文档 if (docs && docs.length > 0) { - console.log('First doc:', docs[0]) await selectDocument(docs[0]) } } finally { @@ -336,15 +375,27 @@ const selectDocument = async (doc: any) => { selectedFile.value = doc.id selectedDocument.value = doc previewUrl.value = '' + previewDownloadUrl.value = '' previewPage.value = 1 previewTotalPages.value = 1 - // 尝试从多个字段获取文件URL + // 优先使用代理接口加载PDF + if (doc.file_key && selectedKnowledge.value) { + previewUrl.value = await loadPdfWithProxy(doc) + if (previewUrl.value) { + return + } + } + + // 如果代理失败,尝试使用file_url const fileUrl = doc.file_url || doc.fileUrl || doc.url || doc.FileURL if (fileUrl) { previewUrl.value = fileUrl - } else if (selectedKnowledge.value && doc.status === 'parsed') { - // 获取文档预览 + return + } + + // 如果没有file_url,调用预览API获取 + if (selectedKnowledge.value) { loadingPreview.value = true try { const { getDocumentPreview } = await import('./knowledge/useKnowledge') @@ -402,29 +453,22 @@ const handleFileSelect = async (event: Event) => { if (result.success) { ElMessage.success('File uploaded successfully') - // 后端返回 result.url 在顶层,result.document 里有 file_url - const fileUrl = result.url || result.document?.file_url - // 添加到文档列表 - const newDoc = result.document || { - id: result.id, - name: file.name, - file_size: file.size, - status: 'parsing', - chunk_count: 0, - uploaded_at: new Date().toISOString(), - file_url: fileUrl - } - // 如果返回了file_url,添加到列表开头 - if (fileUrl) { - previewUrl.value = fileUrl - // 设置选中的文档信息 + // 刷新文档列表以获取最新数据(包括 file_key) + await changeFileFilter(fileFilter.value) + + // 获取刚上传的文档 + const uploadedDoc = knowledgeDocuments.value.find(d => d.id === result.id) + if (uploadedDoc) { + // 选中新上传的文档 selectedFile.value = result.id - selectedDocument.value = newDoc - // 添加到文档列表 - knowledgeDocuments.value = [newDoc, ...knowledgeDocuments.value] - } else { - // 刷新文档列表 - await changeFileFilter(fileFilter.value) + selectedDocument.value = uploadedDoc + + // 使用代理接口加载PDF + if (uploadedDoc.file_key) { + previewUrl.value = await loadPdfWithProxy(uploadedDoc) + } else if (uploadedDoc.file_url) { + previewUrl.value = uploadedDoc.file_url + } } } else { ElMessage.error(result.message || 'Failed to upload file') @@ -453,6 +497,7 @@ const deleteDocument = async (docId: string) => { selectedFile.value = null selectedDocument.value = null previewUrl.value = '' + previewDownloadUrl.value = '' } // 刷新文档列表 await changeFileFilter(fileFilter.value) @@ -739,6 +784,22 @@ const deleteDocument = async (docId: string) => { + + + @@ -925,14 +986,21 @@ const deleteDocument = async (docId: string) => { Loading preview... - - +