# ============================================================================= # 企微IT智能服务台 — ITSM 运维平台待办数据源 # ============================================================================= # 说明:ITSM 一站式运维平台 OpenAPI 数据源实现。 # 通过 ITSMSigner 进行 SHA1 签名认证,调用 ITSM OpenAPI 获取工单待办。 # # 当前状态: # - get_todo_list(): ITSM 列表 API 尚未提供,返回空列表 + 日志告警 # - get_todo_detail(): 已实现工单详情查询(调 workitem/detail API) # # 待 ITSM 列表 API 到位后,只需实现 get_todo_list() 内部逻辑即可。 # ============================================================================= import logging from typing import Any, Dict, List, Optional import httpx import redis.asyncio as aioredis from app.config import settings from app.services.todo_source_service import TodoSourceService from app.utils.itsm_signer import ITSMSigner logger = logging.getLogger(__name__) # ITSM OpenAPI 工单详情端点 ITSM_WORKITEM_DETAIL_PATH = "/openapi/v1/process/workitem/detail" # ITSM API 成功响应码 ITSM_SUCCESS_CODE = 20000 # ITSM HTTP 请求超时 ITSM_TIMEOUT = httpx.Timeout(timeout=30.0, connect=10.0, read=30.0) def _itsm_priority_to_todo(itsm_priority: Any) -> str: """将 ITSM 优先级映射到统一的 todo 优先级(urgent/high/normal)。 Args: itsm_priority: ITSM 返回的优先级字段(可能是字符串或数字) Returns: str: 统一优先级 urgent/high/normal """ if itsm_priority is None: return "normal" priority_str = str(itsm_priority).strip().lower() # 紧急 / urgent / 1 if priority_str in ("urgent", "紧急", "1", "critical", "p0", "p1"): return "urgent" # 高 / high / 2 if priority_str in ("high", "高", "2", "p2"): return "high" return "normal" def _itsm_status_to_todo(itsm_status: Any) -> str: """将 ITSM 状态映射到统一的 todo 状态(pending/processing/resolved)。 Args: itsm_status: ITSM 返回的状态字段 Returns: str: 统一状态 pending/processing/resolved """ if itsm_status is None: return "pending" status_str = str(itsm_status).strip().lower() # 已完成/已关闭类 if status_str in ("resolved", "closed", "done", "完成", "已关闭", "已完成", "resolved", "3"): return "resolved" # 处理中 if status_str in ("processing", "in_progress", "处理中", "2"): return "processing" # 默认待处理 return "pending" class ITSMService(TodoSourceService): """ITSM 运维平台待办数据源实现。 通过 ITSM OpenAPI 获取当前坐席的代办工单。 签名认证使用 ITSMSigner(SHA1 签名),请求头携带 appId/timestamp/sign。 Attributes: agent_userid: 当前坐席的企微 userid redis: Redis 异步客户端(预留,后续 SSO 认证可能需要) base_url: ITSM API 基址 app_id: ITSM OpenAPI app_id app_secret: ITSM OpenAPI app_secret """ def __init__(self, agent_userid: str, redis: aioredis.Redis): """初始化 ITSM 数据源服务。 从 settings 读取 ITSM 配置。如果 itsm_app_id 为空, 所有方法将返回空结果并记日志告警。 Args: agent_userid: 当前坐席的企微 userid redis: Redis 异步客户端实例 """ self.agent_userid = agent_userid self.redis = redis self.base_url = settings.itsm_base_url.rstrip("/") self.app_id = settings.itsm_app_id self.app_secret = settings.itsm_app_secret # ------------------------------------------------------------------ # 公开接口 # ------------------------------------------------------------------ async def get_todo_list(self) -> List[Dict[str, Any]]: """获取 ITSM 代办工单列表。 ⚠️ ITSM 列表 API 尚未提供,当前返回空列表 + 日志告警。 待 API 到位后实现列表查询逻辑。 Returns: List[Dict[str, Any]]: 空列表(API 待实现) """ if not self.app_id: logger.warning("ITSM app_id 未配置,代办列表返回空") return [] logger.warning( "ITSM 代办列表 API 尚未实现,返回空列表。" "待 ITSM 平台方提供列表 API 端点后补充实现。" ) return [] async def get_todo_detail(self, item_id: str) -> Optional[Dict[str, Any]]: """获取 ITSM 工单详情。 通过 ITSM OpenAPI workitem/detail 端点获取工单详情, 并映射为统一 TodoItemData 格式。 Args: item_id: 工单的 process_instance_id(不含 "ticket:" 前缀) Returns: Optional[Dict[str, Any]]: TodoItemData 格式的工单详情 """ if not self.app_id: logger.warning("ITSM app_id 未配置,无法查询工单详情") return None try: # 调用 ITSM workitem detail API detail = await self._get_workitem_detail(item_id, self.agent_userid) if not detail: return None return self._map_to_todo_item(detail) except Exception as e: logger.error( f"获取 ITSM 工单详情失败: item_id={item_id}, error={e}", exc_info=True, ) return None # ------------------------------------------------------------------ # 私有方法 # ------------------------------------------------------------------ async def _do_post(self, url: str, body: Dict[str, Any]) -> Optional[dict]: """发送带签名的 POST 请求到 ITSM API。 使用 ITSMSigner 生成签名请求头,发送 JSON POST 请求。 解析 ITSM 标准响应格式 {code: 20000, data: {...}, message: "..."}。 Args: url: 完整的 ITSM API URL body: 请求体(业务数据) Returns: Optional[dict]: ITSM 响应中的 data 字段,失败返回 None """ headers = ITSMSigner.get_headers(self.app_id, self.app_secret, body) async with httpx.AsyncClient(timeout=ITSM_TIMEOUT) as client: response = await client.post(url, json=body, headers=headers) result = response.json() code = result.get("code") if code != ITSM_SUCCESS_CODE: logger.error( f"ITSM API 调用失败: url={url}, code={code}, " f"message={result.get('message', '')}" ) return None return result.get("data") async def _get_workitem_detail( self, process_instance_id: str, executor: str ) -> Optional[dict]: """调用 ITSM 工单详情 API。 POST {base_url}/openapi/v1/process/workitem/detail Args: process_instance_id: 工单流程实例 ID executor: 当前执行人(坐席 userid) Returns: Optional[dict]: ITSM 返回的工单详情数据 """ url = f"{self.base_url}{ITSM_WORKITEM_DETAIL_PATH}" body = { "process_instance_id": process_instance_id, "executor": executor, } return await self._do_post(url, body) def _map_to_todo_item(self, detail: dict) -> Dict[str, Any]: """将 ITSM 工单详情映射为统一 TodoItemData 格式。 映射规则参考系统设计文档 §8.2: - id: "ticket:{process_instance_id}" - type: "ticket" - title: 工单标题 - priority: ITSM 优先级映射到 urgent/high/normal - status: ITSM 状态映射到 pending/processing/resolved Args: detail: ITSM API 返回的工单详情 Returns: Dict[str, Any]: TodoItemData 格式的待办事项 """ process_instance_id = detail.get("process_instance_id", "") title = detail.get("title", "") itsm_priority = detail.get("priority", "normal") itsm_status = detail.get("status", "pending") creator = detail.get("creator", "") executor = detail.get("executor", "") created_at = detail.get("created_at", "") updated_at = detail.get("updated_at", "") return { "id": f"ticket:{process_instance_id}", "type": "ticket", "title": title or "ITSM 工单", "priority": _itsm_priority_to_todo(itsm_priority), "description": { "process_instance_id": process_instance_id, "executor": executor, "status": itsm_status, "creator": creator, "itsm_priority": itsm_priority, "title": title, }, "status": _itsm_status_to_todo(itsm_status), "assigned_agent_id": self.agent_userid, "corp_id": "", "created_at": created_at, "updated_at": updated_at, }