feat: 展示分块索引同步重试状态

- 返回当前版本任务的真实尝试次数与重试原因,区分模型服务和索引写入失败

- 分离提示与按钮布局,保持操作位置稳定,补充分块状态回归测试
This commit is contained in:
2026-09-08 15:05:29 +08:00
parent b5a355247b
commit d506d06f77
10 changed files with 408 additions and 14 deletions

View File

@@ -282,6 +282,10 @@
"discardChanges": "Discard changes",
"chunkSourceFallback": "Switched to source editing",
"chunkSyncPending": "Updating search index",
"chunkSyncEmbeddingRetry": "Retrying embedding service ({attempt}/{maxAttempts})",
"chunkSyncVectorRetry": "Retrying vector index ({attempt}/{maxAttempts})",
"chunkSyncKeywordRetry": "Retrying keyword index ({attempt}/{maxAttempts})",
"chunkSyncIndexRetry": "Retrying search index ({attempt}/{maxAttempts})",
"chunkSyncSucceeded": "Search index updated",
"chunkSyncFailed": "Index sync failed. Click to retry",
"chunkSyncRetryFailed": "Failed to retry index sync",

View File

@@ -282,6 +282,10 @@
"discardChanges": "放弃修改",
"chunkSourceFallback": "已切换到源码编辑",
"chunkSyncPending": "正在更新检索索引",
"chunkSyncEmbeddingRetry": "向量模型服务重试中({attempt}/{maxAttempts}",
"chunkSyncVectorRetry": "向量索引重试中({attempt}/{maxAttempts}",
"chunkSyncKeywordRetry": "关键词索引重试中({attempt}/{maxAttempts}",
"chunkSyncIndexRetry": "检索索引重试中({attempt}/{maxAttempts}",
"chunkSyncSucceeded": "检索索引已更新",
"chunkSyncFailed": "索引同步失败,点击重试",
"chunkSyncRetryFailed": "索引同步重试失败",

View File

