# ADR-031: Omni-Terminal SSE 架構

> **狀態**: Accepted
> **日期**: 2026-03-27
> **決策者**: 首席架構師 (Claude Code)
> **審核者**: 統帥 (ogt)

## 背景

AWOOOI Phase 19 計畫將平台從「傳統監控儀表板」轉型為「AI 代理人協作空間 (Agentic Workspace)」。核心組件 Omni-Terminal 需要即時雙向通訊能力，支援：

1. **AI 思考軌跡串流** - 展示 OpenClaw 分析過程
2. **動態 UI 生成 (GenUI)** - 根據 AI 回應動態掛載組件
3. **核鑰授權請求** - 高風險操作的確認流程
4. **斷線恢復** - 確保通訊穩定性

### 現有架構約束

- `apps/api/src/core/sse.py` 已有 `EventPublisher` 實作
- `apps/api/src/api/v1/agents.py` 使用混合 SSE 模式
- 前端 `terminal.store.ts` 目前使用 Mock 實作
- 必須符合 leWOOOgo 積木化原則

## 決策

### 1. 採用混合 SSE 模式 (POST Intent + GET Stream)

```
┌─────────┐     POST /terminal/intent     ┌─────────┐
│ Browser │ ────────────────────────────► │   API   │
│ (Web)   │ ◄──── { session_id: "xxx" } ─ │ Server  │
└─────────┘                               └─────────┘
     │
     │       GET /terminal/stream/{session_id}
     ▼
┌─────────┐     SSE (EventSource)         ┌─────────┐
│ Browser │ ◄─────────────────────────── │   API   │
│ (Web)   │                              │ Server  │
└─────────┘                               └─────────┘
```

### 2. API 端點設計

| 端點 | 方法 | 說明 |
|------|------|------|
| `/api/v1/terminal/intent` | POST | 提交意圖，返回 session_id |
| `/api/v1/terminal/stream/{session_id}` | GET | SSE 訂閱 (EventSource 原生) |
| `/api/v1/terminal/abort/{session_id}` | POST | 中斷執行 |
| `/api/v1/terminal/status/{session_id}` | GET | 查詢 Session 狀態 |

### 3. SSE 事件類型

```python
class TerminalEventType(str, Enum):
    # 思考軌跡
    TERMINAL_THOUGHT = "terminal_thought"
    TERMINAL_TOOL_CALL = "terminal_tool_call"

    # GenUI
    TERMINAL_RENDER_UI = "terminal_render_ui"

    # 授權
    TERMINAL_ACTION_REQUEST = "terminal_action_request"
    TERMINAL_ACTION_RESULT = "terminal_action_result"

    # 控制
    TERMINAL_COMPLETE = "terminal_complete"
    TERMINAL_ERROR = "terminal_error"
    TERMINAL_HEARTBEAT = "terminal_heartbeat"
```

### 4. 前端 SSE 狀態機 (7 狀態)

```typescript
type SSEConnectionState =
  | 'disconnected'   // 未連接
  | 'connecting'     // 正在連接
  | 'subscribing'    // 訂閱中
  | 'connected'      // 已連接 (空閒)
  | 'streaming'      // 串流中 (AI 回應)
  | 'reconnecting'   // 重連中
  | 'error'          // 錯誤

// 合法狀態轉換
const VALID_TRANSITIONS = {
  disconnected: ['connecting'],
  connecting: ['subscribing', 'error', 'disconnected'],
  subscribing: ['connected', 'error'],
  connected: ['streaming', 'reconnecting', 'disconnected'],
  streaming: ['connected', 'error'],
  reconnecting: ['connecting', 'error', 'disconnected'],
  error: ['reconnecting', 'disconnected'],
}
```

### 5. Session 儲存策略

- **儲存位置**: Redis (與 Multi-Sig 共用實例)
- **TTL**: 5 分鐘 (無活動自動過期)
- **Key 格式**: `terminal:session:{session_id}`
- **資料結構**:

```python
class TerminalSession(BaseModel):
    session_id: str
    user_id: str
    intent: str
    context: SpatialContext
    status: Literal["pending", "processing", "completed", "aborted", "error"]
    created_at: datetime
    last_event_id: int = 0
```

