Files
inquiry_robot/inquiry-agent/agent/channel/h5/copy_store.py
T

144 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
复制工单号 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 inquiry_form_base() -> str:
"""H5 公网根路径(含 /inquiry-form),不含 userid。"""
return _form_base()
def build_upload_url(token: str) -> str:
"""
拼附件上传页:{inquiry-form}/upload/{token}。
token 为空则返回空串,调用方不要出跳转按钮。
"""
raw = (token or "").strip()
if not raw:
return ""
return f"{_form_base()}/upload/{raw}"
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