feat(api): Sentry + OpenClaw + Telegram 整合 (Phase 10)

- 新增 Telegram 告警發送 (send_sentry_telegram_alert)
- 新增 Approval 創建 (create_sentry_approval)
- 整合 analyze_and_comment 流程:
  1. OpenClaw AI 分析
  2. 建立 Approval 記錄
  3. 發送 Telegram 告警 (含 Y/n 按鈕)
  4. 回寫 Sentry Comment

Memory: project_sentry_openclaw_v2.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-25 09:12:41 +08:00
parent 2a2dac865a
commit 5c95b194b9

View File

@@ -14,15 +14,26 @@ AWOOOI API - Sentry Webhook Handler
🔴 HARD RULE: 時間顯示使用 Asia/Taipei (UTC+8)
"""
import logging
from src.utils.timezone import now_taipei, now_taipei_iso
import uuid
import httpx
import structlog
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
from pydantic import BaseModel
logger = logging.getLogger(__name__)
from src.utils.timezone import now_taipei_iso
from src.services.telegram_gateway import get_telegram_gateway
from src.services.approval_db import get_approval_service
from src.models.approval import (
ApprovalRequestCreate,
RiskLevel,
BlastRadius,
DataImpact,
DryRunCheck,
)
logger = structlog.get_logger(__name__)
router = APIRouter(prefix="/webhooks/sentry", tags=["Sentry Webhook"])
@@ -30,6 +41,14 @@ router = APIRouter(prefix="/webhooks/sentry", tags=["Sentry Webhook"])
OPENCLAW_URL = "http://192.168.0.188:8088"
SENTRY_API_URL = "http://192.168.0.110:9000"
# Sentry Level → Risk Level 映射
SENTRY_LEVEL_TO_RISK = {
"fatal": RiskLevel.CRITICAL,
"error": RiskLevel.HIGH,
"warning": RiskLevel.MEDIUM,
"info": RiskLevel.LOW,
}
class SentryIssuePayload(BaseModel):
"""Sentry Issue Alert Payload (簡化版)"""
@@ -151,27 +170,50 @@ async def analyze_and_comment(
project_slug: str
):
"""
背景任務:分析錯誤並回寫 Sentry Comment
背景任務:分析錯誤 + Telegram 告警 + 建立 Approval
Phase 10: Sentry + OpenClaw AI 整合
1. 呼叫 OpenClaw 分析
2. 發送 Telegram 告警
3. 建立 Approval 供人工審核
4. 回寫 Sentry Comment
"""
try:
logger.info(f"Starting AI analysis for issue {issue_id}")
logger.info("sentry_analysis_started", issue_id=issue_id)
# 呼叫 OpenClaw 分析
# 1. 呼叫 OpenClaw 分析
analysis = await call_openclaw_analyzer(error_context)
# 2. 建立 Approval
approval_id = await create_sentry_approval(
error_context=error_context,
analysis=analysis,
)
# 3. 發送 Telegram 告警
await send_sentry_telegram_alert(
error_context=error_context,
analysis=analysis,
approval_id=approval_id,
)
# 4. 回寫 Sentry Comment (如果分析成功)
if analysis:
# 回寫 Sentry Comment
await post_sentry_comment(
project_slug=project_slug,
issue_id=issue_id,
analysis=analysis
)
logger.info(f"Analysis completed for issue {issue_id}")
else:
logger.warning(f"Analysis returned empty for issue {issue_id}")
logger.info(
"sentry_analysis_completed",
issue_id=issue_id,
approval_id=approval_id,
has_analysis=analysis is not None,
)
except Exception as e:
logger.exception(f"Analysis failed for issue {issue_id}: {e}")
logger.exception("sentry_analysis_failed", issue_id=issue_id, error=str(e))
async def call_openclaw_analyzer(error_context: dict) -> ErrorAnalysisResult | None:
@@ -248,4 +290,128 @@ async def post_sentry_comment(
# )
except Exception as e:
logger.exception(f"Failed to post Sentry comment: {e}")
logger.exception("sentry_comment_failed", issue_id=issue_id, error=str(e))
# =============================================================================
# Telegram 告警 (Phase 10: Sentry + OpenClaw)
# =============================================================================
async def send_sentry_telegram_alert(
error_context: dict,
analysis: ErrorAnalysisResult | None,
approval_id: str,
):
"""
發送 Sentry 錯誤告警到 Telegram
格式 (project_sentry_openclaw_v2.md):
═══════════════════════════
🐛 SENTRY 錯誤告警
═══════════════════════════
📍 components/dashboard.tsx:142
❌ TypeError: Cannot read property 'x' of null
👥 影響用戶: 12
🔄 發生次數: 42
───────────────────────────
🧠 OpenClaw 分析:
「這是 null check 問題...」
───────────────────────────
[ Y 建立修復任務 ] [ n 忽略 ]
"""
try:
telegram = get_telegram_gateway()
await telegram.initialize()
# 提取錯誤資訊
title = error_context.get("title", "Unknown Error")
culprit = error_context.get("culprit", "unknown")
level = error_context.get("level", "error")
# 發送 Sentry 告警卡片 (含 Y/n 按鈕)
await telegram.send_approval_card(
approval_id=approval_id,
risk_level="high" if level in ["fatal", "error"] else "medium",
resource_name=culprit,
root_cause=analysis.root_cause if analysis else title,
suggested_action=analysis.fix_suggestion if analysis else "待分析",
primary_responsibility="FE" if "tsx" in culprit or "jsx" in culprit else "BE",
confidence=analysis.confidence if analysis else 0.0,
namespace="sentry",
)
logger.info("sentry_telegram_sent", approval_id=approval_id)
except Exception as e:
logger.exception("sentry_telegram_failed", error=str(e))
# =============================================================================
# Approval 創建 (Phase 10: Sentry + OpenClaw)
# =============================================================================
async def create_sentry_approval(
error_context: dict,
analysis: ErrorAnalysisResult | None,
) -> str:
"""
為 Sentry 錯誤建立 Approval 記錄
Returns:
str: Approval ID
"""
try:
approval_service = get_approval_service()
# 決定風險等級
level = error_context.get("level", "error")
risk_level = SENTRY_LEVEL_TO_RISK.get(level, RiskLevel.MEDIUM)
# 組裝 Approval 請求
title = error_context.get("title", "Unknown Error")
culprit = error_context.get("culprit", "unknown")
project = error_context.get("project", "unknown")
issue_id = error_context.get("issue_id", "unknown")
approval_request = ApprovalRequestCreate(
source="sentry",
alert_type=f"sentry_{level}",
target_resource=culprit,
namespace=project,
risk_level=risk_level,
root_cause=analysis.root_cause if analysis else title,
suggestion=analysis.fix_suggestion if analysis else "待 AI 分析",
blast_radius=BlastRadius.SERVICE, # Sentry 錯誤通常影響單一服務
data_impact=DataImpact.READ_ONLY,
dry_run_check=DryRunCheck.PASSED,
llm_provider=analysis.analyzed_by if analysis else "pending",
llm_confidence=analysis.confidence if analysis else 0.0,
metadata={
"sentry_issue_id": issue_id,
"sentry_project": project,
"error_count": error_context.get("count", 1),
"first_seen": error_context.get("first_seen"),
"stacktrace": error_context.get("stacktrace", []),
},
)
# 創建 Approval
approval_id = str(uuid.uuid4())
await approval_service.create_approval(
approval_id=approval_id,
request=approval_request,
)
logger.info(
"sentry_approval_created",
approval_id=approval_id,
issue_id=issue_id,
risk_level=risk_level.value,
)
return approval_id
except Exception as e:
logger.exception("sentry_approval_creation_failed", error=str(e))
# 即使創建失敗,也返回一個臨時 ID
return f"temp-{uuid.uuid4().hex[:8]}"