diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py
index dc595622..33b9d692 100644
--- a/apps/api/src/services/telegram_gateway.py
+++ b/apps/api/src/services/telegram_gateway.py
@@ -28,7 +28,7 @@ import html
import json
import os
import re
-from dataclasses import dataclass
+from dataclasses import dataclass, field
from datetime import UTC, datetime
from uuid import NAMESPACE_URL, UUID, uuid5
@@ -3888,6 +3888,11 @@ class WeeklyReportMessage:
git_source_ok: bool = True
cost_source_ok: bool = False
all_zero_actionable_anomaly: bool = False
+ report_source_confidence_percent: int = 0
+ report_source_ok_count: int = 0
+ report_source_total_count: int = 0
+ report_source_gap_ids: list[str] = field(default_factory=list)
+ report_asset_state_lines: list[str] = field(default_factory=list)
def format(self) -> str:
"""格式化為 Telegram HTML"""
@@ -3935,6 +3940,22 @@ class WeeklyReportMessage:
f"├ {html.escape(item)}" if index < len(source_gaps) - 1 else f"└ {html.escape(item)}"
for index, item in enumerate(source_gaps[:5])
)
+ source_health_block = ""
+ if self.report_source_total_count > 0:
+ gap_text = ", ".join(self.report_source_gap_ids[:4]) if self.report_source_gap_ids else "none"
+ asset_lines = self.report_asset_state_lines[:5] or ["尚未讀到資產沉澱 read model"]
+ formatted_assets = "\n".join(
+ f"├ {html.escape(item)}" if index < len(asset_lines) - 1 else f"└ {html.escape(item)}"
+ for index, item in enumerate(asset_lines)
+ )
+ source_health_block = (
+ f"━━━━━━━━━━━━━━━━━━━\n"
+ f"🧾 報表資料源 / 沉澱\n"
+ f"├ 來源: {self.report_source_ok_count}/{self.report_source_total_count} "
+ f"| 信心: {self.report_source_confidence_percent}%\n"
+ f"├ 缺口: {html.escape(gap_text)}\n"
+ f"{formatted_assets}\n"
+ )
message = (
f"═══════════════════════════\n"
@@ -3974,6 +3995,7 @@ class WeeklyReportMessage:
f"━━━━━━━━━━━━━━━━━━━\n"
f"🧩 資料缺口 / 下一步\n"
f"{gap_lines}\n"
+ f"{source_health_block}"
f"只讀判讀:不自動改排程、不直接發修復、不取代人工批准。\n"
)
diff --git a/apps/api/src/services/weekly_report_service.py b/apps/api/src/services/weekly_report_service.py
index b6b5f9e5..d2905eb1 100644
--- a/apps/api/src/services/weekly_report_service.py
+++ b/apps/api/src/services/weekly_report_service.py
@@ -210,6 +210,35 @@ class WeeklyReportService:
except Exception as _disp_e:
logger.warning("weekly_report_disposition_failed", error=str(_disp_e))
+ report_source_confidence = 0
+ report_source_ok = 0
+ report_source_total = 0
+ report_source_gap_ids: list[str] = []
+ report_asset_state_lines: list[str] = []
+ try:
+ from src.services.ai_agent_report_source_health import build_ai_agent_report_source_health
+
+ source_health = await build_ai_agent_report_source_health(days=7)
+ source_rollups = source_health.get("rollups") or {}
+ report_source_confidence = int(source_rollups.get("confidence_percent") or 0)
+ report_source_ok = int(source_rollups.get("source_ok_count") or 0)
+ report_source_total = int(source_rollups.get("source_count") or 0)
+ report_source_gap_ids = [
+ str(source.get("work_item_id"))
+ for source in source_health.get("source_health", [])
+ if source.get("work_item_id")
+ ][:5]
+ report_asset_state_lines = [
+ (
+ f"{asset.get('label')}: {asset.get('state')} "
+ f"{int(asset.get('done_count') or 0)}/"
+ f"{int(asset.get('done_count') or 0) + int(asset.get('blocked_count') or 0)}"
+ )
+ for asset in source_health.get("automation_assets", [])
+ ][:5]
+ except Exception as _source_e:
+ logger.warning("weekly_report_source_health_failed", error=str(_source_e))
+
# 組裝週報
report = WeeklyReportMessage(
week_range=week_range,
@@ -246,6 +275,11 @@ class WeeklyReportService:
and deploys == 0
and disp_total == 0
),
+ report_source_confidence_percent=report_source_confidence,
+ report_source_ok_count=report_source_ok,
+ report_source_total_count=report_source_total,
+ report_source_gap_ids=report_source_gap_ids,
+ report_asset_state_lines=report_asset_state_lines,
)
logger.info(
diff --git a/apps/api/tests/test_telegram_message_templates.py b/apps/api/tests/test_telegram_message_templates.py
index a0225a88..2a529190 100644
--- a/apps/api/tests/test_telegram_message_templates.py
+++ b/apps/api/tests/test_telegram_message_templates.py
@@ -453,6 +453,49 @@ def test_weekly_report_keeps_nonzero_source_status_visible() -> None:
assert "Tokens: 1,200" in body
+def test_weekly_report_includes_report_source_health_assets() -> None:
+ report = WeeklyReportMessage(
+ week_range="2026-W25",
+ report_date="2026-06-18 19:40",
+ alert_total=4,
+ ai_proposal_count=1,
+ commits_count=2,
+ deploy_count=1,
+ stats_source_ok=True,
+ k3s_source_ok=True,
+ git_source_ok=True,
+ cost_source_ok=False,
+ report_source_confidence_percent=40,
+ report_source_ok_count=2,
+ report_source_total_count=5,
+ report_source_gap_ids=[
+ "report-source-gap:incident_summary",
+ "report-source-gap:resolution_stats",
+ "report-source-gap:ai_performance",
+ ],
+ report_asset_state_lines=[
+ "KM: draft_ready 3/6",
+ "PlayBook: draft_required 0/3",
+ "腳本: readback_only 1/1",
+ "排程: no_send_preview 3/3",
+ "Verifier: source_health_ready 1/4",
+ ],
+ )
+
+ body = report.format()
+
+ assert "報表資料源 / 沉澱" in body
+ assert "來源: 2/5" in body
+ assert "信心: 40%" in body
+ assert "report-source-gap:incident_summary" in body
+ assert "report-source-gap:resolution_stats" in body
+ assert "report-source-gap:ai_performance" in body
+ assert "KM: draft_ready 3/6" in body
+ assert "PlayBook: draft_required 0/3" in body
+ assert "Verifier: source_health_ready 1/4" in body
+ assert "不自動改排程" in body
+
+
def test_telegram_html_chunks_preserve_complete_lines() -> None:
"""歷史/詳情長訊息不得用 text[:500] 切壞 HTML tag。"""
lines = [