Archived
- web/app.py:create_app(api_prefix=) 参数化前缀——'' 即根路径模式, 87 条路由直接服务前端使用的路径,无需 Flask 兜底 - web/main.py:WEB_MODE=dual(默认,迁移期)/ fastapi(纯 FastAPI)切换 - 修真实路径缺陷:任务类端点漏了 /api 段(Flask 为 /api/topen 等, 原写成 /topen),根路径模式下前端会 404。已改为 /api/topen、/api/queue、 /api/batch_progress、/api/task/*;fe_error 同步修正 - 奇偶校验路径映射同步更新 验证: - tests/test_root_mode.py 新增 66 项:前端调用的每条路径在纯 FastAPI 模式下 均非 404;root 模式不再暴露 /api/v1 - 纯 FastAPI 模式真实启动(uvicorn, PORT=5002)实测: /health、/、/static 200,/song、/api/task/list 401(鉴权生效) - 全套 pytest 135/135;ruff All checks passed;pyright 0 errors;奇偶 36/36
128 lines
5.4 KiB
Python
128 lines
5.4 KiB
Python
# -*- 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 admin_site as admin_site_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 download as download_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 register as register_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, api_prefix: str = '/api/v1') -> FastAPI:
|
||
"""创建 FastAPI 应用。
|
||
|
||
api_prefix:已迁移路由的挂载前缀。
|
||
- '/api/v1'(默认):与 Flask 根路径并存,便于逐端点契约比对(迁移期)
|
||
- ''(切换完成):FastAPI 直接服务前端使用的根路径,不再需要 Flask 兜底
|
||
serve_flask_fallback:把 Flask 应用挂到根路径兜底(仅 api_prefix 非空时可用)。
|
||
"""
|
||
_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_prefix, tags=['system'])
|
||
app.include_router(site_router.router, prefix=api_prefix, tags=['site'])
|
||
app.include_router(music_router.router, prefix=api_prefix, tags=['music'])
|
||
app.include_router(account_router.router, prefix=api_prefix, tags=['account'])
|
||
app.include_router(podcast_router.router, prefix=api_prefix, tags=['podcast'])
|
||
app.include_router(tasks_router.router, prefix=api_prefix, tags=['tasks'])
|
||
app.include_router(delivery_router.router, prefix=api_prefix, tags=['delivery'])
|
||
app.include_router(stream_router.router, prefix=api_prefix, tags=['stream'])
|
||
app.include_router(auth_status_router.router, prefix=api_prefix, tags=['auth'])
|
||
app.include_router(auth_router.router, prefix=api_prefix, tags=['auth'])
|
||
app.include_router(user_router.router, prefix=api_prefix, tags=['user'])
|
||
app.include_router(admin_router.router, prefix=api_prefix, tags=['admin'])
|
||
app.include_router(register_router.router, prefix=api_prefix, tags=['auth'])
|
||
app.include_router(admin_site_router.router, prefix=api_prefix, tags=['admin'])
|
||
app.include_router(download_router.router, prefix=api_prefix, tags=['download'])
|
||
# 首页与静态资源(迁移完成形态;fallback 模式下同路径也能由 Flask 提供)
|
||
app.include_router(index_router.router, tags=['index'])
|
||
|
||
install_error_handlers(app)
|
||
|
||
if serve_flask_fallback and api_prefix:
|
||
_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))
|