From 78624915b7dabcbbfad12876a76de77f9a704700 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sun, 8 Feb 2026 08:17:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(annotation):=20=E6=B7=BB=E5=8A=A0=E6=A0=87?= =?UTF-8?q?=E6=B3=A8=E4=BB=BB=E5=8A=A1=E7=AE=97=E5=AD=90=E7=BC=96=E6=8E=92?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E9=A1=B5=E9=9D=A2=E5=92=8C=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=AE=97=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 功能概述 为标注任务通用算子编排功能添加完整的前端界面,包括任务创建、列表管理、详情查看等功能,并提供测试算子用于功能验证。 ## 改动内容 ### 前端功能 #### 1. 算子编排页面 - 新增两步创建流程: - 第一步:基本信息(数据集选择、任务名称等) - 第二步:算子编排(选择算子、配置参数、预览 pipeline) - 核心文件: - frontend/src/pages/DataAnnotation/OperatorCreate/CreateTask.tsx - frontend/src/pages/DataAnnotation/OperatorCreate/hooks/useOperatorOperations.ts - frontend/src/pages/DataAnnotation/OperatorCreate/hooks/useDragOperators.ts - frontend/src/pages/DataAnnotation/OperatorCreate/hooks/useCreateStepTwo.tsx #### 2. UI 组件 - 算子库(OperatorLibrary):显示可用算子,支持分类筛选 - 编排区(OperatorOrchestration):拖拽排序算子 - 参数面板(OperatorConfig):配置算子参数 - Pipeline 预览(PipelinePreview):预览算子链 - 核心文件:frontend/src/pages/DataAnnotation/OperatorCreate/components/ #### 3. 任务列表管理 - 在数据标注首页同一 Tab 中添加任务列表 - 支持状态筛选(pending/running/completed/failed/stopped) - 支持关键词搜索 - 支持轮询刷新 - 支持停止任务 - 支持下载结果 - 核心文件:frontend/src/pages/DataAnnotation/Home/components/AutoAnnotationTaskList.tsx #### 4. 任务详情抽屉 - 点击任务名打开详情抽屉 - 显示任务基本信息(名称、状态、进度、时间等) - 显示 pipeline 配置(算子链和参数) - 显示错误信息(如果失败) - 显示产物路径和下载按钮 - 核心文件:frontend/src/pages/DataAnnotation/Home/components/AutoAnnotationTaskDetailDrawer.tsx #### 5. API 集成 - 封装自动标注任务相关接口: - list:获取任务列表 - create:创建任务 - detail:获取任务详情 - delete:删除任务 - stop:停止任务 - download:下载结果 - 核心文件:frontend/src/pages/DataAnnotation/annotation.api.ts #### 6. 路由配置 - 新增路由:/data/annotation/create-auto-task - 集成到数据标注首页 - 核心文件: - frontend/src/routes/routes.ts - frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx #### 7. 算子模型增强 - 新增 runtime 字段用于标注算子筛选 - 核心文件:frontend/src/pages/OperatorMarket/operator.model.ts ### 后端功能 #### 1. 测试算子(test_annotation_marker) - 功能:在图片上绘制测试标记并输出 JSON 标注 - 用途:测试标注功能是否正常工作 - 实现文件: - runtime/ops/annotation/test_annotation_marker/process.py - runtime/ops/annotation/test_annotation_marker/metadata.yml - runtime/ops/annotation/test_annotation_marker/__init__.py #### 2. 算子注册 - 将测试算子注册到 annotation ops 包 - 添加到运行时白名单 - 核心文件: - runtime/ops/annotation/__init__.py - runtime/python-executor/datamate/auto_annotation_worker.py #### 3. 数据库初始化 - 添加测试算子到数据库 - 添加算子分类关联 - 核心文件:scripts/db/data-operator-init.sql ### 问题修复 #### 1. outputDir 默认值覆盖问题 - 问题:前端设置空字符串默认值导致 worker 无法注入真实输出目录 - 解决:过滤掉空/null 的 outputDir,确保 worker 能注入真实输出目录 - 修改位置:frontend/src/pages/DataAnnotation/OperatorCreate/hooks/useOperatorOperations.ts #### 2. targetClasses 默认值类型问题 - 问题:YOLO 算子 metadata 中 targetClasses 默认值是字符串 '[]' 而不是列表 - 解决:改为列表 [] - 修改位置:runtime/ops/annotation/image_object_detection_bounding_box/metadata.yml ## 关键特性 ### 用户体验 - 统一的算子编排界面(与数据清洗保持一致) - 直观的拖拽操作 - 实时的 pipeline 预览 - 完整的任务管理功能 ### 功能完整性 - 任务创建:两步流程,清晰明了 - 任务管理:列表展示、状态筛选、搜索 - 任务操作:停止、下载 - 任务详情:完整的信息展示 ### 可测试性 - 提供测试算子用于功能验证 - 支持快速测试标注流程 ## 验证结果 - ESLint 检查:✅ 通过 - 前端构建:✅ 通过(10.91s) - 功能测试:✅ 所有功能正常 ## 部署说明 1. 执行数据库初始化脚本(如果是新环境) 2. 重启前端服务 3. 重启后端服务(如果修改了 worker 白名单) ## 使用说明 1. 进入数据标注页面 2. 点击创建自动标注任务 3. 选择数据集和文件 4. 从算子库拖拽算子到编排区 5. 配置算子参数 6. 预览 pipeline 7. 提交任务 8. 在任务列表中查看进度 9. 点击任务名查看详情 10. 下载标注结果 ## 相关文件 - 前端页面:frontend/src/pages/DataAnnotation/OperatorCreate/ - 任务管理:frontend/src/pages/DataAnnotation/Home/components/ - API 集成:frontend/src/pages/DataAnnotation/annotation.api.ts - 测试算子:runtime/ops/annotation/test_annotation_marker/ - 数据库脚本:scripts/db/data-operator-init.sql --- .../DataAnnotation/Home/DataAnnotation.tsx | 8 + .../AutoAnnotationTaskDetailDrawer.tsx | 543 ++++++++++++++++++ .../components/AutoAnnotationTaskList.tsx | 459 +++++++++++++++ .../OperatorCreate/CreateTask.tsx | 281 +++++++++ .../components/OperatorConfig.tsx | 73 +++ .../components/OperatorLibrary.tsx | 330 +++++++++++ .../components/OperatorOrchestration.tsx | 202 +++++++ .../OperatorCreate/components/ParamConfig.tsx | 224 ++++++++ .../components/PipelinePreview.tsx | 45 ++ .../OperatorCreate/hooks/useCreateStepTwo.tsx | 88 +++ .../OperatorCreate/hooks/useDragOperators.ts | 141 +++++ .../hooks/useOperatorOperations.ts | 225 ++++++++ .../pages/DataAnnotation/annotation.api.ts | 24 + .../pages/OperatorMarket/operator.model.ts | 29 +- frontend/src/routes/routes.ts | 5 + runtime/ops/annotation/__init__.py | 2 + .../metadata.yml | 2 +- .../test_annotation_marker/__init__.py | 12 + .../test_annotation_marker/metadata.yml | 37 ++ .../test_annotation_marker/process.py | 122 ++++ .../datamate/auto_annotation_worker.py | 2 +- scripts/db/data-operator-init.sql | 9 + 22 files changed, 2847 insertions(+), 16 deletions(-) create mode 100644 frontend/src/pages/DataAnnotation/Home/components/AutoAnnotationTaskDetailDrawer.tsx create mode 100644 frontend/src/pages/DataAnnotation/Home/components/AutoAnnotationTaskList.tsx create mode 100644 frontend/src/pages/DataAnnotation/OperatorCreate/CreateTask.tsx create mode 100644 frontend/src/pages/DataAnnotation/OperatorCreate/components/OperatorConfig.tsx create mode 100644 frontend/src/pages/DataAnnotation/OperatorCreate/components/OperatorLibrary.tsx create mode 100644 frontend/src/pages/DataAnnotation/OperatorCreate/components/OperatorOrchestration.tsx create mode 100644 frontend/src/pages/DataAnnotation/OperatorCreate/components/ParamConfig.tsx create mode 100644 frontend/src/pages/DataAnnotation/OperatorCreate/components/PipelinePreview.tsx create mode 100644 frontend/src/pages/DataAnnotation/OperatorCreate/hooks/useCreateStepTwo.tsx create mode 100644 frontend/src/pages/DataAnnotation/OperatorCreate/hooks/useDragOperators.ts create mode 100644 frontend/src/pages/DataAnnotation/OperatorCreate/hooks/useOperatorOperations.ts create mode 100644 runtime/ops/annotation/test_annotation_marker/__init__.py create mode 100644 runtime/ops/annotation/test_annotation_marker/metadata.yml create mode 100644 runtime/ops/annotation/test_annotation_marker/process.py diff --git a/frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx b/frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx index 9fb8a38..e96b935 100644 --- a/frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx +++ b/frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx @@ -24,6 +24,7 @@ import CreateAnnotationTask from "../Create/components/CreateAnnotationTaskDialo import ExportAnnotationDialog from "./ExportAnnotationDialog"; import { ColumnType } from "antd/es/table"; import { TemplateList } from "../Template"; +import AutoAnnotationTaskList from "./components/AutoAnnotationTaskList"; // Note: DevelopmentInProgress intentionally not used here type AnnotationTaskRowKey = string | number; @@ -325,6 +326,11 @@ export default function DataAnnotation() { > 批量删除 + + + + + } + > + {loading ? ( +
+ +
+ ) : !taskDetail ? ( + + ) : ( + + + + {taskDetail.name} + {taskDetail.id || "-"} + + + + {taskDetail.stopRequested && 停止中} + + + + + + {taskDetail.datasetName} + {taskDetail.datasetId || "-"} + {taskDetail.taskMode} + {taskDetail.executorType} + + {taskDetail.processedImages}/{taskDetail.totalImages} + + + {taskDetail.detectedObjects} + + {taskDetail.createdBy} + + {taskDetail.sourceDatasets.length > 0 + ? taskDetail.sourceDatasets.join("、") + : "-"} + + {taskDetail.createdAt} + {taskDetail.startedAt} + {taskDetail.updatedAt} + {taskDetail.completedAt} + + {taskDetail.heartbeatAt} + + + + + + {taskDetail.pipeline.length > 0 ? ( + + {taskDetail.pipeline.map((step, index) => ( + + {Object.keys(step.overrides).length > 0 ? ( +
+                        {JSON.stringify(step.overrides, null, 2)}
+                      
+ ) : ( + 无参数覆盖 + )} +
+ ))} +
+ ) : ( + + )} + + {Object.keys(taskDetail.config).length > 0 && ( +
+ 任务配置 +
+                  {JSON.stringify(taskDetail.config, null, 2)}
+                
+
+ )} +
+ + {taskDetail.errorMessage && ( + + + {taskDetail.errorMessage} + + } + /> + + )} + + + + + {taskDetail.outputPath ? ( + + {taskDetail.outputPath} + + ) : ( + "-" + )} + + + {taskDetail.outputDatasetId || "-"} + + +
+ +
+
+
+ )} + + ); +} diff --git a/frontend/src/pages/DataAnnotation/Home/components/AutoAnnotationTaskList.tsx b/frontend/src/pages/DataAnnotation/Home/components/AutoAnnotationTaskList.tsx new file mode 100644 index 0000000..481bc2c --- /dev/null +++ b/frontend/src/pages/DataAnnotation/Home/components/AutoAnnotationTaskList.tsx @@ -0,0 +1,459 @@ +import { useMemo, useState } from "react"; +import { App, Badge, Button, Card, Progress, Table, Tooltip } from "antd"; +import { DownloadOutlined, PauseCircleOutlined } from "@ant-design/icons"; +import type { ColumnsType } from "antd/es/table"; +import { useNavigate } from "react-router"; +import { SearchControls } from "@/components/SearchControls"; +import useFetchData from "@/hooks/useFetchData"; +import { formatDateTime } from "@/utils/unit"; +import { + downloadAnnotationOperatorTaskResultUsingGet, + queryAnnotationOperatorTasksUsingGet, + stopAnnotationOperatorTaskByIdUsingPost, +} from "../../annotation.api"; +import AutoAnnotationTaskDetailDrawer from "./AutoAnnotationTaskDetailDrawer"; + +type AutoAnnotationTaskStatus = + | "pending" + | "running" + | "completed" + | "failed" + | "stopped"; + +type AutoAnnotationTaskPayload = { + id?: string; + name?: string; + datasetId?: string; + dataset_id?: string; + datasetName?: string; + dataset_name?: string; + status?: string; + progress?: number; + totalImages?: number; + total_images?: number; + processedImages?: number; + processed_images?: number; + detectedObjects?: number; + detected_objects?: number; + outputPath?: string; + output_path?: string; + stopRequested?: boolean; + stop_requested?: boolean; + createdAt?: string; + created_at?: string; + updatedAt?: string; + updated_at?: string; + completedAt?: string; + completed_at?: string; +}; + +type StatusMeta = { + label: string; + value: string; + color: string; +}; + +type AutoAnnotationTaskRow = { + id: string; + name: string; + datasetId: string; + datasetName: string; + status: StatusMeta; + progress: number; + totalImages: number; + processedImages: number; + detectedObjects: number; + outputPath?: string; + stopRequested: boolean; + createdAt: string; + updatedAt: string; + completedAt: string; +}; + +type FetchParams = Record; +type FetchResult = { + data: { + content: Partial[]; + total: number; + }; +}; + +const AUTO_ANNOTATION_STATUS_MAP: Record = { + pending: { + label: "待处理", + value: "pending", + color: "gray", + }, + running: { + label: "进行中", + value: "running", + color: "blue", + }, + completed: { + label: "已完成", + value: "completed", + color: "green", + }, + failed: { + label: "失败", + value: "failed", + color: "red", + }, + stopped: { + label: "已停止", + value: "stopped", + color: "orange", + }, +}; + +const toSafeNumber = (value: unknown): number => + typeof value === "number" && Number.isFinite(value) ? value : 0; + +const resolveStatus = (status?: string): StatusMeta => { + const normalizedStatus = (status || "").toLowerCase(); + if (normalizedStatus in AUTO_ANNOTATION_STATUS_MAP) { + return AUTO_ANNOTATION_STATUS_MAP[normalizedStatus as AutoAnnotationTaskStatus]; + } + + return { + label: normalizedStatus || "未知", + value: normalizedStatus || "unknown", + color: "default", + }; +}; + +const formatTaskTime = (value?: string) => { + if (!value) { + return "-"; + } + return formatDateTime(value); +}; + +const mapAutoAnnotationTask = ( + task: Partial +): AutoAnnotationTaskRow => { + const status = resolveStatus(task.status); + + return { + id: task.id || "", + name: task.name || "-", + datasetId: task.datasetId || task.dataset_id || "", + datasetName: task.datasetName || task.dataset_name || "-", + status, + progress: Math.max(0, Math.min(100, toSafeNumber(task.progress))), + totalImages: toSafeNumber(task.totalImages ?? task.total_images), + processedImages: toSafeNumber(task.processedImages ?? task.processed_images), + detectedObjects: toSafeNumber(task.detectedObjects ?? task.detected_objects), + outputPath: task.outputPath || task.output_path, + stopRequested: task.stopRequested ?? task.stop_requested ?? false, + createdAt: formatTaskTime(task.createdAt || task.created_at), + updatedAt: formatTaskTime(task.updatedAt || task.updated_at), + completedAt: formatTaskTime(task.completedAt || task.completed_at), + }; +}; + +const fetchAutoAnnotationTasks = async ( + params?: FetchParams +): Promise> => { + const response = await queryAnnotationOperatorTasksUsingGet(); + const tasks = Array.isArray(response?.data) + ? (response.data as AutoAnnotationTaskPayload[]) + : []; + + const keyword = + typeof params?.keyword === "string" ? params.keyword.trim().toLowerCase() : ""; + const status = typeof params?.status === "string" ? params.status.toLowerCase() : ""; + const page = + typeof params?.page === "number" && params.page >= 0 ? params.page : 0; + const size = + typeof params?.size === "number" && params.size > 0 ? params.size : 12; + + const filtered = tasks + .filter((task) => { + if (!keyword) { + return true; + } + + const text = [task.id, task.name, task.datasetName, task.dataset_name] + .filter((value) => typeof value === "string") + .join(" ") + .toLowerCase(); + + return text.includes(keyword); + }) + .filter((task) => { + if (!status) { + return true; + } + return (task.status || "").toLowerCase() === status; + }) + .sort((a, b) => { + const timeA = new Date(a.createdAt || a.created_at || 0).getTime(); + const timeB = new Date(b.createdAt || b.created_at || 0).getTime(); + return timeB - timeA; + }); + + const start = page * size; + const end = start + size; + + return { + data: { + content: filtered.slice(start, end), + total: filtered.length, + }, + }; +}; + +export default function AutoAnnotationTaskList() { + const navigate = useNavigate(); + const { message } = App.useApp(); + const [detailOpen, setDetailOpen] = useState(false); + const [detailTaskId, setDetailTaskId] = useState(""); + + const { + loading, + tableData, + pagination, + searchParams, + handleFiltersChange, + handleKeywordChange, + fetchData, + } = useFetchData( + fetchAutoAnnotationTasks, + mapAutoAnnotationTask, + 10000, + true, + [], + 0 + ); + + const filterOptions = useMemo( + () => [ + { + key: "status", + label: "状态", + options: Object.values(AUTO_ANNOTATION_STATUS_MAP), + }, + ], + [] + ); + + const handleOpenDetail = (task: AutoAnnotationTaskRow) => { + if (!task.id) { + message.warning("任务ID缺失,无法查看详情"); + return; + } + + setDetailTaskId(task.id); + setDetailOpen(true); + }; + + const handleStopTask = async (task: AutoAnnotationTaskRow) => { + try { + await stopAnnotationOperatorTaskByIdUsingPost(task.id); + message.success("已发送停止请求"); + fetchData(); + } catch (error) { + console.error(error); + message.error("停止任务失败,请稍后重试"); + } + }; + + const handleDownload = async (task: AutoAnnotationTaskRow) => { + try { + await downloadAnnotationOperatorTaskResultUsingGet( + task.id, + `${task.name || task.id}_annotations.zip` + ); + message.success("结果下载已开始"); + } catch (error) { + console.error(error); + message.error("下载失败,请确认任务已生成结果"); + } + }; + + const columns: ColumnsType = [ + { + title: "任务名称", + dataIndex: "name", + key: "name", + width: 180, + fixed: "left", + ellipsis: true, + render: (_, record) => ( + + ), + }, + { + title: "任务ID", + dataIndex: "id", + key: "id", + width: 160, + ellipsis: true, + }, + { + title: "源数据集", + dataIndex: "datasetName", + key: "datasetName", + width: 180, + ellipsis: true, + render: (_, record) => { + if (!record.datasetId) { + return record.datasetName; + } + + return ( + + ); + }, + }, + { + title: "状态", + dataIndex: "status", + key: "status", + width: 120, + render: (status: StatusMeta) => ( + + ), + }, + { + title: "进度", + dataIndex: "progress", + key: "progress", + width: 180, + render: (_progress, record) => { + const progressStatus = + record.status.value === "failed" + ? "exception" + : record.status.value === "completed" + ? "success" + : record.status.value === "running" + ? "active" + : "normal"; + + return ( + + ); + }, + }, + { + title: "已处理/总数", + dataIndex: "processedImages", + key: "processedImages", + width: 120, + align: "right", + render: (_, record) => `${record.processedImages}/${record.totalImages}`, + }, + { + title: "检测对象数", + dataIndex: "detectedObjects", + key: "detectedObjects", + width: 120, + align: "right", + }, + { + title: "创建时间", + dataIndex: "createdAt", + key: "createdAt", + width: 180, + ellipsis: true, + }, + { + title: "更新时间", + dataIndex: "updatedAt", + key: "updatedAt", + width: 180, + ellipsis: true, + }, + { + title: "完成时间", + dataIndex: "completedAt", + key: "completedAt", + width: 180, + ellipsis: true, + }, + { + title: "操作", + dataIndex: "actions", + key: "actions", + width: 140, + fixed: "right", + render: (_, record) => { + const canStop = + ["pending", "running"].includes(record.status.value) && + !record.stopRequested; + const canDownload = + ["completed", "stopped", "failed"].includes(record.status.value) && + !!record.outputPath; + + return ( +
+ +
+ ); + }, + }, + ]; + + return ( + +
+ +
+ + + + setDetailOpen(false)} + onRefreshList={() => fetchData()} + /> + + ); +} diff --git a/frontend/src/pages/DataAnnotation/OperatorCreate/CreateTask.tsx b/frontend/src/pages/DataAnnotation/OperatorCreate/CreateTask.tsx new file mode 100644 index 0000000..29c8cc6 --- /dev/null +++ b/frontend/src/pages/DataAnnotation/OperatorCreate/CreateTask.tsx @@ -0,0 +1,281 @@ +import { useEffect, useMemo, useState } from "react"; +import { Button, Form, Input, message, Select, Steps } from "antd"; +import TextArea from "antd/es/input/TextArea"; +import { SaveOutlined, DatabaseOutlined } from "@ant-design/icons"; +import { ArrowLeft } from "lucide-react"; +import { Link, useNavigate } from "react-router"; +import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api"; +import { mapDataset } from "@/pages/DataManagement/dataset.const"; +import { Dataset, DatasetType } from "@/pages/DataManagement/dataset.model"; +import { createAnnotationOperatorTaskUsingPost } from "../annotation.api"; +import { useCreateStepTwo } from "./hooks/useCreateStepTwo"; +import PipelinePreview from "./components/PipelinePreview"; + +interface TaskConfigValues { + name: string; + description?: string; + datasetId: string; + outputDatasetName: string; +} + +const buildDefaultOutputDatasetName = (dataset?: Dataset) => { + if (!dataset?.name) { + return "自动标注结果集"; + } + return `${dataset.name}_auto_annotation`; +}; + +export default function AnnotationOperatorTaskCreate() { + const navigate = useNavigate(); + const [form] = Form.useForm(); + const [currentStep, setCurrentStep] = useState(1); + const [datasets, setDatasets] = useState([]); + const [submitting, setSubmitting] = useState(false); + const [outputNameTouched, setOutputNameTouched] = useState(false); + + const { loading: operatorLoading, selectedOperators, renderStepTwo } = useCreateStepTwo(); + + const selectedDatasetId = Form.useWatch("datasetId", form); + const selectedDataset = useMemo( + () => datasets.find((dataset) => dataset.id === selectedDatasetId), + [datasets, selectedDatasetId] + ); + + const fetchDatasets = async () => { + try { + const { data } = await queryDatasetsUsingGet({ page: 0, pageSize: 1000 }); + const content = data?.content || []; + const mappedDatasets = content.map((item) => mapDataset(item)); + setDatasets(mappedDatasets); + } catch (error) { + console.error("加载数据集失败", error); + message.error("加载数据集失败"); + } + }; + + useEffect(() => { + fetchDatasets(); + }, []); + + useEffect(() => { + if (!selectedDataset || outputNameTouched) { + return; + } + + form.setFieldValue( + "outputDatasetName", + buildDefaultOutputDatasetName(selectedDataset) + ); + }, [form, outputNameTouched, selectedDataset]); + + const canProceed = () => { + if (currentStep === 1) { + const values = form.getFieldsValue(); + return !!values.name && !!values.datasetId && !!values.outputDatasetName; + } + + if (currentStep === 2) { + return selectedOperators.length > 0; + } + + return false; + }; + + const handleNext = async () => { + try { + if (currentStep === 1) { + await form.validateFields(); + + if (selectedDataset?.datasetType !== DatasetType.IMAGE) { + message.error("自动标注算子编排当前仅支持图片数据集"); + return; + } + } + setCurrentStep((prev) => Math.min(prev + 1, 2)); + } catch { + message.error("请完善基本信息"); + } + }; + + const handlePrev = () => { + setCurrentStep((prev) => Math.max(prev - 1, 1)); + }; + + const handleSave = async () => { + try { + const values = await form.validateFields(); + if (selectedOperators.length === 0) { + message.error("请至少选择一个标注算子"); + return; + } + + if (selectedDataset?.datasetType !== DatasetType.IMAGE) { + message.error("自动标注算子编排当前仅支持图片数据集"); + return; + } + + const outputDatasetName = values.outputDatasetName?.trim(); + const pipeline = selectedOperators.map((operator, index) => { + const overrides = { + ...(operator.defaultParams || {}), + ...(operator.overrides || {}), + } as Record; + + if (index === 0 && outputDatasetName) { + overrides.outputDatasetName = outputDatasetName; + } + + return { + operatorId: operator.id, + overrides, + }; + }); + + const payload = { + name: values.name, + datasetId: values.datasetId, + taskMode: "pipeline", + executorType: "annotation_local", + pipeline, + } as Record; + + if (values.description) { + payload.description = values.description; + } + + setSubmitting(true); + await createAnnotationOperatorTaskUsingPost(payload); + message.success("自动标注任务创建成功"); + navigate("/data/annotation"); + } catch (error: unknown) { + const err = error as { message?: string; data?: { message?: string } }; + const msg = err?.message || err?.data?.message || "创建失败,请稍后重试"; + message.error(msg); + console.error(error); + } finally { + setSubmitting(false); + } + }; + + return ( +
+
+
+ + + +

创建自动标注任务

+
+
+ +
+
+ +
+
+ {currentStep === 1 ? ( +
+ + + + + +