feat: 优化 Knowledge 前端和需求文档

- 增强知识库前端交互
- 更新知识库 API 需求文档
- 添加 TODO 待办事项

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 23:02:21 +08:00
parent 4017c0e1df
commit 00eb23cf54
5 changed files with 161 additions and 40 deletions

View File

@@ -28,6 +28,13 @@ Content-Type: application/json
| - docling_url | String | 条件 | Docling URLengine=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,

View File

@@ -29,9 +29,16 @@ Content-Type: application/json
| - docling_url | String | 条件必填 | Docling 服务 URLengine=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 | 切片数量 |

View File

@@ -4,12 +4,26 @@
### 2026-03-08
- [ ] **知识库Knowledge BaseAPI** - 后端待实现
- [x] **知识库Knowledge BaseAPI** - 后端已完成 ✔
- 创建知识库、获取列表、获取详情、删除
- 上传文档、删除文档、重新解析
- 获取文档预览内容
- 详细需求:[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)
---
> 需求完成后请完成者打 ✔

View File

@@ -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<any[]>([]) // 知识库文档列表
const loadingDocuments = ref(false)
const fileInput = ref<HTMLInputElement | null>(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<string> => {
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) => {
<el-option label="S3" value="s3" />
</el-select>
</el-form-item>
<!-- MinIO 配置 -->
<template v-if="storageConfig.type === 'minio'">
<el-form-item label="Endpoint">
<el-input v-model="storageConfig.endpoint" placeholder="http://localhost:9000" />
</el-form-item>
<el-form-item label="Access Key ID">
<el-input v-model="storageConfig.accessKeyId" placeholder="Enter Access Key ID" />
</el-form-item>
<el-form-item label="Secret Access Key">
<el-input v-model="storageConfig.secretAccessKey" type="password" placeholder="Enter Secret Access Key" show-password />
</el-form-item>
<el-form-item label="Bucket">
<el-input v-model="storageConfig.bucket" placeholder="Enter Bucket name" />
</el-form-item>
</template>
</el-form>
</div>
</div>
@@ -925,14 +986,21 @@ const deleteDocument = async (docId: string) => {
<i class="fa-solid fa-spinner fa-spin"></i>
<span>Loading preview...</span>
</div>
<!-- 有预览URL时显示PDF -->
<embed
<!-- blob预览URL时显示PDF (使用iframe) -->
<iframe
v-else-if="previewUrl"
type="application/pdf"
:src="previewUrl"
class="pdf-embed"
/>
<!-- 无预览时显示提示 -->
<!-- 无预览但有下载链接时显示下载按钮 -->
<div v-else-if="previewDownloadUrl" class="preview-no-file">
<i class="fa-solid fa-file-pdf"></i>
<span>Cannot preview PDF directly</span>
<a :href="previewDownloadUrl" target="_blank" class="download-link">
<i class="fa-solid fa-download"></i> Download PDF
</a>
</div>
<!-- 无预览也无下载链接时显示提示 -->
<div v-else class="preview-no-file">
<i class="fa-solid fa-file-pdf"></i>
<span>Document preview not available</span>

View File

@@ -77,6 +77,13 @@ export const createKnowledgeBase = async (params: {
enable_pdf?: boolean
pandoc?: boolean
}
storage_config?: {
type: string
endpoint?: string
access_key_id?: string
secret_access_key?: string
bucket?: string
}
}): Promise<{ success: boolean; id?: string; message?: string }> => {
try {
const response = await fetch(`${API_BASE}/api/knowledge/create`, {
@@ -198,3 +205,9 @@ export const getDocumentPreview = async (kbId: string, docId: string, page: numb
return { success: false, message: 'Failed to get document preview' }
}
}
// 获取文件代理URL (用于预览PDF)
export const getFileProxyUrl = (kbId: string, key: string): string => {
const encodedKey = encodeURIComponent(key)
return `${API_BASE}/api/file_proxy?kb_id=${kbId}&key=${encodedKey}`
}