feat(data-process): 完善后台生成与失败重试
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user