Selaa lähdekoodia

docs(ai-terminal): publish terminal client MVP contract and bootstrap guide

- terminal-client-mvp-contract.md: 12-section contract covering all 11 P0 endpoints,
  auth (HMAC Base64 corrected), SSE events, card action sequences,
  file upload, robot commands, error codes, startup flow, security red lines
- 统一入口客户端前端工程启动指南.md: Vue3 monorepo structure, 3-device differences,
  card rendering MVP specs, SSE handling, API SDK generation, HMAC key guidance
  (VITE_HMAC_SECRET removed — keys held by native shell, not web bundle)
WangKang 1 viikko sitten
vanhempi
commit
ba85efeb19

+ 596 - 0
docs/接口文档/terminal-client-mvp-contract.md

@@ -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` 的结果必须在前端明确标注为"联调演示",不可伪装为真实业务结果。

+ 166 - 0
docs/架构文档/统一入口客户端前端工程启动指南.md

@@ -0,0 +1,166 @@
+# 统一入口客户端前端工程启动指南
+
+> **目标仓库:** `emoon-terminal-client`(独立仓库,不在 AI 中台后端工程内)  
+> **技术栈建议:** Vue 3 + TypeScript + Vite,复用现有 Demo 前端交互资产  
+> **后端契约基准:** `docs/接口文档/terminal-client-mvp-contract.md` + `emoon-ai-openplatform-api-v1.2.openapi.yaml`
+
+---
+
+## 1. 推荐工程结构
+
+```text
+emoon-terminal-client
+├── apps
+│   ├── robot-client        # 机器人端(Android WebView 壳)
+│   ├── kiosk-client        # 自助机端
+│   └── guide-screen-client # 导诊大屏端
+│
+├── packages
+│   ├── api-client          # 统一 OpenPlatform API SDK(自动从 openapi.yaml 生成)
+│   ├── medical-cards       # 医疗卡片组件库(department-selection, doctor-selection 等)
+│   ├── terminal-bridge     # JSBridge 抽象层(导航、扫码、读卡、打印)
+│   ├── terminal-theme      # 主题系统(医梦主题 + 医院定制)
+│   └── terminal-core       # 共享 Hooks / Stores / Utils
+│
+├── package.json
+├── turbo.json              # Turborepo 编排
+└── README.md
+```
+
+## 2. 三端差异速查
+
+| 层次 | 共用 | 差异 |
+|------|------|------|
+| API SDK (`api-client`) | ✅ 统一调用 OpenPlatform | 无 |
+| 卡片组件 (`medical-cards`) | ✅ 科室卡、医生卡、号源卡、路线卡、舌诊卡 | 不同终端的 `allowedCards` 限制不同 |
+| Bridge (`terminal-bridge`) | ✅ 统一 JSBridge 抽象接口 | 机器人有导航;自助机有读卡/打印;导诊大屏能力最少 |
+| 主题 (`terminal-theme`) | ✅ 医梦主题 + 医院主题 | 屏幕尺寸、字体、按钮密度不同 |
+| 首页 | ✅ 聊天主控件可复用 | 三类终端首页布局不同 |
+
+## 3. 首页启动流程(必实现)
+
+```text
+loadConfig(hospitalId, accessKey)
+  → POST /devices/register        # 拿到 deviceId + activateStatus
+  → setInterval 30s: POST /devices/{deviceId}/heartbeat
+  → GET /devices/{deviceId}/scene  # 拿到 homeTemplate + allowedAgents + allowedCards
+  → renderHome(homeTemplate)       # 根据 sceneCode 渲染首页
+```
+
+**阻塞条件:**
+
+- `activateStatus = "pending"` → 展示"等待激活"状态,不进入核心业务
+- `activateStatus = "rejected"` → 展示拒绝原因 + 联系运维入口
+- `activateStatus = "activated"` → 正常进入
+
+## 4. 卡片组件渲染 MVP(P0 必须支持)
+
+| cardKey | 渲染要求 |
+|---------|---------|
+| `department-selection` | 科室列表(名称 + 推荐原因)+ 选择按钮 |
+| `doctor-selection` | 医生列表(姓名 + 职称)+ 选择按钮 |
+| `time-slot-selection` | 号源列表(时间 + 剩余号数)+ 选择按钮 |
+| `confirm-appointment` | 挂号信息摘要 + 二次确认弹窗 |
+| `appointment-success` | 成功页(预约号 + Mock 标识) |
+| `route-card` | 路线文字 + "开始导航"按钮(机器人端调 JSBridge) |
+| `tongue-capture` | 拍照/上传引导 + 提交按钮 |
+
+**卡片通用交互模式:**
+
+```typescript
+// 1. 渲染卡片(cardData 来自 GET /cards/{cardInstanceId})
+renderCard(cardInstanceId, cardKey, cardData)
+
+// 2. 用户操作 → 生成幂等键 → 提交动作
+const idempotencyKey = crypto.randomUUID()  // 客户端生成,同一逻辑操作复用
+const result = await apiClient.cardAction(cardInstanceId, actionName, {
+  idempotencyKey,
+  confirm: action.requiredConfirm ? userConfirmed : true,
+  payload: { /* 用户选择的 departmentId / doctorId / slotId */ }
+})
+
+// 3. 如有 nextCard → 继续渲染下一卡片
+if (result.nextCard) {
+  renderCard(result.nextCard.cardInstanceId, result.nextCard.cardKey, ...)
+}
+```
+
+## 5. SSE 流式对话处理
+
+```typescript
+const eventSource = await apiClient.chatStream({
+  deviceContext: { deviceId, deviceType },
+  message: { type: "text", text: userInput, clientMessageId: crypto.randomUUID() }
+  // 注意:不传 agentId,由后端 AgentRouter 自动路由
+})
+
+eventSource.on("message_delta", (data) => {
+  appendText(data.text)           // 逐字追加到对话气泡
+})
+
+eventSource.on("card_created", (data) => {
+  fetchAndRenderCard(data.cardInstanceId)  // 拉取卡片详情并渲染
+})
+
+eventSource.on("completed", (data) => {
+  saveConversationId(data.conversationId)  // 记录,用于断线恢复
+})
+
+eventSource.on("error", (data) => {
+  showError(data.message)
+})
+```
+
+## 6. API SDK 生成建议
+
+使用 `openapi-generator` 或 `openapi-typescript` 从 `emoon-ai-openplatform-api-v1.2.openapi.yaml` 自动生成 TypeScript SDK:
+
+```bash
+# 示例:openapi-typescript
+npx openapi-typescript \
+  https://raw.githubusercontent.com/<repo>/docs/接口文档/emoon-ai-openplatform-api-v1.2.openapi.yaml \
+  -o packages/api-client/src/generated/schema.ts
+```
+
+## 7. 前端禁止事项
+
+| 禁止 | 原因 |
+|------|------|
+| 请求中传 `agentId` | 后端 AgentRouter 决定路由 |
+| 直连 Dify API | 前端不决定流程编排 |
+| 直连 HIS / 舌诊底层接口 | 必须通过卡片动作闭环 |
+| 自行构造医疗建议文本 | 只展示后端返回的文本和卡片 |
+| 隐藏 Mock 标识 | 后端返回 `"mock":true` 时必须标注"联调演示" |
+| 不传 `idempotencyKey` | 卡片动作必须带幂等键 |
+
+## 8. 环境配置
+
+```typescript
+// packages/api-client/src/config.ts
+export const apiConfig = {
+  baseUrl: import.meta.env.VITE_API_BASE_URL || "https://api.demo.emoon.local",
+  accessKey: import.meta.env.VITE_ACCESS_KEY || "",
+}
+```
+
+**关键:`VITE_*` 前缀变量会被编译进前端 bundle,浏览器端可直接读取。签名密钥不得放入此类变量。**  
+签名应由受控终端壳(Android WebView bridge / Electron main process / 自助机原生层)持有并在每次请求前完成 HMAC 签名计算。前端 API SDK 仅接收已签名的请求头,或调用桥接层提供的 `signAndSend()` 方法。
+
+三个 app 通过各自的 `.env` 文件区分:
+
+```text
+# apps/kiosk-client/.env
+VITE_API_BASE_URL=https://api.demo.emoon.local
+VITE_ACCESS_KEY=kiosk_demo_key
+# 注意:不要在此文件放置签名密钥
+```
+
+## 9. 参考资产
+
+| 资产 | 位置 |
+|------|------|
+| OpenAPI 完整契约 | `docs/接口文档/emoon-ai-openplatform-api-v1.2.openapi.yaml` |
+| 终端接口契约(含 curl 示例) | `docs/接口文档/terminal-client-mvp-contract.md` |
+| 对外接口设计文档 | `docs/接口文档/医梦AI中台对外接口设计文档_v1.2_正式联调基准版.md` |
+| 技术设计文档 | `docs/架构文档/统一入口客户端技术设计文档_v1.0.md` |
+| 现有 Demo 前端 | `emoon-ui/`(交互资产可复用,业务逻辑需重构) |