VWED_server/routes/task_edit_api.py

451 lines
17 KiB
Python
Raw Normal View History

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)