让成交卡等报价文件真正发出后再出现,并收紧 PDF 顶部留白,避免抬头被裁或空一截。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-15 10:36:55 +08:00
co-authored by Cursor
parent cb3bf5238b
commit a7a8487eb3
22 changed files with 1630 additions and 126 deletions
+110
View File
@@ -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}&timestamp={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