feat(governance): automate identity and comparison coverage

This commit is contained in:
ogt
2026-07-14 16:15:41 +08:00
parent 2dcc2122c4
commit 4bfb9dbefd
31 changed files with 2763 additions and 578 deletions

View File

@@ -3,7 +3,7 @@
run_scheduler.py — momo-scheduler 容器入口點
排程任務清單(對齊 app.py init_scheduler + scheduler.py 全任務):
每 30 分鐘:auto_import、whitepage_check
每 30 分鐘:revenue_automation_cycleauto_import → comparison backfill、whitepage_check
每 1 小時momo、edm、festival
每 4 小時competitor_price_feeder、external_offer_sync、icaim_analysis
每 6 小時quality_rescore、action_plan_hygiene
@@ -22,7 +22,7 @@ import logging
import os
import threading
import time
from typing import Optional, Tuple
from typing import Callable, Optional, Tuple
import schedule
@@ -60,6 +60,7 @@ logger = logging.getLogger(__name__)
_AI_CALLS_ERROR_SPIKE_LAST_PUSH_TS = 0.0
_OLLAMA_111_USAGE_LAST_PUSH_TS = 0.0
_REVENUE_AUTOMATION_LOCK = threading.Lock()
def _scheduler_stats_path() -> str:
@@ -122,6 +123,96 @@ def run_pchome_growth_momo_backfill_catchup_task():
return {"status": "failed", "error": str(error)}
def _launch_revenue_automation_background(
job_name: str,
target: Callable[[], object],
) -> dict[str, str]:
"""Launch one revenue job without blocking or overlapping the scheduler loop."""
if not _REVENUE_AUTOMATION_LOCK.acquire(blocking=False):
logger.info("[RevenueAutomation] skipped overlapping job=%s", job_name)
return {"status": "skipped", "reason": "revenue_automation_already_running"}
def _runner():
try:
target()
except Exception as error:
logger.error(
"[RevenueAutomation] background job failed job=%s error=%s",
job_name,
error,
exc_info=True,
)
_notify_scheduler_failure(
job_name,
error,
source="Scheduler.RevenueAutomation",
event_type="revenue_automation_background_failure",
title="PChome 收益自動化背景任務失敗",
)
finally:
_REVENUE_AUTOMATION_LOCK.release()
thread = threading.Thread(
target=_runner,
daemon=True,
name=f"revenue-automation-{job_name}",
)
try:
thread.start()
except Exception:
_REVENUE_AUTOMATION_LOCK.release()
raise
return {"status": "queued", "job": job_name}
def run_revenue_automation_cycle_task() -> dict[str, object]:
"""Refresh sales first, then catch up the bounded same-item mapping lane."""
logger.info("[RevenueAutomation] cycle started: sales acquisition -> comparison backfill")
run_auto_import_task()
backfill = run_pchome_growth_momo_backfill_catchup_task()
logger.info(
"[RevenueAutomation] cycle completed backfill_status=%s",
backfill.get("status"),
)
return {"status": "completed", "backfill": backfill}
def run_revenue_automation_cycle_background_task() -> dict[str, str]:
return _launch_revenue_automation_background(
"revenue-cycle",
run_revenue_automation_cycle_task,
)
def run_pchome_growth_momo_backfill_background_task() -> dict[str, str]:
return _launch_revenue_automation_background(
"pchome-growth-backfill",
run_pchome_growth_momo_backfill_task,
)
def start_revenue_automation_watchdog() -> threading.Thread:
"""Keep revenue refresh alive even when a legacy schedule job blocks run_pending."""
try:
interval_seconds = int(os.getenv("REVENUE_AUTOMATION_WATCHDOG_SECONDS", "1800"))
except ValueError:
interval_seconds = 1800
interval_seconds = max(60, interval_seconds)
def _watchdog():
while True:
run_revenue_automation_cycle_background_task()
time.sleep(interval_seconds)
thread = threading.Thread(
target=_watchdog,
daemon=True,
name="revenue-automation-watchdog",
)
thread.start()
return thread
def _env_flag(name: str, default: bool = False) -> bool:
raw = os.getenv(name)
if raw is None:
@@ -185,8 +276,8 @@ def _notify_scheduler_failure(
def _register_schedules():
schedule.every(30).minutes.do(run_auto_import_task)
logger.info("📅 每 30 分鐘:auto_import")
schedule.every(30).minutes.do(run_revenue_automation_cycle_background_task)
logger.info("📅 每 30 分鐘:revenue_automation_cycle業績匯入 → 比價回填,非阻塞)")
schedule.every(30).minutes.do(run_whitepage_check)
logger.info("📅 每 30 分鐘whitepage_check")
@@ -374,12 +465,9 @@ def _register_schedules():
schedule.every().day.at("10:30").do(run_pchome_match_backfill_task)
logger.info("📅 每日 10:30pchome_match_backfill")
schedule.every().day.at("10:45").do(run_pchome_growth_momo_backfill_task)
schedule.every().day.at("10:45").do(run_pchome_growth_momo_backfill_background_task)
logger.info("📅 每日 10:45pchome_growth_momo_backfill高業績商品補 MOMO 對應)")
schedule.every(30).minutes.do(run_pchome_growth_momo_backfill_catchup_task)
logger.info("📅 每 30 分鐘pchome_growth_momo_backfill_catchup錯過每日任務時自動補跑")
# Operation Ollama-First v5.0 — Phase 1 收尾:每日 23:55 LLM Token 日報
schedule.every().day.at("23:55").do(run_daily_token_report_task)
logger.info("📅 每日 23:55daily_token_report")
@@ -1368,6 +1456,13 @@ if __name__ == "__main__":
_register_schedules()
logger.info("✅ 全部排程任務已註冊")
_revenue_watchdog_thread = start_revenue_automation_watchdog()
logger.info(
"[RevenueAutomation] watchdog=%s alive=%s",
_revenue_watchdog_thread.name,
_revenue_watchdog_thread.is_alive(),
)
try:
from services.openclaw_bot.embedding_worker_runtime import start_embedding_worker