From a318a08ab7cc8e7cfd711eeddc83043fc7ae9641 Mon Sep 17 00:00:00 2001 From: wizardchen Date: Fri, 28 Nov 2025 15:32:51 +0800 Subject: [PATCH] refactor: Implement upload progress tracking in KnowledgeBaseList component and enhance error handling for file uploads --- frontend/src/components/menu.vue | 165 +++++++--- frontend/src/i18n/locales/en-US.ts | 11 + frontend/src/i18n/locales/ru-RU.ts | 11 + frontend/src/i18n/locales/zh-CN.ts | 11 + .../src/views/knowledge/KnowledgeBaseList.vue | 297 +++++++++++++++++- graphiti | 1 - internal/agent/prompts.go | 6 +- internal/agent/tools/grep_chunks.go | 13 +- 8 files changed, 465 insertions(+), 50 deletions(-) delete mode 160000 graphiti diff --git a/frontend/src/components/menu.vue b/frontend/src/components/menu.vue index 71e3b3db..27906fc5 100644 --- a/frontend/src/components/menu.vue +++ b/frontend/src/components/menu.vue @@ -678,6 +678,35 @@ const handleDocUploadClick = async () => { docUploadInput.value?.click() } +const FAILED_FILES_PREVIEW_LIMIT = 10 + +const summarizeFailedFiles = (failedFiles: Array<{ name: string; reason: string }>) => { + const duplicateLabel = t('knowledgeBase.fileExists') + let duplicateCount = 0 + const nonDuplicate: Array<{ name: string; reason: string }> = [] + failedFiles.forEach((file) => { + if (file.reason === duplicateLabel) { + duplicateCount++ + } else { + nonDuplicate.push(file) + } + }) + + const previewList = nonDuplicate.slice(0, FAILED_FILES_PREVIEW_LIMIT).map(f => `• ${f.name}: ${f.reason}`) + let nonDuplicateText = '' + if (previewList.length) { + nonDuplicateText = previewList.join('\n') + if (nonDuplicate.length > FAILED_FILES_PREVIEW_LIMIT) { + nonDuplicateText += `\n${t('knowledgeBase.andMoreFiles', { count: nonDuplicate.length - FAILED_FILES_PREVIEW_LIMIT })}` + } + } + + return { + duplicateCount, + nonDuplicateText, + } +} + const handleDocFileChange = async (event: Event) => { const input = event.target as HTMLInputElement const files = input?.files @@ -736,8 +765,7 @@ const handleDocFileChange = async (event: Event) => { detail: { kbId, uploadId, - fileName: file.name, - file + fileName: file.name } })) @@ -809,24 +837,35 @@ const handleDocFileChange = async (event: Event) => { if (failCount === 0) { MessagePlugin.success(t('knowledgeBase.uploadAllSuccess', { count: successCount })) } else if (successCount > 0) { - // 部分成功,显示详细失败信息 - const failedList = failedFiles.map(f => `• ${f.name}: ${f.reason}`).join('\n') + const { duplicateCount, nonDuplicateText } = summarizeFailedFiles(failedFiles) + const extraSections: string[] = [] + if (duplicateCount > 0) { + extraSections.push(t('knowledgeBase.duplicateFilesSkipped', { count: duplicateCount })) + } + if (nonDuplicateText) { + extraSections.push(t('knowledgeBase.failedFilesList') + '\n' + nonDuplicateText) + } + const extraContent = extraSections.length ? '\n\n' + extraSections.join('\n\n') : '' MessagePlugin.warning({ content: t('knowledgeBase.uploadPartialSuccess', { success: successCount, fail: failCount - }) + '\n\n' + t('knowledgeBase.failedFilesList') + '\n' + failedList, + }) + extraContent, duration: 8000, closeBtn: true }) } else { - // 全部失败,显示详细失败信息 - const failedList = failedFiles.slice(0, 5).map(f => `• ${f.name}: ${f.reason}`).join('\n') - const moreCount = failedFiles.length > 5 ? failedFiles.length - 5 : 0 + const { duplicateCount, nonDuplicateText } = summarizeFailedFiles(failedFiles) + const extraSections: string[] = [] + if (duplicateCount > 0) { + extraSections.push(t('knowledgeBase.duplicateFilesSkipped', { count: duplicateCount })) + } + if (nonDuplicateText) { + extraSections.push(t('knowledgeBase.failedFilesList') + '\n' + nonDuplicateText) + } + const extraContent = extraSections.length ? '\n\n' + extraSections.join('\n\n') : '' MessagePlugin.error({ - content: t('knowledgeBase.uploadAllFailed') + '\n\n' + - t('knowledgeBase.failedFilesList') + '\n' + failedList + - (moreCount > 0 ? `\n${t('knowledgeBase.andMoreFiles', { count: moreCount })}` : ''), + content: t('knowledgeBase.uploadAllFailed') + extraContent, duration: 8000, closeBtn: true }) @@ -950,32 +989,69 @@ const handleDocFolderChange = async (event: Event) => { const failedFiles: Array<{ name: string; reason: string }> = [] for (const file of validFiles) { - try { - // 获取文件的相对路径(webkitRelativePath) - const relativePath = (file as any).webkitRelativePath - let fileName = file.name - - // 如果存在相对路径,提取子文件夹路径并拼接到文件名前 - if (relativePath) { - // webkitRelativePath 格式: "文件夹名/子文件夹/文件名.ext" - // 我们需要去掉第一层文件夹名,保留子路径 - const pathParts = relativePath.split('/') - if (pathParts.length > 2) { - // 有子文件夹,拼接子路径到文件名 - const subPath = pathParts.slice(1, -1).join('/') // 去掉顶层文件夹和文件名本身 - fileName = `${subPath}/${file.name}` - } + // 获取文件的相对路径(webkitRelativePath),用于保留子目录结构 + const relativePath = (file as any).webkitRelativePath + let fileName = file.name + if (relativePath) { + const pathParts = relativePath.split('/') + if (pathParts.length > 2) { + const subPath = pathParts.slice(1, -1).join('/') + fileName = `${subPath}/${file.name}` } - - await uploadKnowledgeFile(kbId, { file, fileName }) + } + + const uploadId = `${file.name}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + let progress = 0 + let status: 'uploading' | 'success' | 'error' = 'uploading' + let errorReason: string | undefined + + window.dispatchEvent(new CustomEvent('knowledgeFileUploadStart', { + detail: { + kbId, + uploadId, + fileName + } + })) + + try { + await uploadKnowledgeFile( + kbId, + { file, fileName }, + (progressEvent: any) => { + if (progressEvent?.total) { + progress = Math.round((progressEvent.loaded * 100) / progressEvent.total) + window.dispatchEvent(new CustomEvent('knowledgeFileUploadProgress', { + detail: { + kbId, + uploadId, + progress + } + })) + } + } + ) successCount++ + status = 'success' + progress = 100 } catch (error: any) { failCount++ - let errorReason = error?.error?.message || error?.message || t('knowledgeBase.uploadFailed') + errorReason = error?.error?.message || error?.message || t('knowledgeBase.uploadFailed') if (error?.code === 'duplicate_file' || error?.error?.code === 'duplicate_file') { errorReason = t('knowledgeBase.fileExists') } - failedFiles.push({ name: file.name, reason: errorReason }) + failedFiles.push({ name: fileName, reason: errorReason }) + status = 'error' + } finally { + window.dispatchEvent(new CustomEvent('knowledgeFileUploadComplete', { + detail: { + kbId, + uploadId, + status, + progress, + error: errorReason, + fileName + } + })) } } @@ -988,24 +1064,35 @@ const handleDocFolderChange = async (event: Event) => { if (failCount === 0) { MessagePlugin.success(t('knowledgeBase.uploadAllSuccess', { count: successCount })) } else if (successCount > 0) { - // 部分成功,显示详细失败信息 - const failedList = failedFiles.map(f => `• ${f.name}: ${f.reason}`).join('\n') + const { duplicateCount, nonDuplicateText } = summarizeFailedFiles(failedFiles) + const extraSections: string[] = [] + if (duplicateCount > 0) { + extraSections.push(t('knowledgeBase.duplicateFilesSkipped', { count: duplicateCount })) + } + if (nonDuplicateText) { + extraSections.push(t('knowledgeBase.failedFilesList') + '\n' + nonDuplicateText) + } + const extraContent = extraSections.length ? '\n\n' + extraSections.join('\n\n') : '' MessagePlugin.warning({ content: t('knowledgeBase.uploadPartialSuccess', { success: successCount, fail: failCount - }) + '\n\n' + t('knowledgeBase.failedFilesList') + '\n' + failedList, + }) + extraContent, duration: 8000, closeBtn: true }) } else { - // 全部失败,显示详细失败信息 - const failedList = failedFiles.slice(0, 5).map(f => `• ${f.name}: ${f.reason}`).join('\n') - const moreCount = failedFiles.length > 5 ? failedFiles.length - 5 : 0 + const { duplicateCount, nonDuplicateText } = summarizeFailedFiles(failedFiles) + const extraSections: string[] = [] + if (duplicateCount > 0) { + extraSections.push(t('knowledgeBase.duplicateFilesSkipped', { count: duplicateCount })) + } + if (nonDuplicateText) { + extraSections.push(t('knowledgeBase.failedFilesList') + '\n' + nonDuplicateText) + } + const extraContent = extraSections.length ? '\n\n' + extraSections.join('\n\n') : '' MessagePlugin.error({ - content: t('knowledgeBase.uploadAllFailed') + '\n\n' + - t('knowledgeBase.failedFilesList') + '\n' + failedList + - (moreCount > 0 ? `\n${t('knowledgeBase.andMoreFiles', { count: moreCount })}` : ''), + content: t('knowledgeBase.uploadAllFailed') + extraContent, duration: 8000, closeBtn: true }) diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index fb788f99..c2e6a192 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -65,6 +65,7 @@ export default { unsupportedFileType: 'Unsupported file type', failedFilesList: 'Failed files:', andMoreFiles: '...and {count} more files', + duplicateFilesSkipped: '{count} duplicate files skipped', uploadFile: 'Upload File', uploadFileDesc: 'Supports PDF, Word, TXT, etc.', importURL: 'Import from URL', @@ -812,6 +813,16 @@ export default { documents: 'Document Count', faqEntries: 'FAQ Entries', chunks: 'Chunk Count' + }, + uploadProgress: { + uploadingTitle: 'Uploading folder documents to "{name}"', + detail: '{completed}/{total} files finished', + keepPageOpen: 'Please keep this page open while files upload.', + completedTitle: 'Upload finished for "{name}"', + completedDetail: 'All {total} files uploaded. Refreshing list to show parsing status...', + refreshing: 'Refreshing list to show parsing status...', + errorTip: 'Some files failed to upload. Please check the notifications.', + unknownKb: 'Knowledge Base {id}' } }, knowledgeEditor: { diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index 4d94ef82..60d2d4be 100644 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -65,6 +65,7 @@ export default { unsupportedFileType: 'Неподдерживаемый тип файла', failedFilesList: 'Неудавшиеся файлы:', andMoreFiles: '...и ещё {count} файлов', + duplicateFilesSkipped: 'Пропущено {count} повторяющихся файлов', uploadFile: 'Загрузить файл', uploadFileDesc: 'Поддерживает PDF, Word, TXT и т.д.', importURL: 'Импорт из URL', @@ -900,6 +901,16 @@ export default { documents: 'Количество документов', faqEntries: 'FAQ записи', chunks: 'Количество фрагментов' + }, + uploadProgress: { + uploadingTitle: 'Загрузка документов папки в «{name}»', + detail: 'Готово {completed} из {total} файлов', + keepPageOpen: 'Пожалуйста, не закрывайте страницу, пока идет загрузка.', + completedTitle: 'Загрузка для «{name}» завершена', + completedDetail: 'Загружено {total} файлов. Обновляем список, чтобы показать статус разбора...', + refreshing: 'Обновляем список и статусы разбора...', + errorTip: 'Часть файлов загрузить не удалось. Проверьте уведомления.', + unknownKb: 'База знаний {id}' } }, knowledgeEditor: { diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 3f739831..9eda1604 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -64,6 +64,7 @@ export default { unsupportedFileType: "不支持的文件格式", failedFilesList: "失败文件列表:", andMoreFiles: "...及其他 {count} 个文件", + duplicateFilesSkipped: "已忽略 {count} 个重复文件", uploadFile: "上传文件", uploadFileDesc: "支持 PDF、Word、TXT 等", importURL: "导入网页", @@ -1136,6 +1137,16 @@ export default { faqEntries: "问答条目", chunks: "分块数量", }, + uploadProgress: { + uploadingTitle: "正在向「{name}」上传文件夹中的文档", + detail: "已完成 {completed}/{total} 个文件", + keepPageOpen: "请保持页面打开,上传完成后会自动刷新解析状态。", + completedTitle: "「{name}」的上传已完成", + completedDetail: "共上传 {total} 个文件,正在刷新列表查看解析状态...", + refreshing: "正在刷新列表并获取最新解析状态...", + errorTip: "部分文件上传失败,请查看右上角通知详情。", + unknownKb: "知识库 {id}", + }, }, knowledgeEditor: { titleCreate: "新建知识库", diff --git a/frontend/src/views/knowledge/KnowledgeBaseList.vue b/frontend/src/views/knowledge/KnowledgeBaseList.vue index 4545bde7..fdf373a7 100644 --- a/frontend/src/views/knowledge/KnowledgeBaseList.vue +++ b/frontend/src/views/knowledge/KnowledgeBaseList.vue @@ -15,6 +15,48 @@ {{ $t('knowledgeList.uninitializedBanner') }} + +
+
+
+ +
+
+
+ {{ + summary.completed === summary.total + ? $t('knowledgeList.uploadProgress.completedTitle', { name: summary.kbName }) + : $t('knowledgeList.uploadProgress.uploadingTitle', { name: summary.kbName }) + }} +
+
+ {{ + summary.completed === summary.total + ? $t('knowledgeList.uploadProgress.completedDetail', { total: summary.total }) + : $t('knowledgeList.uploadProgress.detail', { completed: summary.completed, total: summary.total }) + }} +
+
+ {{ + summary.completed === summary.total + ? $t('knowledgeList.uploadProgress.refreshing') + : $t('knowledgeList.uploadProgress.keepPageOpen') + }} +
+
+ {{ $t('knowledgeList.uploadProgress.errorTip') }} +
+
+
+
+
+
+
+