"""ZYT 管理员身份服务适配器。 桌面软件的密码只发送给 ZYT;客服后台拿到管理员 token 后通过最小会话接口验证, 再签发仅能访问客服数据的短期 token。 """ from __future__ import annotations import json import os import socket import urllib.error import urllib.parse import urllib.request from typing import Any WECOM_RPA_TERMINAL = 7 DEFAULT_ZYT_API_BASE_URL = os.environ.get( "ZYT_API_BASE_URL", "http://127.0.0.1" ).strip().rstrip("/") class ZytAuthError(RuntimeError): pass def api_base_url() -> str: value = DEFAULT_ZYT_API_BASE_URL parsed = urllib.parse.urlparse(value) if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username: raise ZytAuthError("ZYT_API_BASE_URL 配置不正确") return value.rstrip("/") def verify_desktop_token( token: str, *, api_url: str | None = None, timeout: float = 8.0 ) -> dict[str, Any]: token = str(token or "").strip() if not token: raise ZytAuthError("缺少 ZYT 登录令牌") base_url = str(api_url or api_base_url()).strip().rstrip("/") parsed = urllib.parse.urlparse(base_url) if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username: raise ZytAuthError("ZYT 登录 API 域名配置不正确") last_error = "ZYT 登录已失效" # 第一条是新版专用接口;第二条兼容尚未发布桌面终端的线上旧版。 endpoints = ( ("/adminapi/desktop/session", False), ("/adminapi/auth.admin/mySelf", True), ) for path, legacy in endpoints: request = urllib.request.Request( f"{base_url}{path}", headers={ "Accept": "application/json", "User-Agent": "WeCom-RPA-Backend/1.0", # ZYT 管理端登录中间件读取 token 头,不是 Bearer。 "token": token, }, method="GET", ) try: with urllib.request.urlopen(request, timeout=timeout) as response: payload = json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as exc: last_error = f"ZYT 身份服务拒绝请求(HTTP {exc.code})" # 只兼容尚未部署的路由;鉴权拒绝不能降级为网页身份。 if not legacy and exc.code in {404, 405}: continue raise ZytAuthError(last_error) from exc except (urllib.error.URLError, TimeoutError, socket.timeout) as exc: raise ZytAuthError( f"无法连接 ZYT 身份服务:{getattr(exc, 'reason', exc)}" ) from exc except (ValueError, TypeError, UnicodeDecodeError) as exc: raise ZytAuthError("ZYT 身份服务返回了无效数据") from exc if not isinstance(payload, dict): raise ZytAuthError("ZYT 身份服务返回了无效数据") if str(payload.get("code")) != "1": last_error = str( payload.get("msg") if isinstance(payload, dict) else "" ) or "ZYT 登录已失效" if legacy and "ip" in last_error.lower() and "变化" in last_error: raise ZytAuthError( "ZYT 桌面认证接口尚未部署,旧版接口拒绝跨服务器验证。" "请管理员部署 /adminapi/desktop/session 后重新登录" ) raise ZytAuthError(last_error) data = payload.get("data") if not isinstance(data, dict): raise ZytAuthError("ZYT 身份信息不完整") if legacy: admin = data.get("user") if not isinstance(admin, dict) or not admin.get("id"): raise ZytAuthError("ZYT 管理员身份信息不完整") if int(admin.get("disable") or 0) != 0: raise ZytAuthError("ZYT 管理员账号已停用") permissions_value = data.get("permissions") permissions = ( sorted( { str(item).strip() for item in permissions_value if str(item).strip() } ) if isinstance(permissions_value, list) else [] ) role_ids_value = admin.get("role_ids") role_ids: list[int] = [] if isinstance(role_ids_value, list): for item in role_ids_value: try: role_ids.append(int(item)) except (TypeError, ValueError): continue profile_identity = { "user_id": f"admin:{admin['id']}", "sn": str(admin.get("account") or ""), "nickname": str(admin.get("name") or admin.get("account") or ""), "mobile": str(admin.get("phone") or ""), "avatar": str(admin.get("avatar") or ""), "terminal": WECOM_RPA_TERMINAL, "status": "active", "account_type": "admin", "root": bool(int(admin.get("root") or 0)), "role_ids": sorted(set(role_ids)), "permissions": permissions, "permissions_known": isinstance(permissions_value, list), } return profile_identity identity = dict(data) if not identity.get("user_id"): raise ZytAuthError("ZYT 身份信息不完整") if str(identity.get("terminal")) != str(WECOM_RPA_TERMINAL): raise ZytAuthError("当前令牌不是企业微信客服桌面端令牌") if str(identity.get("status") or "") != "active": raise ZytAuthError("ZYT 管理员账号已停用") permissions_value = identity.get("permissions") identity["permissions"] = ( sorted( { str(item).strip() for item in permissions_value if str(item).strip() } ) if isinstance(permissions_value, list) else [] ) identity["permissions_known"] = isinstance(permissions_value, list) identity["root"] = bool(int(identity.get("root") or 0)) # 新版专用接口完整返回权限;旧版缺权限时保留未知状态,患者功能 # 按最小权限关闭,不再跨 IP 请求网页 mySelf 补权限。 return identity raise ZytAuthError(last_error)