feat(data-process): 完善后台生成与失败重试

This commit is contained in:
caoxiaozhu
2026-07-28 10:56:05 +08:00
parent 8a6a6574bb
commit 01d2e6c76a
178 changed files with 4472 additions and 595 deletions

View File

@@ -7,6 +7,7 @@ import type {
DataProcessPdfPages,
DataProcessPreviewBuildPayload,
DataProcessPreviewBuildResult,
DataProcessPreviewProgress,
DataProcessPreviewCreatePayload,
DataProcessPreviewItem,
DataProcessPreviewUpdatePayload,
@@ -17,12 +18,16 @@ import type {
DataProcessPublishResult,
DataProcessQualityScore,
DataProcessResult,
DataProcessResultBatchRegeneratePayload,
DataProcessResultBatchRegenerateResult,
DataProcessResultRegeneratePayload,
DataProcessResultUpdatePayload,
DataProcessSourceContent,
DataProcessSourceFile,
DataProcessTask,
DataProcessTaskCreatePayload,
DataProcessTaskUpdatePayload,
DataProcessWorkflowStep,
} from '@/types/dataProcess'
export type {
@@ -41,6 +46,8 @@ export type {
DataProcessPreviewBuildPayload,
DataProcessPreviewBuildFileResult,
DataProcessPreviewBuildResult,
DataProcessPreviewProgress,
DataProcessPreviewStatus,
DataProcessPreviewFileBuildProgress,
DataProcessPreviewFileStatus,
DataProcessPreviewCreatePayload,
@@ -53,6 +60,11 @@ export type {
DataProcessPublishResult,
DataProcessQualityScore,
DataProcessResult,
DataProcessResultBatchRegenerateFailure,
DataProcessResultBatchRegenerateItem,
DataProcessResultBatchRegeneratePayload,
DataProcessResultBatchRegenerateResult,
DataProcessResultRegeneratePayload,
DataProcessResultStatus,
DataProcessResultUpdatePayload,
DataProcessSplit,
@@ -63,6 +75,7 @@ export type {
DataProcessTaskCreatePayload,
DataProcessTaskUpdatePayload,
DataProcessType,
DataProcessWorkflowStep,
DataProcessXlsxActiveSheet,
DataProcessXlsxPreview,
DataProcessXlsxPreviewRow,
@@ -88,6 +101,14 @@ export const createDataProcessTask = (payload: DataProcessTaskCreatePayload) =>
export const updateDataProcessTask = (taskId: string | number, payload: DataProcessTaskUpdatePayload) =>
put<DataProcessTask>(`/data-process/${encodeURIComponent(taskId)}`, payload)
export const updateDataProcessWorkflowStep = (
taskId: string | number,
workflowStep: DataProcessWorkflowStep,
) => put<DataProcessTask>(
`/data-process/${encodeURIComponent(taskId)}/workflow-step`,
{ workflow_step: workflowStep },
)
export const regenerateDataProcessTask = (
taskId: string | number,
payload: DataProcessRegeneratePayload,
@@ -185,6 +206,19 @@ export const buildDataProcessPreview = (
{ timeout: 5 * 60 * 1000 },
)
export const startDataProcessPreview = (
taskId: string | number,
payload: DataProcessPreviewBuildPayload = {},
) => post<DataProcessPreviewProgress>(
`/data-process/${encodeURIComponent(taskId)}/preview/start`,
payload,
)
export const getDataProcessPreviewProgress = (taskId: string | number) =>
get<DataProcessPreviewProgress>(
`/data-process/${encodeURIComponent(taskId)}/preview/progress`,
)
export function getDataProcessPreview(
taskId: string | number,
params: { source_file_id?: string | number; page?: number; page_size?: number; keyword?: string } = {},
@@ -219,12 +253,12 @@ export const deleteDataProcessPreview = (
export const generateDataProcess = (taskId: string | number) =>
post<DataProcessProgress>(`/data-process/${encodeURIComponent(taskId)}/generate`)
export const stopDataProcess = (taskId: string | number) =>
post<DataProcessProgress>(`/data-process/${encodeURIComponent(taskId)}/stop`)
export const getDataProcessProgress = (taskId: string | number) =>
get<DataProcessProgress>(`/data-process/${encodeURIComponent(taskId)}/progress`)
export const confirmDataProcessResults = (taskId: string | number) =>
post<DataProcessTask>(`/data-process/${encodeURIComponent(taskId)}/confirm-results`)
export function getDataProcessResults(
taskId: string | number,
params: { page?: number; page_size?: number; keyword?: string; status?: string; split?: string } = {},
@@ -249,5 +283,24 @@ export const restoreDataProcessResult = (taskId: string | number, resultId: stri
`/data-process/${encodeURIComponent(taskId)}/results/${encodeURIComponent(resultId)}/restore`,
)
export const regenerateDataProcessResult = (
taskId: string | number,
resultId: string | number,
payload: DataProcessResultRegeneratePayload,
) => post<DataProcessResult>(
`/data-process/${encodeURIComponent(taskId)}/results/${encodeURIComponent(resultId)}/regenerate`,
payload,
{ timeout: 210_000 },
)
export const regenerateDataProcessResults = (
taskId: string | number,
payload: DataProcessResultBatchRegeneratePayload,
) => post<DataProcessResultBatchRegenerateResult>(
`/data-process/${encodeURIComponent(taskId)}/results/regenerate-batch`,
payload,
{ timeout: 240_000 },
)
export const publishDataProcess = (taskId: string | number, payload: DataProcessPublishPayload) =>
post<DataProcessPublishResult>(`/data-process/${encodeURIComponent(taskId)}/publish`, payload)

View File

@@ -13,6 +13,15 @@ const showBackButton = computed(() => {
return route.path.split('/').filter(Boolean).length > 1
})
function handleBack() {
const backRouteName = route.meta.backRouteName
if (typeof backRouteName === 'string' && backRouteName) {
void router.push({ name: backRouteName })
return
}
router.back()
}
function usageClass(val?: number) {
return val != null && val > 80 ? 'is-danger' : ''
}
@@ -51,7 +60,7 @@ onUnmounted(() => {
v-if="showBackButton"
class="back-btn"
link
@click="router.back()"
@click="handleBack"
>
<div class="back-icon-wrap">
<i class="fa fa-arrow-left" />

View File

@@ -138,13 +138,19 @@ const routes: RouteRecordRaw[] = [
path: 'data-process/create',
name: 'data-process-create',
component: () => import('@/views/data-process/DataProcessCreateView.vue'),
meta: { title: '新建数据处理任务', pageSurface: 'self' },
meta: { title: '新建数据处理任务', pageSurface: 'self', backRouteName: 'data-process' },
},
{
path: 'data-process/:id/regenerate',
name: 'data-process-regenerate',
component: () => import('@/views/data-process/DataProcessCreateView.vue'),
meta: { title: '重新生成数据处理任务', pageSurface: 'self' },
meta: { title: '重新生成数据处理任务', pageSurface: 'self', backRouteName: 'data-process' },
},
{
path: 'data-process/:id/workflow',
name: 'data-process-workflow',
component: () => import('@/views/data-process/DataProcessCreateView.vue'),
meta: { title: '数据处理任务进度', pageSurface: 'self', backRouteName: 'data-process' },
},
{
path: 'data-process/:id',

View File

@@ -6,6 +6,8 @@ export type DataProcessResultStatus = 'valid' | 'modified' | 'invalid'
export type DataProcessSplit = 'train' | 'validation' | 'test'
export type DataProcessOutputType = 'standard' | 'reasoning'
export type DataProcessReasoningDetail = 'normal' | 'detailed'
export type DataProcessWorkflowStep = 'create' | 'model' | 'upload' | 'preview' | 'generate' | 'results'
export type DataProcessPreviewStatus = 'idle' | 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'
export interface DataProcessPage<T> {
items: T[]
@@ -61,6 +63,14 @@ export interface DataProcessTask {
duration?: string | null
duration_seconds?: number | null
failure_reason?: string | null
results_confirmed?: boolean
workflow_step?: DataProcessWorkflowStep
preview_status?: DataProcessPreviewStatus
preview_progress?: number
preview_failure_reason?: string | null
preview_total_files?: number
preview_completed_files?: number
preview_run_id?: string | null
source_files?: DataProcessSourceFile[]
}
@@ -248,6 +258,18 @@ export interface DataProcessPreviewBuildResult extends DataProcessPage<DataProce
files?: DataProcessPreviewBuildFileResult[]
}
/** 后台切分任务级进度。完成后应重新读取预览条目及源文件统计。 */
export interface DataProcessPreviewProgress {
task_id: string | number
workflow_step: DataProcessWorkflowStep
preview_status: DataProcessPreviewStatus
preview_progress: number
preview_failure_reason?: string | null
preview_total_files?: number
preview_completed_files?: number
preview_run_id?: string | null
}
export interface DataProcessPreviewCreatePayload {
source_file_id?: string | number | null
original_content?: string
@@ -280,6 +302,7 @@ export interface DataProcessProgress {
duplicate_count?: number
error_count?: number
failure_reason?: string | null
results_confirmed?: boolean
updated_at?: string
}
@@ -306,6 +329,36 @@ export interface DataProcessResultUpdatePayload {
expected_updated_at?: string
}
export interface DataProcessResultRegeneratePayload {
expected_updated_at: string
}
export interface DataProcessResultBatchRegenerateItem {
result_id: string
expected_updated_at: string
}
export interface DataProcessResultBatchRegeneratePayload {
items: DataProcessResultBatchRegenerateItem[]
}
export interface DataProcessResultBatchRegenerateFailure {
result_id: string
code: 'conflict' | 'skipped' | 'generation_failed' | 'internal_error'
message: string
}
export interface DataProcessResultBatchRegenerateResult {
batch_id: string
total: number
succeeded: number
failed: number
remaining_invalid_count: number
duration_ms: number
items: DataProcessResult[]
failures: DataProcessResultBatchRegenerateFailure[]
}
export interface DataProcessQualityScore {
overall?: number
completeness?: number

View File

@@ -2,12 +2,18 @@
* 将数据处理任务的执行状态与当前轮发布指针合并为用户可理解的状态。
* 上一轮保留的 output_datasets 不参与判断,避免将尚未重新发布的当前轮误标为已发布。
*
* @param {{ status?: string, output_dataset_id?: string | number | null }} task
* @param {{ status?: string, preview_status?: string, output_dataset_id?: string | number | null, results_confirmed?: boolean }} task
* @returns {{ label: string, type: 'primary' | 'success' | 'warning' | 'danger' | 'info' }}
*/
export function dataProcessDisplayStatus(task) {
if (task.preview_status === 'running' || task.preview_status === 'queued') {
return { label: '切分中', type: 'primary' }
}
if (task.preview_status === 'failed') return { label: '切分失败', type: 'danger' }
if (task.preview_status === 'cancelled') return { label: '切分已取消', type: 'warning' }
if (task.status === 'running') return { label: '生成中', type: 'primary' }
if (task.status === 'completed') {
if (task.results_confirmed === false) return { label: '待确认', type: 'warning' }
return task.output_dataset_id
? { label: '已发布', type: 'success' }
: { label: '已生成', type: 'warning' }

View File

@@ -27,6 +27,7 @@ import {
} from './create/useDataProcessSourceUpload'
import { useModelsStore } from '@/stores/models'
import {
confirmDataProcessResults,
createDataProcessPreview,
createDataProcessTask,
deleteDataProcessPreview,
@@ -37,10 +38,16 @@ import {
testDataProcessExternalSource,
updateDataProcessPreview,
updateDataProcessTask,
updateDataProcessWorkflowStep,
type DataProcessExternalSourcePayload,
type DataProcessPreviewItem,
} from '@/api/modules/dataProcess'
import type { DataProcessConfig } from '@/types/dataProcess'
import type {
DataProcessConfig,
DataProcessPreviewProgress,
DataProcessTask,
DataProcessWorkflowStep,
} from '@/types/dataProcess'
import type {
ExternalDataSource,
GenerationControlOptions,
@@ -81,7 +88,11 @@ const modelSelectionOptions = computed<GenerationControlOptions>(() => (
))
const uploadedFiles = ref<UploadedDataFile[]>([])
const previewBuilding = ref(false)
const { buildPreviewsByFile } = useDataProcessPreviewBuild()
const {
resumePreviewBuild,
startPreviewBuild,
stopPreviewPolling,
} = useDataProcessPreviewBuild()
const externalSource = reactive<ExternalDataSource>({
type: 'postgresql',
url: '',
@@ -104,14 +115,18 @@ const dirty = ref(false)
const modelSubmitLoading = ref(false)
let allowLeave = false
const {
bulkRegeneration,
generation,
regeneratingResultId,
resultRegenerationBusy,
results,
selectedResultId,
persistResultChanges,
resetDownstream,
restoreResult,
regenerateAllResults,
regenerateResult,
resumeGeneration,
startGeneration,
stopGeneration,
stopGenerationTimer,
updateResultField,
validateResults,
@@ -241,6 +256,26 @@ function taskPayload() {
}
}
async function persistWorkflowStep(step: StepId) {
if (!taskId.value) return
await updateDataProcessWorkflowStep(taskId.value, step as DataProcessWorkflowStep)
}
async function saveTaskConfiguration() {
if (isRegeneration.value) {
const regenerated = await prepareRegeneration(taskPayload())
taskId.value = String(regenerated.task.id)
dirty.value = false
return regenerated.task
}
const saved = taskId.value
? await updateDataProcessTask(taskId.value, taskPayload())
: await createDataProcessTask(taskPayload())
taskId.value = String(saved.id)
dirty.value = false
return saved
}
function externalPayload(): DataProcessExternalSourcePayload {
return {
type: 'postgresql',
@@ -260,6 +295,7 @@ function mapPreviewItem(item: DataProcessPreviewItem): PreviewItem {
sourceFileId: String(item.source_file_id),
originalContent: item.original_content,
editedContent: item.edited_content,
savedEditedContent: item.edited_content,
sourceStart: item.source_start,
sourceEnd: item.source_end,
sourceStartLine: item.source_start_line,
@@ -273,6 +309,18 @@ function mapPreviewItem(item: DataProcessPreviewItem): PreviewItem {
}
}
async function loadAllPreviewItems() {
if (!taskId.value) return []
const first = await getDataProcessPreview(taskId.value, { page: 1, page_size: 500 })
const items = [...first.items]
const pages = Math.ceil(first.total / first.page_size)
for (let page = 2; page <= pages; page += 1) {
const next = await getDataProcessPreview(taskId.value, { page, page_size: 500 })
items.push(...next.items)
}
return items.map(mapPreviewItem)
}
function previewAffectingOptions() {
return previewAffectingOptionsFor(processType.value, structuredOptions.value, unstructuredOptions.value)
}
@@ -291,6 +339,7 @@ function buildPreviewSignature() {
}
const {
isRegeneration,
isWorkflowResume,
originalProcessType,
regenerationPrepared,
hydrating,
@@ -299,8 +348,6 @@ const {
confirmPreviewConfigChange,
prepareRegeneration,
confirmStartGeneration,
returnToDetail,
leaveWarning,
} = useDataProcessRegeneration({
task,
processType,
@@ -324,8 +371,6 @@ async function beforeStartGeneration() {
syncPreviewChanges,
)
}
const leaveWarningMessage = computed(() => leaveWarning(generation.status))
const leaveConfirmText = computed(() => isRegeneration.value ? '返回详情' : '放弃修改')
watch(
[() => task.name, () => task.description, processType, structuredOptions, unstructuredOptions, externalSource],
() => {
@@ -504,6 +549,14 @@ async function nextFromCreate() {
confirmDialogRef.value?.open(options) ?? Promise.resolve(false)
))
if (!confirmed) return
if (!isRegeneration.value) {
try {
await saveTaskConfiguration()
await persistWorkflowStep('model')
} catch {
return
}
}
goToStep('model')
}
@@ -527,18 +580,17 @@ async function nextFromModel() {
if (isRegeneration.value) {
const regenerated = await prepareRegeneration(taskPayload())
taskId.value = String(regenerated.task.id)
dirty.value = false
if (regenerated.preview_invalidated) {
ElMessage.info('切分配置已保存,后续将按新配置重新切分;点击“开始生成”前,原生成结果和已发布数据保持不变')
} else if (regenerated.published_outputs_preserved) {
ElMessage.info('重新生成配置已保存;点击“开始生成”前,原生成结果和已发布数据保持不变')
}
} else {
const saved = taskId.value
? await updateDataProcessTask(taskId.value, taskPayload())
: await createDataProcessTask(taskPayload())
taskId.value = String(saved.id)
await saveTaskConfiguration()
}
dirty.value = true
await persistWorkflowStep('upload')
dirty.value = false
goToStep('upload')
} catch {
// 请求层已展示名称冲突或配置非法等具体原因。
@@ -547,6 +599,76 @@ async function nextFromModel() {
}
}
function applyPreviewProgress(progress: DataProcessPreviewProgress) {
const active = progress.preview_status === 'queued' || progress.preview_status === 'running'
previewBuilding.value = active
for (const file of uploadedFiles.value) {
if (!file.sourceFileId || file.previewStatus === 'success') continue
if (active) {
file.previewStatus = 'processing'
file.previewProgress = Math.max(0, Math.min(99, Number(progress.preview_progress) || 0))
file.previewError = undefined
} else if (progress.preview_status === 'failed' || progress.preview_status === 'cancelled') {
file.previewStatus = 'failed'
file.previewProgress = 0
file.previewError = progress.preview_failure_reason || '切分失败,请重试'
}
}
}
async function completePreviewWorkspace() {
previewItems.value = await loadAllPreviewItems()
const configSignature = buildPreviewConfigSignature()
const previewCounts = new Map<string, number>()
for (const item of previewItems.value) {
previewCounts.set(item.sourceFileId, (previewCounts.get(item.sourceFileId) || 0) + 1)
}
for (const file of uploadedFiles.value) {
const count = previewCounts.get(String(file.sourceFileId)) || 0
file.previewCount = count
file.previewStatus = count > 0 ? 'success' : 'failed'
file.previewProgress = count > 0 ? 100 : 0
file.previewError = count > 0 ? undefined : '该文件没有生成可用的预览条目'
file.previewConfigSignature = count > 0 ? configSignature : undefined
}
const failedCount = uploadedFiles.value.filter((file) => file.previewStatus === 'failed').length
if (failedCount || !previewItems.value.length) {
ElMessage.warning(`${failedCount || 1} 个文件切分失败;修正问题后可重新切分`)
return false
}
selectedPreviewFileId.value = String(uploadedFiles.value[0]?.uid ?? '') || null
selectedPreviewId.value = activePreviewItems.value[0]?.id ?? null
selectedPreviewIdsByFile.value = selectedPreviewId.value && selectedPreviewFileId.value
? { [selectedPreviewFileId.value]: selectedPreviewId.value }
: {}
previewSignature.value = buildPreviewSignature()
resetDownstream()
await persistWorkflowStep('preview')
dirty.value = false
goToStep('preview')
return true
}
async function monitorPreviewBuild(sourceFileIds: string[], resume = false) {
if (!taskId.value) return
previewBuilding.value = true
try {
const finalProgress = resume
? await resumePreviewBuild(taskId.value, applyPreviewProgress)
: await startPreviewBuild(taskId.value, sourceFileIds, applyPreviewProgress)
applyPreviewProgress(finalProgress)
if (finalProgress.preview_status === 'completed') {
await completePreviewWorkspace()
} else if (finalProgress.preview_status === 'failed') {
ElMessage.warning(finalProgress.preview_failure_reason || '文件切分失败,请修正后重试')
}
} catch {
// 请求层已经展示错误;后端运行状态可在重新进入任务后继续查询。
} finally {
previewBuilding.value = false
}
}
async function nextFromUpload() {
if (previewBuilding.value || sourceUploading.value) return
if (!taskId.value) {
@@ -586,59 +708,12 @@ async function nextFromUpload() {
file.previewCount = undefined
}
previewBuilding.value = true
try {
const pendingFileIds = uploadedFiles.value
.filter((file) => file.previewStatus !== 'success' || file.previewConfigSignature !== configSignature)
.map((file) => file.sourceFileId)
.filter((fileId): fileId is string => Boolean(fileId))
// 构建接口是同步响应:请求中只展示真实的“处理中”,响应成功后才记为 100%。
await buildPreviewsByFile(taskId.value, pendingFileIds, (progress) => {
const file = uploadedFiles.value.find((item) => (
String(item.sourceFileId) === String(progress.source_file_id)
))
if (!file) return
file.previewStatus = progress.status
file.previewProgress = progress.progress
file.previewCount = progress.preview_count
file.previewError = progress.error
if (progress.status === 'success') file.previewConfigSignature = configSignature
})
const failedCount = uploadedFiles.value.filter((file) => file.previewStatus === 'failed').length
if (failedCount || !allFilesSucceeded()) {
ElMessage.warning(`${failedCount || 1} 个文件切分失败;修正问题后点击“继续:数据预览”即可重试`)
return
}
try {
const first = await getDataProcessPreview(taskId.value, { page: 1, page_size: 500 })
const items = [...first.items]
const pages = Math.ceil(first.total / first.page_size)
for (let page = 2; page <= pages; page += 1) {
const next = await getDataProcessPreview(taskId.value, { page, page_size: 500 })
items.push(...next.items)
}
previewItems.value = items.map(mapPreviewItem)
} catch {
ElMessage.warning('文件切分已经完成,但预览加载失败;请再次点击继续重试加载')
return
}
if (!previewItems.value.length) {
ElMessage.warning('源文件没有生成可用的预览条目,请检查文件内容和预处理配置')
return
}
selectedPreviewFileId.value = String(uploadedFiles.value[0]?.uid ?? '') || null
selectedPreviewId.value = activePreviewItems.value[0]?.id ?? null
selectedPreviewIdsByFile.value = selectedPreviewId.value && selectedPreviewFileId.value
? { [selectedPreviewFileId.value]: selectedPreviewId.value }
: {}
previewSignature.value = signature
resetDownstream()
goToStep('preview')
} finally {
previewBuilding.value = false
}
const pendingFileIds = uploadedFiles.value
.filter((file) => file.previewStatus !== 'success' || file.previewConfigSignature !== configSignature)
.map((file) => file.sourceFileId)
.filter((fileId): fileId is string => Boolean(fileId))
await persistWorkflowStep('upload')
await monitorPreviewBuild(pendingFileIds)
}
function selectPreviewFile(fileId: string) {
@@ -668,7 +743,9 @@ function updatePreviewContent(id: string, value: string) {
async function syncPreviewChanges() {
if (!taskId.value) throw new Error('任务尚未创建')
const changedItems = previewItems.value.filter((item) => item.status === 'modified' || item.status === 'manual')
const changedItems = previewItems.value.filter((item) => (
item.editedContent !== item.savedEditedContent
))
for (const item of changedItems) {
const saved = await updateDataProcessPreview(taskId.value, item.id, {
edited_content: item.editedContent,
@@ -751,21 +828,67 @@ async function handlePrimaryAction() {
ElMessage.warning('当前没有可生成的预览内容')
return
}
try {
await syncPreviewChanges()
await persistWorkflowStep('generate')
} catch {
return
}
dirty.value = false
goToStep('generate')
return
}
if (currentStepId.value === 'generate') {
if (generation.status === 'success') {
try {
await persistWorkflowStep('results')
} catch {
return
}
dirty.value = false
goToStep('results')
} else if (generation.status !== 'running') {
await startGeneration()
await handleStartGeneration()
}
return
}
await saveTask()
}
function handleBack() {
async function handleStartGeneration() {
try {
await persistWorkflowStep('generate')
} catch {
return
}
const started = await startGeneration()
if (!started) return
dirty.value = false
}
async function persistWorkspaceForStep(targetStep: StepId) {
if (isRegeneration.value && !dirty.value) return
if (currentStepId.value === 'create') {
if (!taskId.value && !task.name.trim()) return
const valid = await taskSetupRef.value?.validate()
if (!valid) throw new Error('请先完善任务信息')
await saveTaskConfiguration()
} else if (currentStepId.value === 'model' && dirty.value) {
await saveTaskConfiguration()
} else if (currentStepId.value === 'preview') {
await syncPreviewChanges()
} else if (currentStepId.value === 'results') {
await persistResultChanges()
}
if (taskId.value) await persistWorkflowStep(targetStep)
dirty.value = false
}
async function persistWorkspaceBeforeLeave() {
await persistWorkspaceForStep(currentStepId.value)
}
async function handleBack() {
if (sourceUploading.value) {
ElMessage.warning('请等待当前文件上传完成')
return
@@ -774,15 +897,25 @@ function handleBack() {
ElMessage.warning('请等待当前文件切分完成')
return
}
if (generation.status === 'running') {
ElMessage.warning('请先停止当前生成任务')
return
if (currentStepId.value === 'generate') return
if (currentStep.value > 0) {
const targetStep = WIZARD_STEPS[currentStep.value - 1]?.id
if (!targetStep) return
try {
await persistWorkspaceForStep(targetStep)
} catch {
return
}
currentStep.value -= 1
}
if (currentStep.value > 0) currentStep.value -= 1
}
async function saveTask() {
if (!taskId.value) {
ElMessage.error('任务尚未创建,请返回上一步重试')
return
}
if (!validateResults()) {
ElMessage.warning('请先修正校验失败的结果')
return
@@ -796,58 +929,103 @@ async function saveTask() {
ElMessage.warning('仍有结果未通过后端质量校验,请继续修正')
return
}
try {
await persistWorkflowStep('results')
await confirmDataProcessResults(taskId.value)
} catch {
return
}
dirty.value = false
allowLeave = true
ElMessage.success('数据处理任务已保存')
await router.push('/data-process')
ElMessage.success('生成结果已确认')
await router.replace({ name: 'data-process-detail', params: { id: taskId.value } })
}
async function handleCancel() {
if (!dirty.value) {
await returnToPreviousPage()
return
}
const confirmed = await confirmDialogRef.value?.open({
title: '确认离开当前页面?',
message: leaveWarningMessage.value,
confirmText: leaveConfirmText.value,
cancelText: '继续编辑',
tone: 'danger',
})
if (!confirmed) return
await returnToPreviousPage()
}
async function returnToPreviousPage() {
allowLeave = true
if (isRegeneration.value && await returnToDetail()) return
router.back()
await router.push({ name: 'data-process' })
}
onBeforeRouteLeave(async () => {
if (sourceUploading.value) {
ElMessage.warning('请等待当前文件上传完成后再离开页面')
onBeforeRouteLeave(async (to) => {
if (allowLeave) return true
if (to.name === 'data-process' && taskId.value) {
try {
await persistWorkspaceBeforeLeave()
allowLeave = true
return true
} catch {
ElMessage.error('当前步骤自动保存失败,已停留在本页,请重试')
return false
}
}
try {
await persistWorkspaceBeforeLeave()
allowLeave = true
return true
} catch {
ElMessage.error('当前步骤自动保存失败,已停留在本页,请重试')
return false
}
if (allowLeave || !dirty.value) return true
const confirmed = await confirmDialogRef.value?.open({
title: '确认离开当前页面?',
message: leaveWarningMessage.value,
confirmText: leaveConfirmText.value,
cancelText: '继续编辑',
tone: 'danger',
})
if (confirmed) allowLeave = true
return Boolean(confirmed)
})
function inferWorkflowStep(sourceTask: DataProcessTask): StepId {
if (sourceTask.status === 'running' || sourceTask.status === 'failed' || sourceTask.status === 'stopped') {
return 'generate'
}
if (sourceTask.status === 'completed' && sourceTask.results_confirmed === false) return 'generate'
if (Number(sourceTask.preview_count || previewItems.value.length) > 0) return 'preview'
if ((sourceTask.source_files || []).length > 0) return 'upload'
return 'model'
}
async function initializeExistingWorkflow() {
const sourceTask = await loadRegenerationSource()
if (!sourceTask) return
taskId.value = String(sourceTask.id)
if (!isWorkflowResume.value) return
if (sourceTask.status === 'completed' && sourceTask.results_confirmed !== false) {
allowLeave = true
await router.replace({ name: 'data-process-detail', params: { id: sourceTask.id } })
return
}
if (sourceTask.preview_status === 'queued' || sourceTask.preview_status === 'running') {
goToStep('upload')
applyPreviewProgress({
task_id: sourceTask.id,
workflow_step: 'upload',
preview_status: sourceTask.preview_status,
preview_progress: Number(sourceTask.preview_progress) || 0,
preview_run_id: sourceTask.preview_run_id,
preview_failure_reason: sourceTask.preview_failure_reason,
preview_total_files: sourceTask.preview_total_files,
preview_completed_files: sourceTask.preview_completed_files,
})
const sourceFileIds = uploadedFiles.value
.map((file) => file.sourceFileId)
.filter((fileId): fileId is string => Boolean(fileId))
void monitorPreviewBuild(sourceFileIds, true)
return
}
let resumeStep = (sourceTask.workflow_step || inferWorkflowStep(sourceTask)) as StepId
if (sourceTask.preview_status === 'failed') resumeStep = 'upload'
if (sourceTask.status === 'running') resumeStep = 'generate'
if (resumeStep === 'preview' && !previewItems.value.length) resumeStep = 'upload'
if (resumeStep === 'results' && sourceTask.status !== 'completed') resumeStep = 'generate'
goToStep(resumeStep)
if (resumeStep === 'generate' || resumeStep === 'results') await resumeGeneration()
}
onBeforeUnmount(() => {
stopPreviewPolling()
stopGenerationTimer()
})
onMounted(() => {
localStorage.removeItem('yg-data-process-create-draft')
void modelsStore.load(true)
if (isRegeneration.value) void loadRegenerationSource()
if (isRegeneration.value || isWorkflowResume.value) void initializeExistingWorkflow()
})
</script>
@@ -954,8 +1132,7 @@ onMounted(() => {
:preview-count="previewItems.length"
:modified-count="modifiedPreviewCount"
:generation="generation"
@stop="stopGeneration"
@retry="startGeneration"
@retry="handleStartGeneration"
/>
<ResultEditorStep
@@ -963,8 +1140,11 @@ onMounted(() => {
v-model:selected-id="selectedResultId"
:items="results"
:preview-items="previewItems"
:regenerating-result-id="regeneratingResultId"
:bulk-regeneration="bulkRegeneration"
@update:field="updateResultField"
@restore:item="restoreResult"
@regenerate:all="regenerateAllResults"
@regenerate:item="regenerateResult"
/>
</div>
</div>
@@ -972,7 +1152,11 @@ onMounted(() => {
<footer class="wizard-footer">
<div class="footer-left">
<el-button v-if="currentStep > 0" :disabled="previewBuilding || sourceUploading" @click="handleBack">
<el-button
v-if="currentStep > 0"
:disabled="currentStepId === 'generate' || previewBuilding || sourceUploading"
@click="handleBack"
>
<i class="fa fa-arrow-left" style="margin-right: 6px;" /> 返回{{ previousStepLabel }}
</el-button>
<el-button v-else @click="handleCancel">{{ isRegeneration ? '返回详情' : '取消' }}</el-button>
@@ -983,8 +1167,8 @@ onMounted(() => {
<el-button
class="wizard-primary-action"
type="primary"
:loading="modelSubmitLoading || generation.status === 'running' || (currentStepId === 'upload' && (sourceUploading || previewBuilding))"
:disabled="hydrating || modelSubmitLoading || Boolean(initializationError) || (currentStepId === 'generate' && generation.status === 'running') || previewBuilding || sourceUploading || (currentStepId === 'upload' && hasUnfinishedUploads)"
:loading="modelSubmitLoading || generation.status === 'running' || resultRegenerationBusy || (currentStepId === 'upload' && (sourceUploading || previewBuilding))"
:disabled="hydrating || modelSubmitLoading || Boolean(initializationError) || resultRegenerationBusy || (currentStepId === 'generate' && generation.status === 'running') || previewBuilding || sourceUploading || (currentStepId === 'upload' && hasUnfinishedUploads)"
@click="handlePrimaryAction"
>
{{ primaryActionLabel }} <i class="fa" :class="primaryActionIcon" style="margin-left: 6px;" />

View File

@@ -286,7 +286,12 @@ async function loadTask(silent = false) {
if (!silent) loading.value = true
loadError.value = ''
try {
detail.value = await getDataProcessTask(taskId.value)
const task = await getDataProcessTask(taskId.value)
if (task.status !== 'completed' || task.results_confirmed === false) {
await router.replace({ name: 'data-process-workflow', params: { id: task.id } })
return
}
detail.value = task
} catch {
detail.value = null
loadError.value = '数据处理任务加载失败,任务可能已删除或当前无权访问。'

View File

@@ -4,6 +4,7 @@ import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import DataTablePage from '@/components/DataTablePage.vue'
import { deleteDataProcessTask, getDataProcessTasks } from '@/api/modules/dataProcess'
import { usePolling } from '@/composables/usePolling'
import type { DataProcessTask, DataProcessType } from '@/types/dataProcess'
import { dataProcessDisplayStatus } from '@/utils/dataProcessStatus.js'
@@ -25,6 +26,12 @@ async function loadData(silent = false) {
try {
const response = await getDataProcessTasks({ page: 1, page_size: 200 })
dataList.value = response.items
if (dataList.value.some((item) => (
item.status === 'running'
|| item.preview_status === 'running'
|| item.preview_status === 'queued'
))) startPolling()
else stopPolling()
} catch {
loadError.value = '数据处理任务加载失败,请稍后重试。'
} finally {
@@ -34,8 +41,11 @@ async function loadData(silent = false) {
/** 查看任务详情 */
function viewDetail(row: unknown) {
const taskId = (row as { id: string | number }).id
router.push({ name: 'data-process-detail', params: { id: taskId } })
const task = row as DataProcessTask
const routeName = task.status === 'completed' && task.results_confirmed !== false
? 'data-process-detail'
: 'data-process-workflow'
router.push({ name: routeName, params: { id: task.id } })
}
/** 删除由后端再次校验任务状态以及发布锁。 */
@@ -86,7 +96,13 @@ function generatedCountLabel(task: DataProcessTask) {
return `${safeCount(task.output_count)}`
}
onMounted(loadData)
const { start: startPolling, stop: stopPolling } = usePolling(
() => loadData(true),
3000,
{ immediate: false },
)
onMounted(() => void loadData())
</script>
<template>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import type { ModelItem } from '@/types'
import type { GenerationControlOptions } from './types'
import {
@@ -17,6 +18,43 @@ const emit = defineEmits<{
'update:options': [value: GenerationControlOptions]
}>()
const promptInputRef = ref<any>(null)
const promptHighlightRef = ref<HTMLElement | null>(null)
function splitPrompt(prompt: string) {
const parts = (prompt || '').split('{{ content }}')
const result: Array<{ text: string, isVar: boolean }> = []
parts.forEach((part, index) => {
if (part) {
result.push({ text: part, isVar: false })
}
if (index < parts.length - 1) {
result.push({ text: '{{ content }}', isVar: true })
}
})
return result
}
function syncScroll(e: Event) {
if (promptHighlightRef.value) {
promptHighlightRef.value.scrollTop = (e.target as HTMLElement).scrollTop
}
}
onMounted(() => {
const textarea = promptInputRef.value?.textarea
if (textarea) {
textarea.addEventListener('scroll', syncScroll)
}
})
onBeforeUnmount(() => {
const textarea = promptInputRef.value?.textarea
if (textarea) {
textarea.removeEventListener('scroll', syncScroll)
}
})
function updateField<K extends keyof GenerationControlOptions>(
field: K,
value: GenerationControlOptions[K],
@@ -24,6 +62,10 @@ function updateField<K extends keyof GenerationControlOptions>(
emit('update:options', { ...props.options, [field]: value })
}
function resetPrompt() {
updateField('generationPrompt', defaultGenerationPrompt(props.options.outputType))
}
function updateQualityRules(value: Array<string | number>) {
const rules = value.filter((item): item is string => typeof item === 'string')
emit('update:options', {
@@ -104,24 +146,36 @@ function modelMeta(model: ModelItem) {
<div class="model-field">
<div class="field-copy">
<strong>默认提示语</strong>
<div class="field-header" style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
<strong>默认提示语</strong>
<el-button link type="primary" @click="resetPrompt" style="font-size: 12px; padding: 0;">恢复默认</el-button>
</div>
<small v-if="options.outputType === 'reasoning'">
当前使用思维链专用提示语系统还会按所选详细程度约束推理结构
</small>
<small v-else>当前使用标准回答提示语只要求问题和最终答案</small>
</div>
<el-input
class="prompt-input"
:model-value="options.generationPrompt"
type="textarea"
:rows="4"
maxlength="500"
show-word-limit
resize="vertical"
placeholder="请输入模型生成内容时需要遵循的要求"
aria-label="模型默认提示语"
@update:model-value="updateField('generationPrompt', $event)"
/>
<div class="prompt-input-wrapper">
<div ref="promptHighlightRef" class="prompt-highlight-layer" aria-hidden="true">
<template v-for="(part, index) in splitPrompt(options.generationPrompt)" :key="index">
<span v-if="part.isVar" class="highlight-var">{{ part.text }}</span>
<span v-else>{{ part.text }}</span>
</template>
</div>
<el-input
ref="promptInputRef"
class="prompt-input"
:model-value="options.generationPrompt"
type="textarea"
:rows="6"
maxlength="2000"
show-word-limit
resize="vertical"
placeholder="请输入模型生成内容时需要遵循的要求"
aria-label="模型默认提示语"
@update:model-value="updateField('generationPrompt', $event)"
/>
</div>
<div class="prompt-variables-hint">
默认提示语已包含 <code v-text="'{{ content }}'" />运行时会替换为当前数据内容
可移动该占位符来调整内容在提示语中的位置
@@ -353,6 +407,50 @@ function modelMeta(model: ModelItem) {
.model-select { width: 100%; }
.prompt-input { width: 100%; }
.prompt-input-wrapper {
position: relative;
width: 100%;
}
.prompt-highlight-layer {
position: absolute;
top: 1px;
left: 1px;
right: 1px;
bottom: 1px;
padding: 5px 11px;
box-sizing: border-box;
pointer-events: none;
font-family: inherit;
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
color: #344054;
overflow-y: auto;
z-index: 1;
}
.prompt-highlight-layer::-webkit-scrollbar {
display: none;
}
.highlight-var {
color: #5b50f2;
background: #f0f0ff;
border-radius: 4px;
padding: 0 4px;
}
.prompt-input :deep(.el-textarea__inner) {
color: transparent !important;
caret-color: #344054;
background: transparent !important;
z-index: 2;
position: relative;
}
.model-option { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.model-option small { color: #98a2b3; font-size: 11px; }
.model-empty { padding: 12px; color: #8a93a3; font-size: 12px; text-align: center; }
@@ -378,10 +476,6 @@ function modelMeta(model: ModelItem) {
.output-type-select {
width: 150px;
:deep(.el-select__wrapper) {
min-height: 44px;
}
}
.output-type-field {

View File

@@ -11,7 +11,6 @@ defineProps<{
}>()
const emit = defineEmits<{
stop: []
retry: []
}>()
</script>
@@ -44,7 +43,7 @@ const emit = defineEmits<{
{{ generation.status === 'idle' ? '准备开始处理'
: generation.status === 'running' ? '正在生成数据'
: generation.status === 'success' ? '数据生成完成'
: '生成已停止' }}
: '生成失败' }}
</h3>
<p>{{ generation.message }}</p>
<el-progress
@@ -58,9 +57,6 @@ const emit = defineEmits<{
<span>应用预览修改</span>
<span>生成标准结果</span>
</div>
<el-button v-if="generation.status === 'running'" plain type="warning" @click="emit('stop')">
停止生成
</el-button>
<el-button v-if="generation.status === 'failed'" plain type="primary" @click="emit('retry')">
重新生成
</el-button>

View File

@@ -1,23 +1,45 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { PreviewItem, ResultItem } from './types'
import type { BulkResultRegenerationState, PreviewItem, ResultItem } from './types'
const props = defineProps<{
items: ResultItem[]
previewItems: PreviewItem[]
selectedId: string | null
regeneratingResultId: string | null
bulkRegeneration: BulkResultRegenerationState
}>()
const emit = defineEmits<{
'update:selectedId': [value: string]
'update:field': [id: string, field: 'instruction' | 'input' | 'output', value: string]
'restore:item': [id: string]
'regenerate:item': [id: string]
'regenerate:all': []
}>()
const search = ref('')
const invalidOnly = ref(false)
const selectedItem = computed(() => props.items.find((item) => item.id === props.selectedId) ?? props.items[0])
const selectedIndex = computed(() => props.items.findIndex((item) => item.id === selectedItem.value?.id))
const invalidCount = computed(() => (
props.items.filter((item) => item.savedStatus === 'invalid').length
))
const bulkRegenerationActive = computed(() => props.bulkRegeneration.status === 'running')
const bulkRegenerationVisible = computed(() => (
props.bulkRegeneration.status !== 'idle' && props.bulkRegeneration.total > 0
))
const bulkRegenerationPercentage = computed(() => (
props.bulkRegeneration.total > 0
? Math.round((props.bulkRegeneration.completed / props.bulkRegeneration.total) * 100)
: 0
))
const itemRegenerating = (id: string) => (
props.regeneratingResultId === id
|| props.bulkRegeneration.targetIds.includes(id)
)
const selectedItemRegenerating = computed(() => (
selectedItem.value ? itemRegenerating(selectedItem.value.id) : false
))
const selectedSource = computed(() => {
const previewItemId = selectedItem.value?.previewItemId
if (!previewItemId) return null
@@ -67,8 +89,34 @@ function selectRelative(offset: number) {
<template>
<section class="result-step">
<div class="result-workspace">
<aside class="result-list-pane">
<div class="pane-header"><strong>生成结果</strong><span> {{ items.length }} </span></div>
<aside class="result-list-pane" :class="{ 'has-bulk-progress': bulkRegenerationVisible }">
<div class="pane-header result-list-header">
<div class="result-list-title"><strong>生成结果</strong><span> {{ items.length }} </span></div>
<el-button
v-if="invalidCount > 0"
size="small"
plain
type="primary"
:loading="bulkRegenerationActive"
:disabled="Boolean(regeneratingResultId) || bulkRegenerationActive"
@click="emit('regenerate:all')"
>
<i v-if="!bulkRegenerationActive" class="fa fa-refresh" style="margin-right: 4px;" />
{{ bulkRegenerationActive ? '重新生成中' : `全部重新生成(${invalidCount}` }}
</el-button>
</div>
<div v-if="bulkRegenerationVisible" class="bulk-regeneration-progress">
<div>
<span>已处理 {{ bulkRegeneration.completed }} / {{ bulkRegeneration.total }}</span>
<span>成功 {{ bulkRegeneration.succeeded }} · 失败 {{ bulkRegeneration.failed }}</span>
</div>
<el-progress
:percentage="bulkRegenerationPercentage"
:show-text="false"
:stroke-width="5"
:color="bulkRegeneration.failed > 0 ? '#d97706' : '#5b50f2'"
/>
</div>
<div class="result-toolbar">
<el-input v-model="search" clearable size="small" placeholder="搜索结果">
<template #prefix><i class="fa fa-search" /></template>
@@ -89,7 +137,12 @@ function selectRelative(offset: number) {
<strong>{{ item.instruction || '未填写指令' }}</strong>
<small>{{ item.output || '未填写输出' }}</small>
</span>
<i class="fa" :class="item.status === 'invalid' ? 'fa-exclamation-circle is-error' : 'fa-check-circle is-valid'" />
<i v-if="itemRegenerating(item.id)" class="css-spinner" />
<i
v-else
class="fa"
:class="item.status === 'invalid' ? 'fa-exclamation-circle is-error' : 'fa-check-circle is-valid'"
/>
</button>
</div>
</aside>
@@ -106,7 +159,19 @@ function selectRelative(offset: number) {
:type="selectedItem.qualityScore >= 80 ? 'success' : selectedItem.qualityScore >= 60 ? 'warning' : 'danger'"
>质量 {{ selectedItem.qualityScore.toFixed(1) }}</el-tag>
</div>
<el-button link @click="emit('restore:item', selectedItem.id)"><i class="fa fa-undo" /> 恢复生成结果</el-button>
<div class="result-header-actions">
<el-button
v-if="selectedItem.savedStatus === 'invalid'"
size="small"
plain
type="primary"
:loading="selectedItemRegenerating"
:disabled="bulkRegenerationActive || (Boolean(regeneratingResultId) && !selectedItemRegenerating)"
@click="emit('regenerate:item', selectedItem.id)"
>
<i v-if="!selectedItemRegenerating" class="fa fa-refresh" style="margin-right: 4px;" /> 重新生成
</el-button>
</div>
</div>
<article class="source-reference" aria-labelledby="source-reference-title">
@@ -126,6 +191,7 @@ function selectRelative(offset: number) {
<label>Instruction <em>必填</em></label>
<el-input
:model-value="selectedItem.instruction"
:disabled="selectedItemRegenerating"
type="textarea"
:rows="3"
@update:model-value="emit('update:field', selectedItem.id, 'instruction', $event)"
@@ -135,6 +201,7 @@ function selectRelative(offset: number) {
<label>Input <span>选填</span></label>
<el-input
:model-value="selectedItem.input"
:disabled="selectedItemRegenerating"
type="textarea"
:rows="2"
@update:model-value="emit('update:field', selectedItem.id, 'input', $event)"
@@ -144,6 +211,7 @@ function selectRelative(offset: number) {
<label>Output <em>必填</em></label>
<el-input
:model-value="selectedItem.output"
:disabled="selectedItemRegenerating"
type="textarea"
:rows="7"
@update:model-value="emit('update:field', selectedItem.id, 'output', $event)"
@@ -155,15 +223,6 @@ function selectRelative(offset: number) {
<div v-else class="validation-success">
<i class="fa fa-check-circle" /> 字段校验通过
</div>
<div v-if="selectedItem.qualityFlags?.length" class="quality-flags">
<el-tag
v-for="flag in selectedItem.qualityFlags"
:key="flag"
size="small"
type="warning"
effect="plain"
>{{ flag }}</el-tag>
</div>
<div class="editor-pagination">
<el-button :disabled="selectedIndex <= 0" @click="selectRelative(-1)">上一条</el-button>
<span>{{ selectedIndex + 1 }} / {{ items.length }}</span>
@@ -194,6 +253,25 @@ function selectRelative(offset: number) {
border-right: 1px solid #e5e8ee;
}
.result-list-title {
min-width: 0;
span {
margin-left: 8px;
color: #8a93a3;
font-size: 11px;
font-weight: 400;
}
}
.result-list-header {
gap: 10px;
:deep(.el-button) {
flex: none;
}
}
.pane-header {
display: flex;
align-items: center;
@@ -227,12 +305,37 @@ function selectRelative(offset: number) {
border-bottom: 1px solid #edf0f5;
}
.bulk-regeneration-progress {
padding: 9px 12px 10px;
background: #fafaff;
border-bottom: 1px solid #e8e7ff;
> div {
display: flex;
justify-content: space-between;
gap: 10px;
margin-bottom: 7px;
color: #667085;
font-size: 10px;
}
}
.result-list {
height: 476px;
padding: 7px;
overflow: auto;
}
.has-bulk-progress .result-list {
height: 420px;
}
.result-header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.result-item {
display: grid;
grid-template-columns: 46px minmax(0, 1fr) 18px;
@@ -294,6 +397,20 @@ function selectRelative(offset: number) {
color: #d97706;
}
.css-spinner {
width: 14px;
height: 14px;
border: 2px solid rgba(91, 80, 242, 0.2);
border-top-color: #5b50f2;
border-radius: 50%;
animation: css-spin 0.8s linear infinite;
display: inline-block;
}
@keyframes css-spin {
to { transform: rotate(360deg); }
}
.result-editor-pane {
min-width: 0;
}
@@ -389,12 +506,6 @@ function selectRelative(offset: number) {
border-radius: 6px;
}
.quality-flags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.validation-error {
color: #b45309;
background: #fff7e8;

View File

@@ -15,16 +15,64 @@ const PREVIOUS_DEFAULT_STANDARD_GENERATION_PROMPT = '你是一名专业的数据
const PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT = '你是一名专业的推理数据生成助手。请严格依据输入内容生成问题、可核验的推理过程和最终答案。推理需要说明关键依据与必要步骤,不得引入来源中不存在的事实;最终答案应准确、完整且语言自然。'
export const DEFAULT_STANDARD_GENERATION_PROMPT = `你是一名专业的数据生成助手。请根据下方来源内容生成可用于监督微调的问答数据。
export const PREVIOUS_DEFAULT_STANDARD_GENERATION_PROMPT_2 = `你是一名专业的数据生成助手。请根据下方来源内容生成可用于监督微调的问答数据。
来源内容:
{{ content }}
要求:严格依据来源生成问题和最终答案,不输出分析、中间思考过程或来源中不存在的信息;答案应准确、完整、语言自然。`
export const DEFAULT_REASONING_GENERATION_PROMPT = `你是一名专业的推理数据生成助手。请根据下方来源内容生成可用于推理模型训练的问答数据。
export const PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT_2 = `你是一名专业的推理数据生成助手。请根据下方来源内容生成可用于推理模型训练的问答数据。
来源内容:
{{ content }}
要求:每条数据必须包含问题、可核验的中间思考过程和最终答案。中间思考过程需说明关键依据、必要步骤和中间计算,不得跳过推理只给结论,也不得引入来源中不存在的事实。`
export const PREVIOUS_DEFAULT_STANDARD_GENERATION_PROMPT_3 = `你是一名专业的数据生成专家。你的任务是根据下方的来源内容提取核心信息并生成高质量的问答数据用于大模型的监督微调SFT
来源内容:
{{ content }}
任务要求:
1. 问题构建:基于来源内容设计问题,问题应当具有实际意义,不仅限于表面的事实问答,可以包括对内容的主旨概括、具体细节的询问或应用场景的探讨。
2. 答案生成:严格依据来源内容生成最终答案,答案需准确无误、逻辑连贯、完整且表达自然流畅。
3. 忠于原文:不输出分析、中间思考过程或任何多余的解释,切勿引入来源内容中未提及的事实或主观臆造的信息。`
export const PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT_3 = `你是一名具有深厚专业知识的推理数据生成专家。你的任务是根据给定的来源内容,生成高质量、逻辑严密且可用于训练推理大语言模型(如 O1 或 DeepSeek-R1的问答数据。
来源内容:
{{ content }}
任务要求:
1. 多维度提问:从来源内容中提取核心概念或关键逻辑,构建具有挑战性的问题,避免简单的信息抽取,提问可以涉及因果分析、方案设计、逻辑推断或跨段落信息整合。
2. 深度思维链CoT
- 必须提供详尽的中间思考过程。
- 在思考过程中,需首先分析问题的核心需求,然后逐步拆解来源内容中的相关事实或数据。
- 对于涉及计算或多步推导的问题,必须一步步展示计算或推导过程,并说明每一步的理论依据或事实来源。
- 如果遇到矛盾或缺失的信息,需在思考过程中指出,并合理假设或推理,但最终答案不得编造来源中完全不相关的事实。
3. 最终答案:在充分推理后给出明确的最终结论。结论应当是对问题的直接回应,语言自然、结构清晰,并且结论必须能从前面的思考过程中自然得出。
数据格式要求:
请确保每条生成的数据清晰包含“问题”、“中间思考过程”和“最终答案”三个部分,且内容充实、专业。`
export const DEFAULT_STANDARD_GENERATION_PROMPT = `你是一名专业的数据生成专家。请基于下方来源内容生成高质量、可直接用于大模型监督微调SFT的问答数据。
来源内容:
{{ content }}
任务要求:
1. 提问有价值:问题应聚焦来源内容的核心信息点,避免无意义的字面抄录或过于宽泛的提问;优先设计需要理解、归纳或推理才能回答的问题,例如主旨概括、细节追问、因果解释或场景应用。
2. 答案忠于来源:严格依据来源内容作答,不得引入来源中不存在的事实、观点或数据;若来源信息不足以完整回答该问题,请如实说明不足,不要编造或臆测。
3. 答案完整且自然:回答应直接回应问题,要点完整、逻辑清晰、语言流畅;涉及多项内容时建议分点陈述,但不必照搬原文措辞。`
export const DEFAULT_REASONING_GENERATION_PROMPT = `你是一名具有深厚专业知识的推理数据生成专家。请基于下方来源内容,生成包含完整思维链、可用于训练推理大语言模型(如 O1、DeepSeek-R1的问答数据。
来源内容:
{{ content }}
任务要求:
1. 提问有挑战性:从来源内容中提炼需要多步思考才能回答的问题,避免可直接定位答案的简单事实抽取;可涉及因果分析、逻辑推断或跨信息整合。
2. 推理可核验:逐步展开思考过程,每个关键结论都应标注其在来源中的依据或必要的中间推导;不得跳过关键步骤只给结论,也不得引入来源中完全不存在的事实。
3. 信息不足时如实处理:遇到来源信息缺失或相互矛盾时,需在推理中明确指出,并基于来源做出合理假设,但不得编造与来源无关的内容。
4. 最终答案须能从前面的推理过程中自然得出,语言简洁、结构清晰。`
export function defaultGenerationPrompt(outputType: 'standard' | 'reasoning') {
return outputType === 'reasoning'
? DEFAULT_REASONING_GENERATION_PROMPT
@@ -32,13 +80,19 @@ export function defaultGenerationPrompt(outputType: 'standard' | 'reasoning') {
}
export function isBuiltInGenerationPrompt(value: string) {
const normalize = (s: string) => s.replace(/\r\n/g, '\n').trim()
const normalizedValue = normalize(value)
return [
LEGACY_DEFAULT_GENERATION_PROMPT,
PREVIOUS_DEFAULT_STANDARD_GENERATION_PROMPT,
PREVIOUS_DEFAULT_STANDARD_GENERATION_PROMPT_2,
PREVIOUS_DEFAULT_STANDARD_GENERATION_PROMPT_3,
PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT,
PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT_2,
PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT_3,
DEFAULT_STANDARD_GENERATION_PROMPT,
DEFAULT_REASONING_GENERATION_PROMPT,
].includes(value)
].map(normalize).includes(normalizedValue)
}
export function createDefaultStructuredOptions(): StructuredProcessOptions {

View File

@@ -123,6 +123,7 @@ export interface PreviewItem {
sourceFileId: string
originalContent: string
editedContent: string
savedEditedContent: string
sourceStart: number | null
sourceEnd: number | null
sourceStartLine: number | null
@@ -151,11 +152,24 @@ export interface ResultItem {
originalInstruction: string
originalInput: string
originalOutput: string
savedInstruction: string
savedInput: string
savedOutput: string
savedStatus: 'valid' | 'modified' | 'invalid'
status: 'valid' | 'modified' | 'invalid'
error?: string
split?: 'train' | 'validation' | 'test'
qualityScore?: number
qualityDetails?: Record<string, number>
qualityFlags?: string[]
updatedAt?: string
}
export interface BulkResultRegenerationState {
status: 'idle' | 'running' | 'completed' | 'partial' | 'failed'
total: number
completed: number
succeeded: number
failed: number
targetIds: string[]
failedIds: string[]
}

View File

@@ -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,

View File

@@ -1,48 +1,139 @@
import { buildDataProcessPreview } from '@/api/modules/dataProcess'
import type { DataProcessPreviewFileBuildProgress } from '@/types/dataProcess'
import { onBeforeUnmount } from 'vue'
import {
getDataProcessPreviewProgress,
startDataProcessPreview,
} from '@/api/modules/dataProcess'
import type {
DataProcessPreviewFileBuildProgress,
DataProcessPreviewProgress,
} from '@/types/dataProcess'
const POLL_INTERVAL_MS = 1500
const MAX_CONSECUTIVE_FAILURES = 3
type PreviewProgressCallback = (progress: DataProcessPreviewProgress) => void
function isActive(progress: DataProcessPreviewProgress) {
return progress.preview_status === 'queued' || progress.preview_status === 'running'
}
function waitForNextPoll() {
return new Promise<void>((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))
}
/**
* 后台切分控制器。
*
* startPreviewBuild 只发起一次 POST随后查询任务级进度resumePreviewBuild
* 不重复启动任务,仅接管已有切分。两者都在 completed/failed 时返回最终进度。
* 响应不依赖逐文件明细,完成后由调用方重新读取预览及源文件统计。
*/
export function useDataProcessPreviewBuild() {
let activePollRun = 0
function stopPreviewPolling() {
activePollRun += 1
}
async function pollUntilSettled(
taskId: string | number,
initialProgress: DataProcessPreviewProgress,
pollRun: number,
onProgress?: PreviewProgressCallback,
) {
let progress = initialProgress
let consecutiveFailures = 0
if (pollRun !== activePollRun) return progress
onProgress?.(progress)
while (isActive(progress) && pollRun === activePollRun) {
await waitForNextPoll()
if (pollRun !== activePollRun) break
try {
progress = await getDataProcessPreviewProgress(taskId)
consecutiveFailures = 0
onProgress?.(progress)
} catch (error) {
consecutiveFailures += 1
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) throw error
}
}
return progress
}
async function startPreviewBuild(
taskId: string | number,
sourceFileIds: Array<string | number>,
onProgress?: PreviewProgressCallback,
): Promise<DataProcessPreviewProgress> {
stopPreviewPolling()
const pollRun = activePollRun
const progress = await startDataProcessPreview(taskId, {
replace_existing: true,
source_file_ids: sourceFileIds,
})
return pollUntilSettled(taskId, progress, pollRun, onProgress)
}
async function resumePreviewBuild(
taskId: string | number,
onProgress?: PreviewProgressCallback,
): Promise<DataProcessPreviewProgress> {
stopPreviewPolling()
const pollRun = activePollRun
const progress = await getDataProcessPreviewProgress(taskId)
return pollUntilSettled(taskId, progress, pollRun, onProgress)
}
/**
* 兼容现有上传步骤的逐文件展示契约;底层仍只启动一个后台切分任务。
* 后端不返回 files[],因此运行中按任务总进度同步展示,完成后调用方
* 必须重新加载预览,不能把这里的可视状态当作文件统计来源。
*/
async function buildPreviewsByFile(
taskId: string | number,
sourceFileIds: Array<string | number>,
onProgress: (progress: DataProcessPreviewFileBuildProgress) => void,
) {
const results: DataProcessPreviewFileBuildProgress[] = []
for (const sourceFileId of sourceFileIds) {
onProgress({ source_file_id: sourceFileId, status: 'processing', progress: 0 })
let progress: DataProcessPreviewFileBuildProgress
try {
const built = await buildDataProcessPreview(taskId, {
replace_existing: true,
source_file_ids: [sourceFileId],
})
const sourceId = String(sourceFileId)
const fileResult = built.files?.find((item) => String(item.source_file_id) === sourceId)
const previewCount = fileResult?.preview_count
?? built.file_counts?.[sourceId]
?? built.items.filter((item) => String(item.source_file_id) === sourceId).length
progress = previewCount > 0
? { source_file_id: sourceFileId, status: 'success', progress: 100, preview_count: previewCount }
: {
source_file_id: sourceFileId,
status: 'failed',
progress: 0,
preview_count: 0,
error: '该文件没有生成可用的预览条目,请检查内容或切分配置',
}
} catch (error) {
progress = {
source_file_id: sourceFileId,
status: 'failed',
progress: 0,
error: error instanceof Error && error.message ? error.message : '切分失败,请重试',
): Promise<DataProcessPreviewFileBuildProgress[]> {
const emitTaskProgress = (taskProgress: DataProcessPreviewProgress) => {
for (const sourceFileId of sourceFileIds) {
if (taskProgress.preview_status === 'completed') {
onProgress({ source_file_id: sourceFileId, status: 'success', progress: 100 })
} else if (taskProgress.preview_status === 'failed' || taskProgress.preview_status === 'cancelled') {
onProgress({
source_file_id: sourceFileId,
status: 'failed',
progress: 0,
error: taskProgress.preview_failure_reason || '切分失败,请重试',
})
} else {
onProgress({
source_file_id: sourceFileId,
status: 'processing',
progress: taskProgress.preview_progress,
})
}
}
results.push(progress)
onProgress(progress)
}
return results
const finalProgress = await startPreviewBuild(taskId, sourceFileIds, emitTaskProgress)
return sourceFileIds.map((sourceFileId) => (
finalProgress.preview_status === 'completed'
? { source_file_id: sourceFileId, status: 'success', progress: 100 }
: {
source_file_id: sourceFileId,
status: 'failed',
progress: 0,
error: finalProgress.preview_failure_reason || '切分失败,请重试',
}
))
}
return { buildPreviewsByFile }
onBeforeUnmount(stopPreviewPolling)
return {
startPreviewBuild,
resumePreviewBuild,
buildPreviewsByFile,
stopPreviewPolling,
}
}

View File

@@ -1,5 +1,5 @@
import { computed, nextTick, ref, type Reactive, type Ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useRoute } from 'vue-router'
import {
getDataProcessPreview,
getDataProcessSourceContent,
@@ -81,11 +81,11 @@ async function loadAllPreviews(taskId: string, mapPreviewItem: RegenerationBindi
export function useDataProcessRegeneration(bindings: RegenerationBindings) {
const route = useRoute()
const router = useRouter()
const isRegeneration = computed(() => route.name === 'data-process-regenerate')
const isWorkflowResume = computed(() => route.name === 'data-process-workflow')
const sourceTaskId = computed(() => (
route.name === 'data-process-regenerate' ? String(route.params.id || '') : ''
isRegeneration.value || isWorkflowResume.value ? String(route.params.id || '') : ''
))
const isRegeneration = computed(() => Boolean(sourceTaskId.value))
const originalProcessType = ref<ProcessType | null>(null)
const originalTaskUpdatedAt = ref('')
const regenerationPrepared = ref(false)
@@ -138,7 +138,7 @@ export function useDataProcessRegeneration(bindings: RegenerationBindings) {
try {
const sourceTask = await getDataProcessTask(sourceTaskId.value)
const sourceType = sourceTask.process_type as ProcessType
originalProcessType.value = sourceType
originalProcessType.value = isRegeneration.value ? sourceType : null
originalTaskUpdatedAt.value = sourceTask.updated_at
bindings.task.name = sourceTask.name
bindings.task.description = sourceTask.description || ''
@@ -151,6 +151,7 @@ export function useDataProcessRegeneration(bindings: RegenerationBindings) {
await hydrateWorkspace(sourceTask, true)
await nextTick()
bindings.dirty.value = false
return sourceTask
} catch (error) {
initializationError.value = error instanceof Error
? error.message
@@ -235,25 +236,10 @@ export function useDataProcessRegeneration(bindings: RegenerationBindings) {
return true
}
async function returnToDetail() {
if (!sourceTaskId.value) return false
await router.replace({ name: 'data-process-detail', params: { id: sourceTaskId.value } })
return true
}
function leaveWarning(generationStatus: string) {
if (isRegeneration.value && regenerationPrepared.value && generationStatus === 'idle') {
return '重新生成配置已保存,但尚未开始生成;现在返回详情不会替换原生成结果或已发布数据,可稍后继续。'
}
if (isRegeneration.value && regenerationPrepared.value) {
return '重新生成已开始;离开页面不会停止服务端处理,已提交的修改不会撤销。'
}
return '当前存在未保存修改,离开后这些修改将不会保留。'
}
return {
sourceTaskId,
isRegeneration,
isWorkflowResume,
originalProcessType,
originalTaskUpdatedAt,
regenerationPrepared,
@@ -264,7 +250,5 @@ export function useDataProcessRegeneration(bindings: RegenerationBindings) {
confirmPreviewConfigChange,
prepareRegeneration,
confirmStartGeneration,
returnToDetail,
leaveWarning,
}
}