@@ -44,7 +44,7 @@ vi.mock('element-plus', async (importOriginal) => {
vi.mock('#/api/request', () => ({ api: {} }));
vi.mock('@easyflow/locales', () => ({
$t: (key: string) => {
$t: (key: string, params: Record<string, number> = {}) => {
const messages: Record<string, string> = {
'documentCollection.continueEditing': '继续编辑',
'documentCollection.deleteChunk': '删除分块',
@@ -54,8 +54,18 @@ vi.mock('@easyflow/locales', () => ({
'documentCollection.chunkSyncSucceeded': '检索索引已更新',
'documentCollection.chunkSyncFailed': '索引同步失败,点击重试',
'documentCollection.chunkSyncRetryFailed': '索引同步重试失败',
'documentCollection.chunkSyncEmbeddingRetry':
'向量模型服务重试中({attempt}/{maxAttempts}',
'documentCollection.chunkSyncVectorRetry':
'向量索引重试中({attempt}/{maxAttempts}',
'documentCollection.chunkSyncKeywordRetry':
'关键词索引重试中({attempt}/{maxAttempts}',
'documentCollection.chunkSyncIndexRetry':
'检索索引重试中({attempt}/{maxAttempts}',
};
return messages[key] || key;
return (messages[key] || key).replaceAll(/\{(\w+)\}/g, (_, name: string) =>
String(params[name]),
);
},
}));
@@ -652,6 +662,113 @@ describe('chunkDocumentTable', () => {
expect(vi.getTimerCount()).toBe(0);
});
it.each([
['EMBEDDING_REQUEST_FAILED', '向量模型服务重试中'],
['VECTOR_UPSERT_FAILED', '向量索引重试中'],
['KEYWORD_UPSERT_FAILED', '关键词索引重试中'],
['INDEX_UPSERT_FAILED', '检索索引重试中'],
])('轮询展示真实次数并区分重试原因 %s', async (code, label) => {
vi.useFakeTimers();
const pendingRow = { ...row, indexSyncStatus: 'PENDING' };
const post = vi.fn().mockResolvedValue({
errorCode: 0,
data: [
{
id: row.id,
indexSyncStatus: 'PENDING',
indexSyncVersion: row.indexSyncVersion,
indexSyncErrorCode: code,
indexSyncAttemptCount: 1,
indexSyncMaxAttempts: 5,
},
],
});
const { wrapper } = mountTable(post, [pendingRow]);
await flushPromises();
expect(wrapper.find('.chunk-sync-retry-hint').exists()).toBe(false);
await vi.advanceTimersByTimeAsync(2000);
await flushPromises();
expect(wrapper.get('.chunk-sync-retry-hint').text()).toBe(
`${label}1/5`,
);
expect(wrapper.get('.chunk-sync-retry-hint').attributes('role')).toBe(
'status',
);
post.mockResolvedValue({
errorCode: 0,
data: [
{
id: row.id,
indexSyncStatus: 'SYNCED',
indexSyncVersion: row.indexSyncVersion,
},
],
});
await vi.advanceTimersByTimeAsync(2000);
await flushPromises();
expect(wrapper.find('.chunk-sync-retry-hint').exists()).toBe(false);
expect(
(wrapper.findComponent(PageData).vm as any).getPageRows()[0]
.indexSyncErrorCode,
).toBeNull();
});
it.each(['xlsx', 'pdf'])(
'两种分块布局均展示重试提示:%s',
async (sourceFileExt) => {
const { wrapper } = mountTable(vi.fn(), [
{
...row,
options: {
...row.options,
sourceFileExt,
sheetName: sourceFileExt === 'xlsx' ? 'Sheet1' : undefined,
},
indexSyncStatus: 'PENDING',
indexSyncErrorCode: 'EMBEDDING_REQUEST_FAILED',
indexSyncAttemptCount: 2,
indexSyncMaxAttempts: 5,
},
]);
await flushPromises();
expect(wrapper.get('.chunk-sync-retry-hint').text()).toBe(
'向量模型服务重试中2/5',
);
},
);
it('新版本保存后清除上个版本的重试提示', async () => {
const { wrapper } = mountTable(
vi.fn().mockResolvedValue({
errorCode: 0,
data: {
content: '新正文',
indexSyncStatus: 'PENDING',
indexSyncVersion: 2,
},
}),
[
{
...row,
indexSyncStatus: 'PENDING',
indexSyncVersion: 1,
indexSyncErrorCode: 'EMBEDDING_REQUEST_FAILED',
indexSyncAttemptCount: 2,
indexSyncMaxAttempts: 5,
},
],
);
await flushPromises();
await getButton(wrapper, 'button.edit').trigger('click');
await flushPromises();
await wrapper.get('.editor-stub').setValue('新正文');
await getButton(wrapper, 'button.save').trigger('click');
await flushPromises();
expect(wrapper.find('.chunk-sync-retry-hint').exists()).toBe(false);
});
it('实时编辑器保真失败时使用同一草稿切换源码模式', async () => {
const { wrapper } = mountTable();
await flushPromises();

View File

@@ -153,6 +153,30 @@ const isSyncSuccessVisible = (row: any) =>
syncSuccessVersions.value[String(row?.id || '')] ===
String(row?.indexSyncVersion ?? '');
const getSyncRetryLabel = (row: any) => {
const attempt = Number(row?.indexSyncAttemptCount);
const maxAttempts = Number(row?.indexSyncMaxAttempts);
if (
row?.indexSyncStatus !== 'PENDING' ||
!row.indexSyncErrorCode ||
!Number.isInteger(attempt) ||
attempt <= 0 ||
!Number.isInteger(maxAttempts) ||
maxAttempts <= 0
) {
return '';
}
const retryKeys: Record<string, string> = {
EMBEDDING_REQUEST_FAILED: 'chunkSyncEmbeddingRetry',
KEYWORD_UPSERT_FAILED: 'chunkSyncKeywordRetry',
VECTOR_UPSERT_FAILED: 'chunkSyncVectorRetry',
};
return $t(
`documentCollection.${retryKeys[row.indexSyncErrorCode] || 'chunkSyncIndexRetry'}`,
{ attempt, maxAttempts },
);
};
const getChunkOptions = (row: any) => row?.options || {};
const getRawMarkdown = (row: any) =>
String(getChunkOptions(row)?.renderMarkdown ?? row?.content ?? '');
@@ -323,6 +347,8 @@ const updateRow = (row: any, updated: any) => {
content: updated?.content ?? row.content,
indexSyncErrorCode: updated?.indexSyncErrorCode ?? null,
indexSyncErrorMessage: updated?.indexSyncErrorMessage ?? null,
indexSyncAttemptCount: updated?.indexSyncAttemptCount ?? null,
indexSyncMaxAttempts: updated?.indexSyncMaxAttempts ?? null,
indexSyncStatus: nextSyncStatus,
indexSyncVersion: nextSyncVersion,
options: updated?.options ?? {
@@ -390,7 +416,13 @@ const pollSyncStatus = async (generation: number) => {
) {
continue;
}
pageDataRef.value?.patchRowById?.(status.id, status);
pageDataRef.value?.patchRowById?.(status.id, {
...status,
indexSyncAttemptCount: status.indexSyncAttemptCount ?? null,
indexSyncMaxAttempts: status.indexSyncMaxAttempts ?? null,
indexSyncErrorCode: status.indexSyncErrorCode ?? null,
indexSyncErrorMessage: status.indexSyncErrorMessage ?? null,
});
if (status.indexSyncStatus === 'SYNCED') {
showSyncSuccessFeedback(status);
} else {
@@ -458,6 +490,8 @@ const retrySync = async (row: any) => {
pageDataRef.value?.patchRowById?.(row.id, {
indexSyncErrorCode: res.data?.indexSyncErrorCode ?? null,
indexSyncErrorMessage: res.data?.indexSyncErrorMessage ?? null,
indexSyncAttemptCount: res.data?.indexSyncAttemptCount ?? null,
indexSyncMaxAttempts: res.data?.indexSyncMaxAttempts ?? null,
indexSyncStatus: res.data?.indexSyncStatus,
indexSyncVersion: res.data?.indexSyncVersion,
});
@@ -785,6 +819,13 @@ const getChunkHeaderLabel = (row: any) => {
</ElButton>
</div>
</div>
<span
v-if="getSyncRetryLabel(row)"
class="chunk-sync-retry-hint text-xs"
role="status"
>
{{ getSyncRetryLabel(row) }}
</span>
<div
v-if="isEditing(row)"
@@ -1033,6 +1074,13 @@ const getChunkHeaderLabel = (row: any) => {
{{ $t('button.delete') }}
</ElButton>
</div>
<span
v-if="getSyncRetryLabel(row)"
class="chunk-sync-retry-hint text-xs"
role="status"
>
{{ getSyncRetryLabel(row) }}
</span>
</template>
</ElTableColumn>
</ElTable>
@@ -1089,6 +1137,21 @@ const getChunkHeaderLabel = (row: any) => {
gap: var(--space-1);
}
.chunk-sync-retry-hint {
display: block;
min-width: 0;
margin-top: var(--space-1);
font-weight: normal;
color: hsl(var(--text-muted));
text-align: right;
overflow-wrap: anywhere;
}
.chunk-item > .chunk-sync-retry-hint {
margin-top: 0;
margin-bottom: var(--space-2);
}
.chunk-sync-status {
display: inline-grid;
flex: 0 0 24px;