|
|
@@ -0,0 +1,596 @@
|
|
|
+# 统一入口客户端 MVP 接口契约
|
|
|
+
|
|
|
+> **受众:** 前端工程师(`emoon-terminal-client`)、终端厂商联调人员
|
|
|
+> **版本:** v1.0-mvp
|
|
|
+> **基准:** `emoon-ai-openplatform-api-v1.2.openapi.yaml`
|
|
|
+> **关联文档:** `医梦AI中台对外接口设计文档_v1.2_正式联调基准版.md`、`统一入口客户端技术设计文档_v1.0.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 接口总览
|
|
|
+
|
|
|
+### 1.1 终端调用白名单
|
|
|
+
|
|
|
+前端/终端 **只允许** 调用以下接口:
|
|
|
+
|
|
|
+| 端点 | 方法 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| `/api/v1/devices/register` | POST | 设备注册 |
|
|
|
+| `/api/v1/devices/{deviceId}/heartbeat` | POST | 心跳上报 |
|
|
|
+| `/api/v1/devices/{deviceId}/scene` | GET | 获取场景配置 |
|
|
|
+| `/api/v1/agent/chat/stream` | POST | 流式对话(SSE) |
|
|
|
+| `/api/v1/files/upload` | POST | 文件上传 |
|
|
|
+| `/api/v1/files/{fileId}` | GET | 查询文件 |
|
|
|
+| `/api/v1/cards/{cardInstanceId}` | GET | 查询卡片实例 |
|
|
|
+| `/api/v1/cards/{cardInstanceId}/actions/{actionName}` | POST | 提交卡片动作 |
|
|
|
+| `/api/v1/devices/{deviceId}/events` | POST | 设备事件上报 |
|
|
|
+| `/api/v1/devices/{deviceId}/commands/poll` | GET | 设备命令轮询 |
|
|
|
+| `/api/v1/devices/{deviceId}/commands/{commandId}/ack` | POST | 设备命令确认 |
|
|
|
+
|
|
|
+### 1.2 前端禁止调用
|
|
|
+
|
|
|
+| 禁止调用的接口 | 原因 |
|
|
|
+|----------------|------|
|
|
|
+| Dify API(任何) | 前端不决定调用哪个 Dify App,后端 AgentRouter 统一路由 |
|
|
|
+| `/api/v1/tools/{toolName}/invoke` | HIS 写操作必须经过 Card Runtime + AgentActionOrchestrator |
|
|
|
+| 舌诊底层接口 | 舌象图片必须先通过 `/files/upload` 上传,由后端 TongueDiagnosisAdapter 统一调用 |
|
|
|
+| `/api/v1/meter/events`(写接口) | 计量事件由后端内部产生,前端不直接写计量 |
|
|
|
+| 任何 HIS/EMR 直连 | 所有 HIS 交互通过卡片动作间接完成 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 鉴权
|
|
|
+
|
|
|
+### 2.1 新终端端点(HMAC-SHA256)
|
|
|
+
|
|
|
+以下端点使用新鉴权(`AuthContextHolder`):
|
|
|
+
|
|
|
+- `/devices/register`
|
|
|
+- `/devices/{deviceId}/heartbeat`
|
|
|
+- `/devices/{deviceId}/scene`
|
|
|
+- `/agent/chat/stream`
|
|
|
+- `/files/upload`
|
|
|
+- `/cards/{cardInstanceId}`
|
|
|
+- `/cards/{cardInstanceId}/actions/{actionName}`
|
|
|
+- `/devices/{deviceId}/commands/poll`
|
|
|
+- `/devices/{deviceId}/commands/{commandId}/ack`
|
|
|
+
|
|
|
+请求头:
|
|
|
+
|
|
|
+```http
|
|
|
+X-Emoon-Access-Key: <access-key>
|
|
|
+X-Emoon-Timestamp: <unix-millis>
|
|
|
+X-Emoon-Nonce: <random-uuid>
|
|
|
+X-Emoon-Signature: <base64(hmac-sha256)>
|
|
|
+```
|
|
|
+
|
|
|
+签名算法(输出为 Base64 编码的 HMAC-SHA256):
|
|
|
+
|
|
|
+```text
|
|
|
+canonical = METHOD + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + sha256(body)
|
|
|
+signature = Base64(HMAC-SHA256(secretKey, canonical))
|
|
|
+```
|
|
|
+
|
|
|
+> 详情见 `docs/接口文档/emoon-ai-openplatform-api-v1.2.openapi.yaml` securitySchemes 章节。
|
|
|
+
|
|
|
+### 2.2 旧端点(兼容)
|
|
|
+
|
|
|
+`/agent/chat`(同步)、`/conversation/*` 沿用旧 HMAC(`X-Emoon-Public-Key` + `X-Emoon-Sign`),终端 MVP 不使用这些端点。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. 启动流程(设备注册 → 场景加载)
|
|
|
+
|
|
|
+```text
|
|
|
+1. 终端启动 → POST /devices/register
|
|
|
+2. 拿到 deviceId → POST /devices/{deviceId}/heartbeat(定时 30s)
|
|
|
+3. 拿到 deviceId → GET /devices/{deviceId}/scene
|
|
|
+4. 渲染首页 UI
|
|
|
+```
|
|
|
+
|
|
|
+### 3.1 设备注册
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X POST https://api.demo.emoon.local/api/v1/devices/register \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -H "X-Emoon-Access-Key: <access-key>" \
|
|
|
+ -H "X-Emoon-Timestamp: 1717000000000" \
|
|
|
+ -H "X-Emoon-Nonce: <uuid>" \
|
|
|
+ -H "X-Emoon-Signature: <signature>" \
|
|
|
+ -d '{
|
|
|
+ "hospitalId": "H001",
|
|
|
+ "deviceCode": "EMOON-KIOSK-001",
|
|
|
+ "deviceType": "self_service_kiosk",
|
|
|
+ "vendor": "emoon",
|
|
|
+ "model": "kiosk-v2",
|
|
|
+ "clientVersion": "0.1.0",
|
|
|
+ "capabilities": {
|
|
|
+ "touch": true,
|
|
|
+ "camera": true,
|
|
|
+ "id_card_ocr": true,
|
|
|
+ "file_upload": true
|
|
|
+ },
|
|
|
+ "location": {
|
|
|
+ "floor": "1F",
|
|
|
+ "area": "门诊大厅"
|
|
|
+ }
|
|
|
+ }'
|
|
|
+```
|
|
|
+
|
|
|
+响应:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "data": {
|
|
|
+ "deviceId": "EMOON-KIOSK-001",
|
|
|
+ "deviceSecret": "sk-xxx...",
|
|
|
+ "activateStatus": "activated",
|
|
|
+ "admissionLevel": "A",
|
|
|
+ "reason": null
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+`activateStatus` 含义:
|
|
|
+
|
|
|
+| 值 | 说明 |
|
|
|
+|----|------|
|
|
|
+| `activated` | 准入通过,可进入核心业务 |
|
|
|
+| `pending` | 待人工准入,只能心跳 |
|
|
|
+| `rejected` | 准入拒绝,只能联系运维 |
|
|
|
+
|
|
|
+### 3.2 心跳
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X POST https://api.demo.emoon.local/api/v1/devices/EMOON-KIOSK-001/heartbeat \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -H "X-Emoon-Access-Key: <access-key>" \
|
|
|
+ -H "X-Emoon-Timestamp: 1717000000000" \
|
|
|
+ -H "X-Emoon-Nonce: <uuid>" \
|
|
|
+ -H "X-Emoon-Signature: <signature>" \
|
|
|
+ -d '{
|
|
|
+ "clientVersion": "0.1.0",
|
|
|
+ "status": "online",
|
|
|
+ "currentSceneCode": "outpatient_kiosk"
|
|
|
+ }'
|
|
|
+```
|
|
|
+
|
|
|
+响应:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "data": {
|
|
|
+ "accepted": true,
|
|
|
+ "serverTime": 1717000000000
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.3 获取场景配置
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X GET https://api.demo.emoon.local/api/v1/devices/EMOON-KIOSK-001/scene \
|
|
|
+ -H "X-Emoon-Access-Key: <access-key>" \
|
|
|
+ -H "X-Emoon-Timestamp: 1717000000000" \
|
|
|
+ -H "X-Emoon-Nonce: <uuid>" \
|
|
|
+ -H "X-Emoon-Signature: <signature>"
|
|
|
+```
|
|
|
+
|
|
|
+响应:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "data": {
|
|
|
+ "deviceId": "EMOON-KIOSK-001",
|
|
|
+ "deviceType": "self_service_kiosk",
|
|
|
+ "sceneCode": "outpatient_kiosk",
|
|
|
+ "homeTemplate": "kiosk_home_v1",
|
|
|
+ "defaultAgent": "opd-guide-agent",
|
|
|
+ "allowedAgents": [
|
|
|
+ "opd-guide-agent",
|
|
|
+ "opd-triage-agent",
|
|
|
+ "opd-registration-agent",
|
|
|
+ "tongue-diagnosis-agent"
|
|
|
+ ],
|
|
|
+ "blockedIntents": [],
|
|
|
+ "capabilities": ["touch", "camera", "id_card_ocr", "file_upload"],
|
|
|
+ "allowedCards": [
|
|
|
+ "department-selection",
|
|
|
+ "doctor-selection",
|
|
|
+ "time-slot-selection",
|
|
|
+ "confirm-appointment",
|
|
|
+ "appointment-success",
|
|
|
+ "tongue-capture"
|
|
|
+ ]
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 流式对话
|
|
|
+
|
|
|
+### 4.1 请求
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -N -X POST https://api.demo.emoon.local/api/v1/agent/chat/stream \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -H "Accept: text/event-stream" \
|
|
|
+ -H "X-Emoon-Access-Key: <access-key>" \
|
|
|
+ -H "X-Emoon-Timestamp: 1717000000000" \
|
|
|
+ -H "X-Emoon-Nonce: <uuid>" \
|
|
|
+ -H "X-Emoon-Signature: <signature>" \
|
|
|
+ -d '{
|
|
|
+ "deviceContext": {
|
|
|
+ "deviceId": "EMOON-KIOSK-001",
|
|
|
+ "deviceType": "self_service_kiosk"
|
|
|
+ },
|
|
|
+ "message": {
|
|
|
+ "type": "text",
|
|
|
+ "text": "我头疼三天,想挂号",
|
|
|
+ "clientMessageId": "m-reg-001"
|
|
|
+ }
|
|
|
+ }'
|
|
|
+```
|
|
|
+
|
|
|
+> **`agentId` 是可选的。** 统一入口客户端不传 `agentId`,由后端 AgentRouter 根据设备类型、场景配置、用户输入和任务状态自动决定路由到哪个智能体。
|
|
|
+
|
|
|
+### 4.2 SSE 事件类型
|
|
|
+
|
|
|
+| 事件 | 说明 | MVP 必达 |
|
|
|
+|------|------|----------|
|
|
|
+| `message_delta` | 文本增量 `{"text":"好的,我来帮您..."}` | ✅ |
|
|
|
+| `message_completed` | 文本完成 `{"messageId":"msg_xxx","conversationId":"conv_xxx"}` | ✅ |
|
|
|
+| `card_created` | 卡片已创建 `{"cardInstanceId":"card_xxx","cardKey":"department-selection"}` | ✅ |
|
|
|
+| `task_updated` | 任务状态变更 `{"taskType":"REGISTRATION","currentStep":"COLLECT_SYMPTOM","status":"ACTIVE"}` | ✅ |
|
|
|
+| `usage_reported` | 用量事件(P1) | ❌ |
|
|
|
+| `error` | 错误 `{"code":"xxx","message":"..."}` | ✅ |
|
|
|
+| `completed` | 本次 SSE 结束 `{"conversationId":"conv_xxx"}` | ✅ |
|
|
|
+
|
|
|
+### 4.3 挂号流程示例 SSE 序列
|
|
|
+
|
|
|
+```text
|
|
|
+event: task_updated
|
|
|
+data: {"taskType":"REGISTRATION","currentStep":"COLLECT_SYMPTOM","status":"ACTIVE","taskId":"task_001"}
|
|
|
+
|
|
|
+event: message_delta
|
|
|
+data: {"text":"好的,我来帮您走挂号流程。请先简单描述您的症状或选择目标科室。"}
|
|
|
+
|
|
|
+event: message_completed
|
|
|
+data: {"messageId":"msg_001","conversationId":"conv_001"}
|
|
|
+
|
|
|
+event: card_created
|
|
|
+data: {"cardInstanceId":"card_001","cardKey":"department-selection","taskId":"task_001"}
|
|
|
+
|
|
|
+event: completed
|
|
|
+data: {"conversationId":"conv_001"}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.4 断线恢复
|
|
|
+
|
|
|
+1. 客户端记录 `conversationId`
|
|
|
+2. 调用 `GET /api/v1/conversation/{conversationId}` 获取最终消息
|
|
|
+3. 调用 `GET /api/v1/cards/{cardInstanceId}` 查询当前卡片状态(如有)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 卡片动作
|
|
|
+
|
|
|
+### 5.1 查询卡片实例
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X GET https://api.demo.emoon.local/api/v1/cards/card_001 \
|
|
|
+ -H "X-Emoon-Access-Key: <access-key>" \
|
|
|
+ -H "X-Emoon-Timestamp: 1717000000000" \
|
|
|
+ -H "X-Emoon-Nonce: <uuid>" \
|
|
|
+ -H "X-Emoon-Signature: <signature>"
|
|
|
+```
|
|
|
+
|
|
|
+响应:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "data": {
|
|
|
+ "cardInstanceId": "card_001",
|
|
|
+ "cardKey": "department-selection",
|
|
|
+ "status": "active",
|
|
|
+ "expiresAt": "2026-06-01T10:00:00",
|
|
|
+ "cardData": {
|
|
|
+ "departments": [
|
|
|
+ {"deptId": "neurology", "name": "神经内科", "reason": "头痛、头晕、肢体麻木等症状可先咨询"},
|
|
|
+ {"deptId": "respiratory", "name": "呼吸内科", "reason": "咳嗽、发热、气喘等症状可先咨询"}
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ "actions": [
|
|
|
+ {"actionName": "select_department", "label": "选择科室"}
|
|
|
+ ]
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2 提交卡片动作
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X POST https://api.demo.emoon.local/api/v1/cards/card_001/actions/select_department \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -H "X-Emoon-Access-Key: <access-key>" \
|
|
|
+ -H "X-Emoon-Timestamp: 1717000000000" \
|
|
|
+ -H "X-Emoon-Nonce: <uuid>" \
|
|
|
+ -H "X-Emoon-Signature: <signature>" \
|
|
|
+ -d '{
|
|
|
+ "idempotencyKey": "idem-001",
|
|
|
+ "confirm": true,
|
|
|
+ "payload": {
|
|
|
+ "departmentId": "neurology"
|
|
|
+ }
|
|
|
+ }'
|
|
|
+```
|
|
|
+
|
|
|
+> **幂等键(`idempotencyKey`)是必填的。** 同一动作重复提交相同幂等键返回首次结果,不会重复扣费或创建重复号源。
|
|
|
+
|
|
|
+响应:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "data": {
|
|
|
+ "status": "completed",
|
|
|
+ "actionId": "action_001",
|
|
|
+ "nextCard": {
|
|
|
+ "cardInstanceId": "card_002",
|
|
|
+ "cardKey": "doctor-selection"
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 5.3 MVP 挂号完整动作序列
|
|
|
+
|
|
|
+```text
|
|
|
+1. 用户说"我要挂号" → SSE 返回 department-selection 卡片 (card_001)
|
|
|
+2. 用户选择科室 → POST /cards/card_001/actions/select_department → 返回 doctor-selection 卡片 (card_002)
|
|
|
+3. 用户选择医生 → POST /cards/card_002/actions/select_doctor → 返回 time-slot-selection 卡片 (card_003)
|
|
|
+4. 用户选择时间 → POST /cards/card_003/actions/select_time_slot → 返回 confirm-appointment 卡片 (card_004)
|
|
|
+5. 用户确认 → POST /cards/card_004/actions/confirm_appointment → 返回 appointment-success 卡片 (card_005)
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 文件上传
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X POST https://api.demo.emoon.local/api/v1/files/upload \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -H "X-Emoon-Access-Key: <access-key>" \
|
|
|
+ -H "X-Emoon-Timestamp: 1717000000000" \
|
|
|
+ -H "X-Emoon-Nonce: <uuid>" \
|
|
|
+ -H "X-Emoon-Signature: <signature>" \
|
|
|
+ -d '{
|
|
|
+ "businessType": "TONGUE_IMAGE",
|
|
|
+ "sha256": "abc123...",
|
|
|
+ "deviceId": "EMOON-KIOSK-001"
|
|
|
+ }'
|
|
|
+```
|
|
|
+
|
|
|
+允许的 `businessType`:
|
|
|
+
|
|
|
+| 值 | 说明 |
|
|
|
+|----|------|
|
|
|
+| `ID_CARD` | 身份证图片 |
|
|
|
+| `TONGUE_IMAGE` | 舌象采集图片 |
|
|
|
+| `REPORT_IMAGE` | 报告图片 |
|
|
|
+| `AUDIO` | 音频文件 |
|
|
|
+
|
|
|
+响应:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "data": {
|
|
|
+ "fileId": "file_001",
|
|
|
+ "businessType": "TONGUE_IMAGE",
|
|
|
+ "retentionPolicy": "episode",
|
|
|
+ "sensitiveLevel": "P3",
|
|
|
+ "status": "available"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **注意:** MVP 阶段文件上传返回 `fileId` 后,前端将 `fileId` 传入后续 `/agent/chat/stream` 的 `attachments` 字段中。舌诊底层接口由后端 `TongueDiagnosisAdapter` 统一调用,前端不直连。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. 机器人命令轮询
|
|
|
+
|
|
|
+机器人端需要异步接收导航命令:
|
|
|
+
|
|
|
+### 7.1 轮询命令
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X GET https://api.demo.emoon.local/api/v1/devices/EMOON-ROBOT-001/commands/poll \
|
|
|
+ -H "X-Emoon-Access-Key: <access-key>" \
|
|
|
+ -H "X-Emoon-Timestamp: 1717000000000" \
|
|
|
+ -H "X-Emoon-Nonce: <uuid>" \
|
|
|
+ -H "X-Emoon-Signature: <signature>"
|
|
|
+```
|
|
|
+
|
|
|
+响应:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "data": {
|
|
|
+ "commands": [
|
|
|
+ {
|
|
|
+ "commandId": "cmd_001",
|
|
|
+ "commandType": "NAVIGATE_TO_LOCATION",
|
|
|
+ "payload": {
|
|
|
+ "locationId": "lab",
|
|
|
+ "routeText": "从门诊大厅向左走 20 米后右转,检验科在左手边"
|
|
|
+ },
|
|
|
+ "status": "PENDING",
|
|
|
+ "expireAt": "2026-06-01T10:05:00"
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 7.2 确认命令
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X POST https://api.demo.emoon.local/api/v1/devices/EMOON-ROBOT-001/commands/cmd_001/ack \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -H "X-Emoon-Access-Key: <access-key>" \
|
|
|
+ -H "X-Emoon-Timestamp: 1717000000000" \
|
|
|
+ -H "X-Emoon-Nonce: <uuid>" \
|
|
|
+ -H "X-Emoon-Signature: <signature>" \
|
|
|
+ -d '{
|
|
|
+ "status": "success",
|
|
|
+ "result": {}
|
|
|
+ }'
|
|
|
+```
|
|
|
+
|
|
|
+响应:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "data": {
|
|
|
+ "acknowledged": "true"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. 设备事件上报
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -X POST https://api.demo.emoon.local/api/v1/devices/EMOON-KIOSK-001/events \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -H "X-Emoon-Access-Key: <access-key>" \
|
|
|
+ -H "X-Emoon-Timestamp: 1717000000000" \
|
|
|
+ -H "X-Emoon-Nonce: <uuid>" \
|
|
|
+ -H "X-Emoon-Signature: <signature>" \
|
|
|
+ -d '{
|
|
|
+ "eventId": "evt-001",
|
|
|
+ "eventType": "TOUCH_INTERACTION",
|
|
|
+ "eventPayload": {"screen": "home", "action": "tap_register"},
|
|
|
+ "occurredAt": "2026-06-01T09:30:00"
|
|
|
+ }'
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 9. 错误响应格式
|
|
|
+
|
|
|
+所有接口统一使用 `R<T>` 包装:
|
|
|
+
|
|
|
+响应:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "data": {
|
|
|
+ "accepted": "true",
|
|
|
+ "eventId": "evt-001"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 9. 错误响应格式
|
|
|
+
|
|
|
+所有接口统一使用 `R<T>` 包装:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 401,
|
|
|
+ "msg": "INVALID_SIGNATURE",
|
|
|
+ "data": null
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+常见错误码:
|
|
|
+
|
|
|
+| code | 说明 |
|
|
|
+|------|------|
|
|
|
+| `401` | 鉴权失败(签名错误、时间戳过期、nonce 重复) |
|
|
|
+| `403` | 设备准入拒绝、能力不允许 |
|
|
|
+| `404` | 设备/卡片/文件不存在 |
|
|
|
+| `409` | 幂等键重复(返回首次结果) |
|
|
|
+| `422` | 参数校验失败 |
|
|
|
+| `500` | 服务端错误 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 10. 前端快速启动流程
|
|
|
+
|
|
|
+```text
|
|
|
+┌───────────────┐
|
|
|
+│ 1. 加载配置 │ 读取 hospitalId、accessKey
|
|
|
+└───────┬───────┘
|
|
|
+ ↓
|
|
|
+┌───────────────┐
|
|
|
+│ 2. 设备注册 │ POST /devices/register
|
|
|
+└───────┬───────┘
|
|
|
+ ↓ 拿到 deviceId
|
|
|
+┌───────────────┐
|
|
|
+│ 3. 开始心跳 │ setInterval 30s → POST /devices/{deviceId}/heartbeat
|
|
|
+└───────┬───────┘
|
|
|
+ ↓
|
|
|
+┌───────────────┐
|
|
|
+│ 4. 获取场景 │ GET /devices/{deviceId}/scene
|
|
|
+└───────┬───────┘
|
|
|
+ ↓ 拿到 homeTemplate、allowedAgents、allowedCards
|
|
|
+┌───────────────┐
|
|
|
+│ 5. 渲染首页 │ 根据 sceneCode + homeTemplate 加载对应 UI
|
|
|
+└───────┬───────┘
|
|
|
+ ↓ 用户输入
|
|
|
+┌───────────────┐
|
|
|
+│ 6. 流式对话 │ POST /agent/chat/stream (SSE)
|
|
|
+└───────┬───────┘
|
|
|
+ ↓ 收到 card_created 事件
|
|
|
+┌───────────────┐
|
|
|
+│ 7. 渲染卡片 │ GET /cards/{cardInstanceId}
|
|
|
+└───────┬───────┘
|
|
|
+ ↓ 用户操作卡片
|
|
|
+┌───────────────┐
|
|
|
+│ 8. 提交动作 │ POST /cards/{cardInstanceId}/actions/{actionName}
|
|
|
+└───────┬───────┘
|
|
|
+ ↓ 可能返回 nextCard
|
|
|
+┌───────────────┐
|
|
|
+│ 9. 渲染下一卡片 │ 循环步骤 7-9 直到流程结束
|
|
|
+└───────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 11. 前端卡片组件映射
|
|
|
+
|
|
|
+| cardKey | 卡片用途 | 关键数据字段 | 支持动作 |
|
|
|
+|---------|---------|-------------|---------|
|
|
|
+| `department-selection` | 科室选择 | `departments[]` | `select_department` |
|
|
|
+| `doctor-selection` | 医生选择 | `doctors[]` | `select_doctor` |
|
|
|
+| `time-slot-selection` | 号源选择 | `slots[]` | `select_time_slot` |
|
|
|
+| `confirm-appointment` | 挂号确认 | `summary` | `confirm_appointment`(需二次确认) |
|
|
|
+| `appointment-success` | 挂号成功 | `appointmentNo` | 无动作,仅展示 |
|
|
|
+| `route-card` | 路线导航 | `routeText` | `start_navigation` |
|
|
|
+| `tongue-capture` | 舌象采集 | `uploadField` | `submit_tongue_image` |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 12. 安全红线(前端)
|
|
|
+
|
|
|
+1. **不传 `agentId`:** 统一入口客户端请求 `/agent/chat/stream` 时 **不应** 传 `agentId`,由后端 AgentRouter 决定路由。
|
|
|
+2. **卡片动作必须带幂等键:** 所有写操作(卡片动作)必须带 `idempotencyKey`。
|
|
|
+3. **不直连任何后端能力:** 前端不调 Dify API、HIS Tool、舌诊底层接口、计量账本接口。
|
|
|
+4. **不构造医疗结论:** 前端只展示后端返回的文本和卡片,不自行生成诊断、用药建议、挂号承诺。
|
|
|
+5. **文件先上传后引用:** 图片/文件通过 `/files/upload` 获得 `fileId` 后,再传入 chat 或卡片动作。
|
|
|
+6. **Mock 结果标识:** 后端返回 `"mock":true` 的结果必须在前端明确标注为"联调演示",不可伪装为真实业务结果。
|