#!/usr/bin/env python # -*- coding: utf-8 -*- """ 任务编辑API模块 提供任务编辑相关的API接口 """ import json from typing import Dict, List, Any, Optional from fastapi import APIRouter, Query, Path, Body, Request from pydantic import ValidationError from routes.model.task_edit_model import ( TaskBasicInfo, TaskBackupRequest, SubTaskListParams,TaskEditRunRequest, TaskInputParam, TaskSaveRequest,ParamValueType ) from services.task_edit_service import TaskEditService from routes.common_api import format_response, error_response from utils.logger import get_logger # 创建路由 router = APIRouter( prefix="/api/vwed-task-edit", tags=["VWED任务编辑"] ) # 设置日志 logger = get_logger("app.task_edit_api") # 自定义错误处理工具函数 def handle_validation_error(e: ValidationError) -> Dict[str, Any]: """ 处理Pydantic验证错误,生成详细的错误信息 Args: e: ValidationError实例 Returns: Dict[str, Any]: 错误信息 """ errors = e.errors() if not errors: return {"message": "验证失败,但未提供具体错误信息", "code": 400} # 获取第一个错误 error = errors[0] error_type = error.get("type", "") loc = error.get("loc", []) msg = error.get("msg", "验证失败") # 转换位置为字符串路径 loc_str = ".".join([str(l) for l in loc]) # 针对枚举错误提供更详细的信息 if error_type == "type_error.enum" or "enum" in error_type: # 如果是参数类型枚举错误 if ".type" in loc_str and "ParamValueType" in str(e): allowed_values = [v.value for v in ParamValueType] error_msg = f"参数 '{loc_str}' 验证失败: 值必须是预设的枚举值之一: {', '.join(allowed_values)}" return {"message": error_msg, "code": 400} # 构造通用错误信息 error_msg = f"参数 '{loc_str}' 验证失败: {msg}" return {"message": error_msg, "code": 400} @router.get("/block") async def get_blocks(): """ 获取系统中所有可用的块数据 用于任务编辑页面左侧的组件面板显示 """ try: blocks = await TaskEditService.get_blocks() return format_response(data=blocks) except Exception as e: logger.error(f"获取块数据失败: {str(e)}") return error_response(message=f"获取块数据失败: {str(e)}", code=500) @router.get("/source/{id}") async def get_task_source(id: str = Path(..., description="任务ID")): """ 获取指定任务的源码详情数据 直接返回vwed_taskdef表中的详细定义 """ try: task_def = await TaskEditService.get_task_source(id) if task_def is None: return error_response(message="任务不存在", code=404) return format_response(data=task_def) except Exception as e: logger.error(f"获取任务源码详情失败: {str(e)}") return error_response(message=f"获取任务源码详情失败: {str(e)}", code=500) @router.post("/save") async def save_task_edit(task_def_data: TaskSaveRequest = Body(...)): """ 保存任务的编辑数据 包括任务的详细定义、输入参数配置、组件配置和连接关系等 Args: task_def_data: 规范化的任务定义数据,符合TaskSaveRequest模型 Returns: 任务保存结果,包含ID、版本号和更新时间 """ try: # 转换为字典,确保数据格式正确 task_data_dict = { "id": task_def_data.id, "detail": task_def_data.detail.model_dump() } # 先检查是否有数据变化 task_id = task_data_dict.get("id") change_result = await TaskEditService.check_task_changes(task_id, task_data_dict) # 如果数据没有变化,直接返回成功,不进行数据库更新 if not change_result.get("changed", True): return format_response( data={ "id": change_result.get("id"), "version": change_result.get("version"), }, message="数据未发生变化" ) # 数据有变化,执行保存操作 result = await TaskEditService.save_task_edit(task_data_dict) if not result.get("success", False): logger.error(f"保存任务编辑数据失败: {result.get('message', '保存失败')}") return error_response(message=result.get("message", "保存失败"), code=result.get("code", 400)) return format_response( data={ "id": result.get("id"), "version": result.get("version"), "updateTime": result.get("updateTime") }, message="保存成功" ) except ValidationError as e: # 处理验证错误 error_info = handle_validation_error(e) logger.error(f"验证错误: {error_info['message']}") return error_response(message=error_info["message"], code=error_info["code"]) except Exception as e: logger.exception("保存任务编辑数据失败") return error_response(message=f"保存任务编辑数据失败: {str(e)}", code=500) @router.post("/backup/{id}") async def backup_task( id: str = Path(..., description="要备份的任务ID"), backup_request: TaskBackupRequest = Body(...) ): """ 创建指定任务的备份副本 系统会从vwed_taskdef表中复制原任务的所有信息 """ try: result = await TaskEditService.backup_task(id, backup_request) if result is None: return error_response(message="任务不存在或备份失败", code=404) return format_response(data=result, message="备份成功") except Exception as e: logger.error(f"备份任务失败: {str(e)}") return error_response(message=f"备份任务失败: {str(e)}", code=500) @router.get("/subtasks/list") async def get_subtasks_list( pageNum: int = Query(1, description="页码"), pageSize: int = Query(100, description="每页记录数"), keyword: Optional[str] = Query(None, description="搜索关键词"), exclude_id: str = Query(..., description="要排除的任务ID,必填") ): """ 获取可以被引用的子任务列表 用于子任务组件的配置 """ try: params = SubTaskListParams( pageNum=pageNum, pageSize=pageSize, keyword=keyword, exclude_id=exclude_id ) result = await TaskEditService.get_subtasks_list(params) return format_response(data=result) except Exception as e: logger.error(f"获取子任务列表失败: {str(e)}") return error_response(message=f"获取子任务列表失败: {str(e)}", code=500) @router.post("/run") async def run_task(request: Request, run_request: TaskEditRunRequest = Body(...)): """ 直接执行指定的任务,并传入任务输入参数 用于在任务编辑页面上直接启动任务 如果任务输入参数中有必填字段,则params中必须包含对应字段,否则会返回400错误: """ try: # 获取客户端IP地址 client_ip = request.client.host if request.client else None # 获取客户端设备信息 (User-Agent) user_agent = request.headers.get("user-agent", "") # 获取token(用于同步到系统任务) tf_api_token = request.headers.get("x-access-token") # 构建客户端信息 client_info = { "user_agent": user_agent, "headers": dict(request.headers), "method": request.method, "url": str(request.url) } client_info_str = json.dumps(client_info, ensure_ascii=False) # 先获取任务定义,检查输入参数是否满足必填要求 task_def = await TaskEditService.get_task_source(run_request.taskId) if task_def is None: return error_response(message="任务不存在", code=404) # 检查必填参数 missing_params = [] if "detail" in task_def and "inputParams" in task_def["detail"]: input_params = task_def["detail"]["inputParams"] # 将请求参数转换为字典格式,方便查找 params_dict = {} if run_request.params: for param in run_request.params: params_dict[param.name] = param.defaultValue # 检查任务定义中的必填参数是否都已提供 for param in input_params: # 如果参数是必填的,但在请求中没有提供 if param.get("required", False) and ( not params_dict or param["name"] not in params_dict ): missing_params.append(param["name"]) # 如果有缺失的必填参数,返回错误 if missing_params: logger.warning(f"任务启动缺少必填参数: {', '.join(missing_params)}, 请求来源: {run_request.source_system}, 设备: {run_request.source_device}") return error_response( message=f"缺少必填参数: {', '.join(missing_params)}", code=400 ) # 检查非系统调度请求的设备是否有正在运行的相同任务 from data.enum.task_record_enum import SourceType if run_request.source_type != SourceType.SYSTEM_SCHEDULING: # 不是系统调度 # 调用服务层方法检查是否有正在运行的相同任务 check_result = await TaskEditService.check_running_task_for_device( run_request.taskId, run_request.source_device ) # 如果有运行中的任务 if check_result.get("has_running_task", False): if check_result.get("allow_restart", False): # 允许重启但仍需记录警告,这是一个需要注意的情况 logger.warning(f"设备 {run_request.source_device} 已有相同任务正在运行,但允许重新启动,来源: {run_request.source_system}") else: # 不允许重启,记录警告并返回错误 logger.warning(f"设备 {run_request.source_device} 已有相同任务正在运行且不允许重启,请求被拒绝,来源: {run_request.source_system}") return error_response( message=check_result.get("message", "相同设备已有此任务正在运行中,请等待任务完成后再次启动"), code=409 # 冲突状态码 ) # 执行任务,传入客户端IP和设备信息以及系统任务令牌 result = await TaskEditService.run_task( run_request, client_ip=client_ip, client_info=client_info_str, tf_api_token=tf_api_token ) if result is None: return error_response(message="任务启动失败", code=500) return format_response(data=result, message="任务启动成功") except Exception as e: logger.error(f"运行任务失败: {str(e)}") return error_response(message=f"运行任务失败: {str(e)}", code=500) @router.post("/stop/{task_record_id}") async def stop_task(task_record_id: str = Path(..., description="任务记录ID")): """ 终止正在运行的任务 Args: task_record_id: 任务记录ID,由run_task接口返回的taskRecordId,而非任务定义ID Returns: 成功或失败的响应 """ try: logger.info(f"准备终止任务: {task_record_id}") # 导入任务调度器 from services.enhanced_scheduler import scheduler # 调用调度器取消任务的方法 result = await scheduler.cancel_task(task_record_id) if not result.get("success", False): logger.warning(f"终止任务失败: {result.get('message')}") return error_response( message=result.get("message", "终止任务失败"), code=400 ) logger.info(f"任务已成功终止: {task_record_id}") return format_response( data={ "taskRecordId": task_record_id, "status": "canceled" }, message="任务已成功终止" ) except Exception as e: logger.log_error_with_trace(f"终止任务异常", e) return error_response(message=f"终止任务失败: {str(e)}", code=500) @router.post("/input-params/{id}") async def save_input_params( id: str = Path(..., description="任务ID"), input_params: List[TaskInputParam] = Body(..., embed=True) ): """ 保存指定任务的输入参数配置信息 系统支持根据获取可用输入参数类型接口返回的字段动态配置参数 """ try: # 将Pydantic模型转换为字典 params_dict = [param.model_dump() for param in input_params] result = await TaskEditService.save_input_params(id, params_dict) if not result["success"]: return error_response(message=result["message"], code=400) # 如果数据没有变化,返回特定提示 if not result["changed"]: logger.warning(f"保存输入参数时数据未发生变化: {id}") return format_response(message=result["message"]) # 数据有变化且保存成功 return format_response( data={ "version": result.get("version"), "updateTime": result.get("updateTime") }, message=result["message"] ) except Exception as e: logger.error(f"保存输入参数失败: {str(e)}") return error_response(message=f"保存输入参数失败: {str(e)}", code=500) @router.get("/input-params/{id}") async def get_input_param_types( id: str = Path(..., description="任务ID") ): """ 获取指定任务的输入参数配置信息 用于编辑任务时展示当前任务已配置的输入参数 """ try: result = await TaskEditService.get_task_input_params(id) return result except Exception as e: return error_response(message=f"获取任务输入参数配置失败: {str(e)}", code=500) @router.post("/basic-settings/{id}") async def save_basic_settings( id: str = Path(..., description="任务ID"), basic_info: TaskBasicInfo = Body(...) ): """ 保存任务的基本信息设置 包括任务名称、备注、是否在任务异常结束或终止时解锁库位等配置 """ try: result = await TaskEditService.save_basic_settings(id, basic_info) if not result["success"]: return error_response(message=result["message"], code=404) # 如果数据没有变化,返回特定提示,但仍然返回当前数据 if not result["changed"]: return format_response( data={ "id": result["id"], "label": result["label"], "remark": result["remark"], "releaseSites": result["releaseSites"] }, message=result["message"] ) # 数据有变化且保存成功 return format_response( data={ "id": result["id"], "label": result["label"], "remark": result["remark"], "releaseSites": result["releaseSites"], "updateTime": result["updateTime"] }, message=result["message"] ) except Exception as e: return error_response(message=f"保存基本设置失败: {str(e)}", code=500) @router.get("/basic-settings/{id}") async def get_basic_settings( id: str = Path(..., description="任务ID") ): """ 获取任务的基本信息设置 包括任务名称、备注、是否在任务异常结束或终止时解锁库位等配置 """ try: result = await TaskEditService.get_basic_settings(id) if not result["success"]: return error_response(message=result["message"], code=404) return format_response(data=result["data"]) except Exception as e: return error_response(message=f"获取任务基本设置失败: {str(e)}", code=500) @router.get("/common-params") async def get_common_params(): """ 获取常用参数的展示字段和字段值类型 用于任务编辑页面中间部分上侧的常用字段展示区域 """ try: result = await TaskEditService.get_common_params() return format_response(data=result) except Exception as e: return error_response(message=f"获取常用参数失败: {str(e)}", code=500)