- FastAPI backend with build queue, WebSocket logs, task management - Vue 3 frontend with build/config/history views - Xcode project build automation with IPA export - Fix: initialize build_dir before try block to ensure cleanup on early failure
76 lines
2.0 KiB
Python
76 lines
2.0 KiB
Python
"""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
|
||
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(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"))
|