diff --git a/CONSTITUTION.md b/CONSTITUTION.md
index ed1c264..e073b3c 100644
--- a/CONSTITUTION.md
+++ b/CONSTITUTION.md
@@ -2,7 +2,7 @@
> 本文件定義專案開發的核心準則與不可違反的規範
> **建立日期**: 2026-01-12
-> **當前版本**: V10.8 (四 AI Agent 自動化 Smoke 趨勢管理版)
+> **當前版本**: V10.9 (四 AI Agent 自動化 Smoke 每日摘要版)
> **最後更新**: 2026-04-29
---
diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt
index d4be781..dbcd5cc 100644
--- a/TODO_NEXT_STEPS.txt
+++ b/TODO_NEXT_STEPS.txt
@@ -7,16 +7,17 @@
- ADR-018:四 AI Agent 自動化控制面立案。
- Memory:新增 `docs/memory/ai_automation_closure_20260429.md`。
- Guide/Skills 替代:新增 `docs/guides/ai_automation_session_sop.md`。
- - SOT:更新 `docs/AI_INTELLIGENCE_MODULE_SOT.md` 至 V10.8 AI Automation Smoke Management 架構。
+ - SOT:更新 `docs/AI_INTELLIGENCE_MODULE_SOT.md` 至 V10.9 AI Automation Smoke Summary 架構。
- Codex 規則:更新 `AGENTS.md`、`CONSTITUTION.md`、ADR/memory 索引。
- Prometheus 指標化:新增 EventRouter / AutoHeal / safe action / replay in-process metrics,並接入 `/metrics`。
- 線上 smoke dashboard:新增 `/ai_automation_smoke` 與 `/api/ai-automation/smoke`,覆蓋 EventRouter、AutoHeal、NemoTron fallback、OpenClaw embedding queue、ElephantAlpha HITL。
- Smoke 趨勢保存:`/api/ai-automation/smoke` 每次快檢追加 JSONL 精簡紀錄,dashboard 顯示最近趨勢。
- Smoke 趨勢管理:新增 JSONL 匯出、清理與每日摘要。
+ - Smoke 每日摘要:新增 Telegram 手動推播 API 與 momo-scheduler 每日 09:10 排程入口。
【下次待辦】
- Superset / Grafana 視覺化:`momo_ai_event_router_dispatch_total`、`momo_ai_event_router_latency_ms_*`、`momo_ai_autoheal_action_total`。
- - Smoke trend 增加每日摘要 Telegram 推播或排程摘要。
+ - Grafana/Superset panel 設定與 Smoke 摘要成效觀察。
================================================================================
品牌資產最終處理與維護 (Phase 7) [DONE]
diff --git a/app.py b/app.py
index c27784f..f65340d 100644
--- a/app.py
+++ b/app.py
@@ -95,9 +95,9 @@ except Exception as e:
sys_log.error(f"無法檢測磁碟空間: {e}")
# 🚩 系統版本定義 (備份與顯示用)
-# 🚩 2026-04-29 V10.8: AI Smoke 趨勢管理 — JSONL 匯出 / 清理 /
-# 每日摘要接入 dashboard
-SYSTEM_VERSION = "V10.8"
+# 🚩 2026-04-29 V10.9: AI Smoke 每日摘要 — Telegram 手動推播 /
+# scheduler 09:10 排程入口
+SYSTEM_VERSION = "V10.9"
# ==========================================
# 🔒 SQL Injection 防護函數
diff --git a/config.py b/config.py
index 8d75be7..5fbf508 100644
--- a/config.py
+++ b/config.py
@@ -253,7 +253,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
-SYSTEM_VERSION = "V10.8"
+SYSTEM_VERSION = "V10.9"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示
diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md
index c03c9b0..4c89d96 100644
--- a/docs/AI_INTELLIGENCE_MODULE_SOT.md
+++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md
@@ -1,8 +1,8 @@
# MOMO PRO — AI 競價情報模組 Single Source of Truth
> **最後更新**: 2026-04-29 (台北時間)
-> **狀態**: 🟢 四 AI Agent 自動化閉環已落地 — EventRouter / AutoHeal / OpenClaw Memory / ElephantAlpha bridge / Prometheus metrics / Smoke Dashboard / Smoke Trend Management 具測試覆蓋
-> **適用版本**: V10.8 AI Automation Smoke Management 架構
+> **狀態**: 🟢 四 AI Agent 自動化閉環已落地 — EventRouter / AutoHeal / OpenClaw Memory / ElephantAlpha bridge / Prometheus metrics / Smoke Dashboard / Smoke Trend Management / Telegram Summary 具測試覆蓋
+> **適用版本**: V10.9 AI Automation Smoke Summary 架構
---
@@ -66,6 +66,7 @@ SQL漏斗(~300筆)
- `/api/ai-automation/smoke` 提供 read-only JSON 狀態,不做外部網路呼叫。
- Smoke API 會將最近快檢結果保存到 JSONL,dashboard 顯示最近狀態趨勢。
- Smoke history 支援 JSONL 匯出、清理與每日 OK / Warning / Critical 摘要。
+- Smoke 每日摘要支援手動 Telegram 推播,並由 `momo-scheduler` 每日 09:10 呼叫 `run_ai_smoke_daily_summary_task()`。
---
diff --git a/docs/adr/ADR-012-agent-action-ladder.md b/docs/adr/ADR-012-agent-action-ladder.md
index e0ec642..a0a735d 100644
--- a/docs/adr/ADR-012-agent-action-ladder.md
+++ b/docs/adr/ADR-012-agent-action-ladder.md
@@ -147,7 +147,8 @@ L1 Hermes 掛 → L0 模板直出 + 🟡 「AI 分析暫不可用」
- 2026-04-29 已補 `/ai_automation_smoke` 與 `/api/ai-automation/smoke`:EventRouter、AutoHeal、NemoTron fallback、OpenClaw embedding queue、ElephantAlpha HITL 線上快檢。
- 2026-04-29 已補 smoke 結果 JSONL 保存與 dashboard 趨勢視覺化。
- 2026-04-29 已補 smoke history JSONL 匯出、清理與每日摘要。
-- 尚未完成:Grafana/Superset 視覺化面板與每日摘要 Telegram 推播。
+- 2026-04-29 已補 smoke 每日摘要 Telegram 手動推播與 momo-scheduler 09:10 排程入口。
+- 尚未完成:Grafana/Superset 視覺化面板與推播成效觀察。
## References
- `services/event_router.py` — 分流入口(Phase 1)
diff --git a/docs/guides/ai_automation_session_sop.md b/docs/guides/ai_automation_session_sop.md
index 2396735..c19e333 100644
--- a/docs/guides/ai_automation_session_sop.md
+++ b/docs/guides/ai_automation_session_sop.md
@@ -28,6 +28,7 @@
- AI 自動化閉環變更必須確認 `/api/ai-automation/smoke` 與 `/ai_automation_smoke` 仍能反映新狀態。
- Smoke dashboard 會保存 JSONL 趨勢;若新增檢查項目,要確保 history compact record 仍保持小而可讀。
- Smoke history 管理只能操作 `MOMO_AI_AUTOMATION_SMOKE_HISTORY` 指向的 JSONL,不得清理 DB 或 EventRouter queue。
+- Smoke 每日摘要推播只讀 history,不得重新執行 smoke,也不得把完整 details 寫進 Telegram。
- L2 action 必須在 `SAFE_ACTIONS` 且可審計、可回放、低副作用。
- AutoHeal 不得 restart / stop / recreate `momo-db` 或 `momo-postgres`。
- raw `ai_insights` 寫入後必須 enqueue embedding;若 enqueue 失敗,必須可 backfill。
diff --git a/docs/memory/ai_automation_closure_20260429.md b/docs/memory/ai_automation_closure_20260429.md
index e7eaba5..2bc82f7 100644
--- a/docs/memory/ai_automation_closure_20260429.md
+++ b/docs/memory/ai_automation_closure_20260429.md
@@ -13,6 +13,7 @@
- 線上 smoke dashboard 已接入 `/ai_automation_smoke`,JSON API 為 `/api/ai-automation/smoke`。
- Smoke API 會保存最近快檢 JSONL 趨勢,dashboard 顯示 OK / Warning / Critical 最近分布。
- Smoke history 已支援 JSONL 匯出、清理與每日摘要;清理只影響 smoke history,不碰 DB 或 EventRouter queue。
+- Smoke 每日摘要已支援手動 Telegram 推播與 scheduler 09:10 排程入口;摘要只讀 JSONL history。
## 已落地範圍
@@ -28,6 +29,7 @@
- Smoke dashboard read-only 檢查 EventRouter queue、AutoHeal protected resources、NemoTron fallback、OpenClaw embedding queue、ElephantAlpha HITL,不做外部網路呼叫。
- Smoke history 只保存精簡紀錄,不保存完整 details,避免長期檔案膨脹與敏感資訊堆積。
- Export API 回傳 `application/x-ndjson`,clear API 只刪除 `MOMO_AI_AUTOMATION_SMOKE_HISTORY` 指向檔案。
+- Daily summary API:`POST /api/ai-automation/smoke/daily-summary/send`。
## 驗證紀錄
@@ -35,6 +37,7 @@
- 2026-04-29 AI smoke dashboard 批次:`2 passed`(單檔 smoke service),後續核心組需持續納入。
- 2026-04-29 AI smoke trend 批次:`5 passed`(smoke + metrics)。
- 2026-04-29 AI smoke management 批次:`7 passed`(smoke + metrics)。
+- 2026-04-29 AI smoke summary 批次:`9 passed`(smoke + metrics)。
- 2026-04-29 L2 安全記憶批次:`24 passed`。
- collect-only:`48 tests collected`。
- `git diff --check` 已通過。
diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md
index e70a06e..359f4ad 100644
--- a/docs/memory/history_logs.md
+++ b/docs/memory/history_logs.md
@@ -28,6 +28,7 @@
- **Smoke Dashboard**: 新增 `/ai_automation_smoke` 與 `/api/ai-automation/smoke`,提供四 Agent 閉環 read-only 快檢。
- **Smoke 趨勢保存**: Smoke API 追加 JSONL 精簡紀錄,dashboard 顯示最近 OK / Warning / Critical 趨勢。
- **Smoke 趨勢管理**: Dashboard 增加 JSONL 匯出、清理與每日摘要,清理範圍限定 smoke history 檔。
+- **Smoke 每日摘要推播**: 新增 Telegram 手動推播 API 與 momo-scheduler 每日 09:10 摘要任務,只讀 smoke history。
### 2026-04-28~29:Phase 3e 重構大戰 + daily_sales cache 隱形 bug 根除
- **app.py 縮減 -10.8%**: 7,386 → 6,590 行,11 commits 全綠零 502。
diff --git a/routes/system_public_routes.py b/routes/system_public_routes.py
index d5144ce..6b02de9 100644
--- a/routes/system_public_routes.py
+++ b/routes/system_public_routes.py
@@ -231,6 +231,16 @@ def ai_automation_smoke_history_clear():
return jsonify(clear_smoke_history())
+@system_public_bp.route('/api/ai-automation/smoke/daily-summary/send', methods=['POST'])
+@login_required
+def ai_automation_smoke_daily_summary_send():
+ """Send compact smoke trend daily summary to Telegram."""
+ from services.ai_automation_smoke_service import send_smoke_daily_summary
+ result = send_smoke_daily_summary()
+ status_code = 200 if result.get("status") == "sent" else 502
+ return jsonify(result), status_code
+
+
@system_public_bp.route('/logs')
def show_logs():
return render_template('logs.html')
diff --git a/run_scheduler.py b/run_scheduler.py
index e50e4b7..e5288ce 100644
--- a/run_scheduler.py
+++ b/run_scheduler.py
@@ -8,7 +8,7 @@ run_scheduler.py — momo-scheduler 容器入口點
每 4 小時:competitor_price_feeder、icaim_analysis
每 6 小時:openclaw_meta_analysis、quality_rescore
每 12 小時:dedup_batch
- 每 1 天 :db_backup(03:00)、cleanup_agent_context(03:30)、backup_monitor(04:00)、daily_report(09:00)
+ 每 1 天 :db_backup(03:00)、cleanup_agent_context(03:30)、backup_monitor(04:00)、daily_report(09:00)、ai_smoke_summary(09:10)
每 1 週 :weekly_strategy(週一 06:00)
每 1 月 :monthly_report(每月1日 07:00)
"""
@@ -36,6 +36,7 @@ from scheduler import (
run_dedup_batch_task,
run_quality_rescore_task,
run_daily_report_task,
+ run_ai_smoke_daily_summary_task,
run_monthly_report_task,
)
@@ -116,6 +117,9 @@ def _register_schedules():
schedule.every().day.at("09:00").do(run_daily_report_task)
logger.info("📅 每日 09:00:daily_report")
+ schedule.every().day.at("09:10").do(run_ai_smoke_daily_summary_task)
+ logger.info("📅 每日 09:10:ai_smoke_daily_summary")
+
# 每月1日 07:00 月報(schedule 不支援 every().month,用每日 07:00 + 日期判斷)
def _monthly_report_gate():
from datetime import datetime as _dt
diff --git a/scheduler.py b/scheduler.py
index 14c4dcd..61571d2 100644
--- a/scheduler.py
+++ b/scheduler.py
@@ -2566,6 +2566,38 @@ def run_monthly_report_task():
logging.error(f"[Scheduler] [MonthlyReport] auto_heal_service 失敗: {_heal_e}")
+def run_ai_smoke_daily_summary_task():
+ """每日 AI 自動化 Smoke trend 摘要推播(只讀 history,不重新執行 smoke)。"""
+ try:
+ from services.ai_automation_smoke_service import send_smoke_daily_summary
+ result = send_smoke_daily_summary()
+ logging.info(
+ "[Scheduler] [AISmokeSummary] 完成 | status=%s sent=%s failed=%s",
+ result.get("status"),
+ result.get("telegram", {}).get("sent", 0),
+ result.get("telegram", {}).get("failed", 0),
+ )
+ _save_stats('ai_smoke_daily_summary', result)
+ except Exception as e:
+ import traceback as _tb
+ logging.error(f"[Scheduler] [AISmokeSummary] 🚨 摘要推播異常: {e}")
+ _save_stats('ai_smoke_daily_summary', {"status": "Error", "error": str(e)})
+ try:
+ from services.event_router import notify_failure
+ notify_failure(
+ task_name="run_ai_smoke_daily_summary_task",
+ error=e,
+ source="Scheduler.AISmokeSummary",
+ event_type="ai_smoke_summary_failure",
+ priority="P2",
+ title="AI Smoke 每日摘要推播異常",
+ trace=_tb.format_exc(),
+ dedup_ttl_sec=3600,
+ )
+ except Exception as _router_e:
+ logging.error(f"[Scheduler] [AISmokeSummary] event_router 失敗: {_router_e}")
+
+
if __name__ == "__main__":
# 此檔案現在由 app.py 導入並由其主執行緒管理排程。
# 若需獨立測試,可在此處臨時加入調用程式碼。
diff --git a/services/ai_automation_smoke_service.py b/services/ai_automation_smoke_service.py
index 2ee1a8d..44a51e9 100644
--- a/services/ai_automation_smoke_service.py
+++ b/services/ai_automation_smoke_service.py
@@ -10,6 +10,7 @@ import os
import json
import threading
from datetime import datetime
+from html import escape
from typing import Any, Dict, List
from sqlalchemy import text
@@ -156,6 +157,55 @@ def clear_smoke_history() -> Dict[str, Any]:
return {"cleared": 0, "path": _HISTORY_PATH, "error": str(exc)[:300]}
+def build_smoke_daily_summary_message(history_limit: int = 200) -> str:
+ records = _load_history(limit=history_limit)
+ summary = _history_summary(records)
+ daily_rows = summary.get("daily", [])
+ latest = summary.get("latest") or {}
+ counts = summary.get("counts") or {"ok": 0, "warning": 0, "critical": 0}
+
+ lines = [
+ "🤖 AI 自動化 Smoke 每日摘要",
+ "━━━━━━━━━━━━━━━━━━━━",
+ f"版本:{escape(SYSTEM_VERSION)}",
+ f"最近狀態:{escape(str(latest.get('status', 'no_data')).upper())}",
+ f"最近時間:{escape(str(latest.get('generated_at', '尚無紀錄')))}",
+ "",
+ "📊 近期統計",
+ f"✅ OK:{counts.get('ok', 0)}",
+ f"⚠️ Warning:{counts.get('warning', 0)}",
+ f"🚨 Critical:{counts.get('critical', 0)}",
+ ]
+
+ if daily_rows:
+ lines += ["", "🗓️ 最近每日摘要"]
+ for row in daily_rows[-5:]:
+ lines.append(
+ f"{escape(str(row.get('date')))}|"
+ f"OK {row.get('ok', 0)} / W {row.get('warning', 0)} / C {row.get('critical', 0)}"
+ )
+ else:
+ lines += ["", "🗓️ 尚無 smoke history;請先開啟 /ai_automation_smoke 執行快檢。"]
+
+ lines += [
+ "━━━━━━━━━━━━━━━━━━━━",
+ "入口:/ai_automation_smoke",
+ ]
+ return "\n".join(lines)
+
+
+def send_smoke_daily_summary(chat_ids: List[Any] | None = None) -> Dict[str, Any]:
+ from services.telegram_templates import send_telegram_with_result
+
+ message = build_smoke_daily_summary_message()
+ result = send_telegram_with_result(message, chat_ids=chat_ids)
+ return {
+ "status": "sent" if result.get("ok") else "failed",
+ "telegram": result,
+ "message_preview": message[:500],
+ }
+
+
def _event_router_check() -> Dict[str, Any]:
try:
from services import event_router
diff --git a/templates/ai_automation_smoke.html b/templates/ai_automation_smoke.html
index e381bb6..d70098d 100644
--- a/templates/ai_automation_smoke.html
+++ b/templates/ai_automation_smoke.html
@@ -120,6 +120,9 @@
+
@@ -292,6 +295,20 @@ document.getElementById('clearHistoryBtn').addEventListener('click', async () =>
btn.disabled = false;
}
});
+document.getElementById('sendSummaryBtn').addEventListener('click', async () => {
+ const btn = document.getElementById('sendSummaryBtn');
+ btn.disabled = true;
+ try {
+ const res = await fetchWithCSRF('/api/ai-automation/smoke/daily-summary/send', {method: 'POST'});
+ const data = await res.json();
+ if (!res.ok) throw new Error((data.telegram?.errors || []).join(', ') || `HTTP ${res.status}`);
+ showToast(`AI Smoke 每日摘要已推播:sent=${data.telegram?.sent || 0}`, 'success');
+ } catch (err) {
+ showToast(`推播失敗:${err.message}`, 'error');
+ } finally {
+ btn.disabled = false;
+ }
+});
loadSmoke();
{% endblock %}
diff --git a/tests/test_ai_automation_smoke_service.py b/tests/test_ai_automation_smoke_service.py
index 4880d29..dd1d203 100644
--- a/tests/test_ai_automation_smoke_service.py
+++ b/tests/test_ai_automation_smoke_service.py
@@ -93,3 +93,43 @@ def test_smoke_history_daily_summary():
{"date": "2026-04-28", "ok": 1, "warning": 0, "critical": 0, "total": 1},
{"date": "2026-04-29", "ok": 0, "warning": 1, "critical": 1, "total": 2},
]
+
+
+def test_build_smoke_daily_summary_message_escapes_history(tmp_path, monkeypatch):
+ from services import ai_automation_smoke_service as smoke
+
+ history_path = tmp_path / "smoke_history.jsonl"
+ history_path.write_text(
+ '{"generated_at":"2026-04-29T01:00:00","status":"warning",'
+ '"summary":{"ok":0,"warning":1,"critical":0,"total":1},'
+ '"checks":[{"name":"