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
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 客户端已关闭")
|