""" 企微 JS-SDK 签名(给复制页写剪贴板)。 本文件职责:取 jsapi_ticket、按当前页 URL 签名。请求线程只打缓存/短 HTTP。 禁止:把 access_token / ticket 写进页面或日志;禁止在这里改工单。 """ from __future__ import annotations import hashlib import logging import secrets import time from typing import Any, Optional import httpx from agent.channel.outbox.sender import WeComAppClient from agent.config import get_settings from agent.redis_coord.keys import TOKEN_JSAPI logger = logging.getLogger(__name__) _TICKET_TTL = 3600 def build_jsapi_signature(*, url: str, kind: str = "corp") -> dict[str, Any]: """ 给前端 ww.register 用的签名。 kind=corp → get_jsapi_ticket;kind=agent → agent_config ticket。 URL 必须与浏览器地址栏一致(不含 hash)。 """ page = (url or "").strip() if not page: raise ValueError("url_empty") ticket = _jsapi_ticket(kind="agent" if kind == "agent" else "corp") if not ticket: raise RuntimeError("jsapi_ticket_missing") nonce = secrets.token_hex(8) timestamp = str(int(time.time())) raw = f"jsapi_ticket={ticket}&noncestr={nonce}×tamp={timestamp}&url={page}" signature = hashlib.sha1(raw.encode("utf-8")).hexdigest() return { "timestamp": int(timestamp), "nonceStr": nonce, "signature": signature, } def _jsapi_ticket(*, kind: str) -> str: cached = _cache_get(kind) if cached: return cached settings = get_settings() if not (settings.wecom_corp_id and settings.wecom_secret): return "" client = WeComAppClient( corp_id=settings.wecom_corp_id, secret=settings.wecom_secret, agent_id=int(settings.wecom_agent_id or "0") or 0, ) token = client.get_access_token() if kind == "agent": api = "https://qyapi.weixin.qq.com/cgi-bin/ticket/get" params = {"access_token": token, "type": "agent_config"} else: api = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket" params = {"access_token": token} with httpx.Client(timeout=8.0) as http: data = http.get(api, params=params).json() errcode = int(data.get("errcode") or 0) ticket = str(data.get("ticket") or "") if errcode != 0 or not ticket: logger.warning("jsapi ticket 失败 kind=%s errcode=%s", kind, errcode) return "" _cache_set(kind, ticket) return ticket def _cache_get(kind: str) -> Optional[str]: redis = _try_redis() if redis is None: return None try: val = redis.raw.get(redis.key(TOKEN_JSAPI, kind)) except Exception: # noqa: BLE001 return None if val is None: return None return val if isinstance(val, str) else val.decode("utf-8") def _cache_set(kind: str, ticket: str) -> None: redis = _try_redis() if redis is None: return try: redis.raw.set(redis.key(TOKEN_JSAPI, kind), ticket, ex=_TICKET_TTL) except Exception as exc: # noqa: BLE001 logger.warning("jsapi ticket 缓存失败:%s", exc) def _try_redis(): try: from agent.redis_coord import get_redis_runtime return get_redis_runtime(get_settings()).client except Exception: # noqa: BLE001 return None