#!/usr/bin/env python # -*- coding: utf-8 -*- """ 同步服务模块 用于与天风系统进行数据同步 """ import json import aiohttp import asyncio from typing import Dict, Any, Optional from datetime import datetime from pydantic import BaseModel # 获取日志记录器 from utils.logger import get_logger logger = get_logger("services.sync_service") # 导入天风API配置 from config.tf_api_config import get_tf_api_config # 获取配置 tf_config = get_tf_api_config() # 接口配置 class TFApiConfig: """系统任务API配置""" # 基础URL BASE_URL = tf_config["base_url"] # 接口路径 CREATE_TASK_PATH = tf_config["endpoints"]["create_task"] CHOOSE_AMR_PATH = tf_config["endpoints"]["choose_amr"] ADD_ACTION_PATH = tf_config["endpoints"]["add_action"] CLOSURE_TASK_PATH = tf_config["endpoints"]["closure_task"] GET_TASK_BLOCK_PATH = tf_config["endpoints"]["get_task_block"] GET_TASK_BLOCK_ACTION_PATH = tf_config["endpoints"]["get_task_block_action"] SET_TASK_IN_PROGRESS_PATH = tf_config["endpoints"]["set_task_in_progress"] SET_TASK_COMPLETED_PATH = tf_config["endpoints"]["set_task_completed"] SET_TASK_TERMINATED_PATH = tf_config["endpoints"]["set_task_terminated"] SET_TASK_FAILED_PATH = tf_config["endpoints"]["set_task_failed"] # 超时设置(秒) TIMEOUT = tf_config["timeout"] # 重试次数 RETRY_TIMES = tf_config["retry_times"] # 模拟模式 MOCK_MODE = tf_config["mock_mode"] # token请求头 TOKEN_HEADER = tf_config["token_header"] class CreateTaskRequest(BaseModel): """创建任务请求参数""" vwedTaskId: str vwedTaskParentId: str name: str isPeriodic: int # 使用字符串类型,因为接口文档中显示为字符串 priority: int # 使用字符串类型,因为接口文档中显示为字符串 createTime: str # 格式: "2025-04-08 22:03:57" sceneId: str needAmr: int class ChooseAmrRequest(BaseModel): """选择AMR请求参数""" vwedTaskId: str # 任务id stationName: str # 关键站点名称 priority: int = 1 # 优先级 appointAmrName: str = "" # 指定AMR名称 appointAmrGroupName: str = "" # 指定AMR分组名称 class AddActionRequest(BaseModel): """添加动作请求参数""" taskBlockId: str stationName: str action: str class ApiResponse(BaseModel): """API响应基础模型""" success: bool message: str code: int result: Optional[Dict[str, Any]] = None timestamp: Optional[int] = None async def create_task(task_record_id: str, task_name: str, is_periodic: bool, priority: int, parent_id: str, token: str, map_id: str, is_agv: int) -> Optional[ApiResponse]: """ 调用系统任务创建任务接口 Args: task_record_id: VWED系统任务实例ID task_name: 任务名称 is_periodic: 是否周期任务 priority: 优先级(1-5) parent_id: 父任务ID,如果没有则为空 token: 认证令牌 map_id: 相关地图ID is_agv: 是否选择AGV Returns: Optional[ApiResponse]: 接口响应,如果请求失败则返回None """ # 构造请求参数 request_data = CreateTaskRequest( vwedTaskId=task_record_id, vwedTaskParentId=parent_id, name=task_name, isPeriodic=int(is_periodic), priority=str(priority), createTime=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), sceneId=map_id, needAmr=is_agv ) # 调用接口 url = f"{TFApiConfig.BASE_URL}{TFApiConfig.CREATE_TASK_PATH}" # 构建请求头 headers = {} if token: headers[TFApiConfig.TOKEN_HEADER] = token headers["x-tenant-id"] = "1000" logger.debug(f"使用认证令牌调用接口,令牌头: {TFApiConfig.TOKEN_HEADER}") else: # headers["Authorization"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDU3ODgwMzksInVzZXJuYW1lIjoiYWRtaW4ifQ.QmFrhe9nq8jdNRVtZYo-QK-31hS7AMAwMjwy8EGERQQ" headers["x-access-token"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDYyNzUwODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.iSp50PmKX1NmscNhVtSRtFwLGpuU1z71zvp0ki4BbTY" headers["x-tenant-id"] = "1000" try: logger.info(f"正在同步创建任务到天风系统: {task_record_id}") logger.debug(f"创建任务请求参数: {request_data.model_dump_json()}") async with aiohttp.ClientSession() as session: async with session.post( url, json=request_data.model_dump(), headers=headers, timeout=TFApiConfig.TIMEOUT ) as response: # 读取响应内容 response_text = await response.text() # 尝试解析JSON try: response_data = json.loads(response_text) if response_data.get("success"): logger.info(f"成功同步任务到系统任务: {task_record_id}") else: logger.warning(f"同步任务到系统任务失败: {response_data.get('message')}") return response_data except json.JSONDecodeError: logger.error(f"解析响应JSON失败: {response_text}") return None except aiohttp.ClientError as e: logger.error(f"调用系统任务创建任务接口失败: {str(e)}") return None except Exception as e: logger.error(f"同步任务到系统任务时发生错误: {str(e)}") return None async def create_choose_amr_task(task_id: str, key_station_name: str, amr_name: str = "", amr_group_name: str = "", token: str = None, priority: int = 1) -> Optional[ApiResponse]: """ 创建选择AMR任务 Args: task_id: 天风任务ID key_station_name: 关键站点名称 amr_name: 指定的AMR名称,可选 amr_group_name: 指定的AMR分组名称,可选 token: 认证令牌 priority: 优先级 Returns: Optional[ApiResponse]: 接口响应,如果请求失败则返回None """ # 构造请求参数 request_data = ChooseAmrRequest( vwedTaskId=task_id, stationName=key_station_name, appointAmrName=amr_name, appointAmrGroupName=amr_group_name, priority=priority ) # 调用接口 url = f"{TFApiConfig.BASE_URL}{TFApiConfig.CHOOSE_AMR_PATH}" # 构建请求头 headers = {} if token: headers[TFApiConfig.TOKEN_HEADER] = token headers["x-tenant-id"] = "1000" else: headers["x-access-token"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDYyNzUwODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.iSp50PmKX1NmscNhVtSRtFwLGpuU1z71zvp0ki4BbTY" headers["x-tenant-id"] = "1000" try: logger.info(f"正在创建选择AMR任务: {task_id}, 站点: {key_station_name}") async with aiohttp.ClientSession() as session: async with session.post( url, json=request_data.model_dump(), headers=headers, timeout=TFApiConfig.TIMEOUT ) as response: # 读取响应内容 response_text = await response.json() # 尝试解析JSON try: if response_text.get("success", False): logger.info(f"成功为任务选择AMR: {task_id}, AMR: {response_text.get('result', {}).get('amrName')}") else: logger.warning(f"为任务选择AMR失败: {response_text.get('message', '未知错误')}") return response_text except json.JSONDecodeError: logger.error(f"解析响应JSON失败: {response_text}") return None except Exception as e: logger.error(f"调用选择AMR接口失败: {str(e)}") return None async def add_action(task_id: str, station_name: str, action: str, token: str = None) -> Optional[ApiResponse]: """ 调用系统任务添加动作接口 Args: task_id: 系统任务ID station_name: 站点名称 action: 动作 token: 认证令牌 Returns: Optional[ApiResponse]: 接口响应,如果请求失败则返回None """ # 构造请求参数 request_data = AddActionRequest( taskBlockId=task_id, stationName=station_name, action=action ) # 调用接口 url = f"{TFApiConfig.BASE_URL}{TFApiConfig.ADD_ACTION_PATH}" # 构建请求头 headers = {} if token: headers[TFApiConfig.TOKEN_HEADER] = token headers["x-tenant-id"] = "1000" else: headers["x-access-token"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDYyNzUwODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.iSp50PmKX1NmscNhVtSRtFwLGpuU1z71zvp0ki4BbTY" headers["x-tenant-id"] = "1000" try: logger.info(f"正在为任务添加动作: {task_id}, 站点: {station_name}, 动作: {action}") async with aiohttp.ClientSession() as session: async with session.post( url, json=request_data.model_dump(), headers=headers, timeout=TFApiConfig.TIMEOUT ) as response: # 读取响应内容 response_text = await response.text() # 尝试解析JSON try: response_data = json.loads(response_text) if response_data.get("success", False): logger.info(f"成功为任务添加动作: {task_id}, 站点: {station_name}, 动作: {action}") else: logger.warning(f"为任务添加动作失败: {response_data.get('message', '未知错误')}") return response_data except json.JSONDecodeError: logger.error(f"解析响应JSON失败: {response_text}") return None except Exception as e: logger.error(f"调用添加动作接口失败: {str(e)}") return None async def closure_task(task_id: str, token: str = None) -> Optional[ApiResponse]: """ 调用系统任务封口任务接口 Args: task_id: 系统任务ID token: 认证令牌 Returns: Optional[ApiResponse]: 接口响应,如果请求失败则返回None """ # 调用接口 url = f"{TFApiConfig.BASE_URL}{TFApiConfig.CLOSURE_TASK_PATH.format(task_id)}" # 构建请求头 headers = {} if token: headers[TFApiConfig.TOKEN_HEADER] = token headers["x-tenant-id"] = "1000" else: headers["x-access-token"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDYyNzUwODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.iSp50PmKX1NmscNhVtSRtFwLGpuU1z71zvp0ki4BbTY" headers["x-tenant-id"] = "1000" try: logger.info(f"正在封口任务: {task_id}") async with aiohttp.ClientSession() as session: async with session.put( url, headers=headers, timeout=TFApiConfig.TIMEOUT ) as response: # 读取响应内容 response_text = await response.json() # 尝试解析JSON try: if response_text.get("success", False): logger.info(f"成功封口任务: {task_id}") else: logger.warning(f"封口任务失败: {response_text.get('message', '未知错误')}") return response_text except json.JSONDecodeError: logger.error(f"解析响应JSON失败: {response_text}") return None except Exception as e: logger.error(f"调用封口任务接口失败: {str(e)}") return None async def get_task_block_detail(task_block_id: str, token: str = None) -> Optional[Dict[str, Any]]: """ 获取任务块详情 Args: task_block_id: 任务块ID token: 认证令牌 Returns: Optional[Dict[str, Any]]: 接口响应,如果请求失败则返回None """ # 调用接口 url = f"{TFApiConfig.BASE_URL}{TFApiConfig.GET_TASK_BLOCK_PATH.format(id=task_block_id)}" # 构建请求头 headers = {} if token: headers[TFApiConfig.TOKEN_HEADER] = token headers["x-tenant-id"] = "1000" else: headers["x-access-token"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDYyNzUwODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.iSp50PmKX1NmscNhVtSRtFwLGpuU1z71zvp0ki4BbTY" headers["x-tenant-id"] = "1000" try: logger.info(f"正在获取任务块详情: {task_block_id}") async with aiohttp.ClientSession() as session: async with session.get( url, headers=headers, timeout=TFApiConfig.TIMEOUT ) as response: # 读取响应内容 response_text = await response.text() # 尝试解析JSON try: response_data = json.loads(response_text) if response_data.get("success", False): logger.info(f"成功获取任务块详情: {task_block_id} 具体详情: {response_data}") else: logger.warning(f"获取任务块详情失败: {response_data.get('message', '未知错误')}") return response_data except json.JSONDecodeError: logger.error(f"解析响应JSON失败: {response_text}") return None except Exception as e: logger.error(f"调用获取任务块详情接口失败: {str(e)}") return None async def get_task_block_action_detail(task_block_id: str, token: str = None) -> Optional[Dict[str, Any]]: """ 获取任务块动作详情 Args: task_block_id: 任务块ID token: 认证令牌 Returns: Optional[Dict[str, Any]]: 接口响应,如果请求失败则返回None """ # 调用接口 url = f"{TFApiConfig.BASE_URL}{TFApiConfig.GET_TASK_BLOCK_ACTION_PATH.format(id=task_block_id)}" # 构建请求头 headers = {} if token: headers[TFApiConfig.TOKEN_HEADER] = token headers["x-tenant-id"] = "1000" else: headers["x-access-token"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDYyNzUwODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.iSp50PmKX1NmscNhVtSRtFwLGpuU1z71zvp0ki4BbTY" headers["x-tenant-id"] = "1000" try: logger.info(f"正在获取任务块动作详情: {task_block_id}") async with aiohttp.ClientSession() as session: async with session.get( url, headers=headers, timeout=TFApiConfig.TIMEOUT ) as response: # 读取响应内容 response_text = await response.text() # 尝试解析JSON try: response_data = json.loads(response_text) if response_data.get("success", False): logger.info(f"成功获取任务块动作详情: {task_block_id} 具体详情: {response_data}") else: logger.warning(f"获取任务块动作详情失败: {response_data.get('message', '未知错误')}") return response_data except json.JSONDecodeError: logger.error(f"解析响应JSON失败: {response_text}") return None except Exception as e: logger.error(f"调用获取任务块动作详情接口失败: {str(e)}") return None async def wait_for_amr_selection(task_block_id: str, token: str = None) -> Optional[Dict[str, Any]]: """ 等待AMR选择完成,轮询任务块详情直到获取到AMR ID Args: task_block_id: 任务块ID token: 认证令牌 Returns: Optional[Dict[str, Any]]: 包含AMR ID的响应,如果请求失败则返回None """ logger.info(f"开始等待任务块 {task_block_id} 的AMR选择结果") retry_count = 0 # 使用固定的重试间隔0.5秒 actual_retry_interval = 0.5 # 无限循环,直到获取到结果 while True: retry_count += 1 response = await get_task_block_detail(task_block_id, token) if not response or not response.get("success", False): logger.warning(f"获取任务块详情失败,继续重试,当前尝试次数: {retry_count}") await asyncio.sleep(actual_retry_interval) continue # 从响应中获取关键字段 result = response.get("result", {}) amr_id = result.get("amrId", "") # 符合以下条件之一就可以返回: # 1. amrId有值 - 表示已经分配了AMR # 2. appointAmrId有值 - 表示指定了特定AMR # 3. appointAmrGroupId有值 - 表示指定了AMR组 if amr_id: if amr_id: logger.info(f"任务块 {task_block_id} 已选择AMR: {amr_id},共尝试 {retry_count} 次") return response # 否则继续等待 if retry_count % 10 == 0: # 每10次请求记录一次INFO级别日志 logger.info(f"任务块 {task_block_id} AMR选择未完成(amrId/appointAmrId/appointAmrGroupId均为空),已尝试 {retry_count} 次,继续等待...") else: # 其他时候记录DEBUG级别日志,减少日志数量 logger.debug(f"任务块 {task_block_id} AMR选择未完成,已尝试 {retry_count} 次") # 等待0.5秒后继续尝试 await asyncio.sleep(actual_retry_interval) async def wait_for_task_block_action_completion(task_block_id: str, token: str = None) -> Optional[Dict[str, Any]]: """ 等待任务块动作完成,轮询任务块动作详情直到获取到动作完成 Args: task_block_id: 任务块ID token: 认证令牌 Returns: Optional[Dict[str, Any]]: 包含AMR ID的响应,如果请求失败则返回None """ logger.info(f"开始等待任务块 {task_block_id} 的动作完成") retry_count = 0 # 使用固定的重试间隔0.5秒 actual_retry_interval = 0.5 # 无限循环,直到获取到结果 while True: retry_count += 1 response = await get_task_block_action_detail(task_block_id, token) if not response or not response.get("success", False): logger.warning(f"获取任务块动作 详情失败,继续重试,当前尝试次数: {retry_count}") await asyncio.sleep(actual_retry_interval) continue # 从响应中获取关键字段 result = response.get("result", {}) action_status = result.get("status", "") if action_status in [3, 4, 5]: logger.info(f"任务块 {task_block_id} 动作已完成,共尝试 {retry_count} 次") return response # 否则继续等待 if retry_count % 10 == 0: # 每10次请求记录一次INFO级别日志 logger.info(f"任务块 {task_block_id} 动作未完成(actionStatus为空),已尝试 {retry_count} 次,继续等待...") else: # 其他时候记录DEBUG级别日志,减少日志数量 logger.debug(f"任务块 {task_block_id} 动作未完成,已尝试 {retry_count} 次") # 等待0.5秒后继续尝试 await asyncio.sleep(actual_retry_interval) async def set_task_in_progress(task_id: str, token: str = None) -> Optional[ApiResponse]: """ 设置系统任务状态为执行中 Args: task_id: 系统任务ID token: 认证令牌 Returns: Optional[ApiResponse]: 接口响应,如果请求失败则返回None """ # 调用接口 url = f"{TFApiConfig.BASE_URL}{TFApiConfig.SET_TASK_IN_PROGRESS_PATH.format(id=task_id)}" # 构建请求头 headers = {} headers[TFApiConfig.TOKEN_HEADER] = token headers["x-tenant-id"] = "1000" try: logger.info(f"正在设置系统任务状态为执行中: {task_id}") async with aiohttp.ClientSession() as session: async with session.put( url, headers=headers, timeout=TFApiConfig.TIMEOUT ) as response: # 读取响应内容 response_text = await response.json() # 尝试解析JSON try: if response_text.get("success", False): logger.info(f"成功设置系统任务状态为执行中: {task_id}") else: logger.warning(f"设置系统任务状态为执行中失败: {response_text.get('message', '未知错误')}") return response_text except json.JSONDecodeError: logger.error(f"解析响应JSON失败: {response_text}") return None except Exception as e: logger.error(f"调用设置任务状态接口失败: {str(e)}") return None async def set_task_completed(task_id: str, token: str = None) -> Optional[ApiResponse]: """ 设置系统任务状态为已完成 Args: task_id: 系统任务ID token: 认证令牌 Returns: Optional[ApiResponse]: 接口响应,如果请求失败则返回None """ # 调用接口 url = f"{TFApiConfig.BASE_URL}{TFApiConfig.SET_TASK_COMPLETED_PATH.format(id=task_id)}" # 构建请求头 headers = {} headers[TFApiConfig.TOKEN_HEADER] = token headers["x-tenant-id"] = "1000" try: logger.info(f"正在设置系统任务状态为已完成: {task_id}") async with aiohttp.ClientSession() as session: async with session.put( url, headers=headers, timeout=TFApiConfig.TIMEOUT ) as response: # 读取响应内容 response_text = await response.json() # 尝试解析JSON try: if response_text.get("success", False): logger.info(f"成功设置系统任务状态为已完成: {task_id}") else: logger.warning(f"设置系统任务状态为已完成失败: {response_text.get('message', '未知错误')}") return response_text except json.JSONDecodeError: logger.error(f"解析响应JSON失败: {response_text}") return None except Exception as e: logger.error(f"调用设置系统任务状态为已完成接口失败: {str(e)}") return None async def set_task_terminated(task_id: str, token: str = None) -> Optional[ApiResponse]: """ 设置系统任务状态为已终止 Args: task_id: 系统任务ID token: 认证令牌 Returns: Optional[ApiResponse]: 接口响应,如果请求失败则返回None """ # 调用接口 url = f"{TFApiConfig.BASE_URL}{TFApiConfig.SET_TASK_TERMINATED_PATH.format(id=task_id)}" # 构建请求头 headers = {} headers[TFApiConfig.TOKEN_HEADER] = token headers["x-tenant-id"] = "1000" try: logger.info(f"正在设置系统任务状态为已终止: {task_id}") async with aiohttp.ClientSession() as session: async with session.put( url, headers=headers, timeout=TFApiConfig.TIMEOUT ) as response: # 读取响应内容 response_text = await response.json() # 尝试解析JSON try: if response_text.get("success", False): logger.info(f"成功设置系统任务状态为已终止: {task_id}") else: logger.warning(f"设置系统任务状态为已终止失败: {response_text.get('message', '未知错误')}") return response_text except json.JSONDecodeError: logger.error(f"解析响应JSON失败: {response_text}") return None except Exception as e: logger.error(f"调用设置任务状态接口失败: {str(e)}") return None async def set_task_failed(task_id: str, token: str = None) -> Optional[ApiResponse]: """ 设置系统任务状态为已失败 Args: task_id: 系统任务ID token: 认证令牌 Returns: Optional[ApiResponse]: 接口响应,如果请求失败则返回None """ # 调用接口 url = f"{TFApiConfig.BASE_URL}{TFApiConfig.SET_TASK_FAILED_PATH.format(id=task_id)}" # 构建请求头 headers = {} headers[TFApiConfig.TOKEN_HEADER] = token headers["x-tenant-id"] = "1000" try: logger.info(f"正在设置任务状态为已失败: {task_id}") async with aiohttp.ClientSession() as session: async with session.put( url, headers=headers, timeout=TFApiConfig.TIMEOUT ) as response: # 读取响应内容 response_text = await response.json() # 尝试解析JSON try: if response_text.get("success", False): logger.info(f"成功设置系统任务状态为已失败: {task_id}") else: logger.warning(f"设置系统任务状态为已失败失败: {response_text.get('message', '未知错误')}") return response_text except json.JSONDecodeError: logger.error(f"解析响应JSON失败: {response_text}") return None except Exception as e: logger.error(f"调用设置任务状态接口失败: {str(e)}") return None