""" 主账唤醒桥接路由(合并原 bridge_api)。 本文件职责:校验 callback token,接收结构化 wake,写入 Redis Stream 后快回。 禁止:在本请求线程跑完整 Graph;禁止本包直连 MySQL / TMS。 Worker:jobs.wake_consumer 认领后 GraphRuntime.wake_from_worker。 """ from __future__ import annotations import logging from typing import Any, Optional from fastapi import APIRouter, Header, HTTPException from pydantic import BaseModel, Field from agent.config import get_settings from agent.graph.wake import WorkerWakePayload from agent.jobs.wake_consumer import enqueue_wake from agent.redis_coord import get_redis_runtime logger = logging.getLogger(__name__) router = APIRouter(prefix="/internal/bridge", tags=["bridge"]) class BridgeWakeBody(BaseModel): """主账 → 智能体结构化唤醒。""" thread_id: str inquiry_no: str quote_version: int = 0 wait_version: int = 0 kind: str = Field(description="如 recognition_done / pdf_done / ledger_transition") detail: dict[str, Any] = Field(default_factory=dict) def _expected_token() -> str: s = get_settings() return (s.agent_callback_token or s.inquiry_agent_callback_token or "").strip() def _check_token(authorization: Optional[str], x_agent_token: Optional[str]) -> None: expected = _expected_token() if not expected: if (get_settings().ytd_env or "").lower() == "prod": raise HTTPException(status_code=503, detail="callback token 未配置") logger.warning("bridge:未配置 callback token,test 允许空校验(仅骨架)") return provided = (x_agent_token or "").strip() if not provided and authorization: auth = authorization.strip() if auth.lower().startswith("bearer "): provided = auth[7:].strip() else: provided = auth if provided != expected: raise HTTPException(status_code=401, detail="unauthorized") @router.get("/ping") def bridge_ping() -> dict[str, str]: return {"status": "ok", "channel": "bridge"} @router.post("/wake") def bridge_wake( body: BridgeWakeBody, authorization: Optional[str] = Header(default=None), x_agent_callback_token: Optional[str] = Header( default=None, alias="X-Agent-Callback-Token" ), ) -> dict[str, Any]: """ 接收结构化 wake:校验后写入 Redis Stream,立即返回。 不在此 invoke Graph。 """ _check_token(authorization, x_agent_callback_token) try: payload = WorkerWakePayload( thread_id=body.thread_id, inquiry_no=body.inquiry_no, quote_version=body.quote_version, wait_version=body.wait_version, kind=body.kind, detail=body.detail, ) entry_id = enqueue_wake(get_redis_runtime().client, payload) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc except Exception as exc: # noqa: BLE001 logger.exception("bridge.wake 入队失败") raise HTTPException(status_code=503, detail=f"enqueue_failed:{exc}") from exc logger.info( "bridge.wake 已入队 Redis thread=%s kind=%s id=%s", body.thread_id, body.kind, entry_id, ) return {"status": "accepted", "entry_id": entry_id}