Просмотр исходного кода

docs: add terminal client MVP development plan

17-task implementation plan with scope, architecture, file structure,
and non-negotiable red lines for the unified terminal client backend.
WangKang 2 недель назад
Родитель
Сommit
ab57197b83
1 измененных файлов с 1559 добавлено и 0 удалено
  1. 1559 0
      docs/superpowers/plans/2026-05-31-terminal-client-mvp.md

+ 1559 - 0
docs/superpowers/plans/2026-05-31-terminal-client-mvp.md

@@ -0,0 +1,1559 @@
+# 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` `<java.version>17</java.version>`. 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 <jwt-or-opaque-token>
+```
+
+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
+<module>emoon-ai-device-api</module>
+<module>emoon-ai-file-api</module>
+```
+
+- [ ] Add implementation modules to `emoon-ai/pom.xml`:
+
+```xml
+<module>emoon-ai-device</module>
+<module>emoon-ai-file</module>
+```
+
+- [ ] Create `emoon-ai-device-api/pom.xml` using the same parent as sibling AI API modules.
+
+Expected shape:
+
+```xml
+<parent>
+    <groupId>com.emoon</groupId>
+    <artifactId>emoon-ai-api</artifactId>
+    <version>${revision}</version>
+    <relativePath>../pom.xml</relativePath>
+</parent>
+<artifactId>emoon-ai-device-api</artifactId>
+```
+
+- [ ] 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<String, Object> capabilities,
+    Map<String, Object> 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/接口文档/emoon-ai-openplatform-api-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<String, Object> 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_<snowflake>`.
+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": "<system prompt>" },
+    { "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<String, Object> 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<String, Object> 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/接口文档/emoon-ai-openplatform-api-v1.2.openapi.yaml`
+- Create: `docs/接口文档/terminal-client-mvp-contract.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.