### 6. 重連策略

- **指數退避**: 1s → 2s → 4s → 8s → 16s (最大)
- **Last-Event-ID**: 支援斷點續傳
- **最大重試**: 5 次
- **重連後**: 自動恢復訂閱

### 7. 分層架構

```
apps/api/src/
├── models/
│   └── terminal.py              # Pydantic 模型
├── services/
│   ├── terminal_service.py      # ITerminalService + 實作
│   └── terminal_session.py      # Session 管理 (Redis)
├── api/v1/
│   └── terminal.py              # 純路由 (無業務邏輯)
└── core/
    └── sse.py                   # 新增 TerminalEventType
```

## 理由

### 為什麼選擇混合模式而非純 POST SSE？

| 考量 | POST SSE | 混合模式 |
|------|----------|----------|
| 瀏覽器支援 | 需 `fetch-event-source` | 原生 EventSource |
| 與現有架構一致性 | 不一致 | ✅ 與 agents.py 一致 |
| 中斷/恢復能力 | 複雜 | ✅ 原生支援 Last-Event-ID |
| 偵錯友善度 | 低 | ✅ 可直接 curl |

### 為什麼選擇 Redis 而非記憶體？

| 考量 | 記憶體 | Redis |
|------|--------|-------|
| Pod 重啟 | 丟失 | ✅ 保留 |
| 水平擴展 | 黏性 Session | ✅ 無狀態 |
| 複雜度 | 低 | 中 |
| **K3s 環境** | 不適用 | ✅ 必要 |

### 為什麼需要 7 狀態機？

現有 `terminal.store.ts` 使用布林標記 (`isConnecting`, `isConnected`)，無法表達：
- 訂閱中 vs 已連接
- 串流中 vs 空閒
- 重連中的不同階段

狀態機提供：
- 明確的狀態轉換規則
- 防止非法狀態 (如「未連接但串流中」)
- 便於偵錯和可觀測性

## 後果

### 優點

1. **與現有架構一致** - 複用 EventPublisher、與 agents.py 模式相同
2. **原生瀏覽器支援** - 不需額外套件
3. **斷點續傳** - Last-Event-ID 支援
4. **K3s 友善** - Redis Session 支援無狀態部署
5. **可觀測** - 狀態機提供清晰的狀態追蹤

### 缺點

1. **兩次請求** - POST + GET 比單一 POST SSE 多一次 roundtrip
2. **Redis 依賴** - 增加基礎設施依賴
3. **Session 管理** - 需處理 TTL、清理等

### 風險

| 風險 | 緩解策略 |
|------|----------|
| SSE 被 Nginx 緩衝 | 確認 `X-Accel-Buffering: no` Header |
| Redis 連線失敗 | Fallback 到記憶體模式 + 告警 |
| Session 過期競爭 | 串流中延長 TTL |
| 前端狀態不一致 | 狀態機強制轉換驗證 |

## 實作指引

### 後端 (Phase 19.1)

```python
# api/v1/terminal.py - Router 層
@router.post("/intent")
async def handle_intent(
    request: TerminalRequest,
    service: ITerminalService = Depends(get_terminal_service)
) -> IntentResponse:
    session_id = await service.process_intent(request)
    return IntentResponse(session_id=session_id, stream_url=f"/terminal/stream/{session_id}")

@router.get("/stream/{session_id}")
async def stream_response(
    session_id: str,
    service: ITerminalService = Depends(get_terminal_service)
) -> StreamingResponse:
    async def generate():
        publisher = await get_publisher()
        client = await publisher.subscribe(topics=[f"terminal:{session_id}"])
        async for event_str in publisher.stream(client):
            yield event_str

    return StreamingResponse(
        generate(),
        media_type="text/event-stream",
        headers={"X-Accel-Buffering": "no", "Cache-Control": "no-cache"}
    )
```

### 前端 (Phase 19.2)

