让成交卡等报价文件真正发出后再出现,并收紧 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
@@ -0,0 +1,126 @@
"""
复制工单号 H5 的短命 token。
本文件职责:签发/校验 token → 工单号;URL 只带 token,禁止带 userid/工单号。
优先 Redis(HTTP 与 Worker 不同进程必须共享);单测无 Redis 时用内存。
禁止:当六态;禁止把工单号拼进公网路径。
"""
from __future__ import annotations
import logging
import secrets
import threading
import time
from typing import Optional
from agent.config import get_settings
from agent.redis_coord.keys import TOKEN_COPY_TICKET
logger = logging.getLogger(__name__)
# 销售可能隔几天才点卡;7 天内同一 token 可重复打开复制。
COPY_TTL_SECONDS = 7 * 24 * 60 * 60
_FORM_FALLBACK = "https://ai.ytd-scm.com/inquiry-form"
_MEM: dict[str, tuple[str, float]] = {}
_MEM_LOCK = threading.Lock()
def issue_copy_token(work_order_no: str, *, ttl_seconds: int = COPY_TTL_SECONDS) -> str:
"""
签发复制页 token。
副作用:写 Redis 或内存。工单号为空则返回空串,调用方不要出跳转按钮。
"""
wo = (work_order_no or "").strip()
if not wo:
return ""
token = secrets.token_urlsafe(24)
if _redis_set(token, wo, ttl_seconds):
return token
expires = time.time() + ttl_seconds
with _MEM_LOCK:
_MEM[token] = (wo, expires)
logger.info("copy token 走内存 wo=%s(HTTP/Worker 分进程时页面会找不到)", wo)
return token
def lookup_copy_token(token: str) -> Optional[str]:
"""校验 token 未过期;返回工单号或 None。不消费,可重复打开。"""
raw = (token or "").strip()
if not raw:
return None
wo = _redis_get(raw)
if wo:
return wo
now = time.time()
with _MEM_LOCK:
hit = _MEM.get(raw)
if not hit:
return None
wo, exp = hit
if exp < now:
_MEM.pop(raw, None)
return None
return wo
def build_copy_ticket_url(work_order_no: str) -> str:
"""
拼复制页绝对地址:{inquiry-form}/copy/{token}。
域名来自环境变量,禁止写死测试/正式切换。缺省回合同域名,保证企微能打开。
"""
token = issue_copy_token(work_order_no)
if not token:
return ""
return f"{_form_base()}/copy/{token}"
def _form_base() -> str:
settings = get_settings()
form = (settings.inquiry_form_public_base_url or "").strip().rstrip("/")
if form:
return form
pub = (settings.public_base_url or "").strip().rstrip("/")
if pub:
return f"{pub}/inquiry-form"
return _FORM_FALLBACK
def _redis_set(token: str, work_order_no: str, ttl_seconds: int) -> bool:
client = _try_redis()
if client is None:
return False
try:
client.raw.set(client.key(TOKEN_COPY_TICKET, token), work_order_no, ex=ttl_seconds)
return True
except Exception as exc: # noqa: BLE001
logger.warning("copy token Redis 写入失败:%s", exc)
return False
def _redis_get(token: str) -> Optional[str]:
client = _try_redis()
if client is None:
return None
try:
val = client.raw.get(client.key(TOKEN_COPY_TICKET, token))
except Exception as exc: # noqa: BLE001
logger.warning("copy token Redis 读取失败:%s", exc)
return None
if val is None:
return None
text = val if isinstance(val, str) else val.decode("utf-8")
return text.strip() or None
def _try_redis():
"""有 Redis 才用;单测 memory 后端也可用,保证同进程能读到。"""
try:
from agent.redis_coord import get_redis_runtime
return get_redis_runtime(get_settings()).client
except Exception: # noqa: BLE001
return None