From 97b7bb9890c6323694cfe120d2c8b0441a749e8e Mon Sep 17 00:00:00 2001 From: wizardchen Date: Thu, 25 Jun 2026 23:51:38 +0800 Subject: [PATCH] fix(knowledge): harden batch reparse UX and async processing Move batch reparse work to an asynq task to avoid HTTP timeouts, add i18n for all locales, guard in-flight documents, and prevent concurrent delete/reparse. --- frontend/src/api/knowledge-base/index.ts | 4 +- frontend/src/i18n/locales/en-US.ts | 5 + frontend/src/i18n/locales/ko-KR.ts | 5 + frontend/src/i18n/locales/ru-RU.ts | 5 + frontend/src/i18n/locales/zh-CN.ts | 5 + .../src/views/knowledge/KnowledgeBase.vue | 181 +++++++----------- .../knowledge/components/DocumentBatchBar.vue | 61 ++---- .../application/service/knowledge_process.go | 35 ++++ internal/handler/knowledge.go | 97 ++++++---- internal/router/sync_task.go | 1 + internal/router/task.go | 3 + internal/types/interfaces/knowledge.go | 2 + internal/types/task.go | 9 + 13 files changed, 214 insertions(+), 199 deletions(-) diff --git a/frontend/src/api/knowledge-base/index.ts b/frontend/src/api/knowledge-base/index.ts index e532d9b8..c12e1661 100644 --- a/frontend/src/api/knowledge-base/index.ts +++ b/frontend/src/api/knowledge-base/index.ts @@ -482,10 +482,10 @@ export function knowledgeSemanticSearch(data: { return post('/api/v1/knowledge-search', data); } -export function batchReparseKnowledge(kbId: string, ids: string[], processConfig?: any) { +export function batchReparseKnowledge(kbId: string, ids: string[], processConfig?: KnowledgeProcessOverrides) { return post(`/api/v1/knowledge/batch-reparse`, { kb_id: kbId, ids, - process_config: processConfig + process_config: processConfig, }); } \ No newline at end of file diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index aa541590..81245c9c 100755 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -575,6 +575,11 @@ export default { confirmBatchDeleteDocument: 'Delete {count} selected documents? This action cannot be undone.', batchDeleteSuccess: 'Deleted {count} documents', batchDeleteFailed: 'Batch delete failed', + confirmBatchReparseDocument: 'Rebuild {count} selected documents? Existing content will be cleared and each document will be re-parsed.', + confirmBatchReparse: 'Confirm and reparse', + batchReparseSuccess: 'Submitted {count} rebuild tasks', + batchReparseFailed: 'Batch rebuild failed', + batchReparseSkippedInFlight: 'Skipped {count} document(s) still being parsed', statusCompleted: 'Completed', statusProcessing: 'Processing', statusFinalizing: 'Optimizing', diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts index 8690e40b..16993a61 100755 --- a/frontend/src/i18n/locales/ko-KR.ts +++ b/frontend/src/i18n/locales/ko-KR.ts @@ -578,6 +578,11 @@ export default { confirmBatchDeleteDocument: "선택한 {count}개 문서를 삭제하시겠습니까? 삭제 후 복구할 수 없습니다.", batchDeleteSuccess: "{count}개 문서가 삭제되었습니다", batchDeleteFailed: "일괄 삭제 실패", + confirmBatchReparseDocument: "선택한 {count}개 문서를 재구축하시겠습니까? 기존 내용이 삭제되고 각 문서가 다시 파싱됩니다.", + confirmBatchReparse: "확인 후 재파싱", + batchReparseSuccess: "재구축 작업 {count}개가 제출되었습니다", + batchReparseFailed: "일괄 재구축 실패", + batchReparseSkippedInFlight: "파싱 중인 문서 {count}개를 건너뛰었습니다", statusCompleted: "완료", statusProcessing: "처리 중", statusFinalizing: "최적화 중", diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index 29ebe476..c5209934 100755 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -500,6 +500,11 @@ export default { confirmBatchDeleteDocument: 'Удалить {count} выбранных документов? Это действие нельзя отменить.', batchDeleteSuccess: 'Удалено документов: {count}', batchDeleteFailed: 'Ошибка пакетного удаления', + confirmBatchReparseDocument: 'Пересобрать {count} выбранных документов? Существующее содержимое будет удалено, и каждый документ будет обработан заново.', + confirmBatchReparse: 'Подтвердить и обработать заново', + batchReparseSuccess: 'Отправлено задач пересборки: {count}', + batchReparseFailed: 'Ошибка пакетной пересборки', + batchReparseSkippedInFlight: 'Пропущено документов, которые ещё обрабатываются: {count}', statusCompleted: 'Завершено', statusProcessing: 'Обработка', statusFinalizing: 'Оптимизация', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index fd227988..578a9160 100755 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -576,6 +576,11 @@ export default { confirmBatchDeleteDocument: "确认删除选中的 {count} 个文档?删除后将无法恢复。", batchDeleteSuccess: "成功删除 {count} 个文档", batchDeleteFailed: "批量删除失败", + confirmBatchReparseDocument: "确认重建选中的 {count} 个文档?现有内容将被清除并重新解析。", + confirmBatchReparse: "确认并重新解析", + batchReparseSuccess: "已提交 {count} 个重建任务", + batchReparseFailed: "批量重建失败", + batchReparseSkippedInFlight: "已跳过 {count} 个正在解析中的文档", statusCompleted: "已完成", statusProcessing: "解析中", statusFinalizing: "优化中", diff --git a/frontend/src/views/knowledge/KnowledgeBase.vue b/frontend/src/views/knowledge/KnowledgeBase.vue index b1f6deb1..a5500875 100644 --- a/frontend/src/views/knowledge/KnowledgeBase.vue +++ b/frontend/src/views/knowledge/KnowledgeBase.vue @@ -61,7 +61,7 @@ import { formatFileSize } from '@/utils/files'; import { useMarqueeSelect } from '@/hooks/useMarqueeSelect'; import type { ParserEngineInfo } from '@/api/system'; const route = useRoute(); -const { t, te } = useI18n(); +const { t } = useI18n(); const kbId = computed(() => (route.params as any).kbId as string || ''); const kbInfo = ref(null); const uploadSourceRef = ref | null>(null); @@ -419,28 +419,34 @@ const batchDeleting = ref(false); const batchReparsing = ref(false); const confirmBatchReparse = async () => { - if (batchReparsing.value || selectedIds.value.size === 0) return; - const ids = Array.from(selectedIds.value); + if (batchReparsing.value || batchDeleting.value || selectedIds.value.size === 0) return; + const allIds = Array.from(selectedIds.value); + const ids = allIds.filter((id) => { + const item = cardList.value.find((c) => c.id === id); + return !item || !isParseInFlight(item.parse_status); + }); + const skipped = allIds.length - ids.length; + if (ids.length === 0) { + MessagePlugin.info(t('knowledgeBase.rebuildInProgress')); + return; + } + if (skipped > 0) { + MessagePlugin.warning(t('knowledgeBase.batchReparseSkippedInFlight', { count: skipped })); + } batchReparsing.value = true; try { const res: any = await batchReparseKnowledge(kbId.value, ids); if (res?.success) { - if (res.partial && res.failed_ids && res.failed_ids.length > 0) { - MessagePlugin.warning(`部分重构任务提交成功,失败 ${res.failed_ids.length} 个`); - } else { - const successMsg = te('knowledgeBase.batchReparseSuccess') - ? t('knowledgeBase.batchReparseSuccess', { count: ids.length }) - : `已提交 ${ids.length} 个重构任务`; - MessagePlugin.success(successMsg); - } + MessagePlugin.success(t('knowledgeBase.batchReparseSuccess', { count: ids.length })); clearSelection(); batchMode.value = false; loadKnowledgeFiles(kbId.value); + scheduleWikiStatusProbes(); } else { - MessagePlugin.error(res?.message || '批量重建失败'); + MessagePlugin.error(res?.message || t('knowledgeBase.batchReparseFailed')); } } catch (e: any) { - MessagePlugin.error(e?.message || '批量重建失败'); + MessagePlugin.error(e?.message || t('knowledgeBase.batchReparseFailed')); } finally { batchReparsing.value = false; } @@ -1897,7 +1903,7 @@ const onCardClick = (item: KnowledgeCard) => { }; const confirmBatchDelete = async () => { - if (batchDeleting.value || selectedIds.value.size === 0) return; + if (batchDeleting.value || batchReparsing.value || selectedIds.value.size === 0) return; const ids = Array.from(selectedIds.value); const deletedIdSet = new Set(ids); batchDeleting.value = true; @@ -2149,8 +2155,7 @@ async function createNewSession(value: string): Promise { #
@@ -2267,31 +2272,19 @@ async function createNewSession(value: string): Promise {
- +
-
-
+