让成交卡等报价文件真正发出后再出现,并收紧 PDF 顶部留白,避免抬头被裁或空一截。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
企微 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
|
||||
Reference in New Issue
Block a user