feat(ai): 自動補抓並重算 PChome 挑品
All checks were successful
CD Pipeline / deploy (push) Successful in 2m18s
All checks were successful
CD Pipeline / deploy (push) Successful in 2m18s
This commit is contained in:
@@ -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
|
||||
|
||||
---
|
||||
|
||||
9
app.py
9
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()
|
||||
|
||||
@@ -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'` 清單。
|
||||
|
||||
| 角色 | 模型 | 主機 | 成本 | 每日限額 |
|
||||
|------|------|------|------|---------|
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
60
scheduler.py
60
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 小時執行一次)
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user