This commit is contained in:
Kevin
2026-04-28 10:41:48 +08:00
parent 482b016872
commit 15884bd68e
60 changed files with 2092 additions and 1994 deletions

View File

@@ -1,11 +1,13 @@
"""语音桌面终端assignment 状态、WebSocket 推送与 HTTP 轮询兜底。"""
"""语音桌面终端assignment 状态、WebSocket 推送与 HTTP 拉取兜底。"""
from __future__ import annotations
import asyncio
import json
from asyncio import Lock
from collections import defaultdict
from collections.abc import Callable
from collections.abc import Awaitable, Callable
from typing import Any
from fastapi import WebSocket
from loguru import logger
@@ -14,6 +16,8 @@ from starlette.websockets import WebSocketDisconnect
from app.config import Settings
from app.services.voice_terminal_binding import VoiceTerminalBindingIndex
PendingHeadFetcher = Callable[[str], Awaitable[Any]]
async def assign_voice_terminal_after_recording_started(
hub: VoiceTerminalHub,
@@ -45,12 +49,18 @@ async def assign_voice_terminal_after_recording_started(
class VoiceTerminalHub:
"""进程内终端连接与当前手术分配(多 worker 需另行同步)。"""
def __init__(self, settings: Settings) -> None:
def __init__(
self,
settings: Settings,
*,
pending_head_fetcher: PendingHeadFetcher | None = None,
) -> None:
cfg = settings.load_or_site_config()
self._bindings = cfg.voice_bindings if cfg else None
self._assignments: dict[str, str] = {}
self._lock = Lock()
self._connections: dict[str, set[WebSocket]] = defaultdict(set)
self._pending_head_fetcher = pending_head_fetcher
@property
def bindings(self) -> VoiceTerminalBindingIndex | None:
@@ -81,6 +91,15 @@ class VoiceTerminalHub:
tid,
surgery_id,
)
self.schedule_notify_pending_head(tid, surgery_id)
def schedule_notify_pending_head(self, terminal_id: str, surgery_id: str) -> None:
"""异步推送队首(含 TTS不阻塞调用方。"""
tid = terminal_id.strip()
sid = (surgery_id or "").strip()
if not tid or not sid:
return
asyncio.create_task(self._notify_pending_head_safe(tid, sid))
async def notify_end(self, terminal_id: str | None, surgery_id: str) -> None:
if not terminal_id:
@@ -103,6 +122,50 @@ class VoiceTerminalHub:
surgery_id,
)
async def notify_pending_head(self, terminal_id: str, surgery_id: str) -> None:
"""向终端推送当前 FIFO 队首(含 TTS无队首时推送 voice_pending_empty。"""
fetcher = self._pending_head_fetcher
tid = terminal_id.strip()
sid = (surgery_id or "").strip()
if not fetcher or not tid or not sid:
return
try:
payload = await fetcher(sid)
except Exception as exc:
logger.warning(
"voice_pending 构建失败 surgery_id={} terminal_id={}: {}",
sid,
tid,
exc,
)
return
if payload is None:
await self._broadcast(
tid,
{"type": "voice_pending_empty", "surgery_id": sid},
)
return
try:
data = payload.model_dump(mode="json")
except Exception as exc:
logger.warning("voice_pending 序列化失败 surgery_id={}: {}", sid, exc)
return
data["type"] = "voice_pending"
await self._broadcast(tid, data)
async def _notify_pending_head_safe(
self, terminal_id: str, surgery_id: str
) -> None:
try:
await self.notify_pending_head(terminal_id, surgery_id)
except Exception as exc:
logger.warning(
"后台 voice_pending 推送失败 terminal_id={} surgery_id={}: {}",
terminal_id,
surgery_id,
exc,
)
async def handle_websocket(self, websocket: WebSocket, terminal_id: str) -> None:
tid = terminal_id.strip()
if not tid:
@@ -125,6 +188,7 @@ class VoiceTerminalHub:
ensure_ascii=False,
)
)
self.schedule_notify_pending_head(tid, sid)
# 不能用 receive_text():桌面端 websocket-client 会发 ping/二进制控制帧,
# ASGI 可能呈现为无 "text" 的 websocket.receivereceive_text 会 KeyError 并掐断连接。
while True: