feat(phase19): OmniTerminal + GenUI + Hybrid SSE 架構實作 (Wave 0-2)

Phase 19 OmniTerminal MVP 完成:
- Wave 0: Backend (Hybrid SSE POST→GET 架構)
- Wave 1: Frontend (OmniTerminal 狀態機 + GenUI Registry)
- Wave 2: UI 組件 (8 個 GenUI 動態卡片)

ADR 文檔:
- ADR-031: OmniTerminal SSE 架構
- ADR-032: GenUI 動態渲染框架
- ADR-033: K3s HA 架構設計

GenUI 組件:
- GenUIRenderer, K8sPodStatusCard, SentryErrorCard
- MetricsSummaryCard, IncidentTimelineCard
- TraceWaterfallCard, ApprovalCard, NuclearKeyButton

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-28 00:17:26 +08:00
parent a5ff57ddc3
commit e5ded3b3f2
31 changed files with 6976 additions and 176 deletions

View File

@@ -0,0 +1,259 @@
"""
Terminal Models
===============
Phase 19.1 - Omni-Terminal API Models
Pydantic models for Terminal SSE communication.
@see ADR-031 Omni-Terminal SSE Architecture
@author Claude Code (首席架構師)
@version 1.0.0
@date 2026-03-28 (台北時間)
"""
from datetime import UTC, datetime
from enum import Enum
from typing import Any
from pydantic import BaseModel, Field
# =============================================================================
# Enums
# =============================================================================
class TerminalSessionStatus(str, Enum):
"""Terminal Session 狀態"""
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
ABORTED = "aborted"
ERROR = "error"
# =============================================================================
# Request Models
# =============================================================================
class SpatialContext(BaseModel):
"""
空間感知上下文 (Ghost Payload)
前端隱形夾帶,讓 AI 知道使用者正在看什麼
"""
current_page: str = Field(
...,
description="統帥當前所在的路由",
examples=["/zh-TW/authorizations", "/zh-TW"],
)
focused_entity_id: str | None = Field(
default=None,
description="正在檢視的實體 ID (incident/approval)",
examples=["INC-2026-0001", "APR-2026-0001"],
)
class TerminalIntentRequest(BaseModel):
"""
Terminal Intent 請求
統帥輸入的指令或問題
"""
intent: str = Field(
...,
description="統帥輸入的原始指令或意圖",
min_length=1,
max_length=2000,
examples=["列出所有待審核的 approval", "為什麼 awoooi-api pod 一直重啟?"],
)
context: SpatialContext = Field(
...,
description="前端空間感知上下文",
)
session_id: str | None = Field(
default=None,
description="現有 Session ID (用於續傳)",
)
class TerminalAbortRequest(BaseModel):
"""中斷執行請求"""
reason: str | None = Field(
default=None,
description="中斷原因",
)
# =============================================================================
# Response Models
# =============================================================================
class TerminalIntentResponse(BaseModel):
"""
Intent 請求回應
返回 session_id 供前端訂閱 SSE
"""
session_id: str = Field(
...,
description="Session UUID",
)
stream_url: str = Field(
...,
description="SSE 串流訂閱 URL",
)
created_at: datetime = Field(
default_factory=lambda: datetime.now(UTC),
)
class TerminalStatusResponse(BaseModel):
"""Session 狀態查詢回應"""
session_id: str
status: TerminalSessionStatus
created_at: datetime
last_event_id: int
message_count: int
class TerminalAbortResponse(BaseModel):
"""中斷執行回應"""
session_id: str
aborted: bool
message: str
# =============================================================================
# SSE Event Models
# =============================================================================
class TerminalThoughtEvent(BaseModel):
"""AI 思考軌跡事件"""
agent: str = Field(
...,
description="代理人名稱",
examples=["System", "Investigator", "Strategist"],
)
msg: str = Field(
...,
description="思考內容",
)
class TerminalToolCallEvent(BaseModel):
"""工具呼叫事件"""
tool: str = Field(
...,
description="工具名稱",
examples=["K8s-Log-Scanner", "Sentry-Query", "Playbook-Lookup"],
)
status: str = Field(
...,
description="執行狀態",
examples=["executing", "completed", "failed"],
)
result: Any | None = Field(
default=None,
description="執行結果",
)
class TerminalRenderUIEvent(BaseModel):
"""GenUI 渲染事件"""
component: str = Field(
...,
description="組件名稱 (必須在前端 Registry 中)",
examples=["ApprovalCard", "MetricsSummaryCard", "SentryErrorCard"],
)
props: dict[str, Any] = Field(
default_factory=dict,
description="組件 Props",
)
position: str = Field(
default="inline",
description="渲染位置",
examples=["inline", "modal", "panel"],
)
id: str | None = Field(
default=None,
description="組件 ID (用於更新/移除)",
)
class TerminalActionRequestEvent(BaseModel):
"""核鑰授權請求事件"""
approval_id: str = Field(
...,
description="Approval ID",
)
risk_level: str = Field(
...,
description="風險等級",
examples=["LOW", "MEDIUM", "CRITICAL"],
)
kubectl: str = Field(
...,
description="要執行的 kubectl 命令",
)
description: str | None = Field(
default=None,
description="操作說明",
)
class TerminalCompleteEvent(BaseModel):
"""串流完成事件"""
session_id: str
message: str = "Stream completed"
class TerminalErrorEvent(BaseModel):
"""錯誤事件"""
session_id: str
error_code: str
message: str
recoverable: bool = True
# =============================================================================
# Session Model (Redis Storage)
# =============================================================================
class TerminalSession(BaseModel):
"""
Terminal Session
儲存在 RedisTTL 5 分鐘
"""
session_id: str
user_id: str | None = None
intent: str
context: SpatialContext
status: TerminalSessionStatus = TerminalSessionStatus.PENDING
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
last_event_id: int = 0
message_count: int = 0
def next_event_id(self) -> int:
"""取得下一個事件 ID"""
self.last_event_id += 1
return self.last_event_id