feat(data-process): 完善后台生成与失败重试
This commit is contained in:
@@ -1,16 +1,17 @@
|
||||
import { reactive, ref, type Ref } from 'vue'
|
||||
import { computed, reactive, ref, type Ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
generateDataProcess,
|
||||
getDataProcessProgress,
|
||||
getDataProcessResults,
|
||||
regenerateDataProcessResult,
|
||||
regenerateDataProcessResults,
|
||||
restoreDataProcessResult,
|
||||
stopDataProcess,
|
||||
updateDataProcessResult,
|
||||
type DataProcessProgress,
|
||||
type DataProcessResult,
|
||||
} from '@/api/modules/dataProcess'
|
||||
import type { GenerationState, ResultItem } from './types'
|
||||
import type { BulkResultRegenerationState, GenerationState, ResultItem } from './types'
|
||||
|
||||
interface GenerationBindings {
|
||||
taskId: Ref<string | null>
|
||||
@@ -20,6 +21,9 @@ interface GenerationBindings {
|
||||
|
||||
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 {
|
||||
@@ -31,11 +35,14 @@ function mapResult(item: DataProcessResult): ResultItem {
|
||||
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,
|
||||
qualityFlags: item.quality_score?.flags || [],
|
||||
updatedAt: item.updated_at,
|
||||
}
|
||||
}
|
||||
@@ -43,6 +50,19 @@ function mapResult(item: DataProcessResult): ResultItem {
|
||||
export function useDataProcessGeneration(bindings: GenerationBindings) {
|
||||
const results = ref<ResultItem[]>([])
|
||||
const selectedResultId = ref<string | null>(null)
|
||||
const regeneratingResultId = ref<string | null>(null)
|
||||
const bulkRegeneration = reactive<BulkResultRegenerationState>({
|
||||
status: 'idle',
|
||||
total: 0,
|
||||
completed: 0,
|
||||
succeeded: 0,
|
||||
failed: 0,
|
||||
targetIds: [],
|
||||
failedIds: [],
|
||||
})
|
||||
const resultRegenerationBusy = computed(() => (
|
||||
Boolean(regeneratingResultId.value) || bulkRegeneration.status === 'running'
|
||||
))
|
||||
const generation = reactive<GenerationState>({
|
||||
status: 'idle',
|
||||
progress: 0,
|
||||
@@ -66,6 +86,16 @@ export function useDataProcessGeneration(bindings: GenerationBindings) {
|
||||
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) {
|
||||
@@ -91,7 +121,7 @@ export function useDataProcessGeneration(bindings: GenerationBindings) {
|
||||
selectedResultId.value = results.value[0]?.id ?? null
|
||||
}
|
||||
|
||||
async function finishFromProgress(progress: DataProcessProgress) {
|
||||
async function finishFromProgress(progress: DataProcessProgress, notify = true) {
|
||||
pollFailureCount = 0
|
||||
applyProgress(progress)
|
||||
if (progress.status === 'completed') {
|
||||
@@ -101,8 +131,7 @@ export function useDataProcessGeneration(bindings: GenerationBindings) {
|
||||
generation.status = 'success'
|
||||
generation.progress = 100
|
||||
generation.message = `已完成 ${results.value.length.toLocaleString()} 条数据处理,可进入结果页检查。`
|
||||
bindings.dirty.value = true
|
||||
ElMessage.success('数据处理完成')
|
||||
if (notify) ElMessage.success('数据处理完成')
|
||||
return
|
||||
}
|
||||
if (progress.status === 'failed' || progress.status === 'stopped') {
|
||||
@@ -141,18 +170,18 @@ export function useDataProcessGeneration(bindings: GenerationBindings) {
|
||||
}
|
||||
|
||||
async function startGeneration() {
|
||||
if (generationStarting || generation.status === 'running') return
|
||||
if (generationStarting || generation.status === 'running') return false
|
||||
const taskId = bindings.taskId.value
|
||||
if (!taskId) {
|
||||
ElMessage.error('任务尚未创建,请返回上一步重试')
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
generationStarting = true
|
||||
let runId: number | null = null
|
||||
try {
|
||||
const canStart = await bindings.beforeGenerate?.()
|
||||
if (canStart === false) return
|
||||
if (canStart === false) return false
|
||||
stopGenerationTimer()
|
||||
const activeRunId = generationRun
|
||||
runId = activeRunId
|
||||
@@ -161,38 +190,53 @@ export function useDataProcessGeneration(bindings: GenerationBindings) {
|
||||
generation.progress = 0
|
||||
generation.message = '正在同步预览修改并启动后端处理,请稍候。'
|
||||
const progress = await generateDataProcess(taskId)
|
||||
if (activeRunId !== generationRun) return
|
||||
if (activeRunId !== generationRun) return false
|
||||
if (progress.status === 'completed' || progress.status === 'failed' || progress.status === 'stopped') {
|
||||
await finishFromProgress(progress)
|
||||
return
|
||||
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 stopGeneration() {
|
||||
async function resumeGeneration() {
|
||||
const taskId = bindings.taskId.value
|
||||
if (!taskId) return
|
||||
stopGenerationTimer()
|
||||
const activeRunId = generationRun
|
||||
pollFailureCount = 0
|
||||
try {
|
||||
const progress = await stopDataProcess(taskId)
|
||||
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 {
|
||||
generation.status = 'running'
|
||||
generation.message = '停止请求失败,继续查询后端任务状态。'
|
||||
const runId = generationRun
|
||||
generationTimer = setTimeout(() => void pollGeneration(runId), POLL_INTERVAL_MS)
|
||||
return
|
||||
} catch (error) {
|
||||
generation.status = 'failed'
|
||||
generation.message = error instanceof Error ? error.message : '查询任务进度失败,请重试。'
|
||||
}
|
||||
generation.status = 'failed'
|
||||
generation.message = '任务已停止,预览修改仍然保留,可以重新生成。'
|
||||
}
|
||||
|
||||
function updateResultField(id: string, field: 'instruction' | 'input' | 'output', value: string) {
|
||||
@@ -208,23 +252,156 @@ export function useDataProcessGeneration(bindings: GenerationBindings) {
|
||||
bindings.dirty.value = true
|
||||
}
|
||||
|
||||
async function restoreResult(id: string) {
|
||||
|
||||
|
||||
async function regenerateResult(id: string) {
|
||||
const taskId = bindings.taskId.value
|
||||
const item = results.value.find((entry) => entry.id === id)
|
||||
if (!taskId || !item) return
|
||||
const restored = await restoreDataProcessResult(taskId, id)
|
||||
const index = results.value.indexOf(item)
|
||||
results.value[index] = mapResult(restored)
|
||||
bindings.dirty.value = true
|
||||
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.originalInstruction
|
||||
|| item.input !== item.originalInput
|
||||
|| item.output !== item.originalOutput
|
||||
item.instruction !== item.savedInstruction
|
||||
|| item.input !== item.savedInput
|
||||
|| item.output !== item.savedOutput
|
||||
))
|
||||
for (const item of changed) {
|
||||
const saved = await updateDataProcessResult(taskId, item.id, {
|
||||
@@ -252,14 +429,18 @@ export function useDataProcessGeneration(bindings: GenerationBindings) {
|
||||
}
|
||||
|
||||
return {
|
||||
bulkRegeneration,
|
||||
generation,
|
||||
regeneratingResultId,
|
||||
resultRegenerationBusy,
|
||||
results,
|
||||
selectedResultId,
|
||||
persistResultChanges,
|
||||
resetDownstream,
|
||||
restoreResult,
|
||||
regenerateAllResults,
|
||||
regenerateResult,
|
||||
resumeGeneration,
|
||||
startGeneration,
|
||||
stopGeneration,
|
||||
stopGenerationTimer,
|
||||
updateResultField,
|
||||
validateResults,
|
||||
|
||||
Reference in New Issue
Block a user