diff --git a/apps/api/src/services/heartbeat_report_service.py b/apps/api/src/services/heartbeat_report_service.py index 52de5210..6fd513dd 100644 --- a/apps/api/src/services/heartbeat_report_service.py +++ b/apps/api/src/services/heartbeat_report_service.py @@ -340,12 +340,12 @@ class HeartbeatReportService: stats.km_total = km_total or 0 # KM 向量化數(embedding IS NOT NULL) - # 注意:knowledge_entries 無 vectorized 欄位,用 embedding 判斷 - vec_count = await db.scalar( - select(func.count()).select_from(KnowledgeEntryRecord) - .where(KnowledgeEntryRecord.embedding.isnot(None)) + # KnowledgeEntryRecord ORM 無 embedding 欄位,改用 raw SQL + from sqlalchemy import text as sa_text + vec_result = await db.execute( + sa_text("SELECT COUNT(*) FROM knowledge_entries WHERE embedding IS NOT NULL") ) - stats.km_vectorized = vec_count or 0 + stats.km_vectorized = vec_result.scalar() or 0 # 24h 修復統計 since = datetime.utcnow() - timedelta(hours=24) @@ -441,50 +441,42 @@ def report_to_telegram_html(report: HeartbeatReport) -> str: # --- AI 服務 --- lines.append("🤖 AI 服務") ollama_probe = report.ai_services.get("ollama", ProbeResult(False, "❌ 無回應")) - latency_str = f" ({ollama_probe.latency_ms:.0f}ms)" if ollama_probe.latency_ms else "" - lines.append(f" Ollama: {ollama_probe.status}{latency_str}") - - # 各模型狀態(縮排顯示) + 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 "❌" - short = model.split(":")[0] - lines.append(f" {icon} {html.escape(short)}") + 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, "❌ 無回應")) - latency_str = f" ({probe.latency_ms:.0f}ms)" if probe.latency_ms else "" - lines.append(f" {display:<18}{probe.status}{latency_str}") + lat = f" ({probe.latency_ms:.0f}ms)" if probe.latency_ms else "" + lines.append(f"{display}: {probe.status}{lat}") lines.append("") # --- MCP Provider --- lines.append("🔌 MCP Provider") - mcp_display = { - "k8s": "K8s MCP", - "ssh": "SSH MCP", - "argocd": "ArgoCD MCP", - "sentry": "Sentry MCP", - } - for key, display in mcp_display.items(): + 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:<18}{probe.status}") + lines.append(f"{display}: {probe.status}") lines.append("") # --- 飛輪狀態 --- fw = report.flywheel lines.append("🔄 飛輪狀態(24h)") - lines.append(f" Playbooks: {fw.playbook_count} 個") + 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}%)") + lines.append(f"今日修復: {fw.success_24h}/{fw.attempt_24h} 次 ({rate}%)") else: - lines.append(f" 今日修復: 0 次") + lines.append("今日修復: 0 次") if fw.km_total > 0: vec_rate = int(fw.km_vectorized / fw.km_total * 100) - lines.append(f" KM 向量化: {fw.km_vectorized}/{fw.km_total} ({vec_rate}%)") + 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')}") + lines.append(f"最後學習: {fw.last_learning_at.strftime('%H:%M')}") lines.append("") @@ -492,15 +484,15 @@ def report_to_telegram_html(report: HeartbeatReport) -> str: 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}") + lines.append(f"ArgoCD: {argocd.status}") + lines.append(f"Velero 備份: {velero.status}") # --- Warnings --- if report.warnings: lines.append("") lines.append(f"⚠️ 需關注({len(report.warnings)} 項)") for w in report.warnings: - lines.append(f" - {html.escape(w)}") + lines.append(f"• {html.escape(w)}") else: lines.append("") lines.append("✅ 全部正常")