# Phase 19 Omni-Terminal 架構腦力激盪會議記錄 v2.0

> **會議日期**: 2026-03-27 21:00-23:45 (台北時間)
> **會議類型**: 架構設計腦力激盪 (完整版 v2.0)
> **主持人**: 統帥 (ogt)
> **參與者**:
> - Gemini (外部 AI 顧問 - 提案方)
> - Claude Code (首席架構師 - 審查方)
> **記錄者**: Claude Code
> **版本**: v2.0 (215 項任務完整工作規格書)

---

## 一、會議背景

### 1.1 會議目的

針對 AWOOOI 前端「典範轉移」進行架構設計討論，將傳統監控儀表板轉型為 **AI-Native 代理人協作空間 (Agentic Workspace)**。

### 1.2 核心命題

> 「絕對不能變成傳統監控後台，必須是真正的 AI 智慧管理平台。」 — 統帥

### 1.3 討論範圍

- Omni-Terminal 全局終端機設計
- SSE 串流架構選型
- GenUI (生成式介面) 實作方案
- 與現有架構的整合策略

---

## 二、Gemini 提案摘要

### 2.1 三大 AI-Native 基因

| 基因 | 說明 | 技術實現 |
|------|------|----------|
| **GenUI** | 畫面跟著 AI 思考動態生成 | SSE 推送 `render_ui` 事件，前端動態掛載 React 組件 |
| **空間感知** | AI 知道統帥正在看什麼 | Ghost Payload (路由 + 焦點實體) |
| **核鑰 UX** | 高風險操作需儀式感授權 | 長按 3 秒 / 滑動解鎖 / 紅黑警戒線 |

### 2.2 五維實體視覺矩陣

| 實體 | 代號 | 視覺表現 |
|------|------|----------|
| 🦞 OpenClaw | 總樞紐 | 雙鉗夾擊動畫 `>--[ ]--<` |
| 🧠 AI | 底層大腦 | 點陣呼吸燈效果 |
| 🕵️ Agent | 幕僚 | `@Investigator` `@Strategist` 標籤 |
| 🤖 機器人 | 執行者 | `[██████░░░]` 方塊進度條 |
| 👑 人類 | 統帥 | `AWAITING COMMANDER OVERRIDE` |

### 2.3 SSE Event 協定 (Gemini 定義)

```python
# Gemini 提出的 4 種事件類型
event: thought      # AI 思考軌跡
event: tool_call    # 工具呼叫狀態
event: render_ui    # GenUI 掛載指令
event: action_request  # 核鑰請求
```

### 2.4 Gemini 程式碼提案

Gemini 提出 `terminal.py` 實作，包含：
- `SpatialContext` / `TerminalRequest` Pydantic 模型
- `format_sse()` SSE 格式化函數
- `openclaw_stream_engine()` AsyncGenerator
- `POST /intent` SSE 端點

---

## 三、首席架構師審查結果

### 3.1 整體評價

| 項目 | 評分 | 說明 |
|------|------|------|
| **概念設計** | ✅ 4.5/5 | 三大基因、五維實體設計優秀 |
| **程式碼實作** | ⚠️ 2.5/5 | 違反多項架構原則，需重構 |
| **leWOOOgo 合規** | 🔴 不合格 | Router 層包含業務邏輯 |
| **SSE 架構** | 🔴 不合格 | 未使用現有 EventPublisher |

### 3.2 發現的問題清單

| # | 問題 | 嚴重度 | 說明 |
|---|------|--------|------|
| 1 | 業務邏輯在 Router 層 | 🔴 P0 | `openclaw_stream_engine` 應在 Service 層 |
| 2 | 未使用現有 EventPublisher | 🔴 P0 | 重複造輪子，缺少 Heartbeat/Backpressure |
| 3 | 模型定義位置錯誤 | 🟡 P1 | 應放 `models/terminal.py` |
| 4 | 無 Protocol/Interface | 🟡 P1 | 違反 DI 原則 |
| 5 | 無 Session 管理 | 🟡 P1 | 無法追蹤、中斷、重連 |
| 6 | POST SSE 需額外套件 | 🟢 P2 | 建議改用混合模式 |

