111 lines
3.3 KiB
Python
111 lines
3.3 KiB
Python
"""
|
||
企微 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
|