This commit is contained in:
2026-06-04 17:59:53 +08:00
parent e6c27ac662
commit 0908495d6f
66 changed files with 4230 additions and 0 deletions
View File
+140
View File
@@ -0,0 +1,140 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.database import get_session
from app.models.orm import Character
from app.models.schemas import (
CharacterResponse,
CharacterCreate,
CharacterStatus,
CharacterSpeakRequest,
SoulUpdateRequest,
CharacterGenerateRequest,
)
from app.services.character_generator import character_generator
from app.services.hermes_client import hermes_client
from app.services.soul_generator import soul_generator
router = APIRouter()
@router.get("", response_model=list[CharacterResponse])
async def list_characters(session: AsyncSession = Depends(get_session)):
result = await session.execute(select(Character))
return list(result.scalars().all())
@router.get("/{character_id}", response_model=CharacterResponse)
async def get_character(character_id: str, session: AsyncSession = Depends(get_session)):
result = await session.execute(select(Character).where(Character.id == character_id))
character = result.scalars().first()
if not character:
raise HTTPException(status_code=404, detail="Character not found")
return character
@router.put("/{character_id}/soul")
async def update_character_soul(
character_id: str,
req: SoulUpdateRequest,
session: AsyncSession = Depends(get_session),
):
result = await session.execute(select(Character).where(Character.id == character_id))
character = result.scalars().first()
if not character:
raise HTTPException(status_code=404, detail="Character not found")
previous = character.hermes_profile
character.soul_md = req.soul_md
character.knowledge_base = req.knowledge_base
await hermes_client.create_profile(previous, req.soul_md)
await session.commit()
await session.refresh(character)
return {"ok": True, "character_id": character_id}
@router.post("/{character_id}/speak")
async def character_speak(
character_id: str,
req: CharacterSpeakRequest,
session: AsyncSession = Depends(get_session),
):
result = await session.execute(select(Character).where(Character.id == character_id))
character = result.scalars().first()
if not character:
raise HTTPException(status_code=404, detail="Character not found")
prompt = req.prompt or "根据当前剧本进度自然发言"
response = await hermes_client.chat(character.hermes_profile, prompt)
return {
"character_id": character_id,
"character_name": character.name,
"response": response,
}
@router.post("/{character_id}/profile")
async def create_character_profile(
character_id: str,
session: AsyncSession = Depends(get_session),
):
result = await session.execute(select(Character).where(Character.id == character_id))
character = result.scalars().first()
if not character:
raise HTTPException(status_code=404, detail="Character not found")
success = await character_generator.create_profile(character)
await session.commit()
return {"ok": success, "character_id": character_id}
@router.post("/generate", response_model=list[CharacterResponse])
async def generate_characters(
req: CharacterGenerateRequest,
session: AsyncSession = Depends(get_session),
):
result = await session.execute(select(Character).where(Character.script_id == req.script_id))
existing = list(result.scalars().all())
for char in existing:
await session.delete(char)
await session.commit()
character_data = [{"name": name} for name in req.character_names]
characters = await character_generator.generate_characters(
session, req.script_id, character_data
)
return characters
@router.post("/{character_id}/status")
async def update_character_status(
character_id: str,
status: CharacterStatus,
session: AsyncSession = Depends(get_session),
):
result = await session.execute(select(Character).where(Character.id == character_id))
character = result.scalars().first()
if not character:
raise HTTPException(status_code=404, detail="Character not found")
character.status = status
await session.commit()
await session.refresh(character)
return {"ok": True, "status": character.status.value}
@router.delete("/{character_id}")
async def delete_character(character_id: str, session: AsyncSession = Depends(get_session)):
result = await session.execute(select(Character).where(Character.id == character_id))
character = result.scalars().first()
if not character:
raise HTTPException(status_code=404, detail="Character not found")
await character_generator.delete_profile(character)
await session.delete(character)
await session.commit()
return {"ok": True}
+78
View File
@@ -0,0 +1,78 @@
from fastapi import APIRouter, Depends
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.database import get_session
from app.models.orm import Message, Character
from app.models.schemas import MessageCreate, MessageResponse
router = APIRouter()
@router.get("/messages", response_model=list[MessageResponse])
async def list_messages(
session: AsyncSession = Depends(get_session),
):
result = await session.execute(
select(Message).order_by(Message.created_at.asc())
)
messages = list(result.scalars().all())
return [
MessageResponse(
id=m.id,
session_id=m.session_id,
character_id=m.character_id,
game_phase=m.game_phase,
msg_type=m.msg_type,
content=m.content,
target_character_id=m.target_character_id,
clue_id=m.clue_id,
metadata=m.metadata or {},
created_at=m.created_at,
character_name=m.character.name if m.character else None,
character_role=m.character.role if m.character else None,
)
for m in messages
]
@router.post("/messages", response_model=MessageResponse)
async def create_message(
req: MessageCreate,
session: AsyncSession = Depends(get_session),
):
message = Message(
session_id=req.session_id,
character_id=req.character_id,
game_phase=req.game_phase,
msg_type=req.msg_type,
content=req.content,
target_character_id=req.target_character_id,
)
session.add(message)
await session.commit()
await session.refresh(message)
char_name = None
char_role = None
if message.character_id:
result = await session.execute(select(Character).where(Character.id == message.character_id))
char = result.scalars().first()
if char:
char_name = char.name
char_role = char.role
return MessageResponse(
id=message.id,
session_id=message.session_id,
character_id=message.character_id,
game_phase=message.game_phase,
msg_type=message.msg_type,
content=message.content,
target_character_id=message.target_character_id,
clue_id=message.clue_id,
metadata=message.metadata or {},
created_at=message.created_at,
character_name=char_name,
character_role=char_role,
)
+83
View File
@@ -0,0 +1,83 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.database import get_session
from app.models.orm import Clue, Vote, Character
from app.models.schemas import ClueCreate, ClueResponse, ClueUnlockRequest, VoteCreate, VoteResponse
router = APIRouter()
@router.get("/{script_id}/clues", response_model=list[ClueResponse])
async def list_clues(script_id: str, session: AsyncSession = Depends(get_session)):
result = await session.execute(select(Clue).where(Clue.script_id == script_id))
return list(result.scalars().all())
@router.post("/{script_id}/clues", response_model=ClueResponse)
async def create_clue(script_id: str, req: ClueCreate, session: AsyncSession = Depends(get_session)):
clue = Clue(
script_id=script_id,
name=req.name,
content=req.content,
clue_type=req.clue_type,
owner_id=req.owner_id,
phase=req.phase,
visibility=req.visibility,
visible_to=req.visible_to,
)
session.add(clue)
await session.commit()
await session.refresh(clue)
return clue
@router.post("/clues/{clue_id}/unlock")
async def unlock_clue(
clue_id: str,
req: ClueUnlockRequest,
session: AsyncSession = Depends(get_session),
):
result = await session.execute(select(Clue).where(Clue.id == clue_id))
clue = result.scalars().first()
if not clue:
raise HTTPException(status_code=404, detail="Clue not found")
from datetime import datetime
clue.is_unlocked = True
clue.unlocked_by = req.character_id
clue.unlocked_at = datetime.utcnow()
await session.commit()
await session.refresh(clue)
return {"ok": True, "clue": ClueResponse.model_validate(clue)}
@router.delete("/clues/{clue_id}")
async def delete_clue(clue_id: str, session: AsyncSession = Depends(get_session)):
result = await session.execute(select(Clue).where(Clue.id == clue_id))
clue = result.scalars().first()
if not clue:
raise HTTPException(status_code=404, detail="Clue not found")
await session.delete(clue)
await session.commit()
return {"ok": True}
@router.get("/votes", response_model=list[VoteResponse])
async def list_votes(round_number: int = 1, session: AsyncSession = Depends(get_session)):
result = await session.execute(select(Vote).where(Vote.round_number == round_number))
return list(result.scalars().all())
@router.post("/votes", response_model=VoteResponse)
async def create_vote(req: VoteCreate, session: AsyncSession = Depends(get_session)):
vote = Vote(
voter_id=req.voter_id,
target_id=req.target_id,
reason=req.reason,
)
session.add(vote)
await session.commit()
await session.refresh(vote)
return vote
+78
View File
@@ -0,0 +1,78 @@
from fastapi import APIRouter, Depends, HTTPException, Body
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel
from app.models.database import get_session
from app.models.schemas import GameStateResponse, DmSpeakRequest
from app.services.game_state_manager import game_state_manager
from app.services.agent_scheduler import agent_scheduler
router = APIRouter()
class StartGameRequest(BaseModel):
script_id: str
class SetSpeakerRequest(BaseModel):
character_id: str
@router.get("/state", response_model=GameStateResponse)
async def get_state(session: AsyncSession = Depends(get_session)):
state = await game_state_manager.get_or_create_state(session)
return game_state_manager.to_response(state)
@router.post("/start", response_model=GameStateResponse)
async def start_game(
req: StartGameRequest,
session: AsyncSession = Depends(get_session),
):
state = await game_state_manager.start_game(session, req.script_id)
return game_state_manager.to_response(state)
@router.post("/pause", response_model=GameStateResponse)
async def pause_game(session: AsyncSession = Depends(get_session)):
state = await game_state_manager.pause_game(session)
return game_state_manager.to_response(state)
@router.post("/resume", response_model=GameStateResponse)
async def resume_game(session: AsyncSession = Depends(get_session)):
state = await game_state_manager.resume_game(session)
return game_state_manager.to_response(state)
@router.post("/phase/next", response_model=GameStateResponse)
async def phase_next(session: AsyncSession = Depends(get_session)):
state = await game_state_manager.next_phase(session)
return game_state_manager.to_response(state)
@router.post("/phase/prev", response_model=GameStateResponse)
async def phase_prev(session: AsyncSession = Depends(get_session)):
state = await game_state_manager.prev_phase(session)
return game_state_manager.to_response(state)
@router.post("/dm/speak")
async def dm_speak(req: DmSpeakRequest):
result = await agent_scheduler.dm_speak(req.content)
return {"content": result, "msg_type": "dm", "phase": req.phase}
@router.post("/speaker/set", response_model=GameStateResponse)
async def set_speaker(
req: SetSpeakerRequest,
session: AsyncSession = Depends(get_session),
):
state = await game_state_manager.set_speaker(session, req.character_id)
return game_state_manager.to_response(state)
@router.post("/reset", response_model=GameStateResponse)
async def reset_game(session: AsyncSession = Depends(get_session)):
state = await game_state_manager.reset(session)
return game_state_manager.to_response(state)
+93
View File
@@ -0,0 +1,93 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.database import get_session
from app.models.orm import Script
from app.models.schemas import ScriptUploadRequest, ScriptParsePreview, ScriptImportRequest, ScriptResponse
from app.services.script_parser import script_parser
router = APIRouter()
@router.post("/upload", response_model=ScriptParsePreview)
async def upload_script(
req: ScriptUploadRequest,
session: AsyncSession = Depends(get_session),
):
parsed = await script_parser.parse(req.content, req.file_type)
return ScriptParsePreview(
title=parsed["title"],
background=parsed["background"],
characters=parsed["characters"],
clues=parsed["clues"],
phases=parsed["phases"],
)
@router.post("/import", response_model=ScriptResponse)
async def import_script(
req: ScriptImportRequest,
session: AsyncSession = Depends(get_session),
):
script = Script(
title=req.title,
background=req.background,
raw_content="",
parsed_data={
"characters": req.characters,
"clues": req.clues,
"phases": req.phases,
},
character_count=len(req.characters),
)
session.add(script)
await session.commit()
await session.refresh(script)
return script
@router.get("/preview")
async def preview_scripts(session: AsyncSession = Depends(get_session)):
result = await session.execute(
select(Script).order_by(Script.created_at.desc())
)
scripts = list(result.scalars().all())
return [
{
"id": s.id,
"title": s.title,
"background": s.background,
"character_count": s.character_count,
"parsed_data": s.parsed_data,
}
for s in scripts
]
@router.get("", response_model=list[ScriptResponse])
async def list_scripts(session: AsyncSession = Depends(get_session)):
result = await session.execute(
select(Script).order_by(Script.created_at.desc())
)
return list(result.scalars().all())
@router.get("/{script_id}", response_model=ScriptResponse)
async def get_script(script_id: str, session: AsyncSession = Depends(get_session)):
result = await session.execute(select(Script).where(Script.id == script_id))
script = result.scalars().first()
if not script:
raise HTTPException(status_code=404, detail="Script not found")
return script
@router.delete("/{script_id}")
async def delete_script(script_id: str, session: AsyncSession = Depends(get_session)):
result = await session.execute(select(Script).where(Script.id == script_id))
script = result.scalars().first()
if not script:
raise HTTPException(status_code=404, detail="Script not found")
await session.delete(script)
await session.commit()
return {"ok": True}