From 38ff2bb7a59d543d4b1f6b300dd79f6e9aeccb12 Mon Sep 17 00:00:00 2001 From: OG T Date: Sun, 12 Apr 2026 22:52:05 +0800 Subject: [PATCH] =?UTF-8?q?fix(heartbeat):=20=E6=94=B9=E7=94=A8=20ADR-075?= =?UTF-8?q?=20TYPE-1=20=E6=A0=BC=E5=BC=8F=20=E2=80=94=20=F0=9F=92=9A=20INF?= =?UTF-8?q?O=20=E6=A8=B9=E7=8B=80=E7=B5=90=E6=A7=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 舊平鋪文字 → ├─/└─ 樹狀結構對齊 ACTION REQUIRED 卡片風格 - 標題: 💚/⚠️ INFO | AWOOOI 系統報告 - 加 ────── 分隔線 - AI/MCP/飛輪/基礎設施各節統一 ├─/└─ 格式 Co-Authored-By: Claude Sonnet 4.6 --- .../src/services/heartbeat_report_service.py | 86 +++++++++++-------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/apps/api/src/services/heartbeat_report_service.py b/apps/api/src/services/heartbeat_report_service.py index 6fd513dd..23229beb 100644 --- a/apps/api/src/services/heartbeat_report_service.py +++ b/apps/api/src/services/heartbeat_report_service.py @@ -427,74 +427,84 @@ def report_to_telegram_html(report: HeartbeatReport) -> str: """ 將 HeartbeatReport 轉換為 Telegram HTML 格式 - 使用 區塊包住需要對齊的內容,確保等寬字型渲染正確。 - 2026-04-12 ogt — 修正 Telegram 空格對齊跑版問題 + ADR-075 TYPE-1 格式 (2026-04-12 ogt): + 💚 INFO | AWOOOI 系統報告 + ├─/└─ 樹狀結構 """ ts = report.timestamp.strftime("%Y-%m-%d %H:%M (台北)") + overall_ok = not report.warnings + + header_icon = "💚" if overall_ok else "⚠️" + header_label = "全系統正常" if overall_ok else f"需關注 {len(report.warnings)} 項" lines = [ - f"📊 AWOOOI 系統心跳報告", + f"{header_icon} INFO | AWOOOI 系統報告", f"⏰ {ts}", + "──────────────────────", "", ] # --- AI 服務 --- + ollama = report.ai_services.get("ollama", ProbeResult(False, "❌")) + ollama_lat = f" {ollama.latency_ms:.0f}ms" if ollama.latency_ms else "" + models_ok = [m.split(":")[0] for m, ok in report.ollama_models.items() if ok] + models_str = " / ".join(models_ok) if models_ok else "無模型" + nem = report.ai_services.get("nemotron", ProbeResult(False, "❌")) + gem = report.ai_services.get("gemini", ProbeResult(False, "❌")) + cla = report.ai_services.get("claude", ProbeResult(False, "❌")) + lines.append("🤖 AI 服務") - ollama_probe = report.ai_services.get("ollama", ProbeResult(False, "❌ 無回應")) - lat = f" ({ollama_probe.latency_ms:.0f}ms)" if ollama_probe.latency_ms else "" - lines.append(f"Ollama: {ollama_probe.status}{lat}") - for model, loaded in report.ollama_models.items(): - icon = "✅" if loaded else "❌" - lines.append(f" {icon} {html.escape(model.split(':')[0])}") - - for svc_name, display in [("nemotron", "Nemotron NIM"), ("gemini", "Gemini API"), ("claude", "Claude API")]: - probe = report.ai_services.get(svc_name, ProbeResult(False, "❌ 無回應")) - lat = f" ({probe.latency_ms:.0f}ms)" if probe.latency_ms else "" - lines.append(f"{display}: {probe.status}{lat}") - + lines.append(f"├─ Ollama: {ollama.status}{ollama_lat} {html.escape(models_str)}") + lines.append(f"├─ Nemotron NIM: {nem.status}" + (f" {nem.latency_ms:.0f}ms" if nem.latency_ms else "")) + lines.append(f"├─ Gemini API: {gem.status}" + (f" {gem.latency_ms:.0f}ms" if gem.latency_ms else "")) + lines.append(f"└─ Claude API: {cla.status}" + (f" {cla.latency_ms:.0f}ms" if cla.latency_ms else "")) lines.append("") # --- MCP Provider --- - lines.append("🔌 MCP Provider") - for key, display in [("k8s", "K8s MCP"), ("ssh", "SSH MCP"), ("argocd", "ArgoCD MCP"), ("sentry", "Sentry MCP")]: - probe = report.mcp_providers.get(key, ProbeResult(False, "❌ 無回應")) - lines.append(f"{display}: {probe.status}") + k8s = report.mcp_providers.get("k8s", ProbeResult(False, "❌")) + ssh = report.mcp_providers.get("ssh", ProbeResult(False, "❌")) + argocd_mcp = report.mcp_providers.get("argocd", ProbeResult(False, "❌")) + sentry_mcp = report.mcp_providers.get("sentry", ProbeResult(False, "❌")) + lines.append("🔌 MCP Provider") + lines.append(f"├─ K8s: {k8s.status} SSH: {ssh.status}") + lines.append(f"└─ ArgoCD: {argocd_mcp.status} Sentry: {sentry_mcp.status}") lines.append("") # --- 飛輪狀態 --- fw = report.flywheel - lines.append("🔄 飛輪狀態(24h)") - lines.append(f"Playbooks: {fw.playbook_count} 個") if fw.attempt_24h > 0: rate = int(fw.success_24h / fw.attempt_24h * 100) - lines.append(f"今日修復: {fw.success_24h}/{fw.attempt_24h} 次 ({rate}%)") + repair_str = f"{fw.success_24h}/{fw.attempt_24h} ({rate}%)" else: - lines.append("今日修復: 0 次") + repair_str = "0 次" + km_str = "" if fw.km_total > 0: vec_rate = int(fw.km_vectorized / fw.km_total * 100) - icon = "✅" if vec_rate >= 90 else "⚠️" - lines.append(f"KM 向量化: {icon} {fw.km_vectorized}/{fw.km_total} ({vec_rate}%)") - if fw.last_learning_at: - lines.append(f"最後學習: {fw.last_learning_at.strftime('%H:%M')}") + km_icon = "✅" if vec_rate >= 90 else "⚠️" + km_str = f"KM: {km_icon} {fw.km_vectorized}/{fw.km_total} ({vec_rate}%)" + learn_str = f" 學習: {fw.last_learning_at.strftime('%H:%M')}" if fw.last_learning_at else "" + lines.append("🔄 飛輪狀態(24h)") + lines.append(f"├─ Playbooks: {fw.playbook_count} 修復: {repair_str}") + lines.append(f"└─ {km_str}{learn_str}" if km_str or learn_str else "└─ KM 統計不可用") lines.append("") # --- 基礎設施 --- - lines.append("🚀 基礎設施") - argocd = report.infra.get("argocd_sync", ProbeResult(False, "❌ 無回應")) - velero = report.infra.get("velero", ProbeResult(False, "❌ 無回應")) - lines.append(f"ArgoCD: {argocd.status}") - lines.append(f"Velero 備份: {velero.status}") + argocd = report.infra.get("argocd_sync", ProbeResult(False, "❌")) + velero = report.infra.get("velero", ProbeResult(False, "❌")) - # --- Warnings --- + lines.append("🚀 基礎設施") + lines.append(f"├─ ArgoCD: {argocd.status}") + lines.append(f"└─ Velero: {velero.status}") + + # --- Warnings / 總結 --- + lines.append("") if report.warnings: - lines.append("") lines.append(f"⚠️ 需關注({len(report.warnings)} 項)") - for w in report.warnings: - lines.append(f"• {html.escape(w)}") + for w in report.warnings[:-1]: + lines.append(f"├─ {html.escape(w)}") + lines.append(f"└─ {html.escape(report.warnings[-1])}") else: - lines.append("") - lines.append("✅ 全部正常") + lines.append(f"✅ {header_label}") return "\n".join(lines)