Initial commit: iOS Build Server

- 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
This commit is contained in:
shen
2026-06-06 17:42:27 +08:00
commit 6f4f625c56
51 changed files with 9968 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
"""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"))