From 8159d22db9985874ed6bc399a6427c91b787492c Mon Sep 17 00:00:00 2001 From: OG T Date: Tue, 24 Mar 2026 12:57:36 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20ClawBot=20=E2=86=92=20OpenClaw=20?= =?UTF-8?q?=E5=85=A8=E5=9F=9F=E6=9B=B4=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 刪除舊版 clawbot.py (已有新版 openclaw.py) - 更新 models/ai.py 類型定義 (ClawBotAnalysisRequest/Response) - 更新 api/v1/ai.py import 與註解 - 更新 Discord username - 更新所有註解與文檔 依據: feedback_openclaw_naming.md (統帥 2026-03-20 正式命名決議) Co-Authored-By: Claude Opus 4.5 --- apps/api/src/api/v1/ai.py | 34 +- apps/api/src/api/v1/approvals.py | 4 +- apps/api/src/core/config.py | 6 +- apps/api/src/core/telemetry.py | 2 +- apps/api/src/db/models.py | 2 +- apps/api/src/models/ai.py | 6 +- apps/api/src/plugins/finops/cost_analyzer.py | 18 +- apps/api/src/routes/agent.py | 14 +- apps/api/src/services/clawbot.py | 704 ------------------ apps/api/src/services/graph_rag.py | 2 +- apps/api/src/services/host_aggregator.py | 4 +- .../api/src/services/notifications/discord.py | 4 +- docs/LOGBOOK.md | 1 + 13 files changed, 49 insertions(+), 752 deletions(-) delete mode 100644 apps/api/src/services/clawbot.py diff --git a/apps/api/src/api/v1/ai.py b/apps/api/src/api/v1/ai.py index 183adfc45..320aaddb0 100644 --- a/apps/api/src/api/v1/ai.py +++ b/apps/api/src/api/v1/ai.py @@ -1,14 +1,14 @@ """ AI Decision API ================ -CAI-101: ClawBot 自動化立案 API +CAI-101: OpenClaw 自動化立案 API Endpoints: - POST /api/v1/ai/analyze-and-propose 流程: 1. 拉取當前監控數據 (host_aggregator) -2. 交給 ClawBot AI 分析 +2. 交給 OpenClaw AI 分析 3. 若需要修復 → 自動建立 ApprovalRecord 4. 前端戰情室即時拉取待簽核卡片 """ @@ -19,8 +19,8 @@ from src.core.logging import get_logger from src.core.trust_engine import get_trust_engine from src.models.ai import ( AIRiskLevel, - ClawBotAnalysisRequest, - ClawBotAnalysisResponse, + OpenClawAnalysisRequest, + OpenClawAnalysisResponse, OpenClawDecision, SuggestedAction, ) @@ -87,7 +87,7 @@ def _create_approval_from_decision(decision: OpenClawDecision) -> ApprovalReques message=decision.risk_level.value.upper(), ), ], - requested_by="ClawBot", + requested_by="OpenClaw", ) @@ -97,19 +97,19 @@ def _create_approval_from_decision(decision: OpenClawDecision) -> ApprovalReques @router.post( "/analyze-and-propose", - response_model=ClawBotAnalysisResponse, + response_model=OpenClawAnalysisResponse, summary="AI 分析並自動立案", - description="拉取當前監控數據,交給 ClawBot 分析。若判定需要修復,自動建立 ApprovalRecord。", + description="拉取當前監控數據,交給 OpenClaw 分析。若判定需要修復,自動建立 ApprovalRecord。", ) async def analyze_and_propose( - request: ClawBotAnalysisRequest | None = None, -) -> ClawBotAnalysisResponse: + request: OpenClawAnalysisRequest | None = None, +) -> OpenClawAnalysisResponse: """ AI 智能分析與自動立案 流程: 1. 從 host_aggregator 取得最新狀態 - 2. 交給 ClawBot AI 分析 + 2. 交給 OpenClaw AI 分析 3. 解析 JSON 結構化輸出 4. 若 suggested_action != NO_ACTION → 建立 ApprovalRecord """ @@ -119,7 +119,7 @@ async def analyze_and_propose( try: snapshot = await HostAggregator.fetch_all() - # 轉換為 ClawBot 需要的格式 (含基準線數據) + # 轉換為 OpenClaw 需要的格式 (含基準線數據) host_statuses = {} for host in snapshot.hosts: # 組裝 metrics 與 baseline @@ -194,7 +194,7 @@ async def analyze_and_propose( # Step 3: 處理決策 if decision is None: - return ClawBotAnalysisResponse( + return OpenClawAnalysisResponse( success=False, message="AI 分析完成,但無法解析決策輸出。請檢查 LLM 回應格式。", ai_provider=provider, @@ -207,7 +207,7 @@ async def analyze_and_propose( "ai_no_action_needed", reasoning=decision.reasoning, ) - return ClawBotAnalysisResponse( + return OpenClawAnalysisResponse( success=True, message="AI 判斷目前無需採取行動。", decision=decision, @@ -229,9 +229,9 @@ async def analyze_and_propose( risk_level=decision.risk_level.value, ) - return ClawBotAnalysisResponse( + return OpenClawAnalysisResponse( success=True, - message=f"ClawBot 已建立待簽核卡片:{decision.suggested_action.value} {decision.target_resource}", + message=f"OpenClaw 已建立待簽核卡片:{decision.suggested_action.value} {decision.target_resource}", decision=decision, approval_created=True, approval_id=str(approval.id), @@ -243,7 +243,7 @@ async def analyze_and_propose( "ai_approval_create_failed", error=str(e), ) - return ClawBotAnalysisResponse( + return OpenClawAnalysisResponse( success=False, message=f"AI 分析成功,但建立授權請求失敗:{str(e)}", decision=decision, @@ -255,7 +255,7 @@ async def analyze_and_propose( @router.get( "/status", summary="AI 服務狀態", - description="檢查 ClawBot AI 服務狀態與可用的 AI 提供者。", + description="檢查 OpenClaw AI 服務狀態與可用的 AI 提供者。", ) async def get_ai_status() -> dict: """檢查 AI 服務狀態""" diff --git a/apps/api/src/api/v1/approvals.py b/apps/api/src/api/v1/approvals.py index b6f87c737..c410cb8fd 100644 --- a/apps/api/src/api/v1/approvals.py +++ b/apps/api/src/api/v1/approvals.py @@ -12,7 +12,7 @@ Endpoints: - POST /api/v1/approvals/{id}/reject - 拒絕請求 信任鏈流程: -1. ClawBot 發起 CRITICAL 操作 → 建立 ApprovalRequest (PENDING) → 寫入 DB +1. OpenClaw 發起 CRITICAL 操作 → 建立 ApprovalRequest (PENDING) → 寫入 DB 2. 第一位簽核者簽核 → 仍為 PENDING (1/2) → 更新 DB 3. 第二位簽核者簽核 → 轉為 APPROVED → 更新 DB 4. BackgroundTasks 觸發 K8s 執行 → EXECUTION_SUCCESS/FAILED → 更新 DB @@ -623,7 +623,7 @@ async def sign_approval( event_type="exec", status="warning", title=f"K8s Executor 已排程執行: {approval.action[:40]}...", - actor="ClawBot", + actor="OpenClaw", actor_role="executor", approval_id=str(approval_id), ) diff --git a/apps/api/src/core/config.py b/apps/api/src/core/config.py index 75b5a0822..e7adf3666 100644 --- a/apps/api/src/core/config.py +++ b/apps/api/src/core/config.py @@ -48,7 +48,7 @@ class Settings(BaseSettings): # ========================================================================== MOCK_MODE: bool = Field( default=False, - description="Enable mock mode for external services (Redis, Ollama, ClawBot, PostgreSQL, SigNoz)", + description="Enable mock mode for external services (Redis, Ollama, OpenClaw, PostgreSQL, SigNoz)", ) # ========================================================================== @@ -106,7 +106,7 @@ class Settings(BaseSettings): # Deprecated: use OPENCLAW_URL instead CLAWBOT_URL: str = Field( default="http://192.168.0.188:8088", # 🔧 修正: OpenClaw 實際 port 是 8088 - description="[Deprecated] Legacy ClawBot URL - use OPENCLAW_URL", + description="[Deprecated] Legacy OpenClaw URL - use OPENCLAW_URL", ) KALI_SCANNER_URL: str = Field( default="http://192.168.0.112:8080", @@ -201,7 +201,7 @@ class Settings(BaseSettings): HEALTH_CHECK_TIMEOUT: float = Field(default=5.0, description="Health check timeout") # ========================================================================== - # Phase 5: OpenClaw AI Engine (正名自 ClawBot) + # Phase 5: OpenClaw AI Engine (正名自 OpenClaw) # Synced from models.json - Ollama First Strategy # ========================================================================== OPENCLAW_URL: str = Field( diff --git a/apps/api/src/core/telemetry.py b/apps/api/src/core/telemetry.py index b06cde6bb..6fdd66dd3 100644 --- a/apps/api/src/core/telemetry.py +++ b/apps/api/src/core/telemetry.py @@ -146,7 +146,7 @@ def setup_telemetry(app) -> bool: excluded_urls="health,healthz,ready,metrics", # 排除健康檢查 ) - # 自動追蹤 HTTPX 外部呼叫 (Ollama, ClawBot, etc.) + # 自動追蹤 HTTPX 外部呼叫 (Ollama, OpenClaw, etc.) HTTPXClientInstrumentor().instrument(tracer_provider=_tracer_provider) # 自動追蹤日誌 (注入 trace_id, span_id) diff --git a/apps/api/src/db/models.py b/apps/api/src/db/models.py index af14734ef..e01609c31 100644 --- a/apps/api/src/db/models.py +++ b/apps/api/src/db/models.py @@ -157,7 +157,7 @@ class TimelineEvent(Base): 事件類型: - system: 系統告警接收 - - agent: ClawBot AI 分析 + - agent: OpenClaw AI 分析 - security: 權限阻擋 - human: 人類授權 - exec: 執行完成 diff --git a/apps/api/src/models/ai.py b/apps/api/src/models/ai.py index f7243b95b..a37e5f19f 100644 --- a/apps/api/src/models/ai.py +++ b/apps/api/src/models/ai.py @@ -1,7 +1,7 @@ """ AI Decision Models - Phase 2 Structured Output =============================================== -CAI-101: ClawBot AI 結構化輸出模型 +CAI-101: OpenClaw AI 結構化輸出模型 防禦性工程鐵律: - 絕對禁止 LLM 輸出無法解析的自由文本 @@ -189,7 +189,7 @@ class OpenClawDecision(BaseModel): return v -class ClawBotAnalysisRequest(BaseModel): +class OpenClawAnalysisRequest(BaseModel): """分析請求""" force_refresh: bool = Field( default=False, @@ -197,7 +197,7 @@ class ClawBotAnalysisRequest(BaseModel): ) -class ClawBotAnalysisResponse(BaseModel): +class OpenClawAnalysisResponse(BaseModel): """分析回應""" success: bool message: str diff --git a/apps/api/src/plugins/finops/cost_analyzer.py b/apps/api/src/plugins/finops/cost_analyzer.py index 2803f2fd1..5d1fd889b 100644 --- a/apps/api/src/plugins/finops/cost_analyzer.py +++ b/apps/api/src/plugins/finops/cost_analyzer.py @@ -9,7 +9,7 @@ Phase 3.3: 商業變現能力 - Day-1 ROI 輸出格式: - total_wasted_usd: 每月浪費金額 -- recommended_actions: ClawBot 可執行的建議清單 +- recommended_actions: OpenClaw 可執行的建議清單 """ import logging @@ -78,7 +78,7 @@ class SavingsType(str, Enum): @dataclass class RecommendedAction: - """建議的優化動作 (ClawBot 可執行)""" + """建議的優化動作 (OpenClaw 可執行)""" action_id: str action_type: Literal["delete", "scale_down", "resize", "migrate"] resource_type: ResourceType @@ -87,7 +87,7 @@ class RecommendedAction: description: str estimated_savings_usd: float risk_level: Literal["low", "medium", "high", "critical"] - command_hint: str # 給 ClawBot 的執行提示 + command_hint: str # 給 OpenClaw 的執行提示 savings_type: SavingsType = SavingsType.REALIZABLE # 節省類型 def to_dict(self) -> dict: @@ -107,7 +107,7 @@ class RecommendedAction: @dataclass class CostReport: - """成本報告 (ClawBot 整合用)""" + """成本報告 (OpenClaw 整合用)""" scan_id: str scanned_at: datetime cluster_name: str @@ -126,13 +126,13 @@ class CostReport: waste_by_namespace: dict[str, float] def to_dict(self) -> dict: - """輸出 ClawBot 可讀取的 JSON 格式""" + """輸出 OpenClaw 可讀取的 JSON 格式""" return { "scanId": self.scan_id, "scannedAt": self.scanned_at.isoformat(), "clusterName": self.cluster_name, - # ClawBot 核心關注 + # OpenClaw 核心關注 "totalWastedUsd": round(self.total_wasted_usd, 2), "totalResourcesScanned": self.total_resources_scanned, "wastedResourcesCount": self.wasted_resources_count, @@ -217,7 +217,7 @@ class IdleResourceScanner: 閒置資源掃描器 偵測並量化 K8s 叢集中的浪費資源, - 轉換為美金金額,供 ClawBot 決策 + 轉換為美金金額,供 OpenClaw 決策 """ def __init__(self, pricing: PricingConfig | None = None): @@ -490,7 +490,7 @@ class IdleResourceScanner: wasted: list[WastedResource], ) -> list[RecommendedAction]: """ - 產生優化建議 (ClawBot 可執行) + 產生優化建議 (OpenClaw 可執行) """ actions = [] action_counter = 0 @@ -585,7 +585,7 @@ class IdleResourceScanner: ╚════════════════════════════════════════════════════════════════╝ Returns: - ClawBot 可直接使用的 JSON 格式 + OpenClaw 可直接使用的 JSON 格式 """ realizable = sum( a.estimated_savings_usd diff --git a/apps/api/src/routes/agent.py b/apps/api/src/routes/agent.py index c4deff8d9..3a869e48f 100644 --- a/apps/api/src/routes/agent.py +++ b/apps/api/src/routes/agent.py @@ -1,5 +1,5 @@ """ -Agent (ClawBot) Endpoints +Agent (OpenClaw) Endpoints ADR-005: BFF 架構 - 所有 AI 調用經過 BFF Phase 1.2: 真實 Ollama 串接 """ @@ -54,10 +54,10 @@ class AgentStatus(BaseModel): @router.post("/chat", response_model=ChatResponse) async def chat_with_agent(request: ChatRequest) -> ChatResponse: - """與 ClawBot 對話""" + """與 OpenClaw 對話""" conversation_id = request.conversation_id or uuid4() - # TODO: 實際調用 ClawBot + # TODO: 實際調用 OpenClaw return ChatResponse( message=f"收到訊息: {request.message}", conversation_id=conversation_id, @@ -67,11 +67,11 @@ async def chat_with_agent(request: ChatRequest) -> ChatResponse: @router.post("/chat/stream") async def chat_with_agent_stream(request: ChatRequest) -> StreamingResponse: - """與 ClawBot 對話 (SSE 串流)""" + """與 OpenClaw 對話 (SSE 串流)""" async def generate(): # TODO: 實際串流 - yield "data: Hello from ClawBot\n\n" + yield "data: Hello from OpenClaw\n\n" yield "data: [DONE]\n\n" return StreamingResponse( @@ -82,7 +82,7 @@ async def chat_with_agent_stream(request: ChatRequest) -> StreamingResponse: @router.get("/status", response_model=AgentStatus) async def get_agent_status() -> AgentStatus: - """ClawBot 狀態""" + """OpenClaw 狀態""" return AgentStatus( status="idle", active_conversations=0, @@ -100,7 +100,7 @@ async def get_agent_thinking( model: str = Query(default=OLLAMA_MODEL, description="Ollama 模型名稱"), ) -> StreamingResponse: """ - ClawBot 思考軌跡 (SSE 串流) + OpenClaw 思考軌跡 (SSE 串流) Phase 1.2: 真實串接 Ollama at 192.168.0.188:11434 """ diff --git a/apps/api/src/services/clawbot.py b/apps/api/src/services/clawbot.py deleted file mode 100644 index 5dd889469..000000000 --- a/apps/api/src/services/clawbot.py +++ /dev/null @@ -1,704 +0,0 @@ -""" -ClawBot AI Decision Engine - True LLM Integration -=================================================== -CAI-101: AI 決策大腦 (Phase 2: 實彈裝填) - -Features: -- 真實 LLM SDK 整合 (Ollama → Gemini → Claude) -- AIOps Agent 專業人格 (K8s 維運 + SRE RCA 專精) -- 強制結構化 JSON 輸出 (符合 API 契約) -- 動態告警上下文注入 -- 優雅降級 Mock Fallback - -防禦性工程鐵律: -- Zero Trust: 預設不信任 LLM 輸出,必須通過 Pydantic 驗證 -- Edge Case: 網路失敗、解析失敗、超時處理 -""" - -import json -import random -import re -import time -from typing import Any - -import httpx -import structlog - -from src.core.config import settings -from src.models.ai import ( - ClawBotDecision, -) - -logger = structlog.get_logger(__name__) - - -# ============================================================================= -# AIOps Agent System Prompt (專業人格) -# ============================================================================= - -CLAWBOT_SYSTEM_PROMPT = """# ClawBot v5.0 - AWOOOI AIOps Agent - -You are ClawBot, a senior Site Reliability Engineer (SRE) AI agent specialized in: -- Kubernetes cluster operations and troubleshooting -- Root Cause Analysis (RCA) for production incidents -- Blast radius assessment for proposed remediation actions -- Risk-aware automated remediation recommendations - -## Your Responsibilities -1. Analyze incoming alerts and system metrics -2. Identify the root cause of incidents -3. Assess the blast radius of potential fixes -4. Recommend the safest remediation action with specific kubectl commands -5. Provide clear, human-readable explanations in Traditional Chinese (繁體中文) - -## Output Rules -- You MUST respond with ONLY valid JSON, no markdown, no explanation outside JSON -- Every field in the schema is REQUIRED -- risk_level MUST be one of: "low", "medium", "critical" -- suggested_action MUST be one of: "RESTART_DEPLOYMENT", "DELETE_POD", "SCALE_DEPLOYMENT", "NO_ACTION" -- confidence MUST be between 0.0 and 1.0 - -## JSON Schema (REQUIRED) -```json -{ - "action_title": "string - 操作標題 (繁體中文, 簡潔)", - "description": "string - 根本原因分析說明 (繁體中文, 2-3 句話)", - "suggested_action": "RESTART_DEPLOYMENT|DELETE_POD|SCALE_DEPLOYMENT|NO_ACTION", - "kubectl_command": "string - 具體的 kubectl 指令", - "target_resource": "string - 目標資源名稱", - "namespace": "string - K8s namespace", - "risk_level": "low|medium|critical", - "blast_radius": { - "affected_pods": "number - 受影響的 Pod 數量", - "estimated_downtime": "string - 預估停機時間", - "related_services": ["array of strings - 相關服務"], - "data_impact": "NONE|READ_ONLY|WRITE|DESTRUCTIVE" - }, - "reasoning": "string - 決策理由 (繁體中文)", - "deviation_analysis": "string - 基準線偏差分析", - "confidence": "number - 0.0 to 1.0", - "affected_services": ["array of strings"] -} -``` - -## Example Response -```json -{ - "action_title": "重新啟動 Payment 服務 Pod", - "description": "Payment 服務發生 OOMKilled,根本原因為記憶體洩漏導致 Java Heap 耗盡。建議立即重啟 Pod 以恢復服務,同時排程開發團隊檢查記憶體洩漏。", - "suggested_action": "DELETE_POD", - "kubectl_command": "kubectl delete pod payment-service-7d4b8c9f5-xk2m3 -n payment", - "target_resource": "payment-service-7d4b8c9f5-xk2m3", - "namespace": "payment", - "risk_level": "critical", - "blast_radius": { - "affected_pods": 1, - "estimated_downtime": "~30s", - "related_services": ["api-gateway", "checkout-service"], - "data_impact": "NONE" - }, - "reasoning": "Pod 已進入 OOMKilled 狀態,ReplicaSet 會自動重建新 Pod,預計 30 秒內恢復", - "deviation_analysis": "Memory 使用率 98%,超出基準線 60% 達 +6.3σ", - "confidence": 0.92, - "affected_services": ["payment-service", "checkout-service"] -} -``` - -Now analyze the following alert: -""" - - -# ============================================================================= -# LLM Analysis Result - Using Pydantic for Schema Enforcement -# ============================================================================= - -# We use ClawBotDecision from models/ai.py for Pydantic validation -# This alias is for backwards compatibility -LLMAnalysisResult = ClawBotDecision - - -# ============================================================================= -# ClawBot Service -# ============================================================================= - -class ClawBotService: - """ - ClawBot AI 決策服務 - True LLM Integration - - 實作 AI_FALLBACK_ORDER 備援機制: - Ollama → Gemini → Claude → Mock - """ - - def __init__(self): - self._http_client: httpx.AsyncClient | None = None - - async def _get_client(self) -> httpx.AsyncClient: - """取得 HTTP 客戶端""" - if self._http_client is None or self._http_client.is_closed: - self._http_client = httpx.AsyncClient( - timeout=httpx.Timeout(120.0, connect=10.0), - ) - return self._http_client - - async def close(self) -> None: - """關閉連線""" - if self._http_client: - await self._http_client.aclose() - self._http_client = None - - # ========================================================================= - # AI Provider Implementations - Enhanced with Structured Output - # ========================================================================= - - async def _call_ollama(self, prompt: str) -> tuple[str, bool]: - """ - 呼叫本機 Ollama (支援 JSON Mode) - """ - try: - client = await self._get_client() - - logger.info( - "ollama_request_start", - url=f"{settings.OLLAMA_URL}/api/generate", - prompt_length=len(prompt), - ) - - response = await client.post( - f"{settings.OLLAMA_URL}/api/generate", - json={ - "model": "llama3.2:3b", # 使用更大的模型提高品質 - "prompt": prompt, - "stream": False, - "format": "json", # 強制 JSON 輸出 - "options": { - "num_predict": 1024, # 增加輸出長度 - "temperature": 0.1, # 低溫度確保穩定輸出 - "top_p": 0.9, - }, - }, - timeout=httpx.Timeout(90.0, connect=10.0), - ) - - logger.info( - "ollama_response_received", - status_code=response.status_code, - ) - - response.raise_for_status() - data = response.json() - result = data.get("response", "") - - logger.info( - "ollama_response_parsed", - response_length=len(result), - ) - - return result, True - - except httpx.TimeoutException as e: - logger.warning("ollama_timeout", error=str(e)) - return f"Timeout: {e}", False - - except Exception as e: - logger.warning( - "ollama_call_failed", - error=str(e), - error_type=type(e).__name__, - ) - return str(e), False - - async def _call_gemini(self, prompt: str) -> tuple[str, bool]: - """ - 呼叫 Google Gemini (支援 JSON Mode) - """ - if not settings.GEMINI_API_KEY: - return "GEMINI_API_KEY not configured", False - - try: - client = await self._get_client() - - # Gemini 1.5 Flash 支援 JSON Mode - response = await client.post( - f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={settings.GEMINI_API_KEY}", - json={ - "contents": [{"parts": [{"text": prompt}]}], - "generationConfig": { - "temperature": 0.1, - "maxOutputTokens": 2048, - "responseMimeType": "application/json", # 強制 JSON 輸出 - }, - }, - timeout=30.0, - ) - response.raise_for_status() - data = response.json() - text = data["candidates"][0]["content"]["parts"][0]["text"] - - logger.info("gemini_response_received", response_length=len(text)) - return text, True - - except Exception as e: - logger.warning("gemini_call_failed", error=str(e)) - return str(e), False - - async def _call_claude(self, prompt: str) -> tuple[str, bool]: - """ - 呼叫 Anthropic Claude (使用 Tool Use 強制 JSON) - """ - if not settings.CLAUDE_API_KEY: - return "CLAUDE_API_KEY not configured", False - - try: - client = await self._get_client() - - # Claude 使用 Tool Use 強制結構化輸出 - response = await client.post( - "https://api.anthropic.com/v1/messages", - headers={ - "x-api-key": settings.CLAUDE_API_KEY, - "anthropic-version": "2023-06-01", - "content-type": "application/json", - }, - json={ - "model": "claude-3-haiku-20240307", - "max_tokens": 2048, - "messages": [{"role": "user", "content": prompt}], - "tools": [{ - "name": "submit_analysis", - "description": "Submit the RCA analysis result in structured format", - "input_schema": { - "type": "object", - "properties": { - "action_title": {"type": "string"}, - "description": {"type": "string"}, - "suggested_action": {"type": "string", "enum": ["RESTART_DEPLOYMENT", "DELETE_POD", "SCALE_DEPLOYMENT", "NO_ACTION"]}, - "kubectl_command": {"type": "string"}, - "target_resource": {"type": "string"}, - "namespace": {"type": "string"}, - "risk_level": {"type": "string", "enum": ["low", "medium", "critical"]}, - "blast_radius": { - "type": "object", - "properties": { - "affected_pods": {"type": "integer"}, - "estimated_downtime": {"type": "string"}, - "related_services": {"type": "array", "items": {"type": "string"}}, - "data_impact": {"type": "string", "enum": ["NONE", "READ_ONLY", "WRITE", "DESTRUCTIVE"]} - }, - "required": ["affected_pods", "estimated_downtime", "related_services", "data_impact"] - }, - "reasoning": {"type": "string"}, - "deviation_analysis": {"type": "string"}, - "confidence": {"type": "number"}, - "affected_services": {"type": "array", "items": {"type": "string"}} - }, - "required": ["action_title", "description", "suggested_action", "kubectl_command", "target_resource", "namespace", "risk_level", "blast_radius", "reasoning", "confidence"] - } - }], - "tool_choice": {"type": "tool", "name": "submit_analysis"}, - }, - timeout=30.0, - ) - response.raise_for_status() - data = response.json() - - # 從 Tool Use 回應中提取 JSON - for block in data.get("content", []): - if block.get("type") == "tool_use" and block.get("name") == "submit_analysis": - tool_input = block.get("input", {}) - logger.info("claude_tool_use_response", input_keys=list(tool_input.keys())) - return json.dumps(tool_input), True - - # Fallback: 嘗試從 text 內容提取 - for block in data.get("content", []): - if block.get("type") == "text": - return block.get("text", ""), True - - return "No valid response from Claude", False - - except Exception as e: - logger.warning("claude_call_failed", error=str(e)) - return str(e), False - - # ========================================================================= - # Mock LLM - Intelligent Fallback - # ========================================================================= - - def _generate_mock_response(self, alert_context: dict) -> str: - """ - Mock LLM 回應生成器 - 智能降級 - - 根據告警類型動態產生合理的 RCA 分析結果 - """ - time.sleep(random.uniform(0.3, 0.8)) # 模擬思考延遲 - - alert_type = alert_context.get("alert_type", "custom") - severity = alert_context.get("severity", "warning") - target = alert_context.get("target_resource", "unknown-service") - namespace = alert_context.get("namespace", "default") - message = alert_context.get("message", "") - metrics = alert_context.get("metrics", {}) - - # 根據告警類型生成專業 RCA - if "oom" in message.lower() or "memory" in alert_type.lower(): - mock_response = { - "action_title": f"重新啟動 {target} Pod (OOMKilled)", - "description": f"[MOCK RCA] {target} 發生 OOMKilled,根本原因為記憶體洩漏或配置不足。建議立即重啟 Pod 恢復服務,並安排開發團隊檢查 Heap 配置。", - "suggested_action": "DELETE_POD", - "kubectl_command": f"kubectl delete pod {target} -n {namespace}", - "target_resource": target, - "namespace": namespace, - "risk_level": "critical" if severity == "critical" else "medium", - "blast_radius": { - "affected_pods": 1, - "estimated_downtime": "~30s", - "related_services": ["api-gateway", "downstream-service"], - "data_impact": "NONE" - }, - "reasoning": "[MOCK] Pod OOMKilled 後 ReplicaSet 將自動重建,服務預計 30 秒內恢復", - "deviation_analysis": f"[MOCK] Memory 使用率 {metrics.get('memory_percent', 95)}%,超出基準線達 +5.2σ", - "confidence": 0.88, - "affected_services": [target, "api-gateway"] - } - elif "db" in alert_type.lower() or "connection" in message.lower() or "pool" in message.lower(): - mock_response = { - "action_title": f"重啟 {target} 資料庫連線池", - "description": f"[MOCK RCA] {target} 資料庫連線池已滿載,根本原因為連線未正確釋放或流量突增。建議重啟服務以重置連線池。", - "suggested_action": "RESTART_DEPLOYMENT", - "kubectl_command": f"kubectl rollout restart deployment/{target} -n {namespace}", - "target_resource": target, - "namespace": namespace, - "risk_level": "critical", - "blast_radius": { - "affected_pods": 3, - "estimated_downtime": "~2 min", - "related_services": ["auth-service", "user-service", "order-service"], - "data_impact": "WRITE" - }, - "reasoning": "[MOCK] 資料庫連線池滿載會導致所有依賴服務無法存取資料,需立即重啟", - "deviation_analysis": f"[MOCK] Active connections: {metrics.get('active_connections', 100)}/{metrics.get('max_connections', 100)}", - "confidence": 0.85, - "affected_services": [target, "auth-service", "api-gateway"] - } - elif "crash" in alert_type.lower() or "pod" in alert_type.lower(): - mock_response = { - "action_title": f"刪除異常 Pod {target}", - "description": f"[MOCK RCA] {target} 發生 CrashLoopBackOff,根本原因為應用程式啟動失敗。建議刪除 Pod 讓 ReplicaSet 重建。", - "suggested_action": "DELETE_POD", - "kubectl_command": f"kubectl delete pod {target} -n {namespace}", - "target_resource": target, - "namespace": namespace, - "risk_level": "medium" if severity != "critical" else "critical", - "blast_radius": { - "affected_pods": 1, - "estimated_downtime": "~30s", - "related_services": ["ingress-controller"], - "data_impact": "NONE" - }, - "reasoning": "[MOCK] CrashLoopBackOff 通常為暫時性啟動問題,重建 Pod 可解決", - "deviation_analysis": f"[MOCK] Restart count: {metrics.get('restart_count', 5)}", - "confidence": 0.82, - "affected_services": [target] - } - elif "cpu" in alert_type.lower() or "high_cpu" in alert_type: - mock_response = { - "action_title": f"擴展 {target} 副本數", - "description": f"[MOCK RCA] {target} CPU 使用率過高,根本原因為流量突增或運算密集任務。建議水平擴展增加副本數。", - "suggested_action": "SCALE_DEPLOYMENT", - "kubectl_command": f"kubectl scale deployment/{target} --replicas=+2 -n {namespace}", - "target_resource": target, - "namespace": namespace, - "risk_level": "medium", - "blast_radius": { - "affected_pods": 0, - "estimated_downtime": "0", - "related_services": [], - "data_impact": "NONE" - }, - "reasoning": "[MOCK] 水平擴展可分散負載,無停機風險", - "deviation_analysis": f"[MOCK] CPU 使用率 {metrics.get('cpu_percent', 95)}%,超出基準線達 +4.5σ", - "confidence": 0.90, - "affected_services": [target] - } - else: - # 通用異常處理 - mock_response = { - "action_title": f"重新啟動 {target} 服務", - "description": f"[MOCK RCA] {target} 發生異常: {message}。建議重啟服務以恢復正常運作。", - "suggested_action": "RESTART_DEPLOYMENT", - "kubectl_command": f"kubectl rollout restart deployment/{target} -n {namespace}", - "target_resource": target, - "namespace": namespace, - "risk_level": "critical" if severity == "critical" else "medium", - "blast_radius": { - "affected_pods": 3, - "estimated_downtime": "~1 min", - "related_services": ["dependent-services"], - "data_impact": "NONE" - }, - "reasoning": f"[MOCK] 根據告警 {alert_type} 判斷需要重啟服務", - "deviation_analysis": "[MOCK] 監控指標顯示異常", - "confidence": 0.75, - "affected_services": [target] - } - - logger.info( - "mock_llm_response_generated", - action_title=mock_response["action_title"], - risk_level=mock_response["risk_level"], - is_mock=True, - ) - - return json.dumps(mock_response) - - # ========================================================================= - # Fallback Chain - # ========================================================================= - - async def _call_with_fallback(self, prompt: str, alert_context: dict | None = None) -> tuple[str, str, bool]: - """ - 依 AI_FALLBACK_ORDER 順序呼叫 AI - - 若 MOCK_MODE=True,直接回傳模擬結果。 - 若所有 Provider 失敗,fallback 到 Mock。 - """ - # Mock Mode: 開發測試用 - if settings.MOCK_MODE: - logger.info("mock_mode_enabled", using="mock_llm") - return self._generate_mock_response(alert_context or {}), "mock", True - - for provider in settings.AI_FALLBACK_ORDER: - logger.info("ai_provider_attempt", provider=provider) - - if provider == "ollama": - response, success = await self._call_ollama(prompt) - elif provider == "gemini": - response, success = await self._call_gemini(prompt) - elif provider == "claude": - response, success = await self._call_claude(prompt) - else: - logger.warning("unknown_ai_provider", provider=provider) - continue - - if success: - logger.info("ai_provider_success", provider=provider) - return response, provider, True - - logger.warning("ai_provider_failed_fallback", provider=provider) - - # 所有 Provider 失敗時,fallback 到 Mock (優雅降級) - logger.warning("all_providers_failed_using_mock", fallback="mock_llm") - return self._generate_mock_response(alert_context or {}), "mock_fallback", True - - # ========================================================================= - # Response Parsing (防禦性解析) - # ========================================================================= - - def _extract_json_from_response(self, text: str) -> str | None: - """從 LLM 回應中提取 JSON""" - # 嘗試直接解析 - try: - json.loads(text) - return text - except json.JSONDecodeError: - pass - - # 嘗試從 markdown code block 提取 - patterns = [ - r"```json\s*([\s\S]*?)\s*```", - r"```\s*([\s\S]*?)\s*```", - r"\{[\s\S]*\}", - ] - - for pattern in patterns: - match = re.search(pattern, text) - if match: - candidate = match.group(1) if "```" in pattern else match.group(0) - try: - json.loads(candidate) - return candidate - except json.JSONDecodeError: - continue - - return None - - def _parse_analysis_result(self, raw_response: str) -> ClawBotDecision | None: - """ - 解析 LLM 分析結果 - 使用 Pydantic Schema Enforcement - - 關鍵:blast_radius 為 REQUIRED,使用 AIBlastRadius Pydantic 模型驗證 - """ - json_str = self._extract_json_from_response(raw_response) - if not json_str: - logger.error("json_extraction_failed", raw_response=raw_response[:200]) - return None - - try: - data = json.loads(json_str) - - # Step 1: 確保 blast_radius 存在且為正確格式 - if "blast_radius" not in data or not isinstance(data["blast_radius"], dict): - data["blast_radius"] = { - "affected_pods": 1, - "estimated_downtime": "~30s", - "related_services": data.get("affected_services", []), - "data_impact": "NONE" - } - else: - # 確保 blast_radius 內的必填欄位存在 - br = data["blast_radius"] - if "affected_pods" not in br: - br["affected_pods"] = 1 - if "estimated_downtime" not in br: - br["estimated_downtime"] = "~30s" - if "related_services" not in br: - br["related_services"] = data.get("affected_services", []) - if "data_impact" not in br: - br["data_impact"] = "NONE" - - # Step 2: 填補其他可選欄位 - if "action_title" not in data: - data["action_title"] = data.get("action", "未知操作") - if "target_resource" not in data: - data["target_resource"] = "unknown" - if "suggested_action" not in data: - data["suggested_action"] = "NO_ACTION" - - # Step 3: 使用 Pydantic 驗證 (會自動正規化 risk_level, data_impact 等) - decision = ClawBotDecision(**data) - - logger.info( - "pydantic_validation_success", - action_title=decision.action_title, - risk_level=decision.risk_level.value, - blast_radius_pods=decision.blast_radius.affected_pods, - ) - - return decision - - except Exception as e: - logger.error( - "pydantic_validation_failed", - error=str(e), - json_str=json_str[:300], - ) - return None - - # ========================================================================= - # Main Analysis Methods - # ========================================================================= - - async def analyze_alert(self, alert_context: dict) -> tuple[LLMAnalysisResult | None, str, str]: - """ - 分析告警並產生 RCA 結果 - - Args: - alert_context: 告警上下文 (alert_type, severity, target_resource, etc.) - - Returns: - (analysis_result, ai_provider, raw_response) - """ - # 格式化告警為 Prompt - alert_json = json.dumps(alert_context, ensure_ascii=False, indent=2) - full_prompt = CLAWBOT_SYSTEM_PROMPT + "\n" + alert_json - - logger.info( - "clawbot_alert_analysis_start", - alert_type=alert_context.get("alert_type"), - target=alert_context.get("target_resource"), - ) - - # 呼叫 LLM - raw_response, provider, success = await self._call_with_fallback(full_prompt, alert_context) - - if not success: - logger.error("clawbot_all_providers_failed") - return None, provider, raw_response - - logger.info( - "clawbot_llm_response_received", - provider=provider, - response_length=len(raw_response), - ) - - # 解析結果 - result = self._parse_analysis_result(raw_response) - - if result: - logger.info( - "clawbot_analysis_complete", - action_title=result.action_title, - risk_level=result.risk_level, - confidence=result.confidence, - provider=provider, - ) - else: - logger.warning( - "clawbot_analysis_parse_failed", - raw_response=raw_response[:300], - ) - - return result, provider, raw_response - - # Legacy method for backwards compatibility - def _parse_decision(self, raw_response: str) -> ClawBotDecision | None: - """解析 LLM 回應為 ClawBotDecision (向後相容)""" - json_str = self._extract_json_from_response(raw_response) - if not json_str: - return None - - try: - data = json.loads(json_str) - risk_mapping = {"high": "critical", "severe": "critical", "warning": "medium"} - if "risk_level" in data: - risk = str(data["risk_level"]).lower() - data["risk_level"] = risk_mapping.get(risk, risk) - - return ClawBotDecision(**data) - except Exception as e: - logger.error("decision_parse_failed", error=str(e)) - return None - - def _format_status_for_llm(self, host_statuses: dict[str, Any]) -> str: - """將主機狀態格式化為精簡文本""" - lines = [] - for host_key, host_data in host_statuses.items(): - if isinstance(host_data, dict): - status = host_data.get("status", "unknown") - if status != "healthy": - lines.append(f"{host_key}:{status}") - return "\n".join(lines[:4]) if lines else "OK" - - async def analyze(self, host_statuses: dict[str, Any]) -> tuple[ClawBotDecision | None, str, str]: - """分析主機狀態 (Legacy 方法)""" - status_text = self._format_status_for_llm(host_statuses) - full_prompt = CLAWBOT_SYSTEM_PROMPT + "\n" + status_text - - raw_response, provider, success = await self._call_with_fallback(full_prompt, {}) - if not success: - return None, provider, raw_response - - decision = self._parse_decision(raw_response) - return decision, provider, raw_response - - -# ============================================================================= -# Singleton -# ============================================================================= - -_clawbot: ClawBotService | None = None - - -def get_clawbot() -> ClawBotService: - """取得全域 ClawBot 實例""" - global _clawbot - if _clawbot is None: - _clawbot = ClawBotService() - return _clawbot - - -async def close_clawbot() -> None: - """關閉 ClawBot 連線""" - global _clawbot - if _clawbot: - await _clawbot.close() - _clawbot = None diff --git a/apps/api/src/services/graph_rag.py b/apps/api/src/services/graph_rag.py index 1e0f5dfc0..31aa034bb 100644 --- a/apps/api/src/services/graph_rag.py +++ b/apps/api/src/services/graph_rag.py @@ -392,7 +392,7 @@ class TopologyGraph: """ 完整分析: Blast Radius + Root Cause - ClawBot 主要呼叫這個方法,一次取得: + OpenClaw 主要呼叫這個方法,一次取得: 1. 向上追溯: 誰會受影響 2. 向下追溯: 誰是根本原因 diff --git a/apps/api/src/services/host_aggregator.py b/apps/api/src/services/host_aggregator.py index 901377e30..de922c651 100644 --- a/apps/api/src/services/host_aggregator.py +++ b/apps/api/src/services/host_aggregator.py @@ -7,7 +7,7 @@ Hosts: - 192.168.0.110: DevOps 金庫 (Harbor, GH Runner) - 192.168.0.112: Kali Security (Scanner API) - 192.168.0.120: K3s Master (awoooi-prod namespace) -- 192.168.0.188: AI+Web 中心 (Nginx, PostgreSQL, Redis, Ollama, ClawBot, SigNoz) +- 192.168.0.188: AI+Web 中心 (Nginx, PostgreSQL, Redis, Ollama, OpenClaw, SigNoz) Features: - asyncio.gather for parallel fetching @@ -311,7 +311,7 @@ HOST_CONFIGS = { ("PostgreSQL", 5432, "tcp", None), ("Redis", 6380, "tcp", None), ("Ollama", 11434, "http", "/api/tags"), - ("ClawBot", 8089, "http", "/health"), + ("OpenClaw", 8089, "http", "/health"), ("SigNoz", 3301, "http", "/api/v1/health"), ], }, diff --git a/apps/api/src/services/notifications/discord.py b/apps/api/src/services/notifications/discord.py index 4f2833a7a..650e6f4e6 100644 --- a/apps/api/src/services/notifications/discord.py +++ b/apps/api/src/services/notifications/discord.py @@ -185,7 +185,7 @@ class DiscordWebhookProvider(NotificationProvider): # 建構 Discord Webhook Payload payload = { - "username": "AWOOOI ClawBot", + "username": "AWOOOI OpenClaw", "avatar_url": "https://i.imgur.com/your-avatar.png", # 可替換 "embeds": [self._build_embed(message)], } @@ -252,7 +252,7 @@ class DiscordWebhookProvider(NotificationProvider): # 發送測試訊息 test_payload = { - "username": "AWOOOI ClawBot", + "username": "AWOOOI OpenClaw", "content": "🔔 **AWOOOI 連線測試** - leWOOOgo Notification System 已就緒!", } diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index c4395f7c9..9cc491772 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -41,6 +41,7 @@ | 時間 | 事件 | 負責人 | |------|------|--------| +| 2026-03-24 12:40 | **🔧 CD 修復**: turbo.json 快取邊界 + CD workflow (kustomize/namespace) + Alertmanager 指向 AWOOOI + 部署驗證鐵律 (HARD_RULES + Skills) | 資深顧問 | | 2026-03-24 10:30 | **🔴🔴 禁止 Mock 測試鐵律**: 統帥明確指示「全面禁止!!!」Mock 測試 + 移除 `test_stats_api.py` 與 `test_webhook_telegram_integration.py` + 新增 `feedback_no_mock_testing.md` | Claude Code | | 2026-03-24 10:15 | **📊 Statistics API 完成**: 6 端點 (summary/timeline/trends/top-resources/feedback/themes) + PostgreSQL date_trunc 優化 + Redis 快取 (5分鐘 TTL) + 12 領域主題萃取 | Claude Code | | 2026-03-24 10:00 | **🔧 Y/n 決策重置修復**: DecisionManager 活躍事件自動建立新 Decision (原本返回舊 COMPLETED 導致按鈕永久禁用) | Claude Code |