Răsfoiți Sursa

引入统一入口客户端相关设计文档

WangKang 1 săptămână în urmă
părinte
comite
f619958e60

+ 4 - 3
docs/接口文档/emoon-ai-openplatform-api-v1.2.openapi.yaml

@@ -1,8 +1,8 @@
 openapi: 3.0.3
 info:
   title: EMOON AI OpenPlatform External API
-  version: 1.2.0
-  description: 医梦 AI 中台对外接口契约 v1.2,正式联调基准版。P0/P1 为厂商联调主基线;P2 为受控规划接口。FHIR 映射从主 YAML 移出,避免误解为当前交付承诺。
+  version: 1.2.1
+  description: 医梦 AI 中台对外接口契约 v1.2.1,正式联调基准版。P0/P1 为厂商联调主基线;P2 为受控规划接口。FHIR 映射从主 YAML 移出,避免误解为当前交付承诺。统一入口客户端调用 Agent 时可不传 agentId,由后端 AgentRouter 决定最终智能体。
 servers:
 - url: https://api.{hospital}.emoon.local/api/v1
   variables:
@@ -2693,11 +2693,12 @@ components:
     AgentChatRequest:
       type: object
       required:
-      - agentId
       - message
       properties:
         agentId:
           type: string
+          nullable: true
+          description: 对外智能体 ID。统一入口客户端可为空,由后端 AgentRouter 根据设备、场景、任务状态和输入内容决定。
         conversationId:
           type: string
           description: 首轮可为空,平台自动创建。

+ 65 - 27
docs/接口文档/医梦AI中台对外接口设计文档_v1.2_正式联调基准版.md

@@ -2,9 +2,9 @@
 
 > 文档定位:厂商对接接口全貌 + MVP 可落地接口契约说明  
 > 适用对象:HIS/EMR/LIS/PACS/叫号/支付厂商、机器人厂商、自助机/导诊屏/诊室屏/大屏厂商、四诊仪厂商、迪耐克/鸿蒙病房互通方、医院信息科、医梦内部研发与测试  
-> OpenAPI 文件:`emoon-ai-openplatform-api-v1.2.openapi.yaml`  
-> 版本:v1.2  
-> 生成时间:2026-05-29 15:30
+> OpenAPI 文件:`emoon-ai-openplatform-api-v1.2.openapi.yaml`(文件名沿用 v1.2,`info.version` 为 1.2.1)
+> 版本:v1.2.1
+> 生成时间:2026-05-31 18:00
 
 ---
 
@@ -20,6 +20,8 @@
 
 本版在 v1.1 基础上采纳本轮审查中合理的问题,形成正式联调基准版:补充文件删除幂等、文件上传幂等语义、`clientFileId`、`sha256`、`retentionPolicy`、住院设备映射边界、工具风险等级、`/meter/events` 查询范围收敛,并将 FHIR 可选映射从主 OpenAPI YAML 移出,避免厂商误解为当前交付承诺。
 
+2026-05-31 补充统一入口客户端口径:本次不新增 P0 URL,而是收敛终端调用白名单、AgentRouter/TaskState 内部职责和前后端工程边界。OpenAPI YAML 文件名沿用 v1.2,`info.version` 升级为 1.2.1,并将 `AgentChatRequest.agentId` 改为可选以支持后端路由。
+
 
 
 ### v1.2 相比 v1.1 的关键修正
@@ -42,6 +44,28 @@
 5. **设备上下文进入每次关键调用**:`deviceContext` 进入 chat、card action、device event、meter event。
 6. **P0 能真实联调,P1/P2 先给全貌**:防止接口规划缺口导致未来返工。
 
+### 0.1 v1.2.1 统一入口客户端补充口径
+
+| 主题 | 接口口径 | 原因 |
+|---|---|---|
+| 终端调用白名单 | 机器人、自助机、导诊屏、诊室屏等统一入口客户端只允许调用 Device、`/agent/chat/stream`、Conversation、File、Card、Device Event、Device Command 相关接口 | 保持终端轻量,避免终端绕过中台审计和计量 |
+| `/agent/chat` | 保留为同步对话、服务端联调和不支持 SSE 的受控调用;统一入口客户端默认使用 `/agent/chat/stream` | 医院终端需要流式反馈和卡片事件,SSE 更适合 P0 体验 |
+| TaskStateService | 不新增公共 `/tasks` P0 接口;`taskId`、`taskType`、`taskStatus` 通过 SSE 事件、会话查询和卡片实例返回 | 减少接口面,避免第一期多维护一组状态查询 API |
+| AgentRouter | 作为 `emoon-ai-agent` 内部能力,不对外暴露路由接口 | 外部厂商只关心结果,不应依赖医梦内部路由规则 |
+| 舌诊底层接口 | 终端只能走 `/files/upload` + `/agent/chat/stream` + `/cards/{id}/actions/{name}`,不得直连舌诊 SDK/API | 舌象图片属于敏感医疗数据,必须统一文件审计和工具审计 |
+| HIS Mock/Real Adapter | Mock 阶段和真实阶段对终端接口不变,差异只在后端 `HospitalAdapter` 配置 | 便于 MVP 先联调,后续替换真实 HIS 不改终端 |
+| 前后端工程边界 | 后端能力仍在 AI 中台统一工程;前端/移动端另起 `emoon-terminal-client` | 符合团队资源现状,避免一个仓库混合终端构建、后端模块和 Demo 服务 |
+
+### 0.2 本接口边界的行业依据
+
+| 依据 | 对接口设计的约束 |
+|---|---|
+| 东软 5G 智慧医院公开方案强调统一信息平台集成各子系统 | 医梦接口只做 AI 入口、设备场景和受控工具,不把自己设计成完整 HIS/EMR 替代品 |
+| 华为智慧病房/医疗物联网公开方案强调物联网平台和多协议设备接入 | 住院 IoMT、床头屏、护理白板优先通过外部病房/物联平台事件接入,不让医梦接口承担原始设备接入责任 |
+| 科大讯飞智慧医疗公开资料强调 AI 嵌入医生现有流程和辅助决策 | 高风险医疗动作必须通过 Card Action 和人工确认,不开放“自动诊断/自动处方”接口 |
+| Dify 官方已有 Workflow 版本控制能力 | 接口层不暴露 Dify 内部发布 API,医梦只维护 Agent 配置、版本映射和输出归一 |
+| 《个人信息保护法》对医疗健康、行踪轨迹等敏感个人信息有严格要求 | 文件、位置、设备事件和服务找人接口必须最小化采集、可审计、可降级 |
+
 ---
 
 ## 1. 接口设计边界
