bea288e414
== 已部署上线 (9项) == - 代办事项真实数据源集成 (企微审批API 8bug修复链) - H5/坐席端 Logo样式统一+绿色背景 - 视频引导页修复 (localStorage key v2) - 坐席端 v9 Vue版本修复 (ElMessage._context) - 截图按钮 v10 修复 (getDisplayMedia user gesture) - 扫码样式恢复+H5扫码登录跳转修复 - H5截图快捷键提示 == 代码完成待部署 (3项) == - 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查) - 会议室预定-小鱼易联终端 (40文件, 40/40测试通过) - IT资产升级审批推送 (asset_service.py) == 需求文档 (2项) == - 坐席端AI辅助消息框-PRD (4项新功能确认) - 坐席端布局优化建议 v2.0 (7天计划) == 新增文档 == - 日报-2026-07-11.md - 知识迭代Bug修复报告-20260711.md - 会议室预定-部署指南.md - CHANGELOG.md 更新 == 测试 == - test_todo_integration.py: 40/40 - test_meetingroom.py: 40/40 - test_bugfix_ki_suggestions.py: 21/21
261 lines
9.0 KiB
Python
261 lines
9.0 KiB
Python
# =============================================================================
|
||
# 企微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,
|
||
}
|