## Phase 19 Omni-Terminal (Wave 0-6 全部完成) ### 核心功能 - SSE 狀態機 (7-State 設計,10/10 分) - GenUI 動態渲染 (6 張卡片 + Zod Schema 驗證) - 核鑰 UX (長按授權 + 風險分級) - Terminal Telemetry (Sentry 整合) ### P0-P2 修復 - P0: Singleton → FastAPI Depends 依賴注入 - P1: Zod Schema 升級 (7 個驗證 Schema) - P1: 錯誤分類碼聚合 (Sentry fingerprint) - P2: Slow Query 監控 (5s 警告 / 10s 嚴重) ### 測試 - test_terminal_service.py: 54 項測試全通過 - 意圖分類: 42 個測試案例 (9 種 IntentType) ### 文檔 - ADR-031: SSE 架構實作紀錄 - ADR-032: GenUI 渲染實作紀錄 - Skills: v1.9 (後端 Terminal 章節) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
9.3 KiB
9.3 KiB
ADR-032: GenUI 動態渲染機制
狀態: Accepted 日期: 2026-03-27 決策者: 首席架構師 (Claude Code) 審核者: 統帥 (ogt)
背景
AWOOOI Phase 19 Omni-Terminal 需要「生成式介面 (GenUI)」能力,讓 AI 能夠根據分析結果動態渲染 UI 組件。例如:
- 顯示審批卡片 (ApprovalCard)
- 顯示指標摘要 (MetricsSummaryCard)
- 顯示執行進度 (ExecutionProgressCard)
- 顯示錯誤追蹤 (SentryErrorCard)
設計挑戰
- 安全性 - 不能執行任意 JavaScript
- 類型安全 - Props 必須有嚴格類型定義
- 效能 - 避免運行時動態載入大量程式碼
- 可維護性 - 新增卡片類型應該簡單
現有參考
ApprovalCard.tsx已存在,需要整合到 GenUI 系統- 設計系統遵循 Nothing.tech 風格 (ADR-002)
決策
1. 採用預編譯 Registry 模式
// genui/registry.ts
import { lazy } from 'react'
export const GENUI_REGISTRY = {
ApprovalCard: lazy(() => import('./cards/ApprovalCard')),
MetricsSummaryCard: lazy(() => import('./cards/MetricsSummaryCard')),
ExecutionProgressCard: lazy(() => import('./cards/ExecutionProgressCard')),
SentryErrorCard: lazy(() => import('./cards/SentryErrorCard')),
LogViewerCard: lazy(() => import('./cards/LogViewerCard')),
TimelineCard: lazy(() => import('./cards/TimelineCard')),
} as const
export type GenUICardType = keyof typeof GENUI_REGISTRY
2. SSE 事件格式
// terminal_render_ui 事件
interface RenderUIEvent {
type: 'terminal_render_ui'
data: {
component: GenUICardType // 必須是 Registry 中的 key
props: Record<string, unknown>
position?: 'inline' | 'modal' | 'panel'
id?: string // 用於更新/移除
}
}
3. 動態渲染器
// genui/GenUIRenderer.tsx
import { Suspense } from 'react'
import { GENUI_REGISTRY, GenUICardType } from './registry'
interface GenUIRendererProps {
component: GenUICardType
props: Record<string, unknown>
}
export function GenUIRenderer({ component, props }: GenUIRendererProps) {
const Component = GENUI_REGISTRY[component]
if (!Component) {
console.error(`[GenUI] Unknown component: ${component}`)
return <UnknownCardFallback name={component} />
}
return (
<Suspense fallback={<CardSkeleton />}>
<Component {...props} />
</Suspense>
)
}
4. Props 類型定義
// genui/types.ts
export interface ApprovalCardProps {
approvalId: string
riskLevel: 'LOW' | 'MEDIUM' | 'CRITICAL'
kubectl: string
title?: string
description?: string
}
export interface MetricsSummaryCardProps {
cpu: number
memory: number
pods: { running: number; total: number }
timestamp: string
}
export interface ExecutionProgressCardProps {
stepId: string
steps: Array<{
name: string
status: 'pending' | 'running' | 'completed' | 'failed'
output?: string
}>
}
// ... 其他卡片類型
export type GenUIPropsMap = {
ApprovalCard: ApprovalCardProps
MetricsSummaryCard: MetricsSummaryCardProps
ExecutionProgressCard: ExecutionProgressCardProps
// ...
}
5. 6 張核心 GenUI 卡片
| 卡片 | 用途 | 觸發場景 |
|---|---|---|
| ApprovalCard | 核鑰授權 | AI 提出高風險操作 |
| MetricsSummaryCard | 指標摘要 | 查詢系統狀態 |
| ExecutionProgressCard | 執行進度 | 執行 kubectl/腳本 |
| SentryErrorCard | 錯誤追蹤 | Sentry 事件分析 |
| LogViewerCard | 日誌查看 | 查詢 Pod 日誌 |
| TimelineCard | 事件時間軸 | 事件回顧/根因分析 |
6. 後端驗證
# services/terminal_service.py
ALLOWED_COMPONENTS = {
"ApprovalCard",
"MetricsSummaryCard",
"ExecutionProgressCard",
"SentryErrorCard",
"LogViewerCard",
"TimelineCard",
}
def validate_render_ui_event(component: str, props: dict) -> bool:
if component not in ALLOWED_COMPONENTS:
raise ValueError(f"Unknown GenUI component: {component}")
# Props 驗證使用 Pydantic
# ...
理由
為什麼選擇預編譯而非運行時?
| 方案 | 安全性 | 效能 | 類型安全 | 複雜度 |
|---|---|---|---|---|
| 預編譯 Registry | ✅ 高 | ✅ 高 | ✅ 完整 | 低 |
| 運行時 eval | ❌ 危險 | ❌ 低 | ❌ 無 | 高 |
| 伺服器渲染 | 中 | 中 | 中 | 高 |
| MDX/Markdown | 中 | 高 | ❌ 有限 | 中 |
預編譯優勢:
- 零 eval 風險 - 只能渲染已註冊的組件
- Code Splitting - lazy() 自動分割
- 類型安全 - GenUIPropsMap 強制 Props 類型
- 首屏快 - 不需下載額外 runtime
為什麼限制 6 張卡片?
- 避免膨脹 - 每張卡片增加 bundle size
- 聚焦核心 - 覆蓋 80% 使用場景
- 可擴展 - 未來按需新增
後果
優點
- 安全 - 不執行任意程式碼,只渲染預定義組件
- 類型安全 - TypeScript 完整覆蓋
- 效能 - React.lazy 自動 code splitting
- 可預測 - 組件行為固定,易於測試
- 可觀測 - 每張卡片有獨立的 Sentry error boundary
缺點
- 靈活性受限 - 新卡片需要開發部署
- 前後端同步 - 新增卡片需同時更新 Registry 和 ALLOWED_COMPONENTS
- Props 演進 - 舊版前端可能收到未知 props
風險
| 風險 | 緩解策略 |
|---|---|
| 未知組件名稱 | UnknownCardFallback 優雅降級 |
| Props 類型不符 | Pydantic 後端驗證 + Zod 前端驗證 |
| Lazy 載入失敗 | ErrorBoundary 包裹 + 重試機制 |
| Bundle 過大 | 限制卡片數量 + 定期審計 |
實作指引
檔案結構
apps/web/src/components/genui/
├── registry.ts # 組件註冊表
├── types.ts # Props 類型定義
├── GenUIRenderer.tsx # 動態渲染器
├── CardSkeleton.tsx # 載入骨架
├── UnknownCardFallback.tsx # 未知組件 fallback
└── cards/
├── ApprovalCard.tsx # 核鑰授權
├── MetricsSummaryCard.tsx # 指標摘要
├── ExecutionProgressCard.tsx # 執行進度
├── SentryErrorCard.tsx # 錯誤追蹤
├── LogViewerCard.tsx # 日誌查看
└── TimelineCard.tsx # 事件時間軸
新增卡片流程
- 定義 Props 類型 -
genui/types.ts - 建立卡片組件 -
genui/cards/NewCard.tsx - 註冊到 Registry -
genui/registry.ts - 後端允許清單 -
terminal_service.py - 測試 - Storybook + 整合測試
Props 驗證範例 (Zod)
// genui/validation.ts
import { z } from 'zod'
export const ApprovalCardPropsSchema = z.object({
approvalId: z.string(),
riskLevel: z.enum(['LOW', 'MEDIUM', 'CRITICAL']),
kubectl: z.string(),
title: z.string().optional(),
description: z.string().optional(),
})
export function validateGenUIProps<T extends GenUICardType>(
component: T,
props: unknown
): GenUIPropsMap[T] {
const schema = SCHEMA_MAP[component]
return schema.parse(props)
}
實作紀錄
更新日期: 2026-03-28 更新者: Claude Code (首席架構師) 首席架構師審查: Phase 19 審查 47/50 (GenUI 架構 9/10)
已完成項目
| 項目 | 檔案 | 狀態 |
|---|---|---|
| Registry (Lazy Loading) | apps/web/src/components/genui/registry.ts |
✅ |
| 動態渲染器 | apps/web/src/components/genui/GenUIRenderer.tsx |
✅ |
| ApprovalCard | apps/web/src/components/genui/ApprovalCard.tsx |
✅ |
| MetricsSummaryCard | apps/web/src/components/genui/MetricsSummaryCard.tsx |
✅ |
| SentryErrorCard | apps/web/src/components/genui/SentryErrorCard.tsx |
✅ |
| IncidentTimelineCard | apps/web/src/components/genui/IncidentTimelineCard.tsx |
✅ |
| K8sPodStatusCard | apps/web/src/components/genui/K8sPodStatusCard.tsx |
✅ |
| TraceWaterfallCard | apps/web/src/components/genui/TraceWaterfallCard.tsx |
✅ |
| NuclearKeyButton | apps/web/src/components/genui/NuclearKeyButton.tsx |
✅ |
| Telemetry 整合 | apps/web/src/lib/telemetry/terminal-telemetry.ts |
✅ |
P1 修復紀錄 (Zod Schema 升級)
| Schema | 驗證內容 |
|---|---|
ApprovalCardSchema |
riskLevel enum 驗證 |
MetricsSummaryCardSchema |
百分比/時間格式驗證 (regex) |
K8sPodStatusCardSchema |
巢狀物件結構驗證 |
NuclearKeyButtonSchema |
risk level enum 驗證 |
SentryErrorCardSchema |
errorId/title 必填 |
IncidentTimelineCardSchema |
events 陣列 + status enum |
TraceWaterfallCardSchema |
spans 陣列 + duration 數值 |
錯誤分類碼 (Sentry 聚合)
errorCode?:
| 'NOT_REGISTERED' // 組件未註冊
| 'DEF_NOT_FOUND' // 定義找不到
| 'ZOD_VALIDATION_FAILED' // Zod 驗證失敗
| 'LEGACY_TYPE_MISMATCH' // 舊版類型不符
| 'RENDER_ERROR' // 渲染錯誤
參考
- ADR-002 Nothing.tech Design System - 設計規範
- ADR-031 Omni-Terminal SSE Architecture - SSE 事件格式
- ApprovalCard.tsx - 現有實作
- React.lazy 文件
- Phase 19 工作規格書