import { computed, reactive, ref, type Ref } from 'vue' import { ElMessage } from 'element-plus' import { generateDataProcess, getDataProcessProgress, getDataProcessResults, regenerateDataProcessResult, regenerateDataProcessResults, restoreDataProcessResult, updateDataProcessResult, type DataProcessProgress, type DataProcessResult, } from '@/api/modules/dataProcess' import type { BulkResultRegenerationState, GenerationState, ResultItem } from './types' interface GenerationBindings { taskId: Ref dirty: Ref beforeGenerate?: () => Promise } const RESULT_PAGE_SIZE = 500 const POLL_INTERVAL_MS = 1500 // 4 个后端 worker 可连续消费三轮,减少每 4 条等待最慢项造成的空闲; // 单条重生成最长 60 秒,因此 12 条仍处于批量接口 240 秒超时预算内。 const BULK_REGENERATION_CHUNK_SIZE = 12 function mapResult(item: DataProcessResult): ResultItem { return { id: String(item.id), previewItemId: item.preview_item_id == null ? null : String(item.preview_item_id), instruction: item.instruction, input: item.input || '', output: item.output, originalInstruction: item.original_instruction ?? item.instruction, originalInput: item.original_input ?? item.input ?? '', originalOutput: item.original_output ?? item.output, savedInstruction: item.instruction, savedInput: item.input || '', savedOutput: item.output, savedStatus: item.status, status: item.status, error: item.error || undefined, split: item.split || undefined, qualityScore: item.quality_score?.overall, updatedAt: item.updated_at, } } export function useDataProcessGeneration(bindings: GenerationBindings) { const results = ref([]) const selectedResultId = ref(null) const regeneratingResultId = ref(null) const bulkRegeneration = reactive({ status: 'idle', total: 0, completed: 0, succeeded: 0, failed: 0, targetIds: [], failedIds: [], }) const resultRegenerationBusy = computed(() => ( Boolean(regeneratingResultId.value) || bulkRegeneration.status === 'running' )) const generation = reactive({ status: 'idle', progress: 0, message: '确认摘要后即可开始生成,过程中可查看实时进度。', }) let generationTimer: ReturnType | null = null let generationRun = 0 let pollFailureCount = 0 let generationStarting = false function stopGenerationTimer() { generationRun += 1 if (generationTimer) clearTimeout(generationTimer) generationTimer = null } function resetDownstream() { stopGenerationTimer() generation.status = 'idle' generation.progress = 0 generation.message = '确认摘要后即可开始生成,过程中可查看实时进度。' results.value = [] selectedResultId.value = null regeneratingResultId.value = null Object.assign(bulkRegeneration, { status: 'idle', total: 0, completed: 0, succeeded: 0, failed: 0, targetIds: [], failedIds: [], }) } function applyProgress(progress: DataProcessProgress) { generation.progress = Math.max(0, Math.min(100, Number(progress.progress) || 0)) generation.message = progress.message || ( progress.status === 'running' ? '后端正在生成标准化结果并进行质量评分。' : progress.status === 'completed' ? '数据处理已完成。' : progress.failure_reason || '任务已停止。' ) } async function loadAllResults(taskId: string) { const first = await getDataProcessResults(taskId, { page: 1, page_size: RESULT_PAGE_SIZE }) const items = [...first.items] const pages = Math.ceil(first.total / first.page_size) for (let page = 2; page <= pages; page += 1) { const next = await getDataProcessResults(taskId, { page, page_size: RESULT_PAGE_SIZE }) items.push(...next.items) } results.value = items.map(mapResult) selectedResultId.value = results.value[0]?.id ?? null } async function finishFromProgress(progress: DataProcessProgress, notify = true) { pollFailureCount = 0 applyProgress(progress) if (progress.status === 'completed') { const taskId = bindings.taskId.value if (!taskId) return await loadAllResults(taskId) generation.status = 'success' generation.progress = 100 generation.message = `已完成 ${results.value.length.toLocaleString()} 条数据处理,可进入结果页检查。` if (notify) ElMessage.success('数据处理完成') return } if (progress.status === 'failed' || progress.status === 'stopped') { generation.status = 'failed' generation.message = progress.failure_reason || progress.message || ( progress.status === 'stopped' ? '任务已停止,可以重新生成。' : '数据处理失败,请检查配置后重试。' ) } } async function pollGeneration(runId: number) { const taskId = bindings.taskId.value if (!taskId || runId !== generationRun || generation.status !== 'running') return try { const progress = await getDataProcessProgress(taskId) if (runId !== generationRun) return pollFailureCount = 0 if (progress.status === 'running' || progress.status === 'pending') { applyProgress(progress) generationTimer = setTimeout(() => void pollGeneration(runId), POLL_INTERVAL_MS) return } await finishFromProgress(progress) } catch (error) { if (runId !== generationRun) return pollFailureCount += 1 if (pollFailureCount <= 3) { generation.message = `进度查询暂时失败,正在重试(${pollFailureCount}/3)…` generationTimer = setTimeout(() => void pollGeneration(runId), POLL_INTERVAL_MS) return } generation.status = 'failed' generation.message = error instanceof Error ? error.message : '查询任务进度失败,请重试。' } } async function startGeneration() { if (generationStarting || generation.status === 'running') return false const taskId = bindings.taskId.value if (!taskId) { ElMessage.error('任务尚未创建,请返回上一步重试') return false } generationStarting = true let runId: number | null = null try { const canStart = await bindings.beforeGenerate?.() if (canStart === false) return false stopGenerationTimer() const activeRunId = generationRun runId = activeRunId generation.status = 'running' pollFailureCount = 0 generation.progress = 0 generation.message = '正在同步预览修改并启动后端处理,请稍候。' const progress = await generateDataProcess(taskId) if (activeRunId !== generationRun) return false if (progress.status === 'completed' || progress.status === 'failed' || progress.status === 'stopped') { await finishFromProgress(progress) return true } applyProgress(progress) generationTimer = setTimeout(() => void pollGeneration(activeRunId), POLL_INTERVAL_MS) return true } catch (error) { if (runId !== null && runId !== generationRun) return generation.status = 'failed' generation.message = error instanceof Error ? error.message : '启动数据处理失败,请重试。' return false } finally { generationStarting = false } } async function resumeGeneration() { const taskId = bindings.taskId.value if (!taskId) return stopGenerationTimer() const activeRunId = generationRun pollFailureCount = 0 try { const progress = await getDataProcessProgress(taskId) if (activeRunId !== generationRun) return if (progress.status === 'running') { generation.status = 'running' applyProgress(progress) generationTimer = setTimeout(() => void pollGeneration(activeRunId), POLL_INTERVAL_MS) return } if (progress.status === 'completed') { await finishFromProgress(progress, false) return } if (progress.status === 'failed' || progress.status === 'stopped') { await finishFromProgress(progress, false) return } generation.status = 'idle' applyProgress(progress) } catch (error) { generation.status = 'failed' generation.message = error instanceof Error ? error.message : '查询任务进度失败,请重试。' } } function updateResultField(id: string, field: 'instruction' | 'input' | 'output', value: string) { const item = results.value.find((entry) => entry.id === id) if (!item) return item[field] = value const valid = item.instruction.trim() && item.output.trim() item.error = valid ? undefined : 'Instruction 和 Output 不能为空' const changed = item.instruction !== item.originalInstruction || item.input !== item.originalInput || item.output !== item.originalOutput item.status = item.error ? 'invalid' : changed ? 'modified' : 'valid' bindings.dirty.value = true } async function regenerateResult(id: string) { const taskId = bindings.taskId.value const item = results.value.find((entry) => entry.id === id) if (!taskId || !item) return false if (resultRegenerationBusy.value) { ElMessage.warning('请等待当前失败结果重新生成完成') return false } if (item.savedStatus !== 'invalid') { ElMessage.warning('只有生成失败的结果可以重新生成') return false } if (!item.updatedAt) { ElMessage.error('结果版本信息缺失,请刷新页面后重试') return false } regeneratingResultId.value = id try { const regenerated = await regenerateDataProcessResult(taskId, id, { expected_updated_at: item.updatedAt, }) const index = results.value.findIndex((entry) => entry.id === id) if (index >= 0) results.value[index] = mapResult(regenerated) ElMessage.success('当前结果已重新生成') return true } catch { return false } finally { regeneratingResultId.value = null } } async function regenerateAllResults() { const taskId = bindings.taskId.value if (!taskId) return false if (resultRegenerationBusy.value) { ElMessage.warning('请等待当前失败结果重新生成完成') return false } const candidates = results.value.filter((item) => item.savedStatus === 'invalid') if (!candidates.length) { ElMessage.info('当前没有需要重新生成的失败结果') return false } const unsaved = candidates.find((item) => ( item.instruction !== item.savedInstruction || item.input !== item.savedInput || item.output !== item.savedOutput )) if (unsaved) { selectedResultId.value = unsaved.id ElMessage.warning('失败结果存在未保存修改,请先保存或恢复后再全部重新生成') return false } const missingVersion = candidates.find((item) => !item.updatedAt) if (missingVersion) { selectedResultId.value = missingVersion.id ElMessage.error('失败结果版本信息缺失,请刷新页面后重试') return false } Object.assign(bulkRegeneration, { status: 'running', total: candidates.length, completed: 0, succeeded: 0, failed: 0, targetIds: candidates.map((item) => item.id), failedIds: [], }) try { for (let offset = 0; offset < candidates.length; offset += BULK_REGENERATION_CHUNK_SIZE) { const chunk = candidates.slice(offset, offset + BULK_REGENERATION_CHUNK_SIZE) try { const regenerated = await regenerateDataProcessResults(taskId, { items: chunk.map((item) => ({ result_id: item.id, expected_updated_at: item.updatedAt as string, })), }) for (const item of regenerated.items) { const index = results.value.findIndex((entry) => entry.id === String(item.id)) if (index >= 0) results.value[index] = mapResult(item) } bulkRegeneration.completed += regenerated.total bulkRegeneration.succeeded += regenerated.succeeded bulkRegeneration.failed += regenerated.failed bulkRegeneration.failedIds.push( ...regenerated.failures.map((failure) => failure.result_id), ) } catch { const remaining = candidates.slice(offset) bulkRegeneration.completed = bulkRegeneration.total bulkRegeneration.failed += remaining.length bulkRegeneration.failedIds.push(...remaining.map((item) => item.id)) break } } if (bulkRegeneration.failed === 0) { bulkRegeneration.status = 'completed' ElMessage.success(`已重新生成 ${bulkRegeneration.succeeded} 条失败结果`) } else if (bulkRegeneration.succeeded > 0) { bulkRegeneration.status = 'partial' ElMessage.warning( `重新生成完成:成功 ${bulkRegeneration.succeeded} 条,仍失败 ${bulkRegeneration.failed} 条`, ) } else { bulkRegeneration.status = 'failed' ElMessage.error(`重新生成失败:${bulkRegeneration.failed} 条结果仍需重试`) } const firstFailed = bulkRegeneration.failedIds.find((id) => ( results.value.find((item) => item.id === id)?.savedStatus === 'invalid' )) if (firstFailed) selectedResultId.value = firstFailed return bulkRegeneration.failed === 0 } catch { const remaining = Math.max(0, bulkRegeneration.total - bulkRegeneration.completed) const remainingIds = candidates .filter((candidate) => ( results.value.find((item) => item.id === candidate.id)?.savedStatus === 'invalid' )) .map((item) => item.id) bulkRegeneration.completed = bulkRegeneration.total bulkRegeneration.failed += remaining bulkRegeneration.failedIds = [...new Set([ ...bulkRegeneration.failedIds, ...remainingIds, ])] bulkRegeneration.status = 'failed' if (bulkRegeneration.failedIds[0]) selectedResultId.value = bulkRegeneration.failedIds[0] ElMessage.error('批量重新生成意外中断,尚未成功的结果保持原状') return false } finally { bulkRegeneration.targetIds = [] } } async function persistResultChanges() { const taskId = bindings.taskId.value if (!taskId) throw new Error('任务尚未创建') const changed = results.value.filter((item) => ( item.instruction !== item.savedInstruction || item.input !== item.savedInput || item.output !== item.savedOutput )) for (const item of changed) { const saved = await updateDataProcessResult(taskId, item.id, { instruction: item.instruction, input: item.input, output: item.output, expected_updated_at: item.updatedAt, }) const index = results.value.findIndex((entry) => entry.id === item.id) if (index >= 0) results.value[index] = mapResult(saved) } } function validateResults() { let firstInvalidId: string | null = null for (const item of results.value) { if (!item.instruction.trim() || !item.output.trim() || item.status === 'invalid') { item.error ||= '结果未通过后端质量校验,请修改后重新保存' item.status = 'invalid' firstInvalidId ??= item.id } } if (firstInvalidId) selectedResultId.value = firstInvalidId return firstInvalidId == null } return { bulkRegeneration, generation, regeneratingResultId, resultRegenerationBusy, results, selectedResultId, persistResultChanges, resetDownstream, regenerateAllResults, regenerateResult, resumeGeneration, startGeneration, stopGenerationTimer, updateResultField, validateResults, } }