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
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
# =============================================================================
|
||||
# 企微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 客户端已关闭")
|
||||
Reference in New Issue
Block a user