refactor: завершение занятия — одна операция end_session вместо четырёх копий в сокетах

This commit is contained in:
gglamer 2026-09-27 07:40:31 +00:00
commit f9b6fafb7c
6 changed files with 299 additions and 69 deletions

View file

@ -30,7 +30,6 @@ from app.domain.events import (
KioState,
PatchSource,
ScoreReady,
SessionEnded,
SessionMode,
StationState,
TimerTick,
@ -39,17 +38,15 @@ from app.domain.events import (
TranscriptAppend,
TraineeToServer,
)
from app.domain.kio import ResponseStatus
from app.dialog.slots import TurnResult
from app.domain.roles import Role
from app.scenarios import store
from app.session.dds import prepare_handoff_queue
from app.session.finish import finish, refresh_archived_report, release_score
from app.session.finish import end_session, refresh_archived_report, release_score
from app.session.hub import LEASE_FENCED_MESSAGE, hub
from app.session.state import now_utc
from app.session.store import (
HintRecorded,
LessonEnded,
LessonStarted,
SelfAssessed,
UtteranceAppended,
@ -242,9 +239,7 @@ async def _handle(session_id: UUID, state, event) -> None:
case "card.submit":
state.on_event("card.submit")
state.kio.registered_at = state.started_at or now_utc()
state.kio.response_status = ResponseStatus.TRANSFERRED
state.dispatched_card = state.kio.model_copy(deep=True)
state.dispatched_at = now_utc()
state.dispatch()
if state.handoff_to_dds:
prepare_handoff_queue(
state,
@ -253,20 +248,14 @@ async def _handle(session_id: UUID, state, event) -> None:
max_waiting=state.desk.max_waiting,
)
state.pending_dds_scenarios = []
else:
state.ended_at = state.dispatched_at
state.end_reason = CallEndReason.COMPLETE
hub.stop_ticker(session_id)
hub.to_station(session_id, state.card_received_event())
hub.to_station(session_id, StationState(snapshot=state.station_snapshot()))
hub.to_observers(session_id, KioState(kio=state.kio))
hub.to_trainee(session_id, CallEnded(reason=CallEndReason.COMPLETE))
if state.handoff_to_dds:
hub.to_trainee(session_id, CallEnded(reason=CallEndReason.COMPLETE))
hub.to_observers(session_id, state.snapshot())
else:
hub.to_observers(session_id, SessionEnded(reason=CallEndReason.COMPLETE))
hub.record(session_id, LessonEnded(state.ended_at, CallEndReason.COMPLETE.value))
await finish(session_id, state)
await end_session(session_id, state, CallEndReason.COMPLETE)
case "call.answer":
first_answer = state.started_at is None
if first_answer:
@ -362,15 +351,7 @@ async def _handle(session_id: UUID, state, event) -> None:
await release_score(session_id, state)
case "call.hangup":
if state.voice is not None:
await state.voice.close()
state.ended_at = now_utc()
state.end_reason = CallEndReason.HANGUP
hub.stop_ticker(session_id)
hub.to_trainee(session_id, CallEnded(reason=CallEndReason.HANGUP))
hub.to_observers(session_id, SessionEnded(reason=CallEndReason.HANGUP))
hub.record(session_id, LessonEnded(state.ended_at, CallEndReason.HANGUP.value))
await finish(session_id, state)
await end_session(session_id, state, CallEndReason.HANGUP)
def _start_voice(session_id: UUID, state, *, initial_statement: bool = True) -> None:

View file

@ -40,7 +40,6 @@ from app.domain.events import (
InstructorToServer,
ModeSet,
ReferenceStarted,
ScoreReady,
SessionEnded,
StationState,
)
@ -49,9 +48,9 @@ from app.domain.timers import TimerCode
from app.scenarios import store
from app.session.dds import prepare_queue
from app.session.hub import LEASE_FENCED_MESSAGE, hub
from app.session.finish import finish, override_score
from app.session.finish import end_session, override_score
from app.session.state import SessionState, now_utc
from app.session.store import LessonEnded, LessonIdentity, LessonRequest, NoteAdded, ScoreOverridden
from app.session.store import LessonIdentity, LessonRequest, NoteAdded, ScoreOverridden
from app.voice.models import get_voice_models
from app.voice.pipeline import FILLERS, prefetch
@ -331,28 +330,9 @@ async def _start(session_id: UUID, event, who=None) -> None:
async def _stop(session_id: UUID) -> None:
state = hub.get(session_id)
if state is None or state.ended:
if state is None:
return
state.ended_at = now_utc()
if state.exercise is Exercise.CARD and state.dispatched_card is None:
state.on_event("card.end")
state.end_reason = CallEndReason.INSTRUCTOR
if state.voice is not None:
await state.voice.close()
hub.stop_ticker(session_id)
hub.to_observers(session_id, SessionEnded(reason=CallEndReason.INSTRUCTOR))
if state.exercise in {Exercise.DDS, Exercise.CARD}:
if state.exercise is Exercise.DDS or state.handoff_to_dds and state.dispatched_card is not None:
hub.to_station(session_id, SessionEnded(reason=CallEndReason.INSTRUCTOR))
else:
hub.to_trainee(session_id, CallEnded(reason=CallEndReason.INSTRUCTOR))
if state.exercise is Exercise.DDS or state.handoff_to_dds and state.dispatched_card is not None:
hub.to_station(session_id, ScoreReady(session_id=session_id))
else:
hub.to_trainee(session_id, CallEnded(reason=CallEndReason.INSTRUCTOR))
hub.record(session_id, LessonEnded(state.ended_at, CallEndReason.INSTRUCTOR.value))
await finish(session_id, state)
await end_session(session_id, state, CallEndReason.INSTRUCTOR)
async def _command(session_id: UUID, event, who) -> None:

View file

@ -24,15 +24,12 @@ from app.domain.events import (
CommandAck,
ErrorEvent,
ErrorKind,
ScoreReady,
SessionEnded,
StationState,
StationToServer,
)
from app.domain.roles import Role
from app.session.finish import finish
from app.session.finish import end_session
from app.session.hub import LEASE_FENCED_MESSAGE, hub
from app.session.store import LessonEnded
log = logging.getLogger(__name__)
router = APIRouter()
@ -44,16 +41,6 @@ def _error(session_id: UUID, message: str) -> None:
hub.to_station(session_id, ErrorEvent(code=ErrorKind.UNSUPPORTED_EVENT, message=message))
async def _finish_dds(session_id: UUID, state) -> None:
ended_at = state.end(CallEndReason.COMPLETE)
hub.stop_ticker(session_id)
hub.to_station(session_id, SessionEnded(reason=CallEndReason.COMPLETE))
hub.to_observers(session_id, SessionEnded(reason=CallEndReason.COMPLETE))
hub.record(session_id, LessonEnded(ended_at, CallEndReason.COMPLETE.value))
await finish(session_id, state)
hub.to_station(session_id, ScoreReady(session_id=session_id))
async def _handle(session_id: UUID, state, event) -> None:
outcome = state.desk.apply(event, state)
if outcome.error is not None:
@ -62,7 +49,7 @@ async def _handle(session_id: UUID, state, event) -> None:
for item in outcome.events:
hub.to_station(session_id, item)
if outcome.finished:
await _finish_dds(session_id, state)
await end_session(session_id, state, CallEndReason.COMPLETE)
if not outcome.changed:
return
hub.to_station(session_id, StationState(snapshot=state.station_snapshot()))