#!/usr/bin/env python # -*- coding: utf-8 -*- """ 任务运行记录服务模块 提供任务运行记录相关的服务方法 """ import json from typing import Dict, List, Any from sqlalchemy import select import datetime from data.models.blockrecord import VWEDBlockRecord from data.models.taskrecord import VWEDTaskRecord from data.models.taskdef import VWEDTaskDef from data.models.tasklog import VWEDTaskLog from data.session import get_async_session from utils.logger import get_logger from data.enum.task_block_record_enum import TaskBlockRecordStatus # 设置日志 logger = get_logger("service.task_record_service") class TaskRecordService: """ 任务运行记录服务类 提供与任务运行记录相关的方法 """ @staticmethod async def get_task_blocks(task_record_id: str) -> Dict[str, Any]: """ 获取指定任务记录下的所有块运行情况 Args: task_record_id: 任务记录ID Returns: Dict: 包含块运行情况的字典 """ try: async with get_async_session() as session: # 构建查询语句 query = select(VWEDBlockRecord).where( VWEDBlockRecord.task_record_id == task_record_id ).order_by(VWEDBlockRecord.started_on) # 执行查询 result = await session.execute(query) blocks = result.scalars().all() if not blocks: return { "success": True, "message": f"未找到任务记录 {task_record_id} 的块运行情况", "data": [] } # 转换为字典列表 block_list = [] for block in blocks: block_dict = { "id": block.id, "block_name": block.block_name, "block_id": block.block_id, "status": block.status, "started_on": block.started_on.isoformat() if block.started_on else None, "ended_on": block.ended_on.isoformat() if block.ended_on else None, "ended_reason": block.ended_reason, "block_execute_name": block.block_execute_name, "block_input_params_value": json.loads(block.block_input_params_value) if block.block_input_params_value else None, "block_out_params_value": json.loads(block.block_out_params_value) if block.block_out_params_value else None, "remark": block.remark } block_list.append(block_dict) return { "success": True, "message": f"成功获取任务记录 {task_record_id} 的块运行情况", "data": block_list } except Exception as e: logger.error(f"获取任务块运行情况失败: {str(e)}") return { "success": False, "message": f"获取任务块运行情况失败: {str(e)}", "data": [] } @staticmethod async def get_block_detail(block_record_id: str) -> Dict[str, Any]: """ 获取指定块记录的详细信息 Args: block_record_id: 块记录ID Returns: Dict: 包含块记录详细信息的字典 """ try: async with get_async_session() as session: # 构建查询语句 query = select(VWEDBlockRecord).where( VWEDBlockRecord.id == block_record_id ) # 执行查询 result = await session.execute(query) block = result.scalars().first() if not block: return { "success": False, "message": f"未找到ID为 {block_record_id} 的块记录", "data": None } # 转换为字典 block_dict = { "id": block.id, "block_name": block.block_name, "block_id": block.block_id, "block_config_id": block.block_config_id, "block_input_params": json.loads(block.block_input_params) if block.block_input_params else None, "block_input_params_value": json.loads(block.block_input_params_value) if block.block_input_params_value else None, "block_out_params_value": json.loads(block.block_out_params_value) if block.block_out_params_value else None, "block_internal_variables": json.loads(block.block_internal_variables) if block.block_internal_variables else None, "block_execute_name": block.block_execute_name, "task_id": block.task_id, "task_record_id": block.task_record_id, "started_on": block.started_on.isoformat() if block.started_on else None, "ended_on": block.ended_on.isoformat() if block.ended_on else None, "ended_reason": block.ended_reason, "status": block.status, "ctrl_status": block.ctrl_status, "input_params": json.loads(block.input_params) if block.input_params else None, "internal_variables": json.loads(block.internal_variables) if block.internal_variables else None, "output_params": json.loads(block.output_params) if block.output_params else None, "version": block.version, "remark": block.remark } return { "success": True, "message": "成功获取块记录详情", "data": block_dict } except Exception as e: logger.error(f"获取块记录详情失败: {str(e)}") return { "success": False, "message": f"获取块记录详情失败: {str(e)}", "data": None } @staticmethod async def stop_task_record(task_record_id: str) -> Dict[str, Any]: """ 停止指定任务记录下的所有运行任务实例,同时禁用定时任务 Args: task_record_id: 任务记录ID Returns: Dict: 包含停止结果的响应 """ # 导入增强版调度器 from services.enhanced_scheduler import scheduler from data.enum.task_record_enum import TaskStatus from datetime import datetime try: async with get_async_session() as session: # 查找所有正在运行的任务记录 running_tasks_query = await session.execute( select(VWEDTaskRecord) .where( VWEDTaskRecord.id == task_record_id, VWEDTaskRecord.status == TaskStatus.RUNNING # 执行中状态码 ) ) running_tasks = running_tasks_query.scalars().first() if not running_tasks: return { "success": True, "message": "任务记录中没有运行中的任务" } # 取消所有运行中的任务 cancel_result = await scheduler.cancel_task(task_record_id) if cancel_result.get("success", False): running_tasks.status = TaskStatus.CANCELED running_tasks.ended_on = datetime.now() running_tasks.ended_reason = "任务终止" await session.commit() return { "success": True, "message": "任务终止成功", "data": { "task_record_id": task_record_id, "status": running_tasks.status, "ended_on": running_tasks.ended_on, "ended_reason": running_tasks.ended_reason } } else: return { "success": False, "message": "任务终止失败", "data": cancel_result } except Exception as e: logger.error(f"任务记录终止失败: {str(e)}") return { "success": False, "message": f"任务记录终止失败: {str(e)}" } @staticmethod async def set_task_error(task_record_id: str, error_reason: str) -> Dict[str, Any]: """ 将指定任务记录及其相关任务块状态设置为错误状态 Args: task_record_id: 任务记录ID error_reason: 错误原因 Returns: Dict: 包含设置结果的响应 """ from services.enhanced_scheduler import scheduler from data.enum.task_record_enum import TaskStatus from datetime import datetime try: async with get_async_session() as session: # 查找任务记录 task_query = await session.execute( select(VWEDTaskRecord) .where(VWEDTaskRecord.id == task_record_id) ) task_record = task_query.scalars().first() if not task_record: return { "success": False, "message": f"未找到任务记录 {task_record_id}" } # 如果任务正在运行,先取消它 if task_record.status == TaskStatus.RUNNING: cancel_result = await scheduler.set_task_error(task_record_id, error_reason) if not cancel_result.get("success", False): logger.warning(f"取消任务 {task_record_id} 失败: {cancel_result}") # 设置任务状态为失败 task_record.status = TaskStatus.FAILED task_record.ended_on = datetime.now() task_record.ended_reason = error_reason # 查找所有相关的任务块记录 blocks_query = await session.execute( select(VWEDBlockRecord) .where(VWEDBlockRecord.task_record_id == task_record_id) ) block_records = blocks_query.scalars().all() # 设置所有任务块状态为失败 updated_blocks = [] for block in block_records: # 只更新未完成的任务块 if block.status not in [TaskBlockRecordStatus.SUCCESS, TaskBlockRecordStatus.FAILED]: block.status = TaskBlockRecordStatus.FAILED block.ended_on = datetime.now() block.ended_reason = error_reason updated_blocks.append({ "block_id": block.id, "block_name": block.block_name, "status": block.status }) # 提交所有更改 await session.commit() return { "success": True, "message": "任务状态设置为错误成功", "data": { "task_record_id": task_record_id, "task_status": task_record.status, "error_reason": error_reason, "ended_on": task_record.ended_on, "updated_blocks_count": len(updated_blocks), "updated_blocks": updated_blocks } } except Exception as e: logger.error(f"设置任务错误状态失败: {str(e)}") return { "success": False, "message": f"设置任务错误状态失败: {str(e)}" } @staticmethod async def get_block_results(task_record_id: str, use_new_version: bool = True) -> Dict[str, Any]: """ 获取指定任务记录的执行结果,按照任务定义的嵌套结构组织 Args: task_record_id: 任务记录ID use_new_version: 是否使用新版本(基于层级关系字段),默认True Returns: Dict: 包含嵌套结构执行结果的响应 """ # 使用新版本(基于层级关系字段) if use_new_version: from services.task_record_service_new import get_block_results_v2 return await get_block_results_v2(task_record_id) # 旧版本(基于JSON解析) from services.execution.handlers.model.block_name import BLOCK_NAME_STR try: async with get_async_session() as session: # 获取任务记录信息,包含任务定义详情 task_query = select(VWEDTaskRecord).where( VWEDTaskRecord.id == task_record_id ) task_result = await session.execute(task_query) task_record = task_result.scalars().first() if not task_record: return { "success": False, "message": f"未找到任务记录 {task_record_id}", "data": None } # 获取所有块执行记录 blocks_query = select(VWEDBlockRecord).where( VWEDBlockRecord.task_record_id == task_record_id ).order_by(VWEDBlockRecord.started_on) blocks_result = await session.execute(blocks_query) blocks = blocks_result.scalars().all() # 获取所有任务日志 logs_query = select(VWEDTaskLog).where( VWEDTaskLog.task_record_id == task_record_id ) logs_result = await session.execute(logs_query) logs = logs_result.scalars().all() # 构建块记录和日志的映射 # 使用 block_id (任务定义中的id) 作为key,可以正确处理换机器人后block_name变化的情况 block_map = {} block_record_map = {} # block_record_id -> block for block in blocks: # 如果同一个block_id有多个记录(换机器人导致),使用最新的 if block.block_id not in block_map or block.started_on > block_map[block.block_id].started_on: block_map[block.block_id] = block block_record_map[block.id] = block # 使用 block_record_id 关联日志,避免block_name重复导致的日志混乱 log_map = {} # block_id -> logs for log in logs: # 通过 block_record_id 找到对应的 block,再通过 block.block_id 建立映射 if log.block_record_id and log.block_record_id in block_record_map: block = block_record_map[log.block_record_id] if block.block_id not in log_map: log_map[block.block_id] = [] log_map[block.block_id].append(log) else: # 兼容旧数据:没有block_record_id的日志,使用task_block_id(即block_name)匹配 # 这种情况下,尝试通过block_name找到对应的block matching_block = None for block in blocks: if block.block_name == log.task_block_id: matching_block = block break if matching_block: if matching_block.block_id not in log_map: log_map[matching_block.block_id] = [] log_map[matching_block.block_id].append(log) # 解析任务定义详情 task_def_detail = None if task_record.task_def_detail: try: task_def_detail = json.loads(task_record.task_def_detail) except json.JSONDecodeError: logger.warning(f"任务记录 {task_record_id} 的task_def_detail解析失败") if not task_def_detail or 'rootBlock' not in task_def_detail: # 如果没有任务定义详情,回退到原始的平铺模式 return await TaskRecordService._get_block_results_fallback( task_record_id, blocks, logs, BLOCK_NAME_STR ) # 构建嵌套结构 nested_results = await TaskRecordService._build_nested_block_results( task_def_detail['rootBlock'], block_map, log_map, BLOCK_NAME_STR, level=0 ) return { "success": True, "message": "成功获取任务记录嵌套执行结果", "data": nested_results } except Exception as e: logger.error(f"获取任务记录执行结果失败: {str(e)}") return { "success": False, "message": f"获取任务记录执行结果失败: {str(e)}" } @staticmethod async def _build_nested_block_results( block_def: Dict[str, Any], block_map: Dict[str, VWEDBlockRecord], log_map: Dict[str, List[VWEDTaskLog]], block_name_mapping: Dict[str, str], level: int = 0 ) -> Dict[str, Any]: """ 递归构建嵌套的块执行结果 Args: block_def: 任务块定义 block_map: 块名称到执行记录的映射 log_map: 块名称到日志记录的映射 block_name_mapping: 块类型名称映射 level: 嵌套层级(用于调试) Returns: Dict: 嵌套的块执行结果 """ from services.execution.handlers.model.block_name import ProgressBlockName block_id = str(block_def.get('id', '')) block_name = block_def.get('name', '') block_type = block_def.get('blockType', '') # 获取执行记录(使用block_id作为key) execution_record = block_map.get(block_id) # 构建基础结果结构 result = { "blockId": block_id, "blockName": block_name, "blockType": block_type, "blockTypeName": block_name_mapping.get(block_type, block_type), "level": level, "executionStatus": None, "executionRecord": None, "logs": [], "children": [] } # 如果有执行记录,添加执行信息 if execution_record: execution_info = { "recordId": execution_record.id, "status": execution_record.status, "startedOn": execution_record.started_on.isoformat() if execution_record.started_on else None, "endedOn": execution_record.ended_on.isoformat() if execution_record.ended_on else None, "endedReason": execution_record.ended_reason, "inputParamsValue": json.loads(execution_record.block_input_params_value) if execution_record.block_input_params_value else None, "outputParamsValue": json.loads(execution_record.block_out_params_value) if execution_record.block_out_params_value else None, "remark": execution_record.remark } result["executionRecord"] = execution_info result["executionStatus"] = execution_record.status else: # 如果没有执行记录,标记为未执行状态 from data.enum.task_block_record_enum import TaskBlockRecordStatus result["executionStatus"] = TaskBlockRecordStatus.NOT_EXECUTED result["executionRecord"] = { "recordId": None, "status": TaskBlockRecordStatus.NOT_EXECUTED, "startedOn": None, "endedOn": None, "endedReason": "任务块尚未执行", "inputParamsValue": None, "outputParamsValue": None, "remark": "任务块尚未执行" } # 如果有日志记录,添加日志信息(使用block_id作为key) if block_id in log_map: logs = log_map[block_id] for log in logs: try: log_messages = json.loads(log.message) if log.message else {} log_info = { "logId": log.id, "level": log.level, "createdAt": log.created_at.isoformat() if log.created_at else None, "message": log_messages.get("message", ""), "output": log_messages.get("output", "") } result["logs"].append(log_info) except json.JSONDecodeError: # 如果日志不是JSON格式,直接使用原始消息 log_info = { "logId": log.id, "level": log.level, "createdAt": log.created_at.isoformat() if log.created_at else None, "message": log.message or "", "output": "" } result["logs"].append(log_info) # 检查是否是迭代类型的块(需要特殊处理子块结果) is_iteration_block = block_type in [ ProgressBlockName.ITERATE_LIST, ProgressBlockName.WHILE, ProgressBlockName.REPEAT_NUM ] # 递归处理子块 children_def = block_def.get('children', {}) if children_def: if is_iteration_block: # 对于迭代类型的块,从日志中提取迭代结果并重新组织 iteration_children = await TaskRecordService._extract_iteration_results( block_def, block_map, log_map, block_name_mapping, level ) result["children"] = iteration_children else: # 普通块:直接递归处理子块 for children_blocks in children_def.values(): if isinstance(children_blocks, list): for child_block in children_blocks: child_result = await TaskRecordService._build_nested_block_results( child_block, block_map, log_map, block_name_mapping, level + 1 ) result["children"].append(child_result) return result @staticmethod async def _extract_iteration_results( block_def: Dict[str, Any], block_map: Dict[str, VWEDBlockRecord], log_map: Dict[str, List[VWEDTaskLog]], block_name_mapping: Dict[str, str], level: int ) -> List[Dict[str, Any]]: """ 从迭代块的日志中提取并重组迭代执行结果 Args: block_def: 迭代块定义 block_map: 块名称到执行记录的映射 log_map: 块名称到日志记录的映射 block_name_mapping: 块类型名称映射 level: 当前层级 Returns: List[Dict]: 按迭代次数组织的子块执行结果 """ block_id = str(block_def.get('id', '')) block_name = block_def.get('name', '') block_type = block_def.get('blockType', '') # 获取该块的日志,从中提取迭代信息(使用block_id作为key) logs = log_map.get(block_id, []) # 查找包含迭代结果的日志 iteration_results = [] iteration_data = None for log in logs: try: log_messages = json.loads(log.message) if log.message else {} print(log_messages, "==================") output = log_messages.get("output", {}) # 检查是否包含迭代结果 if isinstance(output, dict) and "iterationResults" in output: iteration_data = output["iterationResults"] break # 找到迭代结果后退出循环 except (json.JSONDecodeError, KeyError, TypeError) as e: logger.debug(f"解析日志失败: {str(e)}") continue # 如果在当前块的日志中没有找到迭代结果,尝试从所有日志中查找包含该块子块执行结果的日志 if not iteration_data: logger.debug(f"在块 {block_name} 的日志中未找到迭代结果,尝试从所有日志中查找") for log_block_name, log_list in log_map.items(): for log in log_list: try: log_messages = json.loads(log.message) if log.message else {} output = log_messages.get("output", {}) # 检查results数组中是否包含该块的执行结果 if isinstance(output, dict) and "results" in output: results = output.get("results", []) for result_item in results: # 检查child_id是否匹配当前块 if result_item.get("child_id") == block_def.get("id"): # 检查是否包含iterationResults child_output = result_item.get("output", {}) if isinstance(child_output, dict) and "iterationResults" in child_output: iteration_data = child_output["iterationResults"] logger.debug(f"找到块 {block_name} 的迭代结果") break if iteration_data: break except (json.JSONDecodeError, KeyError, TypeError): continue if iteration_data: break # 处理迭代数据 if iteration_data: # 遍历每次迭代 for iteration in iteration_data: iteration_index = iteration.get("index", 0) iteration_result = iteration.get("result", {}) # 提取该次迭代的子块结果 iteration_output = iteration_result.get("output", {}) iteration_block_results = iteration_output.get("results", []) # 构建该次迭代的结果结构 iteration_item = { "iterationIndex": iteration_index, "success": iteration.get("success", False), "children": [] } # 处理该次迭代中的每个子块 children_def = block_def.get('children', {}) if children_def: for branch_name, children_blocks in children_def.items(): if isinstance(children_blocks, list): for child_block in children_blocks: # 为每个子块创建结果对象 child_result = await TaskRecordService._build_iteration_child_result( child_block, iteration_block_results, iteration_index, block_map, log_map, block_name_mapping, level + 1 ) iteration_item["children"].append(child_result) iteration_results.append(iteration_item) # 如果没有找到迭代结果,回退到普通处理方式 if not iteration_results: logger.warning(f"迭代块 {block_name} (ID: {block_def.get('id')}) 没有找到迭代结果,使用普通方式处理") children_def = block_def.get('children', {}) normal_children = [] if children_def: for children_blocks in children_def.values(): if isinstance(children_blocks, list): for child_block in children_blocks: child_result = await TaskRecordService._build_nested_block_results( child_block, block_map, log_map, block_name_mapping, level + 1 ) normal_children.append(child_result) return normal_children return iteration_results @staticmethod async def _build_iteration_child_result( child_block_def: Dict[str, Any], iteration_results: List[Dict[str, Any]], iteration_index: int, block_map: Dict[str, VWEDBlockRecord], log_map: Dict[str, List[VWEDTaskLog]], block_name_mapping: Dict[str, str], level: int ) -> Dict[str, Any]: """ 构建迭代块子块的执行结果 Args: child_block_def: 子块定义 iteration_results: 当前迭代的执行结果列表 iteration_index: 迭代索引 block_map: 块名称到执行记录的映射 log_map: 块名称到日志记录的映射 block_name_mapping: 块类型名称映射 level: 嵌套层级 Returns: Dict: 子块执行结果 """ block_id = str(child_block_def.get('id', '')) block_name = child_block_def.get('name', '') block_type = child_block_def.get('blockType', '') # 从迭代结果中查找该子块的执行输出 child_output = None for result_item in iteration_results: if result_item.get("child_id") == child_block_def.get("id"): child_output = result_item.get("output", {}) break # 获取执行记录(注意:迭代块的子块可能被执行多次,这里获取的是最后一次) # 使用block_id作为key execution_record = block_map.get(block_id) # 构建结果结构 result = { "blockId": block_id, "blockName": block_name, "blockType": block_type, "blockTypeName": block_name_mapping.get(block_type, block_type), "level": level, "iterationIndex": iteration_index, "executionStatus": None, "executionRecord": None, "executionOutput": child_output, # 该次迭代的执行输出 "logs": [], "children": [] } # 如果有执行记录,添加执行信息(注意:这是所有迭代的汇总信息) if execution_record: execution_info = { "recordId": execution_record.id, "status": execution_record.status, "startedOn": execution_record.started_on.isoformat() if execution_record.started_on else None, "endedOn": execution_record.ended_on.isoformat() if execution_record.ended_on else None, "endedReason": execution_record.ended_reason, "inputParamsValue": json.loads(execution_record.block_input_params_value) if execution_record.block_input_params_value else None, "outputParamsValue": json.loads(execution_record.block_out_params_value) if execution_record.block_out_params_value else None, "remark": execution_record.remark } result["executionRecord"] = execution_info result["executionStatus"] = execution_record.status # 添加该次迭代的日志 # 从child_output中提取该次迭代的日志信息(如果有的话) if child_output and isinstance(child_output, dict): # 将child_output作为该次迭代的执行日志 log_info = { "logId": f"iteration_{iteration_index}_{block_name}", "level": 1, "createdAt": None, "message": f"第{iteration_index + 1}次迭代执行结果", "output": child_output } result["logs"].append(log_info) # 递归处理嵌套的子块(如果有) children_def = child_block_def.get('children', {}) if children_def: for children_blocks in children_def.values(): if isinstance(children_blocks, list): for nested_child in children_blocks: nested_result = await TaskRecordService._build_nested_block_results( nested_child, block_map, log_map, block_name_mapping, level + 1 ) result["children"].append(nested_result) return result @staticmethod async def _get_block_results_fallback( task_record_id: str, blocks: List[VWEDBlockRecord], logs: List[VWEDTaskLog], block_name_mapping: Dict[str, str] ) -> Dict[str, Any]: """ 回退方案:当无法解析任务定义时,使用原始的平铺模式 Args: task_record_id: 任务记录ID blocks: 所有块执行记录 logs: 所有日志记录 block_name_mapping: 块类型名称映射 Returns: Dict: 平铺格式的执行结果 """ block_results = [] processed_block_names = set() log_map = {} # 构建日志映射 for log in logs: if log.task_block_id not in log_map: log_map[log.task_block_id] = [] log_map[log.task_block_id].append(log) for block in sorted(blocks, key=lambda x: x.ended_on or x.created_at, reverse=True): if block.block_name in processed_block_names: continue processed_block_names.add(block.block_name) if block.status != TaskBlockRecordStatus.SUCCESS: if block.block_name == "-1": context = f"[{block.block_execute_name}@{block_name_mapping.get(block.block_execute_name, '其他')}] {block.task_record_id}" else: context = f"[{block.block_execute_name}@{block_name_mapping.get(block.block_execute_name, '其他')}] {block.ended_reason}" block_results.append({ "created_at": block.created_at, "context": context, "status": block.status, "blockName": block.block_name, "blockType": block.block_execute_name }) else: # 处理成功的块 if block.block_name == "-1": context = f"[{block.block_execute_name}@{block_name_mapping.get(block.block_execute_name, '其他')}] {block.task_record_id}" block_results.append({ "created_at": block.created_at, "context": context, "status": block.status, "blockName": block.block_name, "blockType": block.block_execute_name }) else: # 查找对应的日志 block_logs = log_map.get(block.block_name, []) if block_logs: for task_log in block_logs: try: messages = json.loads(task_log.message) if task_log.message else {} message = messages.get("message", "") output = messages.get("output", "") context_parts = [f"[{block.block_execute_name}@{block_name_mapping.get(block.block_execute_name, '其他')}]"] if message: context_parts.append(message) if output: if isinstance(output, dict) and output.get("message"): context_parts.append(f"@{output.get('message')}") else: context_parts.append(f"@{str(output)}") block_results.append({ "created_at": task_log.created_at, "context": " ".join(context_parts), "status": block.status, "blockName": block.block_name, "blockType": block.block_execute_name }) except json.JSONDecodeError: context = f"[{block.block_execute_name}@{block_name_mapping.get(block.block_execute_name, '其他')}] {task_log.message}" block_results.append({ "created_at": task_log.created_at, "context": context, "status": block.status, "blockName": block.block_name, "blockType": block.block_execute_name }) else: # 没有日志时使用块的结束原因 context = f"[{block.block_execute_name}@{block_name_mapping.get(block.block_execute_name, '其他')}] {block.ended_reason or '执行完成'}" block_results.append({ "created_at": block.created_at, "context": context, "status": block.status, "blockName": block.block_name, "blockType": block.block_execute_name }) logger.warning(f"任务记录 {task_record_id} 的块 {block.block_name} 没有日志") return { "success": True, "message": "成功获取任务记录执行结果(平铺模式)", "data": sorted(block_results, key=lambda x: x["created_at"], reverse=True) } @staticmethod async def get_task_record_detail(task_record_id: str) -> Dict[str, Any]: """ 获取指定任务记录的详细信息 Args: task_record_id: 任务记录ID Returns: Dict: 包含任务记录详细信息的字典 """ try: async with get_async_session() as session: # 构建查询语句 query = select(VWEDTaskRecord).where( VWEDTaskRecord.id == task_record_id ) # 执行查询 result = await session.execute(query) task_record = result.scalars().first() if not task_record: return { "success": False, "message": f"未找到ID为 {task_record_id} 的任务记录", "data": None } # 计算执行时长(如果任务已结束) execution_time = None if task_record.ended_on and task_record.first_executor_time: time_diff = task_record.ended_on - task_record.first_executor_time execution_time = int(time_diff.total_seconds() * 1000) # 转换为毫秒 elif task_record.executor_time: execution_time = task_record.executor_time # 转换为字典 task_dict = { "id": task_record.id, "task_id": task_record.def_id, "task_name": task_record.def_label, "task_version": task_record.def_version, "status": task_record.status, "input_params": json.loads(task_record.input_params) if task_record.input_params else None, "started_on": task_record.first_executor_time.isoformat() if task_record.first_executor_time else None, "ended_on": task_record.ended_on.isoformat() if task_record.ended_on else None, "ended_reason": task_record.ended_reason, "execution_time": execution_time, "created_at": task_record.created_at.isoformat() if task_record.created_at else None, "updated_at": task_record.updated_at.isoformat() if task_record.updated_at else None, "agv_id": task_record.agv_id, "parent_task_record_id": task_record.parent_task_record_id, "root_task_record_id": task_record.root_task_record_id, "state_description": task_record.state_description, "if_have_child_task": bool(task_record.if_have_child_task) if task_record.if_have_child_task is not None else None, "periodic_task": task_record.periodic_task, "priority": task_record.priority, "work_stations": task_record.work_stations, "work_types": task_record.work_types, "variables": json.loads(task_record.variables) if task_record.variables else None, "source_type": task_record.source_type, "source_system": task_record.source_system, "source_user": task_record.source_user, "source_device": task_record.source_device, "source_ip": task_record.source_ip, "source_time": task_record.source_time.isoformat() if task_record.source_time else None, "source_client_info": task_record.source_client_info, "source_remarks": task_record.source_remarks } return { "success": True, "message": "成功获取任务记录详情", "data": task_dict } except Exception as e: logger.error(f"获取任务记录详情失败: {str(e)}") return { "success": False, "message": f"获取任务记录详情失败: {str(e)}", "data": None } @staticmethod async def switch_robot(task_record_id: str, switch_request: Dict[str, Any], client_ip: str = None, client_info: str = None, tf_api_token: str = None) -> Dict[str, Any]: """ 换机器人功能 两种场景: 1. 未载货: 提取原始任务块,更新选择机器人参数,追加到任务定义 2. 已载货: 先添加放货块,再提取并修改原始任务块(替换GetIdleCrowdedSiteBp/更新siteId),追加到任务定义 Args: task_record_id: 任务记录ID switch_request: 换机器人请求参数 client_ip: 客户端IP地址 client_info: 客户端信息 tf_api_token: 系统任务令牌 Returns: Dict: 包含换机器人结果的响应 """ from data.enum.task_record_enum import TaskStatus from services.enhanced_scheduler import scheduler from services.task_record_switch_robot_helpers import ( cancel_and_cleanup_task, extract_original_task_blocks, recursively_update_select_agv_blocks, validate_site_id_type, find_pick_storage_location_block, replace_get_crowded_site_to_get_site, build_put_cargo_blocks_for_storage_location, build_put_cargo_blocks_for_station, check_select_robot_block_success, validate_and_convert_site_id ) try: async with get_async_session() as session: # 1. 查询任务记录 task_query = await session.execute( select(VWEDTaskRecord) .where(VWEDTaskRecord.id == task_record_id) ) task_record = task_query.scalars().first() if not task_record: return {"success": False, "message": f"未找到任务记录 {task_record_id}", "code": 404} # 2. 查询任务定义以获取 map_id task_def_query = await session.execute( select(VWEDTaskDef) .where(VWEDTaskDef.id == task_record.def_id) ) task_def = task_def_query.scalars().first() if not task_def: return {"success": False, "message": f"未找到任务定义 {task_record.def_id}", "code": 404} map_id = task_def.map_id # 2. 检查任务状态 if task_record.status != TaskStatus.RUNNING: return {"success": False, "message": f"任务状态为 {task_record.status},只能对运行中的任务换机器人", "code": 400} # 3. 解析任务定义详情 task_def_detail = None if task_record.task_def_detail: try: task_def_detail = json.loads(task_record.task_def_detail) except json.JSONDecodeError: return {"success": False, "message": "任务定义解析失败", "code": 400} if not task_def_detail or 'rootBlock' not in task_def_detail: return {"success": False, "message": "任务定义缺少rootBlock", "code": 400} # 3.1 检查原任务中选择机器人块是否执行成功 robot_success, robot_error = await check_select_robot_block_success(session, task_record_id) if not robot_success: return {"success": False, "message": robot_error, "code": 400} # 4. 获取换机器人参数 has_cargo = switch_request.get('has_cargo', False) robot_name = switch_request.get('robot_name') robot_group = switch_request.get('robot_group') robot_label_group = switch_request.get('robot_label_group') switch_reason = switch_request.get('switch_reason', '换机器人') # 5. 取消正在执行的任务并清理资源 # 注意: cancel_task 方法会自动生成所有块的执行记录 await cancel_and_cleanup_task(scheduler, task_record_id, switch_reason) logger.info("已取消任务并清理资源(包括生成块记录)") # 6. 提取原始任务块列表(rootBlock的children) original_blocks = extract_original_task_blocks(task_def_detail) logger.info(f"提取了 {len(original_blocks)} 个原始任务块") # 7. 根据是否载货执行不同逻辑 new_combined_blocks = [] if not has_cargo: # 未载货场景: 更新选择机器人参数,并转换站点到库位 logger.info(f"未载货场景: 更新原始任务块的选择机器人参数") # 7.1 从原始任务块中提取站点参数并转换为库位 # 查找取货的获取库位块,检查是否需要站点转换 from services.task_record_switch_robot_helpers import find_and_convert_station_to_storage converted_blocks = await find_and_convert_station_to_storage( session, original_blocks, map_id ) # 7.2 更新选择机器人参数 updated_blocks = recursively_update_select_agv_blocks( converted_blocks, robot_name, robot_group, robot_label_group ) new_combined_blocks = updated_blocks else: # 已载货场景 logger.info(f"已载货场景: 先添加放货块,再修改原始任务块") # 获取传入的放货参数 site_id = switch_request.get('site_id') action = switch_request.get('action') if not site_id or not action: return {"success": False, "message": "已载货场景需要提供site_id和action参数", "code": 400} # 8. 校验并转换site_id(站点转库位) validate_success, validated_site_id, validate_error = await validate_and_convert_site_id( session, site_id, map_id, task_def_detail, has_cargo ) if not validate_success: return {"success": False, "message": validate_error, "code": 400} logger.info(f"site_id '{site_id}' 校验并转换为: {validated_site_id}") # 9. 构建放货块(使用转换后的site_id,统一使用库位场景) put_cargo_blocks = build_put_cargo_blocks_for_storage_location( validated_site_id, action, robot_name, robot_group, robot_label_group ) logger.info(f"构建了 {len(put_cargo_blocks)} 个放货任务块") # 10. 修改原始任务块 # 10.1 查找取货的获取库位块 pick_block_result = find_pick_storage_location_block(original_blocks) if pick_block_result: pick_block, pick_idx, pick_list = pick_block_result logger.info(f"找到取货的获取库位块: {pick_block.get('blockType')}") # 10.2 如果是GetIdleCrowdedSiteBp,替换为GetIdleSiteBp,并使用转换后的site_id replace_get_crowded_site_to_get_site(pick_block, validated_site_id) # 10.3 更新所有选择机器人块的参数 updated_original_blocks = recursively_update_select_agv_blocks( original_blocks, robot_name, robot_group, robot_label_group ) # 11. 组合新任务块: 放货块 + 修改后的原始任务块 new_combined_blocks = put_cargo_blocks + updated_original_blocks # 12. 更新任务定义,将新组合的块追加到rootBlock的children task_def_detail['rootBlock']['children']['default'].extend(new_combined_blocks) logger.info(f"已将 {len(new_combined_blocks)} 个任务块追加到任务定义") # 13. 更新任务记录的task_def_detail和备注 task_record.task_def_detail = json.dumps(task_def_detail, ensure_ascii=False) scenario_desc = "未载货" if not has_cargo else "已载货" task_record.ended_reason = f"换机器人({scenario_desc}): {switch_reason}" # 14. 将任务状态重置为排队中,准备重新提交 task_record.status = TaskStatus.QUEUED logger.info(f"任务 {task_record_id} 状态重置为排队中,准备重新提交") await session.commit() # 15. 重新提交任务到调度器 logger.info(f"任务 {task_record_id} 重新提交到调度器") submit_result = await scheduler.submit_task(task_record_id=task_record_id) if not submit_result.get("success", False): logger.warning(f"重新提交任务失败: {submit_result}") # 如果提交失败,任务已经更新但未执行,需要回滚状态 task_record.status = TaskStatus.FAILED task_record.ended_reason = f"换机器人失败: {submit_result.get('message', '未知错误')}" task_record.ended_on = datetime.datetime.now() await session.commit() return { "success": False, "message": f"换机器人操作完成但重新提交任务失败: {submit_result.get('message', '未知错误')}", "code": 400, "data": { "task_record_id": task_record_id, "task_updated": True, "task_resubmitted": False } } logger.info(f"任务 {task_record_id} 重新提交成功") # 16. 返回成功结果 return { "success": True, "message": f"换机器人成功({scenario_desc}场景),任务已重新提交", "data": { "task_record_id": task_record_id, "switch_reason": switch_reason, "has_cargo": has_cargo, "robot_name": robot_name, "robot_group": robot_group, "robot_label_group": robot_label_group, "site_id": switch_request.get('site_id') if has_cargo else None, "action": switch_request.get('action') if has_cargo else None, "added_blocks": len(new_combined_blocks), "resubmitted": True } } except Exception as e: logger.error(f"换机器人失败: {str(e)}") import traceback traceback.print_exc() return { "success": False, "message": f"换机器人失败: {str(e)}", "code": 500 } @staticmethod async def retry_failed_task(task_record_id: str, client_ip: str = None, client_info: str = None, tf_api_token: str = None) -> Dict[str, Any]: """ 重新执行失败的任务 Args: task_record_id: 失败的任务记录ID client_ip: 客户端IP地址 client_info: 客户端信息 tf_api_token: 系统任务令牌 Returns: Dict: 包含重发结果的响应 """ from data.enum.task_record_enum import TaskStatus try: async with get_async_session() as session: # 查找失败的任务记录 task_query = await session.execute( select(VWEDTaskRecord) .where(VWEDTaskRecord.id == task_record_id) ) failed_task = task_query.scalars().first() if not failed_task: return { "success": False, "message": f"未找到任务记录 {task_record_id}", "code": 404 } # 检查任务状态是否为失败状态 if failed_task.status not in [TaskStatus.FAILED, TaskStatus.CANCELED]: return { "success": False, "message": f"任务状态为 {failed_task.status},只能重发失败/终止的任务", "code": 400 } # 检查是否有任务定义 if not failed_task.def_id: return { "success": False, "message": "任务记录缺少任务定义ID,无法重发", "code": 400 } # 构建重发请求参数 from routes.model.task_edit_model import TaskEditRunRequest # 解析原任务的输入参数 if failed_task.input_params: input_params = json.loads(failed_task.input_params) else: input_params = None # 构建运行请求 run_request = TaskEditRunRequest( taskId=failed_task.def_id, params=input_params if input_params else [], source_type=failed_task.source_type, source_system=failed_task.source_system or "VWED-RETRY", source_device=failed_task.source_device or "retry-system", source_user=failed_task.source_user, source_remarks=f"重发失败任务: {task_record_id}" ) # 调用任务编辑服务的运行方法 from services.task_edit_service import TaskEditService result = await TaskEditService.run_task( run_request, client_ip=client_ip, client_info=client_info, tf_api_token=tf_api_token ) if not result or not result.get("success", False): return { "success": False, "message": f"重发任务失败: {result.get('message', '未知错误') if result else '任务启动失败'}", "code": 500 } # 记录重发关系 logger.info(f"任务 {task_record_id} 重发成功,新任务记录ID: {result.get('data', {}).get('taskRecordId')}") return { "success": True, "message": "任务重发成功", "data": { "original_task_record_id": task_record_id, "new_task_record_id": result.get("data", {}).get("taskRecordId"), "new_task_id": result.get("data", {}).get("taskId"), "retry_time": datetime.datetime.now().isoformat(), "original_task_name": failed_task.def_label, "original_input_params": json.loads(failed_task.input_params) if failed_task.input_params else {} } } except Exception as e: logger.error(f"重发失败任务异常: {str(e)}") return { "success": False, "message": f"重发任务失败: {str(e)}", "code": 500 }