---
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.