diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 8874a78..6aef90f 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.311 統一競品價差語意:`fetch_competitor_comparison_results()`、competitor PPT 與 OpenClaw competitor prompt 全部改用 `MOMO - PChome`,正值代表 MOMO 較貴 / PChome 低價壓力,負值代表 MOMO 價格優勢;避免 daily/growth 顯示價格壓力但 PPT/AI 反向解讀。 - V10.310 強化 MOMO/PChome 核心比價閉環:PChome feeder 搜尋候選只有強同款 `0.90` 才提前停止,避免第一個 0.76 次佳候選卡掉後續精準搜尋詞;人工否決的候選會被跳過並改挑下一個候選,不再讓已否決商品長期阻塞同 SKU。人工 `reject_identity`、`unit_price_required`、`needs_research` 會立即讓同候選正式 `competitor_prices` 過期,Dashboard 即使尚有舊價也不再顯示正式總價差;手機版比價覆核欄位標籤、覆核按鈕冒泡與候選證據顯示同步修正。 - V10.308 修正商品列表 PChome 比價閉環狀態:`manual_rejected`、`manual_unit_price_required`、`manual_needs_research` 不再掉回籠統「待比對」,改顯示「人工已否決 / 人工標記單位價 / 人工要求補搜尋」與後續 feeder 行為說明,避免人工覆核後 UI 看起來像沒有處理。 - V10.307 將 PChome 人工覆核成效接進 daily/growth/PPT 共用資料出口:`fetch_competitor_coverage()` 讀取 `competitor_match_reviews` 最新決策,輸出人工採用、人工否決、人工單位價與採用率;`daily_sales` 與 `growth_analysis` 的比價資料品質區塊直接顯示這些閉環指標,讓報表與簡報不只看待審數,也能看人工處理成效。 diff --git a/config.py b/config.py index acf84b9..471574d 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.310" +SYSTEM_VERSION = "V10.311" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index ccf6d2f..89bee70 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -340,7 +340,7 @@ LEFT JOIN competitor_prices cp - Dashboard、AI pick、Hermes、Excel export、daily/growth 圖表與 competitor PPT 必須以 `competitor_prices + competitor_price_history + competitor_match_attempts` 為短期唯一生產真相源,且只消費 `identity_v2` matcher 驗證過的配對;舊版僅靠 `match_score` 的快取不可直接進入決策或簡報。 - `pchome_matches` 與 live `pchome_batch()` 僅保留 legacy compatibility,不得作為新簡報或 AI 決策主來源。 -- `services/competitor_intel_repository.py` 是下游頁面、圖表、簡報的共用查詢出口;新增消費端不得各自硬寫不同 match threshold。 +- `services/competitor_intel_repository.py` 是下游頁面、圖表、簡報的共用查詢出口;新增消費端不得各自硬寫不同 match threshold。所有競品報表的價差方向統一為 `MOMO - PChome`:正值代表 MOMO 較貴 / PChome 低價壓力,負值代表 MOMO 價格優勢;daily、growth、OpenClaw、PPT 不得使用反向定義。 - competitor PPT 不可只輸出 matched rows 造成覆蓋率假象;`fetch_competitor_comparison_results()` 必須用 `LEFT JOIN valid_competitor` 保留高營收/高價但尚未有效配對的 MOMO 商品,並帶出 `match_status`、`candidate_count`、`best_match_score` 與 `match_diagnostic`,讓簡報與 AI 文案明確區分「高信心比對」與「待補身份/價格」。 - `services/competitor_identity_revalidator.py` 可對既有 `competitor_prices` legacy row 離線重跑 `identity_v2`:只有新版 matcher 分數 `>= 0.76` 且無 hard veto 才補 `identity_v2` / `legacy_revalidated` tags;預設不刷新 `expires_at`,避免過期價格進入決策。 - `CompetitorPriceFeeder.run_expired_identity_refresh()` 會優先刷新已通過 `identity_v2` 但 TTL 過期的 PChome row:直接用既有 `competitor_product_id` 批次呼叫 PChome 商品 API,再用新版 matcher 重新驗證名稱/規格/價格 sanity,通過後寫回 `competitor_prices` 與 `competitor_price_history`。這條路徑提升新鮮價格覆蓋率,但不降低 match threshold,也不讓過期價格直接進入決策。 diff --git a/routes/openclaw_bot_routes.py b/routes/openclaw_bot_routes.py index bf0f8d2..c101a6f 100644 --- a/routes/openclaw_bot_routes.py +++ b/routes/openclaw_bot_routes.py @@ -2425,9 +2425,9 @@ def _ppt_ai_analysis(prompt_data: str, report_type: str = '') -> str: "═══════════════════════════════\n" "角色設定(絕對不可違反)\n" " 視角 = EwoooC 商品營運決策,不預設單一平台永遠正確\n" - " price_diff = PChome售價 − MOMO售價\n" - " 正值 → PChome較貴 / MOMO具價格優勢 → 可放大 MOMO 價格賣點\n" - " 負值 → PChome較便宜 / PChome低價壓力 → 需評估促銷、組合或服務差異化\n" + " price_diff = MOMO售價 − PChome售價\n" + " 正值 → MOMO較貴 / PChome低價壓力 → 需評估促銷、組合或服務差異化\n" + " 負值 → MOMO較便宜 / MOMO具價格優勢 → 可放大 MOMO 價格賣點\n" " 待補資料不可當成成功配對;必須明確列為資料品質風險\n" "═══════════════════════════════\n\n" f"請以 EwoooC 商品營運視角,針對以下{report_type}輸出一份專業競品分析報告:\n\n" @@ -3332,8 +3332,8 @@ def _generate_ppt_cmd(sub_type: str, sub_arg: str, _chat_id: int, target: str, ) found_c = [r for r in results if r.get('found')] - pchome_low_price_c = [r for r in found_c if r.get('price_diff', 0) < -10] - momo_low_price_c = [r for r in found_c if r.get('price_diff', 0) > 10] + pchome_low_price_c = [r for r in found_c if r.get('price_diff', 0) > 10] + momo_low_price_c = [r for r in found_c if r.get('price_diff', 0) < -10] unit_comparable_c = [ r for r in results if not r.get('found') and r.get('match_status') in ('unit_comparable', 'refresh_unit_comparable') @@ -3347,12 +3347,12 @@ def _generate_ppt_cmd(sub_type: str, sub_arg: str, _chat_id: int, target: str, f"分析週期:{period_label}\n" f"掃描商品:{len(results)} 件 | 高信心比對:{len(found_c)} 件 | 需單位價比較:{len(unit_comparable_c)} 件 | 待補身份/價格:{len(not_found_c)} 件\n" f"PChome 低價壓力(PChome 比 MOMO 便宜):{len(pchome_low_price_c)} 件 | MOMO 價格優勢(MOMO 比 PChome 便宜):{len(momo_low_price_c)} 件\n" - f"平均價差:{avg_diff_c:+.1f}%(正值=PChome較貴、MOMO具價格優勢;負值=PChome較便宜)\n\n" + f"平均價差:{avg_diff_c:+.1f}%(正值=MOMO較貴、PChome低價壓力;負值=MOMO具價格優勢)\n\n" f"PChome低價壓力 TOP3(需研擬因應):" + " / ".join( f"{r['momo_name'][:15]}(PChome便宜NT${abs(r['price_diff']):,.0f})" for r in pchome_low_price_c[:3]) + "\n" f"MOMO價格優勢 TOP3(可加強曝光):" + " / ".join( - f"{r['momo_name'][:15]}(MOMO便宜NT${r['price_diff']:,.0f})" + f"{r['momo_name'][:15]}(MOMO便宜NT${abs(r['price_diff']):,.0f})" for r in momo_low_price_c[:3]) + "\n" f"單位價覆核樣本:" + " / ".join( f"{r['momo_name'][:12]}({(r.get('unit_comparison') or {}).get('summary') or '候選價需換算'})" diff --git a/services/competitor_intel_repository.py b/services/competitor_intel_repository.py index 9930cd0..1e6d744 100644 --- a/services/competitor_intel_repository.py +++ b/services/competitor_intel_repository.py @@ -1057,8 +1057,8 @@ def fetch_competitor_comparison_results( la.error_message, la.attempted_at, {sales_select} - (vc.pchome_price - lm.momo_price) AS price_diff, - ((vc.pchome_price - lm.momo_price) / lm.momo_price * 100) AS price_diff_pct + (lm.momo_price - vc.pchome_price) AS price_diff, + ((lm.momo_price - vc.pchome_price) / vc.pchome_price * 100) AS price_diff_pct FROM latest_momo lm LEFT JOIN valid_competitor vc ON vc.sku = lm.sku LEFT JOIN latest_attempt la ON la.sku = lm.sku diff --git a/services/ppt_generator.py b/services/ppt_generator.py index b57ec97..44f7a98 100644 --- a/services/ppt_generator.py +++ b/services/ppt_generator.py @@ -2530,8 +2530,8 @@ def generate_competitor_ppt(period_label: str, db_data: dict, ai_text: str) -> s results = db_data.get("results", []) found = [r for r in results if r.get("found")] - pc_wins = [r for r in found if r.get("price_diff", 0) < -10] - mo_wins = [r for r in found if r.get("price_diff", 0) > 10] + pc_wins = [r for r in found if r.get("price_diff", 0) > 10] + mo_wins = [r for r in found if r.get("price_diff", 0) < -10] tie = [r for r in found if abs(r.get("price_diff", 0)) <= 10] unit_comparable = [ r for r in results @@ -2566,10 +2566,10 @@ def generate_competitor_ppt(period_label: str, db_data: dict, ai_text: str) -> s for i, (col, lbl, val, sub) in enumerate(cards): _kpi_card(s2, i * (card_w + card_gap) + 0.5, card_t, card_w, 3.2, col, lbl, val, sub) - trend_color = _RED_WARN if avg_pct < -3 else (_BRAND_OG if avg_pct > 3 else "FFA726") - trend_icon = "⚠️ PChome 整體較便宜" if avg_pct < -3 else ("✅ momo 整體具價格優勢" if avg_pct > 3 else "➖ 整體持平") + trend_color = _RED_WARN if avg_pct > 3 else (_BRAND_OG if avg_pct < -3 else "FFA726") + trend_icon = "⚠️ PChome 整體較便宜" if avg_pct > 3 else ("✅ momo 整體具價格優勢" if avg_pct < -3 else "➖ 整體持平") _add_rect(s2, 0.5, 5.3, W - 1, 0.85, trend_color) - _add_text(s2, f"整體定價態勢:{trend_icon} 平均價差 {avg_pct:+.1f}%(PChome - momo)", + _add_text(s2, f"整體定價態勢:{trend_icon} 平均價差 {avg_pct:+.1f}%(momo - PChome)", 0.7, 5.35, W - 1.4, 0.75, bold=True, size=13, color=_WHITE) bar_data = [ @@ -2613,8 +2613,8 @@ def generate_competitor_ppt(period_label: str, db_data: dict, ai_text: str) -> s x = 0.3 diff = r.get("price_diff", 0) pct = r.get("price_diff_pct", 0) - diff_c = _GREEN_KPI if diff < -10 else (_RED_WARN if diff > 10 else _DARK_TEXT) - trend = "PChome便宜" if diff < -10 else ("momo便宜" if diff > 10 else "持平") + diff_c = _RED_WARN if diff > 10 else (_GREEN_KPI if diff < -10 else _DARK_TEXT) + trend = "PChome便宜" if diff > 10 else ("momo便宜" if diff < -10 else "持平") cells = [ (str(ri + 1), _DARK_TEXT), (r.get("momo_name", "")[:24], _DARK_TEXT), diff --git a/tests/test_competitor_intel_cache.py b/tests/test_competitor_intel_cache.py index d32553a..9021b76 100644 --- a/tests/test_competitor_intel_cache.py +++ b/tests/test_competitor_intel_cache.py @@ -49,6 +49,19 @@ def test_competitor_ppt_results_keep_pending_diagnostics_in_export(): assert "\"unit_comparison\"" in source assert "build_unit_price_comparison" in source assert "(vc.pchome_price IS NULL)" in source + assert "(lm.momo_price - vc.pchome_price) AS price_diff" in source + assert "((lm.momo_price - vc.pchome_price) / vc.pchome_price * 100) AS price_diff_pct" in source + + +def test_competitor_ppt_and_ai_use_momo_minus_pchome_gap_direction(): + ppt_source = (ROOT / "services" / "ppt_generator.py").read_text(encoding="utf-8") + route_source = (ROOT / "routes" / "openclaw_bot_routes.py").read_text(encoding="utf-8") + + assert 'pc_wins = [r for r in found if r.get("price_diff", 0) > 10]' in ppt_source + assert 'mo_wins = [r for r in found if r.get("price_diff", 0) < -10]' in ppt_source + assert "平均價差 {avg_pct:+.1f}%(momo - PChome)" in ppt_source + assert "price_diff = MOMO售價 − PChome售價" in route_source + assert "正值=MOMO較貴、PChome低價壓力" in route_source def test_competitor_review_queue_is_canonical_unit_price_handoff():