Files
YG_FT/frontend/src/views/data-process/create/useDataProcessGeneration.ts

449 lines
16 KiB
TypeScript
Raw Normal View History

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<string | null>
dirty: Ref<boolean>
beforeGenerate?: () => Promise<boolean | void>
}
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<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,
message: '确认摘要后即可开始生成,过程中可查看实时进度。',
})
let generationTimer: ReturnType<typeof setTimeout> | 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,
}
}