feat: 站点 JSON、语音终端 WebSocket 指派与客户端联调

- 用 OR_SITE_CONFIG_JSON_FILE 统一术间配置(video_rtsp_urls + voice_or_room_bindings)
- VoiceTerminalHub:assignment、WS 推送与 HTTP 查询;开录/停录后 notify
- 一键联调 orchestrate-and-start 与 /client/surgeries/start 共用指派逻辑,修复 demo 路径不发 WS
- 语音桌面端:SIGINT 退出、shutdown 清理、仅 WS 指派、固定 pending 轮询间隔、界面仅保留录音时长
- 新增/调整契约与绑定测试,文档与示例配置同步

Made-with: Cursor
This commit is contained in:
Kevin
2026-04-27 11:21:16 +08:00
parent 4c3f9a367b
commit 6b3adb4ad8
36 changed files with 1194 additions and 162 deletions

View File

@@ -54,7 +54,7 @@ class BackendResolver:
return VideoBackendKind.RTSP
def rtsp_url_for_camera(self, camera_id: str) -> str:
# Re-read on each use so VIDEO_RTSP_URLS_JSON_FILE can be hot-updated (e.g. dev orchestrator).
# Re-read on each use so OR_SITE_CONFIG_JSON_FILE can be hot-updated (e.g. dev orchestrator).
m = self._s.video_rtsp_url_map()
if camera_id in m:
return m[camera_id]
@@ -67,8 +67,8 @@ class BackendResolver:
f"video_rtsp_url_template missing placeholder: {exc}"
) from exc
raise ValueError(
f"No RTSP URL for camera_id={camera_id!r}: set VIDEO_RTSP_URLS_JSON_FILE, "
f"VIDEO_RTSP_URLS_JSON, or VIDEO_RTSP_URL_TEMPLATE"
f"No RTSP URL for camera_id={camera_id!r}: set OR_SITE_CONFIG_JSON_FILE "
f"(video_rtsp_urls) or VIDEO_RTSP_URL_TEMPLATE"
)
def rtsp_url_after_hikvision_login(self, camera_id: str) -> str:

View File

@@ -223,7 +223,17 @@ class CameraSessionManager:
await self.stop_surgery(surgery_id, require_active=True)
raise
async def stop_surgery(self, surgery_id: str, *, require_active: bool = True) -> None:
def set_voice_terminal_id(self, surgery_id: str, terminal_id: str | None) -> None:
"""开录成功后写入,供停录时向对应桌面终端推送 end。"""
run = self._registry.get_running(surgery_id)
if run is None:
return
tid = (terminal_id or "").strip()
run.state.voice_terminal_id = tid or None
async def stop_surgery(
self, surgery_id: str, *, require_active: bool = True
) -> str | None:
run = await self._registry.unregister(surgery_id)
if run is None:
if require_active:
@@ -231,8 +241,9 @@ class CameraSessionManager:
"RECORDING_NOT_STOPPED",
"停录未能完成:当前没有该手术的活跃录制会话。",
)
return
return None
voice_tid = run.state.voice_terminal_id
run.stop_event.set()
results = await asyncio.gather(*run.tasks, return_exceptions=True)
for res in results:
@@ -255,6 +266,7 @@ class CameraSessionManager:
append_consumption_log_summary(surgery_id, totals)
print_consumption_summary_markdown(totals)
await self._archive.persist_or_archive(surgery_id, details)
return voice_tid
# ------------------------------------------------------------------
# PendingConfirmationStore 协议委托

View File

@@ -85,6 +85,8 @@ class SurgerySessionState:
last_voice_error: str | None = None
#: ``start_surgery`` 创建会话时的 ``time.time()``,用于日志中「相对开录的流逝时间」。
surgery_started_wall: float | None = None
#: 术间绑定配置解析出的语音桌面终端 ID停录时用于推送 end。
voice_terminal_id: str | None = None
@dataclass