|
|
@@ -0,0 +1,1701 @@
|
|
|
+# 医梦 AI|统一入口客户端技术设计文档 v1.0
|
|
|
+
|
|
|
+> 文档定位:统一入口客户端后端 + 前端终端 + Dify Workflow + Card Runtime + MCP/HIS Adapter + 舌诊接口接入的完整技术方案。
|
|
|
+> 适用范围:机器人端、自助机端、导诊大屏端、AI 中台 OpenPlatform、Dify 工作流、设备注册与场景配置、卡片动作闭环、HIS Mock/真实适配、舌诊能力接入。
|
|
|
+> 目标读者:技术负责人、后端工程师、前端工程师、机器人端工程师、产品经理、测试、交付人员。
|
|
|
+> 当前阶段:一期开发计划基准版。
|
|
|
+> 关键修正:本版已修复原《统一入口客户端后端流程设计图谱.md》中第 8、9 节 Mermaid 无法解析的问题。失败原因是节点标签以 `/files/upload` 开头时被 Mermaid 误判为特殊节点语法,本版统一改为引号节点,例如 `J["调用 files upload 上传身份证图片"]`。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 0. 文档结论
|
|
|
+
|
|
|
+统一入口客户端不是一个普通聊天页面,也不是现有 Demo 的简单产品化。它应被定义为:
|
|
|
+
|
|
|
+> **部署在机器人、自助机、导诊大屏等门诊终端上的多设备 AI 场景运行时。前端负责统一交互,后端负责设备上下文、会话任务、Agent 路由、Dify 编排、卡片动作、工具调用、文件治理、审计计量。**
|
|
|
+
|
|
|
+一期建设目标不是“大而全 AI 医院”,而是先打穿三条生产级主链路:
|
|
|
+
|
|
|
+```text
|
|
|
+机器人端:导诊问答 + 路线导航
|
|
|
+自助机端:建档 + 预问诊 + 分诊 + 挂号 Mock + 舌诊
|
|
|
+导诊大屏端:FAQ + 科室介绍 + 路线引导 + 轻量分诊
|
|
|
+```
|
|
|
+
|
|
|
+核心技术控制链路固定为:
|
|
|
+
|
|
|
+```text
|
|
|
+统一入口客户端
|
|
|
+→ 设备注册 / 场景配置
|
|
|
+→ 会话与任务状态
|
|
|
+→ AgentRouter 后端路由
|
|
|
+→ Dify Workflow 场景编排
|
|
|
+→ DifyOutputNormalizer 输出校验
|
|
|
+→ Card Runtime 卡片实例与动作闭环
|
|
|
+→ MCP Tool Service / HIS Adapter / TongueDiagnosisAdapter
|
|
|
+→ Audit / Meter / Trace
|
|
|
+```
|
|
|
+
|
|
|
+必须坚持的工程红线:
|
|
|
+
|
|
|
+| 红线 | 说明 |
|
|
|
+|---|---|
|
|
|
+| 前端不决定调用哪个 Dify App | 前端只传用户输入、设备上下文、文件 ID、卡片动作 |
|
|
|
+| Dify 不直接写 HIS | Dify 只做场景编排、追问、结构化输出、解释 |
|
|
|
+| 卡片不是普通 UI | 挂号、建档、舌诊提交、导航执行都必须走 Card Runtime |
|
|
|
+| 当前没有 HIS 接口时必须显式使用 `HisMockAdapter` | 禁止把随机医生、随机号源写在前端、Dify 或 ChatService 中 |
|
|
|
+| 导诊大屏不处理患者隐私 | 不允许身份证上传、舌象采集、报告解读、个人挂号确认 |
|
|
|
+| 舌诊已有接口也必须纳入中台 | 统一走 File Service + TongueDiagnosisAdapter,不允许前端直连 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 背景与现状
|
|
|
+
|
|
|
+### 1.1 当前 Demo 工程定位
|
|
|
+
|
|
|
+当前 Demo 工程由三部分组成:
|
|
|
+
|
|
|
+| 模块 | 当前职责 | 生产化判断 |
|
|
|
+|---|---|---|
|
|
|
+| Vue 前端 | 聊天界面、快捷入口、业务卡片、语音输入、TTS、机器人导航 JSBridge | 可保留并重构为统一入口客户端 |
|
|
|
+| Spring Boot Demo 后端 | 意图识别、会话状态、SSE、建档、挂号、报告解读、舌诊流程编排 | 不建议作为生产后端保留,应拆解迁入 AI 中台 |
|
|
|
+| Android 机器人壳 | WebView 容器、语音和导航桥接、猎户星空 SDK 对接 | 可保留,定位为 Terminal Bridge 适配层 |
|
|
|
+
|
|
|
+当前 Demo 的主要问题:
|
|
|
+
|
|
|
+| 问题 | 风险 |
|
|
|
+|---|---|
|
|
|
+| `ChatService` 承担过多职责 | 后续会继续堆 if-else,无法维护 |
|
|
|
+| 会话状态以内存 Map 保存 | 服务重启丢失,无法断线恢复 |
|
|
|
+| 患者档案写 CSV | 不具备真实 HIS / 患者主索引能力 |
|
|
|
+| 医生、排班、号源为随机模拟 | 容易误导为真实业务闭环 |
|
|
|
+| 舌诊部分链路可能前端直连 | 权限、审计、计量、文件治理失控 |
|
|
|
+| FAQ 本地 txt | 缺少知识库版本、院区差异、来源引用 |
|
|
|
+| 无 Card Runtime | 卡片只是 UI,不能承载真实动作状态 |
|
|
|
+| 无 Device Registry | 多终端无法统一授权、场景绑定、灰度、监控 |
|
|
|
+| 无完整审计与 Trace | 医疗场景不可接受 |
|
|
|
+
|
|
|
+### 1.2 目标工程定位
|
|
|
+
|
|
|
+目标工程应拆成两条线:
|
|
|
+
|
|
|
+```text
|
|
|
+前端线:emoon-terminal-client
|
|
|
+后端线:emoon-ai-platform / emoon-openplatform
|
|
|
+```
|
|
|
+
|
|
|
+前端继续沿用 Vue 3,但要从 Demo 前端改造成正式 Terminal Client;后端能力纳入 AI 中台,不再保留独立 Demo 后端。
|
|
|
+
|
|
|
+### 1.3 行业参考与设计取舍
|
|
|
+
|
|
|
+统一入口客户端不是凭空设计。公开案例能证明“对话式入口 + 多终端承载 + 后端统一编排”方向可行,但医梦不能照搬大厂完整平台规模,必须按一期 MVP 裁剪。
|
|
|
+
|
|
|
+| 参考对象 | 公开可验证做法 | 对医梦的启发 | 医梦取舍 |
|
|
|
+|---|---|---|---|
|
|
|
+| 支付宝 AI 健康管家 | [蚂蚁集团公开资料](https://www.antgroup.com/news-media/press-releases/1725526800000)显示其提供找医生、读报告、陪看诊、问医保等 AI 健康服务 | 医疗入口正在从多级菜单转向“用户表达诉求,系统组织服务” | 一期只做导诊、挂号 Mock、舌诊和路线,不做医保、真实支付和全量陪诊 |
|
|
|
+| 腾讯云智能导诊 / 腾讯健康智能预问诊 | [腾讯云智能导诊](https://cloud.tencent.com/product/ig)覆盖导诊、智能问答、科普宣教,可应用于微信挂号等场景;[腾讯健康智能预问诊](https://healthcare.tencent.com/production/11)支持公众号、小程序、互联网医院、院内终端等多终端部署 | 导诊和预问诊适合做统一入口,但必须服务后续挂号或医生工作流 | 后端统一路由,前端按设备能力裁剪,不把导诊大屏做成隐私办理终端 |
|
|
|
+| 卫宁 WiNEX | [CHIMA 公开报道](https://www.chima.org.cn/Html/News/Articles/4592.html)显示 WiNEX 覆盖门诊医生站、住院医生站、病区护士站等业务场景,并强调业务/数据/技术中台 | 大厂通过中台支撑多场景,证明共性底座方向可行 | 医梦只沉淀 AI 入口、卡片、设备场景、工具审计,不建设完整医院核心系统 |
|
|
|
+| 科大讯飞智慧医疗 / 智医助理 | [讯飞智慧医疗官网](https://stat.iflyhealth.com/)公开介绍语音、语义理解、临床决策支持等能力 | 语音交互、多轮追问和辅助决策可以进入医疗工作流 | 输出必须定位为就医引导或辅助分析,不做自动诊断和自动处方 |
|
|
|
+| Dify Workflow | [Dify 官方文档](https://docs.dify.ai/en/use-dify/build/version-control)提供 Workflow/Chatflow 版本管理;Dify 也支持流式应用调用 | Dify 可承担场景编排和版本管理 | 医梦只做 Dify 版本映射、输出校验和卡片归一,不自研 Workflow 平台 |
|
|
|
+
|
|
|
+因此,本方案的原则是:交互形态参考互联网医疗入口,后端治理参考医疗中台,落地范围按创业团队资源只做 P0 闭环。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 一期建设边界
|
|
|
+
|
|
|
+### 2.1 终端边界
|
|
|
+
|
|
|
+| 终端 | 一期定位 | 核心能力 | 明确不做 |
|
|
|
+|---|---|---|---|
|
|
|
+| 机器人端 | 展示性最强的语音导诊入口 | 语音问答、科室推荐、路线导航、舌诊入口引导 | 复杂实名建档、支付闭环、正式挂号确认 |
|
|
|
+| 自助机端 | 业务闭环最强的办理入口 | 建档、预问诊、分诊、挂号 Mock、舌诊采集 | 真实医保支付、退号退费、医生站能力 |
|
|
|
+| 导诊大屏端 | 公共导流入口 | FAQ、科室介绍、路线引导、轻量分诊 | 身份证上传、舌象采集、报告解读、个人隐私展示 |
|
|
|
+
|
|
|
+### 2.2 一期能力清单
|
|
|
+
|
|
|
+| 优先级 | 能力 | 说明 |
|
|
|
+|---|---|---|
|
|
|
+| P0 | Device Registry MVP | 设备注册、心跳、能力、场景绑定 |
|
|
|
+| P0 | Scene Profile | 不同终端加载不同首页、Agent 和卡片权限 |
|
|
|
+| P0 | Agent Chat Stream | 统一替换 Demo `/chat/messages` |
|
|
|
+| P0 | AgentRouter | 后端统一路由到不同 Dify App |
|
|
|
+| P0 | Conversation Service | 会话和消息持久化、断线恢复 |
|
|
|
+| P0 | TaskStateService | 挂号、舌诊、导航等任务状态机 |
|
|
|
+| P0 | Card Runtime MVP | 卡片定义、实例、动作、状态、幂等 |
|
|
|
+| P0 | File Service | 身份证、舌象、音频、报告文件治理 |
|
|
|
+| P0 | DifyAgentEngine | 接入四个 Dify Workflow |
|
|
|
+| P0 | HisMockAdapter | 科室、医生、排班、建档、挂号 Mock |
|
|
|
+| P0 | TongueDiagnosisAdapter | 封装已有舌诊接口 |
|
|
|
+| P0 | Audit / Meter / Trace | 审计、用量、链路追踪 |
|
|
|
+| P1 | 真实 HIS Adapter | 等你提供真实接口后替换 Mock |
|
|
|
+| P1 | 报告解读 | 可复用 File Service + Report Agent |
|
|
|
+| P1 | 运营看板 | 会话量、设备在线率、卡片动作成功率、失败原因 |
|
|
|
+
|
|
|
+### 2.3 暂缓范围
|
|
|
+
|
|
|
+```text
|
|
|
+真实支付
|
|
|
+医保结算
|
|
|
+退号退费
|
|
|
+全院鸿蒙无感定位
|
|
|
+医生站插件
|
|
|
+住院入口
|
|
|
+完整账单中心
|
|
|
+多院区域平台
|
|
|
+第三方卡片市场
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. 总体架构设计
|
|
|
+
|
|
|
+### 3.1 后端总览架构图
|
|
|
+
|
|
|
+```mermaid
|
|
|
+flowchart TB
|
|
|
+ subgraph Client["统一入口客户端 Terminal Client"]
|
|
|
+ Robot["机器人端:语音导诊 / 导航 / 问答"]
|
|
|
+ Kiosk["自助机端:建档 / 挂号 / 舌诊 / 报告"]
|
|
|
+ Screen["导诊大屏:FAQ / 科室介绍 / 路线引导"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph OpenPlatform["AI 中台 OpenPlatform"]
|
|
|
+ Auth["鉴权与项目上下文 AuthContextResolver"]
|
|
|
+ Device["设备上下文 DeviceContextResolver"]
|
|
|
+ Scene["场景配置 SceneProfileService"]
|
|
|
+ Conversation["会话服务 ConversationService"]
|
|
|
+ Task["任务状态机 TaskStateService"]
|
|
|
+ Router["AgentRouter:设备策略 + 任务状态 + 规则 + LLM 分类"]
|
|
|
+ Dispatcher["AgentDispatcher:选择 Dify App / Direct / Mock"]
|
|
|
+ AgentOrch["AgentActionOrchestrator:卡片动作后的业务编排"]
|
|
|
+ Card["Card Runtime:卡片定义 / 实例 / 动作 / 状态机"]
|
|
|
+ File["File Service:身份证 / 舌象 / 报告 / 音频"]
|
|
|
+ Audit["Audit / Meter / Trace"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph Dify["Dify Workflow Apps"]
|
|
|
+ Guide["opd-guide-agent:导诊问答 / FAQ / 导航"]
|
|
|
+ Triage["opd-triage-agent:症状追问 / 科室推荐"]
|
|
|
+ Register["opd-registration-agent:建档 / 挂号流程"]
|
|
|
+ Tongue["tongue-diagnosis-agent:舌诊问诊 / 结果解释"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph Tool["MCP / Adapter 工具层"]
|
|
|
+ MCP["MCP Tool Service:统一工具网关"]
|
|
|
+ HisMock["HisMockAdapter:科室 / 医生 / 排班 / 挂号 Mock"]
|
|
|
+ HisReal["RealHisAdapter:后续真实 HIS 接口"]
|
|
|
+ TongueApi["TongueDiagnosisAdapter:已有舌诊接口"]
|
|
|
+ DeviceCmd["Device Command:机器人导航 / 设备指令"]
|
|
|
+ end
|
|
|
+
|
|
|
+ Robot --> OpenPlatform
|
|
|
+ Kiosk --> OpenPlatform
|
|
|
+ Screen --> OpenPlatform
|
|
|
+
|
|
|
+ OpenPlatform --> Auth --> Device --> Scene --> Conversation --> Task --> Router --> Dispatcher
|
|
|
+
|
|
|
+ Dispatcher --> Guide
|
|
|
+ Dispatcher --> Triage
|
|
|
+ Dispatcher --> Register
|
|
|
+ Dispatcher --> Tongue
|
|
|
+
|
|
|
+ Dispatcher --> Card
|
|
|
+ Card --> AgentOrch
|
|
|
+ AgentOrch --> MCP
|
|
|
+ File --> TongueApi
|
|
|
+ MCP --> HisMock
|
|
|
+ MCP --> HisReal
|
|
|
+ AgentOrch --> DeviceCmd
|
|
|
+
|
|
|
+ Card --> Audit
|
|
|
+ AgentOrch --> Audit
|
|
|
+ Dispatcher --> Audit
|
|
|
+ File --> Audit
|
|
|
+ Device --> Audit
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 模块职责
|
|
|
+
|
|
|
+| 模块 | 负责 | 不负责 |
|
|
|
+|---|---|---|
|
|
|
+| Terminal Client | 交互、卡片展示、语音/摄像头/扫码/导航 Bridge | 不做最终业务判断 |
|
|
|
+| Device Registry | 设备身份、能力、位置、心跳、版本、场景绑定 | 不处理医疗业务 |
|
|
|
+| Scene Profile | 根据设备类型返回首页模板、允许 Agent、允许卡片、禁止意图 | 不调用模型 |
|
|
|
+| Conversation Service | 会话、消息、上下文、断线恢复 | 不做意图判断 |
|
|
|
+| TaskStateService | 多轮任务实例、currentStep、任务上下文 | 不调用 Dify |
|
|
|
+| AgentRouter | 统一路由到 Dify App | 不生成医学回答 |
|
|
|
+| DifyAgentEngine | 调用 Dify Workflow,同步/流式解析 | 不直接写 HIS,不直接建卡片实例 |
|
|
|
+| DifyOutputNormalizer | 校验 Dify 输出结构、卡片 schema、风险等级 | 不执行卡片动作 |
|
|
|
+| Card Runtime | 卡片实例、动作、状态机、幂等、动作日志 | 不直接生成自然语言回答 |
|
|
|
+| MCP Tool Service | HIS/EMR/LIS/PACS/叫号/支付等工具统一出口 | 不处理 UI |
|
|
|
+| HisMockAdapter | 无 HIS 接口阶段的显式 Mock | 不伪装成真实业务系统 |
|
|
|
+| TongueDiagnosisAdapter | 封装已有舌诊接口 | 不允许前端直连 |
|
|
|
+| Audit/Meter/Trace | 审计、计量、链路追踪、排障 | 不参与医疗判断 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 前端工程设计
|
|
|
+
|
|
|
+### 4.1 推荐工程结构
|
|
|
+
|
|
|
+```text
|
|
|
+emoon-terminal-client
|
|
|
+├── apps
|
|
|
+│ ├── robot-client
|
|
|
+│ ├── kiosk-client
|
|
|
+│ └── guide-screen-client
|
|
|
+│
|
|
|
+├── packages
|
|
|
+│ ├── api-client
|
|
|
+│ ├── medical-cards
|
|
|
+│ ├── terminal-bridge
|
|
|
+│ ├── terminal-theme
|
|
|
+│ └── terminal-core
|
|
|
+```
|
|
|
+
|
|
|
+### 4.2 三端共用与差异
|
|
|
+
|
|
|
+| 层次 | 共用 | 差异 |
|
|
|
+|---|---|---|
|
|
|
+| API SDK | 统一调用 OpenPlatform 接口 | 无 |
|
|
|
+| 卡片组件 | 科室卡、医生卡、路线卡、舌诊卡、建档卡 | 不同终端允许渲染的卡片不同 |
|
|
|
+| Bridge | 统一 JSBridge 抽象 | 机器人有导航,自助机有读卡/打印,导诊大屏能力最少 |
|
|
|
+| 主题 | 医梦主题 + 医院主题 | 屏幕尺寸、字体、按钮密度不同 |
|
|
|
+| 首页 | 场景入口和聊天主控件可复用 | 三类终端首页布局不同 |
|
|
|
+
|
|
|
+### 4.3 前端调用原则
|
|
|
+
|
|
|
+```text
|
|
|
+前端只调用:
|
|
|
+- /devices/register
|
|
|
+- /devices/{deviceId}/heartbeat
|
|
|
+- /devices/{deviceId}/scene
|
|
|
+- /agent/chat/stream
|
|
|
+- /files/upload
|
|
|
+- /cards/{cardInstanceId}
|
|
|
+- /cards/{cardInstanceId}/actions/{actionName}
|
|
|
+- /devices/{deviceId}/events
|
|
|
+
|
|
|
+前端不调用:
|
|
|
+- Dify API
|
|
|
+- HIS Tool API
|
|
|
+- 舌诊底层接口
|
|
|
+- 计量账本写接口
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 终端启动流程
|
|
|
+
|
|
|
+### 5.1 启动时序图
|
|
|
+
|
|
|
+```mermaid
|
|
|
+sequenceDiagram
|
|
|
+ autonumber
|
|
|
+
|
|
|
+ participant T as 终端客户端
|
|
|
+ participant D as DeviceRegistry
|
|
|
+ participant S as SceneProfileService
|
|
|
+ participant A as AgentCenter
|
|
|
+ participant C as CardDefinitionService
|
|
|
+ participant UI as TerminalUI
|
|
|
+
|
|
|
+ T->>D: POST /devices/register
|
|
|
+ alt 设备准入通过
|
|
|
+ D-->>T: deviceId + deviceSecret + activateStatus=activated
|
|
|
+ else 待人工准入
|
|
|
+ D-->>T: activateStatus=pending + reason
|
|
|
+ T->>UI: 展示待激活状态,不进入核心业务闭环
|
|
|
+ else 准入拒绝或设备等级为 C/D
|
|
|
+ D-->>T: activateStatus=rejected + reason
|
|
|
+ T->>UI: 展示拒绝原因,仅允许健康检查或联系运维
|
|
|
+ end
|
|
|
+
|
|
|
+ T->>D: POST /devices/{deviceId}/heartbeat
|
|
|
+ D-->>T: online + serverTime + upgradePolicy
|
|
|
+
|
|
|
+ T->>S: GET /devices/{deviceId}/scene
|
|
|
+ S->>A: 查询允许调用的 Agent
|
|
|
+ A-->>S: allowedAgents + defaultAgent
|
|
|
+
|
|
|
+ S->>C: 查询允许渲染的卡片类型
|
|
|
+ C-->>S: cardTemplates
|
|
|
+
|
|
|
+ S-->>T: sceneProfile
|
|
|
+ T->>UI: 根据 sceneProfile 加载首页
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2 SceneProfile 示例
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "deviceId": "EMOON-KIOSK-001",
|
|
|
+ "deviceType": "self_service_kiosk",
|
|
|
+ "sceneCode": "outpatient_kiosk",
|
|
|
+ "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",
|
|
|
+ "printer_reserved"
|
|
|
+ ],
|
|
|
+ "homeTemplate": "kiosk_home_v1"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 5.3 三类设备的策略差异
|
|
|
+
|
|
|
+| 策略 | 机器人 | 自助机 | 导诊大屏 |
|
|
|
+|---|---|---|---|
|
|
|
+| FAQ | 允许 | 允许 | 允许 |
|
|
|
+| 科室推荐 | 允许 | 允许 | 允许 |
|
|
|
+| 路线导航 | 允许,且可调用机器人 SDK | 允许展示路线 | 允许展示路线 |
|
|
|
+| 建档 | 不主推,引导到自助机 | 允许 | 禁止 |
|
|
|
+| 挂号 | 只做引导或轻量演示 | 允许 Mock / 后续真实 | 禁止 |
|
|
|
+| 舌象采集 | 可引导,不建议主采集 | 允许 | 禁止 |
|
|
|
+| 报告解读 | 后置 | 后置 | 禁止 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 用户输入主流程
|
|
|
+
|
|
|
+### 6.1 完整时序图
|
|
|
+
|
|
|
+```mermaid
|
|
|
+sequenceDiagram
|
|
|
+ autonumber
|
|
|
+
|
|
|
+ participant U as 用户
|
|
|
+ participant C as 统一入口客户端
|
|
|
+ participant API as AgentChatController
|
|
|
+ participant Auth as AuthContextResolver
|
|
|
+ participant Device as DeviceContextResolver
|
|
|
+ participant Scene as SceneProfileService
|
|
|
+ participant Conv as ConversationService
|
|
|
+ participant Task as TaskStateService
|
|
|
+ participant Router as AgentRouterService
|
|
|
+ participant Dispatch as AgentDispatcher
|
|
|
+ participant Dify as DifyAgentEngine
|
|
|
+ participant Normalizer as DifyOutputNormalizer
|
|
|
+ participant Card as CardRuntime
|
|
|
+ participant Audit as AuditMeterTrace
|
|
|
+
|
|
|
+ U->>C: 输入语音、文本或点击快捷入口
|
|
|
+ C->>API: POST /agent/chat/stream
|
|
|
+
|
|
|
+ API->>Auth: 校验项目、医院、终端鉴权
|
|
|
+ Auth-->>API: projectContext
|
|
|
+
|
|
|
+ API->>Device: 解析 deviceId、deviceType、capabilities
|
|
|
+ Device-->>API: deviceContext
|
|
|
+
|
|
|
+ API->>Scene: 获取当前设备场景配置
|
|
|
+ Scene-->>API: sceneProfile、allowedAgents、blockedIntents
|
|
|
+
|
|
|
+ API->>Conv: 创建或加载 conversation
|
|
|
+ Conv-->>API: conversationContext
|
|
|
+
|
|
|
+ API->>Task: 查询 activeTask
|
|
|
+ Task-->>API: 当前任务状态或空
|
|
|
+
|
|
|
+ API->>Router: 路由决策
|
|
|
+ Router-->>API: RouteDecision
|
|
|
+
|
|
|
+ alt 当前设备不允许该能力
|
|
|
+ API-->>C: SSE 返回 blocked message
|
|
|
+ else 允许执行
|
|
|
+ API->>Dispatch: 分发到目标 Agent
|
|
|
+ Dispatch->>Dify: 调用对应 Dify Workflow
|
|
|
+ Dify-->>Dispatch: 文本、结构化卡片、taskUpdate
|
|
|
+ Dispatch->>Normalizer: 校验 Dify 输出
|
|
|
+ Normalizer-->>Dispatch: NormalizedAgentResponse
|
|
|
+
|
|
|
+ alt 返回卡片
|
|
|
+ Dispatch->>Card: 创建 cardInstance
|
|
|
+ Card-->>Dispatch: cardInstanceId
|
|
|
+ end
|
|
|
+
|
|
|
+ Dispatch->>Conv: 保存消息和任务上下文
|
|
|
+ Dispatch->>Audit: 写审计、用量、Trace
|
|
|
+ Dispatch-->>C: SSE 返回文本、卡片、完成事件
|
|
|
+ end
|
|
|
+```
|
|
|
+
|
|
|
+### 6.2 主接口
|
|
|
+
|
|
|
+```http
|
|
|
+POST /api/v1/agent/chat/stream
|
|
|
+```
|
|
|
+
|
|
|
+请求示例:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "conversationId": "conv_001",
|
|
|
+ "message": {
|
|
|
+ "type": "text",
|
|
|
+ "content": "我头疼三天,有点恶心,想挂号"
|
|
|
+ },
|
|
|
+ "deviceContext": {
|
|
|
+ "deviceId": "EMOON-KIOSK-001"
|
|
|
+ },
|
|
|
+ "attachments": [],
|
|
|
+ "clientContext": {
|
|
|
+ "terminalType": "self_service_kiosk",
|
|
|
+ "locale": "zh-CN"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+SSE 事件示例:
|
|
|
+
|
|
|
+```json
|
|
|
+{ "type": "message_delta", "content": "我先了解一下您的头痛情况。" }
|
|
|
+{ "type": "message_completed", "messageId": "msg_001" }
|
|
|
+{ "type": "card_created", "cardInstanceId": "card_001", "cardKey": "department-selection" }
|
|
|
+{ "type": "usage_reported", "traceId": "trace_001" }
|
|
|
+{ "type": "completed", "conversationId": "conv_001" }
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. AgentRouter 设计
|
|
|
+
|
|
|
+### 7.1 Router 的本质
|
|
|
+
|
|
|
+`AgentRouterService` 不是简单意图识别。它是一个后端业务分发器,需要综合判断:
|
|
|
+
|
|
|
+```text
|
|
|
+设备策略
|
|
|
++ 当前场景
|
|
|
++ 当前会话
|
|
|
++ 当前任务状态
|
|
|
++ 等待卡片状态
|
|
|
++ 确定性规则
|
|
|
++ FAQ 命中
|
|
|
++ LLM 意图分类
|
|
|
+```
|
|
|
+
|
|
|
+正式路由顺序:
|
|
|
+
|
|
|
+| 顺序 | 决策层 | 说明 |
|
|
|
+|---:|---|---|
|
|
|
+| 1 | 设备策略 | 当前终端能不能做这件事 |
|
|
|
+| 2 | 当前任务 | 有 activeTask 时优先推进当前任务 |
|
|
|
+| 3 | 等待卡片 | 有待操作卡片时优先处理卡片动作或提示 |
|
|
|
+| 4 | 确定性规则 | 舌诊、挂号、带我去、我要建档 |
|
|
|
+| 5 | FAQ / 知识库 | 公共问答优先走导诊 Agent |
|
|
|
+| 6 | LLM 分类 | 规则无法判断时兜底 |
|
|
|
+| 7 | 反问澄清 | 低置信度时不强行进入流程 |
|
|
|
+
|
|
|
+一期 MVP 不实现完整规则推理引擎,也不把 `deterministicRouter`、`faqRouter`、`llmIntentClassifier` 做成三套复杂子系统。第一期路由收敛为:
|
|
|
+
|
|
|
+```text
|
|
|
+设备策略
|
|
|
+→ 活跃任务优先
|
|
|
+→ 等待卡片优先
|
|
|
+→ 少量确定性关键词(挂号 / 舌诊 / 带我去 / 建档 / 查科室)
|
|
|
+→ FAQ 精确匹配
|
|
|
+→ LLM 兜底分类
|
|
|
+→ 低置信度反问
|
|
|
+```
|
|
|
+
|
|
|
+这样做的依据是行业产品通常不会完全依赖自然语言路由。支付宝、腾讯健康等对话式就医入口背后仍然有固定服务入口、业务上下文和确认卡片;医梦一期也应优先让确定性流程稳定,再逐步扩大 LLM 路由范围。
|
|
|
+
|
|
|
+### 7.2 AgentRouter 决策流程图
|
|
|
+
|
|
|
+```mermaid
|
|
|
+flowchart TD
|
|
|
+ A["收到用户输入"] --> B["设备策略校验"]
|
|
|
+ B --> C{"当前设备是否禁止该能力"}
|
|
|
+ C -->|"是"| D["返回禁止提示"]
|
|
|
+ C -->|"否"| E["查询当前会话任务"]
|
|
|
+
|
|
|
+ E --> F{"是否存在活跃任务"}
|
|
|
+ F -->|"是"| G{"用户是否明确取消或切换任务"}
|
|
|
+ G -->|"否"| H["优先推进当前任务"]
|
|
|
+ G -->|"是"| I["取消或切换任务"]
|
|
|
+
|
|
|
+ F -->|"否"| J["查询等待中的卡片"]
|
|
|
+ I --> K["确定性规则识别"]
|
|
|
+
|
|
|
+ J --> L{"是否存在待操作卡片"}
|
|
|
+ L -->|"是"| M{"用户输入能否映射为卡片动作"}
|
|
|
+ M -->|"是"| N["转入 AgentActionOrchestrator + CardActionService"]
|
|
|
+ M -->|"否"| O["提示先完成卡片操作"]
|
|
|
+
|
|
|
+ L -->|"否"| K
|
|
|
+ H --> P["进入任务对应 Agent"]
|
|
|
+
|
|
|
+ K --> Q{"规则是否命中"}
|
|
|
+ Q -->|"是"| R["生成路由决策"]
|
|
|
+ Q -->|"否"| S["FAQ 或知识库检索"]
|
|
|
+
|
|
|
+ S --> T{"FAQ 是否命中"}
|
|
|
+ T -->|"是"| U["路由到导诊 Agent"]
|
|
|
+ T -->|"否"| V["LLM 意图分类"]
|
|
|
+
|
|
|
+ V --> W{"置信度是否足够"}
|
|
|
+ W -->|"否"| X["反问澄清"]
|
|
|
+ W -->|"是"| Y["生成路由决策"]
|
|
|
+
|
|
|
+ P --> Y
|
|
|
+ R --> Y
|
|
|
+ U --> Y
|
|
|
+ Y --> Z["AgentDispatcher 调用目标 Dify App"]
|
|
|
+```
|
|
|
+
|
|
|
+### 7.3 Router 决策输出结构
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "routeAgentCode": "opd-triage-agent",
|
|
|
+ "scenarioCode": "OUTPATIENT_TRIAGE",
|
|
|
+ "intentCode": "SYMPTOM_TRIAGE",
|
|
|
+ "confidence": 0.91,
|
|
|
+ "routeReason": "用户描述症状,且当前无活跃任务",
|
|
|
+ "taskPolicy": "CREATE_NEW_TASK",
|
|
|
+ "riskLevel": "L2",
|
|
|
+ "requiredIdentityLevel": "ANONYMOUS_ALLOWED",
|
|
|
+ "allowedOnCurrentDevice": true,
|
|
|
+ "slots": {
|
|
|
+ "symptom": "头疼三天,有点恶心"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 7.4 Router 伪代码
|
|
|
+
|
|
|
+```java
|
|
|
+public RouteDecision route(AgentChatRequest request, ChatContext context) {
|
|
|
+ DevicePolicyResult devicePolicy = devicePolicyService.check(request, context);
|
|
|
+ if (devicePolicy.isBlocked()) {
|
|
|
+ return RouteDecision.blocked(devicePolicy.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ TaskInstance activeTask = taskStateService.getActiveTask(context.getConversationId());
|
|
|
+ if (activeTask != null && !request.isExplicitInterrupt()) {
|
|
|
+ return taskRouter.routeByActiveTask(request, context, activeTask);
|
|
|
+ }
|
|
|
+
|
|
|
+ CardInstance waitingCard = cardService.getWaitingCard(context.getConversationId());
|
|
|
+ if (waitingCard != null && !request.isExplicitCancelOrSwitch()) {
|
|
|
+ return cardWaitingRouter.route(request, context, waitingCard);
|
|
|
+ }
|
|
|
+
|
|
|
+ // MVP: deterministicRouter 先做少量关键词和场景规则,不建设完整规则引擎。
|
|
|
+ Optional<RouteDecision> ruleDecision = deterministicRouter.tryRoute(request, context);
|
|
|
+ if (ruleDecision.isPresent()) {
|
|
|
+ return ruleDecision.get();
|
|
|
+ }
|
|
|
+
|
|
|
+ // MVP: faqRouter 先做精确命中或知识库召回,不做复杂多轮 FAQ 策略。
|
|
|
+ Optional<RouteDecision> faqDecision = faqRouter.tryRoute(request, context);
|
|
|
+ if (faqDecision.isPresent()) {
|
|
|
+ return faqDecision.get();
|
|
|
+ }
|
|
|
+
|
|
|
+ RouteDecision llmDecision = llmIntentClassifier.classify(request, context);
|
|
|
+ if (llmDecision.getConfidence() < 0.65) {
|
|
|
+ return RouteDecision.askClarification();
|
|
|
+ }
|
|
|
+
|
|
|
+ return llmDecision;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. Dify Workflow 设计
|
|
|
+
|
|
|
+### 8.1 四个 Dify App
|
|
|
+
|
|
|
+| Dify App | 场景 | 输入 | 输出 |
|
|
|
+|---|---|---|---|
|
|
|
+| `opd-guide-agent` | FAQ、科室介绍、路线、公共导诊 | 用户问题、设备位置、医院知识库 | 文本、路线卡片、导航意图 |
|
|
|
+| `opd-triage-agent` | 症状追问、预问诊、科室推荐 | 症状描述、追问历史、患者基础信息 | 追问卡片、科室推荐卡片、风险提示 |
|
|
|
+| `opd-registration-agent` | 建档和挂号流程话术编排 | 患者身份、科室、医生、号源、任务状态 | 建档引导、确认卡片、异常解释 |
|
|
|
+| `tongue-diagnosis-agent` | 舌诊问诊、结果解释、后续建议 | 主诉、舌诊结果摘要、患者上下文 | 舌象采集卡片、结果解释、调养建议 |
|
|
|
+
|
|
|
+### 8.2 Dify App 路由关系
|
|
|
+
|
|
|
+```mermaid
|
|
|
+flowchart TD
|
|
|
+ A["统一入口客户端输入"] --> B["AgentRouterService"]
|
|
|
+ B --> C{"意图或任务类型"}
|
|
|
+
|
|
|
+ C -->|"FAQ / 科室 / 路线"| D["opd-guide-agent"]
|
|
|
+ C -->|"症状分诊 / 预问诊"| E["opd-triage-agent"]
|
|
|
+ C -->|"建档 / 挂号"| F["opd-registration-agent"]
|
|
|
+ C -->|"舌诊"| G["tongue-diagnosis-agent"]
|
|
|
+
|
|
|
+ D --> H["DifyOutputNormalizer"]
|
|
|
+ E --> H
|
|
|
+ F --> H
|
|
|
+ G --> H
|
|
|
+
|
|
|
+ H --> I["Card Runtime"]
|
|
|
+ H --> J["ConversationService"]
|
|
|
+ H --> K["Audit / Meter / Trace"]
|
|
|
+```
|
|
|
+
|
|
|
+### 8.3 Dify 输入协议
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "projectId": "hospital_demo",
|
|
|
+ "hospitalId": "H001",
|
|
|
+ "device": {
|
|
|
+ "deviceId": "EMOON-KIOSK-001",
|
|
|
+ "deviceType": "self_service_kiosk",
|
|
|
+ "location": "门诊大厅一楼",
|
|
|
+ "capabilities": ["touch", "camera", "id_card_ocr", "printer"]
|
|
|
+ },
|
|
|
+ "scene": {
|
|
|
+ "sceneCode": "outpatient_kiosk",
|
|
|
+ "allowedActions": ["create_record", "triage", "registration", "tongue_diagnosis"]
|
|
|
+ },
|
|
|
+ "conversation": {
|
|
|
+ "conversationId": "conv_001",
|
|
|
+ "recentMessages": []
|
|
|
+ },
|
|
|
+ "task": {
|
|
|
+ "taskType": "REGISTRATION",
|
|
|
+ "currentStep": "COLLECT_SYMPTOM",
|
|
|
+ "context": {}
|
|
|
+ },
|
|
|
+ "patient": {
|
|
|
+ "identityStatus": "UNKNOWN",
|
|
|
+ "patientId": null
|
|
|
+ },
|
|
|
+ "router": {
|
|
|
+ "intentCode": "SYMPTOM_TRIAGE",
|
|
|
+ "slots": {
|
|
|
+ "symptom": "头疼三天,有点恶心"
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 8.4 Dify 输出协议
|
|
|
+
|
|
|
+Dify 必须输出结构化 JSON。不能只自由输出 Markdown。
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "schemaVersion": "1.0",
|
|
|
+ "answer": "根据您的描述,我建议先补充几个问题,以便判断更合适的科室。",
|
|
|
+ "taskUpdate": {
|
|
|
+ "taskType": "REGISTRATION",
|
|
|
+ "currentStep": "COLLECT_SYMPTOM",
|
|
|
+ "status": "ACTIVE",
|
|
|
+ "contextPatch": {
|
|
|
+ "symptom": "头疼三天,有点恶心"
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "cards": [
|
|
|
+ {
|
|
|
+ "cardKey": "symptom-followup",
|
|
|
+ "cardData": {
|
|
|
+ "questions": [
|
|
|
+ "头痛是持续性还是阵发性?",
|
|
|
+ "是否伴随发热或视物模糊?"
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ "actions": [
|
|
|
+ {
|
|
|
+ "actionName": "submit_symptom_followup",
|
|
|
+ "label": "提交补充信息"
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "safety": {
|
|
|
+ "riskLevel": "L2",
|
|
|
+ "needHuman": false,
|
|
|
+ "disclaimer": "本结果仅用于就医引导,不能替代医生诊断。"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 8.5 输出校验规则
|
|
|
+
|
|
|
+DifyOutputNormalizer 是 `DifyAgentEngine` 的内部后处理组件,不是独立 Maven 模块,也不属于 `emoon-openplatform`。它应落在 `emoon-ai-agent` 的 adapter/application 边界内,职责是把 Dify 的文本、结构化 JSON、SSE 事件和 Workflow 输出归一成平台稳定事件。
|
|
|
+
|
|
|
+| 校验项 | 处理 |
|
|
|
+|---|---|
|
|
|
+| JSON 不合法 | 降级为普通文本,记录 Dify 输出告警 |
|
|
|
+| `cardKey` 不存在 | 不创建卡片 |
|
|
|
+| `cardData` 不符合 schema | 不创建卡片,返回平台错误 |
|
|
|
+| `actionName` 未在卡片定义中 | 拒绝非法动作 |
|
|
|
+| 当前设备不允许该卡片 | 拒绝创建卡片 |
|
|
|
+| 风险等级过高 | 强制人工确认或二次确认 |
|
|
|
+| 敏感字段进入 Dify 输出 | 拦截并记录安全告警 |
|
|
|
+
|
|
|
+Normalizer 不执行卡片动作、不调用 HIS、不扣能力值。校验不通过时,只能拒绝创建卡片、记录告警并返回可解释降级结果,避免 Dify 输出不稳定影响后续 Card Runtime、审计和计量。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 9. TaskStateService 设计
|
|
|
+
|
|
|
+### 9.1 任务状态表
|
|
|
+
|
|
|
+建议新增表:`ai_task_instance`
|
|
|
+
|
|
|
+| 字段 | 说明 |
|
|
|
+|---|---|
|
|
|
+| `id` | 任务实例 ID |
|
|
|
+| `conversation_id` | 会话 ID |
|
|
|
+| `task_type` | `GUIDE` / `TRIAGE` / `REGISTRATION` / `TONGUE_DIAGNOSIS` |
|
|
|
+| `status` | `ACTIVE` / `WAITING_CARD_ACTION` / `PROCESSING` / `COMPLETED` / `CANCELLED` / `FAILED` |
|
|
|
+| `current_step` | 当前步骤 |
|
|
|
+| `agent_code` | 当前绑定 Dify App |
|
|
|
+| `context_json` | 任务上下文 |
|
|
|
+| `patient_id` | 患者 ID,可为空 |
|
|
|
+| `device_id` | 发起设备 |
|
|
|
+| `trace_id` | 链路 ID |
|
|
|
+| `created_at` | 创建时间 |
|
|
|
+| `updated_at` | 更新时间 |
|
|
|
+
|
|
|
+### 9.2 挂号任务状态机
|
|
|
+
|
|
|
+```text
|
|
|
+REGISTRATION_START
|
|
|
+→ CHECK_PATIENT_IDENTITY
|
|
|
+→ NEED_CREATE_RECORD
|
|
|
+→ PATIENT_RECORD_CONFIRM
|
|
|
+→ COLLECT_SYMPTOM
|
|
|
+→ TRIAGE_RECOMMEND_DEPARTMENT
|
|
|
+→ WAIT_SELECT_DEPARTMENT
|
|
|
+→ QUERY_DOCTOR_SCHEDULE
|
|
|
+→ WAIT_SELECT_DOCTOR
|
|
|
+→ WAIT_SELECT_TIME_SLOT
|
|
|
+→ WAIT_CONFIRM_APPOINTMENT
|
|
|
+→ CREATE_APPOINTMENT
|
|
|
+→ APPOINTMENT_SUCCESS
|
|
|
+```
|
|
|
+
|
|
|
+一期 Mock 阶段不要求一次落完完整挂号状态机。核心验证步骤收敛为:
|
|
|
+
|
|
|
+```text
|
|
|
+COLLECT_SYMPTOM
|
|
|
+→ TRIAGE_RECOMMEND_DEPARTMENT
|
|
|
+→ WAIT_SELECT_DEPARTMENT
|
|
|
+→ QUERY_DOCTOR_SCHEDULE
|
|
|
+→ WAIT_SELECT_DOCTOR
|
|
|
+→ WAIT_SELECT_TIME_SLOT
|
|
|
+→ WAIT_CONFIRM_APPOINTMENT
|
|
|
+→ CREATE_APPOINTMENT
|
|
|
+→ APPOINTMENT_SUCCESS
|
|
|
+```
|
|
|
+
|
|
|
+`CHECK_PATIENT_IDENTITY`、`NEED_CREATE_RECORD`、`PATIENT_RECORD_CONFIRM` 可以先复用自助机既有身份证/就诊卡能力或使用 Mock 患者上下文。OCR 建档不作为第一期挂号闭环的硬前置,避免把 MVP 卡在硬件和身份接口联调上。
|
|
|
+
|
|
|
+### 9.3 舌诊任务状态机
|
|
|
+
|
|
|
+```text
|
|
|
+TONGUE_START
|
|
|
+→ CHECK_PATIENT_IDENTITY
|
|
|
+→ COLLECT_CHIEF_COMPLAINT
|
|
|
+→ WAIT_UPLOAD_TONGUE_IMAGE
|
|
|
+→ CHECK_IMAGE_QUALITY
|
|
|
+→ START_TONGUE_DIAGNOSIS
|
|
|
+→ POLL_TONGUE_RESULT
|
|
|
+→ SHOW_TONGUE_RESULT
|
|
|
+→ COMPLETED
|
|
|
+```
|
|
|
+
|
|
|
+### 9.4 导航任务状态机
|
|
|
+
|
|
|
+```text
|
|
|
+GUIDE_START
|
|
|
+→ UNDERSTAND_DESTINATION
|
|
|
+→ MATCH_LOCATION
|
|
|
+→ SHOW_ROUTE_CARD
|
|
|
+→ SEND_ROBOT_NAV_COMMAND
|
|
|
+→ WAIT_DEVICE_EVENT
|
|
|
+→ NAVIGATION_SUCCESS / NAVIGATION_FAILED
|
|
|
+```
|
|
|
+
|
|
|
+### 9.5 状态优先原则
|
|
|
+
|
|
|
+```text
|
|
|
+有 activeTask 时,优先推进当前任务。
|
|
|
+没有 activeTask 时,再做意图路由。
|
|
|
+```
|
|
|
+
|
|
|
+示例:
|
|
|
+
|
|
|
+```text
|
|
|
+当前任务:REGISTRATION
|
|
|
+当前步骤:WAIT_SELECT_TIME_SLOT
|
|
|
+用户输入:下午
|
|
|
+系统行为:解释为选择下午时段
|
|
|
+错误行为:重新做意图识别,把“下午”判成未知意图
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 10. Card Runtime 设计
|
|
|
+
|
|
|
+### 10.1 卡片职责
|
|
|
+
|
|
|
+卡片不是普通 UI 组件,而是确定性医疗业务动作的承载单元。
|
|
|
+
|
|
|
+| 卡片 | 用途 |
|
|
|
+|---|---|
|
|
|
+| `idcard-capture` | 身份证采集 |
|
|
|
+| `patient-record-confirm` | 建档信息确认 |
|
|
|
+| `symptom-followup` | 症状追问 |
|
|
|
+| `department-selection` | 科室推荐和选择 |
|
|
|
+| `doctor-selection` | 医生选择 |
|
|
|
+| `time-slot-selection` | 号源时段选择 |
|
|
|
+| `confirm-appointment` | 挂号最终确认 |
|
|
|
+| `appointment-success` | 挂号结果 |
|
|
|
+| `route-card` | 路线展示 / 导航确认 |
|
|
|
+| `tongue-capture` | 舌象采集 |
|
|
|
+| `tongue-diagnosis-result` | 舌诊结果 |
|
|
|
+
|
|
|
+### 10.2 卡片状态机
|
|
|
+
|
|
|
+```text
|
|
|
+active
|
|
|
+→ submitted
|
|
|
+→ processing
|
|
|
+→ completed
|
|
|
+
|
|
|
+active
|
|
|
+→ expired
|
|
|
+
|
|
|
+processing
|
|
|
+→ failed
|
|
|
+→ active
|
|
|
+```
|
|
|
+
|
|
|
+### 10.3 卡片动作闭环图
|
|
|
+
|
|
|
+```mermaid
|
|
|
+flowchart LR
|
|
|
+ subgraph FE["统一入口客户端"]
|
|
|
+ A1["展示卡片"]
|
|
|
+ A2["用户点击按钮"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph CR["Card Runtime"]
|
|
|
+ B1["校验卡片实例"]
|
|
|
+ B2["幂等校验"]
|
|
|
+ B3["记录动作日志"]
|
|
|
+ B4["更新卡片状态"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph TS["TaskStateService"]
|
|
|
+ C1["读取任务状态"]
|
|
|
+ C2["推进当前步骤"]
|
|
|
+ C3["保存任务上下文"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph Tool["工具与业务执行"]
|
|
|
+ D1["MCP Tool Service"]
|
|
|
+ D2["HisMockAdapter 或 RealHisAdapter"]
|
|
|
+ D3["TongueDiagnosisAdapter"]
|
|
|
+ D4["DeviceCommandService"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph AI["Dify 或 Agent"]
|
|
|
+ E1["可选 synthetic message"]
|
|
|
+ E2["生成下一轮文本或卡片"]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph Log["审计与计量"]
|
|
|
+ F1["AuditLog"]
|
|
|
+ F2["MeterEvent"]
|
|
|
+ F3["TraceLog"]
|
|
|
+ end
|
|
|
+
|
|
|
+ A1 --> A2 --> B1 --> B2 --> B3 --> C1 --> C2
|
|
|
+ C2 --> AG["AgentActionOrchestrator"]
|
|
|
+ AG --> D1
|
|
|
+ D1 --> D2
|
|
|
+ AG --> D3
|
|
|
+ AG --> D4
|
|
|
+ D2 --> B4
|
|
|
+ D3 --> B4
|
|
|
+ D4 --> B4
|
|
|
+ B4 --> C3
|
|
|
+ C3 --> E1 --> E2
|
|
|
+ B4 --> F1
|
|
|
+ B4 --> F2
|
|
|
+ B4 --> F3
|
|
|
+```
|
|
|
+
|
|
|
+### 10.4 卡片动作接口
|
|
|
+
|
|
|
+```http
|
|
|
+POST /api/v1/cards/{cardInstanceId}/actions/{actionName}
|
|
|
+```
|
|
|
+
|
|
|
+请求示例:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "conversationId": "conv_001",
|
|
|
+ "idempotencyKey": "idem_001",
|
|
|
+ "payload": {
|
|
|
+ "departmentId": "neurology"
|
|
|
+ },
|
|
|
+ "deviceContext": {
|
|
|
+ "deviceId": "EMOON-KIOSK-001"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 11. MCP/HIS Adapter 设计
|
|
|
+
|
|
|
+### 11.1 Adapter 模式
|
|
|
+
|
|
|
+在没有真实 HIS 接口前,必须显式使用 `HisMockAdapter`。
|
|
|
+
|
|
|
+```text
|
|
|
+CardActionController
|
|
|
+→ AgentActionOrchestrator
|
|
|
+→ CardActionService(状态校验 / 幂等 / 快照)
|
|
|
+→ McpToolService
|
|
|
+→ HospitalAdapter SPI
|
|
|
+→ HisMockAdapter
|
|
|
+```
|
|
|
+
|
|
|
+后续接入真实接口后:
|
|
|
+
|
|
|
+```text
|
|
|
+CardActionController
|
|
|
+→ AgentActionOrchestrator
|
|
|
+→ CardActionService(状态校验 / 幂等 / 快照)
|
|
|
+→ McpToolService
|
|
|
+→ HospitalAdapter SPI
|
|
|
+→ RealHisAdapter
|
|
|
+```
|
|
|
+
|
|
|
+这样保持唯一工具调用链:卡片负责确定性状态,Agent 编排负责业务动作,MCP 负责工具治理。禁止出现 Card Runtime 直接绕过 Agent 编排调用 HIS/舌诊/设备 SDK。
|
|
|
+
|
|
|
+### 11.2 P0 工具清单
|
|
|
+
|
|
|
+| 工具 | Mock 阶段 | 真实阶段 |
|
|
|
+|---|---|---|
|
|
|
+| `queryDepartments` | 返回静态科室 | HIS/院内基础数据 |
|
|
|
+| `queryDoctors` | 根据科室返回模拟医生 | HIS 医生字典/排班系统 |
|
|
|
+| `querySchedules` | 返回模拟排班号源 | HIS 号源接口 |
|
|
|
+| `createPatient` | 创建 Mock 患者 ID | 患者主索引 / HIS 建档 |
|
|
|
+| `lockSchedule` | Mock 锁号 | HIS 锁号接口 |
|
|
|
+| `createAppointment` | Mock 挂号结果 | HIS 挂号/预约接口 |
|
|
|
+| `cancelAppointment` | P1 | HIS 退号/取消接口 |
|
|
|
+| `queryQueue` | P1 | 叫号系统 |
|
|
|
+
|
|
|
+### 11.3 禁止做法
|
|
|
+
|
|
|
+```text
|
|
|
+禁止 ChatService 直接随机生成医生
|
|
|
+禁止前端写死号源
|
|
|
+禁止 Dify 直接返回“挂号成功”
|
|
|
+禁止 AppointmentService 直接生成二维码冒充真实支付
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 12. File Service 与舌诊接入
|
|
|
+
|
|
|
+### 12.1 文件原则
|
|
|
+
|
|
|
+所有图像、音频、报告、身份证、舌象都必须先上传到 File Service,业务接口只传 `fileId`。
|
|
|
+
|
|
|
+```text
|
|
|
+前端上传文件
|
|
|
+→ File Service 保存元数据
|
|
|
+→ 返回 fileId
|
|
|
+→ Agent / CardAction 使用 fileId
|
|
|
+→ Adapter 在后端受控访问文件
|
|
|
+```
|
|
|
+
|
|
|
+### 12.2 文件元数据字段
|
|
|
+
|
|
|
+| 字段 | 说明 |
|
|
|
+|---|---|
|
|
|
+| `file_id` | 文件 ID |
|
|
|
+| `project_id` | 项目 ID |
|
|
|
+| `hospital_id` | 医院 ID |
|
|
|
+| `device_id` | 上传设备 |
|
|
|
+| `business_type` | `ID_CARD` / `TONGUE_IMAGE` / `REPORT_IMAGE` / `AUDIO` |
|
|
|
+| `sha256` | 文件摘要,用于去重和审计 |
|
|
|
+| `storage_path` | 存储路径 |
|
|
|
+| `retention_policy` | 留存策略 |
|
|
|
+| `sensitive_level` | 敏感等级 |
|
|
|
+| `created_at` | 创建时间 |
|
|
|
+
|
|
|
+### 12.3 舌诊 Adapter
|
|
|
+
|
|
|
+| 方法 | 说明 |
|
|
|
+|---|---|
|
|
|
+| `checkImageQuality(fileId)` | 校验是否为舌象、清晰度、光照等 |
|
|
|
+| `startDiagnosis(fileId, chiefComplaint, patientContext)` | 调用已有舌诊接口启动诊断 |
|
|
|
+| `queryDiagnosisResult(diagnosisId)` | 查询结果 |
|
|
|
+| `normalizeResult(rawResult)` | 归一化为平台舌诊结果结构 |
|
|
|
+
|
|
|
+### 12.4 舌诊红线
|
|
|
+
|
|
|
+| 错误做法 | 正确做法 |
|
|
|
+|---|---|
|
|
|
+| 前端直接调用舌诊接口 | 后端统一通过 `TongueDiagnosisAdapter` 调用 |
|
|
|
+| base64 直接塞进 Dify | 先上传文件,后续只传 `fileId` 或摘要 |
|
|
|
+| 舌诊结果写成诊断 | 表述为 AI 辅助分析,医生确认优先 |
|
|
|
+| 导诊大屏采集舌象 | 禁止 |
|
|
|
+| 无文件审计 | 必须记录文件上传、访问、删除、诊断调用 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 13. 业务流程一:建档 + 分诊 + 挂号 Mock
|
|
|
+
|
|
|
+### 13.1 完整流程图(已修复 Mermaid)
|
|
|
+
|
|
|
+```mermaid
|
|
|
+flowchart TD
|
|
|
+ A["用户提出挂号或描述症状"] --> B["调用 agent chat stream"]
|
|
|
+ B --> C["AgentRouter 路由"]
|
|
|
+ C --> D{"当前设备是否允许挂号"}
|
|
|
+
|
|
|
+ D -->|"导诊大屏"| E["提示前往自助机办理实名挂号"]
|
|
|
+ D -->|"机器人"| F["机器人先推荐科室并引导到自助机"]
|
|
|
+ D -->|"自助机"| G["创建 REGISTRATION 任务"]
|
|
|
+
|
|
|
+ G --> H{"是否已有患者身份"}
|
|
|
+
|
|
|
+ H -->|"否"| I["创建身份证采集卡片"]
|
|
|
+ I --> J["调用 files upload 上传身份证图片"]
|
|
|
+ J --> K["OCR 识别身份证"]
|
|
|
+ K --> L["创建建档确认卡片"]
|
|
|
+ L --> M["CardAction 确认建档"]
|
|
|
+ M --> N["HisMockAdapter 创建患者"]
|
|
|
+ N --> O["患者身份就绪"]
|
|
|
+
|
|
|
+ H -->|"是"| O
|
|
|
+
|
|
|
+ O --> P{"是否已有症状信息"}
|
|
|
+ P -->|"否"| Q["registration agent 追问症状"]
|
|
|
+ Q --> R["用户补充症状"]
|
|
|
+ R --> S["triage agent 症状分析"]
|
|
|
+
|
|
|
+ P -->|"是"| S
|
|
|
+
|
|
|
+ S --> T["创建科室推荐卡片"]
|
|
|
+ T --> U["用户选择科室"]
|
|
|
+ U --> V["MCP 查询医生"]
|
|
|
+ V --> W["创建医生选择卡片"]
|
|
|
+
|
|
|
+ W --> X["用户选择医生"]
|
|
|
+ X --> Y["MCP 查询排班"]
|
|
|
+ Y --> Z["创建时段选择卡片"]
|
|
|
+
|
|
|
+ Z --> AA["用户选择时段"]
|
|
|
+ AA --> AB["MCP 锁定号源"]
|
|
|
+ AB --> AC["创建确认挂号卡片"]
|
|
|
+
|
|
|
+ AC --> AD["用户最终确认"]
|
|
|
+ AD --> AE["MCP 创建预约"]
|
|
|
+ AE --> AF["返回挂号成功卡片"]
|
|
|
+ AF --> AG["写审计 用量 Trace"]
|
|
|
+```
|
|
|
+
|
|
|
+### 13.2 后端实现步骤
|
|
|
+
|
|
|
+| 步骤 | 模块 | 说明 |
|
|
|
+|---:|---|---|
|
|
|
+| 1 | `AgentRouterService` | 识别为挂号/症状分诊,并判断设备是否允许 |
|
|
|
+| 2 | `TaskStateService` | 创建 `REGISTRATION` 任务 |
|
|
|
+| 3 | `CardRuntime` | 创建身份证采集卡 |
|
|
|
+| 4 | `FileService` | 上传身份证图片,返回 `fileId` |
|
|
|
+| 5 | `OCRService` | 识别身份证信息 |
|
|
|
+| 6 | `CardRuntime` | 创建建档确认卡 |
|
|
|
+| 7 | `AgentActionOrchestrator + CardActionService` | 用户确认后进入建档动作 |
|
|
|
+| 8 | `HisMockAdapter` | 创建 Mock 患者 |
|
|
|
+| 9 | `opd-triage-agent` | 多轮追问与科室推荐 |
|
|
|
+| 10 | `McpToolService` | 查询医生、排班、锁号、创建预约 |
|
|
|
+| 11 | `CardRuntime` | 返回医生卡、时段卡、确认卡、成功卡 |
|
|
|
+| 12 | `Audit/Meter/Trace` | 全链路记录 |
|
|
|
+
|
|
|
+### 13.3 开发验收标准
|
|
|
+
|
|
|
+| 验收项 | 标准 |
|
|
|
+|---|---|
|
|
|
+| 自助机可发起挂号 | 能进入 REGISTRATION 任务 |
|
|
|
+| 导诊大屏不可挂号 | 返回明确限制提示 |
|
|
|
+| 机器人不做实名挂号 | 可以推荐科室,并引导去自助机 |
|
|
|
+| 身份证上传 | 走 File Service,不直接 base64 穿透业务接口 |
|
|
|
+| 医生号源 | 全部来自 `HisMockAdapter`,不写死在前端或 Dify |
|
|
|
+| 确认挂号 | 必须走 CardAction,且带幂等键 |
|
|
|
+| 结果可恢复 | conversationId 可查历史消息和卡片状态 |
|
|
|
+| 审计完整 | 能查到路由、任务、卡片动作、工具调用、traceId |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 14. 业务流程二:舌诊
|
|
|
+
|
|
|
+### 14.1 完整流程图(已修复 Mermaid)
|
|
|
+
|
|
|
+```mermaid
|
|
|
+flowchart TD
|
|
|
+ A["用户提出舌诊"] --> B["调用 agent chat stream"]
|
|
|
+ B --> C["AgentRouter 路由到舌诊 Agent"]
|
|
|
+ C --> D{"当前设备是否允许舌象采集"}
|
|
|
+
|
|
|
+ D -->|"导诊大屏"| E["禁止采集隐私图像 提示去自助机"]
|
|
|
+ D -->|"机器人"| F["机器人讲解并引导到自助机"]
|
|
|
+ D -->|"自助机"| G["创建 TONGUE_DIAGNOSIS 任务"]
|
|
|
+
|
|
|
+ G --> H{"是否需要患者身份"}
|
|
|
+ H -->|"需要且无身份"| I["创建临时身份或进入建档流程"]
|
|
|
+ H -->|"已有身份或允许临时"| J["Dify 采集主诉"]
|
|
|
+ I --> J
|
|
|
+
|
|
|
+ J --> K["创建舌象采集卡片"]
|
|
|
+ K --> L["调用 files upload 上传舌象图片"]
|
|
|
+ L --> M["FileService 保存 fileId"]
|
|
|
+
|
|
|
+ M --> N["CardAction 提交舌象图片"]
|
|
|
+ N --> O["TongueDiagnosisAdapter 校验图片质量"]
|
|
|
+
|
|
|
+ O --> P{"图片质量是否合格"}
|
|
|
+ P -->|"否"| Q["提示重新拍摄"]
|
|
|
+ Q --> K
|
|
|
+ P -->|"是"| R["TongueDiagnosisAdapter 启动诊断"]
|
|
|
+
|
|
|
+ R --> S["轮询或查询诊断结果"]
|
|
|
+ S --> T["返回舌诊结果"]
|
|
|
+ T --> U["创建舌诊结果卡片"]
|
|
|
+
|
|
|
+ U --> V["舌诊 Agent 解释结果"]
|
|
|
+ V --> W["任务完成"]
|
|
|
+ W --> X["审计 用量 Trace"]
|
|
|
+```
|
|
|
+
|
|
|
+### 14.2 后端实现步骤
|
|
|
+
|
|
|
+| 步骤 | 模块 | 说明 |
|
|
|
+|---:|---|---|
|
|
|
+| 1 | `AgentRouterService` | 识别为舌诊,并做设备策略校验 |
|
|
|
+| 2 | `TaskStateService` | 创建 `TONGUE_DIAGNOSIS` 任务 |
|
|
|
+| 3 | `tongue-diagnosis-agent` | 收集主诉 |
|
|
|
+| 4 | `CardRuntime` | 创建舌象采集卡 |
|
|
|
+| 5 | `FileService` | 上传舌象图片,返回 `fileId` |
|
|
|
+| 6 | `AgentActionOrchestrator + CardActionService` | 提交舌象图片动作 |
|
|
|
+| 7 | `TongueDiagnosisAdapter` | 校验图片质量、启动诊断、查询结果 |
|
|
|
+| 8 | `CardRuntime` | 创建舌诊结果卡 |
|
|
|
+| 9 | `tongue-diagnosis-agent` | 对结果做患者可理解解释 |
|
|
|
+| 10 | `Audit/Meter/Trace` | 全链路记录 |
|
|
|
+
|
|
|
+### 14.3 舌诊验收标准
|
|
|
+
|
|
|
+| 验收项 | 标准 |
|
|
|
+|---|---|
|
|
|
+| 导诊大屏不能舌诊采集 | 返回隐私限制提示 |
|
|
|
+| 自助机可以舌诊 | 能上传舌象并获取结果 |
|
|
|
+| 机器人只做引导 | 不强依赖机器人摄像头采集质量 |
|
|
|
+| 文件必须有 `fileId` | 不允许 base64 直接进入 Dify |
|
|
|
+| 舌诊结果卡片化 | 结果以 `tongue-diagnosis-result` 卡展示 |
|
|
|
+| 结果表述安全 | 强调 AI 辅助分析,不替代医生诊断 |
|
|
|
+| 审计完整 | 文件上传、诊断调用、结果展示均可追踪 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 15. 业务流程三:机器人导诊导航
|
|
|
+
|
|
|
+### 15.1 完整流程图
|
|
|
+
|
|
|
+```mermaid
|
|
|
+flowchart TD
|
|
|
+ A["用户提出路线或带路请求"] --> B["调用 agent chat stream"]
|
|
|
+ B --> C["AgentRouter 判断为导诊导航"]
|
|
|
+
|
|
|
+ C --> D{"当前设备类型"}
|
|
|
+
|
|
|
+ D -->|"机器人"| E["允许机器人导航"]
|
|
|
+ D -->|"导诊大屏"| F["只展示路线图或文字路线"]
|
|
|
+ D -->|"自助机"| G["展示路线并可打印或扫码带走"]
|
|
|
+
|
|
|
+ E --> H["guide agent 理解目标地点"]
|
|
|
+ F --> H
|
|
|
+ G --> H
|
|
|
+
|
|
|
+ H --> I["LocationService 匹配医院点位"]
|
|
|
+ I --> J{"是否匹配到地点"}
|
|
|
+
|
|
|
+ J -->|"否"| K["反问澄清目标地点"]
|
|
|
+ J -->|"是"| L["创建路线卡片"]
|
|
|
+
|
|
|
+ L --> M{"是否机器人且支持导航"}
|
|
|
+
|
|
|
+ M -->|"否"| N["前端展示路线说明"]
|
|
|
+ M -->|"是"| O["DeviceCommandService 创建导航命令"]
|
|
|
+
|
|
|
+ O --> P["机器人端 Robot Bridge 接收命令"]
|
|
|
+ P --> Q["调用机器人 SDK"]
|
|
|
+ Q --> R["机器人上报导航事件"]
|
|
|
+
|
|
|
+ R --> S["调用 devices events 上报"]
|
|
|
+ S --> T["更新导航任务状态"]
|
|
|
+ T --> U["审计 Trace"]
|
|
|
+```
|
|
|
+
|
|
|
+### 15.2 导航实现边界
|
|
|
+
|
|
|
+| 能力 | 所属模块 |
|
|
|
+|---|---|
|
|
|
+| 用户说“带我去检验科” | Terminal Client 收集输入 |
|
|
|
+| 判断这是导航请求 | AgentRouter |
|
|
|
+| 识别目标地点 | `opd-guide-agent` + LocationService |
|
|
|
+| 判断机器人是否能导航 | Device Policy / capabilities |
|
|
|
+| 下发导航命令 | DeviceCommandService |
|
|
|
+| 调用机器人 SDK | Robot Bridge |
|
|
|
+| 导航成功/失败回传 | DeviceEventService |
|
|
|
+
|
|
|
+一期导航命令采用设备轮询模式(poll),不要求机器人厂商开放公网回调。`DeviceCommandService` 将命令保存到 `ai_device_command`,状态为 `PENDING`,默认 5 分钟过期;设备离线期间不主动推送,下一次心跳或 `/devices/{deviceId}/commands/poll` 时再拉取。超时未 ack 的命令标记为 `EXPIRED`,任务状态回到路线卡片提示人工引导或重新发起。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 16. API 设计清单
|
|
|
+
|
|
|
+### 16.1 P0 接口
|
|
|
+
|
|
|
+| 模块 | 方法 | 路径 | 用途 |
|
|
|
+|---|---|---|---|
|
|
|
+| 通用 | POST | `/auth/token` | 获取 Token,可选 |
|
|
|
+| 通用 | GET | `/health` | 健康检查 |
|
|
|
+| 通用 | GET | `/capabilities` | 查询项目能力 |
|
|
|
+| 设备 | POST | `/devices/register` | 设备注册或激活 |
|
|
|
+| 设备 | POST | `/devices/{deviceId}/heartbeat` | 设备心跳 |
|
|
|
+| 设备 | GET | `/devices/{deviceId}/scene` | 获取设备场景配置 |
|
|
|
+| 设备 | POST | `/devices/{deviceId}/events` | 上报设备事件 |
|
|
|
+| Agent | POST | `/agent/chat` | 同步对话,服务端联调或不支持 SSE 的受控调用 |
|
|
|
+| Agent | POST | `/agent/chat/stream` | SSE 流式对话,统一入口客户端默认使用 |
|
|
|
+| 会话 | POST | `/conversations` | 创建会话 |
|
|
|
+| 会话 | GET | `/conversations/{conversationId}` | 查询会话 |
|
|
|
+| 会话 | GET | `/conversations/{conversationId}/messages` | 查询消息 |
|
|
|
+| 文件 | POST | `/files/upload` | 上传身份证、舌象、报告、音频 |
|
|
|
+| 文件 | GET | `/files/{fileId}` | 查询文件元数据 |
|
|
|
+| 卡片 | GET | `/cards/{cardInstanceId}` | 查询卡片实例 |
|
|
|
+| 卡片 | POST | `/cards/{cardInstanceId}/actions/{actionName}` | 执行卡片动作 |
|
|
|
+| 工具 | POST | `/tools/{toolName}/invoke` | 内部/受控工具调用 |
|
|
|
+| 用量 | GET | `/usage/summary` | 基础用量汇总 |
|
|
|
+
|
|
|
+### 16.2 写操作幂等要求
|
|
|
+
|
|
|
+以下接口必须携带幂等键:
|
|
|
+
|
|
|
+```text
|
|
|
+/cards/{cardInstanceId}/actions/{actionName}
|
|
|
+/tools/{toolName}/invoke 写操作
|
|
|
+/files/upload 可选 clientFileId + sha256 去重
|
|
|
+/devices/{deviceId}/events 高风险事件可加事件 ID
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 17. 数据库表规划
|
|
|
+
|
|
|
+### 17.1 Agent 与路由
|
|
|
+
|
|
|
+| 表 | 说明 |
|
|
|
+|---|---|
|
|
|
+| `ai_agent_app` | 运行态 Agent 定义 |
|
|
|
+| `ai_agent_engine_config` | Dify App 配置、baseUrl、apiKeyRef、输出版本 |
|
|
|
+| `ai_agent_route_rule` | 路由规则配置 |
|
|
|
+| `ai_agent_route_log` | 每次路由决策记录 |
|
|
|
+| `ai_intent_sample` | 真实意图样本沉淀 |
|
|
|
+
|
|
|
+### 17.2 会话与任务
|
|
|
+
|
|
|
+| 表 | 说明 |
|
|
|
+|---|---|
|
|
|
+| `ai_conversation` | 会话主表 |
|
|
|
+| `ai_conversation_message` | 消息表 |
|
|
|
+| `ai_task_instance` | 任务实例和当前步骤 |
|
|
|
+| `ai_task_event_log` | 任务状态流转日志 |
|
|
|
+
|
|
|
+### 17.3 卡片
|
|
|
+
|
|
|
+| 表 | 说明 |
|
|
|
+|---|---|
|
|
|
+| `ai_card_definition` | 卡片定义 |
|
|
|
+| `ai_card_schema` | 卡片数据 schema |
|
|
|
+| `ai_card_instance` | 卡片实例 |
|
|
|
+| `ai_card_action_log` | 卡片动作日志 |
|
|
|
+| `ai_card_snapshot` | 卡片快照 |
|
|
|
+
|
|
|
+### 17.4 设备
|
|
|
+
|
|
|
+| 表 | 说明 |
|
|
|
+|---|---|
|
|
|
+| `ai_device` | 设备主表 |
|
|
|
+| `ai_device_capability` | 设备能力 |
|
|
|
+| `ai_device_scene_binding` | 设备场景绑定 |
|
|
|
+| `ai_device_event` | 设备事件 |
|
|
|
+| `ai_device_command` | 设备命令 |
|
|
|
+
|
|
|
+### 17.5 文件、工具、审计
|
|
|
+
|
|
|
+| 表 | 说明 |
|
|
|
+|---|---|
|
|
|
+| `ai_file_object` | 文件元数据 |
|
|
|
+| `ai_file_access_log` | 文件访问日志 |
|
|
|
+| `ai_tool_catalog` | MCP 工具目录 |
|
|
|
+| `ai_tool_invocation_log` | 工具调用日志 |
|
|
|
+| `ai_hospital_adapter_config` | HIS Mock / Real Adapter 配置 |
|
|
|
+| `ai_audit_log` | 审计日志 |
|
|
|
+| `ai_usage_log` | 原始模型调用记录 |
|
|
|
+| `ai_meter_event` | 能力值事件 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 18. 后端类设计
|
|
|
+
|
|
|
+### 18.1 Controller 层
|
|
|
+
|
|
|
+| 类 | 职责 |
|
|
|
+|---|---|
|
|
|
+| `AgentChatController` | `/agent/chat`、`/agent/chat/stream` |
|
|
|
+| `CardActionController` | `/cards/{cardInstanceId}/actions/{actionName}` |
|
|
|
+| `DeviceController` | 设备注册、心跳、场景、事件 |
|
|
|
+| `FileController` | 文件上传、文件元数据查询 |
|
|
|
+| `ConversationController` | 会话创建、查询、消息查询 |
|
|
|
+
|
|
|
+### 18.2 Application / Service 层
|
|
|
+
|
|
|
+| 类 | 职责 |
|
|
|
+|---|---|
|
|
|
+| `AgentChatApplicationService` | 串联鉴权、设备、会话、任务、路由、Dify、卡片、审计 |
|
|
|
+| `AuthContextResolver` | 项目/医院/终端鉴权上下文 |
|
|
|
+| `DeviceContextResolver` | 解析设备身份、能力、位置 |
|
|
|
+| `SceneProfileService` | 返回终端场景配置 |
|
|
|
+| `ConversationService` | 会话和消息持久化 |
|
|
|
+| `TaskStateService` | 任务状态机 |
|
|
|
+| `AgentRouterService` | 决定调用哪个 Dify App |
|
|
|
+| `AgentDispatcher` | 根据 RouteDecision 调用目标 Agent |
|
|
|
+| `DifyAgentEngine` | Dify API/SSE 协议适配 |
|
|
|
+| `DifyOutputNormalizer` | 校验和归一化 Dify 输出 |
|
|
|
+| `CardDefinitionService` | 卡片定义管理 |
|
|
|
+| `CardInstanceService` | 创建和查询卡片实例 |
|
|
|
+| `AgentActionOrchestrator` | 承接卡片动作后的业务编排,按需调用 MCP、舌诊或设备命令 |
|
|
|
+| `CardActionService` | 处理卡片动作校验、幂等、状态推进和快照 |
|
|
|
+| `McpToolService` | 工具调用统一入口 |
|
|
|
+| `HospitalAdapter` | HIS 适配 SPI |
|
|
|
+| `HisMockAdapter` | Mock HIS 实现 |
|
|
|
+| `TongueDiagnosisAdapter` | 舌诊接口适配 |
|
|
|
+| `DeviceCommandService` | 机器人导航等设备命令 |
|
|
|
+| `AuditLogService` | 审计 |
|
|
|
+| `MeterEventProducer` | 用量事件 |
|
|
|
+
|
|
|
+### 18.3 核心伪代码
|
|
|
+
|
|
|
+```java
|
|
|
+public class AgentChatApplicationService {
|
|
|
+
|
|
|
+ public SseEmitter chatStream(AgentChatRequest request) {
|
|
|
+ ChatContext context = contextBuilder.build(request);
|
|
|
+
|
|
|
+ Conversation conversation = conversationService.createOrLoad(request, context);
|
|
|
+
|
|
|
+ RouteDecision route = agentRouterService.route(request, context);
|
|
|
+ if (route.isBlocked()) {
|
|
|
+ return sseResponseFactory.blocked(route);
|
|
|
+ }
|
|
|
+
|
|
|
+ AgentDispatchRequest dispatchRequest = dispatchRequestFactory.build(
|
|
|
+ request,
|
|
|
+ context,
|
|
|
+ conversation,
|
|
|
+ route
|
|
|
+ );
|
|
|
+
|
|
|
+ return agentDispatcher.dispatchStream(dispatchRequest);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+```java
|
|
|
+public class AgentDispatcher {
|
|
|
+
|
|
|
+ public SseEmitter dispatchStream(AgentDispatchRequest request) {
|
|
|
+ AgentApp app = agentAppService.getByCode(request.getRouteAgentCode());
|
|
|
+ AgentEngine engine = agentEngineFactory.getEngine(app.getEngineType());
|
|
|
+
|
|
|
+ return engine.chatStream(request, event -> {
|
|
|
+ NormalizedAgentEvent normalized = outputNormalizer.normalize(event);
|
|
|
+
|
|
|
+ if (normalized.hasCard()) {
|
|
|
+ cardInstanceService.createFromAgentResponse(normalized, request);
|
|
|
+ }
|
|
|
+
|
|
|
+ conversationService.appendEvent(normalized);
|
|
|
+ auditLogService.record(normalized);
|
|
|
+ meterEventProducer.produce(normalized);
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 19. 研发计划
|
|
|
+
|
|
|
+### 19.1 阶段拆分
|
|
|
+
|
|
|
+```mermaid
|
|
|
+flowchart TD
|
|
|
+ A["第 1 步 Router 骨架"] --> B["第 2 步 TaskStateService"]
|
|
|
+ B --> C["第 3 步 DifyAgentEngine"]
|
|
|
+ C --> D["第 4 步 Card Runtime MVP"]
|
|
|
+ D --> E["第 5 步 HisMockAdapter"]
|
|
|
+ E --> F["第 6 步 TongueDiagnosisAdapter"]
|
|
|
+ F --> G["第 7 步 三端联调"]
|
|
|
+
|
|
|
+ A1["AgentRouterService / RouteDecision / DevicePolicyRouter / TaskRouter / LlmIntentClassifier"] --> A
|
|
|
+ B1["ai_task_instance / RegistrationTaskStateMachine / TongueTaskStateMachine / GuideTaskStateMachine"] --> B
|
|
|
+ C1["DifyApiClient 复用 / DifyOutputNormalizer / DifyAppConfig"] --> C
|
|
|
+ D1["CardDefinition / CardInstance / CardActionLog / IdempotencyService"] --> D
|
|
|
+ E1["queryDepartments / queryDoctors / querySchedules / createPatient / createAppointment"] --> E
|
|
|
+ F1["checkImageQuality / startDiagnosis / queryDiagnosisResult"] --> F
|
|
|
+ G1["机器人导诊导航 / 自助机建档挂号舌诊 / 导诊大屏 FAQ 路线"] --> G
|
|
|
+```
|
|
|
+
|
|
|
+### 19.2 建议排期
|
|
|
+
|
|
|
+| 阶段 | 时间 | 目标 | 主要产物 |
|
|
|
+|---|---:|---|---|
|
|
|
+| 阶段 1 | 3-5 天 | 协议冻结 | SSE 协议、Card Schema、DeviceContext、RouteDecision、Mock Tool 契约 |
|
|
|
+| 阶段 2 | 1 周 | Router + Task 骨架 | AgentRouter、TaskStateService、ai_task_instance、基础路由日志 |
|
|
|
+| 阶段 3 | 1 周 | Dify + Conversation | DifyAgentEngine、DifyOutputNormalizer、ConversationService |
|
|
|
+| 阶段 4 | 1 周 | Card Runtime | CardDefinition、CardInstance、CardAction、幂等、状态机 |
|
|
|
+| 阶段 5 | 1 周 | HisMock + File + 舌诊 | HisMockAdapter、FileService、TongueDiagnosisAdapter |
|
|
|
+| 阶段 6 | 1-2 周 | 三端前端改造 | Vue 3 Terminal Client、三端首页、卡片组件、Bridge SDK |
|
|
|
+| 阶段 7 | 1 周 | 联调与验收 | 三条主链路、审计、Trace、演示脚本、失败兜底脚本 |
|
|
|
+
|
|
|
+### 19.3 任务拆解
|
|
|
+
|
|
|
+#### 后端任务
|
|
|
+
|
|
|
+| 编号 | 任务 | 负责人建议 | 备注 |
|
|
|
+|---|---|---|---|
|
|
|
+| BE-01 | 定义 `AgentChatRequest/Response` 与 SSE 事件 | 后端 | 所有前端依赖 |
|
|
|
+| BE-02 | 实现 `DeviceRegistry` MVP | 后端 | 三类终端启动依赖 |
|
|
|
+| BE-03 | 实现 `SceneProfileService` | 后端 | 控制不同终端能力 |
|
|
|
+| BE-04 | 实现 `ConversationService` | 后端 | 替代内存 Map |
|
|
|
+| BE-05 | 实现 `AgentRouterService` | 后端核心 | 最关键模块 |
|
|
|
+| BE-06 | 实现 `TaskStateService` | 后端核心 | 防止多轮流程混乱 |
|
|
|
+| BE-07 | 实现 `DifyAgentEngine` | 后端/AI | 复用已有 Dify API 客户端 |
|
|
|
+| BE-08 | 实现 `DifyOutputNormalizer` | 后端/AI | 卡片 schema 校验 |
|
|
|
+| BE-09 | 实现 `Card Runtime MVP` | 后端核心 | 卡片动作闭环 |
|
|
|
+| BE-10 | 实现 `FileService` | 后端 | 舌象、身份证上传依赖 |
|
|
|
+| BE-11 | 实现 `HisMockAdapter` | 后端 | 无真实 HIS 阶段必须显式 Mock |
|
|
|
+| BE-12 | 实现 `TongueDiagnosisAdapter` | 后端/AI | 对接已有舌诊接口 |
|
|
|
+| BE-13 | 实现审计、Trace、基础用量 | 后端 | 联调排障必须有 |
|
|
|
+
|
|
|
+#### 前端任务
|
|
|
+
|
|
|
+| 编号 | 任务 | 备注 |
|
|
|
+|---|---|---|
|
|
|
+| FE-01 | 从 Demo 抽出 `medical-cards` | 复用卡片组件,但接入中台 schema |
|
|
|
+| FE-02 | 实现 `api-client` | 替换 Demo `/chat/messages` |
|
|
|
+| FE-03 | 实现三端首页 | robot / kiosk / guide-screen |
|
|
|
+| FE-04 | 实现 SSE 消费与断线恢复 | 对接 message、card、error、completed |
|
|
|
+| FE-05 | 实现 File Upload | 身份证、舌象 |
|
|
|
+| FE-06 | 实现 Card Action 调用 | 所有按钮统一走 card action |
|
|
|
+| FE-07 | 实现 Robot Bridge SDK | 导航、语音、状态回调 |
|
|
|
+| FE-08 | 终端能力降级 | 浏览器无 Bridge 时不崩溃 |
|
|
|
+
|
|
|
+#### Dify 任务
|
|
|
+
|
|
|
+| 编号 | 任务 | 备注 |
|
|
|
+|---|---|---|
|
|
|
+| AI-01 | 创建 `opd-guide-agent` | FAQ、路线、科室介绍 |
|
|
|
+| AI-02 | 创建 `opd-triage-agent` | 症状追问、科室推荐 |
|
|
|
+| AI-03 | 创建 `opd-registration-agent` | 建档/挂号话术,不直接写 HIS |
|
|
|
+| AI-04 | 创建 `tongue-diagnosis-agent` | 主诉采集、结果解释 |
|
|
|
+| AI-05 | 统一输出 JSON schema | 必须可被 Normalizer 校验 |
|
|
|
+| AI-06 | 准备失败兜底话术 | HIS Mock 失败、舌诊失败、文件失败 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 20. 测试与验收标准
|
|
|
+
|
|
|
+### 20.1 机器人端验收
|
|
|
+
|
|
|
+| 验收项 | 标准 |
|
|
|
+|---|---|
|
|
|
+| 设备启动 | 能注册、心跳、获取 sceneProfile |
|
|
|
+| 语音问答 | 能进入导诊 Agent,返回文本和 TTS |
|
|
|
+| 科室推荐 | 能根据症状推荐科室 |
|
|
|
+| 导航 | 能生成路线卡,机器人可执行导航命令 |
|
|
|
+| 事件上报 | 导航成功/失败能上报 DeviceEvent |
|
|
|
+| 限制策略 | 不能做复杂实名建档和隐私采集 |
|
|
|
+
|
|
|
+### 20.2 自助机端验收
|
|
|
+
|
|
|
+| 验收项 | 标准 |
|
|
|
+|---|---|
|
|
|
+| 建档 | 能采集身份证、识别、确认、生成 Mock 患者 |
|
|
|
+| 分诊 | 能追问症状并推荐科室 |
|
|
|
+| 挂号 Mock | 能选择科室、医生、时段、确认并返回成功卡 |
|
|
|
+| 舌诊 | 能上传舌象、调用舌诊接口、展示结果卡 |
|
|
|
+| 卡片动作 | 所有按钮走 CardAction |
|
|
|
+| 幂等 | 重复点击不会重复建档/挂号 |
|
|
|
+| 断线恢复 | 能恢复会话和卡片状态 |
|
|
|
+
|
|
|
+### 20.3 导诊大屏验收
|
|
|
+
|
|
|
+| 验收项 | 标准 |
|
|
|
+|---|---|
|
|
|
+| FAQ | 能回答公共导诊问题 |
|
|
|
+| 科室介绍 | 能查科室位置、职责、就诊说明 |
|
|
|
+| 路线 | 能展示路线说明 |
|
|
|
+| 轻量分诊 | 能推荐科室,但不展示隐私 |
|
|
|
+| 隐私限制 | 禁止身份证、舌象、报告、个人挂号 |
|
|
|
+
|
|
|
+### 20.4 后端统一验收
|
|
|
+
|
|
|
+| 验收项 | 标准 |
|
|
|
+|---|---|
|
|
|
+| RouteLog | 每次路由都有记录 |
|
|
|
+| TaskLog | 每次任务状态流转可查 |
|
|
|
+| CardActionLog | 每次卡片动作可查 |
|
|
|
+| ToolInvocationLog | 每次 Mock/HIS 工具调用可查 |
|
|
|
+| FileAccessLog | 每次文件上传和访问可查 |
|
|
|
+| TraceId | 一次用户请求贯穿全链路 |
|
|
|
+| MeterEvent | Agent 调用、舌诊、卡片关键动作可生成用量事件 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 21. 风险与控制
|
|
|
+
|
|
|
+| 风险 | 表现 | 控制策略 |
|
|
|
+|---|---|---|
|
|
|
+| Demo 后端继续膨胀 | ChatService 越写越大 | 直接冻结 Demo 后端新增能力,迁入中台 |
|
|
|
+| Dify 输出不稳定 | 卡片 JSON 不合法 | 强制 Normalizer 校验,不合法降级 |
|
|
|
+| 前端绕过后端 | 直接调用舌诊/HIS/导航 | API 权限和代码 review 强控 |
|
|
|
+| 无 HIS 接口导致假闭环 | 演示像真实挂号 | 明确 HisMockAdapter,文案标注演示/Mock |
|
|
|
+| 设备差异被低估 | 三端 UI 和能力混乱 | SceneProfile + DevicePolicy 强控 |
|
|
|
+| 舌诊隐私风险 | 大屏或机器人采集敏感图像 | 只允许自助机主采集,机器人引导,大屏禁止 |
|
|
|
+| 状态机缺失 | 用户中途切换导致流程错乱 | activeTask 优先,卡片等待优先 |
|
|
|
+| Mermaid 再次解析失败 | 节点文本不兼容 | 所有复杂节点使用引号,避免节点标签以 `/` 开头 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 22. Mermaid 兼容性说明
|
|
|
+
|
|
|
+本项目文档中的 Mermaid 图建议遵守:
|
|
|
+
|
|
|
+```text
|
|
|
+1. 节点文本统一使用双引号:A["文本"]。
|
|
|
+2. 不要让节点标签以 / 开头,例如不要写 J[/files/upload 上传]。
|
|
|
+3. 不要在节点中使用过多 HTML,例如复杂 <br/>。
|
|
|
+4. 分支文字使用简单中文,例如 -->|"是"|。
|
|
|
+5. 一张图节点过多时拆图,不要硬塞进一张图。
|
|
|
+```
|
|
|
+
|
|
|
+本版已将原来可能失败的写法:
|
|
|
+
|
|
|
+```text
|
|
|
+flowchart TD
|
|
|
+ J[/files/upload 上传身份证图片]
|
|
|
+```
|
|
|
+
|
|
|
+修正为兼容性更好的写法:
|
|
|
+
|
|
|
+```text
|
|
|
+flowchart TD
|
|
|
+ J["调用 files upload 上传身份证图片"]
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 23. 最终总流程图
|
|
|
+
|
|
|
+```mermaid
|
|
|
+flowchart TD
|
|
|
+ A["终端启动"] --> B["设备注册 心跳 场景加载"]
|
|
|
+ B --> C["前端加载终端首页"]
|
|
|
+ C --> D["用户输入文本 语音 或点击入口"]
|
|
|
+ D --> E["调用 agent chat stream"]
|
|
|
+
|
|
|
+ E --> F["鉴权"]
|
|
|
+ F --> G["解析设备上下文"]
|
|
|
+ G --> H["加载场景配置"]
|
|
|
+ H --> I["加载会话"]
|
|
|
+ I --> J["加载任务状态"]
|
|
|
+ J --> K["AgentRouter 决策"]
|
|
|
+
|
|
|
+ K --> L{"路由结果"}
|
|
|
+ L -->|"导诊"| M["opd-guide-agent"]
|
|
|
+ L -->|"分诊"| N["opd-triage-agent"]
|
|
|
+ L -->|"挂号"| O["opd-registration-agent"]
|
|
|
+ L -->|"舌诊"| P["tongue-diagnosis-agent"]
|
|
|
+ L -->|"禁止"| Q["返回禁止提示"]
|
|
|
+ L -->|"低置信度"| R["反问澄清"]
|
|
|
+
|
|
|
+ M --> S["Dify 输出归一化"]
|
|
|
+ N --> S
|
|
|
+ O --> S
|
|
|
+ P --> S
|
|
|
+
|
|
|
+ S --> T{"是否有卡片"}
|
|
|
+ T -->|"否"| U["保存消息并 SSE 返回文本"]
|
|
|
+ T -->|"是"| V["Card Runtime 创建卡片实例"]
|
|
|
+
|
|
|
+ V --> W["前端展示卡片"]
|
|
|
+ W --> X["用户执行卡片动作"]
|
|
|
+ X --> Y["AgentActionOrchestrator + CardActionService"]
|
|
|
+
|
|
|
+ Y --> Z{"动作类型"}
|
|
|
+ Z -->|"HIS 工具"| AA["MCP 加 HIS Adapter"]
|
|
|
+ Z -->|"舌诊"| AB["TongueDiagnosisAdapter"]
|
|
|
+ Z -->|"设备命令"| AC["DeviceCommandService"]
|
|
|
+ Z -->|"本地状态"| AD["TaskStateService"]
|
|
|
+
|
|
|
+ AA --> AE["更新卡片和任务状态"]
|
|
|
+ AB --> AE
|
|
|
+ AC --> AE
|
|
|
+ AD --> AE
|
|
|
+
|
|
|
+ AE --> AF["审计 用量 Trace"]
|
|
|
+ AF --> AG{"是否需要继续对话"}
|
|
|
+ AG -->|"是"| AH["synthetic message 继续 Dify"]
|
|
|
+ AG -->|"否"| AI["流程结束"]
|
|
|
+ AH --> S
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 24. 最终开发口径
|
|
|
+
|
|
|
+这套方案的开发口径可以压缩成一句话:
|
|
|
+
|
|
|
+> **统一入口客户端一期要把现有 Demo 的交互资产迁移为生产级中台运行时:Vue 3 前端独立成 Terminal Client,后端能力进入 AI 中台,优先打通机器人导诊导航、自助机建档分诊挂号 Mock、自助机舌诊、导诊大屏公共导流四类闭环。**
|
|
|
+
|
|
|
+团队实施时不要先追求“像蚂蚁阿福的皮”,而要先建立稳定的控制链路:
|
|
|
+
|
|
|
+```text
|
|
|
+设备场景
|
|
|
+→ 会话任务
|
|
|
+→ 后端路由
|
|
|
+→ Dify 编排
|
|
|
+→ 卡片动作
|
|
|
+→ 工具适配
|
|
|
+→ 审计计量
|
|
|
+```
|