From 8a3d50933b4b8c39444a73e1c0f3dcf9dd120220 Mon Sep 17 00:00:00 2001 From: OoO Date: Fri, 1 May 2026 14:02:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20=E8=87=AA=E5=8B=95=E8=A3=9C?= =?UTF-8?q?=E6=8A=93=E4=B8=A6=E9=87=8D=E7=AE=97=20PChome=20=E6=8C=91?= =?UTF-8?q?=E5=93=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONSTITUTION.md | 2 +- app.py | 9 +++-- docs/AI_INTELLIGENCE_MODULE_SOT.md | 3 +- routes/ai_routes.py | 7 +++- run_scheduler.py | 6 ++- scheduler.py | 60 ++++++++++++++++++++++++++++++ services/agent_actions.py | 1 + tests/test_frontend_v2_assets.py | 11 ++++++ 8 files changed, 91 insertions(+), 8 deletions(-) diff --git a/CONSTITUTION.md b/CONSTITUTION.md index 491adfd..3830a96 100644 --- a/CONSTITUTION.md +++ b/CONSTITUTION.md @@ -2,7 +2,7 @@ > 本文件定義專案開發的核心準則與不可違反的規範 > **建立日期**: 2026-01-12 -> **當前版本**: V10.48 (Priority backfill for unmatched PChome products) +> **當前版本**: V10.49 (Scheduled PChome backfill with pick regeneration) > **最後更新**: 2026-05-01 --- diff --git a/app.py b/app.py index 3c15d0d..9ad92e0 100644 --- a/app.py +++ b/app.py @@ -54,7 +54,7 @@ try: # 導入自定義模組 try: - from scheduler import run_momo_task, run_edm_task, run_festival_task, run_auto_import_task, run_whitepage_check, run_competitor_price_feeder_task + from scheduler import run_momo_task, run_edm_task, run_festival_task, run_auto_import_task, run_whitepage_check, run_competitor_price_feeder_task, run_pchome_match_backfill_task from database.manager import DatabaseManager from database.models import Base, Product, PriceRecord, MonthlySummaryAnalysis from database.edm_models import PromoProduct @@ -95,8 +95,8 @@ except Exception as e: sys_log.error(f"無法檢測磁碟空間: {e}") # 🚩 系統版本定義 (備份與顯示用) -# 🚩 2026-05-01 V10.48: Priority backfill for unmatched PChome products -SYSTEM_VERSION = "V10.48" +# 🚩 2026-05-01 V10.49: Scheduled PChome backfill with pick regeneration +SYSTEM_VERSION = "V10.49" # ========================================== # 🔒 SQL Injection 防護函數 @@ -1120,6 +1120,9 @@ def init_scheduler(): schedule.every(4).hours.do(run_competitor_price_feeder_task) sys_log.info(f"📅 已設定每 4 小時執行 PChome 競品價格抓取任務") + schedule.every().day.at("10:30").do(run_pchome_match_backfill_task) + sys_log.info(f"📅 已設定每日 10:30 執行 PChome 待比對補抓與挑品重算任務") + # 啟動排程執行緒 scheduler_thread = threading.Thread(target=run_schedule, daemon=True) scheduler_thread.start() diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index 0b7aad4..b171ba1 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -35,7 +35,8 @@ SQL漏斗(~300筆) - 寫入策略使用 `strategy='product_pick'`,保留在既有 AI 決策表,不新增假頁面或暫存 JSON。 - 後台入口:`POST /api/ai/product-picks/generate`,`/ai_intelligence` 可手動產生清單。 - 配對來源仍以 PChome crawler 真實搜尋結果為準;無競品資料時不生成挑品。 -- 比對覆蓋率補強入口:`POST /api/ai/pchome-match/backfill`,優先補抓仍無有效 PChome 配對的高價 ACTIVE 商品。 +- 比對覆蓋率補強入口:`POST /api/ai/pchome-match/backfill`,優先補抓仍無有效 PChome 配對的高價 ACTIVE 商品,完成後自動重算 AI 挑品清單。 +- 排程閉環:`run_pchome_match_backfill_task` 每日 10:30 執行,補抓 PChome 待比對商品、寫入歷史價格,再重算 `strategy='product_pick'` 清單。 | 角色 | 模型 | 主機 | 成本 | 每日限額 | |------|------|------|------|---------| diff --git a/routes/ai_routes.py b/routes/ai_routes.py index 2cb9709..56002ca 100644 --- a/routes/ai_routes.py +++ b/routes/ai_routes.py @@ -1660,12 +1660,14 @@ def api_pchome_match_backfill(): try: from config import DATABASE_PATH from sqlalchemy import create_engine + from services.ai_product_pick_agent import generate_product_pick_list from services.competitor_price_feeder import CompetitorPriceFeeder engine = create_engine(DATABASE_PATH) result = CompetitorPriceFeeder(engine=engine).run_unmatched_priority(limit=limit) + pick_result = generate_product_pick_list(engine, limit=30) logger.info( - "[PChomeBackfill] done total=%s matched=%s no=%s low=%s errors=%s history=%s duration=%ss", + "[PChomeBackfill] done total=%s matched=%s no=%s low=%s errors=%s history=%s duration=%ss pick_written=%s", result.total_skus, result.matched, result.skipped_no_result, @@ -1673,6 +1675,7 @@ def api_pchome_match_backfill(): result.errors, result.history_written, result.duration_sec, + pick_result.written, ) except Exception as exc: logger.error(f"[PChomeBackfill] 背景補抓失敗: {exc}") @@ -1682,7 +1685,7 @@ def api_pchome_match_backfill(): return jsonify({ 'success': True, - 'message': f'已啟動 PChome 待比對補抓,優先處理 {limit} 筆高價未配對商品', + 'message': f'已啟動 PChome 待比對補抓,優先處理 {limit} 筆高價未配對商品;完成後會重算 AI 挑品清單', 'limit': limit, }), 202 diff --git a/run_scheduler.py b/run_scheduler.py index e5288ce..c188e14 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)、ai_smoke_summary(09:10) + 每 1 天 :db_backup(03:00)、cleanup_agent_context(03:30)、backup_monitor(04:00)、daily_report(09:00)、ai_smoke_summary(09:10)、pchome_match_backfill(10:30) 每 1 週 :weekly_strategy(週一 06:00) 每 1 月 :monthly_report(每月1日 07:00) """ @@ -28,6 +28,7 @@ from scheduler import ( run_auto_import_task, run_whitepage_check, run_competitor_price_feeder_task, + run_pchome_match_backfill_task, run_icaim_analysis_task, run_weekly_strategy_task, run_db_backup_task, @@ -120,6 +121,9 @@ def _register_schedules(): schedule.every().day.at("09:10").do(run_ai_smoke_daily_summary_task) logger.info("📅 每日 09:10:ai_smoke_daily_summary") + schedule.every().day.at("10:30").do(run_pchome_match_backfill_task) + logger.info("📅 每日 10:30:pchome_match_backfill") + # 每月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 bbc96cb..fc0ec62 100644 --- a/scheduler.py +++ b/scheduler.py @@ -2061,6 +2061,66 @@ def run_competitor_price_feeder_task(): logging.error(f"[Scheduler] [Feeder] event_router 失敗: {_router_e}") +def run_pchome_match_backfill_task(): + """ + PChome 待比對商品補抓任務(每日執行) + 優先處理尚未有有效 PChome 配對的高價 ACTIVE 商品,寫入 competitor_prices + 與 competitor_price_history,完成後重算 product_pick 挑品清單。 + """ + try: + from config import DATABASE_PATH + from sqlalchemy import create_engine + from services.ai_product_pick_agent import generate_product_pick_list + from services.competitor_price_feeder import CompetitorPriceFeeder + + now_str = datetime.now(TAIPEI_TZ).strftime('%Y-%m-%d %H:%M') + logging.info(f"[Scheduler] [PChomeBackfill] 🚀 啟動待比對補抓任務 | {now_str}") + + engine = create_engine(DATABASE_PATH) + feeder_result = CompetitorPriceFeeder(engine=engine).run_unmatched_priority(limit=120) + pick_result = generate_product_pick_list(engine, limit=30) + + stats = { + "total_skus": feeder_result.total_skus, + "matched": feeder_result.matched, + "skipped_no_result": feeder_result.skipped_no_result, + "skipped_low_score": feeder_result.skipped_low_score, + "errors": feeder_result.errors, + "duration_sec": feeder_result.duration_sec, + "history_written": feeder_result.history_written, + "pick_candidates": pick_result.candidates, + "pick_written": pick_result.written, + "status": "Success", + } + logging.info( + f"[Scheduler] [PChomeBackfill] ✅ 完成 | " + f"matched={feeder_result.matched}/{feeder_result.total_skus} " + f"history_written={feeder_result.history_written} " + f"pick_written={pick_result.written} " + f"errors={feeder_result.errors} " + f"耗時={feeder_result.duration_sec}s" + ) + _save_stats('pchome_match_backfill', stats) + + except Exception as e: + import traceback as _tb + logging.error(f"[Scheduler] [PChomeBackfill] 🚨 任務異常 | Error: {e}") + _save_stats('pchome_match_backfill', {"status": "Failed", "error": str(e)}) + try: + from services.event_router import notify_failure + notify_failure( + task_name="run_pchome_match_backfill_task", + error=e, + source="Scheduler.PChomeBackfill", + event_type="pchome_match_backfill_failure", + priority="P2", + title="PChome 待比對補抓任務異常", + trace=_tb.format_exc(), + ) + except Exception as _router_e: + logging.error(f"[Scheduler] [PChomeBackfill] event_router 失敗: {_router_e}") + + def run_icaim_analysis_task(): """ ICAIM 競價情報分析排程任務(每 6 小時執行一次) diff --git a/services/agent_actions.py b/services/agent_actions.py index bed6126..4ebe19b 100644 --- a/services/agent_actions.py +++ b/services/agent_actions.py @@ -77,6 +77,7 @@ def _store_action_memory( ALLOWED_RETRY_TASKS = { "run_auto_import_task", "run_momo_task", "run_edm_task", "run_competitor_price_feeder_task", "run_backup_monitor_task", + "run_pchome_match_backfill_task", "run_icaim_analysis_task", "run_festival_task", "run_whitepage_check", "run_icaim_analysis_task", "run_db_backup_task", "run_promo_event_task", } diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py index a7d608a..bae3b68 100644 --- a/tests/test_frontend_v2_assets.py +++ b/tests/test_frontend_v2_assets.py @@ -159,9 +159,20 @@ def test_ai_product_pick_agent_uses_real_competitor_data_and_dashboard_action(): assert "generate_product_pick_list(engine" in route_source assert "@ai_bp.route('/api/ai/pchome-match/backfill', methods=['POST'])" in route_source assert "run_unmatched_priority(limit=limit)" in route_source + assert "generate_product_pick_list(engine, limit=30)" in route_source + assert "完成後會重算 AI 挑品清單" in route_source assert "match_rate" in route_source assert "product_pick_count" in route_source + scheduler_source = (ROOT / "scheduler.py").read_text(encoding="utf-8") + run_scheduler_source = (ROOT / "run_scheduler.py").read_text(encoding="utf-8") + agent_actions_source = (ROOT / "services/agent_actions.py").read_text(encoding="utf-8") + assert "def run_pchome_match_backfill_task" in scheduler_source + assert "_save_stats('pchome_match_backfill'" in scheduler_source + assert "run_pchome_match_backfill_task" in run_scheduler_source + assert "每日 10:30:pchome_match_backfill" in run_scheduler_source + assert '"run_pchome_match_backfill_task"' in agent_actions_source + assert "產生挑品清單" in template assert "補抓待比對" in template assert "generatePickList" in template