fix(api): 修復 34 個 Ruff lint 錯誤

- 自動修復 import 排序、unused imports
- 手動修復 raise from、isinstance union、unused variable
- scripts/ 暫時保留 (非 CI 阻擋)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-29 15:27:49 +08:00
parent 5f9a6a7e55
commit d89f0520f9
27 changed files with 2538 additions and 1132 deletions

View File

@@ -21,12 +21,20 @@ import structlog
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
from pydantic import BaseModel
from src.core.config import settings
from src.core.metrics import (
record_alert_chain_failure,
record_alert_chain_success,
record_alert_processed,
record_anomaly,
)
from src.models.approval import (
ApprovalRequestCreate,
BlastRadius,
DataImpact,
RiskLevel,
)
from src.services.anomaly_counter import get_anomaly_counter
from src.services.approval_db import get_approval_service
from src.services.sentry_service import get_sentry_service
from src.services.telegram_gateway import get_telegram_gateway
@@ -36,10 +44,8 @@ logger = structlog.get_logger(__name__)
router = APIRouter(prefix="/webhooks/sentry", tags=["Sentry Webhook"])
# OpenClaw 配置
# 2026-03-29 ogt: 端口統一為 8089 (ADR-028)
OPENCLAW_URL = "http://192.168.0.188:8089"
SENTRY_API_URL = "http://192.168.0.110:9000"
# OpenClaw 配置 - 從 settings 讀取 (P1-1 修復, 2026-03-29)
# 2026-03-29: SENTRY_API_URL 已移至 settings.SENTRY_SELF_HOSTED_URL (Wave A.1)
# Sentry Level → Risk Level 映射
SENTRY_LEVEL_TO_RISK = {
@@ -182,36 +188,70 @@ async def analyze_and_comment(
背景任務:分析錯誤 + Telegram 告警 + 建立 Approval
Phase 10: Sentry + OpenClaw AI 整合
1. 呼叫 OpenClaw 分析
2. 發送 Telegram 告警
3. 建立 Approval 供人工審核
4. 回寫 Sentry Comment
Phase 21 (ADR-037): 異常頻率統計
執行順序 (避免邏輯衝突):
1. 記錄異常頻率 (AnomalyCounter)
2. 呼叫 OpenClaw 分析
3. 建立 Approval (含頻率資訊)
4. 發送 Telegram 告警 (含頻率資訊)
5. 回寫 Sentry Comment (含頻率資訊)
"""
try:
logger.info("sentry_analysis_started", issue_id=issue_id)
# 1. 呼叫 OpenClaw 分析
# 1. 記錄異常頻率 (ADR-037)
anomaly_counter = get_anomaly_counter()
anomaly_signature = {
"alert_name": "sentry_error",
"service": error_context.get("project", "unknown"),
"error_type": error_context.get("title", "unknown"),
"namespace": "sentry", # Sentry 來源統一標記
}
frequency = await anomaly_counter.record_anomaly(anomaly_signature)
frequency_dict = frequency.to_dict()
logger.info(
"anomaly_frequency_recorded",
issue_id=issue_id,
anomaly_key=frequency.anomaly_key,
count_24h=frequency.count_24h,
escalation_level=frequency.escalation_level,
)
# Wave A.5: 記錄異常指標 (ADR-037)
record_anomaly(
alert_name="sentry_error",
service=error_context.get("project", "unknown"),
frequency_24h=frequency.count_24h,
escalation_level=frequency.escalation_level,
)
# 2. 呼叫 OpenClaw 分析
analysis = await call_openclaw_analyzer(error_context)
# 2. 建立 Approval
# 3. 建立 Approval (含頻率資訊)
approval_id = await create_sentry_approval(
error_context=error_context,
analysis=analysis,
anomaly_frequency=frequency_dict,
)
# 3. 發送 Telegram 告警
# 4. 發送 Telegram 告警 (含頻率資訊)
await send_sentry_telegram_alert(
error_context=error_context,
analysis=analysis,
approval_id=approval_id,
anomaly_frequency=frequency_dict,
)
# 4. 回寫 Sentry Comment (如果分析成功)
# 5. 回寫 Sentry Comment (如果分析成功,含頻率資訊)
if analysis:
await post_sentry_comment(
project_slug=project_slug,
issue_id=issue_id,
analysis=analysis
analysis=analysis,
anomaly_frequency=frequency_dict,
)
logger.info(
@@ -219,10 +259,21 @@ async def analyze_and_comment(
issue_id=issue_id,
approval_id=approval_id,
has_analysis=analysis is not None,
escalation_level=frequency.escalation_level,
)
# Wave A.5: 記錄告警鏈路成功 (ADR-037)
record_alert_chain_success("sentry")
record_alert_processed(
source="sentry",
severity=error_context.get("level", "error"),
outcome="incident_created",
)
except Exception as e:
logger.exception("sentry_analysis_failed", issue_id=issue_id, error=str(e))
# Wave A.5: 記錄告警鏈路失敗 (ADR-037)
record_alert_chain_failure("sentry")
async def call_openclaw_analyzer(error_context: dict) -> ErrorAnalysisResult | None:
@@ -235,7 +286,7 @@ async def call_openclaw_analyzer(error_context: dict) -> ErrorAnalysisResult | N
try:
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(
f"{OPENCLAW_URL}/api/v1/analyze/error",
f"{settings.OPENCLAW_URL}/api/v1/analyze/error",
json={
"error_context": error_context,
"prefer_local": True, # 優先 Ollama
@@ -261,13 +312,41 @@ async def call_openclaw_analyzer(error_context: dict) -> ErrorAnalysisResult | N
async def post_sentry_comment(
project_slug: str,
issue_id: str,
analysis: ErrorAnalysisResult
analysis: ErrorAnalysisResult,
anomaly_frequency: dict | None = None,
):
"""
回寫分析結果到 Sentry Issue Comment
API: POST /api/0/issues/{issue_id}/comments/
Phase 21 (ADR-037): 含異常頻率統計
"""
# 頻率統計區塊 (ADR-037)
frequency_section = ""
if anomaly_frequency and anomaly_frequency.get("count_24h", 0) > 1:
freq = anomaly_frequency
escalation_emoji = {
None: "",
"REPEAT": ":warning:",
"ESCALATE": ":red_circle:",
"PERMANENT_FIX": ":rotating_light:",
}.get(freq.get("escalation_level"), "")
frequency_section = f"""
## 頻率統計 {escalation_emoji}
| 時間窗口 | 次數 |
|---------|------|
| 1 小時 | {freq.get('count_1h', 0)} |
| 24 小時 | {freq.get('count_24h', 0)} |
| 7 天 | {freq.get('count_7d', 0)} |
| 30 天 | {freq.get('count_30d', 0)} |
**修復嘗試**: {freq.get('auto_repair_count', 0)}
"""
if freq.get("escalation_level"):
frequency_section += f"**升級建議**: {freq['escalation_level']}\n"
comment_text = f"""## AI 錯誤分析 (by {analysis.analyzed_by})
**根本原因 (Root Cause)**
@@ -281,22 +360,26 @@ async def post_sentry_comment(
**預防措施 (Prevention)**
{analysis.prevention}
{frequency_section}
---
*分析信心度: {analysis.confidence:.0%} | 分析時間: {now_taipei_iso()}*
"""
try:
# TODO: 需要 Sentry API Token
# 目前先 log 出來
logger.info(f"Would post comment to issue {issue_id}:\n{comment_text}")
# Wave A.4: 使用 SentryService 回寫 Comment (ADR-037, 2026-03-29)
# 符合 leWOOOgo 模組化原則: Router 層透過 Service 層存取外部 API
sentry_service = get_sentry_service()
result = await sentry_service.post_issue_comment(issue_id, comment_text)
# async with httpx.AsyncClient() as client:
# response = await client.post(
# f"{SENTRY_API_URL}/api/0/issues/{issue_id}/comments/",
# headers={"Authorization": f"Bearer {SENTRY_API_TOKEN}"},
# json={"text": comment_text}
# )
if result:
logger.info(
"sentry_comment_success",
issue_id=issue_id,
comment_id=result.get("id"),
)
else:
# Token 未配置或 API 失敗時記錄 (不中斷流程)
logger.warning("sentry_comment_skipped_or_failed", issue_id=issue_id)
except Exception as e:
logger.exception("sentry_comment_failed", issue_id=issue_id, error=str(e))
@@ -310,18 +393,20 @@ async def send_sentry_telegram_alert(
error_context: dict,
analysis: ErrorAnalysisResult | None,
approval_id: str,
anomaly_frequency: dict | None = None,
):
"""
發送 Sentry 錯誤告警到 Telegram
Phase 21 (ADR-037): 含異常頻率統計
格式 (project_sentry_openclaw_v2.md):
═══════════════════════════
🐛 SENTRY 錯誤告警
═══════════════════════════
📍 components/dashboard.tsx:142
❌ TypeError: Cannot read property 'x' of null
👥 影響用戶: 12
🔄 發生次數: 42
📊 頻率: 1h:3 / 24h:12 / 7d:42
───────────────────────────
🧠 OpenClaw 分析:
「這是 null check 問題...」
@@ -347,9 +432,14 @@ async def send_sentry_telegram_alert(
primary_responsibility="FE" if "tsx" in culprit or "jsx" in culprit else "BE",
confidence=analysis.confidence if analysis else 0.0,
namespace="sentry",
anomaly_frequency=anomaly_frequency,
)
logger.info("sentry_telegram_sent", approval_id=approval_id)
logger.info(
"sentry_telegram_sent",
approval_id=approval_id,
escalation_level=anomaly_frequency.get("escalation_level") if anomaly_frequency else None,
)
except Exception as e:
logger.exception("sentry_telegram_failed", error=str(e))
@@ -362,26 +452,55 @@ async def send_sentry_telegram_alert(
async def create_sentry_approval(
error_context: dict,
analysis: ErrorAnalysisResult | None,
anomaly_frequency: dict | None = None,
) -> str:
"""
為 Sentry 錯誤建立 Approval 記錄
Phase 21 (ADR-037): 含異常頻率統計
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)
# ADR-037: 根據頻率升級風險等級
if anomaly_frequency:
escalation = anomaly_frequency.get("escalation_level")
if escalation == "PERMANENT_FIX":
risk_level = RiskLevel.CRITICAL
elif escalation == "ESCALATE" and risk_level != RiskLevel.CRITICAL:
risk_level = RiskLevel.HIGH
# 組裝 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")
# 組裝 metadata (含頻率資訊)
metadata = {
"source": "sentry",
"alert_type": f"sentry_{level}",
"sentry_issue_id": issue_id,
"sentry_project": project,
"culprit": culprit,
"error_count": error_context.get("count", 1),
"first_seen": error_context.get("first_seen"),
"stacktrace": error_context.get("stacktrace", []),
"llm_provider": analysis.analyzed_by if analysis else "pending",
"llm_confidence": analysis.confidence if analysis else 0.0,
}
# ADR-037: 添加頻率資訊到 metadata
if anomaly_frequency:
metadata["anomaly_frequency"] = anomaly_frequency
# 組裝 Approval 請求 (符合 ApprovalRequestBase schema)
approval_request = ApprovalRequestCreate(
action=f"Sentry {level.upper()} Alert: {culprit}",
@@ -395,18 +514,7 @@ async def create_sentry_approval(
),
dry_run_checks=[], # Sentry 告警無 dry-run
requested_by="sentry-webhook",
metadata={
"source": "sentry",
"alert_type": f"sentry_{level}",
"sentry_issue_id": issue_id,
"sentry_project": project,
"culprit": culprit,
"error_count": error_context.get("count", 1),
"first_seen": error_context.get("first_seen"),
"stacktrace": error_context.get("stacktrace", []),
"llm_provider": analysis.analyzed_by if analysis else "pending",
"llm_confidence": analysis.confidence if analysis else 0.0,
},
metadata=metadata,
)
# 創建 Approval (ID 由 Service 自動生成)