103 lines
3.3 KiB
Python
103 lines
3.3 KiB
Python
"""
|
||
主账唤醒桥接路由(合并原 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}
|