fix(frontend): 补重试解析状态刷新
This commit is contained in:
@@ -128,6 +128,10 @@ export function parseRagDocuments(data: RagDocumentParseRequest) {
|
||||
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) {
|
||||
return post<boolean, RagDocumentChunkRequest>('/rag/documents/chunk', data);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
|
||||
Reference in New Issue
Block a user