doc_id: API-202606-001 feature_id: FEAT-202606-001-unified-entry-client type: api title: 统一入口客户端 MVP 接口契约 status: approved owner: 医梦研发团队 created_at: 2026-06-17 updated_at: 2026-06-17 reviewers: [] related_docs:
受众: 前端工程师(
emoon-terminal-client)、终端厂商联调人员
版本: v1.0-mvp
基准:docs/api/openapi/医梦AI中台开放平台接口-v1.2.openapi.yaml
关联文档:对外接口联调基准.md、详细设计.md
统一入口客户端引入后,接口契约的核心不是“前端传一个 agentId 后端回答”,而是“终端上报设备和感知事实,后端返回场景配置、动线阶段、下一步服务和可执行卡片”。MVP 保持接口数量不扩张,优先在现有 /devices/{deviceId}/scene、/devices/{deviceId}/events、/agent/chat/stream 中增加可选字段。
前端/终端 只允许 调用以下接口:
| 端点 | 方法 | 说明 |
|---|---|---|
/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 | 设备命令确认 |
| 禁止调用的接口 | 原因 |
|---|---|
| Dify API(任何) | 前端不决定调用哪个 Dify App,后端 AgentRouter 统一路由 |
/api/v1/tools/{toolName}/invoke |
HIS 写操作必须经过 Card Runtime + AgentActionOrchestrator |
| 舌诊底层接口 | 舌象图片必须先通过 /files/upload 上传,由后端 TongueDiagnosisAdapter 统一调用 |
/api/v1/meter/events(写接口) |
计量事件由后端内部产生,前端不直接写计量 |
| 任何 HIS/EMR 直连 | 所有 HIS 交互通过卡片动作间接完成 |
以下端点使用新鉴权(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请求头:
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):
canonical = METHOD + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + sha256(body)
signature = Base64(HMAC-SHA256(secretKey, canonical))
详情见
docs/api/openapi/医梦AI中台开放平台接口-v1.2.openapi.yamlsecuritySchemes 章节。
/agent/chat(同步)、/conversation/* 沿用旧 HMAC(X-Emoon-Public-Key + X-Emoon-Sign),终端 MVP 不使用这些端点。
1. 终端启动 → POST /devices/register
2. 拿到 deviceId → POST /devices/{deviceId}/heartbeat(定时 30s)
3. 拿到 deviceId → GET /devices/{deviceId}/scene
4. 渲染首页 UI
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": "门诊大厅"
}
}'
响应:
{
"code": 200,
"data": {
"deviceId": "EMOON-KIOSK-001",
"deviceSecret": "sk-xxx...",
"activateStatus": "activated",
"admissionLevel": "A",
"reason": null
}
}
activateStatus 含义:
| 值 | 说明 |
|---|---|
activated |
准入通过,可进入核心业务 |
pending |
待人工准入,只能心跳 |
rejected |
准入拒绝,只能联系运维 |
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"
}'
响应:
{
"code": 200,
"data": {
"accepted": true,
"serverTime": 1717000000000
}
}
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>"
响应:
{
"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"],
"perceptionCapabilities": [
"QR_IDENTITY_BINDING",
"NFC_TOUCH",
"CARD_READ",
"BLE_ZONE_DETECT"
],
"journeyPolicy": {
"mode": "MVP_STATIC",
"serviceFindPatientEnabled": true,
"requiresPatientConfirm": true
},
"allowedCards": [
"department-selection",
"doctor-selection",
"time-slot-selection",
"confirm-appointment",
"appointment-success",
"tongue-capture"
]
}
}
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 根据设备类型、场景配置、用户输入和任务状态自动决定路由到哪个智能体。
感知上下文为可选字段,P0 客户端可以不传;支持 NFC/刷卡/近场能力的终端可传入最近一次感知事实,后端仍以服务端 HIS/叫号/任务状态为准。
{
"perceptionContext": {
"identityBindingId": "bind_001",
"locationLevel": "L1",
"zoneCode": "clinic_room_301_door",
"source": "NFC_TOUCH",
"occurredAt": "2026-06-03T09:30:00+08:00"
}
}
客户端请求不得传入 journeyState。P0 阶段服务端完全忽略任何通过 inputs 或扩展字段伪造的动线状态,动线状态只能由服务端通过 HIS/叫号/任务状态/卡片动作/设备事件推导后返回。断线恢复使用 conversationId 和卡片查询接口,不使用客户端回传的动线对象。
| 事件 | 说明 | 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"} |
✅ |
journey_updated |
动线阶段变更 {"stageCode":"WAITING_CHECKIN","nextAction":"CHECK_IN","source":"SERVER_DERIVED"} |
✅ |
usage_reported |
用量事件(P1) | ❌ |
error |
错误 {"code":"xxx","message":"..."} |
✅ |
completed |
本次 SSE 结束 {"conversationId":"conv_xxx"} |
✅ |
event: task_updated
data: {"taskType":"REGISTRATION","currentStep":"COLLECT_SYMPTOM","status":"ACTIVE","taskId":"task_001"}
event: journey_updated
data: {"stageCode":"REGISTRATION_IN_PROGRESS","nextAction":"SELECT_DEPARTMENT","source":"SERVER_DERIVED","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"}
conversationIdGET /api/v1/conversation/{conversationId} 获取最终消息GET /api/v1/cards/{cardInstanceId} 查询当前卡片状态(如有)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>"
响应:
{
"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": "选择科室"}
]
}
}
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)是必填的。 同一动作重复提交相同幂等键返回首次结果,不会重复扣费或创建重复号源。
响应:
{
"code": 200,
"data": {
"status": "completed",
"actionId": "action_001",
"nextCard": {
"cardInstanceId": "card_002",
"cardKey": "doctor-selection"
}
}
}
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)
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 |
音频文件 |
响应:
{
"code": 200,
"data": {
"fileId": "file_001",
"businessType": "TONGUE_IMAGE",
"retentionPolicy": "episode",
"sensitiveLevel": "P3",
"status": "available"
}
}
注意: MVP 阶段文件上传返回
fileId后,前端将fileId传入后续/agent/chat/stream的attachments字段中。舌诊底层接口由后端TongueDiagnosisAdapter统一调用,前端不直连。
机器人端需要异步接收导航命令:
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>"
响应:
{
"code": 200,
"data": {
"commands": [
{
"commandId": "cmd_001",
"commandType": "NAVIGATE_TO_LOCATION",
"payload": {
"locationId": "lab",
"routeText": "从门诊大厅向左走 20 米后右转,检验科在左手边"
},
"status": "PENDING",
"expireAt": "2026-06-01T10:05:00"
}
]
}
}
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": {}
}'
响应:
{
"code": 200,
"data": {
"acknowledged": "true"
}
}
设备事件是感知事实的入口,但不是自动业务动作。比如 NEARBY_DETECTED 只能说明患者或终端进入服务区域,后端还必须结合身份授权、HIS/叫号状态和患者确认,才能创建签到、缴费、导航等卡片。
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"
}'
eventPayload 采用“事件类型固定字段 + 扩展字段”的方式,不要求所有事件共用同一个扁平结构。服务端按 eventType 校验必要字段,未识别字段只作为审计摘要保存,不参与高风险业务判断。
常用感知事件:
| eventType | 必要 eventPayload | 后端用途 |
|---|---|---|
QR_SCANNED |
{ "qrType": "visit", "visitId": "..." } |
身份/就诊绑定 |
CARD_READ |
{ "cardType": "hospital_card" } |
身份确认,不在日志保存完整卡号 |
NEARBY_DETECTED |
{ "zoneCode": "clinic_room_301_door", "level": "L2" } |
推导是否进入服务区域 |
TOUCH_INTERACTION |
{ "screen": "home", "action": "tap_checkin" } |
记录用户主动触发 |
CUSTOM |
{ "businessState": "WAITING_PAYMENT" } |
联调阶段承载厂商扩展状态 |
隐私约束:P0 不上报连续坐标、不保存患者移动轨迹;位置字段只能表达服务区域,如诊区、诊室门口、自助机点位。
所有接口统一使用 R<T> 包装:
响应:
{
"code": 200,
"data": {
"accepted": "true",
"eventId": "evt-001"
}
}
所有接口统一使用 R<T> 包装:
{
"code": 401,
"msg": "INVALID_SIGNATURE",
"data": null
}
常见错误码:
| code | 说明 |
|---|---|
401 |
鉴权失败(签名错误、时间戳过期、nonce 重复) |
403 |
设备准入拒绝、能力不允许 |
404 |
设备/卡片/文件不存在 |
409 |
幂等键重复(返回首次结果) |
422 |
参数校验失败 |
500 |
服务端错误 |
┌───────────────┐
│ 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 直到流程结束
└───────────────┘
| 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 |
agentId: 统一入口客户端请求 /agent/chat/stream 时 不应 传 agentId,由后端 AgentRouter 决定路由。idempotencyKey。/files/upload 获得 fileId 后,再传入 chat 或卡片动作。"mock":true 的结果必须在前端明确标注为"联调演示",不可伪装为真实业务结果。