@@ -73,8 +97,8 @@
 |通用平台|全部厂商|POST|`/auth/token`|获取短期访问 Token|厂商使用 client_credentials 获取 accessToken;也可用于服务端系统对接。|
 |通用平台|全部厂商|GET|`/health`|平台健康检查|用于联调前探活,不返回敏感配置。|
 |通用平台|全部厂商|GET|`/capabilities`|查询项目已开通能力|让厂商知道当前项目可调用哪些 Agent、工具、设备能力和计费策略。|
-|智能体与会话|机器人/自助机/导诊屏/诊室屏|POST|`/agent/chat`|智能体同步对话|首轮 conversationId 可为空,平台自动创建会话并返回。|
-|智能体与会话|机器人/自助机/导诊屏/诊室屏|POST|`/agent/chat/stream`|智能体流式对话 SSE|POST + SSE;必须支持断线后按 conversationId 查询最终消息和卡片。|
+|智能体与会话|受控服务端/不支持 SSE 的终端|POST|`/agent/chat`|智能体同步对话|首轮 conversationId 可为空,平台自动创建会话并返回;统一入口客户端默认不使用该接口。|
+|智能体与会话|机器人/自助机/导诊屏/诊室屏|POST|`/agent/chat/stream`|智能体流式对话 SSE|POST + SSE;必须支持断线后按 conversationId 查询最终消息、任务状态和卡片。|
 |智能体与会话|终端厂商/患者端/医生端|POST|`/conversations`|创建会话|可选预创建;chat 不传 conversationId 时自动创建。|
 |智能体与会话|终端厂商/患者端/医生端|GET|`/conversations/{conversationId}`|查询会话详情|用于断线恢复、历史状态恢复。|
 |智能体与会话|终端厂商/患者端/医生端|GET|`/conversations/{conversationId}/messages`|查询会话消息|用于 SSE 断线后的最终文本和卡片恢复。|
@@ -146,7 +170,6 @@
 |`POST /devices/{deviceId}/events`|
 |`GET /devices/{deviceId}/commands/poll`|
 |`POST /devices/{deviceId}/commands/{commandId}/ack`|
-|`POST /agent/chat`|
 |`POST /agent/chat/stream`|
 ### 自助机/导诊屏/大屏/诊室屏厂商
 
@@ -158,7 +181,7 @@
 |`POST /devices/{deviceId}/events`|
 |`GET /cards/{cardInstanceId}`|
 |`POST /cards/{cardInstanceId}/actions/{actionName}`|
-|`POST /agent/chat`|
+|`POST /agent/chat/stream`|
 ### HIS/EMR/LIS/PACS/叫号/支付厂商
 
 |建议接入接口|
@@ -175,7 +198,7 @@
 |`POST /devices/register`|
 |`POST /devices/{deviceId}/heartbeat`|
 |`POST /files/upload`|
-|`POST /agent/chat`|
+|`POST /agent/chat/stream`|
 |`GET /cards/{cardInstanceId}`|
 |`POST /cards/{cardInstanceId}/actions/{actionName}`|
 ### 迪耐克/鸿蒙病房/住院互通方
@@ -184,8 +207,8 @@
 |---|
 |`POST /events/ingest`|
 |`POST /devices/register(仅映射终端)`|
-|`POST /devices/{deviceId}/events`|
-|`POST /agent/chat`|
+|`POST /devices/{deviceId}/events(仅标准业务事件,不做硬件心跳主责)`|
+|`POST /agent/chat/stream(仅明确需要 AI 嵌入时使用)`|
 |`GET /tools/catalog`|
 ### 医院信息科/运营方
 
@@ -274,7 +297,7 @@ SHA256_HEX(request_body)
 
 | 接口类型 | 幂等键建议 |
 |---|---|
-| 对话请求 | `projectId + agentId + clientMessageId` |
+| 对话请求 | `projectId + resolvedAgentId/routeKey + clientMessageId`;统一入口客户端未传 `agentId` 时由后端路由结果补齐 |
 | 设备事件 | `projectId + deviceId + eventType + clientEventId` |
 | 卡片动作 | `projectId + cardInstanceId + actionName + clientActionId` |
 | Tool 写操作 | `projectId + toolName + businessKey` |
@@ -344,7 +367,7 @@ SHA256_HEX(request_body)
 
 | 字段 | 说明 |
 |---|---|
-| `agentId` | 对外智能体 ID |
+| `agentId` | 对外智能体 ID;统一入口客户端可为空,由后端 `AgentRouter` 根据设备、场景、任务状态和输入内容决定 |
 | `conversationId` | 可为空;为空时平台自动创建并返回 |
 | `user` | 用户上下文,预留 `patientId/visitId/encounterId/authorizationId` |
 | `deviceContext` | 设备上下文,设备调用必须传 |
@@ -386,7 +409,7 @@ MVP 只处理:
 ### 5.3 设备接入三步握手
 
 ```text
-设备启动 -> /devices/register -> /devices/{deviceId}/scene -> /agent/chat 或 /cards/actions
+设备启动 -> /devices/register -> /devices/{deviceId}/scene -> /agent/chat/stream 或 /cards/actions
                     -> 定时 /devices/{deviceId}/heartbeat
                     -> 有事件时 /devices/{deviceId}/events
 ```
@@ -459,7 +482,7 @@ MVP 只处理:
 
 1. 前端不直接调用 HIS。
 2. 卡片动作先校验卡片状态、权限、幂等和有效期。
-3. 需要写 HIS 时由 Card Runtime 进入 MCP Tool Server。
+3. 需要写 HIS 时由 `AgentActionOrchestrator` 承接业务编排,`CardActionService` 做状态校验和快照,再进入 MCP Tool Server。
 4. 医疗高风险动作必须人工确认。
 
 ### 5.5 受控工具接口
@@ -590,7 +613,7 @@ Content-Type: application/json
 
 - `projectId`
 - `partnerId`
-- `agentId`
+- `agentId`,Router 决定场景时由后端补齐最终命中的 Agent ID
 - `deviceId`
 - `scenarioCode`
 - `billingEpisodeId`
@@ -629,13 +652,16 @@ Content-Type: application/json
 ### 9.1 P0 开发顺序
 
 1. 通用鉴权、HMAC Header、错误响应、幂等键。
