feat: 完善知识库批量导入与公共 API
- 新增批量异步导入、状态查询、失败重试与中断恢复链路 - 拆分知识库读取、导入、维护权限并完善 Public API 契约 - 补充数据库迁移、管理端交互、接口说明与相关测试
This commit is contained in:
@@ -32,6 +32,7 @@ import { buildKnowledgeShareUrl } from '#/api/knowledge-share';
|
||||
import { api, SseClient } from '#/api/request';
|
||||
import documentIcon from '#/assets/ai/knowledge/document.svg';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import { resolveDocumentTaskErrorText } from '#/views/ai/documentCollection/document-import-error';
|
||||
import { buildKnowledgePath } from '#/views/ai/documentCollection/share-path';
|
||||
|
||||
interface DocumentStatusPayload {
|
||||
@@ -40,6 +41,7 @@ interface DocumentStatusPayload {
|
||||
failedChunks?: number;
|
||||
knowledgeId?: number | string;
|
||||
lastTaskError?: string;
|
||||
lastTaskErrorCode?: string;
|
||||
parseCurrentStage?: string;
|
||||
parseStatusMessage?: string;
|
||||
processStatus?: string;
|
||||
@@ -86,6 +88,7 @@ const STREAM_RECONNECT_DELAY = 1500;
|
||||
const STREAM_RELOAD_DELAY = 250;
|
||||
|
||||
const pageDataRef = ref();
|
||||
const retryingDocumentIds = ref<Set<string>>(new Set());
|
||||
const taskStatusStreamClient = new SseClient();
|
||||
let reconnectTimer: null | ReturnType<typeof setTimeout> = null;
|
||||
let reloadTimer: null | ReturnType<typeof setTimeout> = null;
|
||||
@@ -102,7 +105,7 @@ defineExpose({
|
||||
},
|
||||
});
|
||||
|
||||
const processingStatuses = new Set(['INDEXING', 'PARSING']);
|
||||
const processingStatuses = new Set(['INDEXING', 'PARSING', 'SPLITTING']);
|
||||
|
||||
const isProcessingStatus = (status?: string) =>
|
||||
processingStatuses.has(status || '');
|
||||
@@ -113,7 +116,8 @@ const resolvedPermissions = computed(() => ({
|
||||
canDownloadContent: props.permissions?.canDownloadContent ?? true,
|
||||
}));
|
||||
|
||||
const hasPermission = (key: PermissionKey) => Boolean(resolvedPermissions.value[key]);
|
||||
const hasPermission = (key: PermissionKey) =>
|
||||
Boolean(resolvedPermissions.value[key]);
|
||||
|
||||
const statusMetaMap: Record<
|
||||
string,
|
||||
@@ -142,6 +146,14 @@ const statusMetaMap: Record<
|
||||
icon: Loading,
|
||||
toneClass: 'status-pill--warning',
|
||||
},
|
||||
SPLIT_FAILED: {
|
||||
icon: CloseBold,
|
||||
toneClass: 'status-pill--danger',
|
||||
},
|
||||
SPLITTING: {
|
||||
icon: Loading,
|
||||
toneClass: 'status-pill--warning',
|
||||
},
|
||||
READY_FOR_INDEX: {
|
||||
icon: Opportunity,
|
||||
toneClass: 'status-pill--primary',
|
||||
@@ -204,9 +216,21 @@ const parseStageLabels: Record<string, string> = {
|
||||
};
|
||||
|
||||
const getProcessingHint = (row: any) =>
|
||||
row.parseStatusMessage ||
|
||||
parseStageLabels[row.parseCurrentStage || ''] ||
|
||||
'';
|
||||
row.parseStatusMessage || parseStageLabels[row.parseCurrentStage || ''] || '';
|
||||
|
||||
const getErrorText = (row: any) => {
|
||||
const taskError = resolveDocumentTaskErrorText(row, $t);
|
||||
if (taskError) {
|
||||
return taskError;
|
||||
}
|
||||
if (
|
||||
row.processStatus === 'SPLIT_FAILED' ||
|
||||
row.processStatus === 'INDEX_FAILED'
|
||||
) {
|
||||
return $t('documentCollection.importDoc.splitOrIndexFailed');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const clearReconnectTimer = () => {
|
||||
if (!reconnectTimer) {
|
||||
@@ -242,6 +266,7 @@ const patchDocumentRow = (payload: DocumentStatusPayload) => {
|
||||
completedChunks: payload.completedChunks,
|
||||
failedChunks: payload.failedChunks,
|
||||
lastTaskError: payload.lastTaskError,
|
||||
lastTaskErrorCode: payload.lastTaskErrorCode,
|
||||
parseCurrentStage: payload.parseCurrentStage,
|
||||
parseStatusMessage: payload.parseStatusMessage,
|
||||
processStatus: payload.processStatus,
|
||||
@@ -363,17 +388,33 @@ const handleContinue = (row: any) => {
|
||||
emits('continueProcess', row);
|
||||
};
|
||||
|
||||
const handleRetryParse = async (row: any) => {
|
||||
await requestTaskAction(
|
||||
'/api/v1/document/import/task/retryParse',
|
||||
{
|
||||
knowledgeId: props.knowledgeId,
|
||||
documentId: row.id,
|
||||
},
|
||||
getStatusLabel('PARSING'),
|
||||
);
|
||||
const handleRetry = async (row: any) => {
|
||||
const documentId = String(row.id);
|
||||
if (retryingDocumentIds.value.has(documentId)) {
|
||||
return;
|
||||
}
|
||||
retryingDocumentIds.value = new Set([
|
||||
documentId,
|
||||
...retryingDocumentIds.value,
|
||||
]);
|
||||
try {
|
||||
await requestTaskAction(
|
||||
'/api/v1/document/import/task/retry',
|
||||
{
|
||||
knowledgeId: props.knowledgeId,
|
||||
documentId: row.id,
|
||||
},
|
||||
getStatusLabel('PARSING'),
|
||||
);
|
||||
} finally {
|
||||
const next = new Set(retryingDocumentIds.value);
|
||||
next.delete(documentId);
|
||||
retryingDocumentIds.value = next;
|
||||
}
|
||||
};
|
||||
|
||||
const isRetrying = (row: any) => retryingDocumentIds.value.has(String(row.id));
|
||||
|
||||
const handleView = (row: any) => {
|
||||
emits('viewDoc', row.id);
|
||||
};
|
||||
@@ -428,13 +469,18 @@ const primaryActionConfigs: Record<
|
||||
label: () => $t('button.viewSegmentation'),
|
||||
},
|
||||
INDEX_FAILED: {
|
||||
handler: handleContinue,
|
||||
label: () => $t('button.continueProcess'),
|
||||
handler: handleRetry,
|
||||
label: () => $t('documentCollection.importDoc.retry'),
|
||||
permission: 'canCreateContent',
|
||||
},
|
||||
PARSE_FAILED: {
|
||||
handler: handleRetryParse,
|
||||
label: () => $t('button.retryParse'),
|
||||
handler: handleRetry,
|
||||
label: () => $t('documentCollection.importDoc.retry'),
|
||||
permission: 'canCreateContent',
|
||||
},
|
||||
SPLIT_FAILED: {
|
||||
handler: handleRetry,
|
||||
label: () => $t('documentCollection.importDoc.retry'),
|
||||
permission: 'canCreateContent',
|
||||
},
|
||||
READY_FOR_INDEX: {
|
||||
@@ -583,7 +629,8 @@ watch(
|
||||
<div
|
||||
v-if="
|
||||
row.processStatus === 'INDEXING' ||
|
||||
row.processStatus === 'PARSING'
|
||||
row.processStatus === 'PARSING' ||
|
||||
row.processStatus === 'SPLITTING'
|
||||
"
|
||||
class="status-progress"
|
||||
>
|
||||
@@ -595,19 +642,24 @@ watch(
|
||||
{{ getProgressText(row) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="row.processStatus === 'PARSING' && getProcessingHint(row)"
|
||||
v-if="
|
||||
row.processStatus === 'PARSING' && getProcessingHint(row)
|
||||
"
|
||||
class="status-progress__hint"
|
||||
>
|
||||
{{ getProcessingHint(row) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="row.lastTaskError"
|
||||
class="status-error"
|
||||
:title="row.lastTaskError"
|
||||
<ElTooltip
|
||||
v-else-if="getErrorText(row)"
|
||||
:content="getErrorText(row)"
|
||||
placement="top"
|
||||
:show-after="300"
|
||||
>
|
||||
{{ row.lastTaskError }}
|
||||
</div>
|
||||
<div class="status-error">
|
||||
{{ getErrorText(row) }}
|
||||
</div>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
@@ -624,6 +676,8 @@ watch(
|
||||
v-if="getPrimaryActionLabel(row)"
|
||||
link
|
||||
type="primary"
|
||||
:disabled="isRetrying(row)"
|
||||
:loading="isRetrying(row)"
|
||||
@click="handlePrimaryAction(row)"
|
||||
>
|
||||
{{ getPrimaryActionLabel(row) }}
|
||||
|
||||
Reference in New Issue
Block a user