feat: add MMGame backend and frontend source code

This commit is contained in:
gmh01
2026-08-01 21:30:23 +08:00
parent 26ce0dd600
commit c176c04aab
42 changed files with 3606 additions and 0 deletions
View File
+144
View File
@@ -0,0 +1,144 @@
import uuid
from datetime import datetime, timedelta
from jose import jwt, JWTError
from passlib.context import CryptContext
from fastapi import APIRouter, Depends, HTTPException, Header
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel
from app.database import get_db
from app.models.user import User
from app.models.game import GameSession
from app.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
router = APIRouter(prefix="/api/auth", tags=["auth"])
class RegisterRequest(BaseModel):
email: str
password: str
nickname: str
class LoginRequest(BaseModel):
email: str
password: str
class GuestRequest(BaseModel):
nickname: str = "游客"
class AuthResponse(BaseModel):
token: str
user_id: str
nickname: str
is_guest: bool
def create_token(user_id: str) -> str:
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return jwt.encode({"sub": user_id, "exp": expire, "v": 0}, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
async def get_current_user(authorization: str = Header(""), db: AsyncSession = Depends(get_db)) -> User:
if not authorization.startswith("Bearer "):
raise HTTPException(401, "未登录")
token = authorization[7:]
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
user_id = payload.get("sub")
except JWTError:
raise HTTPException(401, "无效的token")
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(401, "用户不存在")
return user
@router.post("/register")
async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User).where(User.email == req.email))
if result.scalar_one_or_none():
raise HTTPException(400, "邮箱已注册")
user = User(
id=str(uuid.uuid4()),
email=req.email,
nickname=req.nickname,
password_hash=pwd_context.hash(req.password),
)
db.add(user)
await db.commit()
return AuthResponse(token=create_token(user.id), user_id=user.id, nickname=user.nickname, is_guest=False)
@router.post("/login")
async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User).where(User.email == req.email))
user = result.scalar_one_or_none()
if not user or not user.password_hash or not pwd_context.verify(req.password, user.password_hash):
raise HTTPException(401, "邮箱或密码错误")
return AuthResponse(token=create_token(user.id), user_id=user.id, nickname=user.nickname, is_guest=False)
@router.post("/guest")
async def guest_login(req: GuestRequest, db: AsyncSession = Depends(get_db)):
user = User(
id=str(uuid.uuid4()),
nickname=req.nickname,
is_guest=True,
)
db.add(user)
await db.commit()
return AuthResponse(token=create_token(user.id), user_id=user.id, nickname=user.nickname, is_guest=True)
@router.post("/logout")
async def logout():
return {"status": "ok"}
@router.get("/profile")
async def get_profile(user: User = Depends(get_current_user)):
return {
"user_id": user.id,
"email": user.email,
"nickname": user.nickname,
"avatar_url": user.avatar_url,
"is_guest": user.is_guest,
"role": user.role,
"game_count": user.game_count,
"created_at": user.created_at.isoformat() if user.created_at else "",
}
@router.put("/profile")
async def update_profile(data: dict, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
if "nickname" in data:
user.nickname = data["nickname"]
if "avatar_url" in data:
user.avatar_url = data["avatar_url"]
await db.commit()
return {"status": "ok"}
@router.get("/history")
async def get_history(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(GameSession).order_by(GameSession.created_at.desc()).limit(50)
)
games = result.scalars().all()
return [
{
"id": g.id,
"script_id": g.script_id,
"status": g.status,
"phase": g.phase,
"started_at": g.started_at.isoformat() if g.started_at else "",
"completed_at": g.completed_at.isoformat() if g.completed_at else "",
"created_at": g.created_at.isoformat(),
}
for g in games
]
+694
View File
@@ -0,0 +1,694 @@
import json
import random
import uuid
import asyncio
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pydantic import BaseModel
from app.database import get_db
from app.models.game import GameSession, SessionPlayer, ChatMessage, ClueState, Vote, GameStatus, MessageType
from app.scripts.sample import get_script_by_id, get_script_by_title
from app.agents.npc import NPCAgent
from app.agents.dm import DMAgent
router = APIRouter(prefix="/api/games", tags=["games"])
active_connections: dict[str, list[WebSocket]] = {}
game_locks: dict[str, asyncio.Lock] = {}
def get_lock(game_id: str) -> asyncio.Lock:
if game_id not in game_locks:
game_locks[game_id] = asyncio.Lock()
return game_locks[game_id]
async def broadcast(game_id: str, message: dict):
if game_id in active_connections:
data = json.dumps(message, ensure_ascii=False)
for ws in active_connections[game_id][:]:
try:
await ws.send_text(data)
except Exception:
active_connections[game_id].remove(ws)
async def broadcast_to_role(game_id: str, target_role_id: str, message: dict):
if game_id in active_connections:
data = json.dumps(message, ensure_ascii=False)
for ws in active_connections[game_id][:]:
try:
role_id = getattr(ws, "role_id", None)
if role_id is None or role_id == target_role_id:
await ws.send_text(data)
except Exception:
active_connections[game_id].remove(ws)
class CreateGameRequest(BaseModel):
script_id: str
human_role_id: str = ""
class JoinGameRequest(BaseModel):
role_id: str
class ChatRequest(BaseModel):
type: str = "public"
content: str
target_role_id: str | None = None
sender_role_id: str = ""
class VoteRequest(BaseModel):
target_role_id: str
reason: str = ""
voter_role_id: str = ""
@router.post("/create")
async def create_game(req: CreateGameRequest, db: AsyncSession = Depends(get_db)):
script = get_script_by_id(req.script_id)
if not script:
raise HTTPException(404, "剧本不存在")
game = GameSession(
id=str(uuid.uuid4()),
script_id=req.script_id,
status=GameStatus.waiting.value,
phase=script["phases"][0]["name"] if script["phases"] else "",
config={"script": script},
)
db.add(game)
await db.commit()
for role in script["roles"]:
is_human = role["id"] == req.human_role_id
sp = SessionPlayer(
id=str(uuid.uuid4()),
session_id=game.id,
role_id=role["id"],
role_name=role["name"],
is_human=is_human,
avatar_url="",
)
db.add(sp)
first_phase_clues = script["phases"][0].get("clues", []) if script["phases"] else []
for clue_id in first_phase_clues:
cs = ClueState(id=str(uuid.uuid4()), session_id=game.id, clue_id=clue_id, status="released")
db.add(cs)
await db.commit()
return {"game_id": game.id}
@router.post("/{game_id}/join")
async def join_game(game_id: str, req: JoinGameRequest, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(GameSession).where(GameSession.id == game_id))
game = result.scalar_one_or_none()
if not game:
raise HTTPException(404, "游戏不存在")
player = SessionPlayer(
id=str(uuid.uuid4()),
session_id=game_id,
role_id=req.role_id,
is_human=True,
)
db.add(player)
await db.commit()
script = get_script_by_id(game.script_id) or {}
for role in script.get("roles", []):
if role["id"] == req.role_id:
return {
"player_id": player.id,
"role_id": req.role_id,
"role_name": role["name"],
"public_profile": role["publicProfile"],
"secret_profile": role["secretProfile"],
"secret": role.get("secret", ""),
"goal": role["goal"],
}
return {"player_id": player.id, "role_id": req.role_id}
@router.post("/{game_id}/start")
async def start_game(game_id: str, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(GameSession).where(GameSession.id == game_id))
game = result.scalar_one_or_none()
if not game:
raise HTTPException(404, "游戏不存在")
game.status = GameStatus.playing.value
game.started_at = datetime.utcnow()
await db.commit()
script = game.config.get("script", {}) if game.config else {}
background = script.get("background", "")
title = script.get("title", "")
dm_msg = ChatMessage(
id=str(uuid.uuid4()),
session_id=game_id,
sender_role_id="dm",
sender_name="DM",
message_type=MessageType.system.value,
content=f"欢迎来到【{title}】。\n{background}\n\n请各位玩家阅读角色信息,做好准备后,在公屏中发送「准备好了」开始游戏。",
phase_index=game.phase_index,
)
db.add(dm_msg)
await db.commit()
await broadcast(game_id, {
"type": "game_started",
"data": {
"phase": game.phase,
"phase_index": game.phase_index,
"message": {
"id": dm_msg.id,
"sender_name": "DM",
"sender_role_id": "dm",
"message_type": "system",
"content": dm_msg.content,
"created_at": dm_msg.created_at.isoformat(),
}
}
})
return {"status": "started", "phase": game.phase}
@router.post("/{game_id}/chat")
async def send_chat(game_id: str, req: ChatRequest, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(GameSession).where(GameSession.id == game_id))
game = result.scalar_one_or_none()
if not game:
raise HTTPException(404, "游戏不存在")
sender_name = req.sender_role_id
players_result = await db.execute(
select(SessionPlayer).where(SessionPlayer.session_id == game_id, SessionPlayer.role_id == req.sender_role_id)
)
player = players_result.scalar_one_or_none()
script = game.config.get("script", {}) if game.config else {}
for role in script.get("roles", []):
if role["id"] == req.sender_role_id:
sender_name = role["name"]
break
msg = ChatMessage(
id=str(uuid.uuid4()),
session_id=game_id,
sender_role_id=req.sender_role_id,
sender_name=sender_name,
message_type=req.type,
content=req.content,
target_role_id=req.target_role_id,
phase_index=game.phase_index,
)
db.add(msg)
npc_responses = await _generate_npc_responses_inline(game_id, req, script, sender_name, game.phase, game.phase_index, db)
for npc_msg_data in npc_responses:
if npc_msg_data["_msg"] is not None:
db.add(npc_msg_data["_msg"])
await db.commit()
msg_data = {
"type": "chat_message",
"data": {
"id": msg.id,
"sender_role_id": req.sender_role_id,
"sender_name": sender_name,
"message_type": req.type,
"content": req.content,
"target_role_id": req.target_role_id,
"phase_index": game.phase_index,
"created_at": msg.created_at.isoformat(),
}
}
if req.type == "private" and req.target_role_id:
await broadcast_to_role(game_id, req.sender_role_id, msg_data)
await broadcast_to_role(game_id, req.target_role_id, msg_data)
else:
await broadcast(game_id, msg_data)
for npc in npc_responses:
npc_broadcast = {k: v for k, v in npc.items() if k != "_msg"}
if npc["data"]["message_type"] == "private" and npc["data"].get("target_role_id"):
await broadcast_to_role(game_id, req.sender_role_id, npc_broadcast)
await broadcast_to_role(game_id, npc["data"]["target_role_id"], npc_broadcast)
else:
await broadcast(game_id, npc_broadcast)
return {
"status": "ok",
"message_id": msg.id,
"npc_responses": [
{k: v for k, v in n.items() if k != "_msg"}
for n in npc_responses
],
}
@router.post("/{game_id}/phase/advance")
async def advance_phase(game_id: str, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(GameSession).where(GameSession.id == game_id))
game = result.scalar_one_or_none()
if not game:
raise HTTPException(404, "游戏不存在")
script = game.config.get("script", {}) if game.config else {}
phases = script.get("phases", [])
next_idx = game.phase_index + 1
if next_idx >= len(phases):
return {"status": "already_at_end"}
game.phase_index = next_idx
game.phase = phases[next_idx]["name"]
await db.commit()
phase = phases[next_idx]
msg = ChatMessage(
id=str(uuid.uuid4()),
session_id=game_id,
sender_role_id="dm",
sender_name="DM",
message_type=MessageType.system.value,
content=f"{phase['name']}\n{phase['description']}\n\n{phase.get('publicInfo', '')}",
phase_index=next_idx,
)
db.add(msg)
await db.commit()
clues_to_release = phase.get("clues", [])
for clue_id in clues_to_release:
existing = await db.execute(
select(ClueState).where(ClueState.session_id == game_id, ClueState.clue_id == clue_id)
)
if not existing.scalar_one_or_none():
cs = ClueState(id=str(uuid.uuid4()), session_id=game_id, clue_id=clue_id, status="released")
db.add(cs)
await db.commit()
await broadcast(game_id, {
"type": "phase_change",
"data": {
"phase": game.phase,
"phase_index": next_idx,
"message": {
"id": msg.id,
"sender_name": "DM",
"sender_role_id": "dm",
"message_type": "system",
"content": msg.content,
"created_at": msg.created_at.isoformat(),
},
"new_clues": clues_to_release,
}
})
return {"status": "ok", "phase": game.phase, "phase_index": next_idx}
@router.post("/{game_id}/clue/{clue_id}/reveal")
async def reveal_clue(game_id: str, clue_id: str, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(ClueState).where(ClueState.session_id == game_id, ClueState.clue_id == clue_id)
)
clue_state = result.scalar_one_or_none()
if not clue_state:
raise HTTPException(404, "线索不存在或未发放")
clue_state.status = "revealed"
await db.commit()
game_result = await db.execute(select(GameSession).where(GameSession.id == game_id))
game = game_result.scalar_one()
script = game.config.get("script", {}) if game.config else {}
clue_info = None
for c in script.get("clues", []):
if c["id"] == clue_id:
clue_info = c
break
await broadcast(game_id, {
"type": "clue_revealed",
"data": {"clue_id": clue_id, "clue": clue_info}
})
return {"status": "revealed", "clue": clue_info}
@router.post("/{game_id}/vote")
async def submit_vote(game_id: str, req: VoteRequest, db: AsyncSession = Depends(get_db)):
voter = await db.execute(
select(SessionPlayer).where(SessionPlayer.session_id == game_id, SessionPlayer.role_id == req.voter_role_id)
)
voter_player = voter.scalar_one_or_none()
if not voter_player:
raise HTTPException(404, "投票者不存在")
target = await db.execute(
select(SessionPlayer).where(SessionPlayer.session_id == game_id, SessionPlayer.role_id == req.target_role_id)
)
target_player = target.scalar_one_or_none()
if not target_player:
raise HTTPException(404, "投票目标不存在")
vote = Vote(
id=str(uuid.uuid4()),
session_id=game_id,
voter_id=voter_player.id,
target_id=target_player.id,
reason=req.reason,
)
db.add(vote)
await db.commit()
await broadcast(game_id, {
"type": "vote_cast",
"data": {
"voter_role_id": req.voter_role_id,
"target_role_id": req.target_role_id,
"voter_name": voter_player.role_name,
}
})
return {"status": "ok"}
@router.post("/{game_id}/vote/end")
async def end_vote(game_id: str, db: AsyncSession = Depends(get_db)):
game_result = await db.execute(select(GameSession).where(GameSession.id == game_id))
game = game_result.scalar_one_or_none()
if not game:
raise HTTPException(404, "游戏不存在")
script = game.config.get("script", {}) if game.config else {}
existing_votes_result = await db.execute(select(Vote).where(Vote.session_id == game_id))
existing_votes = existing_votes_result.scalars().all()
existing_voter_ids = {v.voter_id for v in existing_votes}
players_result = await db.execute(select(SessionPlayer).where(SessionPlayer.session_id == game_id))
players_list = players_result.scalars().all()
players = {p.id: p for p in players_list}
clues_result = await db.execute(select(ClueState).where(ClueState.session_id == game_id, ClueState.status == "released"))
released_clue_states = clues_result.scalars().all()
released_clue_ids = {c.clue_id for c in released_clue_states}
all_clues = {c["id"]: c for c in script.get("clues", [])}
known_clues = [all_clues[cid]["description"] for cid in released_clue_ids if cid in all_clues]
npc_players = [p for p in players_list if not p.is_human and p.id not in existing_voter_ids]
all_roles = script.get("roles", [])
role_dict = {r["id"]: r for r in all_roles}
for npc in npc_players:
targets = [p for p in players_list if p.id != npc.id]
if not targets:
continue
npc_role = role_dict.get(npc.role_id)
if npc_role:
agent = NPCAgent(npc_role, script.get("background", ""), "")
vote_reason = await agent.respond("投票阶段", [], "请投票选出你最怀疑的人,并说明理由",
known_clues=known_clues, all_roles=all_roles)
vote_content = vote_reason.get("content", "")
target_id = random.choice(targets).id
for t in targets:
if t.role_name and t.role_name in vote_content:
target_id = t.id
break
else:
target_id = random.choice(targets).id
vote = Vote(
id=str(uuid.uuid4()),
session_id=game_id,
voter_id=npc.id,
target_id=target_id,
reason="",
)
db.add(vote)
target_player = players[target_id]
await broadcast(game_id, {
"type": "vote_cast",
"data": {
"voter_role_id": npc.role_id,
"voter_name": npc.role_name,
"target_role_id": target_player.role_id,
"target_name": target_player.role_name,
}
})
await db.commit()
votes_result = await db.execute(select(Vote).where(Vote.session_id == game_id))
votes = votes_result.scalars().all()
tally: dict[str, list[str]] = {}
for v in votes:
target_role = players[v.target_id].role_id if v.target_id in players else "unknown"
voter_role = players[v.voter_id].role_id if v.voter_id in players else "unknown"
if target_role not in tally:
tally[target_role] = []
tally[target_role].append(voter_role)
max_votes = 0
accused_role_id = ""
for role_id, voters in tally.items():
if len(voters) > max_votes:
max_votes = len(voters)
accused_role_id = role_id
accused_name = accused_role_id
for role in script.get("roles", []):
if role["id"] == accused_role_id:
accused_name = role["name"]
break
truth = script.get("truth", "")
game.status = GameStatus.completed.value
game.completed_at = datetime.utcnow()
await db.commit()
result_msg = ChatMessage(
id=str(uuid.uuid4()),
session_id=game_id,
sender_role_id="dm",
sender_name="DM",
message_type=MessageType.system.value,
content=f"投票结束!最多票指向:{accused_name}{max_votes}票)\n\n真相:{truth}",
phase_index=game.phase_index,
)
db.add(result_msg)
await db.commit()
await broadcast(game_id, {
"type": "vote_result",
"data": {
"tally": {role_id: voters for role_id, voters in tally.items()},
"accused_role_id": accused_role_id,
"accused_name": accused_name,
"truth": truth,
"game_id": game_id,
"message": {
"id": result_msg.id,
"sender_name": "DM",
"sender_role_id": "dm",
"message_type": "system",
"content": result_msg.content,
"created_at": result_msg.created_at.isoformat(),
}
}
})
if game_id in active_connections:
del active_connections[game_id]
return {"status": "completed", "accused": accused_name, "truth": truth}
@router.get("/{game_id}/state")
async def get_game_state(game_id: str, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(GameSession).where(GameSession.id == game_id))
game = result.scalar_one_or_none()
if not game:
raise HTTPException(404, "游戏不存在")
clues_result = await db.execute(select(ClueState).where(ClueState.session_id == game_id))
clues = clues_result.scalars().all()
players_result = await db.execute(select(SessionPlayer).where(SessionPlayer.session_id == game_id))
players = players_result.scalars().all()
msgs_result = await db.execute(
select(ChatMessage).where(ChatMessage.session_id == game_id).order_by(ChatMessage.created_at)
)
msgs = msgs_result.scalars().all()
script = game.config.get("script", {}) if game.config else {}
return {
"id": game.id,
"script_id": game.script_id,
"status": game.status,
"phase": game.phase,
"phase_index": game.phase_index,
"script": script,
"players": [
{"id": p.id, "role_id": p.role_id, "role_name": p.role_name, "is_human": p.is_human, "status": p.status}
for p in players
],
"clues": [{"clue_id": c.clue_id, "status": c.status} for c in clues],
"messages": [
{
"id": m.id, "sender_role_id": m.sender_role_id, "sender_name": m.sender_name,
"message_type": m.message_type, "content": m.content, "target_role_id": m.target_role_id,
"clue_id": m.clue_id, "phase_index": m.phase_index,
"created_at": m.created_at.isoformat(),
}
for m in msgs
],
}
@router.get("/{game_id}/replay")
async def get_replay(game_id: str, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(GameSession).where(GameSession.id == game_id))
game = result.scalar_one_or_none()
if not game:
raise HTTPException(404, "游戏不存在")
msgs_result = await db.execute(
select(ChatMessage).where(ChatMessage.session_id == game_id).order_by(ChatMessage.created_at)
)
msgs = msgs_result.scalars().all()
votes_result = await db.execute(
select(Vote).where(Vote.session_id == game_id)
)
votes = votes_result.scalars().all()
players_result = await db.execute(select(SessionPlayer).where(SessionPlayer.session_id == game_id))
players = {p.id: p for p in players_result.scalars().all()}
script = game.config.get("script", {}) if game.config else {}
vote_details = []
for v in votes:
voter = players.get(v.voter_id)
target = players.get(v.target_id)
vote_details.append({
"voter_name": voter.role_name if voter else "unknown",
"target_name": target.role_name if target else "unknown",
"reason": v.reason,
})
return {
"game_id": game_id,
"script_title": script.get("title", game.script_id),
"truth": script.get("truth", ""),
"roles": [
{"id": r["id"], "name": r["name"], "publicProfile": r["publicProfile"],
"secretProfile": r["secretProfile"], "goal": r["goal"]}
for r in script.get("roles", [])
],
"messages": [
{"id": m.id, "sender_name": m.sender_name, "content": m.content,
"type": m.message_type, "time": m.created_at.isoformat()}
for m in msgs
],
"votes": vote_details,
}
@router.websocket("/ws/{game_id}")
async def game_websocket(websocket: WebSocket, game_id: str, role_id: str = Query("")):
await websocket.accept()
websocket.role_id = role_id
if game_id not in active_connections:
active_connections[game_id] = []
active_connections[game_id].append(websocket)
try:
while True:
data = await websocket.receive_text()
msg = json.loads(data)
msg_type = msg.get("type", "")
msg_data = msg.get("data", {})
if msg_type == "ping":
await websocket.send_text(json.dumps({"type": "pong"}))
except WebSocketDisconnect:
if game_id in active_connections and websocket in active_connections[game_id]:
active_connections[game_id].remove(websocket)
except Exception:
if game_id in active_connections and websocket in active_connections[game_id]:
active_connections[game_id].remove(websocket)
async def _generate_npc_responses_inline(game_id: str, req, script: dict, sender_name: str, phase_name: str, phase_index: int, db: AsyncSession) -> list[dict]:
npc_responses = []
try:
npc_roles = [r for r in script.get("roles", []) if r["id"] != req.sender_role_id]
random.shuffle(npc_roles)
if not npc_roles:
return npc_responses
recent_msgs_result = await db.execute(
select(ChatMessage).where(ChatMessage.session_id == game_id).order_by(ChatMessage.created_at.desc()).limit(10)
)
recent_msgs = recent_msgs_result.scalars().all()
recent_list = [{"sender_name": m.sender_name, "content": m.content} for m in recent_msgs]
clues_result = await db.execute(select(ClueState).where(ClueState.session_id == game_id, ClueState.status == "released"))
released_clue_states = clues_result.scalars().all()
released_clue_ids = {c.clue_id for c in released_clue_states}
all_clues = {c["id"]: c for c in script.get("clues", [])}
known_clues = [all_clues[cid]["description"] for cid in released_clue_ids if cid in all_clues]
phases = script.get("phases", [])
current_phase = phases[phase_index] if phase_index < len(phases) else None
phase_goal = current_phase["description"] if current_phase else ""
all_roles = script.get("roles", [])
for npc_role in npc_roles[:2]:
agent = NPCAgent(npc_role, script.get("background", ""), "")
is_private = req.type == "private" and req.target_role_id == npc_role["id"]
response = await agent.respond(phase_name, recent_list, req.content, is_private,
known_clues=known_clues, phase_goal=phase_goal, all_roles=all_roles)
content = response.get("content", "")
action = response.get("action", "")
if not content or action == "remain_silent":
continue
now = datetime.utcnow()
npc_msg = ChatMessage(
id=str(uuid.uuid4()),
session_id=game_id,
sender_role_id=npc_role["id"],
sender_name=npc_role["name"],
message_type="private" if is_private else "public",
content=content,
target_role_id=req.sender_role_id if is_private else None,
phase_index=phase_index,
created_at=now,
)
npc_responses.append({
"_msg": npc_msg,
"type": "chat_message",
"data": {
"id": npc_msg.id,
"sender_role_id": npc_role["id"],
"sender_name": npc_role["name"],
"message_type": npc_msg.message_type,
"content": content,
"target_role_id": npc_msg.target_role_id,
"phase_index": phase_index,
"created_at": npc_msg.created_at.isoformat(),
}
})
except Exception:
pass
return npc_responses
+45
View File
@@ -0,0 +1,45 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pydantic import BaseModel
from app.database import get_db
from app.models.script import Script
from app.scripts.sample import SCRIPTS
router = APIRouter(prefix="/api/scripts", tags=["scripts"])
class ScriptListItem(BaseModel):
id: str
title: str
type: str
difficulty: int
min_players: int
max_players: int
duration: str
background: str
@router.get("")
async def list_scripts():
items = []
for s in SCRIPTS:
items.append(ScriptListItem(
id=s["id"],
title=s["title"],
type=s["type"],
difficulty=s["difficulty"],
min_players=s["playerCount"]["min"],
max_players=s["playerCount"]["max"],
duration=s["duration"],
background=s["background"],
))
return items
@router.get("/{script_id}")
async def get_script(script_id: str):
for s in SCRIPTS:
if s["id"] == script_id:
return s
raise HTTPException(404, "剧本不存在")