""" FastAPI 应用入口(app 内主入口,符合架构计划) """ from contextlib import asynccontextmanager from pathlib import Path from app.core.logging import get_logger, setup_logging setup_logging() from app.core.config import settings from app.core.runtime_constants import asr_defaults, otel_defaults from app.core.telemetry import instrument_fastapi_app, setup_telemetry setup_telemetry( service_name=otel_defaults.service_name or "life-echo-api", ) from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from app.core.errors import register_exception_handlers from app.core.middleware import RequestIdMiddleware from app.core.openapi import custom_openapi from app.features.auth.router import router as auth_router from app.features.content.router import router as content_router from app.features.conversation.router import router as conversation_router from app.features.conversation.ws.router import websocket_endpoint from app.features.memoir.router import router as memoir_router from app.features.memory.router import router as memory_router from app.features.payment.router import router as payment_router from app.features.plan.router import router as plan_router from app.features.quota.router import router as quota_router from app.features.tasks.router import router as tasks_router from app.features.user.router import feedback_router as user_feedback_router from app.features.user.router import router as user_router # 聚合注册所有 feature 的 model 到 Base.metadata(供 Alembic 等使用) from app.features.auth import models as _auth_models # noqa: F401 from app.features.conversation import models as _conv_models # noqa: F401 from app.features.memory import models as _memory_models # noqa: F401 from app.features.memoir import models as _memoir_models # noqa: F401 from app.features.payment import models as _payment_models # noqa: F401 from app.features.story import models as _story_models # noqa: F401 from app.features.user import models as _user_models # noqa: F401 logger = get_logger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期:启动迁移/Redis/ASR/支付预初始化,关闭时释放连接。""" import asyncio from app.core.alembic_startup import run_alembic_upgrade_at_startup logger.info("Life Echo API 正在启动...") if settings.app_environment == "production" and not ( settings.api_cors_origins or "" ).strip(): logger.warning( "生产环境未配置 API_CORS_ORIGINS;浏览器跨域请求将无法携带 credentials" ) await asyncio.to_thread(run_alembic_upgrade_at_startup) try: from app.core.celery_broker_dev import maybe_purge_celery_broker_on_startup from app.core.redis import redis_service _redis = await redis_service.get_client() logger.info("Redis 连接已建立") await maybe_purge_celery_broker_on_startup(_redis) except Exception as e: logger.warning("Redis 连接失败(会话存储将不可用): {}", e) try: from app.core.dependencies import get_asr_provider provider = get_asr_provider() ensure_ready = getattr(provider, "ensure_ready", None) if callable(ensure_ready): asr_ready = await asyncio.to_thread(ensure_ready) else: asr_ready = True if asr_ready: logger.info( "ASR 服务已就绪(腾讯云一句话识别,引擎 {})", asr_defaults.engine_type, ) else: logger.warning("ASR 服务未就绪,语音转写将不可用") except Exception as e: logger.warning("ASR 初始化检查失败: {}", e) try: def _init_wechat_pay_client(): from app.features.payment.deps import get_payment_service svc = get_payment_service() if svc.is_method_available("wechat"): _ = svc.wechat_client await asyncio.to_thread(_init_wechat_pay_client) logger.info("微信支付客户端已预初始化") except Exception as e: logger.warning("微信支付预初始化失败(首次下单时再初始化): {}", e) yield logger.info("Life Echo API 正在关闭...") try: from app.core.telemetry import shutdown_telemetry shutdown_telemetry() except Exception as e: logger.warning("关闭 OpenTelemetry 失败: {}", e) try: from app.core.redis import redis_service await redis_service.close() logger.info("Redis 连接已关闭") except Exception as e: logger.warning("关闭 Redis 连接失败: {}", e) app = FastAPI( title="Life Echo API", version="1.0.0", docs_url="/docs" if settings.enable_docs else None, redoc_url="/redoc" if settings.enable_docs else None, openapi_url="/openapi.json" if settings.enable_docs else None, lifespan=lifespan, ) instrument_fastapi_app(app) # OpenAPI 全局增强 app.openapi = lambda: custom_openapi(app) # type: ignore[assignment] # Middleware(注册顺序:LIFO,先注册的后执行) app.add_middleware(RequestIdMiddleware) _origins = [ o.strip() for o in (settings.api_cors_origins or "").split(",") if o.strip() ] _allow_creds = bool(_origins) app.add_middleware( CORSMiddleware, allow_origins=_origins if _origins else ["*"], allow_credentials=_allow_creds, allow_methods=["*"], allow_headers=["*"], ) # 全局异常处理 register_exception_handlers(app) # ── Feature routers ────────────────────────────────────────── app.include_router(auth_router) app.websocket("/ws/conversation/{conversation_id}")(websocket_endpoint) app.include_router(conversation_router) app.include_router(memoir_router) app.include_router(memory_router) app.include_router(user_router) app.include_router(user_feedback_router) app.include_router(plan_router) app.include_router(payment_router) app.include_router(quota_router) app.include_router(tasks_router) app.include_router(content_router) # static 在 api/ 下,app/main.py 在 api/app/ 下 _static_dir = Path(__file__).resolve().parent.parent / "static" app.mount("/static", StaticFiles(directory=_static_dir), name="static") @app.get("/health", include_in_schema=False) async def health(): return {"status": "ok"}