```typescript
// stores/terminal.store.ts
const useTerminalStore = create<TerminalState>()((set, get) => ({
  connectionState: 'disconnected' as SSEConnectionState,

  async sendIntent(intent: string, context: SpatialContext) {
    const { transitionTo } = get()

    transitionTo('connecting')

    // Step 1: POST intent
    const { session_id, stream_url } = await fetch('/api/v1/terminal/intent', {
      method: 'POST',
      body: JSON.stringify({ intent, context }),
    }).then(r => r.json())

    transitionTo('subscribing')

    // Step 2: GET stream
    const es = new EventSource(stream_url)
    es.onopen = () => transitionTo('connected')
    es.onmessage = (e) => {
      transitionTo('streaming')
      // Handle event...
    }
    es.onerror = () => transitionTo('error')
  },
}))
```

## 實作紀錄

> **更新日期**: 2026-03-31 (Phase 19.6 完成)
> **更新者**: Claude Code (首席架構師)
> **首席架構師審查**: Phase 19 審查 47/50 (SSE 狀態機 10/10 ⭐)

### 已完成項目

| 項目 | 檔案 | 狀態 |
|------|------|------|
| 後端 Router | `apps/api/src/api/v1/terminal.py` | ✅ |
| 後端 Service | `apps/api/src/services/terminal_service.py` | ✅ |
| 後端 Models | `apps/api/src/models/terminal.py` | ✅ |
| 前端 Store | `apps/web/src/stores/terminal.store.ts` | ✅ |
| 前端 UI | `apps/web/src/components/terminal/OmniTerminal.tsx` | ✅ |
| Telemetry | `apps/web/src/lib/telemetry/terminal-telemetry.ts` | ✅ |
| 單元測試 (Service) | `apps/api/tests/test_terminal_service.py` | ✅ (54 項通過) |
| 單元測試 (Router) | `apps/api/tests/test_terminal.py` | ✅ (Phase 19.6) |
| 單元測試 (GenUI) | `apps/web/src/components/genui/__tests__/registry.test.ts` | ✅ (Phase 19.6, Vitest) |
| E2E 測試 | `apps/web/tests/e2e/terminal.spec.ts` | ✅ (Phase 19.6, Playwright) |

### P0-P2 修復紀錄

| 優先級 | 修復項目 | 說明 |
|--------|----------|------|
| P0 | Singleton → FastAPI Depends | `get_terminal_service()` 依賴注入 |
| P2 | Slow Query 監控 | 5s 警告 / 10s 嚴重 + Sentry 告警 |

### Phase 19.6 測試補全 (2026-03-31)

| 測試類型 | 檔案 | 覆蓋範圍 |
|----------|------|----------|
| Router 層 | `test_terminal.py` | 4 端點 + 422/404 錯誤案例 + Session 流程 |
| GenUI Zod | `registry.test.ts` | 7 組件 Schema + validateProps 錯誤碼分類 |
| E2E API | `terminal.spec.ts` | POST/GET/Abort 完整流程 + 404/422 驗證 |

### 驗證結果

```bash
# Python 測試
cd apps/api && python -m pytest tests/test_terminal_service.py tests/test_terminal.py -v
# Service: 54 passed; Router: ~18 passed

# GenUI Zod 單元測試 (需先 pnpm install 安裝 vitest)
cd apps/web && pnpm vitest run

# E2E 測試
cd apps/web && pnpm playwright test tests/e2e/terminal.spec.ts

# 意圖分類覆蓋
# - 42 個分類測試案例
# - 9 種 IntentType 全覆蓋
```

---

## 參考

- [ADR-004 State Management](./ADR-004-state-management.md) - Zustand 狀態管理
- [ADR-006 AI Fallback Strategy](./ADR-006-ai-fallback-strategy.md) - AI 備援順序
- [agents.py](../../apps/api/src/api/v1/agents.py) - 現有 SSE 實作參考
- [core/sse.py](../../apps/api/src/core/sse.py) - EventPublisher 實作
- [Phase 19 工作規格書](../../.claude/projects/-Users-ogt-awoooi/memory/project_phase19_omni_terminal.md)
- [會議紀錄](../meetings/2026-03-27-phase19-omni-terminal-brainstorm.md)
