diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py
index d81bf2de..afe2a34c 100644
--- a/apps/api/src/services/telegram_gateway.py
+++ b/apps/api/src/services/telegram_gateway.py
@@ -3621,10 +3621,27 @@ class WeeklyReportMessage:
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'}"
+ f"統計={'正常' if self.stats_source_ok else '失效'} / "
+ f"K3s={'正常' if self.k3s_source_ok else '失效'} / "
+ f"Git={'正常' if self.git_source_ok else '失效'} / "
+ f"成本={'正常' if self.cost_source_ok else '缺資料'}"
+ )
+ source_gaps: list[str] = []
+ if not self.stats_source_ok:
+ source_gaps.append("告警 / AI 統計資料源失效:建立 report-source-gap:stats_api")
+ if not self.k3s_source_ok:
+ source_gaps.append("K3s 指標資料源失效:建立 report-source-gap:k3s_metrics")
+ if not self.git_source_ok:
+ source_gaps.append("開發活動資料源失效:建立 report-source-gap:gitea_activity")
+ if not self.cost_source_ok:
+ source_gaps.append("AI 成本資料源缺資料:建立 report-source-gap:ai_cost_ledger")
+ if actionable_all_zero:
+ source_gaps.insert(0, "全 0 不是健康:必須追查資料鏈路 freshness / confidence")
+ if not source_gaps:
+ source_gaps.append("資料源通過;持續比對趨勢、異常與修復沉澱")
+ gap_lines = "\n".join(
+ f"├ {html.escape(item)}" if index < len(source_gaps) - 1 else f"└ {html.escape(item)}"
+ for index, item in enumerate(source_gaps[:5])
)
message = (
@@ -3662,6 +3679,10 @@ class WeeklyReportMessage:
f"💰 AI 成本\n"
f"├ 費用: ${self.ai_cost_week:.2f}\n"
f"└ Tokens: {self.ai_tokens_week:,}\n"
+ f"━━━━━━━━━━━━━━━━━━━\n"
+ f"🧩 資料缺口 / 下一步\n"
+ f"{gap_lines}\n"
+ f"只讀判讀:不自動改排程、不直接發修復、不取代人工批准。\n"
)
# Sprint 4 F1: 處置分佈(有資料才加)
@@ -3678,7 +3699,7 @@ class WeeklyReportMessage:
f"└ 自動化率: {auto_rate}%"
)
- return message[:1800]
+ return message[:2400]
@dataclass
diff --git a/apps/api/src/services/weekly_report_service.py b/apps/api/src/services/weekly_report_service.py
index a484423d..b6b5f9e5 100644
--- a/apps/api/src/services/weekly_report_service.py
+++ b/apps/api/src/services/weekly_report_service.py
@@ -106,6 +106,13 @@ class WeeklyReportService:
timeout=10,
cwd="/app", # K8s 容器內的工作目錄
)
+ if result.returncode != 0:
+ logger.warning(
+ "git_stats_commits_failed",
+ returncode=result.returncode,
+ stderr=result.stderr[-300:],
+ )
+ return 0, 0, False
commits = len(result.stdout.strip().split("\n")) if result.stdout.strip() else 0
# 取得部署次數 (計算含 "deploy" 或 "CD" 的 commits)
@@ -116,6 +123,13 @@ class WeeklyReportService:
timeout=10,
cwd="/app",
)
+ if result_deploy.returncode != 0:
+ logger.warning(
+ "git_stats_deploys_failed",
+ returncode=result_deploy.returncode,
+ stderr=result_deploy.stderr[-300:],
+ )
+ return commits, 0, False
deploys = len(result_deploy.stdout.strip().split("\n")) if result_deploy.stdout.strip() else 0
return commits, deploys, True
diff --git a/apps/api/tests/test_report_generation_service.py b/apps/api/tests/test_report_generation_service.py
index 532a9707..b23b495e 100644
--- a/apps/api/tests/test_report_generation_service.py
+++ b/apps/api/tests/test_report_generation_service.py
@@ -31,10 +31,66 @@ from src.services.report_generation_service import (
ReportGenerationService,
_seconds_until_next_report,
)
+from src.services import weekly_report_service as weekly_report_module
+from src.services.weekly_report_service import WeeklyReportService
_TZ_TAIPEI = timezone(timedelta(hours=8))
+# =============================================================================
+# WeeklyReportService Git 資料源可信度
+# =============================================================================
+
+
+class TestWeeklyReportGitStats:
+ """週報不能把 Git 資料源失敗偽裝成 0 commits / 0 deploys。"""
+
+ def test_git_log_failure_marks_source_failed(self, monkeypatch):
+ class Result:
+ returncode = 128
+ stdout = ""
+ stderr = "fatal: not a git repository"
+
+ monkeypatch.setattr(
+ weekly_report_module.subprocess,
+ "run",
+ lambda *args, **kwargs: Result(),
+ )
+
+ svc = WeeklyReportService(stats_service=object(), k3s_monitor=object())
+ commits, deploys, source_ok = svc._get_git_stats(datetime.now(_TZ_TAIPEI))
+
+ assert commits == 0
+ assert deploys == 0
+ assert source_ok is False
+
+ def test_git_deploy_log_failure_marks_source_failed_after_commits(self, monkeypatch):
+ class CommitResult:
+ returncode = 0
+ stdout = "abc123 feat: one\nbcd234 fix: two\n"
+ stderr = ""
+
+ class DeployResult:
+ returncode = 128
+ stdout = ""
+ stderr = "fatal: bad revision"
+
+ calls = []
+
+ def fake_run(*args, **kwargs):
+ calls.append(args[0])
+ return CommitResult() if len(calls) == 1 else DeployResult()
+
+ monkeypatch.setattr(weekly_report_module.subprocess, "run", fake_run)
+
+ svc = WeeklyReportService(stats_service=object(), k3s_monitor=object())
+ commits, deploys, source_ok = svc._get_git_stats(datetime.now(_TZ_TAIPEI))
+
+ assert commits == 2
+ assert deploys == 0
+ assert source_ok is False
+
+
# =============================================================================
# DailyKpi 計算屬性
# =============================================================================
diff --git a/apps/api/tests/test_telegram_message_templates.py b/apps/api/tests/test_telegram_message_templates.py
index dac27d7d..4ec9eb43 100644
--- a/apps/api/tests/test_telegram_message_templates.py
+++ b/apps/api/tests/test_telegram_message_templates.py
@@ -265,10 +265,14 @@ def test_weekly_report_marks_all_zero_as_low_trust_anomaly() -> None:
assert "報表資料信任度" in body
assert "判定: 低可信" in body
- assert "Stats=fail" in body
- assert "Git=fail" in body
- assert "Cost=missing" in body
+ assert "統計=失效" in body
+ assert "Git=失效" in body
+ assert "成本=缺資料" in body
assert "全 0: actionable_anomaly" in body
+ assert "資料缺口 / 下一步" in body
+ assert "全 0 不是健康" in body
+ assert "report-source-gap:stats_api" in body
+ assert "report-source-gap:gitea_activity" in body
assert "告警統計" in body
@@ -291,6 +295,7 @@ def test_weekly_report_keeps_nonzero_source_status_visible() -> None:
assert "判定: 可參考" in body
assert "全 0: no" in body
+ assert "資料源通過" in body
assert "Commits: 5" in body
assert "Tokens: 1,200" in body