"""FastAPI 入口:REST 接口 + WebSocket 端点 + 托管前端构建产物。""" from __future__ import annotations from pathlib import Path import uvicorn from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from pydantic import BaseModel from .config import CONFIG from .game.room import MANAGER app = FastAPI(title="狼人杀", version="0.1.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) ROOT = Path(__file__).resolve().parents[2] DIST = ROOT / "frontend" / "dist" # ---------------------------------------------------------------- REST class CreateRoomReq(BaseModel): board: str = "classic12" host_name: str = "房主" @app.get("/api/health") async def health(): return { "status": "ok", "llm_configured": CONFIG.llm.configured, "model": CONFIG.llm.model if CONFIG.llm.configured else None, } @app.get("/api/boards") async def boards(): from .game.board import BOARDS return [{"key": b.key, "name": b.name, "seat_count": b.seat_count} for b in BOARDS.values()] @app.post("/api/rooms") async def create_room(req: CreateRoomReq): code = MANAGER.create(req.board) return {"code": code} # ---------------------------------------------------------------- WebSocket @app.websocket("/ws/{code}") async def websocket_endpoint(ws: WebSocket, code: str): await ws.accept() room = MANAGER.get(code) if room is None: await ws.send_json({"type": "error", "message": "房间不存在或已解散"}) await ws.close(code=4404) return name = (ws.query_params.get("name") or "玩家").strip()[:12] or "玩家" token = ws.query_params.get("token") or None member, err = await room.attach(ws, name, token) await room.send_snapshot_to(member) if err: await room.send_error(member, err) try: while True: try: data = await ws.receive_json() except ValueError: continue await room.handle_message(member, data) except WebSocketDisconnect: await room.detach(member, ws) except Exception: await room.detach(member, ws) # ---------------------------------------------------------------- 静态前端 @app.get("/{full_path:path}", include_in_schema=False) async def spa(full_path: str): if full_path.startswith("api/") or full_path == "api": raise HTTPException(status_code=404) if full_path and (DIST / full_path).is_file(): return FileResponse(DIST / full_path) index = DIST / "index.html" if index.is_file(): return FileResponse(index) raise HTTPException(status_code=404, detail="前端未构建:请先在 frontend 目录执行 pnpm build") def main() -> None: uvicorn.run(app, host=CONFIG.server.host, port=CONFIG.server.port) if __name__ == "__main__": main()