219 lines
7.5 KiB
Python
219 lines
7.5 KiB
Python
|
|
# =============================================================================
|
|||
|
|
# 企微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 客户端已关闭")
|