## 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>
382 lines
10 KiB
TypeScript
382 lines
10 KiB
TypeScript
/**
|
|
* GenUI Component Registry
|
|
* =========================
|
|
* Phase 19.4a - 動態 UI 組件註冊表
|
|
*
|
|
* ADR-032: Pre-compiled Registry Pattern
|
|
* - 所有 GenUI 組件必須在此註冊
|
|
* - 支援 Lazy Loading (React.lazy)
|
|
* - Props 類型驗證 (Zod Schema)
|
|
*
|
|
* Phase 19 首席架構師審查 P1 改進:
|
|
* - 升級 Props Schema 為 Zod 驗證
|
|
* - 支援格式驗證 (如百分比、時間單位)
|
|
* - 錯誤分類便於聚合分析
|
|
*
|
|
* @see ADR-032 GenUI Dynamic Rendering
|
|
* @author Claude Code (首席架構師)
|
|
* @version 1.1.0 - Zod Schema 升級
|
|
* @date 2026-03-28 (台北時間)
|
|
*/
|
|
|
|
import { lazy, type ComponentType } from 'react'
|
|
import { z, type ZodSchema } from 'zod'
|
|
|
|
// =============================================================================
|
|
// Zod Schemas (Phase 19 首席架構師審查 P1)
|
|
// =============================================================================
|
|
|
|
/** ApprovalCard Props Schema */
|
|
export const ApprovalCardSchema = z.object({
|
|
approvalId: z.string().min(1),
|
|
riskLevel: z.enum(['low', 'medium', 'high', 'critical']),
|
|
kubectl: z.string().optional(),
|
|
})
|
|
|
|
/** MetricsSummaryCard Props Schema */
|
|
export const MetricsSummaryCardSchema = z.object({
|
|
rps: z.number().min(0),
|
|
errorRate: z.string().regex(/^\d+(\.\d+)?%$/, 'Must be percentage format (e.g., "0.05%")'),
|
|
p99Latency: z.string().regex(/^\d+(\.\d+)?(ms|s)$/, 'Must be time format (e.g., "450ms")'),
|
|
status: z.enum(['healthy', 'warning', 'critical']),
|
|
})
|
|
|
|
/** SentryErrorCard Props Schema */
|
|
export const SentryErrorCardSchema = z.object({
|
|
errorId: z.string().min(1),
|
|
title: z.string().min(1),
|
|
count: z.number().int().min(0),
|
|
lastSeen: z.string(), // ISO date string
|
|
})
|
|
|
|
/** IncidentTimelineCard Props Schema */
|
|
export const IncidentTimelineCardSchema = z.object({
|
|
incidentId: z.string().min(1),
|
|
events: z.array(z.object({
|
|
timestamp: z.string(),
|
|
message: z.string(),
|
|
type: z.string().optional(),
|
|
})),
|
|
status: z.enum(['active', 'resolved', 'acknowledged']),
|
|
})
|
|
|
|
/** K8sPodStatusCard Props Schema */
|
|
export const K8sPodStatusCardSchema = z.object({
|
|
namespace: z.string().min(1),
|
|
pods: z.array(z.object({
|
|
name: z.string(),
|
|
status: z.string(),
|
|
ready: z.boolean().optional(),
|
|
})),
|
|
summary: z.object({
|
|
total: z.number().int().min(0),
|
|
running: z.number().int().min(0),
|
|
failed: z.number().int().min(0).optional(),
|
|
}),
|
|
})
|
|
|
|
/** TraceWaterfallCard Props Schema */
|
|
export const TraceWaterfallCardSchema = z.object({
|
|
traceId: z.string().min(1),
|
|
spans: z.array(z.object({
|
|
spanId: z.string(),
|
|
name: z.string(),
|
|
duration: z.number().min(0),
|
|
startTime: z.number().optional(),
|
|
})),
|
|
duration: z.number().min(0),
|
|
})
|
|
|
|
/** NuclearKeyButton Props Schema */
|
|
export const NuclearKeyButtonSchema = z.object({
|
|
label: z.string().min(1),
|
|
riskLevel: z.enum(['low', 'medium', 'high', 'critical']),
|
|
approvalId: z.string().optional(),
|
|
})
|
|
|
|
// =============================================================================
|
|
// Registry Types
|
|
// =============================================================================
|
|
|
|
/** 組件 Props 基礎類型 */
|
|
export interface BaseGenUIProps {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
data: any
|
|
}
|
|
|
|
/** GenUI 組件定義 */
|
|
export interface GenUIComponentDef {
|
|
/** 組件名稱 (唯一識別符) */
|
|
name: string
|
|
/** 組件描述 */
|
|
description: string
|
|
/** React 組件 (支援 lazy loading) */
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
component: ComponentType<any>
|
|
/** Props Schema (Zod 驗證) */
|
|
zodSchema?: ZodSchema
|
|
/** Legacy Props Schema (向後相容) */
|
|
propsSchema?: Record<string, 'string' | 'number' | 'boolean' | 'object' | 'array'>
|
|
/** 是否允許在 Terminal 中渲染 */
|
|
allowInTerminal: boolean
|
|
/** 風險等級 (決定是否需要確認) */
|
|
riskLevel?: 'low' | 'medium' | 'high'
|
|
}
|
|
|
|
// =============================================================================
|
|
// Lazy Component Imports
|
|
// =============================================================================
|
|
|
|
// 核心 GenUI 組件 (lazy loaded)
|
|
const ApprovalCard = lazy(() =>
|
|
import('./ApprovalCard').then(m => ({ default: m.ApprovalCard }))
|
|
)
|
|
|
|
const MetricsSummaryCard = lazy(() =>
|
|
import('./MetricsSummaryCard').then(m => ({ default: m.MetricsSummaryCard }))
|
|
)
|
|
|
|
const SentryErrorCard = lazy(() =>
|
|
import('./SentryErrorCard').then(m => ({ default: m.SentryErrorCard }))
|
|
)
|
|
|
|
const IncidentTimelineCard = lazy(() =>
|
|
import('./IncidentTimelineCard').then(m => ({ default: m.IncidentTimelineCard }))
|
|
)
|
|
|
|
const K8sPodStatusCard = lazy(() =>
|
|
import('./K8sPodStatusCard').then(m => ({ default: m.K8sPodStatusCard }))
|
|
)
|
|
|
|
const TraceWaterfallCard = lazy(() =>
|
|
import('./TraceWaterfallCard').then(m => ({ default: m.TraceWaterfallCard }))
|
|
)
|
|
|
|
const NuclearKeyButton = lazy(() =>
|
|
import('./NuclearKeyButton').then(m => ({ default: m.NuclearKeyButton }))
|
|
)
|
|
|
|
// =============================================================================
|
|
// Component Registry
|
|
// =============================================================================
|
|
|
|
/**
|
|
* GenUI 組件註冊表
|
|
*
|
|
* ADR-032: 所有可動態渲染的組件必須在此註冊
|
|
*/
|
|
export const GENUI_REGISTRY: Record<string, GenUIComponentDef> = {
|
|
// -------------------------------------------------------------------------
|
|
// 核心卡片 (6 張)
|
|
// -------------------------------------------------------------------------
|
|
|
|
ApprovalCard: {
|
|
name: 'ApprovalCard',
|
|
description: '核鑰授權卡 - 顯示待簽核操作',
|
|
component: ApprovalCard,
|
|
zodSchema: ApprovalCardSchema,
|
|
propsSchema: {
|
|
approvalId: 'string',
|
|
riskLevel: 'string',
|
|
kubectl: 'string',
|
|
},
|
|
allowInTerminal: true,
|
|
riskLevel: 'high',
|
|
},
|
|
|
|
MetricsSummaryCard: {
|
|
name: 'MetricsSummaryCard',
|
|
description: '指標摘要卡 - 顯示 SignOz 即時指標',
|
|
component: MetricsSummaryCard,
|
|
zodSchema: MetricsSummaryCardSchema,
|
|
propsSchema: {
|
|
rps: 'number',
|
|
errorRate: 'string',
|
|
p99Latency: 'string',
|
|
status: 'string',
|
|
},
|
|
allowInTerminal: true,
|
|
riskLevel: 'low',
|
|
},
|
|
|
|
SentryErrorCard: {
|
|
name: 'SentryErrorCard',
|
|
description: '錯誤追蹤卡 - 顯示 Sentry 錯誤詳情',
|
|
component: SentryErrorCard,
|
|
zodSchema: SentryErrorCardSchema,
|
|
propsSchema: {
|
|
errorId: 'string',
|
|
title: 'string',
|
|
count: 'number',
|
|
lastSeen: 'string',
|
|
},
|
|
allowInTerminal: true,
|
|
riskLevel: 'low',
|
|
},
|
|
|
|
IncidentTimelineCard: {
|
|
name: 'IncidentTimelineCard',
|
|
description: '事件時間軸卡 - 顯示 Incident 歷程',
|
|
component: IncidentTimelineCard,
|
|
zodSchema: IncidentTimelineCardSchema,
|
|
propsSchema: {
|
|
incidentId: 'string',
|
|
events: 'array',
|
|
status: 'string',
|
|
},
|
|
allowInTerminal: true,
|
|
riskLevel: 'low',
|
|
},
|
|
|
|
K8sPodStatusCard: {
|
|
name: 'K8sPodStatusCard',
|
|
description: 'Pod 狀態卡 - 顯示 K8s Pod 健康狀態',
|
|
component: K8sPodStatusCard,
|
|
zodSchema: K8sPodStatusCardSchema,
|
|
propsSchema: {
|
|
namespace: 'string',
|
|
pods: 'array',
|
|
summary: 'object',
|
|
},
|
|
allowInTerminal: true,
|
|
riskLevel: 'low',
|
|
},
|
|
|
|
TraceWaterfallCard: {
|
|
name: 'TraceWaterfallCard',
|
|
description: '追蹤瀑布圖卡 - 顯示 SignOz Trace 詳情',
|
|
component: TraceWaterfallCard,
|
|
zodSchema: TraceWaterfallCardSchema,
|
|
propsSchema: {
|
|
traceId: 'string',
|
|
spans: 'array',
|
|
duration: 'number',
|
|
},
|
|
allowInTerminal: true,
|
|
riskLevel: 'low',
|
|
},
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 核鑰 UX 組件
|
|
// -------------------------------------------------------------------------
|
|
|
|
NuclearKeyButton: {
|
|
name: 'NuclearKeyButton',
|
|
description: '核鑰授權按鈕 - 長按確認高風險操作',
|
|
component: NuclearKeyButton,
|
|
zodSchema: NuclearKeyButtonSchema,
|
|
propsSchema: {
|
|
label: 'string',
|
|
riskLevel: 'string',
|
|
},
|
|
allowInTerminal: true,
|
|
riskLevel: 'high',
|
|
},
|
|
}
|
|
|
|
// =============================================================================
|
|
// Registry API
|
|
// =============================================================================
|
|
|
|
/**
|
|
* 取得組件定義
|
|
*/
|
|
export function getComponent(name: string): GenUIComponentDef | undefined {
|
|
return GENUI_REGISTRY[name]
|
|
}
|
|
|
|
/**
|
|
* 檢查組件是否已註冊
|
|
*/
|
|
export function isRegistered(name: string): boolean {
|
|
return name in GENUI_REGISTRY
|
|
}
|
|
|
|
/**
|
|
* 取得所有已註冊的組件名稱
|
|
*/
|
|
export function getRegisteredComponents(): string[] {
|
|
return Object.keys(GENUI_REGISTRY)
|
|
}
|
|
|
|
/**
|
|
* 取得適合 Terminal 渲染的組件
|
|
*/
|
|
export function getTerminalComponents(): GenUIComponentDef[] {
|
|
return Object.values(GENUI_REGISTRY).filter(c => c.allowInTerminal)
|
|
}
|
|
|
|
/** 驗證結果類型 (Phase 19 首席架構師審查 P1) */
|
|
export interface ValidationResult {
|
|
valid: boolean
|
|
errors: string[]
|
|
/** 錯誤分類碼 (便於 Sentry 聚合) */
|
|
errorCode?: 'UNKNOWN_COMPONENT' | 'ZOD_VALIDATION_FAILED' | 'LEGACY_TYPE_MISMATCH'
|
|
}
|
|
|
|
/**
|
|
* 驗證 Props (Zod + Legacy 雙模式)
|
|
*
|
|
* Phase 19 首席架構師審查 P1 改進:
|
|
* - 優先使用 Zod Schema 驗證 (更精確的格式檢查)
|
|
* - 回退到 Legacy propsSchema (向後相容)
|
|
* - 返回錯誤分類碼便於 Sentry 聚合
|
|
*/
|
|
export function validateProps(
|
|
componentName: string,
|
|
props: Record<string, unknown>
|
|
): ValidationResult {
|
|
const def = GENUI_REGISTRY[componentName]
|
|
if (!def) {
|
|
return {
|
|
valid: false,
|
|
errors: [`Unknown component: ${componentName}`],
|
|
errorCode: 'UNKNOWN_COMPONENT',
|
|
}
|
|
}
|
|
|
|
// 優先使用 Zod Schema 驗證 (Phase 19 P1)
|
|
if (def.zodSchema) {
|
|
const result = def.zodSchema.safeParse(props)
|
|
if (!result.success) {
|
|
const errors = result.error.errors.map(e =>
|
|
`${e.path.join('.')}: ${e.message}`
|
|
)
|
|
return {
|
|
valid: false,
|
|
errors,
|
|
errorCode: 'ZOD_VALIDATION_FAILED',
|
|
}
|
|
}
|
|
return { valid: true, errors: [] }
|
|
}
|
|
|
|
// Legacy propsSchema 回退驗證
|
|
if (!def.propsSchema) {
|
|
return { valid: true, errors: [] }
|
|
}
|
|
|
|
const errors: string[] = []
|
|
|
|
for (const [key, expectedType] of Object.entries(def.propsSchema)) {
|
|
const value = props[key]
|
|
|
|
if (value === undefined) {
|
|
// Optional props are OK
|
|
continue
|
|
}
|
|
|
|
const actualType = Array.isArray(value) ? 'array' : typeof value
|
|
|
|
if (actualType !== expectedType) {
|
|
errors.push(`${key}: expected ${expectedType}, got ${actualType}`)
|
|
}
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
errorCode: errors.length > 0 ? 'LEGACY_TYPE_MISMATCH' : undefined,
|
|
}
|
|
}
|