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:
@@ -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 服務狀態"""
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -157,7 +157,7 @@ class TimelineEvent(Base):
|
||||
|
||||
事件類型:
|
||||
- system: 系統告警接收
|
||||
- agent: ClawBot AI 分析
|
||||
- agent: OpenClaw AI 分析
|
||||
- security: 權限阻擋
|
||||
- human: 人類授權
|
||||
- exec: 執行完成
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -392,7 +392,7 @@ class TopologyGraph:
|
||||
"""
|
||||
完整分析: Blast Radius + Root Cause
|
||||
|
||||
ClawBot 主要呼叫這個方法,一次取得:
|
||||
OpenClaw 主要呼叫這個方法,一次取得:
|
||||
1. 向上追溯: 誰會受影響
|
||||
2. 向下追溯: 誰是根本原因
|
||||
|
||||
|
||||
@@ -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"),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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 已就緒!",
|
||||
}
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user