From 780abf11f1368be0df509e330624481a90d2fbf6 Mon Sep 17 00:00:00 2001 From: bruce Date: Sun, 24 May 2026 23:17:40 +0800 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20=E8=A1=A5=E9=87=8D=E8=AF=95?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E7=8A=B6=E6=80=81=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/ragDocuments.ts | 4 + .../rag/RagDocumentBatchUploadDialog.vue | 10 +- .../rag/__tests__/RagDocumentsPage.spec.ts | 151 ++++++++++++++++-- .../pages/rag/documents/RagDocumentsPage.vue | 47 +++++- 4 files changed, 196 insertions(+), 16 deletions(-) diff --git a/frontend/src/api/ragDocuments.ts b/frontend/src/api/ragDocuments.ts index 600be32..8b13404 100644 --- a/frontend/src/api/ragDocuments.ts +++ b/frontend/src/api/ragDocuments.ts @@ -128,6 +128,10 @@ export function parseRagDocuments(data: RagDocumentParseRequest) { return post('/rag/documents/parse', data); } +export function retryParseRagDocuments(data: RagDocumentParseRequest) { + return post('/rag/documents/retryParse', data); +} + export function chunkRagDocuments(data: RagDocumentChunkRequest) { return post('/rag/documents/chunk', data); } diff --git a/frontend/src/components/rag/RagDocumentBatchUploadDialog.vue b/frontend/src/components/rag/RagDocumentBatchUploadDialog.vue index 168986e..f766578 100644 --- a/frontend/src/components/rag/RagDocumentBatchUploadDialog.vue +++ b/frontend/src/components/rag/RagDocumentBatchUploadDialog.vue @@ -5,6 +5,7 @@ import { computed, ref, watch } from 'vue'; import { batchUploadRagDocuments, + type RagDocument, SOURCE_TYPE_RAG, } from '@/api/ragDocuments'; import type { RagStore } from '@/api/ragStores'; @@ -17,7 +18,7 @@ const props = defineProps<{ const emit = defineEmits<{ (event: 'update:modelValue', value: boolean): void; - (event: 'uploaded'): void; + (event: 'uploaded', documentIds: string[]): void; }>(); const submitting = ref(false); @@ -83,16 +84,19 @@ async function submitUpload() { submitting.value = true; try { - await batchUploadRagDocuments({ + const response = await batchUploadRagDocuments({ storeId: uploadStoreId.value, sourceType: SOURCE_TYPE_RAG, files: uploadFiles.value, documentSummary: uploadSummary.value || undefined, remark: uploadRemark.value || undefined, }); + const ids = (response.data ?? []) + .map((doc: RagDocument) => doc.id ?? '') + .filter(Boolean); visible.value = false; ElMessage.success('文档已上传'); - emit('uploaded'); + emit('uploaded', ids); } finally { submitting.value = false; } diff --git a/frontend/src/pages/rag/__tests__/RagDocumentsPage.spec.ts b/frontend/src/pages/rag/__tests__/RagDocumentsPage.spec.ts index a83c642..7affdc6 100644 --- a/frontend/src/pages/rag/__tests__/RagDocumentsPage.spec.ts +++ b/frontend/src/pages/rag/__tests__/RagDocumentsPage.spec.ts @@ -1,9 +1,9 @@ import { flushPromises, mount } from '@vue/test-utils'; import ElementPlus from 'element-plus'; -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import RagDocumentsPage from '../RagDocumentsPage.vue'; -import { chunkRagDocuments, getRagDocumentById, parseRagDocuments, queryRagDocuments } from '@/api/ragDocuments'; +import { chunkRagDocuments, getRagDocumentById, queryRagDocuments, retryParseRagDocuments } from '@/api/ragDocuments'; import { queryRagStores } from '@/api/ragStores'; const routeQuery = vi.hoisted(() => ({ storeId: undefined as string | undefined })); @@ -86,12 +86,148 @@ vi.mock('@/api/ragDocuments', () => ({ deleteRagDocument: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })), batchUploadRagDocuments: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: [] })), chunkRagDocuments: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })), - parseRagDocuments: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: [] })), + retryParseRagDocuments: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: [] })), })); describe('RagDocumentsPage', () => { - it('loads documents from query api', async () => { + beforeEach(() => { routeQuery.storeId = undefined; + vi.useRealTimers(); + + vi.mocked(queryRagDocuments).mockReset(); + vi.mocked(queryRagDocuments).mockImplementation((query?: { storeId?: string }) => { + const rows = [ + { + id: '11', + storeId: '1', + attachmentId: '101', + documentTitle: '产品制度总则', + documentSummary: '制度摘要', + parseStatus: 'UPLOADED', + indexStatus: 'PENDING', + enabled: true, + remark: '制度文档', + createTime: '2026-05-21 10:00:00', + }, + { + id: '22', + storeId: '2', + attachmentId: '202', + documentTitle: 'FAQ 手册', + documentSummary: 'FAQ 摘要', + parseStatus: 'PARSED', + indexStatus: 'INDEXED', + enabled: false, + remark: '常见问题', + createTime: '2026-05-21 11:00:00', + }, + ]; + const data = query?.storeId ? rows.filter((row) => row.storeId === query.storeId) : rows; + return Promise.resolve({ resultcode: '0', message: null, data }); + }); + + vi.mocked(retryParseRagDocuments).mockReset(); + vi.mocked(retryParseRagDocuments).mockResolvedValue({ resultcode: '0', message: null, data: [] }); + + vi.mocked(chunkRagDocuments).mockReset(); + vi.mocked(chunkRagDocuments).mockResolvedValue({ resultcode: '0', message: null, data: true }); + + vi.mocked(getRagDocumentById).mockReset(); + vi.mocked(getRagDocumentById).mockImplementation((id: string) => + Promise.resolve({ + resultcode: '0', + message: null, + data: { + id, + storeId: '2', + attachmentId: '202', + documentTitle: 'FAQ 手册', + documentSummary: 'FAQ 摘要', + enabled: false, + remark: '常见问题', + }, + }), + ); + }); + + it('keeps polling document status after retry parse is submitted', async () => { + vi.useFakeTimers(); + vi.mocked(queryRagDocuments) + .mockResolvedValueOnce({ + resultcode: '0', + message: null, + data: [ + { + id: '33', + storeId: '1', + attachmentId: '303', + documentTitle: '失败文档', + documentSummary: '失败摘要', + parseStatus: 'FAILED', + indexStatus: 'PENDING', + enabled: true, + remark: '失败', + createTime: '2026-05-21 11:00:00', + }, + ], + }) + .mockResolvedValueOnce({ + resultcode: '0', + message: null, + data: [ + { + id: '33', + storeId: '1', + attachmentId: '303', + documentTitle: '失败文档', + documentSummary: '失败摘要', + parseStatus: 'PARSING', + indexStatus: 'PENDING', + enabled: true, + remark: '失败', + createTime: '2026-05-21 11:00:00', + }, + ], + }) + .mockResolvedValue({ + resultcode: '0', + message: null, + data: [ + { + id: '33', + storeId: '1', + attachmentId: '303', + documentTitle: '失败文档', + documentSummary: '失败摘要', + parseStatus: 'PARSED', + indexStatus: 'PENDING', + enabled: true, + remark: '失败', + createTime: '2026-05-21 11:00:00', + }, + ], + }); + + const wrapper = mount(RagDocumentsPage, { + global: { + plugins: [ElementPlus], + }, + }); + + await flushPromises(); + await wrapper.get('[data-test="doc-retry-parse-33"]').trigger('click'); + await flushPromises(); + + await vi.advanceTimersByTimeAsync(1500); + await flushPromises(); + + expect(retryParseRagDocuments).toHaveBeenCalledWith({ documentIds: ['33'] }); + expect(vi.mocked(queryRagDocuments)).toHaveBeenCalledTimes(3); + + vi.useRealTimers(); + }); + + it('loads documents from query api', async () => { const wrapper = mount(RagDocumentsPage, { global: { plugins: [ElementPlus], @@ -107,7 +243,6 @@ describe('RagDocumentsPage', () => { }); it('renders document filters as a form-style query bar', async () => { - routeQuery.storeId = undefined; const wrapper = mount(RagDocumentsPage, { global: { plugins: [ElementPlus], @@ -139,7 +274,6 @@ describe('RagDocumentsPage', () => { }); it('loads backend detail when editing a row', async () => { - routeQuery.storeId = undefined; const wrapper = mount(RagDocumentsPage, { global: { plugins: [ElementPlus], @@ -154,7 +288,6 @@ describe('RagDocumentsPage', () => { }); it('opens chunk dialog with chunk strategy options from row action', async () => { - routeQuery.storeId = undefined; const wrapper = mount(RagDocumentsPage, { global: { plugins: [ElementPlus], @@ -172,7 +305,6 @@ describe('RagDocumentsPage', () => { }); it('submits chunk request with selected chunk strategy', async () => { - routeQuery.storeId = undefined; const wrapper = mount(RagDocumentsPage, { global: { plugins: [ElementPlus], @@ -195,7 +327,6 @@ describe('RagDocumentsPage', () => { }); it('retries parse for failed document', async () => { - routeQuery.storeId = undefined; vi.mocked(queryRagDocuments).mockResolvedValueOnce({ resultcode: '0', message: null, @@ -224,7 +355,7 @@ describe('RagDocumentsPage', () => { await wrapper.get('[data-test="doc-retry-parse-33"]').trigger('click'); await flushPromises(); - expect(parseRagDocuments).toHaveBeenCalledWith({ documentIds: ['33'] }); + expect(retryParseRagDocuments).toHaveBeenCalledWith({ documentIds: ['33'] }); }); it('renders reusable upload dialog with drag upload area', async () => { diff --git a/frontend/src/pages/rag/documents/RagDocumentsPage.vue b/frontend/src/pages/rag/documents/RagDocumentsPage.vue index d9e1a81..9b12ccf 100644 --- a/frontend/src/pages/rag/documents/RagDocumentsPage.vue +++ b/frontend/src/pages/rag/documents/RagDocumentsPage.vue @@ -8,8 +8,8 @@ import { chunkRagDocuments, deleteRagDocument, getRagDocumentById, - parseRagDocuments, queryRagDocuments, + retryParseRagDocuments, saveRagDocument, RAG_CHUNK_STRATEGY, type RagChunkStrategy, @@ -116,6 +116,45 @@ async function loadDocs() { } } +async function refreshParseProgress(documentIds: string[]) { + if (documentIds.length === 0) { + return; + } + let rounds = 0; + while (rounds < 6) { + await new Promise((resolve) => setTimeout(resolve, 1500)); + await loadDocs(); + const targetDocs = docRows.value.filter((row) => documentIds.includes(String(row.id ?? ''))); + const allDone = targetDocs.every((row) => row.parseStatus === 'PARSED' || row.parseStatus === 'FAILED'); + if (allDone) { + return; + } + rounds += 1; + } +} + +function markRowsParsing(documentIds: string[]) { + if (documentIds.length === 0) { + return; + } + const idSet = new Set(documentIds); + docRows.value = docRows.value.map((row) => { + if (!idSet.has(String(row.id ?? ''))) { + return row; + } + return { + ...row, + parseStatus: 'PARSING', + errorMessage: null, + }; + }); +} + +async function handleUploaded(documentIds: string[]) { + await loadDocs(); + await refreshParseProgress(documentIds); +} + function handleSearch() { loadDocs(); } @@ -262,9 +301,11 @@ async function retryParseRows(rows: RagDocument[]) { } retryParsing.value = true; try { - await parseRagDocuments({ documentIds: ids }); + await retryParseRagDocuments({ documentIds: ids }); + markRowsParsing(ids); ElMessage.success('已提交解析重试任务'); await loadDocs(); + await refreshParseProgress(ids); } finally { retryParsing.value = false; } @@ -504,7 +545,7 @@ onMounted(() => { v-model="uploadDialogVisible" :stores="storeOptions" :locked-store-id="queryForm.storeId || null" - @uploaded="loadDocs" + @uploaded="handleUploaded" />