多醫院機器人平台

1. 架構定位

採用:

Modular Monolith + Hexagonal Architecture + Clean Dependency Rule

原則:

  • 目前維持單一 NestJS 應用。
  • 使用模組邊界隔離業務能力。
  • 使用 Ports & Adapters 隔離外部系統。
  • 程式碼依賴只能指向 Application、Domain 與 Ports。
  • 不拆微服務、不拆資料庫。
  • 第二家醫院出現前,不建立額外 npm packages 或 repositories。

2. 目標結構

src/
├── operations/
│   ├── task/
│   ├── robot/
│   ├── fleet/
│   ├── device/
│   ├── maps/
│   ├── facility/
│   ├── charging/
│   ├── application/
│   ├── domain/
│   ├── ports/
│   └── operations.module.ts
├── access/
│   ├── authentication/
│   ├── authorization/
│   ├── users/
│   ├── sessions/
│   ├── audit/
│   └── access.module.ts
├── product-api/
│   ├── controllers/
│   ├── gateways/
│   ├── dto/
│   └── product-api.module.ts
├── adapters/
│   ├── rmf/
│   ├── robot-control/
│   ├── fleet-manager/
│   ├── mongo/
│   ├── redis/
│   ├── websocket/
│   ├── scheduler/
│   └── adapters.module.ts
├── hospital/
│   └── csh/
│       ├── controllers/
│       ├── workflows/
│       ├── policies/
│       ├── integrations/
│       ├── config/
│       └── csh-hospital.module.ts
├── app.module.ts
└── main.ts

3. 核心依賴方向

flowchart TB
    API["Product API"] --> APP["Application Services"]
    HOSPITAL["Hospital Workflows"] --> APP
    WORKER["Workers / Schedulers"] --> APP
    RMF_IN["RMF State Listener"] --> APP

    APP --> DOMAIN["Domain Rules"]
    APP --> PORTS["Ports"]

    RMF_OUT["RMF Adapter"] --> PORTS
    ROBOT["Robot Control Adapter"] --> PORTS
    FLEET["Fleet Manager Adapter"] --> PORTS
    MONGO["Mongo Adapter"] --> PORTS
    REDIS["Redis Adapter"] --> PORTS
    WS["WebSocket Adapter"] --> PORTS

允許:

Controller       → Application
Hospital Workflow → Application
Worker           → Application
Application      → Domain、Ports
Adapter          → Ports
AppModule        → Modules、Adapters

禁止:

Controller  → Repository / RMF / Redis
Application → Concrete Adapter / Mongo Document
Domain      → NestJS / MongoDB / Redis / HTTP / RMF
Port        → Concrete Adapter
Operations  → Hospital Implementation

4. Operations 範圍

Operations 負責跨醫院共用的機器人產品能力:

Task

  • 建立、取消、繼續任務
  • Task lifecycle
  • Task queue
  • Priority
  • Retry
  • Task history
  • Multi-station task
  • Return、charging task

Robot 與 Fleet

  • Robot、Fleet
  • Robot availability
  • Robot assignment
  • Battery gate
  • Busy、reservation
  • Task ownership
  • Runtime state normalization

Maps、Device、Facility

  • Map、Floor、Waypoint、Route
  • Charging Station
  • Device state
  • Door、Elevator 操作
  • Facility availability

Orchestration

  • Dispatch、cancel、reset pose
  • Auto charging
  • Emergency return lifecycle
  • Queue processing
  • Robot state ingestion
  • Operations events

Operations 不包含:

  • CSH、CMP、HIS
  • 特定醫院或 Station 名稱
  • SSO provider
  • 醫院審批及通知規則
  • RMF payload
  • HTTP DTO

5. Hospital 範圍

Hospital 負責:

  • HIS、Pharmacy、Employee integration
  • CSH、CMP
  • Azure AD、LDAP、Keycloak
  • Hospital Controller
  • Hospital Workflow
  • Approval、Emergency、Escalation Policy
  • Hospital Config
  • Station alias
  • Medication status mapping
  • HMI/HIS 通知規則

分類方式:

類型歸屬
跨醫院相同能力Operations
只有設定值不同Hospital Config
決策規則不同Hospital Policy
外部協定不同Hospital Adapter
單一醫院 APIHospital Controller

平台不得使用醫院名稱判斷。


6. Access 範圍

Access 負責:

  • Login、logout、refresh token
  • Users
  • Roles、permissions
  • Session
  • User activity
  • Account lock/unlock
  • Audit log

Hospital Authentication Adapter 將外部身分轉為:

Actor
├── id
└── roles

Operations 只使用 Actor 執行 audit 與記錄 trigger source;RBAC 判斷留在 Access Guard/API 層。


7. Product API 範圍

共用 API

  • Tasks
  • Robots
  • Fleet
  • Devices
  • Maps
  • Dashboard
  • System settings
  • WebSocket gateways

Hospital API

  • CMP messages
  • Employee sync
  • Medication
  • HIS robots
  • Hospital SSO callback

