fix(aihr): recover stalled media processing
This commit is contained in:
@@ -75,6 +75,13 @@ export type ProcessingOverview = {
|
||||
rules: ProcessingRules;
|
||||
};
|
||||
|
||||
export type ProcessingRetryResponse = {
|
||||
itemId: number;
|
||||
batchId: string;
|
||||
fileName: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type LocalImportRequest = {
|
||||
directory: string;
|
||||
category: string;
|
||||
@@ -113,6 +120,14 @@ export function getProcessingOverview(): Promise<ApiResult<ProcessingOverview>>
|
||||
});
|
||||
}
|
||||
|
||||
export function retryProcessingTask(id: number): Promise<ApiResult<ProcessingRetryResponse>> {
|
||||
return request({
|
||||
url: `/api/knowledge/doc/processing-tasks/${id}/retry`,
|
||||
method: 'post',
|
||||
headers: { repeatSubmit: false }
|
||||
});
|
||||
}
|
||||
|
||||
export function importLocalKnowledgeDocs(data: LocalImportRequest): Promise<ApiResult<LocalImportResponse>> {
|
||||
return request({
|
||||
url: '/api/knowledge/doc/import-local',
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const read = (path: string) => readFileSync(resolve(process.cwd(), path), 'utf8');
|
||||
|
||||
describe('资料处理媒体重试契约', () => {
|
||||
it('把零文本媒体展示为待重试并从原文件重新入队', () => {
|
||||
const api = read('src/api/aihr/processing.ts');
|
||||
const page = read('src/views/knowledge/processing.vue');
|
||||
|
||||
expect(api).toContain('/api/knowledge/doc/processing-tasks/${id}/retry');
|
||||
expect(page).toContain("const statusTabs = ['全部', '待重试', '解析中', '已完成', '失败']");
|
||||
expect(page).toContain('canRetryProcessingTask(row)');
|
||||
expect(page).toContain('await retryProcessingTask(task.id)');
|
||||
expect(page).toContain('已从原文件重新排队');
|
||||
});
|
||||
});
|
||||
@@ -149,6 +149,20 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="summary" label="AI摘要" min-width="230" show-overflow-tooltip />
|
||||
<el-table-column prop="updatedAt" label="更新时间" width="138" />
|
||||
<el-table-column label="操作" width="86" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="canRetryProcessingTask(row)"
|
||||
link
|
||||
type="danger"
|
||||
:loading="taskRetryingId === row.id"
|
||||
@click="retryTask(row)"
|
||||
>
|
||||
重试
|
||||
</el-button>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<footer class="table-footer">
|
||||
@@ -210,7 +224,7 @@
|
||||
</div>
|
||||
<div v-if="displayOverview.rules.failedTasks > 0" class="risk-tip">
|
||||
<el-icon><WarningFilled /></el-icon>
|
||||
<span>失败任务 {{ displayOverview.rules.failedTasks }} 个,建议先重试网络/格式异常文件</span>
|
||||
<span>需处理任务 {{ displayOverview.rules.failedTasks }} 个,可从原文件重试图片/视频解析</span>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
@@ -260,7 +274,7 @@
|
||||
import { CircleCheck, Document, Files, Operation, Refresh, Search, Upload, WarningFilled } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { getProcessingOverview, type ProcessingOverview, type ProcessingTask } from '@/api/aihr/processing';
|
||||
import { getProcessingOverview, retryProcessingTask, type ProcessingOverview, type ProcessingTask } from '@/api/aihr/processing';
|
||||
import { uploadKnowledgeDocAsync, listUploadItems, retryUploadItem, type UploadItem } from '@/api/aihr/sop';
|
||||
import { listKnowledgeSpaces, type KnowledgeSpace } from '@/api/aihr/space';
|
||||
import { listPersonalPublishRequests, reviewPersonalPublishRequest, type PersonalPublishRequestRow } from '@/api/aihr/personal';
|
||||
@@ -294,9 +308,10 @@ const uploadBatch = ref<UploadItem[]>([]);
|
||||
const uploadBatchId = ref('');
|
||||
const enqueueFailures = ref<string[]>([]);
|
||||
const retryingId = ref<number | null>(null);
|
||||
const taskRetryingId = ref<number | null>(null);
|
||||
let batchPollTimer: number | undefined;
|
||||
|
||||
const statusTabs = ['全部', '等待解析', '解析中', '已完成', '失败'];
|
||||
const statusTabs = ['全部', '待重试', '解析中', '已完成', '失败'];
|
||||
const fileTypes = ['全部', 'Word', 'PDF', 'PPT', 'Excel', '图片', '视频'];
|
||||
|
||||
const seedOverview: ProcessingOverview = {
|
||||
@@ -304,7 +319,7 @@ const seedOverview: ProcessingOverview = {
|
||||
{ key: 'total', label: '资料总量', value: '3.2 GB', note: '已接入 1,286 个文件', type: 'danger' },
|
||||
{ key: 'completed', label: '已完成', value: '1,148', note: '较昨日 ↑128 (12.6%)', type: 'success' },
|
||||
{ key: 'processing', label: '处理中', value: '36', note: '进行中任务', type: 'primary' },
|
||||
{ key: 'failed', label: '失败', value: '7', note: '较昨日 ↑2 (22.2%)', type: 'warning' }
|
||||
{ key: 'failed', label: '需处理', value: '7', note: '可从任务列表重试', type: 'warning' }
|
||||
],
|
||||
categories: [
|
||||
{ name: '住宅 SOP', files: 368, size: '2.1 GB', icon: 'folder', type: 'danger' },
|
||||
@@ -331,7 +346,7 @@ const seedOverview: ProcessingOverview = {
|
||||
''
|
||||
),
|
||||
task(2, '住宅 SOP', '物业收费催缴流程.pdf', 'PDF', '8.1MB', '解析中', 'primary', 62, 0, '待向量化', 'warning', '2026-07-02 14:28', '查看', ''),
|
||||
task(3, '培训课件', '巡检标准培训课件.pptx', 'PPT', '42MB', '等待解析', 'warning', 0, 0, '待处理', 'warning', '2026-07-02 14:25', '开始', ''),
|
||||
task(3, '培训课件', '巡检标准培训课件.pptx', 'PPT', '42MB', '待重试', 'warning', 0, 0, '失败原因', 'danger', '2026-07-02 14:25', '重试', ''),
|
||||
task(4, '图片素材', '工单照片归档.zip', '图片', '286MB', 'OCR排队', 'primary', 0, 0, '待处理', 'warning', '2026-07-02 14:20', '查看', ''),
|
||||
task(
|
||||
5,
|
||||
@@ -401,7 +416,7 @@ const emptyOverview: ProcessingOverview = {
|
||||
{ key: 'total', label: '资料总量', value: '0', note: '暂无导入文件', type: 'danger' },
|
||||
{ key: 'completed', label: '已完成', value: '0', note: '等待真实上传', type: 'success' },
|
||||
{ key: 'processing', label: '处理中', value: '0', note: '暂无任务', type: 'primary' },
|
||||
{ key: 'failed', label: '失败', value: '0', note: '暂无失败', type: 'warning' }
|
||||
{ key: 'failed', label: '需处理', value: '0', note: '暂无异常', type: 'warning' }
|
||||
],
|
||||
categories: [],
|
||||
tasks: [],
|
||||
@@ -605,6 +620,23 @@ async function retryBatchItem(item: UploadItem) {
|
||||
}
|
||||
}
|
||||
|
||||
function canRetryProcessingTask(task: ProcessingTask) {
|
||||
return task.action === '重试' && ['图片', '视频'].includes(task.fileType);
|
||||
}
|
||||
|
||||
async function retryTask(task: ProcessingTask) {
|
||||
taskRetryingId.value = task.id;
|
||||
try {
|
||||
await retryProcessingTask(task.id);
|
||||
ElMessage.success(`已从原文件重新排队:${task.fileName}`);
|
||||
await loadOverview();
|
||||
} catch (error) {
|
||||
ElMessage.error(uploadFailureMessage(error));
|
||||
} finally {
|
||||
taskRetryingId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
const batchCounts = computed(() => ({
|
||||
queued: uploadBatch.value.filter((item) => item.status === 0).length,
|
||||
processing: uploadBatch.value.filter((item) => item.status === 1).length,
|
||||
@@ -635,7 +667,7 @@ function uploadFailureMessage(error: unknown) {
|
||||
|
||||
function matchStatus(status: string) {
|
||||
if (activeStatus.value === '全部') return true;
|
||||
if (activeStatus.value === '等待解析') return ['等待解析', 'OCR排队'].includes(status);
|
||||
if (activeStatus.value === '待重试') return ['待重试', '等待解析', 'OCR排队'].includes(status);
|
||||
return status === activeStatus.value;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user