from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from starlette.responses import FileResponse from .database import init_db from .routers.novels import router as novels_router from .routers.outlines import router as outlines_router from .routers.characters import router as characters_router from .routers.chapters import router as chapters_router from .routers.world_setting import router as world_setting_router from .routers.ai_config import router as ai_config_router from .routers.chat import router as chat_router from .routers.files import router as files_router from .routers.skills import router as skills_router # 前端构建产物目录(Docker 中为 /app/static,本地开发时不存在则跳过) STATIC_DIR = Path(__file__).resolve().parent.parent / "static" @asynccontextmanager async def lifespan(app: FastAPI): init_db() yield app = FastAPI(title="Noval", version="0.1.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(novels_router) app.include_router(outlines_router) app.include_router(characters_router) app.include_router(chapters_router) app.include_router(world_setting_router) app.include_router(ai_config_router) app.include_router(chat_router) app.include_router(files_router) app.include_router(skills_router) @app.get("/api/health") def health(): return {"status": "ok"} # 生产环境:serve 前端静态文件(Vue SPA) if STATIC_DIR.is_dir(): # 静态资源(js/css/img) app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets") # 所有非 /api 路径返回 index.html(Vue Router history 模式) @app.get("/{path:path}") async def spa_fallback(path: str): file_path = STATIC_DIR / path if file_path.is_file(): return FileResponse(file_path) return FileResponse(STATIC_DIR / "index.html")