落地并存测服全端:智能体 Graph/路由、后台切 8180、主账与部署脚本齐备,便于线上重发与验收。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-13 21:15:58 +08:00
co-authored by Cursor
parent 0eb1adecc8
commit 8103b3b651
143 changed files with 8968 additions and 277 deletions
+88 -4
View File
@@ -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}