From a85e9ced08c60a93f6bf0f7c2aad1105a5cce0a6 Mon Sep 17 00:00:00 2001 From: OG T Date: Tue, 7 Apr 2026 12:17:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(api+telegram):=20Sprint=204=20Phase=20C+D?= =?UTF-8?q?=20=E2=80=94=20API=20=E7=AB=AF=E9=BB=9E=20+=20Telegram=20?= =?UTF-8?q?=E8=99=95=E7=BD=AE=E7=B5=B1=E8=A8=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase C: API 端點 - C1: GET /api/v1/stats/disposition — 完整處置分佈統計 - DispositionSummary: auto/human/manual/cold_start + auto_rate - DispositionByAnomaly: 按異常類型明細 (最多 20 筆) - Redis SCAN + HGETALL 聚合 - C2: GET /api/v1/auto-repair/stats 擴充 disposition_summary Phase D: Telegram 告警格式 - D1: 告警卡片加處置統計行 - 🤖 自動: N | 👤 審核: N | 🔧 手動: N - 自動化率百分比 - D2: 歷史按鈕強化處置分佈明細 - 完整 5 項計數 + 自動化率 Co-Authored-By: Claude Haiku 4.5 --- apps/api/src/api/v1/auto_repair.py | 20 ++++ apps/api/src/api/v1/stats.py | 125 ++++++++++++++++++++++ apps/api/src/services/telegram_gateway.py | 41 ++++++- 3 files changed, 183 insertions(+), 3 deletions(-) diff --git a/apps/api/src/api/v1/auto_repair.py b/apps/api/src/api/v1/auto_repair.py index dd5bd1e38..005cd27a2 100644 --- a/apps/api/src/api/v1/auto_repair.py +++ b/apps/api/src/api/v1/auto_repair.py @@ -190,12 +190,32 @@ async def get_auto_repair_stats() -> dict: total_executions = sum(p.total_executions for p in playbooks) total_success = sum(p.success_count for p in playbooks) + # 2026-04-07 Claude Code: Sprint 4 C2 — 加入處置分佈摘要 + disposition_summary = {"auto_repair": 0, "human_approved": 0, "manual_resolved": 0, "cold_start_trust": 0, "total": 0} + try: + from src.services.anomaly_counter import get_anomaly_counter + counter = get_anomaly_counter() + pattern = f"{counter.PREFIX_DISPOSITION}*" + async for key in counter.redis.scan_iter(match=pattern, count=100): + raw = await counter.redis.hgetall(key) + for field in disposition_summary: + disposition_summary[field] += int(raw.get(field.encode(), raw.get(field, 0))) + except Exception: + pass + + total_disp = disposition_summary["total"] + auto_cnt = disposition_summary["auto_repair"] + disposition_summary["cold_start_trust"] + return { "approved_playbooks": len(playbooks), "high_quality_playbooks": high_quality_count, "total_executions": total_executions, "overall_success_rate": total_success / total_executions if total_executions > 0 else 0.0, "auto_repair_eligible": high_quality_count > 0, + "disposition_summary": { + **disposition_summary, + "auto_rate": auto_cnt / total_disp if total_disp > 0 else 0.0, + }, } diff --git a/apps/api/src/api/v1/stats.py b/apps/api/src/api/v1/stats.py index 43f6fdd16..330e9bddb 100644 --- a/apps/api/src/api/v1/stats.py +++ b/apps/api/src/api/v1/stats.py @@ -401,3 +401,128 @@ async def trigger_weekly_report( "success": success, "message": "週報已發送" if success else "週報發送失敗", } + + +# ============================================================================= +# 2026-04-07 Claude Code: Sprint 4 C1 — 告警處置統計 +# ============================================================================= + + +class DispositionSummary(BaseModel): + """處置類型分佈""" + total: int = Field(default=0, description="總處置次數") + auto_repair: int = Field(default=0, description="自動修復次數") + human_approved: int = Field(default=0, description="人工審核批准次數") + manual_resolved: int = Field(default=0, description="手動處理次數") + cold_start_trust: int = Field(default=0, description="冷啟動信任次數") + auto_rate: float = Field(default=0.0, description="自動化率 (auto_repair + cold_start) / total") + human_rate: float = Field(default=0.0, description="人工介入率") + + +class DispositionByAnomaly(BaseModel): + """按異常類型的處置分佈""" + anomaly_key: str + alert_name: str = "" + disposition: DispositionSummary + + +class DispositionResponse(BaseModel): + """處置統計 API 回應""" + summary: DispositionSummary + by_anomaly: list[DispositionByAnomaly] = Field(default_factory=list) + + +@router.get( + "/disposition", + response_model=DispositionResponse, + summary="告警處置統計", +) +async def get_disposition_stats( + stats: StatsServiceDep, + days: int = Query(default=7, ge=1, le=90, description="統計天數"), +) -> DispositionResponse: + """ + 取得告警處置類型分佈統計。 + 2026-04-07 Claude Code: Sprint 4 C1 + + 包含: + - 總覽: 自動修復/人工審核/手動處理/冷啟動信任 各幾次 + - 自動化率 + - 按異常類型明細 + """ + try: + from src.services.anomaly_counter import get_anomaly_counter + counter = get_anomaly_counter() + + # 掃描所有活躍的 anomaly disposition keys + pattern = f"{counter.PREFIX_DISPOSITION}*" + keys = [] + async for key in counter.redis.scan_iter(match=pattern, count=100): + keys.append(key) + + total_summary = { + "auto_repair": 0, "human_approved": 0, + "manual_resolved": 0, "cold_start_trust": 0, "total": 0, + } + by_anomaly: list[DispositionByAnomaly] = [] + + for key in keys: + raw = await counter.redis.hgetall(key) + # Extract anomaly_key from Redis key + key_str = key.decode() if isinstance(key, bytes) else key + anomaly_key = key_str.replace(counter.PREFIX_DISPOSITION, "") + + d = { + "auto_repair": int(raw.get(b"auto_repair", raw.get("auto_repair", 0))), + "human_approved": int(raw.get(b"human_approved", raw.get("human_approved", 0))), + "manual_resolved": int(raw.get(b"manual_resolved", raw.get("manual_resolved", 0))), + "cold_start_trust": int(raw.get(b"cold_start_trust", raw.get("cold_start_trust", 0))), + "total": int(raw.get(b"total", raw.get("total", 0))), + } + + for k in total_summary: + total_summary[k] += d[k] + + # 嘗試取得 alert_name + alert_name = "" + try: + meta_key = f"{counter.PREFIX_METADATA}{anomaly_key}" + meta_raw = await counter.redis.hget(meta_key, "signature") + if meta_raw: + import json + sig = json.loads(meta_raw) + alert_name = sig.get("alert_name", "") + except Exception: + pass + + if d["total"] > 0: + auto_cnt = d["auto_repair"] + d["cold_start_trust"] + by_anomaly.append(DispositionByAnomaly( + anomaly_key=anomaly_key, + alert_name=alert_name, + disposition=DispositionSummary( + **d, + auto_rate=auto_cnt / d["total"] if d["total"] > 0 else 0, + human_rate=d["human_approved"] / d["total"] if d["total"] > 0 else 0, + ), + )) + + # Sort by total descending + by_anomaly.sort(key=lambda x: x.disposition.total, reverse=True) + + total = total_summary["total"] + auto_cnt = total_summary["auto_repair"] + total_summary["cold_start_trust"] + + return DispositionResponse( + summary=DispositionSummary( + **total_summary, + auto_rate=auto_cnt / total if total > 0 else 0, + human_rate=total_summary["human_approved"] / total if total > 0 else 0, + ), + by_anomaly=by_anomaly[:20], # 最多回傳 20 筆 + ) + + except Exception as e: + import structlog + structlog.get_logger(__name__).warning("disposition_stats_error", error=str(e)) + return DispositionResponse(summary=DispositionSummary()) diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index 8887dd29e..5b73575af 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -240,12 +240,32 @@ class TelegramMessage: "PERMANENT_FIX": "🚨", }.get(freq.get("escalation_level"), "") + # 2026-04-07 Claude Code: Sprint 4 D1 — 處置統計行 + auto_r = freq.get("auto_repair_count", 0) + human_a = freq.get("human_approved_count", 0) + manual_r = freq.get("manual_resolved_count", 0) + cold_s = freq.get("cold_start_trust_count", 0) + total_res = freq.get("total_resolution_count", 0) + + # 處置分佈行 (只在有處置紀錄時顯示) + disposition_line = "" + if total_res > 0: + auto_total = auto_r + cold_s + auto_rate = int(auto_total / total_res * 100) if total_res > 0 else 0 + disposition_line = ( + f"├ 🤖 自動: {auto_total}" + f" | 👤 審核: {human_a}" + f" | 🔧 手動: {manual_r}\n" + f"├ 自動化率: {auto_rate}%\n" + ) + frequency_block = ( f"━━━━━━━━━━━━━━━━━━━\n" f"📊 頻率統計 {escalation_emoji}\n" - f"├ 1h: {freq.get('count_1h', 0)} 次\n" - f"├ 24h: {freq.get('count_24h', 0)} 次\n" - f"└ 修復: {freq.get('auto_repair_count', 0)} 次\n" + f"├ 1h: {freq.get('count_1h', 0)} 次" + f" | 24h: {freq.get('count_24h', 0)} 次\n" + f"{disposition_line}" + f"└ 累計修復: {auto_r} 次\n" ) if freq.get("escalation_level"): frequency_block += f"🔺 升級: {freq['escalation_level']}\n" @@ -2550,6 +2570,21 @@ class TelegramGateway: if fs.last_repair_action: lines.append(f" 最後動作: {html.escape(fs.last_repair_action[:80])}") + # 2026-04-07 Claude Code: Sprint 4 D2 — 處置分佈明細 + total_res = fs.total_resolution_count + if total_res > 0: + auto_total = fs.auto_repair_count + fs.cold_start_trust_count + auto_rate = int(auto_total / total_res * 100) if total_res > 0 else 0 + lines += [ + f"", + f"📋 處置分佈 (共 {total_res} 次)", + f" 🤖 自動修復: {fs.auto_repair_count}", + f" ❄️ 冷啟動信任: {fs.cold_start_trust_count}", + f" 👤 人工審核: {fs.human_approved_count}", + f" 🔧 手動處理: {fs.manual_resolved_count}", + f" 📈 自動化率: {auto_rate}%", + ] + await self.send_notification("\n".join(lines)) except Exception as e: