fix(frontend): 补重试解析状态刷新

This commit is contained in:
2026-05-24 23:17:40 +08:00
parent d8079d6277
commit 780abf11f1
4 changed files with 196 additions and 16 deletions

View File

@@ -128,6 +128,10 @@ export function parseRagDocuments(data: RagDocumentParseRequest) {
return post<RagDocumentParseResponse[], RagDocumentParseRequest>('/rag/documents/parse', data); return post<RagDocumentParseResponse[], RagDocumentParseRequest>('/rag/documents/parse', data);
} }
export function retryParseRagDocuments(data: RagDocumentParseRequest) {
return post<RagDocumentParseResponse[], RagDocumentParseRequest>('/rag/documents/retryParse', data);
}
export function chunkRagDocuments(data: RagDocumentChunkRequest) { export function chunkRagDocuments(data: RagDocumentChunkRequest) {
return post<boolean, RagDocumentChunkRequest>('/rag/documents/chunk', data); return post<boolean, RagDocumentChunkRequest>('/rag/documents/chunk', data);
} }

View File

@@ -5,6 +5,7 @@ import { computed, ref, watch } from 'vue';
import { import {
batchUploadRagDocuments, batchUploadRagDocuments,
type RagDocument,
SOURCE_TYPE_RAG, SOURCE_TYPE_RAG,
} from '@/api/ragDocuments'; } from '@/api/ragDocuments';
import type { RagStore } from '@/api/ragStores'; import type { RagStore } from '@/api/ragStores';
@@ -17,7 +18,7 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'update:modelValue', value: boolean): void; (event: 'update:modelValue', value: boolean): void;
(event: 'uploaded'): void; (event: 'uploaded', documentIds: string[]): void;
}>(); }>();
const submitting = ref(false); const submitting = ref(false);
@@ -83,16 +84,19 @@ async function submitUpload() {
submitting.value = true; submitting.value = true;
try { try {
await batchUploadRagDocuments({ const response = await batchUploadRagDocuments({
storeId: uploadStoreId.value, storeId: uploadStoreId.value,
sourceType: SOURCE_TYPE_RAG, sourceType: SOURCE_TYPE_RAG,
files: uploadFiles.value, files: uploadFiles.value,
documentSummary: uploadSummary.value || undefined, documentSummary: uploadSummary.value || undefined,
remark: uploadRemark.value || undefined, remark: uploadRemark.value || undefined,
}); });
const ids = (response.data ?? [])
.map((doc: RagDocument) => doc.id ?? '')
.filter(Boolean);
visible.value = false; visible.value = false;
ElMessage.success('文档已上传'); ElMessage.success('文档已上传');
emit('uploaded'); emit('uploaded', ids);
} finally { } finally {
submitting.value = false; submitting.value = false;
} }

View File

@@ -1,9 +1,9 @@
import { flushPromises, mount } from '@vue/test-utils'; import { flushPromises, mount } from '@vue/test-utils';
import ElementPlus from 'element-plus'; 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 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'; import { queryRagStores } from '@/api/ragStores';
const routeQuery = vi.hoisted(() => ({ storeId: undefined as string | undefined })); 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 })), deleteRagDocument: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
batchUploadRagDocuments: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: [] })), batchUploadRagDocuments: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: [] })),
chunkRagDocuments: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })), 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', () => { describe('RagDocumentsPage', () => {
it('loads documents from query api', async () => { beforeEach(() => {
routeQuery.storeId = undefined; 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, { const wrapper = mount(RagDocumentsPage, {
global: { global: {
plugins: [ElementPlus], plugins: [ElementPlus],
@@ -107,7 +243,6 @@ describe('RagDocumentsPage', () => {
}); });
it('renders document filters as a form-style query bar', async () => { it('renders document filters as a form-style query bar', async () => {
routeQuery.storeId = undefined;
const wrapper = mount(RagDocumentsPage, { const wrapper = mount(RagDocumentsPage, {
global: { global: {
plugins: [ElementPlus], plugins: [ElementPlus],
@@ -139,7 +274,6 @@ describe('RagDocumentsPage', () => {
}); });
it('loads backend detail when editing a row', async () => { it('loads backend detail when editing a row', async () => {
routeQuery.storeId = undefined;
const wrapper = mount(RagDocumentsPage, { const wrapper = mount(RagDocumentsPage, {
global: { global: {
plugins: [ElementPlus], plugins: [ElementPlus],
@@ -154,7 +288,6 @@ describe('RagDocumentsPage', () => {
}); });
it('opens chunk dialog with chunk strategy options from row action', async () => { it('opens chunk dialog with chunk strategy options from row action', async () => {
routeQuery.storeId = undefined;
const wrapper = mount(RagDocumentsPage, { const wrapper = mount(RagDocumentsPage, {
global: { global: {
plugins: [ElementPlus], plugins: [ElementPlus],
@@ -172,7 +305,6 @@ describe('RagDocumentsPage', () => {
}); });
it('submits chunk request with selected chunk strategy', async () => { it('submits chunk request with selected chunk strategy', async () => {
routeQuery.storeId = undefined;
const wrapper = mount(RagDocumentsPage, { const wrapper = mount(RagDocumentsPage, {
global: { global: {
plugins: [ElementPlus], plugins: [ElementPlus],
@@ -195,7 +327,6 @@ describe('RagDocumentsPage', () => {
}); });
it('retries parse for failed document', async () => { it('retries parse for failed document', async () => {
routeQuery.storeId = undefined;
vi.mocked(queryRagDocuments).mockResolvedValueOnce({ vi.mocked(queryRagDocuments).mockResolvedValueOnce({
resultcode: '0', resultcode: '0',
message: null, message: null,
@@ -224,7 +355,7 @@ describe('RagDocumentsPage', () => {
await wrapper.get('[data-test="doc-retry-parse-33"]').trigger('click'); await wrapper.get('[data-test="doc-retry-parse-33"]').trigger('click');
await flushPromises(); await flushPromises();
expect(parseRagDocuments).toHaveBeenCalledWith({ documentIds: ['33'] }); expect(retryParseRagDocuments).toHaveBeenCalledWith({ documentIds: ['33'] });
}); });
it('renders reusable upload dialog with drag upload area', async () => { it('renders reusable upload dialog with drag upload area', async () => {

View File

@@ -8,8 +8,8 @@ import {
chunkRagDocuments, chunkRagDocuments,
deleteRagDocument, deleteRagDocument,
getRagDocumentById, getRagDocumentById,
parseRagDocuments,
queryRagDocuments, queryRagDocuments,
retryParseRagDocuments,
saveRagDocument, saveRagDocument,
RAG_CHUNK_STRATEGY, RAG_CHUNK_STRATEGY,
type RagChunkStrategy, 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() { function handleSearch() {
loadDocs(); loadDocs();
} }
@@ -262,9 +301,11 @@ async function retryParseRows(rows: RagDocument[]) {
} }
retryParsing.value = true; retryParsing.value = true;
try { try {
await parseRagDocuments({ documentIds: ids }); await retryParseRagDocuments({ documentIds: ids });
markRowsParsing(ids);
ElMessage.success('已提交解析重试任务'); ElMessage.success('已提交解析重试任务');
await loadDocs(); await loadDocs();
await refreshParseProgress(ids);
} finally { } finally {
retryParsing.value = false; retryParsing.value = false;
} }
@@ -504,7 +545,7 @@ onMounted(() => {
v-model="uploadDialogVisible" v-model="uploadDialogVisible"
:stores="storeOptions" :stores="storeOptions"
:locked-store-id="queryForm.storeId || null" :locked-store-id="queryForm.storeId || null"
@uploaded="loadDocs" @uploaded="handleUploaded"
/> />
<el-dialog <el-dialog