fix(heartbeat): 改用 ADR-075 TYPE-1 格式 — 💚 INFO 樹狀結構
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 15m4s
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 15m4s
舊平鋪文字 → ├─/└─ 樹狀結構對齊 ACTION REQUIRED 卡片風格 - 標題: 💚/⚠️ INFO | AWOOOI 系統報告 - 加 ────── 分隔線 - AI/MCP/飛輪/基礎設施各節統一 ├─/└─ 格式 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -427,74 +427,84 @@ def report_to_telegram_html(report: HeartbeatReport) -> str:
|
|||||||
"""
|
"""
|
||||||
將 HeartbeatReport 轉換為 Telegram HTML 格式
|
將 HeartbeatReport 轉換為 Telegram HTML 格式
|
||||||
|
|
||||||
使用 <code> 區塊包住需要對齊的內容,確保等寬字型渲染正確。
|
ADR-075 TYPE-1 格式 (2026-04-12 ogt):
|
||||||
2026-04-12 ogt — 修正 Telegram 空格對齊跑版問題
|
💚 INFO | AWOOOI 系統報告 + ├─/└─ 樹狀結構
|
||||||
"""
|
"""
|
||||||
ts = report.timestamp.strftime("%Y-%m-%d %H:%M (台北)")
|
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 = [
|
lines = [
|
||||||
f"📊 <b>AWOOOI 系統心跳報告</b>",
|
f"{header_icon} <b>INFO | AWOOOI 系統報告</b>",
|
||||||
f"⏰ {ts}",
|
f"⏰ {ts}",
|
||||||
|
"──────────────────────",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
|
|
||||||
# --- AI 服務 ---
|
# --- 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("🤖 <b>AI 服務</b>")
|
lines.append("🤖 <b>AI 服務</b>")
|
||||||
ollama_probe = report.ai_services.get("ollama", ProbeResult(False, "❌ 無回應"))
|
lines.append(f"├─ Ollama: {ollama.status}{ollama_lat} <code>{html.escape(models_str)}</code>")
|
||||||
lat = f" ({ollama_probe.latency_ms:.0f}ms)" if ollama_probe.latency_ms else ""
|
lines.append(f"├─ Nemotron NIM: {nem.status}" + (f" {nem.latency_ms:.0f}ms" if nem.latency_ms else ""))
|
||||||
lines.append(f"Ollama: {ollama_probe.status}{lat}")
|
lines.append(f"├─ Gemini API: {gem.status}" + (f" {gem.latency_ms:.0f}ms" if gem.latency_ms else ""))
|
||||||
for model, loaded in report.ollama_models.items():
|
lines.append(f"└─ Claude API: {cla.status}" + (f" {cla.latency_ms:.0f}ms" if cla.latency_ms else ""))
|
||||||
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("")
|
lines.append("")
|
||||||
|
|
||||||
# --- MCP Provider ---
|
# --- MCP Provider ---
|
||||||
lines.append("🔌 <b>MCP Provider</b>")
|
k8s = report.mcp_providers.get("k8s", ProbeResult(False, "❌"))
|
||||||
for key, display in [("k8s", "K8s MCP"), ("ssh", "SSH MCP"), ("argocd", "ArgoCD MCP"), ("sentry", "Sentry MCP")]:
|
ssh = report.mcp_providers.get("ssh", ProbeResult(False, "❌"))
|
||||||
probe = report.mcp_providers.get(key, ProbeResult(False, "❌ 無回應"))
|
argocd_mcp = report.mcp_providers.get("argocd", ProbeResult(False, "❌"))
|
||||||
lines.append(f"{display}: {probe.status}")
|
sentry_mcp = report.mcp_providers.get("sentry", ProbeResult(False, "❌"))
|
||||||
|
|
||||||
|
lines.append("🔌 <b>MCP Provider</b>")
|
||||||
|
lines.append(f"├─ K8s: {k8s.status} SSH: {ssh.status}")
|
||||||
|
lines.append(f"└─ ArgoCD: {argocd_mcp.status} Sentry: {sentry_mcp.status}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# --- 飛輪狀態 ---
|
# --- 飛輪狀態 ---
|
||||||
fw = report.flywheel
|
fw = report.flywheel
|
||||||
lines.append("🔄 <b>飛輪狀態(24h)</b>")
|
|
||||||
lines.append(f"Playbooks: {fw.playbook_count} 個")
|
|
||||||
if fw.attempt_24h > 0:
|
if fw.attempt_24h > 0:
|
||||||
rate = int(fw.success_24h / fw.attempt_24h * 100)
|
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:
|
else:
|
||||||
lines.append("今日修復: 0 次")
|
repair_str = "0 次"
|
||||||
|
km_str = ""
|
||||||
if fw.km_total > 0:
|
if fw.km_total > 0:
|
||||||
vec_rate = int(fw.km_vectorized / fw.km_total * 100)
|
vec_rate = int(fw.km_vectorized / fw.km_total * 100)
|
||||||
icon = "✅" if vec_rate >= 90 else "⚠️"
|
km_icon = "✅" if vec_rate >= 90 else "⚠️"
|
||||||
lines.append(f"KM 向量化: {icon} {fw.km_vectorized}/{fw.km_total} ({vec_rate}%)")
|
km_str = f"KM: {km_icon} {fw.km_vectorized}/{fw.km_total} ({vec_rate}%)"
|
||||||
if fw.last_learning_at:
|
learn_str = f" 學習: {fw.last_learning_at.strftime('%H:%M')}" if fw.last_learning_at else ""
|
||||||
lines.append(f"最後學習: {fw.last_learning_at.strftime('%H:%M')}")
|
|
||||||
|
|
||||||
|
lines.append("🔄 <b>飛輪狀態(24h)</b>")
|
||||||
|
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("")
|
||||||
|
|
||||||
# --- 基礎設施 ---
|
# --- 基礎設施 ---
|
||||||
lines.append("🚀 <b>基礎設施</b>")
|
argocd = report.infra.get("argocd_sync", ProbeResult(False, "❌"))
|
||||||
argocd = report.infra.get("argocd_sync", ProbeResult(False, "❌ 無回應"))
|
velero = report.infra.get("velero", ProbeResult(False, "❌"))
|
||||||
velero = report.infra.get("velero", ProbeResult(False, "❌ 無回應"))
|
|
||||||
lines.append(f"ArgoCD: {argocd.status}")
|
|
||||||
lines.append(f"Velero 備份: {velero.status}")
|
|
||||||
|
|
||||||
# --- Warnings ---
|
lines.append("🚀 <b>基礎設施</b>")
|
||||||
|
lines.append(f"├─ ArgoCD: {argocd.status}")
|
||||||
|
lines.append(f"└─ Velero: {velero.status}")
|
||||||
|
|
||||||
|
# --- Warnings / 總結 ---
|
||||||
|
lines.append("")
|
||||||
if report.warnings:
|
if report.warnings:
|
||||||
lines.append("")
|
|
||||||
lines.append(f"⚠️ <b>需關注({len(report.warnings)} 項)</b>")
|
lines.append(f"⚠️ <b>需關注({len(report.warnings)} 項)</b>")
|
||||||
for w in report.warnings:
|
for w in report.warnings[:-1]:
|
||||||
lines.append(f"• {html.escape(w)}")
|
lines.append(f"├─ {html.escape(w)}")
|
||||||
|
lines.append(f"└─ {html.escape(report.warnings[-1])}")
|
||||||
else:
|
else:
|
||||||
lines.append("")
|
lines.append(f"✅ <b>{header_label}</b>")
|
||||||
lines.append("✅ <b>全部正常</b>")
|
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|||||||
Reference in New Issue
Block a user