Controller 只負責:

驗證輸入
→ DTO mapping
→ 呼叫 Application Service
→ Response mapping

8. 最小 Ports

FleetCommandPort

負責:

  • Dispatch
  • Cancel
  • Reset pose

FleetRegistryPort

負責:

  • 從 Fleet Manager 取得 Robot 清單

RobotControlPort

負責:

  • 發送 Robot/HMI task event
  • 發送 ROS stop signal
  • 播放 Robot video

FacilityControlPort

負責:

  • Open
  • Close

Door 與 Elevator 暫時共用同一個 Port。

RobotStateStore

負責:

  • 讀取 Robot runtime state
  • 儲存正規化 runtime state
  • 列出 Fleet/Robot state

OperationsEventPublisher

負責發布:

  • Task completed/cancelled/failed
  • Robot unavailable
  • Emergency started/cleared
  • Fleet state changed

Repository Ports

依 use case 逐步建立:

  • TaskRepository
  • TaskQueueRepository
  • RobotRepository
  • FleetRepository
  • MapRepository
  • DeviceRepository

普通內部 Service 不建立 interface。


9. Operations 與 Hospital 溝通

Commands

CreateTask
CancelTask
ContinueTask
UpdateTaskProgress
EmergencyReturn
RequestCharging
ControlFacility
PlayRobotVideo

Queries

GetTask
SearchTaskHistory
GetRobotAvailability
GetRobotCurrentTask
GetAvailableChargingStations
GetFleetState
GetMap

Events

TaskCreated
TaskDispatched
TaskStarted
TaskCompleted
TaskCancelled
TaskFailed
EmergencyStarted
EmergencyCleared
RobotUnavailable

Command、Query、Event 不包含:

  • RMF payload
  • MongoDB ObjectId
  • Mongoose Document
  • Express Request
  • CSH/HIS 格式

目前透過 NestJS DI 直接呼叫,不使用 HTTP 或 Message Broker。


10. RMF 整合

Outbound

Application
FleetCommandPort
RmfFleetAdapter
Open-RMF

RMF Adapter 負責:

  • RMF payload
  • HTTP request
  • API key
  • Timeout
  • Response mapping
  • Error mapping

Inbound

RMF / Socket.IO
RmfStateAdapter
UpdateRobotStateService
RobotStateStore
Redis

RMF 原始資料不得進入 Domain。


11. Realtime 架構

業務事件

OperationsEventPublisher 發送:

  • Task status change
  • Emergency state
  • Robot availability change

Dashboard Snapshot

留在 Product API / WebSocket Adapter:

  • Fleet status
  • Task overview
  • Live task queue
  • IoT health
  • Active alerts

Snapshot 不需要包裝成 Domain Event。


12. 資料權威來源

資料權威來源
Task lifecycle、history、queueMongoDB
Task 與 Robot ownershipMongoDB
Robot、Fleet、Map、Device 設定MongoDB
Robot 實際物理狀態RMF
最新 Robot runtime stateRedis
Busy、HMI reservationRedis,具備 TTL
Dashboard 即時資料Redis / Application Query

派工判斷:

Mongo Task Ownership
+ Redis Runtime State
+ Redis Busy Lease
= Robot Availability

Redis 無法讀取時停止新派工。

MongoDB 可以保存最後一次 Robot snapshot,但不能單獨作為即時派工依據。


13. Worker 與派工可靠性

Queue 狀態:

PENDING
→ PROCESSING
→ DISPATCHING
→ IN_PROGRESS
→ COMPLETED / FAILED

Queue record 包含:

workerId
leaseExpiresAt
attemptCount
lastError
idempotencyKey

規則:

  • 使用 MongoDB findOneAndUpdate 原子領取。
  • 同一任務只能由一個 Worker 取得。
  • Worker 中斷後,lease 到期允許重試。
  • taskHistoryId 作為 dispatch idempotency key。
  • 呼叫 RMF 前先記錄 DISPATCHING
  • RMF inbound task state 負責狀態 reconciliation。
  • 提供 stuck DISPATCHING 恢復流程。
  • Robot Keeper 使用唯一條件避免重複建立充電任務。
  • 外部 RMF 呼叫不放進 Mongo transaction。

暫不導入 Kafka、RabbitMQ、Outbox 或額外 Worker framework。


14. Persistence

Mongo Adapter 負責:

  • Mongoose schema
  • Mongo query
  • Index
  • ObjectId 轉換
  • Document mapping
  • Atomic claim
  • Transaction

Redis Adapter 負責:

  • Robot runtime state
  • Busy lease
  • Reservation
  • Task tracking
  • Cache

規則:

  • Domain/Application 使用字串 ID。
  • ObjectId 只存在 Mongo Adapter。
  • Mongoose Document 不離開 Adapter。
  • Feature 不直接存取其他 Feature 的 Repository。
  • 初期共用 MongoDB 與 Redis。

15. Error Handling