### 3.3 正確架構設計 (首席架構師建議)

```
apps/api/src/
├── models/
│   └── terminal.py           # Pydantic 模型
├── services/
│   ├── terminal_service.py   # ITerminalService + 實作
│   └── terminal_session.py   # Session 管理
├── api/v1/
│   └── terminal.py           # 純路由
└── core/
    └── sse.py                # 新增 EventType
```

**正確的 SSE 模式** (參照 agents.py):
```
POST /terminal/intent         → 返回 session_id
GET  /terminal/stream/{id}    → 原生 EventSource 訂閱
POST /terminal/abort/{id}     → 中斷執行
GET  /terminal/status/{id}    → 查詢狀態
```

---

## 四、Z-Index 層級審查 (v2.0 - 7 Tier 系統)

### 4.1 現有問題分析

| 問題 | 嚴重度 | 說明 |
|------|--------|------|
| Toast/Terminal 同層 | 🔴 P0 | 兩者都是 `z-50`，互相遮擋 |
| tailwind.config 定義無效 | 🟡 P1 | 組件使用 inline z-50，不讀 config |
| 無集中管理 | 🟡 P1 | 層級散落各組件，難以維護 |

### 4.2 現有層級分佈

| 層級 | 組件 | 問題 |
|------|------|------|
| `-z-10` | DotMatrixBg | ✅ OK |
| `z-10` | 主內容區 | ✅ OK |
| `z-30` | Header | ✅ OK |
| `z-40` | Sidebar | ✅ OK |
| `z-50` | OmniTerminal | 🔴 衝突 |
| `z-50` | Toast | 🔴 衝突 |
| `z-50` | Dialog | 🔴 衝突 |

### 4.3 新 7-Tier Z-Index 系統 (首席架構師設計)

```typescript
// lib/constants/z-index.ts
export const Z_INDEX = {
  // Tier 0: 背景層
  BACKGROUND: -10,

  // Tier 1: 基礎內容
  BASE: 0,
  CONTENT: 10,
  CARD_OVERLAY: 15,

  // Tier 2: 導航結構
  HEADER: 30,
  SIDEBAR: 40,

  // Tier 3: 浮動面板 (核心修復區)
  SLIDE_PANEL: 50,
  OMNI_TERMINAL: 52,  // Terminal 在 Panel 之上
  DROPDOWN: 54,

  // Tier 4: 通知層
  TOAST: 60,
  TOOLTIP: 62,

  // Tier 5: 模態層
  DIALOG: 70,
  SLIDE_PANEL_MODAL: 72,
  CONFIRM_DIALOG: 75,

  // Tier 6: 核鑰層 (最高優先)
  NUCLEAR_KEY_BACKDROP: 90,
  NUCLEAR_KEY_MODAL: 95,
  CRITICAL_ALERT: 99,

  // Tier 7: 開發工具
  DEVTOOLS: 100,
} as const

export type ZIndexKey = keyof typeof Z_INDEX
```

### 4.4 遷移策略

| 步驟 | 行動 | 影響檔案 |
|------|------|----------|
| 1 | 建立 `lib/constants/z-index.ts` | 新檔案 |
| 2 | 移除 `tailwind.config.ts` 的 zIndex 擴展 | 1 檔案 |
| 3 | 更新 OmniTerminal 使用 `Z_INDEX.OMNI_TERMINAL` | 1 檔案 |
| 4 | 更新 Toast 使用 `Z_INDEX.TOAST` | 1 檔案 |
| 5 | 更新 Dialog 使用 `Z_INDEX.DIALOG` | 1 檔案 |
| 6 | 全局搜索 `z-\d+` 並替換 | ~12 檔案 |

---

## 五、關鍵討論議題

### 5.1 SSE 模式選擇

| 方案 | 優點 | 缺點 |
|------|------|------|
| **A) POST SSE** (Gemini) | Ghost Payload 完整傳遞 | 需 `fetch-event-source` 套件 |
| **B) 混合模式** (首席架構師) | 原生 EventSource、與 agents.py 一致 | 需兩次請求 |

**結論**: 傾向採用 **混合模式 B**

### 5.2 Session 儲存方式

