# ============================================================================= # 企微IT智能服务台 — 小鱼易联终端管理服务 # ============================================================================= # 说明:封装小鱼易联开放平台API,提供: # 1. API签名生成(支持签名1.0和2.0) # 2. 终端状态查询 # 3. 推送自定义页面URL到终端(方案B预留) # 4. 终端列表查询 # # 参考:小鱼易联开放平台 https://openapi.xylink.com/ # ============================================================================= import hashlib import hmac import logging from typing import Any, Dict, List, Optional import httpx logger = logging.getLogger(__name__) class XyLinkService: """小鱼易联终端管理服务。 封装小鱼易联开放平台API调用,包括终端状态查询和URL推送。 P0阶段终端通过浏览器直接访问URL(方案A),此服务为方案B预留。 """ def __init__( self, enterprise_id: str, client_id: str, client_secret: str, api_base: str = "https://sdk.xylink.com/api/rest/external/v1/", ext_id: str = "", ) -> None: """初始化小鱼易联API客户端。 Args: enterprise_id: 小鱼易联企业ID client_id: SDK客户端ID client_secret: SDK客户端密钥 api_base: API基础URL ext_id: 企业ID(扩展ID) """ self.enterprise_id = enterprise_id self.client_id = client_id self.client_secret = client_secret self.api_base = api_base.rstrip("/") + "/" self.ext_id = ext_id # 创建httpx异步客户端 self.client = httpx.AsyncClient( timeout=httpx.Timeout(connect=5.0, read=15.0, write=10.0, pool=5.0) ) # ========================================================================== # 签名生成 # ========================================================================== def _generate_signature(self, params: Dict[str, Any], timestamp: str) -> str: """生成API签名(签名2.0 — HMAC-SHA256)。 小鱼易联API签名算法: 1. 将参数按key排序 2. 拼接为 key1=value1&key2=value2... 格式 3. 追加 ×tamp={timestamp} 4. 使用 client_secret 作为密钥,HMAC-SHA256 计算签名 Args: params: 请求参数 timestamp: 时间戳(毫秒) Returns: str: 签名字符串(hex) """ # 按key排序并拼接 sorted_keys = sorted(params.keys()) sign_str = "&".join(f"{k}={params[k]}" for k in sorted_keys) sign_str += f"×tamp={timestamp}" # HMAC-SHA256 签名 signature = hmac.new( self.client_secret.encode("utf-8"), sign_str.encode("utf-8"), hashlib.sha256, ).hexdigest() return signature def _build_headers(self, params: Dict[str, Any]) -> Dict[str, str]: """构建请求头(含签名认证)。 Args: params: 请求参数 Returns: Dict[str, str]: 请求头字典 """ import time timestamp = str(int(time.time() * 1000)) signature = self._generate_signature(params, timestamp) return { "Content-Type": "application/json", "xy-EnterpriseId": self.enterprise_id, "xy-ClientId": self.client_id, "xy-Timestamp": timestamp, "xy-Signature": signature, } # ========================================================================== # 终端管理 API # ========================================================================== async def get_terminal_status(self, sn: str) -> Dict[str, Any]: """查询终端在线状态。 Args: sn: 终端序列号 Returns: Dict[str, Any]: 终端状态信息(含 online/status 等字段) Raises: Exception: 查询失败 """ url = f"{self.api_base}terminal/{sn}/status" params = {"sn": sn} headers = self._build_headers(params) try: response = await self.client.get(url, params=params, headers=headers) result = response.json() if response.status_code != 200: logger.error(f"查询终端状态失败: sn={sn}, status={response.status_code}") raise Exception(f"小鱼易联API错误: HTTP {response.status_code}") logger.info(f"查询终端状态成功: sn={sn}") return result except httpx.HTTPError as e: logger.error(f"查询终端状态网络错误: sn={sn}, error={e}") raise Exception(f"小鱼易联API网络错误: {e}") from e async def push_url_to_terminal(self, sn: str, url: str) -> Dict[str, Any]: """推送自定义页面URL到终端(方案B预留)。 通过小鱼易联API将指定URL推送到终端设备, 终端收到后自动打开该URL(用于远程推送会议室状态页面)。 Args: sn: 终端序列号 url: 要推送的页面URL Returns: Dict[str, Any]: 推送结果 Raises: Exception: 推送失败 """ api_url = f"{self.api_base}terminal/{sn}/push-url" params = {"sn": sn, "url": url} headers = self._build_headers(params) try: response = await self.client.post(api_url, json=params, headers=headers) result = response.json() if response.status_code != 200: logger.error(f"推送URL到终端失败: sn={sn}, status={response.status_code}") raise Exception(f"小鱼易联API错误: HTTP {response.status_code}") logger.info(f"推送URL到终端成功: sn={sn}, url={url}") return result except httpx.HTTPError as e: logger.error(f"推送URL到终端网络错误: sn={sn}, error={e}") raise Exception(f"小鱼易联API网络错误: {e}") from e async def list_terminals(self) -> List[Dict[str, Any]]: """查询终端列表。 Returns: List[Dict[str, Any]]: 终端列表,每项含 sn/name/status 等 Raises: Exception: 查询失败 """ url = f"{self.api_base}terminals" params: Dict[str, Any] = {} headers = self._build_headers(params) try: response = await self.client.get(url, params=params, headers=headers) result = response.json() if response.status_code != 200: logger.error(f"查询终端列表失败: status={response.status_code}") raise Exception(f"小鱼易联API错误: HTTP {response.status_code}") terminals = result.get("terminals", []) logger.info(f"查询终端列表成功: count={len(terminals)}") return terminals except httpx.HTTPError as e: logger.error(f"查询终端列表网络错误: {e}") raise Exception(f"小鱼易联API网络错误: {e}") from e # ========================================================================== # 资源清理 # ========================================================================== async def close(self) -> None: """关闭 HTTP 客户端连接池。""" await self.client.aclose() logger.info("XyLinkService HTTP 客户端已关闭")