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