feat(ai): 自動補抓並重算 PChome 挑品
All checks were successful
CD Pipeline / deploy (push) Successful in 2m18s

This commit is contained in:
OoO
2026-05-01 14:02:37 +08:00
parent 043a7dc915
commit 8a3d50933b
8 changed files with 91 additions and 8 deletions

View File

@@ -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
View File

@@ -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()

View File

@@ -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'` 清單。
| 角色 | 模型 | 主機 | 成本 | 每日限額 |
|------|------|------|------|---------|

View File

@@ -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

View File

@@ -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_backup03:00、cleanup_agent_context03:30、backup_monitor04:00、daily_report09:00、ai_smoke_summary09:10
每 1 天 db_backup03:00、cleanup_agent_context03:30、backup_monitor04:00、daily_report09:00、ai_smoke_summary09:10、pchome_match_backfill10: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:10ai_smoke_daily_summary")
schedule.every().day.at("10:30").do(run_pchome_match_backfill_task)
logger.info("📅 每日 10:30pchome_match_backfill")
# 每月1日 07:00 月報schedule 不支援 every().month用每日 07:00 + 日期判斷)
def _monthly_report_gate():
from datetime import datetime as _dt

View File

@@ -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 小時執行一次)

View File

@@ -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",
}

View File

@@ -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:30pchome_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