| 方案 | 優點 | 缺點 |
|------|------|------|
| **Redis** | Pod 重啟不丟失 | 複雜度高 |
| **記憶體** | 簡單 | Pod 重啟丟失 |

**結論**: 先用記憶體，後續再改 Redis

### 5.3 OpenClaw 串流化時機

| 方案 | 說明 |
|------|------|
| **立即改造** | 改 `openclaw.py` 為 AsyncGenerator |
| **先模擬** | 用 `asyncio.sleep` 模擬，驗證架構後再改 |

**結論**: 先模擬，Phase 19.3 再真實改造

### 5.4 多 SSE 連接共存

Dashboard 已有 SSE 連接，Terminal 新增後是否需要「焦點模式」？

**結論**: 允許多 SSE 共存，不需焦點模式

### 5.5 Ghost Payload 範圍

| 方案 | 傳送內容 |
|------|----------|
| **完整版** (Gemini) | 路由 + 焦點 + viewport_metrics |
| **最小化** (首席架構師) | 路由 + 焦點 |

**結論**: 採用最小化，符合隱私原則

### 5.6 斷線恢復策略

| 方案 | 說明 |
|------|------|
| **A) 自動重連** | 用戶無感 |
| **B) 手動重試** | 明確 |
| **C) Last-Event-ID** | 斷點續傳 |

**結論**: 採用 A + Last-Event-ID

---

## 六、已定案的架構決策 (7 項)

| # | 決策點 | 裁示 | 說明 |
|---|--------|------|------|
| Q1 | SSE 模式 | ✅ 混合模式 | POST intent → GET stream |
| Q2 | Session 儲存 | ✅ Redis | 5 分鐘 TTL，K3s 必備 |
| Q3 | 先模擬串流 | ✅ 同意 | asyncio.sleep 解耦開發 |
| Q4 | 多 SSE 共存 | ✅ 允許 | 不暫停其他 SSE |
| Q5 | 快捷鍵 | 🛠️ CMD+J | 避免 CMD+K 與瀏覽器衝突 |
| Q6 | Ghost Payload | ✅ 最小化 | 只傳 current_page + entity_id |
| Q7 | 重連策略 | ✅ 自動 | 指數退避 + Last-Event-ID |

### 6.1 SSE 狀態機設計

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

