Files
operating-room-monitor-server/docs/客户端手术通信接口说明.md
Kevin 6b3adb4ad8 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
2026-04-27 11:21:16 +08:00

457 lines
21 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 手术室监控服务:客户端手术通信接口说明
## 能力概览
| **能力** | **说明** |
| --------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **探活** | `GET /health`,用于检查进程和数据库状态,详见 5.1 节。 |
| **开始手术** | `POST /client/surgeries/start`,只有在开录确认成功后才返回 `200`。 |
| **结束手术** | `POST /client/surgeries/end`,只有在停录确认成功后才返回 `200`。 |
| **查询结果** | `GET /client/surgeries/{surgery_id}/result`,至少存在一条消耗明细时返回 `200`;否则返回 `503`,常见错误码为 `RESULT_NOT_READY`。 |
| **待确认播报** | `GET /client/surgeries/{surgery_id}/pending-confirmation`,拉取队首低置信度任务,返回话术文本和 MP3 Base64。 |
| **待确认答复** | `POST /client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve`,上传医生答复的 WAV 录音,服务端完成 ASR 后入账或关闭。该录音与播报音频无关。 |
## 1. 服务与基础信息
| **项目** | **说明** |
| ----------------------- | ------------------------------------------- |
| **协议** | `HTTP/HTTPS` |
| **端口** | `38080`,生产环境以实际入口为准 |
| **路由** | 无全局前缀;业务接口位于 `/client/...`,健康检查位于 `/health` |
| **`start` / `end` 请求体** | JSON |
| **`resolve` 请求体** | `multipart/form-data`,字段名为 `audio` |
| **在线文档** | `/docs``/redoc` |
## 2. 摄像头 ID 与 RTSP
RTSP 地址、账号、口令等由客户端对接工程师提供给服务端运维,运维再写入服务端环境。客户端只在 `POST /client/surgeries/start` 中传 `camera_ids`
| **camera_id** | **RTSP** | **备注** |
| ------------- | -------------------------------- | ------ |
| `or-cam-01` | `rtsp://...`(由现场或 NVR 文档整理后交给运维) | 术间、机位 |
| `or-cam-02` | `...` | `...` |
## 3. HTTP 路由一览
| **序号** | **方法** | **路径** | **说明** |
| ------ | ------ | ------------------------------------------------------------------------------- | ------- |
| 1 | `GET` | `/health` | 探活 |
| 2 | `POST` | `/client/surgeries/start` | 开始手术 |
| 3 | `POST` | `/client/surgeries/end` | 结束手术 |
| 4 | `GET` | `/client/surgeries/{surgery_id}/result` | 查询手术结果 |
| 5 | `GET` | `/client/surgeries/{surgery_id}/pending-confirmation` | 拉取待确认耗材 |
| 6 | `POST` | `/client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` | 提交医生答复 |
| 7 | `GET` | `/client/voice-terminals/{terminal_id}/assignment` | 可选:查询当前指派(调试或简易集成;**官方桌面客户端仅用 WebSocket** |
| 8 | `WS` | `/client/voice-terminals/ws?terminal_id=...` | 语音桌面终端长连接,接收开录/停录指派(**推荐** |
| | | | |
**术间与语音终端绑定(服务端配置)**
- **唯一配置源**:环境变量 **`OR_SITE_CONFIG_JSON_FILE`** 指向手术室 **站点 JSON**UTF-8须同时包含 `video_rtsp_urls``voice_or_room_bindings`(见仓库 `app/resources/or_site_config.sample.json`)。`voice_or_room_bindings` 为数组,每项含 `or_room_id``camera_ids``voice_terminal_id``camera_ids` 在数组内须唯一,`voice_terminal_id` 全局唯一。
- **`POST /client/surgeries/start`** 在 **HTTP 200 且开录已成功** 后:用请求体中的 `camera_ids``voice_or_room_bindings` 中解析终端(**精确匹配**术间 camera 集合,或 **开录路集为某术间 camera 集合的子集** 时匹配该术间);命中则向对应 `voice_terminal_id` 推送 **`action":"start"`**(并更新 assignment未配置站点文件、或数组为空、或未命中则仅打日志不影响 200。
- **`POST /client/surgeries/end`** 在停录 **HTTP 200** 后:向该手术会话记录的终端推送 **`action":"end"`**(并清除 assignment
- 推送 JSON 形如:`{"type":"voice_assignment","action":"start"|"end","surgery_id":"123456"}`
- **多 worker**:当前实现为进程内内存;多 Uvicorn worker 时需 sticky session 或 Redis 等另行同步。
## 4. 流程
### 4.1 时序图
```mermaid
sequenceDiagram
participant Client as 客户端
participant Server as 服务端
Client->>Server: POST /client/surgeries/start
Note over Client,Server: body: surgery_id, camera_ids, candidate_consumables
Server-->>Client: 200 accepted开录已确认
par 术中
loop 轮询结果
Client->>Server: GET .../result
Server-->>Client: 200 或 503 RESULT_NOT_READY
end
loop 轮询待确认
Client->>Server: GET .../pending-confirmation
Server-->>Client: 200 或 404
opt 有待确认
Client->>Client: 播放 prompt_audio_mp3_base64
Client->>Server: POST .../resolvemultipart audio
Server-->>Client: 200 accepted
end
end
end
Client->>Server: POST /client/surgeries/end
Server-->>Client: 200 accepted停录已确认
Client->>Server: GET .../result
Server-->>Client: 200持久化后可查时返回
```
### 4.2 状态图
```mermaid
flowchart LR
A[未开始] -->|start 200| B[录制 / 推理中]
B -->|result 200| C[有消耗数据可查]
B -->|pending 200| D[待确认]
D -->|resolve| B
B -->|end 200| E[已结束]
E -->|result 200| C
```
## 5. 接口详情
**路径参数 `surgery_id`**
| **约束项** | **说明** |
| ------- | --------- |
| **长度** | 固定 `6` |
| **字符集** | 仅数字 |
| **正则** | `^\d{6}$` |
**业务错误响应**
多数业务失败在 `4xx` 或 `5xx` 下返回如下 JSON
```
{
"detail": {
"code": "错误码字符串",
"message": "人类可读说明",
"surgery_id": "123456"
}
}
```
### 5.1 探活
**基本信息**
| **项目** | **内容** |
| ------- | --------- |
| **方法** | `GET` |
| **路径** | `/health` |
| **请求体** | 无 |
**响应说明**
| **HTTP** | **说明** | **响应体示例** |
| -------- | ----------- | ------------------------------------------------ |
| `200` | 进程正常且数据库可连通 | `{"status":"ok","database":"connected"}` |
| `503` | 数据库不可用(降级) | `{"status":"degraded","database":"unavailable"}` |
### 5.2 开始手术
**基本信息**
| **项目** | **内容** |
| ---------------- | --------------------------------- |
| **方法** | `POST` |
| **路径** | `/client/surgeries/start` |
| **Content-Type** | `application/json; charset=utf-8` |
**业务说明**
- 服务端会为 `camera_ids` 中的每个摄像头建立拉流与推理任务,只有在确认开录成功(如首帧就绪)后才返回 HTTP `200`
- `candidate_consumables` 为空时,服务端会展开为目录中的全部耗材名。
**请求体JSON**
| **字段** | **类型** | **必填** | **说明** |
| ----------------------- | ---------- | ------ | ----------------------------------- |
| `surgery_id` | `string` | 是 | 6 位数字 |
| `camera_ids` | `string[]` | 是 | 至少 1 个;必须与运维配置的摄像头 ID 完全一致,见第 2 节 |
| `candidate_consumables` | `string[]` | 否 | 非空时仅这些名称参与自动记账与待确认;缺省或 `[]` 时使用全部候选 |
**响应体200**
| **字段** | **类型** | **说明** |
| ------------ | -------- | ----------------- |
| `surgery_id` | `string` | 与请求一致 |
| `status` | `string` | 成功时通常为 `accepted` |
| `message` | `string` | 说明文案 |
**状态码**
| **HTTP** | **说明** |
| -------- | -------------------------------------------------------- |
| `200` | 开录已确认 |
| `422` | 参数校验失败,例如 `surgery_id` 非 6 位或 `camera_ids` 为空数组 |
| `503` | 开录未确认或录制子系统故障;`detail.code` 常见为 `RECORDING_CANNOT_START` |
**请求示例**
```
{
"surgery_id": "123456",
"camera_ids": ["or-cam-01", "or-cam-02"],
"candidate_consumables": ["纱布", "缝线", "止血钳"]
}
```
**响应示例200**
```
{
"surgery_id": "123456",
"status": "accepted",
"message": "摄像头录制已开始,手术已启动。"
}
```
### 5.3 结束手术
**基本信息**
| **项目** | **内容** |
| ---------------- | --------------------------------- |
| **方法** | `POST` |
| **路径** | `/client/surgeries/end` |
| **Content-Type** | `application/json; charset=utf-8` |
**业务说明**
停止该 `surgery_id` 关联的全部摄像头任务,只有在确认停录完成后才返回 `200`
**请求体JSON**
| **字段** | **类型** | **必填** | **说明** |
| ------------ | -------- | ------ | ------ |
| `surgery_id` | `string` | 是 | 6 位数字 |
**响应体200**
字段含义与 5.2 节一致,`message` 示例为 `摄像头录制已停止,手术已结束。`
**状态码**
| **HTTP** | **说明** |
| -------- | -------------------------------------------------- |
| `200` | 停录已确认 |
| `422` | 参数校验失败 |
| `503` | 停录未确认或故障;`detail.code` 常见为 `RECORDING_NOT_STOPPED` |
**请求示例**
```
{
"surgery_id": "123456"
}
```
### 5.4 查询手术结果
**基本信息**
| **项目** | **内容** |
| -------- | --------------------------------------- |
| **方法** | `GET` |
| **路径** | `/client/surgeries/{surgery_id}/result` |
| **路径参数** | `surgery_id` |
| **请求体** | 无 |
**业务说明**
- 仅当存在至少一条消耗明细时返回 `200`
- 无明细(包括已归档但零消耗)、手术未开始、未成功开录或当前尚不可查时,返回 `503`
- 上述 `503` 场景的常见错误码为 `RESULT_NOT_READY`
**响应体200**
| **字段** | **类型** | **说明** |
| ------------ | -------- | ----------------------- |
| `surgery_id` | `string` | 手术号 |
| `status` | `string` | 成功时通常为 `completed` |
| `message` | `string` | 说明 |
| `details` | `array` | 消耗明细列表,字段见下文 |
| `summary` | `array` | 按 `item_id` 汇总的结果,字段见下文 |
**`details[]` 元素**
| **字段** | **类型** | **说明** |
| ----------- | --------- | ---------------------------------- |
| `item_id` | `string` | 物品 ID有目录时多为产品编码否则通常与名称或模型类名一致 |
| `item_name` | `string` | 物品名称 |
| `qty` | `integer` | 本条记录数量,当前恒为 `1`;一次识别或一次人工确认只追加一条明细 |
| `doctor_id` | `string` | 记账关联的医生或系统标识 |
| `timestamp` | `string` | ISO 8601 时间(`date-time` |
**`summary[]` 元素**
| **字段** | **类型** | **说明** |
| ---------------- | --------- | -------------------------- |
| `item_id` | `string` | 与明细一致 |
| `item_name` | `string` | 名称,通常取该 `item_id` 首条明细中的名称 |
| `total_quantity` | `integer` | 该物品在本台手术中的合计数量,`>= 0` |
**状态码**
| **HTTP** | **说明** |
| -------- | ------------------------------ |
| `200` | 至少有一条明细 |
| `422` | `surgery_id` 路径不符合约束 |
| `503` | `RESULT_NOT_READY`,当前无可用明细或不可查 |
**响应示例200**
```
{
"surgery_id": "123456",
"status": "completed",
"message": "查询成功。",
"details": [
{
"item_id": "19246-3-14",
"item_name": "医用纱布敷料",
"qty": 1,
"doctor_id": "6611",
"timestamp": "2026-04-21T10:30:00+08:00"
}
],
"summary": [
{
"item_id": "19246-3-14",
"item_name": "医用纱布敷料",
"total_quantity": 1
}
]
}
```
### 5.5 拉取待确认耗材
**基本信息**
| **项目** | **内容** |
| -------- | ----------------------------------------------------- |
| **方法** | `GET` |
| **路径** | `/client/surgeries/{surgery_id}/pending-confirmation` |
| **路径参数** | `surgery_id` |
| **请求体** | 无 |
**业务说明**
- 返回当前 FIFO 队首的一条低置信度识别任务。
- `prompt_audio_mp3_base64` 与 `prompt_text` 内容一致,为标准 Base64 的 MP3 字符串(无换行)。
- 客户端解码后应按 `audio/mpeg` 播放。
**响应体200**
| **字段** | **类型** | **说明** |
| ------------------------- | -------- | ------------------------- |
| `surgery_id` | `string` | 手术号 |
| `confirmation_id` | `string` | 待确认项 ID提交 5.6 节接口时原样放入路径 |
| `prompt_text` | `string` | 播报或展示用语,与 MP3 内容一致 |
| `prompt_audio_mp3_base64` | `string` | MP3 的 Base64 |
| `options` | `array` | 候选项列表,字段见下文 |
| `model_top1_label` | `string` | 模型原始 Top1 类名,可能不在本台候选内 |
| `model_top1_confidence` | `number` | Top1 置信度 |
| `created_at` | `string` | 创建时间ISO 8601 |
**`options[]` 元素**
| **字段** | **类型** | **说明** |
| ------------ | -------- | ---------- |
| `label` | `string` | 展示给医生的选项名称 |
| `confidence` | `number` | 该选项对应的置信度 |
**状态码**
| **HTTP** | **说明** |
| -------- | ----------------------------------------------------- |
| `200` | 当前有一条待确认 |
| `404` | 无待确认或手术未活跃;常见错误码为 `NO_PENDING_CONFIRMATION` |
| `422` | 例如话术为空导致无法 TTS错误码见响应 `TTS_TEXT_EMPTY` |
| `503` | 语音服务未配置或 TTS 失败;例如 `BAIDU_NOT_CONFIGURED``TTS_ERROR` |
### 5.6 提交待确认结果(医生语音)
**基本信息**
| **项目** | **内容** |
| ---------------- | ------------------------------------------------------------------------------- |
| **方法** | `POST` |
| **路径** | `/client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` |
| **Content-Type** | `multipart/form-data` |
**路径参数**
| **参数** | **约束** | **说明** |
| ----------------- | ---------- | -------------------------------- |
| `surgery_id` | 6 位数字 | 同 5.0 节 |
| `confirmation_id` | 长度 1 到 128 | 与 5.5 节响应中的 `confirmation_id` 一致 |
**请求体multipart**
| **字段名** | **类型** | **必填** | **说明** |
| ------- | ------ | ------ | ------------------------------------------------------ |
| `audio` | `file` | 是 | 单个 `.wav` 文件;建议使用 16 kHz 单声道 PCM `.wav` 扩展名会返回 `422` |
**业务说明**
音频上传至对象存储后执行 ASR 和候选解析。若识别为确认某个候选项,则记一条消耗;若识别为否认全部候选,则不记消耗。
**响应体200**
| **字段** | **类型** | **说明** |
| ------------------ | ---------------- | ------------------------ |
| `surgery_id` | `string` | 手术号 |
| `confirmation_id` | `string` | 待确认 ID |
| `status` | `string` | 成功时为 `accepted` |
| `message` | `string` | 说明 |
| `resolved_label` | `string \| null` | 确认后的耗材名称;否认全部候选时为 `null` |
| `rejected` | `boolean` | 是否否认全部候选,不记消耗时为 `true` |
| `asr_text` | `string \| null` | 语音识别文本 |
| `audio_object_key` | `string \| null` | 对象存储中的原始 WAV 键,便于追溯 |
**状态码**
| **HTTP** | **说明** |
| -------- | ------------------------------------------------------------------------------------- |
| `200` | 已受理并完成解析 |
| `404` | 确认项不存在或手术未活跃;例如 `CONFIRMATION_NOT_FOUND` |
| `409` | 当前确认项已处理过;例如 `CONFIRMATION_ALREADY_RESOLVED` |
| `422` | 空文件、非 `.wav``VOICE_AUDIO_INVALID`、ASR/解析失败等,具体错误码见响应 |
| `503` | MinIO、百度等依赖不可用例如 `MINIO_NOT_CONFIGURED``MINIO_UPLOAD_FAILED``BAIDU_NOT_CONFIGURED` |
**cURL 示例**
```
curl -sS -X POST \
"http://<主机>:38080/client/surgeries/123456/pending-confirmation/<confirmation_id>/resolve" \
-F "audio=@/path/to/voice.wav;type=audio/wav"
```
### 5.7 语音终端 assignmentHTTP可选
**路径** `GET /client/voice-terminals/{terminal_id}/assignment`
仓库内 **手术室耗材语音确认桌面客户端** 仅通过 **§5.8 WebSocket** 接收指派,**不调用**本接口。此处供运维脚本、未实现 WS 的第三方临时拉取 `active_surgery_id`
**响应 200**
| **字段** | **类型** | **说明** |
| -------- | -------- | -------- |
| `voice_terminal_id` | `string` | 与路径一致 |
| `active_surgery_id` | `string \| null` | 当前指派手术 6 位号;无指派时为 `null` |
### 5.8 语音终端 WebSocket
**路径** `GET ws://<主机>:<端口>/client/voice-terminals/ws?terminal_id=<终端ID>`HTTPS 部署时使用 `wss://`
**说明**
- 连接成功后,若服务端已有该终端的 assignment会立即收到一条 **`action":"start"`** 的 JSON与下文推送格式一致
- 术中由服务端在 **`start` / `end` 成功后** 向已连接终端推送 JSON`{"type":"voice_assignment","action":"start"|"end","surgery_id":"123456"}`
- 客户端可发送任意文本作心跳;服务端当前仅依赖 WebSocket 协议级 ping由网关或客户端库实现