# -*- coding: utf-8 -*- """FastAPI 应用工厂。 设计(strangler 迁移): - 已迁移的路由挂在 `/api/v1` 前缀下,与遗留 Flask 的同名路由(根路径)并存, 便于逐端点比对响应契约(.zcode/refactor/parity.py)。 - `serve_flask_fallback=True` 时,把未迁移的遗留 Flask 应用整体挂到根路径兜底, 实现「新端点走 FastAPI、其余走 Flask」的单进程双框架运行;迁移完成后删掉该开关。 注意(默认关闭的原因):Flask fallback 会在 FastAPI 进程内 import main, 进而初始化 MusicAPIService / 打开 SQLite —— 与生产既有的 waitress 进程是 两套独立进程语义。生产切换前需按计划把状态与任务模型统一(见迁移计划阶段 C), 当前仅用于开发期联调。 """ from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from core.config import APP_VERSION from core.security import init_secret_key from web.errors import install_error_handlers from web.middleware import ResponseHygieneMiddleware from web.routers import account as account_router from web.routers import admin as admin_router from web.routers import auth as auth_router from web.routers import auth_status as auth_status_router from web.routers import delivery as delivery_router from web.routers import index as index_router from web.routers import music as music_router from web.routers import podcast as podcast_router from web.routers import site as site_router from web.routers import stream as stream_router from web.routers import system as system_router from web.routers import tasks as tasks_router from web.routers import user as user_router @asynccontextmanager async def _lifespan(app: FastAPI): """应用生命周期:关闭时释放 aiohttp 连接池(含按事件循环隔离的 session)。""" yield try: from core import aiohttp_compat, http_client await http_client.client.close() await aiohttp_compat.close() except Exception: pass def _init_audio_secret() -> None: """注入音频签名密钥(与 Flask 同源,令两栈 token 互认)。""" try: import auth_system init_secret_key(auth_system.get_secret_key()) except Exception: pass def create_app(serve_flask_fallback: bool = False) -> FastAPI: """创建 FastAPI 应用。""" _init_audio_secret() app = FastAPI( title='云村小助手API服务', version=APP_VERSION, docs_url='/api/v1/docs', redoc_url=None, openapi_url='/api/v1/openapi.json', lifespan=_lifespan, ) # 响应侧统一处理:安全头 + 音频直链签名保护 + 版本指纹抹除 app.add_middleware(ResponseHygieneMiddleware) # 静态资源(/static/site_auth.js 等)——与 Flask static_folder 同目录 _static_dir = Path(__file__).resolve().parent.parent / 'static' if _static_dir.is_dir(): app.mount('/static', StaticFiles(directory=str(_static_dir)), name='static') # 已迁移路由:统一挂 /api/v1(迁移期与 Flask 根路径并存,便于对比) app.include_router(system_router.router, prefix='/api/v1', tags=['system']) app.include_router(site_router.router, prefix='/api/v1', tags=['site']) app.include_router(music_router.router, prefix='/api/v1', tags=['music']) app.include_router(account_router.router, prefix='/api/v1', tags=['account']) app.include_router(podcast_router.router, prefix='/api/v1', tags=['podcast']) app.include_router(tasks_router.router, prefix='/api/v1', tags=['tasks']) app.include_router(delivery_router.router, prefix='/api/v1', tags=['delivery']) app.include_router(stream_router.router, prefix='/api/v1', tags=['stream']) app.include_router(auth_status_router.router, prefix='/api/v1', tags=['auth']) app.include_router(auth_router.router, prefix='/api/v1', tags=['auth']) app.include_router(user_router.router, prefix='/api/v1', tags=['user']) app.include_router(admin_router.router, prefix='/api/v1', tags=['admin']) # 首页与静态资源(迁移完成形态;fallback 模式下同路径也能由 Flask 提供) app.include_router(index_router.router, tags=['index']) install_error_handlers(app) if serve_flask_fallback: _mount_flask_fallback(app) return app def _mount_flask_fallback(app: FastAPI) -> None: """把遗留 Flask WSGI 应用挂到根路径,兜底未迁移的路由。""" from starlette.middleware.wsgi import WSGIMiddleware import main as legacy_flask app.mount('/', WSGIMiddleware(legacy_flask.app))