// 合法轉換
const VALID_TRANSITIONS: Record<SSEConnectionState, SSEConnectionState[]> = {
  disconnected: ['connecting'],
  connecting: ['subscribing', 'error', 'disconnected'],
  subscribing: ['connected', 'error'],
  connected: ['streaming', 'reconnecting', 'disconnected'],
  streaming: ['connected', 'error'],
  reconnecting: ['connecting', 'error', 'disconnected'],
  error: ['reconnecting', 'disconnected'],
}
```

### 6.2 快捷鍵系統設計

```typescript
// lib/constants/shortcuts.ts
export const SHORTCUTS = {
  TOGGLE_TERMINAL: {
    key: 'j',
    modifiers: ['meta'],      // CMD (Mac) / Ctrl (Win)
    description: 'Toggle Omni-Terminal',
    action: 'terminal.toggle',
  },
  FOCUS_INPUT: {
    key: 'i',
    modifiers: ['meta', 'shift'],
    description: 'Focus Terminal Input',
    action: 'terminal.focus',
  },
  ABORT_STREAM: {
    key: 'Escape',
    modifiers: [],
    description: 'Abort Current Stream',
    action: 'terminal.abort',
    contextRequired: 'terminal',
  },
} as const
```

---

## 七、待統帥裁示問題

| # | 問題 | 選項 | 首席架構師建議 |
|---|------|------|----------------|
| Q1 | SSE 模式 | 混合 vs POST | 混合 |
| Q2 | Session 儲存 | Redis vs 記憶體 | 先記憶體 |
| Q3 | 先模擬串流？ | 是 vs 直接真實 | 是 |
| Q4 | 多 SSE 共存？ | 允許 vs 焦點模式 | 允許 |
| Q5 | 快捷鍵 | CMD+K vs CMD+/ | CMD+K (現有) |
| Q6 | Ghost Payload | 完整 vs 最小化 | 最小化 |
| Q7 | 斷線恢復 | 自動+Last-Event-ID | 是 |

---

## 八、Phase 19 工作清單總覽 (v2.0 完整版 - 215 項)

### 8.1 工作量統計

| Phase | 區塊名稱 | 任務數 | 預估時間 | 優先級 |
|-------|----------|--------|----------|--------|
| **19.Z** | Z-Index 重構 | 12 | 2.5h | 🔴 P0 |
| **19.K** | 快捷鍵重構 | 11 | 2.5h | 🔴 P0 |
| **19.A** | 動畫系統 | 10 | 2.5h | 🟠 P1 |
| **19.S** | SSE 狀態機 | 10 | 2.5h | 🔴 P0 |
| **19.1** | 後端 SSE 基礎設施 | 14 | 5h | 🔴 P0 |
| **19.2** | 前端 SSE + UI | 42 | 8h | 🔴 P0 |
| **19.3** | OpenClaw 串流化 | 8 | 5h | 🟠 P1 |
| **19.4** | GenUI 卡片系統 | 57 | 13h | 🟠 P1 |
| **19.5** | 核鑰 UX 強化 | 12 | 3h | 🟠 P1 |
| **19.R** | 響應式設計 | 8 | 2h | 🟡 P2 |
| **19.I** | i18n 整合 | 7 | 1.5h | 🟡 P2 |
| **19.Y** | 無障礙規範 | 9 | 2h | 🟡 P2 |
| **19.O** | 可觀測性整合 | 20 | 6h | 🟠 P1 |
| **19.6** | 測試與文檔 | 8 | 3h | 🟡 P2 |
| **總計** | - | **215** | **~58h** | - |

### 8.2 執行波次 (Wave)

**Wave 0 - 基礎建設 (7.5h)**
- 19.Z: Z-Index 重構 (12 項)
- 19.K: 快捷鍵重構 (11 項)
- 19.A: 動畫系統 (10 項)

**Wave 1 - 核心通道 (15.5h)**
- 19.S: SSE 狀態機 (10 項)
- 19.1: 後端 SSE (14 項)
- 19.2: 前端 SSE (42 項)

**Wave 2 - AI 整合 (8.1h)**
- 19.3: OpenClaw 串流 (8 項)
- 19.4a: GenUI 基礎 + ApprovalCard (15 項)

**Wave 3 - GenUI 擴充 (11h)**
- 19.4b-f: 5 張 GenUI 卡片 (42 項)

**Wave 4 - 強化 (8.5h)**
- 19.5: 核鑰 UX (12 項)
- 19.R: 響應式 (8 項)
- 19.I: i18n (7 項)
- 19.Y: 無障礙 (9 項)

**Wave 5 - 收尾 (9h)**
- 19.O: 可觀測性 (20 項)
- 19.6: 測試文檔 (8 項)

### 8.3 關鍵里程碑

| 里程碑 | 完成條件 | Wave |
|--------|----------|------|
| M1: 物理降臨 | OmniTerminal 出現在所有頁面 | Wave 0 |
| M2: SSE 連通 | 前後端 SSE 串流成功 | Wave 1 |
| M3: AI 對話 | 可與 OpenClaw 對話 | Wave 2 |
| M4: GenUI 完整 | 6 張卡片全部可用 | Wave 3 |
| M5: 生產就緒 | 測試通過 + 可觀測性完整 | Wave 5 |

---

## 九、會議結論

### 9.1 共識

1. **Gemini 概念設計優秀**，三大基因 + 五維實體視覺矩陣值得採納
2. **程式碼需重構**，必須符合 leWOOOgo 積木化原則
3. **採用混合 SSE 模式**，與現有 agents.py 架構一致
4. **先模擬後真實**，Phase 19.1-19.2 模擬驗證，19.3 真實串流

### 9.2 下一步行動

1. 統帥裁示 7 個待決問題
2. 按 Wave 1 順序開始實作
3. 每個 Wave 完成後進行架構審查

### 9.3 風險提醒

| 風險 | 緩解策略 |
|------|----------|
| SSE 穿透 Nginx | 確認 `X-Accel-Buffering: no` |
| LLM 輸出不可控 | Pydantic 強制驗證 + 回退 |
| 前端組件膨脹 | Registry 限 10 個核心卡片 |
| z-index 衝突 | 建立 Z_INDEX 常量系統 |

---

## 十、相關文件

| 文件 | 說明 |
|------|------|
| `project_phase19_omni_terminal.md` | 完整工作清單 |
| `project_master_workplan.md` | 整合到主工作計畫 |
| `LOGBOOK.md` | 進度追蹤 |
| `project_current_status.md` | 當前狀態快照 |

---

## 附錄 A: Gemini 原始程式碼提案

```python
import json
import asyncio
from typing import Optional, List, AsyncGenerator
from fastapi import APIRouter, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field

