Initial commit: 网页联机狼人杀(FastAPI+WebSocket 后端 / React+TS+Sass 前端)
This commit is contained in:
commit
7f5ea6bee8
55 files changed
+6999
No files matched your search
@@ -0,0 +1,101 @@
|
||||
"""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()
|
||||
Reference in new issue
Block a user