Files
YG_FT/frontend/src/views/data-process/create/useDataProcessGeneration.ts
2026-07-30 16:54:00 +08:00

458 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
const generationStarting = ref(false)
const generationRestoring = ref(false)
const canReturnFromGeneration = computed(() => (
generation.status === 'idle' && !generationStarting.value && !generationRestoring.value
))
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.value || generation.status === 'running') return false
const taskId = bindings.taskId.value
if (!taskId) {
ElMessage.error('任务尚未创建,请返回上一步重试')
return false
}
generationStarting.value = 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.value = false
}
}
async function resumeGeneration() {
const taskId = bindings.taskId.value
if (!taskId) return
generationRestoring.value = true
try {
stopGenerationTimer()
const activeRunId = generationRun
pollFailureCount = 0
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 : '查询任务进度失败,请重试。'
} finally {
generationRestoring.value = false
}
}
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,
canReturnFromGeneration,
generation,
generationStarting,
regeneratingResultId,
resultRegenerationBusy,
results,
selectedResultId,
persistResultChanges,
resetDownstream,
regenerateAllResults,
regenerateResult,
resumeGeneration,
startGeneration,
stopGenerationTimer,
updateResultField,
validateResults,
}
}