router = APIRouter()

class SpatialContext(BaseModel):
    """前端隱形夾帶的空間感知數據 (Ghost Payload)"""
    current_page: str = Field(..., description="統帥當前所在的路由")
    focused_incident_id: Optional[str] = Field(None, description="正在檢視的事件 ID")
    viewport_metrics: List[str] = Field(default_factory=list, description="畫面上正在顯示的指標")

class TerminalRequest(BaseModel):
    """Omni-Terminal 輸入的絕對合約"""
    intent: str = Field(..., description="統帥輸入的原始指令或意圖")
    context: SpatialContext = Field(..., description="前端空間感知上下文")

def format_sse(event: str, data: dict) -> str:
    return f"event: {event}\ndata: {json.dumps(data)}\n\n"

async def openclaw_stream_engine(request: TerminalRequest) -> AsyncGenerator[str, None]:
    yield format_sse("thought", {"agent": "System", "msg": f"空間感知啟動: {request.context.current_page}"})
    await asyncio.sleep(0.5)
    yield format_sse("thought", {"agent": "Investigator", "msg": f"解析意圖: '{request.intent}'"})
    await asyncio.sleep(1.0)
    yield format_sse("tool_call", {"tool": "K8s-Log-Scanner", "status": "executing"})
    await asyncio.sleep(1.5)
    yield format_sse("render_ui", {"component": "MetricsCard", "props": {"cpu": 98, "memory": 88}})
    await asyncio.sleep(1.0)
    yield format_sse("action_request", {"proposalId": "PRP-001", "riskLevel": "CRITICAL"})

@router.post("/intent")
async def handle_terminal_intent(request: TerminalRequest):
    return StreamingResponse(
        openclaw_stream_engine(request),
        media_type="text/event-stream",
        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}
    )
```

---

## 附錄 B: 首席架構師建議的正確架構

```python
# api/v1/terminal.py - Router 層 (純路由)
from src.services.terminal_service import get_terminal_service
from src.core.sse import get_publisher

@router.post("/intent")
async def handle_intent(request: TerminalRequest) -> IntentResponse:
    service = get_terminal_service()
    session_id = await service.process_intent(request)
    return IntentResponse(session_id=session_id)

@router.get("/stream/{session_id}")
async def stream_response(session_id: str) -> StreamingResponse:
    service = get_terminal_service()

    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")
```

```python
# services/terminal_service.py - Service 層 (業務邏輯)
@runtime_checkable
class ITerminalService(Protocol):
    async def process_intent(self, request: TerminalRequest) -> str: ...
    async def get_session(self, session_id: str) -> dict | None: ...

class TerminalService:
    def __init__(self, openclaw: OpenClawService):
        self._openclaw = openclaw

    async def process_intent(self, request: TerminalRequest) -> str:
        session_id = str(uuid4())
        asyncio.create_task(self._run_analysis(session_id, request))
        return session_id

    async def _run_analysis(self, session_id: str, request: TerminalRequest):
        publisher = await get_publisher()

        await publisher.publish(SSEEvent(
            type=EventType.TERMINAL_THOUGHT,
            data={"agent": "System", "msg": f"空間感知: {request.context.current_page}"}
        ), topic=f"terminal:{session_id}")

        # 調用 OpenClaw...
```

---

**會議結束時間**: 2026-03-27 22:30 (台北時間)

**下次會議**: 待統帥裁示後，開始 Wave 1 實作
