From dcabebbcf2c48ed9bd98fbfb1014a9a10ac043d6 Mon Sep 17 00:00:00 2001 From: OoO Date: Sun, 24 May 2026 21:12:27 +0800 Subject: [PATCH] Expose PChome rescore review metrics --- .env.example | 1 + TODO_NEXT_STEPS.txt | 1 + config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 3 ++- docs/memory/code_modularization_inventory_20260430.md | 3 ++- docs/memory/history_logs.md | 1 + routes/dashboard_routes.py | 10 ++++++++++ services/competitor_intel_repository.py | 5 ++++- services/openclaw_strategist_service.py | 9 +++++++-- templates/daily_sales.html | 4 ++++ templates/dashboard_v2.html | 7 ++++--- templates/growth_analysis.html | 2 ++ tests/test_competitor_intel_cache.py | 6 +++++- tests/test_frontend_v2_assets.py | 7 ++++++- tests/test_openclaw_daily_template.py | 8 ++++++++ 15 files changed, 58 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index f9ee77a..b08b2dd 100644 --- a/.env.example +++ b/.env.example @@ -74,6 +74,7 @@ ALERT_WEBHOOK_PASSWORD=your_secure_webhook_password_here AUTO_FIX_ENABLED=true # --- GitLab CI/CD --- +GITLAB_ENABLED=false GITLAB_URL=http://192.168.0.110:8929 GITLAB_TOKEN=your_gitlab_token_here GITLAB_PROJECT_ID=1 diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index c913c29..5b6260c 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.450 補 PChome 覆核 fast-count UI 語意與重算可採用指標:預設全量覆核頁跳過 exact count 時,模板會顯示「約」作為快取總數提示;搜尋、分類、單一狀態仍是精準總數。`fetch_competitor_coverage()` 同步輸出 `rescore_accepted_count`,讓 Dashboard、daily/growth 與 OpenClaw 摘要能把「重算可採用待審」從一般覆核隊列拆出來。 - V10.449 修正 PChome 覆核 exact count 條件:只有預設「全部覆核、無搜尋、無分類」頁跳過 exact count;只要有搜尋詞、分類篩選或單一 review status,就保留精準總數,避免分頁資訊失準。 - V10.448 讓 PChome 覆核「全部」頁跳過 exact count:`review_status=all` 使用 shared overview cache 的待處理總數作為分頁總數提示,只查當頁 50 筆;單一狀態分流仍保留 exact count,降低全量覆核頁互動成本。 - V10.447 反轉 PChome 覆核頁查詢方向:review queue page 先從最新 `competitor_match_attempts` 的可覆核狀態縮小候選,再 join ACTIVE 商品與最新價,並用 `NOT EXISTS` 排除已有有效 identity_v2 正式價;避免每次「全部覆核」先掃全站 ACTIVE 商品。 diff --git a/config.py b/config.py index 4a6e6e7..7244064 100644 --- a/config.py +++ b/config.py @@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.449" +SYSTEM_VERSION = "V10.450" 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 e81090b..d0b7b30 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -2,7 +2,7 @@ > **最後更新**: 2026-05-24 (台北時間) > **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 備援預設關閉 -> **適用版本**: V10.449 +> **適用版本**: V10.450 --- @@ -81,6 +81,7 @@ SQL漏斗(~300筆) - 排程閉環:`run_pchome_match_backfill_task` 每日 10:30 執行,補抓 PChome 待比對商品、寫入歷史價格,再重算 `strategy='product_pick'` 清單。 - PChome / MOMO 競價摘要出口 `services/competitor_intel_repository.py` 使用 30 分鐘共享快取(`COMPETITOR_INTEL_CACHE_TTL_SECONDS` 可調),避免 `/growth_analysis`、`/daily_sales`、PPT/AI 報表每次請求重跑昂貴覆蓋率與價差趨勢查詢;`run_competitor_price_feeder_task` 與 PChome backfill 完成後會主動清除快取。快取只包摘要輸出,不改 matcher 的高信心門檻與 identity_v2 準確性規則。 - 商品看板第一屏:`/` 的 V2 看板直接以 `products`、`price_records`、`competitor_prices`、`competitor_match_attempts`、`competitor_match_reviews`、`ai_price_recommendations` 顯示比對覆蓋率、PChome 優勢、MOMO 威脅、AI 挑品、待比對優先清單與 PChome 覆核隊列;`filter=ai_picks` 可查看 50 品 AI 挑品列表,`filter=pchome_review` 可直接查看需人工處理的比價覆核 SKU,並以 DB 分頁支援 search/category/status 後的完整隊列,不得只截前 50 筆。覆核狀態篩選必須至少包含全部、需單位價、已排除、低信心、價格過期、找不到同款與人工閉環,讓人工可依 matcher 診斷類型分批處理。列內顯示候選 PChome 商品、候選價、match score、單位價換算摘要、人工動作與 matcher 診斷原因標籤(品牌不符、商品線不符、容量差異、組合差異、需單位價、價差極端等),不得只顯示籠統「待比對」。`/api/export/excel/pchome-review` 必須匯出同一套覆核隊列、人工處置、候選 PChome、單位價比較與原始診斷,讓人工覆核、簡報與後續 AI 分析共用同一份證據。`/api/pchome-review//decision` 是人工閉環入口:`accept_identity` 才可把候選寫入 `competitor_prices` 與 `competitor_price_history` 並打上 `manual_review/manual_accept/identity_v2`;`reject_identity`、`unit_price_required` 與 `needs_research` 只寫 `competitor_match_reviews` 並追加 manual attempt,不得把不同販售組合或否決候選灌入正式價差。PChome feeder 後續搜尋同一候選時必須讀取 `competitor_match_reviews`:已否決候選寫 `manual_rejected` 並跳過正式寫入,且必須繼續評估下一個候選,不能讓已否決候選長期阻塞同 SKU;已標記單位價候選寫 `manual_unit_price_required`;已要求補搜尋候選寫 `manual_needs_research` 並停留在覆核隊列;已採用候選可保守補到最低門檻並保留 `manual_review/manual_accept` 標籤。搜尋候選池只有強同款分數達 `0.90` 才可提前停止,避免 0.76 灰區候選卡掉後續更精準搜尋詞。人工 `reject_identity`、`unit_price_required`、`needs_research` 若命中當前正式候選,必須將同候選 `competitor_prices` 過期,不得繼續顯示正式總價差。商品列表必須將 `manual_rejected`、`manual_unit_price_required`、`manual_needs_research` 顯示為明確人工閉環狀態,不可回落成籠統「待比對」。`fetch_competitor_coverage()` 必須輸出人工採用、人工否決、人工單位價與採用率,daily/growth/PPT 共用 payload 必須顯示人工閉環成效,避免只呈現待審數。商品看板深度快取同時寫入 `data/dashboard_full_cache.pkl`,供多個 Gunicorn worker 共用,避免部署後各 worker 重複重建 7,000+ 商品統計造成開頁變慢;所有資料異動與 AI 挑品重算都透過 `clear_dashboard_cache()` 同步清除記憶體與共享快取,手動重算 API 會立即預熱商品看板快取,避免第一位使用者承擔重建成本。 +- PChome re-score 回收線:`rescore_accepted_current` 只能表示最新版 matcher 判定「可人工採用」,不可直接寫入正式 `competitor_prices`;`fetch_competitor_coverage()` 必須輸出 `rescore_accepted_count`,Dashboard、daily/growth 與 OpenClaw 競品摘要都要把「重算可採用待審」獨立呈現,避免和一般低信心/單位價覆核混在一起。 | 角色 | 模型 | 主機 | 成本 | 每日限額 | |------|------|------|------|---------| diff --git a/docs/memory/code_modularization_inventory_20260430.md b/docs/memory/code_modularization_inventory_20260430.md index 777def0..c8f109a 100644 --- a/docs/memory/code_modularization_inventory_20260430.md +++ b/docs/memory/code_modularization_inventory_20260430.md @@ -50,6 +50,7 @@ - 2026-05-21 追記:同步 browse.sh 診斷計畫寫入 `competitor_match_attempts` 後的 `services/competitor_price_feeder.py` 行數;此處只更新 inventory,不變更模組化決策。 - 2026-05-24 追記:同步背景 PChome 近門檻身份回收與 focused identity 系列更新後的 `services/marketplace_product_matcher.py` 行數;此處只更新 inventory,不變更商品比對行為。 - 2026-05-24 追記:同步 111 fallback circuit breaker、NemoTron 決策信封與 Telegram template governance 後的 `run_scheduler.py`、`services/ollama_service.py`、`services/nemoton_dispatcher_service.py`、`services/telegram_templates.py` 行數;此處只更新 inventory,不變更模組化決策。 +- 2026-05-24 追記:同步 PChome 覆核頁 fast-count、輕量 render 與重算可採用指標後的 `routes/dashboard_routes.py` 行數;此處只更新 inventory,不變更 dashboard 行為。 ## 達到或超過 800 行檔案清單 @@ -63,7 +64,7 @@ | 3681 | `routes/admin_observability_routes.py` | P0 觀測台巨型 Blueprint | `services/observability_query_service.py` / `services/observability_action_service.py` / route glue | | 1796 | `routes/ai_routes.py` | P1 AI Blueprint | route glue / AI orchestration service / prompt builders | | 2154 | `services/nemoton_dispatcher_service.py` | P1 NemoTron service | NIM client / tool-call parser / action dispatcher | -| 2026 | `routes/dashboard_routes.py` | P1 Dashboard Blueprint | competitor decision overview / dashboard query service;首頁資料整併需抽 service | +| 2535 | `routes/dashboard_routes.py` | P1 Dashboard Blueprint | competitor decision overview / dashboard query service;首頁資料整併需抽 service | | 1485 | `routes/vendor_routes.py` | P1 Vendor Blueprint | route glue / stockout mutation/email;V2 page query、stockout list/batches API query、vendor list/detail query 已抽到 `services/vendor_stockout_query_service.py` | | 1390 | `services/telegram_bot_service.py` | P1 Telegram service | command handlers / message formatters / bot client | | 1237 | `app.py` | P1 bootstrap | 保持只做 app setup;繼續往 app_factory / extension setup 抽;Phase 42 只做 metadata table name 對齊 | diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index 29e6a60..1314a8c 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-05-24:PChome 近門檻身份回收第二輪 +- **V10.450 PChome 覆核 fast-count UI 語意與重算可採用指標**: 預設全量覆核頁跳過 exact count 時,模板會以「約」標記快取總數,避免操作員把快取總數誤認為即時計算;搜尋、分類與單一狀態分流仍保留精準總數。`fetch_competitor_coverage()` 同步輸出 `rescore_accepted_count`,Dashboard、daily/growth 與 OpenClaw 摘要會把「重算可採用待審」獨立顯示,不再只混在一般覆核隊列。 - **V10.449 PChome 覆核 exact count 條件修正**: 只有預設「全部覆核、無搜尋、無分類」頁跳過 exact count;若使用搜尋詞、分類篩選或單一 review status,仍保留精準總數,避免操作員分頁資訊失準。 - **V10.448 PChome 覆核全量頁跳過 exact count**: `review_status=all` 改用 shared overview cache 的待處理總數作為分頁總數提示,當頁只查 50 筆;單一狀態分流仍保留 exact count,避免每次操作全量覆核頁都為總筆數重掃整個 review queue。 - **V10.447 PChome 覆核頁查詢方向反轉**: review queue page 改由最新 `competitor_match_attempts` 的可覆核狀態先縮小候選,再 join ACTIVE 商品與最新價,並用 `NOT EXISTS` 排除已有有效 `identity_v2` 正式 PChome 價格;避免「全部覆核」每次先掃全站 ACTIVE 商品後才過濾,提高核心比價覆核頁可操作性。 diff --git a/routes/dashboard_routes.py b/routes/dashboard_routes.py index 32676a5..1728996 100644 --- a/routes/dashboard_routes.py +++ b/routes/dashboard_routes.py @@ -676,6 +676,11 @@ def _merge_competitor_review_context(overview, review_context): overview.update({ 'review_queue_count': int(coverage.get('actionable_review_count') or len(review_queue) or 0), 'unit_comparable_count': int(coverage.get('unit_comparable_count') or 0), + 'rescore_accepted_count': int( + coverage.get('rescore_accepted_count') + or review_status_counts.get('rescore_accepted') + or 0 + ), 'review_status_counts': review_status_counts, 'review_queue': review_queue[:3], }) @@ -819,6 +824,7 @@ def _load_competitor_decision_overview(session, latest_items=None): 'pending_priority': [], 'review_queue_count': 0, 'unit_comparable_count': 0, + 'rescore_accepted_count': 0, 'review_queue': [], } @@ -1688,6 +1694,7 @@ def _load_cached_competitor_overview_for_review(now_taipei, review_queue, review if not overview.get('review_queue'): overview['review_queue'] = list(review_queue[:3]) overview.setdefault('unit_comparable_count', 0) + overview.setdefault('rescore_accepted_count', 0) return overview @@ -1727,6 +1734,7 @@ def _render_pchome_review_dashboard( ) review_queue = review_page.get('items') or [] review_queue_total = int(review_page.get('total') or 0) + review_total_is_estimated = False if review_queue_total < 0: review_queue_total = int( (overview_hint.get('review_status_counts') or {}).get('all') @@ -1734,6 +1742,7 @@ def _render_pchome_review_dashboard( or len(review_queue) or 0 ) + review_total_is_estimated = True review_queue_map = { str(row.get('sku') or ''): row for row in review_queue @@ -1774,6 +1783,7 @@ def _render_pchome_review_dashboard( current_category=category_filter, current_filter=filter_type, current_review_status=review_status, + review_total_is_estimated=review_total_is_estimated, review_status_options=review_status_options, search_query=search_query, current_sort=sort_by, diff --git a/services/competitor_intel_repository.py b/services/competitor_intel_repository.py index e0d5fb3..7e940f7 100644 --- a/services/competitor_intel_repository.py +++ b/services/competitor_intel_repository.py @@ -403,7 +403,7 @@ def _cached_payload(cache_key: str, producer, ttl_seconds: int = COMPETITOR_INTE def fetch_competitor_coverage(engine) -> dict: return _cached_payload( - f"coverage:v4:floor={PCHOME_MATCH_SCORE_FLOOR}:manual_reviews=1", + f"coverage:v5:floor={PCHOME_MATCH_SCORE_FLOOR}:manual_reviews=1:rescore=1", lambda: _fetch_competitor_coverage_uncached(engine), ) @@ -422,6 +422,7 @@ def _fetch_competitor_coverage_uncached(engine) -> dict: "match_rate": 0, "attempt_status": {}, "unit_comparable_count": 0, + "rescore_accepted_count": 0, "actionable_review_count": 0, "manual_review_summary": manual_review_summary, "manual_review_total": manual_review_summary["total"], @@ -509,6 +510,7 @@ def _fetch_competitor_coverage_uncached(engine) -> dict: for row in rows } unit_count = sum(statuses.get(status, 0) for status in UNIT_COMPARABLE_STATUSES) + rescore_accepted_count = int(statuses.get("rescore_accepted_current") or 0) actionable_count = sum(statuses.get(status, 0) for status in ACTIONABLE_ATTEMPT_STATUSES) return { "active_with_price": active, @@ -517,6 +519,7 @@ def _fetch_competitor_coverage_uncached(engine) -> dict: "match_rate": round(valid / max(active, 1) * 100, 1), "attempt_status": statuses, "unit_comparable_count": unit_count, + "rescore_accepted_count": rescore_accepted_count, "actionable_review_count": actionable_count, "manual_review_summary": manual_review_summary, "manual_review_total": manual_review_summary["total"], diff --git a/services/openclaw_strategist_service.py b/services/openclaw_strategist_service.py index ea79f04..72d3234 100644 --- a/services/openclaw_strategist_service.py +++ b/services/openclaw_strategist_service.py @@ -592,7 +592,8 @@ def _fetch_competitor_summary() -> Dict[str, Any]: ) SELECT SUM(CASE WHEN attempt_status IN ('unit_comparable', 'refresh_unit_comparable') THEN 1 ELSE 0 END) AS unit_comparable_count, - SUM(CASE WHEN attempt_status IN ('unit_comparable', 'refresh_unit_comparable', 'identity_veto', 'low_score', 'refresh_low_score', 'recoverable_low_score', 'true_low_confidence', 'protected_existing_match', 'expired_match', 'no_result', 'refresh_no_result') THEN 1 ELSE 0 END) AS review_queue_count + SUM(CASE WHEN attempt_status = 'rescore_accepted_current' THEN 1 ELSE 0 END) AS rescore_accepted_count, + SUM(CASE WHEN attempt_status IN ('rescore_accepted_current', 'unit_comparable', 'refresh_unit_comparable', 'identity_veto', 'low_score', 'refresh_low_score', 'recoverable_low_score', 'true_low_confidence', 'protected_existing_match', 'expired_match', 'no_result', 'refresh_no_result') THEN 1 ELSE 0 END) AS review_queue_count FROM latest_attempt """)).fetchone() return { @@ -601,7 +602,8 @@ def _fetch_competitor_summary() -> Dict[str, Any]: "undercut_count": int(row[2] or 0), "premium_count": int(row[3] or 0), "unit_comparable_count": int((attempt_row[0] if attempt_row else 0) or 0), - "review_queue_count": int((attempt_row[1] if attempt_row else 0) or 0), + "rescore_accepted_count": int((attempt_row[1] if attempt_row else 0) or 0), + "review_queue_count": int((attempt_row[2] if attempt_row else 0) or 0), } return {} except Exception as e: @@ -1487,6 +1489,7 @@ def generate_weekly_strategy_report( 被競品削價數:{competitor_summary.get('undercut_count', 0)} 個 我方具優勢數:{competitor_summary.get('premium_count', 0)} 個 需單位價覆核:{competitor_summary.get('unit_comparable_count', 0)} 個 + 重算可採用待審:{competitor_summary.get('rescore_accepted_count', 0)} 個 TOP 威脅品項(近48h Hermes 偵測): {_format_threats(threats)} @@ -1767,6 +1770,7 @@ def _legacy_full_gemini_daily_report() -> dict: 被削價風險:{competitor_summary.get('undercut_count', 0)} 個(價差超過10%) 平均價差:{competitor_summary.get('avg_gap_pct', 0):+.1f}% 單位價/身份覆核隊列:{competitor_summary.get('review_queue_count', 0)} 個 + 重算可採用待審:{competitor_summary.get('rescore_accepted_count', 0)} 個 請按以下結構輸出(使用 HTML 標題): @@ -1944,6 +1948,7 @@ def generate_monthly_report() -> dict: 月均價差:{competitor_summary.get('avg_gap_pct', 0):+.1f}% 被削價風險SKU:{competitor_summary.get('undercut_count', 0)} 個 需單位價覆核SKU:{competitor_summary.get('unit_comparable_count', 0)} 個 + 重算可採用待審SKU:{competitor_summary.get('rescore_accepted_count', 0)} 個 【價格變動概況】 本月調價次數:{price_trend_data.get('price_changes', 0)} 次 diff --git a/templates/daily_sales.html b/templates/daily_sales.html index 40409e8..152a242 100644 --- a/templates/daily_sales.html +++ b/templates/daily_sales.html @@ -359,6 +359,10 @@ 需單位價覆核 {{ comp_coverage.unit_comparable_count | default(0) | number_format }} +
+ 重算可採用待審 + {{ comp_coverage.rescore_accepted_count | default(0) | number_format }} +
人工採用 {{ comp_coverage.manual_accept_count | default(0) | number_format }} diff --git a/templates/dashboard_v2.html b/templates/dashboard_v2.html index b4a5259..05e1996 100644 --- a/templates/dashboard_v2.html +++ b/templates/dashboard_v2.html @@ -42,7 +42,8 @@
比價覆核
{{ overview.review_queue_count | default(0) | number_format }}
@@ -61,7 +62,7 @@
PCHOME MATCH BACKFILL
待比對補抓產線
- 待補抓 {{ overview.pending_match_count | default(0) | number_format }} · 覆核 {{ overview.review_queue_count | default(0) | number_format }} · 單位價 {{ overview.unit_comparable_count | default(0) | number_format }} + 待補抓 {{ overview.pending_match_count | default(0) | number_format }} · 覆核 {{ overview.review_queue_count | default(0) | number_format }} · 重算可採用 {{ overview.rescore_accepted_count | default(0) | number_format }} · 單位價 {{ overview.unit_comparable_count | default(0) | number_format }}