2026-09-07 02:23:33 +08:00
|
|
|
|
"""FastAPI 入口(B7 集成 · T-01/T-02 演进):路由挂载、trace 中间件、审计中间件、lifespan、统一错误体。
|
2026-09-05 17:09:21 +08:00
|
|
|
|
|
2026-09-06 20:48:50 +08:00
|
|
|
|
lifespan(B7 挂账⑤⑥):
|
2026-09-07 02:23:33 +08:00
|
|
|
|
- 启动期校验:非 development 环境须 JWT 就绪(RS256 公钥已配置;
|
|
|
|
|
|
T-01 前的 debug 工厂在场同样拒绝——AUTH_FACTORY_IS_DEBUG 双保险);
|
|
|
|
|
|
- Redis 网关单例注册(惰性连接,publish/DEL/EXISTS 失败降级不阻塞业务);
|
2026-09-06 20:48:50 +08:00
|
|
|
|
- shutdown 统一 dispose 数据库引擎(utils/db 工厂,B6 复审 P3 泄漏收口)。
|
|
|
|
|
|
trace:X-Trace-Id 透传/生成 + 响应头回写(trace.py 约束:call_next 前 set,
|
|
|
|
|
|
同步路由线程池由 anyio 传播,一致性 B8 断言兜底)。
|
2026-09-07 02:23:33 +08:00
|
|
|
|
audit(T-02):平台访问审计(http_access)+ input_guard_log 双写见
|
|
|
|
|
|
audit_middleware;X-Request-Id 独立生成(B7 复审 P3-4)。
|
2026-09-06 20:48:50 +08:00
|
|
|
|
"""
|
2026-09-05 17:09:21 +08:00
|
|
|
|
|
2026-09-06 20:48:50 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-09-07 02:32:03 +08:00
|
|
|
|
import logging
|
2026-09-06 20:48:50 +08:00
|
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import FastAPI, Request
|
2026-09-07 02:32:03 +08:00
|
|
|
|
from fastapi.responses import JSONResponse
|
2026-09-06 20:48:50 +08:00
|
|
|
|
|
|
|
|
|
|
from app.api import deps
|
2026-09-07 02:32:03 +08:00
|
|
|
|
from app.api.audit_middleware import audit_middleware
|
2026-09-07 02:41:42 +08:00
|
|
|
|
from app.api.chat import router as chat_router
|
2026-09-06 20:48:50 +08:00
|
|
|
|
from app.api.risk import router as risk_router
|
|
|
|
|
|
from app.api.simulate import router as simulate_router
|
2026-09-05 17:09:21 +08:00
|
|
|
|
from app.config.settings import settings
|
2026-09-07 02:23:33 +08:00
|
|
|
|
from app.service.auth_service import jwt_ready
|
2026-09-06 20:48:50 +08:00
|
|
|
|
from app.service.risk import redis_gateway
|
|
|
|
|
|
from app.utils.db import dispose_engines
|
2026-09-07 02:32:03 +08:00
|
|
|
|
from app.utils.response import error_body, register_error_handlers
|
|
|
|
|
|
from app.utils.trace import (
|
2026-09-10 18:00:20 +08:00
|
|
|
|
HEADER_ID_PATTERN,
|
2026-09-07 02:32:03 +08:00
|
|
|
|
bind_request_id,
|
|
|
|
|
|
new_trace,
|
|
|
|
|
|
reset_request_id,
|
|
|
|
|
|
reset_trace,
|
|
|
|
|
|
set_trace,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
2026-09-05 17:09:21 +08:00
|
|
|
|
|
2026-09-06 20:48:50 +08:00
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
|
|
async def lifespan(_: FastAPI):
|
2026-09-07 02:23:33 +08:00
|
|
|
|
if settings.app_env != "development":
|
|
|
|
|
|
if deps.AUTH_FACTORY_IS_DEBUG:
|
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
|
"debug auth factory is wired but app_env is not 'development'; "
|
|
|
|
|
|
"deploy T-01 JWT auth first or set app_env=development"
|
|
|
|
|
|
)
|
|
|
|
|
|
reason = jwt_ready()
|
|
|
|
|
|
if reason:
|
|
|
|
|
|
raise RuntimeError(f"JWT auth not ready for non-development env: {reason}")
|
2026-09-07 03:03:12 +08:00
|
|
|
|
reason = jwt_ready() if settings.app_env != "development" else None
|
|
|
|
|
|
if reason:
|
|
|
|
|
|
raise RuntimeError(f"JWT auth not ready for non-development env: {reason}")
|
|
|
|
|
|
if settings.app_env == "development" and settings.jwt_dev_secret == "change-me-in-dev-only":
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
"JWT_DEV_SECRET is the public default; issued dev tokens are forgeable "
|
|
|
|
|
|
"(demo/CI only, never expose to untrusted networks)"
|
|
|
|
|
|
)
|
2026-09-09 18:10:03 +08:00
|
|
|
|
if not settings.deepseek_api_key:
|
|
|
|
|
|
from app.service.agent_service import _DEGRADED_PREFIX
|
|
|
|
|
|
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
"DEEPSEEK_API_KEY 未配置,对话将走降级回复(前缀 %s),LLM 能力不可用",
|
|
|
|
|
|
_DEGRADED_PREFIX,
|
|
|
|
|
|
)
|
2026-09-06 20:48:50 +08:00
|
|
|
|
redis_gateway.set_gateway(redis_gateway.RedisGateway())
|
|
|
|
|
|
try:
|
|
|
|
|
|
yield
|
|
|
|
|
|
finally:
|
|
|
|
|
|
redis_gateway.set_gateway(None)
|
|
|
|
|
|
dispose_engines()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app = FastAPI(title="JinRong Agent Platform", version="0.2.0", lifespan=lifespan)
|
|
|
|
|
|
register_error_handlers(app)
|
|
|
|
|
|
app.include_router(risk_router)
|
|
|
|
|
|
app.include_router(simulate_router)
|
2026-09-07 02:41:42 +08:00
|
|
|
|
app.include_router(chat_router) # T-06 对话最小闭环
|
2026-09-06 20:48:50 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-09-07 02:32:03 +08:00
|
|
|
|
@app.middleware("http")
|
|
|
|
|
|
async def audit_middleware_entry(request: Request, call_next):
|
|
|
|
|
|
"""T-02 访问审计:先注册(执行序在 trace 之内,trace_id/request_id 已绑定)。"""
|
|
|
|
|
|
return await audit_middleware(request, call_next)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-06 20:48:50 +08:00
|
|
|
|
@app.middleware("http")
|
|
|
|
|
|
async def trace_middleware(request: Request, call_next):
|
2026-09-07 02:32:03 +08:00
|
|
|
|
"""trace_id/request_id 贯通 + 未捕获异常兜底(B7 复审 P2-2)。
|
|
|
|
|
|
|
|
|
|
|
|
异常发生在本中间件之内时,Starlette 的 ServerErrorMiddleware(栈外层)
|
|
|
|
|
|
生成的 500 响应不经过用户中间件——trace 头丢失的根因;此处 catch 后
|
|
|
|
|
|
直接产出统一错误体,保证 500 也带 X-Trace-Id/X-Request-Id。
|
|
|
|
|
|
"""
|
2026-09-06 20:48:50 +08:00
|
|
|
|
incoming = request.headers.get("X-Trace-Id", "")
|
2026-09-10 18:00:20 +08:00
|
|
|
|
trace_id = incoming if HEADER_ID_PATTERN.fullmatch(incoming) else new_trace()
|
2026-09-06 20:48:50 +08:00
|
|
|
|
token = set_trace(trace_id)
|
2026-09-07 02:32:03 +08:00
|
|
|
|
request_id, rid_token = bind_request_id(request.headers.get("X-Request-Id", ""))
|
2026-09-06 20:48:50 +08:00
|
|
|
|
try:
|
2026-09-07 02:32:03 +08:00
|
|
|
|
try:
|
|
|
|
|
|
response = await call_next(request)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
logger.exception("unhandled error on %s %s", request.method, request.url.path)
|
|
|
|
|
|
response = JSONResponse(
|
|
|
|
|
|
status_code=500, content=error_body("INTERNAL_ERROR", "internal server error")
|
|
|
|
|
|
)
|
2026-09-06 20:48:50 +08:00
|
|
|
|
finally:
|
|
|
|
|
|
reset_trace(token)
|
2026-09-07 02:32:03 +08:00
|
|
|
|
reset_request_id(rid_token)
|
2026-09-06 20:48:50 +08:00
|
|
|
|
response.headers["X-Trace-Id"] = trace_id
|
2026-09-07 02:32:03 +08:00
|
|
|
|
response.headers["X-Request-Id"] = request_id
|
2026-09-06 20:48:50 +08:00
|
|
|
|
return response
|
2026-09-05 17:09:21 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/health")
|
|
|
|
|
|
def health():
|
|
|
|
|
|
return {"status": "ok", "env": settings.app_env}
|