VWED_server/routes/task_edit_api.py
2025-05-12 15:43:21 +08:00

451 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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)