-2. `/agent/chat` + `/conversations/{'{id}'}` + `/conversations/{'{id}'}/messages`。
-3. `/files/upload` + `/files/{'{fileId}'}` + `DELETE /files/{'{fileId}'}` 幂等删除。
-4. `/devices/register` + `/heartbeat` + `/scene`。
-5. `/cards/{'{id}'}` + `/cards/{'{id}'}/actions/{'{actionName}'}`。
-6. `/tools/{'{toolName}'}/invoke` 只接查询类 HIS 工具。
-7. `/meter/events` 只按 `billingEpisodeId` / `traceId` 精确查询 + 计量事件落库。
-8. `/agent/chat/stream` 做 P0 四类 SSE 事件。
+2. `/devices/register` + `/heartbeat` + `/scene`,让统一入口客户端能先启动和拿到场景。
+3. `/agent/chat/stream` + `/conversations/{'{id}'}` + `/conversations/{'{id}'}/messages`,统一入口客户端默认走 SSE。
+4. `AgentRouter` 和 `TaskStateService` 先作为 `emoon-ai-agent` 内部能力实现,不新增公共 `/tasks` P0 API。
+5. `/files/upload` + `/files/{'{fileId}'}` + `DELETE /files/{'{fileId}'}` 幂等删除。
+6. `/cards/{'{id}'}` + `/cards/{'{id}'}/actions/{'{actionName}'}`,卡片动作由 Agent 编排链路推进状态和工具调用。
+7. `/tools/{'{toolName}'}/invoke` 只接查询类 HIS 工具和显式 Mock 写工具,普通终端不可直接调用。
+8. `/devices/{'{deviceId}'}/events` + `/commands/poll` + `/commands/{'{commandId}'}/ack`,先支撑机器人导航、扫码、打印、异常事件。
+9. `/meter/events` 只按 `billingEpisodeId` / `traceId` 精确查询 + 计量事件落库。
+
+`/agent/chat` 保留为同步接口和服务端联调工具,不作为统一入口客户端 P0 主链路。
 
 ### 9.2 不建议第一期实现的内容
 
@@ -646,7 +672,18 @@ Content-Type: application/json
 - FHIR 网关 / FHIR Translate 主接口;
 - 第三方卡片插件市场;
 - 全院鸿蒙无感定位;
-- 所有设备深度控制命令。
+- 所有设备深度控制命令;
+- 公共 `/tasks` 查询与任务编排 API;
+- 终端直连 Dify、HIS Tool、舌诊底层接口或计量写接口。
+
+### 9.3 统一入口客户端 P0 接口白名单
+
+| 终端类型 | 默认允许接口 | 第一阶段不允许 |
+|---|---|---|
+| 机器人 | Device 注册/心跳/场景/事件/命令轮询、`/agent/chat/stream`、Conversation、Card | 直连 HIS Tool、直连 Dify、真实缴费、医保、退费 |
+| 自助机 | Device、`/agent/chat/stream`、Conversation、File、Card | 前端写死号源、前端直接调用舌诊接口、自动完成高风险动作 |
+| 导诊屏/大屏 | Device、`/agent/chat/stream`、Conversation、Card、轻量事件 | 舌象采集、实名挂号、支付、医保 |
+| 诊室屏/医生站插件 | Device、`/agent/chat/stream`、Conversation、File、Card | 自动诊断、自动处方、绕过医生确认 |
 
 ---
 
