""" H5 补问 / 附件上传 / 复制工单号通道。 本文件职责:挂载 /inquiry-form;用 token 绑定身份或工单号(URL 禁止 userid)。 禁止:从 URL 读 userid;禁止在本包改六态或直连 TMS;禁止同步 invoke Graph。 """ from __future__ import annotations import logging from typing import Any, Optional from fastapi import APIRouter, HTTPException, Query from fastapi.responses import HTMLResponse from pydantic import BaseModel, Field from agent.channel.clarification import validate_token from agent.channel.h5.copy_page import expired_copy_page, render_copy_page from agent.channel.h5.copy_store import lookup_copy_token from agent.handlers.card_action import handle_card_action logger = logging.getLogger(__name__) router = APIRouter(prefix="/inquiry-form", tags=["h5"]) class H5SubmitBody(BaseModel): """H5 提交体:业务字段放 fields;身份只认服务端 token 绑定。""" action: str = Field(default="submit_clarify") fields: dict[str, Any] = Field(default_factory=dict) @router.get("/ping") def h5_ping() -> dict[str, str]: """存活探测。""" return {"status": "ok", "channel": "h5"} @router.get("/copy/{token}", response_class=HTMLResponse) def h5_copy_ticket(token: str) -> HTMLResponse: """ 复制工单号页:token 换工单号,页面写入剪贴板。 请求线程只读 token,不进 Graph、不发企微消息。 """ wo = lookup_copy_token(token) if not wo: return HTMLResponse(expired_copy_page(), status_code=410) return HTMLResponse(render_copy_page(wo)) @router.get("/jsapi-signature") def h5_jsapi_signature( url: str = Query(default=""), kind: str = Query(default="corp"), ) -> dict[str, Any]: """ 给复制页 ww.register 签名。失败给空对象,页面改走浏览器剪贴板。 """ from agent.channel.wecom.jsapi import build_jsapi_signature try: return build_jsapi_signature(url=url, kind=kind) except Exception as exc: # noqa: BLE001 logger.warning("jsapi 签名失败 kind=%s err=%s", kind, exc) raise HTTPException(status_code=503, detail="jsapi_unavailable") from exc @router.get("/{token}") def h5_open(token: str) -> dict[str, Any]: """ 打开补问页:校验 token 未过期。 返回允许动作与 wait_version;不返回内部异常栈。 """ rec = validate_token(token) if rec is None: raise HTTPException(status_code=410, detail="表单已过期或无效,请回企微重新打开") return { "status": "ok", "thread_id": rec.thread_id, "wait_version": rec.wait_version, "allowed_actions": rec.allowed_actions, "note": "骨架:字段列表后续按合同生成", } @router.post("/{token}/submit") def h5_submit(token: str, body: H5SubmitBody) -> dict[str, Any]: """ 提交补问:消费 token,转入 card_action 骨架(不跑整图)。 副作用:token 一次性;入队/Handler 后续接 Worker。 """ rec = validate_token(token, action=body.action or "submit_clarify", consume=True) if rec is None: raise HTTPException(status_code=410, detail="表单已过期或无效,请回企微重新打开") handle_card_action( sender_id=rec.sender_id, thread_id=rec.thread_id, wait_version=rec.wait_version, action=body.action or "submit_clarify", payload=body.fields, ) logger.info( "h5.submit ok thread=%s wait=%s action=%s", rec.thread_id, rec.wait_version, body.action, ) return {"status": "accepted", "thread_id": rec.thread_id}