"""FastAPI 主入口""" from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from .database import init_db from .routers import config, apps, tasks, auth, users from .services.log_streamer import log_streamer from .config import BACKEND_PORT STATIC_DIR = Path(__file__).parent / "static" @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期""" init_db() yield app = FastAPI( title="iOS 自动打包服务", version="1.0.0", lifespan=lifespan, ) # CORS app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 路由 app.include_router(auth.router) app.include_router(users.router) app.include_router(config.router) app.include_router(apps.router) app.include_router(tasks.router) @app.websocket("/ws/tasks/{task_id}") async def websocket_logs(websocket: WebSocket, task_id: str): """WebSocket 实时日志""" await websocket.accept() try: async for msg in log_streamer.subscribe(task_id): await websocket.send_json(msg) except WebSocketDisconnect: pass except Exception: pass @app.get("/api/health") async def health(): """健康检查""" return {"status": "ok", "port": BACKEND_PORT} # 静态文件服务(生产模式:前端 build 产物由 FastAPI 直接 serving) if STATIC_DIR.exists(): app.mount("/assets", StaticFiles(directory=str(STATIC_DIR / "assets")), name="assets") @app.get("/{full_path:path}") async def serve_spa(request: Request, full_path: str): """SPA catch-all:非 API 路由统一返回 index.html""" file_path = STATIC_DIR / full_path if file_path.is_file(): return FileResponse(str(file_path)) return FileResponse(str(STATIC_DIR / "index.html"))