@@ -673,8 +710,8 @@ Content-Type: application/json
 
 ```text
 医梦AI中台厂商对接资料包/
-├── 医梦AI中台对外接口设计文档_v1.1_全量规划版.md
-├── emoon-ai-openplatform-api-v1.1.openapi.yaml
+├── 医梦AI中台对外接口设计文档_v1.2.1_正式联调基准版.md
+├── emoon-ai-openplatform-api-v1.2.openapi.yaml
 ├── Postman Collection
 ├── HMAC签名示例-Java
 ├── HMAC签名示例-TypeScript
@@ -686,15 +723,16 @@ Content-Type: application/json
 当前这版已经可以作为内部评审和厂商预沟通底稿。若要发正式联调版,应再补 Postman Collection、HMAC 示例和 HIS Mock Server。
 
 
-## 12. v1.2 正式联调基准结论
+## 12. v1.2.1 正式联调基准结论
 
-v1.2 可以作为厂商正式联调基准,但仍需坚持以下边界:
+v1.2.1 可以作为厂商正式联调基准。它不扩大主 OpenAPI YAML 的 P0 接口面,只补充统一入口客户端的调用边界和工程归属。仍需坚持以下边界:
 
 1. **主 YAML 只代表当前联调基准**:P0/P1 是主要联调范围;P2 只做规划说明,不代表春节前承诺。
 2. **FHIR 不在主基线内**:如医院已有 FHIR 网关,需要单独评估后另出 FHIR 映射文件。
 3. **住院设备不按门诊总包处理**:护理白板、床头屏、护士 PDA 等住院终端默认通过迪耐克/鸿蒙病房体系映射接入。
 4. **文件和工具接口是高风险区域**:文件必须有用途、来源、保存策略和幂等;工具必须有 riskLevel、scope、幂等和审计。
 5. **计量查询不是账单接口**:`/meter/events` 只用于联调排障和计量核验,正式对账以后续账单中心为准。
+6. **统一入口客户端不直连底层能力**:终端前端不得持有 Dify Key,不得调用 HIS Tool,不得直连舌诊底层接口,不得写计量账本。
 
 对外发送建议:
 

+ 48 - 15
docs/架构文档/AI中台二期+三期需求技术方案.md

@@ -7,8 +7,8 @@
 | 二期主题 | OpenPlatform + Dify Workflow + MCP Server + Card Runtime + Terminal Runtime + Device Registry/Adapter |
 | 三期主题 | 能力值预付账户、设备授权、设备维度计量、额度、账单、统计、运营、审计 |
 | 计费主粒度 | 项目 / 医院客户;科室、智能体、用户、设备、场景作为分摊与统计维度 |
-| 版本 | v6.0 |
-| 更新时间 | 2026-05-29 |
+| 版本 | v6.1 |
+| 更新时间 | 2026-05-31 |
 
 ## 1. 结论摘要
 
@@ -86,6 +86,23 @@
 6. 所有医疗高风险输出必须有医生或患者确认,不做自动诊断、自动处方、自动签到、自动缴费。
 7. 每个上线场景必须有可回放样例:请求、响应、数据库记录、traceId、计量事件、人工确认记录。
 
+### 1.4 统一入口客户端补充设计采纳结论
+
+本节采纳 `docs/架构文档/统一入口客户端技术设计文档_v1.0.md` 的边界,但按创业公司 MVP 做收敛:统一入口客户端后端能力进入 AI 中台统一工程,前端和移动端另起 `emoon-terminal-client` 工程,不新增一套 Demo 后端。
+
+| 结论 | 正式方案口径 | MVP 落地 |
+| --- | --- | --- |
+| 后端归属 | Device Registry、Scene Profile、AgentRouter、TaskStateService、Card Runtime、File Service、HisMockAdapter、TongueDiagnosisAdapter 都归 AI 中台模块 | 复用 OpenPlatform 对外入口和 Maven 多模块工程,不新建独立终端后端 |
+| 前端归属 | 机器人、自助机、导诊屏三端进入独立 `emoon-terminal-client` 工程 | 后端只提供 OpenAPI、场景配置、卡片 schema 和设备命令协议 |
+| P0 业务链路 | 机器人导诊导航、自助机建档/分诊/挂号 Mock/舌诊、导诊屏 FAQ/科室介绍/路线/轻分诊 | 每条链路必须能回放 conversationId、taskId、cardInstanceId、traceId |
+| TaskStateService | 任务状态是 AI 中台内部状态,不作为第一期公共 `/tasks` API 暴露 | 任务状态通过 `/agent/chat/stream`、`/conversations/*` 和 `/cards/*` 返回给终端 |
+| AgentRouter | 先按设备能力、场景、任务状态和少量规则路由,再少量使用 LLM 意图分类兜底 | 不在第一期建设复杂规则引擎和在线学习系统 |
+| 舌诊接入 | 前端只上传文件和执行卡片动作,后端通过 `TongueDiagnosisAdapter` 调专精能力 | 不允许前端直连舌诊底层接口,不把 base64 图片塞进 Dify |
+| HIS 接入 | 无真实 HIS 前必须显式使用 `HisMockAdapter` | Mock 数据只用于 POC,不得冒充真实支付、真实挂号或真实医保 |
+| 终端接口白名单 | 终端只调用设备、SSE 对话、文件、卡片、事件、命令轮询等 P0 接口 | 终端不得直连 Dify、HIS Tool、计量写接口和舌诊底层接口 |
+
+该取舍与行业实践一致:东软类智慧医院平台强调统一信息平台集成各子系统,华为/医惠类病房方案强调物联平台统一接入设备,Dify 官方已提供 Workflow 版本能力。医梦作为创业团队,应只做 AI 入口层、卡片闭环、设备场景治理和必要适配,不复制大厂完整医院平台和物联网底座。
+
 ## 2. 当前工程基线
 
 ### 2.1 后端工程结构
@@ -187,7 +204,7 @@
 | --- | --- | --- |
 | `CardDefinitionService` | `emoon-system` | 管理卡片定义、版本、灰度、启停 |
 | `CardInstanceService` | `emoon-mcp-api` 接口,`emoon-openplatform` 或 `emoon-mcp` 实现 | 创建实例、保存快照、查询状态 |
-| `CardActionService` | `emoon-mcp-api` 接口,`emoon-openplatform` 或 `emoon-mcp` 实现 | 校验动作、幂等、调用 MCP、更新状态 |
+| `CardActionService` | `emoon-ai-card` 实现,动作编排由 `emoon-ai-agent` 承接 | 校验动作、幂等、保存快照、更新状态;不得直接绕过 Agent 调 MCP |
 | `CardRenderConfigService` | 前端配合,后端提供定义查询 | 前端按 UI 配置渲染 |
 
 二期对话链路中,`AgentChatServiceImpl` 在收到 `cardKey/cardData` 后必须触发 `CardInstanceService.createFromAgentResponse()` 这类能力,不能只把卡片数据透传给前端。
@@ -671,7 +688,20 @@ Dify 接入不是简单 HTTP 转发。OpenPlatform 必须把 Dify 的不稳定
 | 医疗原文 | 禁止 | 如完整病历、检查报告原文、诊断原文 |
 | 图像原始引用 | 禁止 | 舌诊/面诊原图 fileId 不进入 Dify 卡片输出 |
 
-患者 PII 只能通过“前端卡片动作 -> CardActionService -> MCP Tool Service”的加密后端链路提交,不能进入 Dify 的输入、输出或 Workflow 变量。
+患者 PII 只能通过“前端卡片动作 -> AgentActionOrchestrator -> CardActionService -> MCP Tool Service”的加密后端链路提交,不能进入 Dify 的输入、输出或 Workflow 变量。
+
+DifyOutputNormalizer 作为 `DifyAgentEngine` 的内部输出后处理步骤实现,不新增独立 Maven 模块,也不放在 `emoon-openplatform`。建议落在 `emoon-ai-agent` 的 adapter/application 边界内,职责包括:
+
+| 职责 | 处理规则 |
+| --- | --- |
+| JSON 合法性校验 | 不合法时降级为普通文本并记录 Dify 输出告警 |
+| `cardKey` 校验 | 不存在或未注册时拒绝创建卡片 |
+| `cardData` schema 校验 | 不通过时拒绝创建卡片,并返回可观测错误 |
+| 风险等级归一 | 将 Dify 输出映射为平台医疗安全等级 |
+| 敏感字段拦截 | 阻止 PII、完整病历原文、舌象原图引用进入卡片输出 |
+| SSE 事件归一 | 将 Dify 流式事件转换为平台 `message_delta`、`card_created`、`usage_reported` 等稳定事件 |
+
+Normalizer 不执行卡片动作、不调用 HIS、不扣能力值。它只负责把 Dify 的不稳定输出转换成平台可审计、可校验、可降级的稳定事件,避免开发人员把校验逻辑散落到 `openplatform`、Card Runtime 或前端。
 
 #### 5.8.3 流式响应与卡片时机
 
@@ -762,9 +792,9 @@ L1 客户可以使用 DirectLLM 作为主引擎,不强依赖 Dify。L2/L3 的
 | 症状追问与分诊 | Dify Workflow | LLM 节点和知识检索生成候选科室 |
 | 获取科室和医生 | MCP Server | 调 HIS 查询科室、医生、号源 |
 | 展示科室/医生卡片 | Card Runtime + 前端 | 创建卡片实例,前端渲染 |
-| 用户选择医生时间 | Card Runtime | 校验动作、写动作日志 |
+| 用户选择医生时间 | AgentActionOrchestrator + Card Runtime | Agent 编排承接动作,Card Runtime 校验状态并写动作日志 |
 | 锁定号源 | MCP Server | 写操作,必须幂等和短锁 |
-| 确认挂号 | Card Runtime + MCP | 用户确认后创建预约或订单 |
+| 确认挂号 | AgentActionOrchestrator + Card Runtime + MCP | 用户确认后由 Agent 编排推进预约或订单创建 |
 | 返回结果 | OpenPlatform | 更新会话和卡片状态,返回成功卡片 |
 | 产生计量事件 | 三期计量中心 | 按“智能分诊 / 挂号服务”计量 |
 
@@ -774,19 +804,21 @@ L1 客户可以使用 DirectLLM 作为主引擎,不强依赖 Dify。L2/L3 的
 flowchart LR
     Front["前端卡片"]
     API["OpenPlatform Card Action API"]
+    Agent["AgentActionOrchestrator"]
     Card["CardActionService"]
     Tool["MCP Tool Service"]
     Adapter["HospitalAdapter"]
     HIS["HIS / EMR / LIS / PACS"]
 
     Front --> API
-    API --> Card
-    Card --> Tool
+    API --> Agent
+    Agent --> Card
+    Agent --> Tool
     Tool --> Adapter
     Adapter --> HIS
 ```
 
-实现时,`CardActionService -> MCP Tool Service` 在同进程部署时可以是模块内 Service 调用;MCP 独立部署后改为内部 HTTP/RPC。无论哪种部署,前端和 Dify 都不能绕过 OpenPlatform/Card Runtime 直接执行卡片动作。
+实现时,`AgentActionOrchestrator -> MCP Tool Service` 在同进程部署时可以是模块内 Service 调用;MCP 独立部署后改为内部 HTTP/RPC。无论哪种部署,前端和 Dify 都不能绕过 OpenPlatform、Agent 编排和 Card Runtime 直接执行卡片动作。
 
 #### 5.10.1 统一入口客户端启动链路
 
@@ -934,7 +966,7 @@ flowchart TD
     Dify["Dify Workflow<br/>意图识别 / 分诊 / RAG"]
     DeptTool["MCP: 查询科室/医生/号源"]
     DeptCard["科室/医生/时间卡片"]
-    Action["CardActionService<br/>用户选择"]
+    Action["AgentActionOrchestrator + CardActionService<br/>用户选择"]
     Confirm["确认挂号卡片<br/>L3 二次确认"]
     WriteTool["MCP: 锁号 + 创建预约"]
     HIS["HIS"]
@@ -959,7 +991,7 @@ flowchart TD
     API["OpenPlatform<br/>鉴权 / 患者授权"]
     Dify["Dify Workflow<br/>返回建档卡片结构"]
     Form["前端建档卡片<br/>用户填写 PII"]
-    CardAPI["CardActionService<br/>字段校验 / 加密 / 审计"]
+    CardAPI["AgentActionOrchestrator + CardActionService<br/>字段校验 / 加密 / 审计"]
     MCP["MCP: his_create_patient"]
     HIS["HIS 患者主索引"]
     Meter["计量中心<br/>建档业务事件"]
@@ -973,7 +1005,7 @@ flowchart TD
     MCP -.MCP_TOOL_WRITE 内部成本.-> Meter
 ```
 
-安全规则:Dify 只输出建档卡片结构,不预填姓名、身份证、手机号等 PII。PII 从前端提交到 CardActionService 后,经后端加密链路进入 MCP/HIS;日志、Outbox、计量事件只保存摘要、字段完整度、HIS patientId 和 traceId。
+安全规则:Dify 只输出建档卡片结构,不预填姓名、身份证、手机号等 PII。PII 从前端提交后,经 Agent 编排、CardActionService 状态校验和后端加密链路进入 MCP/HIS;日志、Outbox、计量事件只保存摘要、字段完整度、HIS patientId 和 traceId。
 
 #### 5.11.3 舌诊
 
@@ -1242,6 +1274,7 @@ flowchart LR
 | `ai_agent_app` | 智能体元数据 | `agent_id`、`project_id`、`engine_config_id`、`agent_type`、`status` |
 | `ai_agent_engine_config` | 引擎连接配置 | `engine_type`、`config_json`、`project_id`、`status` |
 | `ai_conversation` | 开放平台会话 | `conversation_id`、`external_conversation_id`、`agent_id`、`user_id` |
+| `ai_task_instance` | 多轮业务任务状态 | `task_id`、`conversation_id`、`task_type`、`status`、`current_step`、`agent_code`、`context_json`、`device_id`、`trace_id` |
 | `ai_usage_log` | 原始调用日志 | `prompt_tokens`、`completion_tokens`、`workflow_run_id`、`latency_ms` |
 | `ai_card_definition` | 卡片定义 | `card_key`、`version`、`schema_json`、`ui_config_json`、`actions_json` |
 | `ai_card_instance` | 卡片实例 | `instance_id`、`conversation_id`、`message_id`、`state_json`、`status` |
@@ -1310,7 +1343,7 @@ flowchart LR
 
 | 接口类型 | 幂等键建议 | 重复请求返回 |
 | --- | --- | --- |
-| 对话请求 | `projectId + agentId + clientMessageId` | 返回第一次会话消息结果 |
+| 对话请求 | `projectId + resolvedAgentId/routeKey + clientMessageId` | 返回第一次会话消息结果;统一入口客户端未传 `agentId` 时由后端路由结果补齐 |
 | 设备事件 | `projectId + deviceId + eventType + clientEventId` | 返回第一次事件接收结果 |
 | 卡片动作 | `projectId + cardInstanceId + actionName + clientActionId` | 返回第一次动作结果 |
 | MCP 写工具 | `projectId + toolName + businessKey` | 返回第一次工具调用结果或最终查询结果 |
@@ -3427,7 +3460,7 @@ Redis 锁失败或 Redis 不可用时的降级策略:
 **读图要点:**
 
 - 卡片不是前端 UI,而是医疗业务动作的状态机。
-- 任何会改变 HIS、患者档案、预约订单、床位、账本的动作,都必须经过 `CardActionService`。
+- 任何会改变 HIS、患者档案、预约订单、床位、账本的动作,都必须经过 Agent 编排和 `CardActionService` 状态校验
 - 卡片动作产生业务事实,MCP 工具调用多为内部成本事实,二者不能重复向客户扣费。
 
 ### 21.4 MCP Tool Server:医院系统唯一工具出口
@@ -3467,7 +3500,7 @@ Redis 锁失败或 Redis 不可用时的降级策略:
 **读图要点:**
 
 - Dify 只能接收必要患者摘要,不应接收完整病历、身份证、手机号、舌/面原图等敏感原文。
-- PII 和医疗原文必须走“前端卡片动作 → CardActionService → MCP/HIS”的后端加密链路。
+- PII 和医疗原文必须走“前端卡片动作 → AgentActionOrchestrator → CardActionService → MCP/HIS”的后端加密链路。
 - 计量、账单、运营报告只保存计费用字段和聚合指标,不保存患者隐私明细。
 
 ### 21.8 初级工程师交付路线地图:先闭环,再扩场景

+ 65 - 11
docs/架构文档/AI中台工程约束.md

@@ -6,9 +6,9 @@
 | 当前章节 | Maven 工程依赖 + Controller/Service 调用方向 + 接口→模块映射 |
 | 适用范围 | 现有后端工程、二期 AI Agent / Card / MCP / Device / File、三期计量 / 账务 / 运营 |
 | 约束目标 | 高内聚、低耦合、可演进、可测试、可独立部署 |
-| 版本 | v2.0 |
-| 更新时间 | 2026-05-29 |
-| 变更说明 | 新增第 2 章:基于 v1.2 接口文档的接口归属、Controller/Service 层规则、调用方向禁止清单、Device/File 新模块职责、PR 评审检查清单 |
+| 版本 | v2.1 |
+| 更新时间 | 2026-05-31 |
+| 变更说明 | 补充统一入口客户端落位:后端能力仍归 AI 中台模块,前端/移动端另起 `emoon-terminal-client`;明确 AgentRouter、TaskStateService、TongueDiagnosisAdapter、HisMockAdapter 的模块归属;修正 Card 动作不能直连 MCP 的调用方向 |
 
 ## 1. Maven 工程依赖规范约束
 
@@ -34,6 +34,8 @@ API 模块只定义契约,不包含实现和重依赖。
 MCP 只负责工具协议和治理,不承载所有院内系统适配细节。
 ```
 
+本规约的工程取舍基于公开可验证案例做裁剪:东软类智慧医院平台证明“统一入口集成子系统”方向可行,但医梦只做 AI 入口层;华为/医惠类智慧病房证明病房物联网接入是重资产平台能力,因此住院设备只做事件映射;Dify 官方已有 Workflow 版本能力,因此后端只做版本映射和输出治理;科大讯飞类医疗 AI 产品强调嵌入医生流程,因此高风险动作必须通过卡片、工具审计和人工确认。以上案例用于约束边界,不作为照搬大厂平台规模的理由。
+
 ### 1.2 当前工程基线
 
 现有工程大体可以分为以下几层:
@@ -115,6 +117,8 @@ flowchart TB
     AgentApi["emoon-ai-agent-api"]
     CardApi["emoon-ai-card-api"]
     McpApi["emoon-ai-mcp-api"]
+    DeviceApi["emoon-ai-device-api"]
+    FileApi["emoon-ai-file-api"]
     MeterApi["emoon-ai-meter-api"]
     BillingApi["emoon-ai-billing-api"]
     ContractApi["emoon-ai-contract-api"]
@@ -123,6 +127,8 @@ flowchart TB
     Agent["emoon-ai-agent"]
     Card["emoon-ai-card"]
     Mcp["emoon-ai-mcp"]
+    Device["emoon-ai-device"]
+    File["emoon-ai-file"]
     Meter["emoon-ai-meter"]
     Billing["emoon-ai-billing"]
     Contract["emoon-ai-contract"]
@@ -134,6 +140,8 @@ flowchart TB
     Admin --> SystemApi
     Admin --> AgentApi
     Admin --> CardApi
+    Admin --> DeviceApi
+    Admin --> FileApi
     Admin --> BillingApi
     Admin --> ContractApi
     Admin --> OperationApi
@@ -141,6 +149,8 @@ flowchart TB
     Open --> SystemApi
     Open --> AgentApi
     Open --> CardApi
+    Open --> DeviceApi
+    Open --> FileApi
     Open --> MeterApi
 
     Extend --> AgentApi
@@ -150,6 +160,8 @@ flowchart TB
     Agent --> AgentApi
     Agent --> CardApi
     Agent --> McpApi
+    Agent --> DeviceApi
+    Agent --> FileApi
     Agent --> MeterApi
     Agent --> ContractApi
     Agent --> Common
@@ -161,9 +173,18 @@ flowchart TB
 
     Mcp --> McpApi
     Mcp --> SystemApi
+    Mcp --> FileApi
     Mcp --> Common
     Mcp --> External
 
+    Device --> DeviceApi
+    Device --> SystemApi
+    Device --> Common
+
+    File --> FileApi
+    File --> SystemApi
+    File --> Common
+
     Meter --> MeterApi
     Meter --> ContractApi
     Meter --> Common
@@ -238,8 +259,8 @@ flowchart TB
 
 | 模块 | 规划职责 |
 | --- | --- |
-| `emoon-ai-agent-api` | 定义 AI 调用、会话、流式事件、引擎抽象、编排命令 |
-| `emoon-ai-agent` | 实现 Agent 主链路、Dify 引擎、DirectLLM 引擎、Mock 引擎、SSE 事件转换、降级、幂等、Outbox |
+| `emoon-ai-agent-api` | 定义 AI 调用、会话、流式事件、引擎抽象、编排命令、终端任务状态只读 DTO |
+| `emoon-ai-agent` | 实现 Agent 主链路、AgentRouter、ConversationService、TaskStateService、DifyAgentEngine、DifyOutputNormalizer、DirectLLM 引擎、Mock 引擎、SSE 事件转换、降级、幂等、Outbox |
 
 `emoon-ai-agent` 是 AI 中台二期的主链路模块,但不能变成所有 AI 功能的大杂烩。
 
@@ -249,6 +270,8 @@ flowchart TB
 emoon-ai-agent-api
 emoon-ai-card-api
 emoon-ai-mcp-api
+emoon-ai-device-api
+emoon-ai-file-api
 emoon-ai-meter-api
 emoon-ai-contract-api
 emoon-system-api
@@ -272,6 +295,10 @@ admin
 emoon-ai-agent 可以产生计量事件,但不能直接扣款。
 emoon-ai-agent 可以读取合同授权快照,但不能维护合同。
 emoon-ai-agent 可以调用 MCP 工具 API,但不能绕过 MCP 治理直连 HIS。
+AgentRouter 和 TaskStateService 是 Agent 主链路内部能力,第一期不新增公共 /tasks API。
+TaskStateService 必须持久化到 `ai_task_instance`,不能只放 Redis 或 `ai_conversation.ext_json`;Redis 只能作为短期缓存。
+DifyOutputNormalizer 是 `DifyAgentEngine` 的内部组件,负责输出结构校验、卡片 schema 校验、风险等级归一和 SSE 事件归一,不新增独立模块。
+卡片动作涉及 HIS 写操作时,由 Agent 编排链路调用 Card 做状态校验,再调用 MCP 工具,不允许 Card 自己直连 MCP。
 ```
 
 #### 1.4.4 AI Card 模块
@@ -305,7 +332,7 @@ Card  -> MCP
 | 模块 | 规划职责 |
 | --- | --- |
 | `emoon-ai-mcp-api` | 定义工具注册、工具调用、工具权限、工具审计、工具结果契约 |
-| `emoon-ai-mcp` | 实现工具注册表、工具路由、权限校验、限流熔断、工具调用审计 |
+| `emoon-ai-mcp` | 实现工具注册表、工具路由、权限校验、限流熔断、工具调用审计、HisMockAdapter、RealHisAdapter、TongueDiagnosisAdapter |
 
 `emoon-ai-mcp` 不等于所有院内系统适配。它负责工具协议治理,具体 HIS / EMR / LIS / PACS 的适配可以继续放在当前合适模块中,或后续独立为 `hospital-integration` / `emoon-hospital-adapter`。
 
@@ -315,6 +342,8 @@ Card  -> MCP
 MCP 管工具治理,不管业务编排。
 HIS 适配管系统协议,不管 Agent 对话。
 Agent 管流程编排,不管 HIS 细节。
+TongueDiagnosisAdapter 在 MVP 阶段作为 MCP 受控工具适配器处理,后续形成独立专精产品线时再拆模块。
+HisMockAdapter 必须显式命名和配置,不能把 Mock 结果包装成真实医院交易。
 ```
 
 #### 1.4.6 计量模块
@@ -468,6 +497,31 @@ emoon-ai-mcp
 emoon-openplatform
 ```
 
+#### 1.4.12 统一入口客户端工程归属
+
+统一入口客户端不是一个新的后端系统。所有后端能力必须落在 AI 中台现有目标模块中:
+
+| 能力 | 归属模块 | 说明 |
+| --- | --- | --- |
+| 设备注册、心跳、场景配置、设备事件、命令轮询 | `emoon-ai-device` | 支撑机器人、自助机、导诊屏、诊室屏启动和运维 |
+| AgentRouter、ConversationService、TaskStateService、DifyAgentEngine、DifyOutputNormalizer | `emoon-ai-agent` | 统一入口客户端只调用 Agent API,不感知内部路由和输出归一 |
+| 卡片定义、实例、动作幂等、快照 | `emoon-ai-card` | 卡片动作的业务编排由 Agent 主链路承接 |
+| 文件上传、元数据、删除、访问审计 | `emoon-ai-file` | 舌象、身份证、报告、音频都先拿 `fileId` |
+| HisMockAdapter、RealHisAdapter、TongueDiagnosisAdapter | `emoon-ai-mcp` | 作为受控工具适配器,不让终端直连 |
+| 审计、计量、Outbox | `emoon-ai-meter` / `emoon-system` | 记录事实,不参与前端渲染 |
+
+前端和移动端独立为 `emoon-terminal-client` 工程:
+
+```text
+emoon-terminal-client
+├── apps/robot-client
+├── apps/kiosk-client
+├── apps/guide-screen-client
+└── packages/api-client / medical-cards / terminal-bridge / terminal-theme / terminal-core
+```
+
+后端仓库只维护 OpenAPI、DTO、业务模块和 Mock/Adapter。不得在 `emoon-backend` 内新增长期维护的终端 Demo 后端、前端页面构建或设备 SDK UI 代码。临时 Demo 只能作为联调样例,且必须通过 OpenPlatform API 调用正式后端能力。
+
 ### 1.5 依赖白名单
 
 #### 1.5.1 API 模块依赖白名单
@@ -969,7 +1023,7 @@ Controller 只负责 HTTP 协议适配。如果直接调 Mapper,会绕过 Appl
 | GET | `/devices/{id}/commands/poll` | emoon-openplatform | emoon-ai-device | 🔴 同上 |
 | POST | `/devices/{id}/commands/{cmd}/ack` | emoon-openplatform | emoon-ai-device | 🔴 同上 |
 | GET | `/cards/{id}` | emoon-openplatform | emoon-ai-card | 🔴 需新建 |
-| POST | `/cards/{id}/actions/{name}` | emoon-openplatform | emoon-ai-card | 🟡 路径需对齐 |
+| POST | `/cards/{id}/actions/{name}` | emoon-openplatform | emoon-ai-agent + emoon-ai-card | 🟡 路径需对齐;Agent 编排动作,Card 负责状态 |
 | POST | `/tools/{name}/invoke` | emoon-openplatform | emoon-ai-mcp | 🟡 雏形存在 |
 | GET | `/usage/summary` | emoon-openplatform | emoon-ai-meter | 🔴 meter 模块空壳 |
 | GET | `/meter/events` | emoon-openplatform | emoon-ai-meter | 🔴 同上 |
@@ -1034,7 +1088,6 @@ flowchart LR
 
     OC --> AG & CD & MC & DV & FL & MT & SY
     AG --> CD & MC & MT & CT & KN & SY & DF
-    CD --> MC
     MC --> HS
     DV --> SY
     FL --> SY
@@ -1049,8 +1102,9 @@ flowchart LR
 | 调用方向 | 是否允许 | 原因 |
 |---------|---------|------|
 | Controller → Service(通过 API 接口) | ✅ 允许 | 标准调用路径 |
-| emoon-ai-card → emoon-ai-mcp | ✅ 允许 | 卡片动作触发的 HIS 写操作必须通过 MCP |
 | emoon-ai-agent → emoon-ai-mcp | ✅ 允许 | Dify Workflow 工具调用必须通过 MCP |
+| emoon-ai-agent → emoon-ai-card | ✅ 允许 | Agent 编排链路需要创建卡片、推进卡片动作和写审计 |
+| emoon-ai-card → emoon-ai-mcp | ❌ 禁止 | 卡片动作触发的 HIS 写操作必须回到 Agent 编排链路,再由 Agent 调 MCP |
 | emoon-ai-mcp → emoon-ai-agent | ❌ 禁止 | 工具不编排 Agent 流程 |
 | emoon-ai-card → emoon-ai-agent | ❌ 禁止 | 卡片不发起 Agent 调用 |
 | emoon-ai-device → emoon-ai-agent | ❌ 禁止 | 设备不编排 AI 流程 |
@@ -1092,7 +1146,7 @@ Dify 调用只能出现在 `emoon-ai-agent` 的 `adapter` 包中,通过 `Agent
 import com.emoon.ai.mcp.application.HisLockScheduleService;
 ```
 
-卡片需要写 HIS 时,必须通过 Agent 编排链路(Card → Agent → MCP → HIS),
+卡片需要写 HIS 时,必须由 `CardActionController` 调 Agent 编排服务,Agent 调 Card 做状态校验和快照,再由 Agent 调 MCP 完成 HIS 写操作。
 不能出现 Card → MCP 的短路调用。
 
 **F4. 前端/设备端直接调 `/tools/{toolName}/invoke`**
@@ -1101,7 +1155,7 @@ import com.emoon.ai.mcp.application.HisLockScheduleService;
 ❌ 禁止:机器人、自助机、导诊屏厂商的 HTTP Client 直接 POST /api/v1/tools/his.createRegistration/invoke
 ```
 
-普通设备厂商只能通过 `/agent/chat`、`/cards/{id}/actions/{name}` 和 `/devices/*` 接入。
+普通设备厂商只能通过 `/agent/chat/stream`、`/cards/{id}/actions/{name}`、`/files/*` 和 `/devices/*` 接入。
 Tool Invoke 仅开放给医梦 MCP Tool Server、HIS/EMR/LIS/PACS 等受控系统适配方。
 
 **F5. Service 层写 HMAC 签名逻辑**

+ 23 - 4
docs/架构文档/工程规约生效说明.md

@@ -3,8 +3,8 @@
 | 项目 | 内容 |
 | --- | --- |
 | 文档定位 | 说明工程规约为什么存在、如何生效、违反后怎么处理 |
-| 适用范围 | Maven 模块结构、二期 Agent/Card/MCP、三期 Meter/Billing/Contract/Operation |
-| 更新时间 | 2026-05-27 |
+| 适用范围 | Maven 模块结构、二期 Agent/Card/MCP/Device/File、统一入口客户端后端能力、三期 Meter/Billing/Contract/Operation |
+| 更新时间 | 2026-05-31 |
 
 ## 1. 当前生效方式
 
@@ -18,6 +18,8 @@
 | 架构边界测试 | `emoon-admin/src/test/java/com/emoon/architecture/AiPlatformArchitectureTest.java` | 测试扫描源码,拦截新增违规 |
 | 历史违规基线 | `docs/architecture/legacy-architecture-baseline.txt` | 保留当前历史包袱,不影响启动;后续只允许减少,不允许增加 |
 
+规约采用“可验证案例 + 创业公司裁剪”的原则:参考东软智慧医院统一平台、华为/医惠智慧病房物联平台、Dify Workflow 版本能力、科大讯飞医疗 AI 辅助决策等公开实践,但只落地 AI 中台必须掌握的入口、卡片、设备场景、工具审计和计量能力,不复制大厂完整医院平台、物联网平台和重运营体系。
+
 ## 2. Maven 模块规约
 
 ### 2.1 新 AI API 聚合模块
@@ -27,6 +29,8 @@ emoon-infra/emoon-modules-api/emoon-ai-api
 ├── emoon-ai-agent-api
 ├── emoon-ai-card-api
 ├── emoon-ai-mcp-api
+├── emoon-ai-device-api
+├── emoon-ai-file-api
 ├── emoon-ai-meter-api
 ├── emoon-ai-billing-api
 ├── emoon-ai-contract-api
@@ -54,6 +58,8 @@ emoon-infra/emoon-modules/emoon-ai
 ├── emoon-ai-agent
 ├── emoon-ai-card
 ├── emoon-ai-mcp
+├── emoon-ai-device
+├── emoon-ai-file
 ├── emoon-ai-meter
 ├── emoon-ai-billing
 ├── emoon-ai-contract
@@ -75,7 +81,21 @@ Maven reactor 已纳入 emoon-modules。
 后续新增实现必须按 docs/templates/ai-platform-module-skeleton.md 的包结构落位。
 ```
 
-### 2.3 legacy 兼容原则
+### 2.3 统一入口客户端工程边界
+
+统一入口客户端后端能力仍在 AI 中台 Maven 工程中,不能新建长期维护的终端 Demo 后端:
+
+| 能力 | 后端落位 |
+| --- | --- |
+| Device Registry、Scene Profile、设备事件、设备命令 | `emoon-ai-device` |
+| AgentRouter、ConversationService、TaskStateService、DifyAgentEngine、DifyOutputNormalizer | `emoon-ai-agent` |
+| Card Runtime、卡片实例、卡片动作状态 | `emoon-ai-card` |
+| File Service、舌象/身份证/报告文件元数据 | `emoon-ai-file` |
+| HisMockAdapter、RealHisAdapter、TongueDiagnosisAdapter | `emoon-ai-mcp` |
+
+前端和移动端另起 `emoon-terminal-client` 工程,包含机器人、自助机、导诊屏等端侧应用和 `terminal-bridge`。该工程不进入后端 Maven reactor,后端只通过 OpenAPI、卡片 schema、设备命令协议和联调样例对其提供契约。
+
+### 2.4 legacy 兼容原则
 
 `emoon-mcp-api` 暂时保留,不立即删除。
 
@@ -267,4 +287,3 @@ mvn -pl emoon-admin -DskipTests=false -Dtest=AiPlatformArchitectureTest test
 ```
 
 如果本机 Maven 仓库缺少 surefire provider,首次运行可能需要联网下载 Maven 测试插件依赖。该架构测试本身不引入额外第三方库。
-

+ 1701 - 0
docs/架构文档/统一入口客户端技术设计文档_v1.0.md

@@ -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 编排
+→ 卡片动作
+→ 工具适配
+→ 审计计量
+```