Files
wecom_it_smart_desk/backend/app/services/xylink_service.py
T
Simon bea288e414 feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (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
2026-07-11 23:13:10 +08:00

219 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# 企微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. 追加 &timestamp={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"&timestamp={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 客户端已关闭")