fix(frontend): 补重试解析状态刷新
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user