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:
259
apps/api/src/models/terminal.py
Normal file
259
apps/api/src/models/terminal.py
Normal 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
|
||||
|
||||
儲存在 Redis,TTL 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
|
||||
Reference in New Issue
Block a user