doc_id: DDS-202606-003 feature_id: FEAT-202606-001-unified-entry-client type: detailed-design title: 统一入口客户端前端工程启动指南 status: approved owner: 医梦研发团队 created_at: 2026-06-17 updated_at: 2026-06-17 reviewers: [] related_docs:
目标仓库:
emoon-terminal-client(独立仓库,不在 AI 中台后端工程内)
技术栈建议: Vue 3 + TypeScript + Vite,复用现有 Demo 前端交互资产
后端契约基准:docs/initiatives/FEAT-202606-001-unified-entry-client/MVP接口契约.md+docs/api/openapi/医梦AI中台开放平台接口-v1.2.openapi.yaml
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
| 层次 | 共用 | 差异 |
|---|---|---|
API SDK (api-client) |
✅ 统一调用 OpenPlatform | 无 |
卡片组件 (medical-cards) |
✅ 科室卡、医生卡、号源卡、路线卡、舌诊卡 | 不同终端的 allowedCards 限制不同 |
Bridge (terminal-bridge) |
✅ 统一 JSBridge 抽象接口 | 机器人有导航;自助机有读卡/打印;导诊大屏能力最少 |
动线区 (journey-panel) |
✅ 渲染后端 journeyState 和下一步服务 |
导诊屏偏公共导流,自助机偏业务办理,机器人偏导航 |
感知状态 (perception-status) |
✅ 展示身份绑定、授权、扫码/NFC/刷卡状态 | 不同终端感知能力来自 perceptionCapabilities |
主题 (terminal-theme) |
✅ 医梦主题 + 医院主题 | 屏幕尺寸、字体、按钮密度不同 |
| 首页 | ✅ 聊天主控件可复用 | 三类终端首页布局不同 |
loadConfig(hospitalId, accessKey)
→ POST /devices/register # 拿到 deviceId + activateStatus
→ setInterval 30s: POST /devices/{deviceId}/heartbeat
→ GET /devices/{deviceId}/scene # 拿到 homeTemplate + allowedAgents + allowedCards
→ initPerception(scene.perceptionCapabilities)
→ initJourneyPanel(scene.journeyPolicy)
→ renderHome(homeTemplate) # 根据 sceneCode 渲染首页
阻塞条件:
activateStatus = "pending" → 展示"等待激活"状态,不进入核心业务activateStatus = "rejected" → 展示拒绝原因 + 联系运维入口activateStatus = "activated" → 正常进入首页推荐布局:
顶部:设备/院区/身份绑定/授权状态
左侧:公共快捷入口、人工服务、无障碍入口
中央:AI 对话、虚拟形象、业务卡片
右侧:当前动线阶段、下一步任务、排队/缴费/检查提醒
底部:通知、异常、扫码/NFC/人工降级入口
journeyState 只能由后端返回后展示。前端可以上报扫码、NFC、刷卡、近场等 perceptionContext 或设备事件,但不能自行判断“已签到”“可缴费”“可挂号成功”。
| cardKey | 渲染要求 |
|---|---|
department-selection |
科室列表(名称 + 推荐原因)+ 选择按钮 |
doctor-selection |
医生列表(姓名 + 职称)+ 选择按钮 |
time-slot-selection |
号源列表(时间 + 剩余号数)+ 选择按钮 |
confirm-appointment |
挂号信息摘要 + 二次确认弹窗 |
appointment-success |
成功页(预约号 + Mock 标识) |
route-card |
路线文字 + "开始导航"按钮(机器人端调 JSBridge) |
tongue-capture |
拍照/上传引导 + 提交按钮 |
卡片通用交互模式:
// 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, ...)
}
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("journey_updated", (data) => {
renderJourneyPanel(data) // P0 必须处理;前端不得从 task/card 反推动线
})
eventSource.on("completed", (data) => {
saveConversationId(data.conversationId) // 记录,用于断线恢复
})
eventSource.on("error", (data) => {
showError(data.message)
})
使用 openapi-generator 或 openapi-typescript 从 docs/api/openapi/医梦AI中台开放平台接口-v1.2.openapi.yaml 自动生成 TypeScript SDK:
# 示例:openapi-typescript
npx openapi-typescript \
https://raw.githubusercontent.com/<repo>/docs/api/openapi/医梦AI中台开放平台接口-v1.2.openapi.yaml \
-o packages/api-client/src/generated/schema.ts
| 禁止 | 原因 |
|---|---|
请求中传 agentId |
后端 AgentRouter 决定路由 |
| 直连 Dify API | 前端不决定流程编排 |
| 直连 HIS / 舌诊底层接口 | 必须通过卡片动作闭环 |
| 自行构造医疗建议文本 | 只展示后端返回的文本和卡片 |
| 自行推导动线阶段并放开能力 | 动线阶段由后端结合 HIS/叫号/任务状态判断 |
| 保存患者连续位置轨迹 | P0 只允许服务区域级事件,不保存移动路线 |
| 隐藏 Mock 标识 | 后端返回 "mock":true 时必须标注"联调演示" |
不传 idempotencyKey |
卡片动作必须带幂等键 |
// 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 文件区分:
# apps/kiosk-client/.env
VITE_API_BASE_URL=https://api.demo.emoon.local
VITE_ACCESS_KEY=kiosk_demo_key
# 注意:不要在此文件放置签名密钥
| 资产 | 位置 |
|---|---|
| OpenAPI 完整契约 | docs/api/openapi/医梦AI中台开放平台接口-v1.2.openapi.yaml |
| 终端接口契约(含 curl 示例) | docs/initiatives/FEAT-202606-001-unified-entry-client/MVP接口契约.md |
| 对外接口设计文档 | docs/initiatives/FEAT-202606-001-unified-entry-client/对外接口联调基准.md |
| 技术设计文档 | docs/initiatives/FEAT-202606-001-unified-entry-client/详细设计.md |
| 现有 Demo 前端 | emoon-ui/(交互资产可复用,业务逻辑需重构) |