落地并存测服全端:智能体 Graph/路由、后台切 8180、主账与部署脚本齐备,便于线上重发与验收。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,18 +1,102 @@
|
||||
"""
|
||||
主账唤醒桥接路由(合并原 bridge_api)。
|
||||
|
||||
本文件职责:接收主账鉴权后的 wake 事件,入队由 Worker 结构化 resume。
|
||||
本文件职责:校验 callback token,接收结构化 wake,写入 Redis Stream 后快回。
|
||||
禁止:在本请求线程跑完整 Graph;禁止本包直连 MySQL / TMS。
|
||||
Worker:jobs.wake_consumer 认领后 GraphRuntime.wake_from_worker。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
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]:
|
||||
"""主账唤醒路径占位;正式实现需校验 AGENT_CALLBACK_TOKEN。"""
|
||||
return {"status": "ok", "channel": "bridge", "note": "scaffold-only"}
|
||||
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}
|
||||
|
||||
Reference in New Issue
Block a user