External Error
→ Adapter Error Mapping
→ Application Error
→ API Exception Filter
→ HTTP Response

Domain/Application 使用與 transport 無關的錯誤碼。

Domain 不建立 NestJS HttpException


16. Configuration

Operations Config

  • Queue limit
  • Retry count
  • Worker lease
  • Battery threshold
  • Idle threshold
  • RMF timeout

Hospital Config

  • Station names
  • HIS/Pharmacy URL
  • SSO
  • Cron
  • Emergency destination
  • Medication mapping
  • HMI endpoint

環境變數由 Composition Root 或 Adapter 讀取並注入。Application/Domain 不直接存取 process.env


17. Observability

關鍵 log 欄位:

traceId
taskHistoryId
robotId
fleetId
operation
attempt
outcome
duration

最低監控項目:

  • Queue latency
  • Dispatch success/failure
  • Retry count
  • Expired lease
  • Stuck dispatch
  • RMF state age
  • Redis failure
  • Unavailable Robot count

沿用現有 logger 與 trace,不建立額外 observability package。


18. NestJS Modules

AppModule
├── OperationsModule
├── AccessModule
├── ProductApiModule
├── RmfAdapterModule
├── PersistenceModule
└── CshHospitalModule

AppModule 只負責:

  • Module composition
  • Port/Adapter binding
  • Global middleware
  • Global exception filter
  • Configuration

使用現有 ESLint no-restricted-imports 管理依賴,不增加 Nx 或 dependency-cruiser。


19. 導入順序

Phase 1:Fleet Port

  • 建立 FleetCommandPort
  • 將 OpenRmfApiService 改為 Adapter。
  • 遷移 dispatch、cancel、reset pose。
  • 建立 FakeFleetCommandPort 測試。

Phase 2:Controller 瘦身

  • Repository 呼叫移入 Application。
  • Auto-charge 移入 Operations。
  • Device open/close 經過 Application。

Phase 3:Robot 與 Fleet 外部邊界

  • 建立 RobotControlPort
  • 建立 FleetRegistryPort
  • 移除 Service 內直接 Axios/ROS 呼叫。

Phase 4:Worker 可靠性

  • Atomic claim
  • Processing lease
  • Idempotency key
  • Dispatch reconciliation
  • Robot Keeper 去重

Phase 5:Runtime State

  • 建立 RobotStateStore
  • 正規化 RMF inbound state。
  • 分離 Redis runtime state 與 Mongo durable state。

Phase 6:Persistence 隔離

  • 移除 Application/Domain 的 ObjectId
  • 移除 Mongo Document。
  • 建立 model mapping。
  • RMF payload 移入 Adapter。

Phase 7:Hospital 與 Access 邊界

  • 集中 CSH/CMP/HIS/Medication。
  • 建立 AccessModule。
  • Station 改用 Config。
  • 共用 lifecycle 留在 Operations。

Phase 8:Modules 與依賴規則

  • 建立主要 NestJS Modules。
  • 精簡 AppModule。
  • 加入 ESLint import restrictions。

20. 第二家醫院

先進行需求分類:

相同能力 → Operations
值不同   → Hospital Config
規則不同 → Hospital Policy
協定不同 → Hospital Adapter
單院 API → Hospital Controller

預設模式

同一 monorepo、多個 Hospital Apps:

hospital-platform/
├── packages/
│   └── operations/
├── apps/
│   ├── hospital-1-api/
│   └── hospital-2-api/
└── adapters/

獨立 Repository 條件

只有符合以下條件才拆:

  • 不同維護團隊
  • 程式碼存取隔離
  • Hospital application 明顯分歧
  • 不同技術棧
  • 獨立審查及發布流程

21. Package 與版本策略

第二家醫院證明共用面後建立:

@company/operations
@company/product-api
@company/contracts

@company/contracts 只在有獨立契約消費者時建立。

所有 packages 採同步版本:

@company/operations   1.0.0
@company/product-api  1.0.0
@company/contracts    1.0.0

各醫院可部署不同版本:

Hospital 1 → Platform 1.2.0
Hospital 2 → Platform 1.0.0

22. 最終驗收標準

  • Controller 只呼叫 Application Service。
  • Application 不依賴 Concrete Adapter。
  • Domain 不依賴 NestJS、MongoDB、Redis、HTTP、RMF。
  • ObjectId 和 Mongo Document 只存在 Adapter。
  • RMF outbound 只經過 FleetCommandPort
  • Robot HMI/ROS 只經過 RobotControlPort
  • RMF inbound state 已正規化。
  • Mongo、Redis、RMF 的權威範圍明確。
  • 多 Worker 不會重複派送。
  • Dispatch、cancel、events 具備 idempotency。
  • CSH/CMP 只存在於 Hospital 邊界。
  • Access 與 Operations 分離。
  • AppModule 只負責 Composition。
  • 不拆微服務或資料庫。
  • 不建立尚未被實際重用的 package、repository 或 Policy。