- Added new modules for advisor compliance, KYC sessions, and script templates, enhancing the advisor agent's capabilities. - Implemented a comprehensive API structure under the `/api/advisor-agent` prefix, ensuring clear organization and access to new features. - Established database models and repositories for compliance rules and KYC sessions, facilitating robust data management. - Integrated exception handling and response models to improve error management and user feedback. - Updated settings to include new configurations for compliance and KYC features, ensuring flexibility and adaptability. This update significantly expands the advisor agent's functionality, providing essential tools for compliance and customer interaction while maintaining a structured API design.
192 lines
7.5 KiB
Python
192 lines
7.5 KiB
Python
"""FastAPI 入口:宿主 Wave 0(auth/chat/TraceMiddleware)+ 风控模块 router/中间件/lifespan。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import re
|
||
from contextlib import asynccontextmanager
|
||
|
||
from fastapi import FastAPI, Request
|
||
from fastapi.exceptions import RequestValidationError
|
||
from fastapi.responses import JSONResponse
|
||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||
|
||
from app.api import deps
|
||
from app.api.audit_middleware import audit_middleware
|
||
from app.api.auth import router as auth_router
|
||
from app.api.advisors import router as advisors_router
|
||
from app.api.chat import router as chat_router
|
||
from app.api.compliance import router as compliance_router
|
||
from app.api.customers import router as customers_router
|
||
from app.api.products import router as products_router
|
||
from app.api.risk import router as risk_router
|
||
from app.api.simulate import router as simulate_router
|
||
from app.api.staff import router as staff_router
|
||
from app.api.advisor_compliance import router as advisor_compliance_router
|
||
from app.api.advisor_script_templates import router as advisor_script_templates_router
|
||
from app.api.allocation import router as advisor_allocation_router
|
||
from app.api.analyst import router as analyst_router
|
||
from app.api.copy import router as advisor_copy_router
|
||
from app.api.dashboard import router as advisor_dashboard_router
|
||
from app.api.guard import router as advisor_guard_router
|
||
from app.api.kyc import router as advisor_kyc_router
|
||
from app.api.market import fund_router as advisor_market_fund_router
|
||
from app.api.market import router as advisor_market_alerts_router
|
||
from app.api.ready import build_ready_payload, router as ready_router
|
||
from app.api.visitor import router as visitor_router
|
||
from app.advisor_exceptions import AdvisorAppError
|
||
from app.config.settings import settings
|
||
from app.middleware.trace import TraceMiddleware
|
||
from app.repository.audit_repository import AuditRepository
|
||
from app.service.auth_service import jwt_ready
|
||
from app.service.risk import redis_gateway
|
||
from app.utils.db import dispose_engines
|
||
from app.utils.exceptions import AppError
|
||
from app.utils.exceptions import ApiError, AppError, PermissionDenied
|
||
from app.utils.response import error_body, fail, register_error_handlers, trace_headers
|
||
from app.utils.trace import (
|
||
bind_request_id,
|
||
new_trace,
|
||
reset_request_id,
|
||
reset_trace,
|
||
set_trace,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
_TRACE_ID_PATTERN = re.compile(r"^[A-Za-z0-9._-]{1,64}$")
|
||
|
||
# FastAPI 已注册 handler 的异常须原样冒泡;BaseHTTPMiddleware 对未捕获异常会 re-raise。
|
||
_HANDLED_APP_EXCEPTIONS = (
|
||
ApiError,
|
||
AppError,
|
||
PermissionDenied,
|
||
RequestValidationError,
|
||
StarletteHTTPException,
|
||
)
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(_: FastAPI):
|
||
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}")
|
||
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)"
|
||
)
|
||
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.add_middleware(TraceMiddleware)
|
||
app.include_router(auth_router)
|
||
app.include_router(customers_router)
|
||
app.include_router(products_router)
|
||
app.include_router(advisors_router)
|
||
app.include_router(staff_router)
|
||
app.include_router(compliance_router)
|
||
app.include_router(chat_router)
|
||
app.include_router(visitor_router)
|
||
app.include_router(risk_router)
|
||
app.include_router(simulate_router)
|
||
app.include_router(analyst_router)
|
||
app.include_router(advisor_compliance_router)
|
||
app.include_router(advisor_script_templates_router)
|
||
app.include_router(advisor_kyc_router)
|
||
app.include_router(advisor_market_alerts_router)
|
||
app.include_router(advisor_market_fund_router)
|
||
app.include_router(advisor_copy_router)
|
||
app.include_router(advisor_guard_router)
|
||
app.include_router(advisor_dashboard_router)
|
||
app.include_router(advisor_allocation_router)
|
||
app.include_router(ready_router)
|
||
|
||
|
||
@app.get("/api/ready", tags=["platform"], include_in_schema=True)
|
||
def api_ready_probe(request: Request):
|
||
"""与 ready_router 同逻辑;在 app 上再挂一份,避免旧进程/路由表遗漏时 404。"""
|
||
trace_id = getattr(request.state, "trace_id", "unknown")
|
||
return build_ready_payload(app, trace_id)
|
||
|
||
|
||
@app.middleware("http")
|
||
async def audit_middleware_entry(request: Request, call_next):
|
||
return await audit_middleware(request, call_next)
|
||
|
||
|
||
@app.middleware("http")
|
||
async def trace_middleware(request: Request, call_next):
|
||
incoming = request.headers.get("X-Trace-Id", "")
|
||
trace_id = incoming if _TRACE_ID_PATTERN.fullmatch(incoming) else new_trace()
|
||
request.state.trace_id = trace_id
|
||
token = set_trace(trace_id)
|
||
request_id, rid_token = bind_request_id(request.headers.get("X-Request-Id", ""))
|
||
request.state.request_id = request_id
|
||
try:
|
||
try:
|
||
response = await call_next(request)
|
||
except _HANDLED_APP_EXCEPTIONS:
|
||
raise
|
||
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"),
|
||
headers=trace_headers(request),
|
||
)
|
||
finally:
|
||
reset_trace(token)
|
||
reset_request_id(rid_token)
|
||
response.headers["X-Trace-Id"] = trace_id
|
||
response.headers["X-Request-Id"] = request_id
|
||
return response
|
||
|
||
|
||
@app.get("/health")
|
||
def health(request: Request, probe: bool = False):
|
||
trace_id = getattr(request.state, "trace_id", "unknown")
|
||
if probe:
|
||
return build_ready_payload(app, trace_id)
|
||
return {"status": "ok", "env": settings.app_env, "trace_id": trace_id}
|
||
|
||
|
||
@app.exception_handler(AdvisorAppError)
|
||
async def advisor_app_error_handler(request: Request, exc: AdvisorAppError):
|
||
trace_id = getattr(request.state, "trace_id", "unknown")
|
||
body = fail(int(exc.status_code), exc.message, trace_id, data={"error_code": exc.code, **(exc.data or {})})
|
||
return JSONResponse(status_code=exc.status_code, content=body.model_dump())
|
||
|
||
|
||
@app.exception_handler(AppError)
|
||
async def app_error_handler(request: Request, exc: AppError):
|
||
trace_id = getattr(request.state, "trace_id", "unknown")
|
||
if exc.audit_event:
|
||
agent_type = request.headers.get("X-Agent-Type", "platform")
|
||
try:
|
||
AuditRepository().insert(
|
||
trace_id=trace_id,
|
||
event_type=exc.audit_event,
|
||
agent_type=agent_type if agent_type in ("customer", "advisor", "analyst", "risk") else "platform",
|
||
actor_id="anonymous",
|
||
decision=exc.error_code,
|
||
input_summary={"message": exc.message},
|
||
)
|
||
except Exception:
|
||
pass
|
||
body = fail(exc.code, exc.message, trace_id, data={"error_code": exc.error_code})
|
||
return JSONResponse(status_code=exc.http_status, content=body.model_dump())
|