|
|
@@ -38,24 +38,38 @@
|
|
|
</div>
|
|
|
|
|
|
<div class="kb-tabs-wrapper">
|
|
|
- <DocumentTable
|
|
|
- :kb="activeKB"
|
|
|
- :documents="currentDocuments"
|
|
|
- :total="docTotal"
|
|
|
- :pageSize="docPageSize"
|
|
|
- :currentPage="docPage"
|
|
|
- @upload="openUploadDialog"
|
|
|
- @detail="openDocumentDetail"
|
|
|
- @preview="onPreviewFile"
|
|
|
- @toggle-status="onToggleDocStatus"
|
|
|
- @delete="onDeleteDocument"
|
|
|
- @download="onDownloadDocument"
|
|
|
- @batch-download="onBatchDownload"
|
|
|
- @refresh="loadDocuments"
|
|
|
- @search="onDocSearch"
|
|
|
- @page-change="onDocPageChange"
|
|
|
- @size-change="onDocSizeChange"
|
|
|
- />
|
|
|
+ <el-tabs v-model="activeTab" type="card" class="kb-tabs">
|
|
|
+ <el-tab-pane label="文档管理" name="docs">
|
|
|
+ <DocumentTable
|
|
|
+ :kb="activeKB"
|
|
|
+ :documents="currentDocuments"
|
|
|
+ :total="docTotal"
|
|
|
+ :pageSize="docPageSize"
|
|
|
+ :currentPage="docPage"
|
|
|
+ @upload="openUploadDialog"
|
|
|
+ @detail="openDocumentDetail"
|
|
|
+ @preview="onPreviewFile"
|
|
|
+ @toggle-status="onToggleDocStatus"
|
|
|
+ @delete="onDeleteDocument"
|
|
|
+ @download="onDownloadDocument"
|
|
|
+ @batch-download="onBatchDownload"
|
|
|
+ @refresh="loadDocuments"
|
|
|
+ @search="onDocSearch"
|
|
|
+ @page-change="onDocPageChange"
|
|
|
+ @size-change="onDocSizeChange"
|
|
|
+ />
|
|
|
+ </el-tab-pane>
|
|
|
+ <el-tab-pane label="元数据配置" name="metadata">
|
|
|
+ <MetadataConfig
|
|
|
+ :fields="metadataFields"
|
|
|
+ :sysMetaEnabled="sysMetaEnabled"
|
|
|
+ @create="onCreateField"
|
|
|
+ @update="onUpdateField"
|
|
|
+ @delete="onDeleteField"
|
|
|
+ @toggle-system="onToggleSystemMeta"
|
|
|
+ />
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
</div>
|
|
|
</template>
|
|
|
<div v-else class="kb-empty-state">
|
|
|
@@ -75,9 +89,12 @@
|
|
|
<DocumentDetail
|
|
|
:visible="detailDrawerVisible"
|
|
|
:document="selectedDocument"
|
|
|
+ :metadataFields="metadataFields"
|
|
|
@close="detailDrawerVisible = false"
|
|
|
+ @download="onDownloadDocument"
|
|
|
@toggle-status="onToggleDocStatus"
|
|
|
@delete="onDeleteDocument"
|
|
|
+ @save-meta="onSaveDocMetadata"
|
|
|
/>
|
|
|
</div>
|
|
|
</template>
|
|
|
@@ -87,6 +104,7 @@ import KnowledgeBaseList from './KnowledgeBaseList.vue'
|
|
|
import DocumentTable from './DocumentTable.vue'
|
|
|
import UploadDocument from './UploadDocument.vue'
|
|
|
import DocumentDetail from './DocumentDetail.vue'
|
|
|
+import MetadataConfig from './MetadataConfig.vue'
|
|
|
|
|
|
const TAG_COLORS = [
|
|
|
'#409EFF', '#67C23A', '#E6A23C', '#F56C6C',
|
|
|
@@ -99,7 +117,8 @@ export default {
|
|
|
KnowledgeBaseList,
|
|
|
DocumentTable,
|
|
|
UploadDocument,
|
|
|
- DocumentDetail
|
|
|
+ DocumentDetail,
|
|
|
+ MetadataConfig
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
@@ -115,6 +134,8 @@ export default {
|
|
|
// 全部从接口获取
|
|
|
knowledgeBases: [],
|
|
|
allDocuments: [],
|
|
|
+ metadataFields: [],
|
|
|
+ sysMetaEnabled: true,
|
|
|
docPage: 1,
|
|
|
docPageSize: 20,
|
|
|
docTotal: 0,
|
|
|
@@ -169,6 +190,7 @@ export default {
|
|
|
this.activeKBId = this.knowledgeBases[0].id
|
|
|
this.fetchDocumentList()
|
|
|
this.fetchKBTags()
|
|
|
+ this.fetchMetadata()
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
@@ -201,7 +223,7 @@ export default {
|
|
|
id: item.id || item.document_id,
|
|
|
kbId: this.activeKBId,
|
|
|
name: item.name || item.document_name || item.file_name || '',
|
|
|
- size: this.formatSize(item.data_source_detail_dict && item.data_source_detail_dict.upload_file && item.data_source_detail_dict.upload_file.size),
|
|
|
+ size: this.formatSize(this.getFileSize(item)),
|
|
|
type: (item.type || item.file_type || item.name || '').split('.').pop().toLowerCase(),
|
|
|
version: item.version || 1,
|
|
|
status: (item.display_status === 'available' ? 'enabled' : item.display_status) || (item.enabled === false ? 'disabled' : 'enabled'),
|
|
|
@@ -210,11 +232,53 @@ export default {
|
|
|
? Math.round(item.completed_segments / item.total_segments * 100)
|
|
|
: (item.embedProgress || 0),
|
|
|
uploadTime: this.formatTimestamp(item.created_at || item.createdBy),
|
|
|
- metadata: item.metadata || item.doc_metadata || {}
|
|
|
+ metadata: this.formatMetadata(item.metadata || item.doc_metadata || [])
|
|
|
})) : []
|
|
|
}
|
|
|
},
|
|
|
|
|
|
+ // 获取文件大小(兼容不同接口返回结构)
|
|
|
+ getFileSize(item) {
|
|
|
+ return item.data_source_info?.upload_file?.size
|
|
|
+ || item.data_source_detail_dict?.upload_file?.size
|
|
|
+ || item.size
|
|
|
+ || 0
|
|
|
+ },
|
|
|
+
|
|
|
+ // 格式化元数据为可读格式
|
|
|
+ formatMetadata(metadata) {
|
|
|
+ if (!Array.isArray(metadata)) return metadata
|
|
|
+ const result = {}
|
|
|
+ const labelMap = {
|
|
|
+ 'document_name': '文档名称',
|
|
|
+ 'uploader': '上传者',
|
|
|
+ 'upload_date': '上传时间',
|
|
|
+ 'last_update_date': '更新时间',
|
|
|
+ 'source': '来源'
|
|
|
+ }
|
|
|
+ const sourceMap = {
|
|
|
+ 'file_upload': '文件上传',
|
|
|
+ 'api': 'API导入',
|
|
|
+ 'notion': 'Notion导入',
|
|
|
+ 'web': '网页抓取'
|
|
|
+ }
|
|
|
+ metadata.forEach(item => {
|
|
|
+ const key = item.name
|
|
|
+ let value = item.value
|
|
|
+ // 时间戳转日期
|
|
|
+ if (item.type === 'time' && value) {
|
|
|
+ value = this.formatTimestamp(parseFloat(value))
|
|
|
+ }
|
|
|
+ // 来源转中文
|
|
|
+ if (key === 'source' && sourceMap[value]) {
|
|
|
+ value = sourceMap[value]
|
|
|
+ }
|
|
|
+ const label = labelMap[key] || key
|
|
|
+ result[label] = value
|
|
|
+ })
|
|
|
+ return result
|
|
|
+ },
|
|
|
+
|
|
|
async fetchDocumentDetail(docId) {
|
|
|
const res = await this.apiCall(2026052714301008, {
|
|
|
dataset_id: String(this.activeKBId),
|
|
|
@@ -227,13 +291,13 @@ export default {
|
|
|
this.allDocuments[idx] = {
|
|
|
...this.allDocuments[idx],
|
|
|
name: d.name || d.document_name || this.allDocuments[idx].name,
|
|
|
- size: this.formatSize(d.data_source_detail_dict && d.data_source_detail_dict.upload_file && d.data_source_detail_dict.upload_file.size) || this.allDocuments[idx].size,
|
|
|
+ size: this.formatSize(this.getFileSize(d)) || this.allDocuments[idx].size,
|
|
|
type: (d.type || d.file_type || d.name || '').split('.').pop().toLowerCase(),
|
|
|
version: d.version || this.allDocuments[idx].version,
|
|
|
status: (d.display_status === 'available' ? 'enabled' : d.display_status) || (d.enabled === false ? 'disabled' : (d.enabled === true ? 'enabled' : this.allDocuments[idx].status)),
|
|
|
embedStatus: d.indexing_status || d.embedStatus || this.allDocuments[idx].embedStatus,
|
|
|
embedProgress: d.embedProgress || this.allDocuments[idx].embedProgress,
|
|
|
- metadata: d.metadata || d.doc_metadata || this.allDocuments[idx].metadata
|
|
|
+ metadata: this.formatMetadata(d.doc_metadata || d.metadata || this.allDocuments[idx].metadata)
|
|
|
}
|
|
|
}
|
|
|
this.selectedDocument = { ...this.allDocuments[idx] || this.selectedDocument }
|
|
|
@@ -262,6 +326,7 @@ export default {
|
|
|
this.activeTab = 'docs'
|
|
|
this.fetchDocumentList()
|
|
|
this.fetchKBTags()
|
|
|
+ this.fetchMetadata()
|
|
|
},
|
|
|
|
|
|
|
|
|
@@ -400,24 +465,14 @@ export default {
|
|
|
this.selectedDocument = { ...doc }
|
|
|
this.detailDrawerVisible = true
|
|
|
this.fetchDocumentDetail(doc.id)
|
|
|
- // 获取预览地址
|
|
|
- this.previewUrl = ''
|
|
|
- const res = await this.apiCall(2026052714301010, {
|
|
|
- dataset_id: String(this.activeKBId),
|
|
|
- document_id: String(doc.id)
|
|
|
- })
|
|
|
- if (res && res.code === 1 && res.data && res.data.url) {
|
|
|
- const base = res.difyBaseUrl || ''
|
|
|
- const previewPath = res.data.url.replace('as_attachment=true', 'as_attachment=false')
|
|
|
- this.previewUrl = base + previewPath
|
|
|
- }
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
- async onToggleDocStatus(docId) {
|
|
|
- const doc = this.allDocuments.find(d => d.id === docId)
|
|
|
- if (!doc) return
|
|
|
+ async onToggleDocStatus(docId, callback) {
|
|
|
+ const idx = this.allDocuments.findIndex(d => d.id === docId)
|
|
|
+ if (idx === -1) return
|
|
|
+ const doc = this.allDocuments[idx]
|
|
|
const action = doc.status === 'enabled' ? 'disable' : 'enable'
|
|
|
|
|
|
const ok = await this.apiCall(2026052714301016, {
|
|
|
@@ -426,12 +481,19 @@ export default {
|
|
|
action
|
|
|
})
|
|
|
if (ok && ok.code === 1) {
|
|
|
- doc.status = action === 'enable' ? 'enabled' : 'disabled'
|
|
|
+ const newStatus = action === 'enable' ? 'enabled' : 'disabled'
|
|
|
+ // 用 $set 触发 Vue 2 响应式更新
|
|
|
+ this.$set(this.allDocuments, idx, { ...doc, status: newStatus })
|
|
|
+ // 同步更新详情抽屉的数据
|
|
|
+ if (this.selectedDocument && this.selectedDocument.id === docId) {
|
|
|
+ this.selectedDocument = { ...this.selectedDocument, status: newStatus }
|
|
|
+ }
|
|
|
this.$message.success(action === 'enable' ? '文档已启用' : '文档已禁用')
|
|
|
}
|
|
|
+ callback && callback()
|
|
|
},
|
|
|
|
|
|
- async onDeleteDocument(docId) {
|
|
|
+ async onDeleteDocument(docId, callback) {
|
|
|
const ok = await this.apiCall(2026052714301009, {
|
|
|
dataset_id: String(this.activeKBId),
|
|
|
document_id: String(docId)
|
|
|
@@ -446,6 +508,7 @@ export default {
|
|
|
if (kb && kb.docCount > 0) kb.docCount--
|
|
|
this.$message.success('文档已删除')
|
|
|
}
|
|
|
+ callback && callback()
|
|
|
},
|
|
|
|
|
|
triggerDownload(url, filename) {
|
|
|
@@ -482,6 +545,127 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
+ // ====== 元数据 API ======
|
|
|
+ async fetchMetadata() {
|
|
|
+ if (!this.activeKBId) return
|
|
|
+ const [sysRes, customRes] = await Promise.all([
|
|
|
+ this.apiCall(2026052715313903, { dataset_id: String(this.activeKBId), iscustom: false }),
|
|
|
+ this.apiCall(2026052715313903, { dataset_id: String(this.activeKBId), iscustom: true })
|
|
|
+ ])
|
|
|
+ // Dify 类型 → 前端显示类型
|
|
|
+ const difyToFrontend = (t) => {
|
|
|
+ const map = { string: 'text', number: 'number', time: 'date' }
|
|
|
+ return map[t] || 'text'
|
|
|
+ }
|
|
|
+ const fields = []
|
|
|
+ // 系统元数据:data.fields[]
|
|
|
+ if (sysRes && sysRes.code === 1 && sysRes.data) {
|
|
|
+ const list = sysRes.data.fields || []
|
|
|
+ list.forEach(item => {
|
|
|
+ fields.push({
|
|
|
+ id: item.name,
|
|
|
+ name: item.name,
|
|
|
+ key: item.name,
|
|
|
+ type: difyToFrontend(item.type),
|
|
|
+ options: [],
|
|
|
+ isSystem: true,
|
|
|
+ enabled: true
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ // 自定义元数据:data.doc_metadata[]
|
|
|
+ if (customRes && customRes.code === 1 && customRes.data) {
|
|
|
+ this.sysMetaEnabled = customRes.data.built_in_field_enabled !== false
|
|
|
+ const list = customRes.data.doc_metadata || []
|
|
|
+ list.forEach(item => {
|
|
|
+ fields.push({
|
|
|
+ id: item.id,
|
|
|
+ name: item.name,
|
|
|
+ key: item.name,
|
|
|
+ type: difyToFrontend(item.type),
|
|
|
+ options: [],
|
|
|
+ isSystem: false,
|
|
|
+ enabled: true
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ this.metadataFields = fields
|
|
|
+ },
|
|
|
+
|
|
|
+ async onCreateField(form, callback) {
|
|
|
+ // 前端类型 → Dify 类型映射
|
|
|
+ const typeMap = { text: 'string', date: 'time' }
|
|
|
+ const res = await this.apiCall(2026052715313904, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ name: form.name,
|
|
|
+ type: typeMap[form.type] || 'string'
|
|
|
+ })
|
|
|
+ if (res && res.code === 1) {
|
|
|
+ this.$message.success('元数据创建成功')
|
|
|
+ this.fetchMetadata()
|
|
|
+ }
|
|
|
+ callback && callback()
|
|
|
+ },
|
|
|
+
|
|
|
+ async onUpdateField({ id, name }, callback) {
|
|
|
+ const res = await this.apiCall(2026052715313906, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ metadata_id: String(id),
|
|
|
+ name
|
|
|
+ })
|
|
|
+ if (res && res.code === 1) {
|
|
|
+ this.$message.success('元数据更新成功')
|
|
|
+ this.fetchMetadata()
|
|
|
+ }
|
|
|
+ callback && callback()
|
|
|
+ },
|
|
|
+
|
|
|
+ async onDeleteField(id, callback) {
|
|
|
+ const res = await this.apiCall(2026052715313905, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ metadata_id: String(id)
|
|
|
+ })
|
|
|
+ if (res && res.code === 1) {
|
|
|
+ this.$message.success('元数据删除成功')
|
|
|
+ this.fetchMetadata()
|
|
|
+ }
|
|
|
+ callback && callback()
|
|
|
+ },
|
|
|
+
|
|
|
+ async onToggleSystemMeta() {
|
|
|
+ const res = await this.apiCall(2026052715313907, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ enable: !this.sysMetaEnabled
|
|
|
+ })
|
|
|
+ if (res && res.code === 1) {
|
|
|
+ this.$message.success(this.sysMetaEnabled ? '系统元数据已禁用' : '系统元数据已启用')
|
|
|
+ this.fetchMetadata()
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async onSaveDocMetadata({ docId, metadata }) {
|
|
|
+ const metadataArr = Object.entries(metadata).map(([key, value]) => {
|
|
|
+ const field = this.metadataFields.find(f => f.key === key)
|
|
|
+ return {
|
|
|
+ id: field ? field.id : key,
|
|
|
+ name: field ? field.name : key,
|
|
|
+ value: Array.isArray(value) ? value.join(',') : (value || '')
|
|
|
+ }
|
|
|
+ }).filter(item => item.value)
|
|
|
+
|
|
|
+ const res = await this.apiCall(2026053015572501, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ document_id: String(docId),
|
|
|
+ partial_update: true,
|
|
|
+ metadata: metadataArr
|
|
|
+ })
|
|
|
+ if (res && res.code === 1) {
|
|
|
+ this.$message.success('元数据保存成功')
|
|
|
+ // 重新获取文档详情,确保数据一致
|
|
|
+ this.fetchDocumentDetail(docId)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
|
|
|
|
|
|
// ====== 工具方法 ======
|