feat: 新增外部数据源拉取与 DPO 输出格式支持

- 支持从 PostgreSQL 数据库拉取结构化数据作为训练来源
- 新增 DPO (Direct Preference Optimization) 输出类型
- 支持 chosen/rejected 字段的编辑、校验和发布
- 完善数据预处理切分逻辑和元数据管理
- 移除 OCR 扫描 PDF 功能,保持基础文本解析能力

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
caoxiaozhu
2026-08-11 14:17:45 +08:00
parent f809825a7d
commit 5f6e7523cf
26 changed files with 927 additions and 144 deletions

View File

@@ -20,6 +20,7 @@ import {
import { useDataProcessGeneration } from './create/useDataProcessGeneration'
import { useDataProcessPreviewBuild } from './create/useDataProcessPreviewBuild'
import { useDataProcessRegeneration } from './create/useDataProcessRegeneration'
import { createDefaultExternalSource, externalSourcePayload, restoreExternalSourceConfig, sourceConfigForBackend } from './create/externalSourceConfig'
import {
loadCanonicalSourceContent,
mapDataProcessSourceFile,
@@ -39,26 +40,20 @@ import {
updateDataProcessPreview,
updateDataProcessTask,
updateDataProcessWorkflowStep,
type DataProcessExternalSourcePayload,
type DataProcessPreviewItem,
} from '@/api/modules/dataProcess'
import type {
DataProcessConfig,
DataProcessPreviewProgress,
DataProcessTask,
DataProcessWorkflowStep,
} from '@/types/dataProcess'
import type { DataProcessConfig, DataProcessPreviewProgress, DataProcessTask, DataProcessWorkflowStep } from '@/types/dataProcess'
import type {
ExternalDataSource,
GenerationControlOptions,
PreviewItem,
ProcessType,
SourceMode,
StepId,
StructuredProcessOptions,
UnstructuredProcessOptions,
UploadedDataFile,
} from './create/types'
const router = useRouter()
const modelsStore = useModelsStore()
const { list: modelList, loaded: modelsLoaded } = storeToRefs(modelsStore)
@@ -71,7 +66,7 @@ const PREVIEW_MODEL_VERSION = 'backend-pipeline-v4'
const WIZARD_STEPS = [
{ id: 'create', title: '创建任务', desc: '填写任务信息与处理配置' },
{ id: 'model', title: '大模型选择', desc: '选择生成模型并设置输出要求' },
{ id: 'upload', title: '上传文件', desc: '上传或接入待处理的源数据' },
{ id: 'upload', title: '数据来源', desc: '选择本地上传或外部数据源拉取' },
{ id: 'preview', title: '数据预览', desc: '核对源文件与预览内容' },
{ id: 'generate', title: '开始生成', desc: '确认摘要并启动处理' },
{ id: 'results', title: '结果编辑与保存', desc: '检查、修改并保存结果' },
@@ -81,11 +76,13 @@ const currentStepId = computed<StepId>(() => WIZARD_STEPS[currentStep.value]?.id
const task = reactive({ name: '', description: '' })
const taskId = ref<string | null>(null)
const processType = ref<ProcessType>('structured')
const sourceMode = ref<SourceMode>('local')
const structuredOptions = ref<StructuredProcessOptions>(createDefaultStructuredOptions())
const unstructuredOptions = ref<UnstructuredProcessOptions>(createDefaultUnstructuredOptions())
const modelSelectionOptions = computed<GenerationControlOptions>(() => (
processType.value === 'unstructured' ? unstructuredOptions.value : structuredOptions.value
))
const activeOutputType = computed(() => modelSelectionOptions.value.outputType)
const uploadedFiles = ref<UploadedDataFile[]>([])
const previewBuilding = ref(false)
const {
@@ -93,16 +90,7 @@ const {
startPreviewBuild,
stopPreviewPolling,
} = useDataProcessPreviewBuild()
const externalSource = reactive<ExternalDataSource>({
type: 'postgresql',
url: '',
authMode: 'none',
username: '',
password: '',
limit: 1000,
query: '',
fileName: 'external-data.jsonl',
})
const externalSource = reactive<ExternalDataSource>(createDefaultExternalSource())
const externalPulling = ref(false)
const externalConnected = ref(false)
const fileName = computed(() => uploadedFiles.value.map(f => f.name).join(', '))
@@ -135,6 +123,7 @@ const {
} = useDataProcessGeneration({
taskId,
dirty,
outputType: activeOutputType,
beforeGenerate: beforeStartGeneration,
})
const { enqueueSourceUpload, sourceUploading } = useDataProcessSourceUpload({
@@ -174,7 +163,7 @@ const previewFiles = computed(() => uploadedFiles.value.map((file) => {
}))
const primaryActionLabel = computed(() => {
if (currentStepId.value === 'create') return '继续:选择大模型'
if (currentStepId.value === 'model') return '继续:上传文件'
if (currentStepId.value === 'model') return '继续:选择数据来源'
if (currentStepId.value === 'upload') {
if (sourceUploading.value) return '正在上传'
return previewBuilding.value ? '正在切分' : '继续:数据预览'
@@ -198,7 +187,6 @@ function goToStep(stepId: StepId) {
const nextStepIndex = WIZARD_STEPS.findIndex((step) => step.id === stepId)
if (nextStepIndex >= 0) currentStep.value = nextStepIndex
}
function updateModelSelectionOptions(value: GenerationControlOptions) {
if (processType.value === 'unstructured') {
unstructuredOptions.value = { ...unstructuredOptions.value, ...value }
@@ -206,12 +194,12 @@ function updateModelSelectionOptions(value: GenerationControlOptions) {
}
structuredOptions.value = { ...structuredOptions.value, ...value }
}
function toBackendConfig(): DataProcessConfig {
const options = processType.value === 'unstructured'
? unstructuredOptions.value
: structuredOptions.value
const common = {
...sourceConfigForBackend(sourceMode.value, externalSource),
preprocess_options: [...options.preprocessOptions],
semantic_enrichment: options.semanticEnrichment,
dataset_split: { ...options.datasetSplit },
@@ -247,7 +235,6 @@ function toBackendConfig(): DataProcessConfig {
qa_pairs_per_row: structuredOptions.value.qaPairsPerRow,
}
}
function taskPayload() {
return {
name: task.name.trim(),
@@ -256,12 +243,10 @@ function taskPayload() {
config: toBackendConfig(),
}
}
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())
@@ -277,19 +262,6 @@ async function saveTaskConfiguration() {
return saved
}
function externalPayload(): DataProcessExternalSourcePayload {
return {
type: 'postgresql',
url: externalSource.url.trim(),
auth_mode: externalSource.authMode,
username: externalSource.username || undefined,
password: externalSource.password || undefined,
limit: externalSource.limit,
query: externalSource.query?.trim() || undefined,
file_name: externalSource.fileName || 'external-data.jsonl',
}
}
function mapPreviewItem(item: DataProcessPreviewItem): PreviewItem {
const sourceLocator = item.quality_score?.source_locator
return {
@@ -378,7 +350,7 @@ async function beforeStartGeneration() {
)
}
watch(
[() => task.name, () => task.description, processType, structuredOptions, unstructuredOptions, externalSource],
[() => task.name, () => task.description, processType, sourceMode, structuredOptions, unstructuredOptions, externalSource],
() => {
if (!hydrating.value) dirty.value = true
},
@@ -396,6 +368,13 @@ watch(processType, (nextType, previousType) => {
ElMessage.info('处理类型已变更,请重新上传或拉取匹配的源数据')
})
watch(processType, (nextType) => {
if (nextType === 'unstructured' && sourceMode.value === 'external') {
sourceMode.value = 'local'
externalConnected.value = false
}
})
watch(generationOptionsSignature, (currentSignature, previousSignature) => {
if (hydrating.value) return
if (currentSignature === previousSignature) return
@@ -453,6 +432,16 @@ function updateExternalSource(value: ExternalDataSource) {
externalConnected.value = false
}
async function updateSourceMode(value: SourceMode) {
const previous = sourceMode.value
sourceMode.value = value
externalConnected.value = false
dirty.value = true
externalPulling.value = true
try { await saveTaskConfiguration() } catch { sourceMode.value = previous }
finally { externalPulling.value = false }
}
async function handleTestConnection() {
if (!taskId.value) {
ElMessage.error('任务尚未创建,请返回模型选择步骤后重试')
@@ -464,7 +453,8 @@ async function handleTestConnection() {
}
externalPulling.value = true
try {
const result = await testDataProcessExternalSource(taskId.value, externalPayload())
await saveTaskConfiguration()
const result = await testDataProcessExternalSource(taskId.value, externalSourcePayload(externalSource))
externalConnected.value = result.connected
if (result.connected) ElMessage.success(result.message || '数据源连接测试成功')
else ElMessage.warning(result.message || '数据源连接失败')
@@ -490,7 +480,8 @@ async function handlePullData() {
}
externalPulling.value = true
try {
const response = await pullDataProcessExternalSource(taskId.value, externalPayload())
await saveTaskConfiguration()
const response = await pullDataProcessExternalSource(taskId.value, externalSourcePayload(externalSource))
const newFiles: UploadedDataFile[] = []
for (const file of response.files) {
const content = await loadCanonicalSourceContent(taskId.value, file.id)
@@ -620,6 +611,12 @@ function applyPreviewProgress(progress: DataProcessPreviewProgress) {
async function completePreviewWorkspace() {
previewItems.value = await loadAllPreviewItems()
for (const file of uploadedFiles.value) {
if (!file.content && file.sourceFileId && file.fileFormat?.replace('.', '') === 'pdf') {
file.content = await loadCanonicalSourceContent(taskId.value as string, file.sourceFileId)
.catch(() => file.content)
}
}
const configSignature = buildPreviewConfigSignature()
const previewCounts = new Map<string, number>()
for (const item of previewItems.value) {
@@ -678,7 +675,7 @@ async function nextFromUpload() {
return
}
if (uploadedFiles.value.length === 0) {
ElMessage.warning(processType.value === 'external' ? '请先拉取至少一个数据源' : '请上传至少一个源数据文件')
ElMessage.warning(sourceMode.value === 'external' ? '请先拉取至少一个数据源' : '请上传至少一个源数据文件')
return
}
const failedUploads = uploadedFiles.value.filter((file) => file.status === 'failed')
@@ -992,6 +989,9 @@ async function initializeExistingWorkflow() {
const sourceTask = await loadRegenerationSource()
if (!sourceTask) return
taskId.value = String(sourceTask.id)
const restoredSource = restoreExternalSourceConfig(sourceTask.config || {})
sourceMode.value = restoredSource.mode
Object.assign(externalSource, restoredSource.source)
if (!isWorkflowResume.value) return
if (sourceTask.status === 'completed' && sourceTask.results_confirmed !== false) {
allowLeave = true
@@ -1102,6 +1102,7 @@ onMounted(() => {
<SourceUploadStep
v-else-if="currentStepId === 'upload'"
:process-type="processType"
:source-mode="sourceMode"
:uploaded-files="uploadedFiles"
:external-source="externalSource"
:external-pulling="externalPulling"
@@ -1109,6 +1110,7 @@ onMounted(() => {
:preview-building="previewBuilding"
:source-uploading="sourceUploading"
@update:external-source="updateExternalSource"
@update:source-mode="updateSourceMode"
@file-change="handleFileChange"
@remove-file="handleRemoveFile"
@use-sample="useSampleFile"
@@ -1154,6 +1156,7 @@ onMounted(() => {
:preview-items="previewItems"
:regenerating-result-id="regeneratingResultId"
:bulk-regeneration="bulkRegeneration"
:output-type="activeOutputType"
@update:field="updateResultField"
@regenerate:all="regenerateAllResults"
@regenerate:item="regenerateResult"
@@ -1179,8 +1182,8 @@ onMounted(() => {
<el-button
class="wizard-primary-action"
type="primary"
:loading="modelSubmitLoading || generationStarting || generation.status === 'running' || resultRegenerationBusy || (currentStepId === 'upload' && (sourceUploading || previewBuilding))"
:disabled="hydrating || modelSubmitLoading || generationStarting || Boolean(initializationError) || resultRegenerationBusy || (currentStepId === 'generate' && generation.status === 'running') || previewBuilding || sourceUploading || (currentStepId === 'upload' && hasUnfinishedUploads)"
:loading="externalPulling || modelSubmitLoading || generationStarting || generation.status === 'running' || resultRegenerationBusy || (currentStepId === 'upload' && (sourceUploading || previewBuilding))"
:disabled="externalPulling || hydrating || modelSubmitLoading || generationStarting || 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;" />