Files
awoooi/apps/api/src/services/notifications/discord.py
OG T 8159d22db9 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>
2026-03-24 12:57:36 +08:00

275 lines
9.9 KiB
Python

"""
Discord Webhook Provider
========================
Phase 6: leWOOOgo Output Plugins
精美戰報格式:
- Discord Embed 豐富內容
- 狀態顏色標示
- 簽核者、影響範圍完整呈現
"""
import httpx
from src.core.config import settings
from src.core.logging import get_logger
from .base import (
ExecutionStatus,
NotificationMessage,
NotificationProvider,
NotificationResult,
NotificationStatus,
)
logger = get_logger("awoooi.notifications.discord")
class DiscordWebhookProvider(NotificationProvider):
"""
Discord Webhook 通知提供者
使用 Discord Embed 格式發送精美戰報
"""
def __init__(self, webhook_url: str | None = None):
self._webhook_url = webhook_url or settings.DISCORD_WEBHOOK_URL
self._client: httpx.AsyncClient | None = None
@property
def name(self) -> str:
return "discord"
@property
def enabled(self) -> bool:
return bool(self._webhook_url)
async def _get_client(self) -> httpx.AsyncClient:
"""取得 HTTP Client (timeout=5s 防止主執行緒阻塞)"""
if self._client is None:
self._client = httpx.AsyncClient(
timeout=httpx.Timeout(5.0, connect=3.0), # 總超時 5s, 連線 3s
)
return self._client
def _get_embed_color(self, status: ExecutionStatus) -> int:
"""取得 Embed 顏色 (Discord 使用十進位整數)"""
if status == ExecutionStatus.SUCCESS:
return 0x00FF00 # 綠色
elif status == ExecutionStatus.FAILED:
return 0xFF0000 # 紅色
elif status == ExecutionStatus.DRY_RUN_BLOCKED:
return 0xFFA500 # 橙色
return 0x808080 # 灰色
def _build_embed(self, message: NotificationMessage) -> dict:
"""
建構 Discord Embed 精美戰報
格式:
┌────────────────────────────────────────┐
│ ✅ 任務執行成功 │
│ ───────────────────────────────────── │
│ 🎯 動作: 重新啟動 harbor-core │
│ 📋 描述: Pod CrashLoopBackOff 修復 │
│ ───────────────────────────────────── │
│ 👥 簽核者: CTO 林技術長, CISO 陳資安長 │
│ 🔴 風險等級: CRITICAL │
│ ───────────────────────────────────── │
│ 💥 影響範圍 │
│ • 受影響 Pods: 3 │
│ • 預估停機: ~30s │
│ • 相關服務: api, auth │
│ ───────────────────────────────────── │
│ 🤖 AI Provider: Ollama (信心度: 85%) │
│ ⏱️ 執行時間: 234ms │
└────────────────────────────────────────┘
"""
# 標題
title = f"{message.status_emoji} {message.status_text}"
# 描述
description = f"**{message.action_title}**"
if message.action_description:
description += f"\n{message.action_description[:200]}"
# 簽核者欄位
signers_value = message.signers_display
if message.signers:
signers_details = []
for s in message.signers:
detail = f"{s.get('name', 'Unknown')}"
if s.get("comment"):
detail += f" - _{s['comment'][:50]}_"
signers_details.append(detail)
signers_value = "\n".join(signers_details)
# 影響範圍欄位
blast_radius_lines = [
f"• 受影響 Pods: **{message.affected_pods}**",
f"• 預估停機: **{message.estimated_downtime}**",
f"• 資料影響: **{message.data_impact.upper()}**",
]
if message.related_services:
services = ", ".join(message.related_services[:5])
blast_radius_lines.append(f"• 相關服務: {services}")
# 執行細節
execution_lines = [
f"• 操作類型: **{message.operation_type}**",
f"• Namespace: `{message.namespace}`",
]
if message.duration_ms:
execution_lines.append(f"• 執行時間: **{message.duration_ms}ms**")
if message.error_message:
execution_lines.append(f"• 錯誤: `{message.error_message[:100]}`")
# AI 資訊
ai_lines = [f"• Provider: **{message.ai_provider}**"]
if message.confidence:
ai_lines.append(f"• 信心度: **{message.confidence:.0%}**")
# 建構 Embed
embed = {
"title": title,
"description": description,
"color": self._get_embed_color(message.execution_status),
"fields": [
{
"name": f"👥 簽核者 ({len(message.signers)}/{message.required_signatures})",
"value": signers_value or "",
"inline": True,
},
{
"name": f"{message.risk_emoji} 風險等級",
"value": message.risk_level.upper(),
"inline": True,
},
{
"name": "💥 影響範圍 (Blast Radius)",
"value": "\n".join(blast_radius_lines),
"inline": False,
},
{
"name": "⚙️ 執行細節",
"value": "\n".join(execution_lines),
"inline": True,
},
{
"name": "🤖 AI 分析",
"value": "\n".join(ai_lines),
"inline": True,
},
],
"footer": {
"text": f"AWOOOI leWOOOgo Engine | Approval ID: {message.approval_id[:8]}...",
"icon_url": "https://cdn.discordapp.com/emojis/1234567890.png", # 可替換
},
"timestamp": message.timestamp.isoformat(),
}
return embed
async def send(self, message: NotificationMessage) -> NotificationResult:
"""發送 Discord 精美戰報"""
if not self.enabled:
logger.warning("discord_webhook_disabled", reason="No webhook URL configured")
return NotificationResult(
status=NotificationStatus.SKIPPED,
provider=self.name,
message="Discord webhook not configured",
)
try:
client = await self._get_client()
# 建構 Discord Webhook Payload
payload = {
"username": "AWOOOI OpenClaw",
"avatar_url": "https://i.imgur.com/your-avatar.png", # 可替換
"embeds": [self._build_embed(message)],
}
logger.info(
"discord_sending_notification",
approval_id=message.approval_id,
status=message.execution_status.value,
)
# 發送請求
response = await client.post(
self._webhook_url,
json=payload,
)
if response.status_code in (200, 204):
logger.info(
"discord_notification_sent",
approval_id=message.approval_id,
status_code=response.status_code,
)
return NotificationResult(
status=NotificationStatus.SUCCESS,
provider=self.name,
message="Discord notification sent successfully",
response_data={"status_code": response.status_code},
)
else:
error_text = response.text[:200]
logger.error(
"discord_notification_failed",
approval_id=message.approval_id,
status_code=response.status_code,
error=error_text,
)
return NotificationResult(
status=NotificationStatus.FAILED,
provider=self.name,
message=f"Discord API error: {response.status_code}",
error=error_text,
)
except Exception as e:
logger.exception(
"discord_notification_exception",
approval_id=message.approval_id,
error=str(e),
)
return NotificationResult(
status=NotificationStatus.FAILED,
provider=self.name,
message="Exception occurred",
error=str(e),
)
async def test_connection(self) -> bool:
"""測試 Discord Webhook 連線"""
if not self.enabled:
return False
try:
client = await self._get_client()
# 發送測試訊息
test_payload = {
"username": "AWOOOI OpenClaw",
"content": "🔔 **AWOOOI 連線測試** - leWOOOgo Notification System 已就緒!",
}
response = await client.post(
self._webhook_url,
json=test_payload,
)
return response.status_code in (200, 204)
except Exception as e:
logger.error("discord_connection_test_failed", error=str(e))
return False
async def close(self) -> None:
"""關閉 HTTP client"""
if self._client:
await self._client.aclose()
self._client = None