refactor: ClawBot → OpenClaw 全域更名

- 刪除舊版 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 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-24 12:57:36 +08:00
parent fb62aa06f0
commit 8159d22db9
13 changed files with 49 additions and 752 deletions

View File

@@ -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 服務狀態"""

View File

@@ -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),
)

View File

@@ -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(

View File

@@ -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)

View File

@@ -157,7 +157,7 @@ class TimelineEvent(Base):
事件類型:
- system: 系統告警接收
- agent: ClawBot AI 分析
- agent: OpenClaw AI 分析
- security: 權限阻擋
- human: 人類授權
- exec: 執行完成

View File

@@ -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

View File

@@ -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

View File

@@ -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
"""

View File

@@ -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

View File

@@ -392,7 +392,7 @@ class TopologyGraph:
"""
完整分析: Blast Radius + Root Cause
ClawBot 主要呼叫這個方法,一次取得:
OpenClaw 主要呼叫這個方法,一次取得:
1. 向上追溯: 誰會受影響
2. 向下追溯: 誰是根本原因

View File

@@ -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"),
],
},

View File

@@ -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 已就緒!",
}

View File

@@ -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 |