From e12e6a8f964f8009f22cbe903a7d1ba2726911b8 Mon Sep 17 00:00:00 2001 From: OoO Date: Sat, 2 May 2026 13:07:30 +0800 Subject: [PATCH] feat(telegram): ADR-019 Phase 6 - daily data freshness probe + cron 09:05 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-019 Phase 6:每日 09:05 OpenClawBot scheduler 主動巡檢 realtime_sales_monthly 最新資料日期,落後超過閾值時透過 EventRouter 發 Telegram 警告。 新增 services/data_freshness_probe.py: - gap == 0:靜默不發(資料齊全) - gap == 1:info(昨日資料齊,正常) - gap == 2~3:warning - gap >= 4:alert(P2,ETL 大概率出問題) - latest_date 取不到:alert(DB 連線異常) routes/openclaw_bot_routes.py 加 cron job openclaw_data_freshness_probe (hour=9 minute=5,避開 09:00 的其他既有 job)。 從『用戶月初按按鈕踩坑才發現資料缺口』的被動模式,轉成 agent 主動巡檢通知。 配合 Phase 1(PPT freshness gate)+ Phase 2(agent tool)+ Phase 3(cmd 路徑 agent dispatch)+ Phase 4(對話 state),Telegram Bot 互動層的『rigid default + 靜默空白』反模式根除。 Co-Authored-By: Claude Opus 4.7 (1M context) --- routes/openclaw_bot_routes.py | 13 +++- services/data_freshness_probe.py | 128 +++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 services/data_freshness_probe.py diff --git a/routes/openclaw_bot_routes.py b/routes/openclaw_bot_routes.py index fef2854..2a3f886 100644 --- a/routes/openclaw_bot_routes.py +++ b/routes/openclaw_bot_routes.py @@ -3323,8 +3323,19 @@ def start_scheduler(): id="openclaw_check_anomalies", replace_existing=True, ) + # ADR-019 Phase 6: 每日 09:00 主動巡檢資料新鮮度,缺資料時透過 EventRouter 發警告 + try: + from services.data_freshness_probe import run_data_freshness_probe + _scheduler.add_job( + run_data_freshness_probe, + CronTrigger(hour=9, minute=5), + id="openclaw_data_freshness_probe", + replace_existing=True, + ) + except ImportError as _e: + sys_log.warning(f"[OpenClawBot] data_freshness_probe 未安裝,跳過:{_e}") _scheduler.start() - sys_log.info("[OpenClawBot] Scheduler started ✓ (competitor/morning/excel/evening/weekly/anomaly)") + sys_log.info("[OpenClawBot] Scheduler started ✓ (competitor/morning/excel/evening/weekly/anomaly/freshness)") except ImportError: sys_log.warning("[OpenClawBot] APScheduler 未安裝 — 排程功能停用") except Exception as e: diff --git a/services/data_freshness_probe.py b/services/data_freshness_probe.py new file mode 100644 index 0000000..daf2e63 --- /dev/null +++ b/services/data_freshness_probe.py @@ -0,0 +1,128 @@ +"""ADR-019 Phase 6:每日資料新鮮度主動巡檢。 + +每天 09:00 由 OpenClawBot scheduler 觸發,檢查 realtime_sales_monthly 最新資料 +日期,若落後超過閾值就透過 EventRouter 主動發 Telegram 警告,避免用戶月初/ +ETL 卡住才靠按按鈕踩坑發現。 + +EventRouter severity 對應: +- gap == 0:今日資料已進,靜默不發 +- gap == 1:昨日資料齊全,info(不發或只 log) +- gap == 2~3:warning,可能 ETL 跑慢 +- gap >= 4:alert(P2),ETL 大概率出問題 +""" + +from __future__ import annotations + +from datetime import datetime, date +import logging + +from services.event_router import dispatch_sync + +logger = logging.getLogger("DataFreshnessProbe") + +_DEFAULT_WARN_GAP_DAYS = 2 +_DEFAULT_ALERT_GAP_DAYS = 4 + + +def _query_latest_date(): + """獨立 import 避免循環依賴。回傳 'YYYY-MM-DD' 字串或 None。""" + try: + from routes.openclaw_bot_routes import latest_date + return latest_date() + except Exception as e: + logger.warning(f"[DataFreshnessProbe] 取得 latest_date 失敗:{e}") + return None + + +def _parse_date(s: str) -> date | None: + if not s: + return None + try: + return datetime.strptime(s.replace('/', '-'), '%Y-%m-%d').date() + except (ValueError, AttributeError): + return None + + +def run_data_freshness_probe( + warn_gap_days: int = _DEFAULT_WARN_GAP_DAYS, + alert_gap_days: int = _DEFAULT_ALERT_GAP_DAYS, +) -> dict: + """主動巡檢一次,缺資料時透過 EventRouter 發通知。回傳 probe 結果摘要。""" + today = date.today() + latest_str = _query_latest_date() + latest_dt = _parse_date(latest_str) + + result = { + 'today': today.isoformat(), + 'latest_date': latest_str, + 'gap_days': None, + 'severity': 'unknown', + 'notified': False, + } + + if latest_dt is None: + logger.error("[DataFreshnessProbe] 無法取得 latest_date — 視為嚴重異常") + result['severity'] = 'alert' + try: + dispatch_sync({ + 'source': 'OpenClawBot.DataFreshnessProbe', + 'event_type': 'data_freshness_unknown', + 'severity': 'alert', + 'title': '⚠️ 資料新鮮度巡檢失敗', + 'status': '無法取得 latest_date', + 'impact': 'P2 — 業績資料源不可達,所有報表 / NL 查詢可能失準', + 'summary': '請檢查 realtime_sales_monthly 表與 DB 連線。', + }) + result['notified'] = True + except Exception as e: + logger.error(f"[DataFreshnessProbe] EventRouter dispatch 失敗:{e}") + return result + + gap = (today - latest_dt).days + result['gap_days'] = gap + + if gap <= 0: + result['severity'] = 'success' + logger.info(f"[DataFreshnessProbe] 資料齊全(latest={latest_dt}, today={today})") + return result + + if gap == 1: + result['severity'] = 'info' + logger.info(f"[DataFreshnessProbe] 落後 1 天(正常作息,ETL 通常隔天匯入)") + return result + + if gap < warn_gap_days: + result['severity'] = 'info' + return result + + severity = 'alert' if gap >= alert_gap_days else 'warning' + result['severity'] = severity + + try: + dispatch_sync({ + 'source': 'OpenClawBot.DataFreshnessProbe', + 'event_type': 'data_freshness_lag', + 'severity': severity, + 'title': f'📊 業績資料落後 {gap} 天', + 'status': f'最新資料:{latest_str}(今日:{today.isoformat()})', + 'impact': ( + f'P2 — 月報/日報 PPT 與 NL 業績查詢可能空白或失準,' + f'請檢查 realtime_sales_monthly ETL 任務(run_momo_task / run_auto_import_task)' + if severity == 'alert' else + f'P3 — 資料落後 {gap} 天,請確認 ETL 是否正常排程' + ), + 'summary': f'gap={gap}d, latest={latest_str}', + 'payload': { + 'gap_days': gap, + 'latest_date': latest_str, + 'probe_at': datetime.now().isoformat(timespec='seconds'), + }, + }) + result['notified'] = True + logger.warning( + f"[DataFreshnessProbe] {severity}: gap={gap}d, latest={latest_str} → notified" + ) + except Exception as e: + logger.error(f"[DataFreshnessProbe] EventRouter dispatch 失敗:{e}") + + return result