--- doc_id: DEV-202605-001 feature_id: FEAT-202606-001-unified-entry-client type: dev-progress title: Unified Terminal Client MVP Implementation Plan status: implemented owner: 医梦研发团队 created_at: 2026-05-31 updated_at: 2026-05-31 reviewers: [] related_docs: - PRD-202606-001 - DDS-202606-001 - API-202606-001 related_modules: - emoon-openplatform - emoon-ai-device - emoon-ai-agent - emoon-ai-card - emoon-ai-mcp tags: - 统一入口客户端 - MVP - 开发计划 --- # Unified Terminal Client MVP Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build a production-shaped MVP for the unified terminal client backend: device startup, scene loading, streamed chat, central intent routing with DeepSeek JSON, task state, card action loop, HIS mock, file/tongue adapter boundary, audit/trace hooks, and a minimal frontend integration contract. **Architecture:** Keep backend capabilities inside the AI platform Maven modules. `emoon-openplatform` exposes HTTP/SSE controllers; `emoon-ai-device` owns device registry and scene profile; `emoon-ai-agent` owns central routing, DeepSeek intent classification, conversation, task state, Dify/Mock dispatch and output normalization; `emoon-ai-card` owns card definitions/instances/action state; `emoon-ai-mcp` owns HisMockAdapter/TongueDiagnosisAdapter tool boundaries; `emoon-ai-file` owns file metadata. The terminal frontend lives in a separate `emoon-terminal-client` repo and only calls the OpenPlatform contract. **Tech Stack:** Java 17, Spring Boot/RuoYi-Vue-Plus module style, Maven multi-module, MyBatis-Plus, Redis optional cache, upgraded OpenPlatform HMAC/Bearer auth for new v1 controllers, DeepSeek OpenAI-compatible chat API for MVP intent JSON, deterministic template replies for MVP dialogue text, Dify later behind `DifyAgentEngine`, JUnit tests where module tests already run. Java baseline follows the root `pom.xml` `17`. If a submodule still declares Java 21 compiler settings, treat that as a separate build-baseline cleanup and do not introduce new Java 21 code in this MVP. --- ## Scope And MVP Cut MVP builds one reliable backend loop first: ```text device register -> heartbeat -> scene profile -> /agent/chat/stream -> AgentRouter -> DeepSeek JSON intent -> task/card response -> card action -> AgentActionOrchestrator -> HisMockAdapter -> card/task update -> audit/trace/meter event stub ``` MVP includes: - Robot: FAQ/guide intent and navigation command stub. - Kiosk: registration mock flow through department/doctor/time/confirm cards. - Guide screen: FAQ/department/route only, no privacy actions. MVP excludes: - Real HIS, payment,医保, refund, doctor station plugin, inpatient UI, public `/tasks` API, full billing ledger, full Dify workflow release system, full frontend implementation inside this backend repo. --- ## File Structure To Create Or Modify ### Maven Modules Create: - `emoon-infra/emoon-modules-api/emoon-ai-api/emoon-ai-device-api` - `emoon-infra/emoon-modules-api/emoon-ai-api/emoon-ai-file-api` - `emoon-infra/emoon-modules/emoon-ai/emoon-ai-device` - `emoon-infra/emoon-modules/emoon-ai/emoon-ai-file` Modify: - `emoon-infra/emoon-modules-api/emoon-ai-api/pom.xml` - `emoon-infra/emoon-modules/emoon-ai/pom.xml` - `emoon-openplatform/pom.xml` - `emoon-openplatform/src/main/java/.../controller/v1/*` ### API Contracts Create package roots: - `com.emoon.ai.device.api` - `com.emoon.ai.file.api` - `com.emoon.ai.agent.api` - `com.emoon.ai.card.api` - `com.emoon.ai.mcp.api` Key DTO/facade files: - `DeviceRegistryFacade` - `DeviceRegisterCommand`, `DeviceRegisterResult` - `HeartbeatCommand`, `HeartbeatResult` - `SceneProfileQuery`, `SceneProfileResult` - `DeviceEventCommand`, `DeviceCommandPollResult`, `DeviceCommandAckCommand` - `AgentChatCommand`, `AgentStreamEvent`, `ChatMessageCommand` - `IntentClassifyResult`, `RouteDecision` - `TaskInstanceDto`, `TaskStatus`, `TaskType` - `CardInstanceDto`, `CardActionCommand`, `CardActionResult` - `FileUploadCommand`, `FileObjectDto` - `McpToolInvokeCommand`, `McpToolInvokeResult` ### Implementation Modules Create package roots: ```text emoon-ai-device/src/main/java/com/emoon/ai/device ├── application ├── domain ├── infrastructure └── mapper emoon-ai-agent/src/main/java/com/emoon/ai/agent ├── application ├── domain ├── infrastructure/deepseek ├── infrastructure/dify └── infrastructure/normalizer emoon-ai-card/src/main/java/com/emoon/ai/card ├── application ├── domain ├── infrastructure └── mapper emoon-ai-mcp/src/main/java/com/emoon/ai/mcp ├── application ├── domain └── infrastructure/his emoon-ai-file/src/main/java/com/emoon/ai/file ├── application ├── domain ├── infrastructure └── mapper ``` ### OpenPlatform Controllers Create or modify under `emoon-openplatform/src/main/java/com/emoon/openplatform/controller/v1`: - `DeviceController` - `AgentChatController` - `CardActionController` - `FileController` - `ConversationController` Controller rule: ```text Controller only parses HTTP/header/SSE and calls Facade. No Mapper, no Dify client, no DeepSeek client, no HIS adapter, no billing logic. ``` --- ## Task 0: Upgrade OpenPlatform Auth For New v1 Controllers **Why first:** Current `emoon-openplatform` v1 controllers still use legacy `SignUtil.verify()` MD5 signing. New unified terminal endpoints must start on the target auth model so device, chat, card action and file interfaces do not inherit a known legacy security path. **Files:** - Create: `emoon-openplatform/src/main/java/com/emoon/openplatform/auth/HmacAuthInterceptor.java` - Create: `emoon-openplatform/src/main/java/com/emoon/openplatform/auth/BearerTokenInterceptor.java` - Create: `emoon-openplatform/src/main/java/com/emoon/openplatform/auth/OpenPlatformAuthContext.java` - Create: `emoon-openplatform/src/main/java/com/emoon/openplatform/config/OpenPlatformAuthConfig.java` - Modify: existing v1 controller auth wiring only where needed. - Keep: `emoon-openplatform/src/main/java/com/emoon/openplatform/util/SignUtil.java` for legacy compatibility, marked as deprecated usage for old callers. - [ ] Implement HMAC-SHA256 request auth: ```text Required headers: X-Emoon-Access-Key X-Emoon-Timestamp X-Emoon-Nonce X-Emoon-Signature Canonical payload: METHOD + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + sha256(body) ``` - [ ] Reject timestamp drift and replay nonce: ```text timestamp drift: 5 minutes nonce cache: Redis if available, local cache only for dev profile ``` - [ ] Implement Bearer token auth for service/system callers: ```text Authorization: Bearer ``` MVP can validate an opaque token from configuration for local/dev deployments. JWT validation can be added when the hospital IAM or gateway contract is known. - [ ] Apply new auth to new terminal endpoints: ```text POST /api/v1/devices/register POST /api/v1/devices/{deviceId}/heartbeat GET /api/v1/devices/{deviceId}/scene POST /api/v1/agent/chat/stream POST /api/v1/cards/{cardInstanceId}/actions POST /api/v1/files/upload ``` - [ ] Keep legacy MD5 route only for existing clients: ```text Existing AgentController/CardController behavior can remain until migration. New terminal controllers must not call SignUtil.verify(). ``` - [ ] Tests: ```text valid HMAC request -> 200 expired timestamp -> 401 reused nonce -> 401 invalid signature -> 401 valid bearer token -> 200 new terminal controller without auth -> 401 ``` **Commit:** `feat(openplatform): add hmac bearer auth for terminal endpoints` --- ## Task 1: Create Maven Device/File Modules **Files:** - Modify: `emoon-infra/emoon-modules-api/emoon-ai-api/pom.xml` - Modify: `emoon-infra/emoon-modules/emoon-ai/pom.xml` - Create: `emoon-infra/emoon-modules-api/emoon-ai-api/emoon-ai-device-api/pom.xml` - Create: `emoon-infra/emoon-modules-api/emoon-ai-api/emoon-ai-file-api/pom.xml` - Create: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-device/pom.xml` - Create: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-file/pom.xml` - [ ] Add API modules to `emoon-ai-api/pom.xml`: ```xml emoon-ai-device-api emoon-ai-file-api ``` - [ ] Add implementation modules to `emoon-ai/pom.xml`: ```xml emoon-ai-device emoon-ai-file ``` - [ ] Create `emoon-ai-device-api/pom.xml` using the same parent as sibling AI API modules. Expected shape: ```xml com.emoon emoon-ai-api ${revision} ../pom.xml emoon-ai-device-api ``` - [ ] Create `emoon-ai-file-api/pom.xml` with artifactId `emoon-ai-file-api`. - [ ] Create `emoon-ai-device/pom.xml` depending on `emoon-ai-device-api`, `emoon-system-api`, and common MyBatis/Spring dependencies following sibling implementation modules. - [ ] Create `emoon-ai-file/pom.xml` depending on `emoon-ai-file-api`, `emoon-system-api`, and common OSS/MyBatis dependencies. - [ ] Run: ```bash mvn -pl emoon-infra/emoon-modules-api/emoon-ai-api/emoon-ai-device-api,emoon-infra/emoon-modules-api/emoon-ai-api/emoon-ai-file-api,emoon-infra/emoon-modules/emoon-ai/emoon-ai-device,emoon-infra/emoon-modules/emoon-ai/emoon-ai-file -am -DskipTests compile ``` Expected: modules compile or fail only on missing parent dependency names that must be aligned with sibling module POMs. **Commit:** `feat(ai-terminal): add device and file maven modules` --- ## Task 2: Define MVP Database Migration **Files:** - Create: `script/sql/ai-terminal-mvp.sql` - Later apply to project database with normal migration process. Migration rule: ```text Do not drop or recreate existing tables. For existing ai_conversation, ai_card_definition and ai_card_instance, write guarded ALTER/CREATE INDEX steps through information_schema or the project's normal migration tool. For new tables, use CREATE TABLE. project_id type must follow the current project schema. The SQL below uses VARCHAR for external OpenPlatform project codes; if the deployed DB uses numeric sys_project IDs like the current `AiConversation.projectId`, use the same numeric type instead. ``` - [ ] Add these tables with minimal fields: ```sql CREATE TABLE ai_device_registry ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_id VARCHAR(64) NOT NULL, project_id VARCHAR(64) NOT NULL, hospital_id VARCHAR(64) NOT NULL, device_type VARCHAR(64) NOT NULL, management_mode VARCHAR(32) NOT NULL DEFAULT 'emoon_managed', vendor VARCHAR(64), model VARCHAR(128), location_json JSON, capabilities_json JSON, status VARCHAR(32) NOT NULL DEFAULT 'pending', admission_level VARCHAR(8) NOT NULL DEFAULT 'B', client_version VARCHAR(64), last_heartbeat DATETIME, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_device_id (device_id), KEY idx_project_hospital (project_id, hospital_id) ); CREATE TABLE ai_device_scene_binding ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_id VARCHAR(64) NOT NULL, scene_code VARCHAR(64) NOT NULL, ui_template VARCHAR(64) NOT NULL, agent_bindings_json JSON NOT NULL, tool_scopes_json JSON, card_scopes_json JSON, status VARCHAR(32) NOT NULL DEFAULT 'enabled', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_device_scene (device_id, scene_code) ); CREATE TABLE ai_device_event ( id BIGINT PRIMARY KEY AUTO_INCREMENT, event_id VARCHAR(96) NOT NULL, device_id VARCHAR(64) NOT NULL, event_type VARCHAR(64) NOT NULL, event_payload JSON, trace_id VARCHAR(96), occurred_at DATETIME NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_device_event (device_id, event_id), KEY idx_device_time (device_id, occurred_at) ); CREATE TABLE ai_device_command ( id BIGINT PRIMARY KEY AUTO_INCREMENT, command_id VARCHAR(96) NOT NULL, device_id VARCHAR(64) NOT NULL, command_type VARCHAR(64) NOT NULL, payload_json JSON NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'PENDING', expire_at DATETIME NOT NULL, ack_at DATETIME, ack_result_json JSON, trace_id VARCHAR(96), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_command_id (command_id), KEY idx_device_status (device_id, status) ); -- Existing table: ai_conversation -- Existing entity: emoon-ai-agent com.emoon.mcp.domain.AiConversation -- Do not recreate/drop this table. Migration must be guarded by information_schema -- or the project's migration tool so columns/indexes are added only when absent. ALTER TABLE ai_conversation ADD COLUMN hospital_id VARCHAR(64) NULL COMMENT '医院标识,用于统一入口终端上下文'; ALTER TABLE ai_conversation ADD COLUMN device_id VARCHAR(64) NULL COMMENT '统一入口终端设备ID'; ALTER TABLE ai_conversation ADD COLUMN client_message_id VARCHAR(96) NULL COMMENT '客户端消息幂等ID'; CREATE INDEX idx_ai_conversation_project_user ON ai_conversation(project_id, user_id); CREATE INDEX idx_ai_conversation_device ON ai_conversation(device_id); CREATE TABLE ai_conversation_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, message_id VARCHAR(96) NOT NULL, conversation_id VARCHAR(96) NOT NULL, role VARCHAR(32) NOT NULL, content TEXT, event_json JSON, trace_id VARCHAR(96), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_message_id (message_id), KEY idx_conv_time (conversation_id, created_at) ); CREATE TABLE ai_task_instance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id VARCHAR(96) NOT NULL, project_id VARCHAR(64) NOT NULL, conversation_id VARCHAR(96) NOT NULL, task_type VARCHAR(64) NOT NULL, status VARCHAR(32) NOT NULL, current_step VARCHAR(64) NOT NULL, agent_code VARCHAR(64), context_json JSON, patient_id VARCHAR(96), device_id VARCHAR(64), trace_id VARCHAR(96), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_task_id (task_id), KEY idx_project (project_id), KEY idx_conv_status (conversation_id, status) ); -- Existing table: ai_card_definition -- Existing entity: emoon-system-api com.emoon.system.domain.AiCardDefinition -- Existing mapper: emoon-system AiCardDefinitionMapper -- Do not recreate/drop this table. Reuse existing columns: card_key, version, -- schema_json, ui_config_json, actions_json, status, is_system, is_latest. CREATE INDEX idx_ai_card_definition_card_key ON ai_card_definition(card_key); -- Existing table: ai_card_instance -- Existing entity: emoon-ai-card com.emoon.mcp.domain.AiCardInstance -- Do not recreate/drop this table. Reuse instance_id/state_json/input_data/output_data. -- Add only terminal task correlation fields when absent. ALTER TABLE ai_card_instance ADD COLUMN task_id VARCHAR(96) NULL COMMENT '统一入口终端任务ID'; ALTER TABLE ai_card_instance ADD COLUMN trace_id VARCHAR(96) NULL COMMENT '链路追踪ID'; CREATE INDEX idx_ai_card_instance_conv_status ON ai_card_instance(conversation_id, status); CREATE INDEX idx_ai_card_instance_task ON ai_card_instance(task_id); CREATE TABLE ai_card_action_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, action_id VARCHAR(96) NOT NULL, card_instance_id VARCHAR(96) NOT NULL, action_name VARCHAR(64) NOT NULL, idempotency_key VARCHAR(128) NOT NULL, action_payload_json JSON, status VARCHAR(32) NOT NULL, trace_id VARCHAR(96), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_idempotency (idempotency_key), KEY idx_card_action (card_instance_id, action_name) ); CREATE TABLE ai_file_object ( id BIGINT PRIMARY KEY AUTO_INCREMENT, file_id VARCHAR(96) NOT NULL, project_id VARCHAR(64) NOT NULL, hospital_id VARCHAR(64), device_id VARCHAR(64), business_type VARCHAR(64) NOT NULL, sha256 VARCHAR(128), storage_path VARCHAR(512) NOT NULL, retention_policy VARCHAR(32) NOT NULL, sensitive_level VARCHAR(32) NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'available', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_file_id (file_id), KEY idx_project_type (project_id, business_type) ); ``` - [ ] Seed core card definitions: ```sql INSERT INTO ai_card_definition(card_key, version, name, schema_json, actions_json, status, is_system, is_latest) VALUES ('department-selection', '1.0', '科室选择', JSON_OBJECT('required', JSON_ARRAY('departments')), JSON_ARRAY(JSON_OBJECT('actionName', 'select_department')), '0', '1', TRUE), ('doctor-selection', '1.0', '医生选择', JSON_OBJECT('required', JSON_ARRAY('doctors')), JSON_ARRAY(JSON_OBJECT('actionName', 'select_doctor')), '0', '1', TRUE), ('time-slot-selection', '1.0', '号源选择', JSON_OBJECT('required', JSON_ARRAY('slots')), JSON_ARRAY(JSON_OBJECT('actionName', 'select_time_slot')), '0', '1', TRUE), ('confirm-appointment', '1.0', '挂号确认', JSON_OBJECT('required', JSON_ARRAY('summary')), JSON_ARRAY(JSON_OBJECT('actionName', 'confirm_appointment', 'requiredConfirm', TRUE)), '0', '1', TRUE), ('appointment-success', '1.0', '挂号成功', JSON_OBJECT('required', JSON_ARRAY('appointmentNo')), JSON_ARRAY(), '0', '1', TRUE), ('route-card', '1.0', '路线导航', JSON_OBJECT('required', JSON_ARRAY('routeText')), JSON_ARRAY(JSON_OBJECT('actionName', 'start_navigation')), '0', '1', TRUE), ('tongue-capture', '1.0', '舌象采集', JSON_OBJECT('required', JSON_ARRAY('uploadField')), JSON_ARRAY(JSON_OBJECT('actionName', 'submit_tongue_image')), '0', '1', TRUE) ON DUPLICATE KEY UPDATE name = VALUES(name), schema_json = VALUES(schema_json), actions_json = VALUES(actions_json), status = VALUES(status), is_system = VALUES(is_system), is_latest = VALUES(is_latest); ``` - [ ] Seed one kiosk device and scene: ```sql INSERT INTO ai_device_registry(device_id, project_id, hospital_id, device_type, status, admission_level, capabilities_json) VALUES ('EMOON-KIOSK-001', 'hospital_demo', 'H001', 'self_service_kiosk', 'activated', 'A', JSON_ARRAY('touch','camera','id_card_ocr','file_upload')); INSERT INTO ai_device_scene_binding(device_id, scene_code, ui_template, agent_bindings_json, card_scopes_json) VALUES ('EMOON-KIOSK-001', 'outpatient_kiosk', 'kiosk_home_v1', JSON_OBJECT('defaultAgent','opd-guide-agent','allowedAgents',JSON_ARRAY('opd-guide-agent','opd-triage-agent','opd-registration-agent','tongue-diagnosis-agent')), JSON_ARRAY('department-selection','doctor-selection','time-slot-selection','confirm-appointment','appointment-success','tongue-capture')); ``` **Commit:** `feat(ai-terminal): add mvp database schema` --- ## Task 3: Implement Device Registry And Scene Profile **Files:** - Create API DTOs in `emoon-ai-device-api/src/main/java/com/emoon/ai/device/api` - Create services in `emoon-ai-device/src/main/java/com/emoon/ai/device/application` - Create controller in `emoon-openplatform/src/main/java/com/emoon/openplatform/controller/v1/DeviceController.java` - [ ] Define `DeviceRegisterCommand`: ```java public record DeviceRegisterCommand( String projectId, String hospitalId, String deviceCode, String deviceType, String vendor, String model, String clientVersion, Map capabilities, Map location ) {} ``` - [ ] Define `DeviceRegisterResult`: ```java public record DeviceRegisterResult( String deviceId, String deviceSecret, String activateStatus, String admissionLevel, String reason ) {} ``` Allowed `activateStatus`: ```text activated: A/B approved device, can enter core flow. pending: waiting for ops approval, can only heartbeat. rejected: C/D or unknown device, cannot enter core flow. ``` - [ ] Implement `DeviceRegistryFacade.register(command)`: Rules: ```text Existing activated device: return activated. Known project + allowed deviceType: create/update as activated with A/B. Unknown deviceCode: create pending. Device type blocked or explicit C/D: return rejected. ``` - [ ] Implement `heartbeat(deviceId)`: Updates `last_heartbeat`, `client_version`, status summary. Reject if device not found or status rejected. - [ ] Implement `scene(deviceId)`: Returns: ```json { "deviceId": "EMOON-KIOSK-001", "deviceType": "self_service_kiosk", "sceneCode": "outpatient_kiosk", "homeTemplate": "kiosk_home_v1", "defaultAgent": "opd-guide-agent", "allowedAgents": ["opd-guide-agent", "opd-triage-agent", "opd-registration-agent", "tongue-diagnosis-agent"], "blockedIntents": [], "capabilities": ["touch", "camera", "id_card_ocr", "file_upload"], "allowedCards": ["department-selection", "doctor-selection", "time-slot-selection", "confirm-appointment", "appointment-success", "tongue-capture"] } ``` - [ ] Add OpenPlatform endpoints: ```text POST /api/v1/devices/register POST /api/v1/devices/{deviceId}/heartbeat GET /api/v1/devices/{deviceId}/scene ``` - [ ] Test manually with curl: ```bash curl -X POST http://localhost:8080/api/v1/devices/register \ -H 'Content-Type: application/json' \ -d '{"projectId":"hospital_demo","hospitalId":"H001","deviceCode":"EMOON-KIOSK-001","deviceType":"self_service_kiosk","clientVersion":"0.1.0","capabilities":{"touch":true,"camera":true}}' ``` Expected: ```json {"data":{"deviceId":"EMOON-KIOSK-001","activateStatus":"activated","admissionLevel":"A"}} ``` **Commit:** `feat(ai-terminal): implement device startup` --- ## Task 4: Define Agent Chat Stream Contract **Files:** - Modify/create in `emoon-ai-agent-api` - Modify `docs/api/openapi/医梦AI中台开放平台接口-v1.2.openapi.yaml` only if contract diverges. - Create `AgentChatController` in `emoon-openplatform`. - [ ] Define `AgentChatCommand`: ```java public record AgentChatCommand( String projectId, String hospitalId, String conversationId, String clientMessageId, String agentId, String userId, String deviceId, String deviceType, String text, Map inputs ) {} ``` `agentId` is nullable. For terminal client, Router resolves it. - [ ] Define SSE event names: ```text message_delta message_completed card_created task_updated usage_reported error completed ``` - [ ] Implement controller endpoint: ```text POST /api/v1/agent/chat/stream ``` Controller behavior: ```text 1. Validate auth/project context. 2. Build AgentChatCommand. 3. Call AgentChatApplicationService.chatStream(command). 4. Return SseEmitter or existing project SSE wrapper. ``` - [ ] First implementation can return a temporary SSE smoke-test response. Remove this hard-coded text once Task 6.5 templates are wired: ```json { "type": "message_delta", "text": "您好,我可以帮您导诊、分诊或挂号。" } { "type": "message_completed", "conversationId": "conv_xxx", "messageId": "msg_xxx" } { "type": "completed", "conversationId": "conv_xxx" } ``` - [ ] Manual test: ```bash curl -N -X POST http://localhost:8080/api/v1/agent/chat/stream \ -H 'Content-Type: application/json' \ -d '{"deviceContext":{"deviceId":"EMOON-KIOSK-001","deviceType":"self_service_kiosk"},"message":{"type":"text","text":"我要挂号","clientMessageId":"m001"}}' ``` Expected: streaming text events without requiring `agentId`. **Commit:** `feat(ai-terminal): add agent chat stream contract` --- ## Task 5: Conversation And TaskStateService **Files:** - Create `ConversationService` and `TaskStateService` in `emoon-ai-agent`. - Reuse existing `com.emoon.mcp.domain.AiConversation` and `AiConversationMapper`. - Create Mapper/DO only for `ai_conversation_message` and `ai_task_instance`, plus small mapper methods needed for the new terminal fields. - [ ] Implement `ConversationService.createOrLoad(command)`: Rules: ```text If conversationId absent: create `conv_`. If conversationId present: verify project scope and load. Always append user message with traceId. ``` - [ ] Implement `TaskStateService.getActiveTask(conversationId)`: Returns active task where status in: ```text ACTIVE WAITING_CARD_ACTION PROCESSING ``` - [ ] Implement `TaskStateService.createTask(conversationId, taskType, agentCode, currentStep, context)`: Default steps: ```text REGISTRATION -> COLLECT_SYMPTOM TONGUE_DIAGNOSIS -> COLLECT_CHIEF_COMPLAINT GUIDE -> UNDERSTAND_DESTINATION ``` - [ ] Implement `TaskStateService.patchContext(taskId, contextPatch)`: Merge patch into `context_json`; do not overwrite existing unrelated keys. - [ ] Tests: ```text create conversation without ID -> returns new ID. get active task when none -> null/empty. create registration task -> currentStep COLLECT_SYMPTOM. patch context with departmentId -> preserves symptom. ``` **Commit:** `feat(ai-terminal): add conversation and task state` --- ## Task 6: Central Intent Recognition With DeepSeek JSON **Files:** - Create: `emoon-ai-agent/src/main/java/com/emoon/ai/agent/infrastructure/deepseek/DeepSeekIntentClient.java` - Create: `emoon-ai-agent/src/main/java/com/emoon/ai/agent/application/IntentClassifier.java` - Create: `emoon-ai-agent/src/main/java/com/emoon/ai/agent/domain/IntentClassifyResult.java` - Config keys in normal Spring config: - `emoon.ai.deepseek.base-url` - `emoon.ai.deepseek.api-key` - `emoon.ai.deepseek.model` - `emoon.ai.deepseek.timeout-ms` - [ ] Define output JSON schema: ```json { "intentCode": "REGISTRATION", "confidence": 0.91, "taskType": "REGISTRATION", "routeAgentCode": "opd-registration-agent", "scenarioCode": "OUTPATIENT_REGISTRATION", "slots": { "symptom": "头疼三天", "targetLocation": null }, "riskLevel": "L1", "needClarification": false, "clarificationQuestion": null } ``` Allowed `intentCode`: ```text GUIDE_FAQ ROUTE_NAVIGATION TRIAGE REGISTRATION TONGUE_DIAGNOSIS CANCEL_TASK UNKNOWN ``` - [ ] Prompt DeepSeek with strict JSON instruction: ```text 你是医院统一入口客户端的中央意图识别器。只输出 JSON,不输出 Markdown。 你不能做诊断,不能承诺真实挂号成功,不能生成支付或医保结果。 根据用户输入、设备类型、当前任务、等待卡片,识别意图。 设备限制: - guide_screen 不允许建档、挂号确认、舌象采集、报告解读。 - robot 允许导诊和路线导航,不做正式实名挂号。 - self_service_kiosk 允许建档、分诊、挂号 Mock、舌诊采集。 输出字段必须完整: intentCode, confidence, taskType, routeAgentCode, scenarioCode, slots, riskLevel, needClarification, clarificationQuestion。 ``` - [ ] Implement `DeepSeekIntentClient.classify(input)` using OpenAI-compatible chat completions. Request shape: ```json { "model": "deepseek-chat", "temperature": 0, "response_format": { "type": "json_object" }, "messages": [ { "role": "system", "content": "" }, { "role": "user", "content": "{\"text\":\"我要挂神经内科\",\"deviceType\":\"self_service_kiosk\"}" } ] } ``` - [ ] Add deterministic fallback before DeepSeek: ```text contains "挂号" or "预约" -> REGISTRATION contains "舌诊" or "看舌头" -> TONGUE_DIAGNOSIS contains "带我去" or "怎么走" -> ROUTE_NAVIGATION contains "科室" or "挂什么科" -> TRIAGE ``` - [ ] Add fallback after DeepSeek: ```text JSON parse fail -> UNKNOWN with needClarification=true. confidence < 0.65 -> UNKNOWN with clarification question. routeAgentCode not in scene allowedAgents -> blocked route decision. ``` - [ ] Unit tests: ```text "我要挂号" on kiosk -> REGISTRATION, opd-registration-agent. "带我去检验科" on robot -> ROUTE_NAVIGATION, opd-guide-agent. "我要舌诊" on guide_screen -> blocked by device policy. DeepSeek invalid JSON -> UNKNOWN clarification. ``` **Commit:** `feat(ai-terminal): add deepseek intent classifier` --- ## Task 6.5: Add Deterministic MVP Reply Templates **Why:** DeepSeek is used only as a structured intent classifier in MVP. It must not freely generate patient-facing medical dialogue. Until `DifyAgentEngine` is introduced in P1, natural language replies come from audited static templates selected by route decision, task step and device type. **Files:** - Create: `emoon-ai-agent/src/main/java/com/emoon/ai/agent/application/TerminalReplyTemplateService.java` - Create: `emoon-ai-agent/src/main/java/com/emoon/ai/agent/domain/TerminalReply.java` - Add tests beside agent application tests. - [ ] Implement template selection by route decision: ```text REGISTRATION -> "好的,我来帮您走挂号流程。请先简单描述您的症状或选择目标科室。" TRIAGE -> "我先帮您做科室分诊建议。请描述主要不适、持续时间和是否伴随发热等情况。" TONGUE_DIAGNOSIS -> "可以,我会先采集舌象图片,并给出仅供参考的中医体质分析。请按屏幕提示上传清晰舌象。" ROUTE_NAVIGATION -> "我来帮您查找路线。请告诉我要去的科室、检查项目或地点名称。" GUIDE_FAQ -> "我可以回答门诊流程、科室位置、就诊准备等问题。" UNKNOWN -> "我还没有理解您的需求。您可以说挂号、分诊、路线导航或门诊咨询。" BLOCKED -> use RouteDecision.blockedMessage. ``` - [ ] Keep templates risk-limited: ```text No diagnosis conclusion. No promise of real appointment success. No payment/医保 result. No medication advice. No free-form DeepSeek text shown directly to patient. ``` - [ ] Make templates configurable later: ```text MVP: enum/map in Java code. P1: move to ai_agent_reply_template or Dify workflow response nodes after ops review. ``` - [ ] Tests: ```text REGISTRATION route returns registration template. blocked route returns blockedMessage. UNKNOWN route returns clarification-safe template. DeepSeek clarificationQuestion is only displayed after whitelist/risk check, otherwise use UNKNOWN template. ``` **Commit:** `feat(ai-terminal): add deterministic reply templates` --- ## Task 7: AgentRouter MVP **Files:** - Create: `AgentRouterService` - Create: `DevicePolicyRouter` - Create: `TaskRouter` - Create: `CardWaitingRouter` - Use `IntentClassifier` from Task 6. - [ ] Implement route order: ```text 1. Device policy. 2. Active task. 3. Waiting card. 4. Deterministic keyword route. 5. DeepSeek JSON route. 6. Clarification. ``` - [ ] Device policy: ```text guide_screen blocks REGISTRATION final actions and TONGUE_DIAGNOSIS capture. robot allows GUIDE_FAQ, ROUTE_NAVIGATION, TRIAGE; registration becomes guide-to-kiosk. kiosk allows REGISTRATION, TRIAGE, TONGUE_DIAGNOSIS. ``` - [ ] Route decision structure: ```java public record RouteDecision( boolean blocked, String blockedMessage, String routeAgentCode, String intentCode, String taskPolicy, String taskType, String scenarioCode, double confidence, Map slots ) {} ``` - [ ] Task policy values: ```text CREATE_NEW_TASK CONTINUE_ACTIVE_TASK WAIT_CARD_ACTION DIRECT_ANSWER ASK_CLARIFICATION BLOCKED ``` - [ ] Tests: ```text active task wins over new random text. waiting card returns WAIT_CARD_ACTION. guide_screen "我要挂号" returns blocked or guide-to-kiosk. kiosk "我要挂号" creates REGISTRATION task. ``` **Commit:** `feat(ai-terminal): implement mvp agent router` --- ## Task 8: Card Runtime MVP **Files:** - Use/create `emoon-ai-card-api`, `emoon-ai-card`. - Create: - `CardDefinitionService` - `CardInstanceService` - `CardActionService` - Reuse existing `AiCardDefinitionMapper`, `AiCardInstanceMapper`, `AiCardDefinition`, and `AiCardInstance`; create only `ai_card_action_log` Mapper/DO and terminal-specific query methods. - [ ] Implement `createCard(conversationId, taskId, cardKey, cardData, traceId)`: Validation: ```text cardKey exists and status enabled. cardData contains required fields declared in schema_json.required. device scene allows cardKey. ``` - [ ] Implement action states: ```text active -> submitted -> processing -> completed active -> expired processing -> failed ``` - [ ] Implement `CardActionService.submit(command)`: ```text 1. Check idempotency key. 2. Load card instance. 3. Reject expired/completed/cancelled. 4. Check actionName exists in card definition. 5. Write ai_card_action_log with submitted. 6. Return CardActionResult for AgentActionOrchestrator. ``` - [ ] P0 cards to support: ```text department-selection doctor-selection time-slot-selection confirm-appointment appointment-success route-card tongue-capture ``` - [ ] Tests: ```text create department card with departments passes. create department card without departments fails. submit same idempotency key twice returns first result. submit expired card returns CARD_EXPIRED. ``` **Commit:** `feat(ai-terminal): implement card runtime mvp` --- ## Task 9: HisMockAdapter And Tool Gateway MVP **Files:** - Use/create `emoon-ai-mcp-api`, `emoon-ai-mcp`. - Create: - `McpToolService` - `HospitalAdapter` - `HisMockAdapter` - `ToolInvocationLogService` - [ ] Define tools: ```text queryDepartments queryDoctors querySchedules lockSchedule createAppointment ``` - [ ] HisMock static data: ```json { "departments": [ { "deptId": "neurology", "name": "神经内科", "reason": "头痛、头晕、肢体麻木等症状可先咨询" }, { "deptId": "respiratory", "name": "呼吸内科", "reason": "咳嗽、发热、气喘等症状可先咨询" } ], "doctors": { "neurology": [ { "doctorId": "doc_n_001", "name": "王医生", "title": "副主任医师" } ] }, "schedules": { "doc_n_001": [ { "slotId": "slot_001", "time": "2026-06-01 09:00", "remain": 3 } ] } } ``` - [ ] Implement `createAppointment` mock: Return: ```json { "appointmentNo": "MOCK-APPT-20260601-0001", "mock": true, "message": "这是联调 Mock 预约结果,不代表真实医院挂号成功。" } ``` - [ ] Enforce red lines: ```text No QR payment code. No医保结算. No "真实挂号成功" wording. No random doctor/source from frontend or Dify. ``` **Commit:** `feat(ai-terminal): add his mock adapter` --- ## Task 10: AgentActionOrchestrator For Registration Mock **Files:** - Create: `emoon-ai-agent/src/main/java/com/emoon/ai/agent/application/AgentActionOrchestrator.java` - Use `CardActionService`, `TaskStateService`, `McpToolService`, `CardInstanceService`. - [ ] Implement action flow: ```text select_department: store departmentId in task context call queryDoctors create doctor-selection card select_doctor: store doctorId call querySchedules create time-slot-selection card select_time_slot: store slotId create confirm-appointment card confirm_appointment: call lockSchedule call createAppointment complete task create appointment-success card ``` - [ ] Ensure all actions: ```text Call CardActionService first for idempotency/state. Use task context as source of truth. Write traceId into card/action/tool events. Return synthetic message payload for optional Dify continuation. ``` - [ ] Tests: ```text select_department creates doctor-selection card. select_doctor creates time-slot-selection card. confirm_appointment creates appointment-success and completes task. repeat confirm_appointment with same idempotency key returns first result. ``` **Commit:** `feat(ai-terminal): orchestrate registration card actions` --- ## Task 11: Agent Chat Application Service End-To-End **Files:** - Create: `AgentChatApplicationService` - Wire: - `ConversationService` - `DeviceRegistryFacade` - `SceneProfileService` - `TaskStateService` - `AgentRouterService` - `TerminalReplyTemplateService` - `MockAgentEngine` - `CardInstanceService` - [ ] Implement flow: ```text 1. Resolve device and scene. 2. Create/load conversation. 3. Append user message. 4. Route request. 5. Select patient-facing text through `TerminalReplyTemplateService`. 6. If blocked -> template blocked message + completed. 7. If registration task create/continue -> create first relevant card. 8. If guide/FAQ -> return template text answer or curated FAQ text. 9. If route navigation -> create route-card. 10. Write trace/audit/meter stub event. ``` - [ ] MVP without Dify: For first executable MVP, return deterministic cards and audited static reply templates after Router. This proves backend loop before Dify complexity and avoids unconstrained medical text generation. - [ ] Add DeepSeek route: For unknown/ambiguous text, call DeepSeek JSON classifier and route based on `intentCode`. - [ ] Dify can be enabled behind feature flag: ```text emoon.ai.agent.engine-mode=mock|deepseek-router|dify ``` - [ ] Manual E2E test: ```bash curl -N -X POST http://localhost:8080/api/v1/agent/chat/stream \ -H 'Content-Type: application/json' \ -d '{"deviceContext":{"deviceId":"EMOON-KIOSK-001","deviceType":"self_service_kiosk"},"message":{"type":"text","text":"我头疼三天,想挂号","clientMessageId":"m-reg-001"}}' ``` Expected event sequence: ```json {"type":"task_updated","taskType":"REGISTRATION","currentStep":"COLLECT_SYMPTOM"} {"type":"message_delta","text":"好的,我来帮您走挂号流程。请先简单描述您的症状或选择目标科室。"} {"type":"card_created","cardKey":"department-selection","cardInstanceId":"card_xxx"} {"type":"completed","conversationId":"conv_xxx"} ``` **Commit:** `feat(ai-terminal): wire chat stream mvp` --- ## Task 12: Device Command MVP For Robot Navigation **Files:** - Use `emoon-ai-device`. - Create: - `DeviceCommandService` - endpoint `/devices/{deviceId}/commands/poll` - endpoint `/devices/{deviceId}/commands/{commandId}/ack` - [ ] Implement command creation: ```text commandType = NAVIGATE_TO_LOCATION payload = { "locationId": "lab", "routeText": "从门诊大厅向左..." } status = PENDING expireAt = now + 5 minutes ``` - [ ] Poll behavior: ```text Return PENDING commands where expireAt > now. Mark expired commands as EXPIRED before query. Do not require robot public callback. ``` - [ ] Ack behavior: ```text success -> status SUCCESS, ack_at set. failed -> status FAILED, ack_result_json stores reason. processing -> keep PROCESSING. rejected -> status REJECTED. ``` - [ ] Manual test: ```bash curl http://localhost:8080/api/v1/devices/EMOON-ROBOT-001/commands/poll ``` Expected: ```json {"data":{"commands":[]}} ``` **Commit:** `feat(ai-terminal): add robot command polling` --- ## Task 13: File Service And Tongue Adapter Boundary **Files:** - Use `emoon-ai-file-api`, `emoon-ai-file`. - Use `emoon-ai-mcp` for `TongueDiagnosisAdapter`. - Create controller `FileController`. - [ ] Implement `/files/upload` MVP: Accepted `businessType`: ```text ID_CARD TONGUE_IMAGE REPORT_IMAGE AUDIO ``` Return: ```json { "fileId": "file_xxx", "businessType": "TONGUE_IMAGE", "retentionPolicy": "episode", "sensitiveLevel": "P3" } ``` - [ ] MVP storage: ```text If OSS already configured: use existing OSS abstraction. If not configured in local dev: write to controlled local dev path and mark storage_path as local-dev://file_xxx. ``` - [ ] Implement `TongueDiagnosisAdapter` as interface with mock implementation: ```java TongueDiagnosisResult startDiagnosis(String fileId, String chiefComplaint, Map patientContext) ``` Mock result: ```json { "diagnosisId": "mock_tongue_001", "summary": "舌象图片已接收,当前为演示分析结果。", "riskLevel": "L1", "mock": true } ``` - [ ] Red lines: ```text Frontend never calls tongue API directly. Dify never receives base64 image. Card/Dify text says AI辅助分析, not diagnosis. ``` **Commit:** `feat(ai-terminal): add file and tongue adapter mvp` --- ## Task 14: Audit, Trace, Meter Stubs **Files:** - Use `emoon-ai-meter-api`, `emoon-ai-meter` or simple stub if meter module not ready. - Create: - `TraceContext` - `AuditEventPublisher` - `MeterEventProducer` - [ ] Generate `traceId` at OpenPlatform entry if absent. - [ ] Include `traceId` in: ```text conversation message task instance card instance card action log tool invocation log device event meter event SSE error ``` - [ ] MVP meter events: ```text AGENT_CHAT_COMPLETED CARD_ACTION_CONFIRMED MCP_TOOL_WRITE DEVICE_COMMAND_CREATED FILE_UPLOADED ``` - [ ] For MVP, meter event can write to a table or log file, but method signature must match future meter: ```java void produce(MeterEventCommand command) ``` - [ ] Do not block medical flow on meter failure: ```text Business result succeeds. Outbox/meter failure is logged and retryable. ``` **Commit:** `feat(ai-terminal): add trace audit meter hooks` --- ## Task 15: OpenAPI And Frontend Contract Package **Files:** - Modify: `docs/api/openapi/医梦AI中台开放平台接口-v1.2.openapi.yaml` - Create: `docs/initiatives/FEAT-202606-001-unified-entry-client/MVP接口契约.md` - [ ] Confirm OpenAPI includes: ```text POST /devices/register POST /devices/{deviceId}/heartbeat GET /devices/{deviceId}/scene POST /agent/chat/stream POST /files/upload GET /cards/{cardInstanceId} POST /cards/{cardInstanceId}/actions/{actionName} POST /devices/{deviceId}/events GET /devices/{deviceId}/commands/poll POST /devices/{deviceId}/commands/{commandId}/ack ``` - [ ] Confirm `AgentChatRequest.agentId` is optional. - [ ] Create terminal contract doc with request examples for: ```text startup chat stream card action file upload robot command poll device event ``` - [ ] Add frontend forbidden calls: ```text No Dify API. No /tools/{toolName}/invoke. No tongue low-level API. No meter write API. No direct HIS. ``` **Commit:** `docs(ai-terminal): publish terminal mvp contract` --- ## Task 16: Minimal Frontend Repo Bootstrap Guidance This backend repo should not contain the terminal frontend. Create a separate repository: ```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 ``` - [ ] Backend delivers `openapi.yaml` and examples. - [ ] Frontend first screen flow: ```text load config -> register device -> heartbeat -> fetch scene -> render home -> open chat stream ``` - [ ] Frontend card rendering MVP: ```text department-selection doctor-selection time-slot-selection confirm-appointment appointment-success route-card tongue-capture ``` - [ ] Frontend calls only: ```text /devices/register /devices/{deviceId}/heartbeat /devices/{deviceId}/scene /agent/chat/stream /files/upload /cards/{cardInstanceId} /cards/{cardInstanceId}/actions/{actionName} /devices/{deviceId}/events /devices/{deviceId}/commands/poll /devices/{deviceId}/commands/{commandId}/ack ``` **Commit in frontend repo:** `feat: bootstrap terminal client mvp` --- ## Task 17: End-To-End MVP Acceptance - [ ] Start backend. - [ ] Seed DB. - [ ] Device startup acceptance: ```text register returns activated. heartbeat updates last_heartbeat. scene returns allowedAgents and allowedCards. pending/rejected device cannot call core flow. ``` - [ ] Chat acceptance: ```text kiosk "我头疼三天,想挂号" -> REGISTRATION task + department-selection card. guide screen "我要舌诊" -> blocked/privacy guidance. robot "带我去检验科" -> route-card + command if robot supports navigation. ``` - [ ] Card action acceptance: ```text select_department -> doctor-selection. select_doctor -> time-slot-selection. select_time_slot -> confirm-appointment. confirm_appointment -> appointment-success mock card. ``` - [ ] Safety acceptance: ```text No frontend request to /tools. No Dify key in frontend response. No base64 image in Dify inputs. No "真实挂号成功" in mock result. All write actions have idempotency key. ``` - [ ] Trace acceptance: ```text One traceId can find conversation, task, card, action, tool mock, meter stub. ``` **Commit:** `test(ai-terminal): verify terminal mvp e2e` --- ## Recommended Implementation Order 1. Task 0 OpenPlatform auth upgrade. 2. Task 1 Maven modules. 3. Task 2 schema. 4. Task 3 device startup. 5. Task 4 chat stream contract. 6. Task 5 conversation/task and Task 6 DeepSeek intent JSON can run in parallel after Task 4. 7. Task 6.5 deterministic reply templates. 8. Task 7 router. 9. Task 8 card runtime Mapper/DO/service skeleton and Task 9 HisMockAdapter can run in parallel after Task 7 contract is stable. 10. Task 10 registration action orchestrator. 11. Task 11 chat end-to-end. 12. Task 12 robot command poll. 13. Task 13 file/tongue. 14. Task 14 trace/audit/meter. 15. Task 15 OpenAPI/frontend contract. 16. Task 16 frontend repo bootstrap. 17. Task 17 acceptance. Parallel work split for a 3-person MVP team: ```text Engineer A: Task 0 -> Task 3 -> Task 11 integration. Engineer B: Task 1/2 -> Task 5 -> Task 8. Engineer C: Task 4 -> Task 6 -> Task 6.5 -> Task 9. Task 10 waits for Task 8 and Task 9. Task 11 waits for Task 5, Task 6.5, Task 7, Task 8 and Task 10. ``` Stop after Task 11 if schedule is tight. At that point the highest-value MVP already proves: ```text device -> scene -> chat -> route -> task -> card -> action -> mock HIS -> trace ``` --- ## Non-Negotiable Red Lines - Do not create `terminal-backend`. - Do not put frontend code in backend Maven modules. - Do not let frontend call Dify, HIS Tool, tongue low-level API, or meter write API. - Do not make `agentId` required for unified terminal client chat. - Do not persist TaskState only in Redis. - Do not let Card Runtime directly call MCP; action orchestration goes through `AgentActionOrchestrator`. - Do not present Mock appointment as real hospital appointment. - Do not include payment,医保, refund, inpatient UI, or full billing in MVP.