diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index 8a3a855b..267e9693 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -3168,6 +3168,11 @@ class WeeklyReportMessage: disposition_manual: int = 0 disposition_cold_start: int = 0 disposition_total: int = 0 + stats_source_ok: bool = True + k3s_source_ok: bool = True + git_source_ok: bool = True + cost_source_ok: bool = False + all_zero_actionable_anomaly: bool = False def format(self) -> str: """格式化為 Telegram HTML""" @@ -3175,6 +3180,29 @@ class WeeklyReportMessage: alert_health = "✅" if self.resolved_rate >= 80 else "⚠️" ai_health = "✅" if self.ai_success_rate >= 70 else "⚠️" k3s_health = "✅" if self.k3s_uptime_pct >= 99 else "⚠️" + source_ok_count = sum([ + self.stats_source_ok, + self.k3s_source_ok, + self.git_source_ok, + self.cost_source_ok, + ]) + all_zero = ( + self.alert_total == 0 + and self.ai_proposal_count == 0 + and self.ai_executed_count == 0 + and self.commits_count == 0 + and self.deploy_count == 0 + and self.ai_tokens_week == 0 + and self.disposition_total == 0 + ) + actionable_all_zero = self.all_zero_actionable_anomaly or all_zero + report_trust = "低可信" if actionable_all_zero or source_ok_count < 4 else "可參考" + source_status = ( + f"Stats={'ok' if self.stats_source_ok else 'fail'} / " + f"K3s={'ok' if self.k3s_source_ok else 'fail'} / " + f"Git={'ok' if self.git_source_ok else 'fail'} / " + f"Cost={'ok' if self.cost_source_ok else 'missing'}" + ) message = ( f"═══════════════════════════\n" @@ -3182,6 +3210,11 @@ class WeeklyReportMessage: f"═══════════════════════════\n" f"📅 {html.escape(self.week_range)} | {html.escape(self.report_date)}\n" f"━━━━━━━━━━━━━━━━━━━\n" + f"🧭 報表資料信任度\n" + f"├ 判定: {report_trust}\n" + f"├ 來源: {html.escape(source_status)}\n" + f"└ 全 0: {'actionable_anomaly' if actionable_all_zero else 'no'}\n" + f"━━━━━━━━━━━━━━━━━━━\n" f"{alert_health} 告警統計\n" f"├ 總數: {self.alert_total}\n" f"├ Critical: {self.alert_critical}\n" @@ -3222,7 +3255,7 @@ class WeeklyReportMessage: f"└ 自動化率: {auto_rate}%" ) - return message[:1200] + return message[:1800] @dataclass diff --git a/apps/api/src/services/weekly_report_service.py b/apps/api/src/services/weekly_report_service.py index 568c5a45..a484423d 100644 --- a/apps/api/src/services/weekly_report_service.py +++ b/apps/api/src/services/weekly_report_service.py @@ -94,7 +94,7 @@ class WeeklyReportService: return week_range, monday, sunday - def _get_git_stats(self, since: datetime) -> tuple[int, int]: + def _get_git_stats(self, since: datetime) -> tuple[int, int, bool]: """取得 Git 統計 (commits, deploys)""" try: # 取得本週 commits 數量 @@ -118,10 +118,10 @@ class WeeklyReportService: ) deploys = len(result_deploy.stdout.strip().split("\n")) if result_deploy.stdout.strip() else 0 - return commits, deploys + return commits, deploys, True except Exception as e: logger.warning("git_stats_failed", error=str(e)) - return 0, 0 + return 0, 0, False async def generate_report(self) -> WeeklyReportMessage: """ @@ -134,25 +134,29 @@ class WeeklyReportService: report_date = now.strftime("%Y-%m-%d %H:%M") # 取得統計數據 (7 天) + stats_source_ok = True try: incident_summary = await self._stats_service.get_incident_summary(days=7) resolution_stats = await self._stats_service.get_resolution_stats(days=7) ai_performance = await self._stats_service.get_ai_performance(days=7) except Exception as e: logger.warning("stats_fetch_failed", error=str(e)) + stats_source_ok = False incident_summary = {} resolution_stats = {} ai_performance = {} # 取得 K3s 狀態 + k3s_source_ok = True try: k3s_status = await self._k3s_monitor.collect_cluster_status() except Exception as e: logger.warning("k3s_fetch_failed", error=str(e)) + k3s_source_ok = False k3s_status = None # 取得 Git 統計 - commits, deploys = self._get_git_stats(monday) + commits, deploys, git_source_ok = self._get_git_stats(monday) # 計算指標 total_incidents = incident_summary.get("total_incidents", 0) @@ -216,6 +220,18 @@ class WeeklyReportService: disposition_manual=disp_manual, disposition_cold_start=disp_cold, disposition_total=disp_total, + stats_source_ok=stats_source_ok, + k3s_source_ok=k3s_source_ok, + git_source_ok=git_source_ok, + cost_source_ok=False, + all_zero_actionable_anomaly=( + total_incidents == 0 + and ai_proposals == 0 + and ai_executed == 0 + and commits == 0 + and deploys == 0 + and disp_total == 0 + ), ) logger.info( diff --git a/apps/api/tests/test_telegram_message_templates.py b/apps/api/tests/test_telegram_message_templates.py index d7e5888e..bb9ee64a 100644 --- a/apps/api/tests/test_telegram_message_templates.py +++ b/apps/api/tests/test_telegram_message_templates.py @@ -18,6 +18,7 @@ from src.services.telegram_gateway import ( SentryErrorMessage, TelegramGateway, TelegramMessage, + WeeklyReportMessage, ) @@ -53,6 +54,51 @@ def test_auto_repair_status_line_distinguishes_auto_resolved() -> None: assert "CPU 92%->30%" in result +def test_weekly_report_marks_all_zero_as_low_trust_anomaly() -> None: + report = WeeklyReportMessage( + week_range="2026-W24", + report_date="2026-06-12 10:00", + stats_source_ok=False, + k3s_source_ok=True, + git_source_ok=False, + cost_source_ok=False, + all_zero_actionable_anomaly=True, + ) + + body = report.format() + + assert "報表資料信任度" in body + assert "判定: 低可信" in body + assert "Stats=fail" in body + assert "Git=fail" in body + assert "Cost=missing" in body + assert "全 0: actionable_anomaly" in body + assert "告警統計" in body + + +def test_weekly_report_keeps_nonzero_source_status_visible() -> None: + report = WeeklyReportMessage( + week_range="2026-W24", + report_date="2026-06-12 10:00", + alert_total=3, + ai_proposal_count=2, + commits_count=5, + deploy_count=1, + ai_tokens_week=1200, + stats_source_ok=True, + k3s_source_ok=True, + git_source_ok=True, + cost_source_ok=True, + ) + + body = report.format() + + assert "判定: 可參考" in body + assert "全 0: no" in body + assert "Commits: 5" in body + assert "Tokens: 1,200" in body + + def test_telegram_html_chunks_preserve_complete_lines() -> None: """歷史/詳情長訊息不得用 text[:500] 切壞 HTML tag。""" lines = [