2025-04-30 16:57:46 +08:00
|
|
|
|
#!/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)
|
2025-05-12 15:43:21 +08:00
|
|
|
|
return result
|
2025-04-30 16:57:46 +08:00
|
|
|
|
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)
|
|
|
|
|
|