diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 6077e7f..82a1b27 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,8 @@ ================================================================================ 【已完成】 + - V10.298 補市場情報 candidate queue review AI summary persistence run receipt:新增 `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_run_receipt` 與 UI 按鈕,審核操作員貼回的 metadata_json CLI writer output、post-write smoke、dedupe key、summary payload hash、artifact path 與 token 外洩風險;API/UI 仍不讀 approval token、不執行 CLI、不連 DB、不寫 `metadata_json`、不派送 Telegram、不掛 scheduler。 + - V10.297 將 PChome 單位價覆核隊列接回商品看板第一屏:KPI 顯示待處理/需單位價覆核數,焦點區列出候選 PChome 商品、候選價、match score 與人工動作;新增 `filter=pchome_review` 的比價覆核隊列,讓使用者可直接進入待處理 SKU,不再只在 daily/growth/PPT 摘要看到統計。 - V10.296 補核心 MOMO/PChome 比價第三層語意與覆核閉環:同核心商品但買送、套組、件數不同且只有單一基礎規格時標記 `unit_comparable`,只寫入 `competitor_match_attempts`;商品看板、daily/growth 報表、OpenClaw/PPT 摘要共用 `competitor_intel_repository` 的覆核隊列,顯示「需單位價比較」、候選商品、候選 PChome 價格與單位價換算證據;多容量/多品項套組仍保持不可比較,避免把不同販售組合直接寫進正式總價差。 - V10.289 重排 Elephant Alpha L3 HITL `ea_escalation` Telegram 告警:改成專業 incident brief 格式,分成決策狀態、背景摘要、風險摘要、TOP 待審 SKU 與建議處置;價格行動會拆出 MOMO/PChome 價格、價差、人工處置與 PChome ID,避免長 bullet 難讀。 - V10.284 關閉 Code Review Hermes LLM scan 預設路徑:Step 2 改 deterministic fast static scan,不再讓部署後先卡三段 Ollama timeout;若需要 LLM scan 可用 `CODE_REVIEW_HERMES_LLM_SCAN_ENABLED=true` 顯式開啟,仍只走本地矩陣、不走 Gemini。 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index 6ca5dcb..58abbc2 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -2,7 +2,7 @@ > **最後更新**: 2026-05-20 (台北時間) > **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 僅備援 / 鎖定場景 -> **適用版本**: V10.296 +> **適用版本**: V10.297 --- @@ -56,7 +56,7 @@ SQL漏斗(~300筆) - 比對覆蓋率補強入口:`POST /api/ai/pchome-match/backfill`,優先補抓仍無有效 PChome 配對的高價 ACTIVE 商品,完成後自動重算 AI 挑品清單。 - 排程閉環:`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`、`ai_price_recommendations` 顯示比對覆蓋率、PChome 優勢、MOMO 威脅、AI 挑品與待比對優先清單;`filter=ai_picks` 可查看 50 品 AI 挑品列表,並在列表上方顯示平均信心、平均價差、最大價差與估算總價差空間,列表列內顯示 AI 排名與建議理由,且可透過 `/api/export/excel/ai-picks` 匯出 50 品 Excel 操作清單。商品看板深度快取同時寫入 `data/dashboard_full_cache.pkl`,供多個 Gunicorn worker 共用,避免部署後各 worker 重複重建 7,000+ 商品統計造成開頁變慢;所有資料異動與 AI 挑品重算都透過 `clear_dashboard_cache()` 同步清除記憶體與共享快取,手動重算 API 會立即預熱商品看板快取,避免第一位使用者承擔重建成本。 +- 商品看板第一屏:`/` 的 V2 看板直接以 `products`、`price_records`、`competitor_prices`、`competitor_match_attempts`、`ai_price_recommendations` 顯示比對覆蓋率、PChome 優勢、MOMO 威脅、AI 挑品、待比對優先清單與 PChome 覆核隊列;`filter=ai_picks` 可查看 50 品 AI 挑品列表,`filter=pchome_review` 可直接查看需人工處理的比價覆核 SKU,列內顯示候選 PChome 商品、候選價、match score、單位價換算摘要與人工動作。商品看板深度快取同時寫入 `data/dashboard_full_cache.pkl`,供多個 Gunicorn worker 共用,避免部署後各 worker 重複重建 7,000+ 商品統計造成開頁變慢;所有資料異動與 AI 挑品重算都透過 `clear_dashboard_cache()` 同步清除記憶體與共享快取,手動重算 API 會立即預熱商品看板快取,避免第一位使用者承擔重建成本。 | 角色 | 模型 | 主機 | 成本 | 每日限額 | |------|------|------|------|---------| diff --git a/routes/dashboard_routes.py b/routes/dashboard_routes.py index bf88a22..d1bd2c1 100644 --- a/routes/dashboard_routes.py +++ b/routes/dashboard_routes.py @@ -406,21 +406,36 @@ def _load_pchome_match_attempt_map(session, skus): try: stmt = text(""" - WITH ranked AS ( + WITH latest_momo AS ( SELECT - sku, - attempt_status, - candidate_count, - best_competitor_product_id, - best_competitor_product_name, - best_competitor_price, - best_match_score, - error_message, - attempted_at, - ROW_NUMBER() OVER (PARTITION BY sku ORDER BY attempted_at DESC) AS rn - FROM competitor_match_attempts - WHERE source = 'pchome' - AND sku IN :skus + p.i_code AS sku, + p.name AS momo_product_name, + pr.price AS momo_price, + ROW_NUMBER() OVER (PARTITION BY p.id ORDER BY pr.timestamp DESC, pr.id DESC) AS rn + FROM products p + JOIN price_records pr ON pr.product_id = p.id + WHERE p.i_code IN :skus + ), + ranked AS ( + SELECT + cma.sku, + cma.attempt_status, + cma.candidate_count, + cma.best_competitor_product_id, + cma.best_competitor_product_name, + cma.best_competitor_price, + cma.best_match_score, + cma.error_message, + cma.attempted_at, + lm.momo_product_name, + lm.momo_price, + ROW_NUMBER() OVER (PARTITION BY cma.sku ORDER BY cma.attempted_at DESC) AS rn + FROM competitor_match_attempts cma + LEFT JOIN latest_momo lm + ON lm.sku = cma.sku + AND lm.rn = 1 + WHERE cma.source = 'pchome' + AND cma.sku IN :skus ) SELECT * FROM ranked @@ -458,6 +473,42 @@ def _format_dashboard_dt(value): return str(value) +def _get_session_engine(session): + try: + return session.get_bind() + except Exception: + return getattr(session, 'bind', None) + + +def _load_competitor_review_context(session, limit=12): + try: + from services.competitor_intel_repository import ( + fetch_competitor_coverage, + fetch_competitor_review_queue, + ) + engine = _get_session_engine(session) + if not engine: + return {'coverage': {}, 'review_queue': []} + return { + 'coverage': fetch_competitor_coverage(engine) or {}, + 'review_queue': fetch_competitor_review_queue(engine, limit=limit) or [], + } + except Exception as exc: + sys_log.warning(f"[Dashboard] PChome 覆核隊列讀取略過: {exc}") + return {'coverage': {}, 'review_queue': []} + + +def _merge_competitor_review_context(overview, review_context): + coverage = review_context.get('coverage') or {} + review_queue = review_context.get('review_queue') or [] + 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), + 'review_queue': review_queue[:3], + }) + return overview + + def _parse_agent_footprint(value): if not value: return {} @@ -538,6 +589,9 @@ def _load_competitor_decision_overview(session, latest_items=None): 'top_pchome_advantages': [], 'top_momo_threats': [], 'pending_priority': [], + 'review_queue_count': 0, + 'unit_comparable_count': 0, + 'review_queue': [], } if latest_items: @@ -658,6 +712,10 @@ def _load_competitor_decision_overview(session, latest_items=None): } for row in sorted(pending_items, key=lambda row: row['momo_price'], reverse=True)[:3] ] + _merge_competitor_review_context( + overview, + _load_competitor_review_context(session, limit=12), + ) _DASHBOARD_DATA_CACHE[cache_key] = overview _DASHBOARD_DATA_CACHE[cache_ts_key] = time.time() return overview @@ -852,6 +910,10 @@ def _load_competitor_decision_overview(session, latest_items=None): } for row in session.execute(pending_sql).mappings().all() ] + _merge_competitor_review_context( + overview, + _load_competitor_review_context(session, limit=12), + ) _DASHBOARD_DATA_CACHE[cache_key] = overview _DASHBOARD_DATA_CACHE[cache_ts_key] = time.time() return overview @@ -861,6 +923,10 @@ def _load_competitor_decision_overview(session, latest_items=None): session.rollback() except Exception: pass + _merge_competitor_review_context( + default, + _load_competitor_review_context(session, limit=12), + ) _DASHBOARD_DATA_CACHE[cache_key] = default _DASHBOARD_DATA_CACHE[cache_ts_key] = time.time() return default @@ -1643,9 +1709,24 @@ def index(): ai_pick_skus = [] ai_pick_map = {} ai_pick_summary = None + review_queue = [] + review_queue_map = {} + review_queue_order = {} if filter_type == 'ai_picks': ai_pick_skus, ai_pick_map = _load_ai_pick_selection(session, PRODUCT_PICK_LIST_LIMIT) ai_pick_summary = _summarize_ai_pick_selection(ai_pick_map) + elif filter_type == 'pchome_review': + review_context = _load_competitor_review_context(session, limit=50) + review_queue = review_context.get('review_queue') or [] + review_queue_map = { + str(row.get('sku') or ''): row + for row in review_queue + if row.get('sku') + } + review_queue_order = { + sku: idx + for idx, sku in enumerate(review_queue_map.keys(), start=1) + } # 先處理搜尋 if search_query: @@ -1671,6 +1752,12 @@ def index(): i for i in base_items if str(i['record'].product.i_code) in pick_set ] + elif filter_type == 'pchome_review': + review_set = set(review_queue_map.keys()) + filtered_items = [ + i for i in base_items + if str(i['record'].product.i_code) in review_set + ] elif filter_type == 'delisted': for item in today_delisted_items: class DelistedRecord: @@ -1720,6 +1807,9 @@ def index(): if filter_type == 'ai_picks': sku = str(item['record'].product.i_code) return -ai_pick_map.get(sku, {}).get('rank', 9999) + if filter_type == 'pchome_review': + sku = str(item['record'].product.i_code) + return -review_queue_order.get(sku, 9999) return item['record'].timestamp sorted_items = sorted(filtered_items, key=get_sort_key, reverse=reverse) @@ -1736,6 +1826,7 @@ def index(): item['safe_created_at'] = getattr(item['record'].product, 'created_at', None) sku = str(item['record'].product.i_code) item['ai_pick'] = ai_pick_map.get(sku) + item['pchome_review'] = review_queue_map.get(sku) item['safe_momo_url'] = ( item.get('safe_product_url') or normalize_momo_product_url(item['record'].product.url, sku) @@ -1820,6 +1911,7 @@ def index(): most_active_count=most_active_count, competitor_overview=competitor_overview, ai_pick_list_limit=PRODUCT_PICK_LIST_LIMIT, + build_momo_product_url=_build_momo_product_url, active_page='dashboard') except Exception as e: sys_log.error(f"[Web] [Dashboard] 渲染錯誤 | Error: {e}") diff --git a/templates/dashboard_v2.html b/templates/dashboard_v2.html index d710a0d..f91d8b2 100644 --- a/templates/dashboard_v2.html +++ b/templates/dashboard_v2.html @@ -39,9 +39,12 @@
-
待比對
-
{{ 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 }} +
資料新鮮度
@@ -143,8 +146,42 @@
-
補資料優先
- {% if overview.pending_priority %} +
覆核與補資料
+ {% if overview.review_queue %} +
+ {% for item in overview.review_queue %} +
+ {{ item.name }} +
+ {{ item.status_label }} + MOMO ${{ item.momo_price | int | number_format }} + {% if item.candidate_pc_price %} + 候選 PChome ${{ item.candidate_pc_price | int | number_format }} + {% endif %} + {% if item.best_match_score %} + match {{ (item.best_match_score * 100) | round(0) | int }}% + {% endif %} +
+
{{ item.action_label }}
+ {% if item.unit_comparison and item.unit_comparison.summary %} +
{{ item.unit_comparison.summary }}
+ {% endif %} + +
+ {% endfor %} +
+ 查看高優先覆核隊列 + {% elif overview.pending_priority %}
{% for item in overview.pending_priority %}
@@ -172,8 +209,8 @@ {% endfor %}
{% else %} -
待比對清單已清空
-
目前 ACTIVE 商品都有有效 PChome 配對或尚無最新 MOMO 價格
+
覆核隊列已清空
+
目前 ACTIVE 商品沒有高優先 PChome 覆核項目
{% endif %}
@@ -203,6 +240,7 @@
全部 AI挑品 + 比價覆核 新上架 漲價 降價 @@ -223,10 +261,12 @@
04 - {{ 'AI 挑品清單' if current_filter == 'ai_picks' else '商品列表' }} + {{ 'AI 挑品清單' if current_filter == 'ai_picks' else ('比價覆核隊列' if current_filter == 'pchome_review' else '商品列表') }} {% if current_filter == 'ai_picks' %} {{ total_items | number_format }} / {{ ai_pick_list_limit }} 品 + {% elif current_filter == 'pchome_review' %} + {{ total_items | number_format }} / {{ overview.review_queue_count | default(0) | number_format }} 待處理 {% else %} {{ total_items | number_format }} 筆 {% endif %} @@ -293,7 +333,7 @@ {% endif %}
- +
@@ -305,6 +345,8 @@ {% if current_filter == 'ai_picks' %} + {% elif current_filter == 'pchome_review' %} + {% endif %} {% endif %} {% else %} -
分類競價判讀AI 建議覆核動作 昨日漲跌 @@ -442,6 +484,35 @@ 尚無建議理由 {% endif %} + {% elif current_filter == 'pchome_review' %} + + {% set review = item.pchome_review %} +
+
+ {{ review.status_label if review else match_status.label }} + {% if review and review.best_match_score %} + match {{ (review.best_match_score * 100) | round(0) | int }}% + {% endif %} +
+
{{ review.action_label if review else decision.summary }}
+ {% if review %} +
+ {% if review.candidate_count %} + {{ review.candidate_count }} 筆候選 + {% endif %} + {% if review.candidate_pc_id %} + PChome {{ review.candidate_pc_id }} + {% endif %} + {% if review.attempted_at %} + {{ review.attempted_at }} + {% endif %} +
+ {% if review.unit_comparison and review.unit_comparison.summary %} +
{{ review.unit_comparison.summary }}
+ {% endif %} + {% endif %} +
+
{% if item.yesterday_diff > 0 %} @@ -471,7 +542,7 @@
+
{% if search_query %} 找不到與「{{ search_query }}」相關的商品 diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py index 409b9c7..e7cf66e 100644 --- a/tests/test_frontend_v2_assets.py +++ b/tests/test_frontend_v2_assets.py @@ -130,11 +130,15 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data(): assert "warm_full_dashboard_cache" in route_source assert "force_rebuild=False" in route_source assert "def _load_competitor_decision_overview(session, latest_items=None)" in route_source + assert "fetch_competitor_review_queue" in route_source assert "_load_competitor_decision_overview(session, unique_items)" in route_source assert "item_map = {}" in route_source assert "competitor_map = _load_pchome_competitor_map(session, item_map.keys())" in route_source assert "ai_price_recommendations" in route_source assert "pending_match_count" in route_source + assert "review_queue_count" in route_source + assert "unit_comparable_count" in route_source + assert "filter_type == 'pchome_review'" in route_source assert "MockRecord" not in route_source assert "{% for item in items %}" in dashboard assert "比價監控總覽" in dashboard @@ -143,8 +147,13 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data(): assert "overview.top_picks" in dashboard assert "overview.top_momo_threats" in dashboard assert "overview.pending_priority" in dashboard + assert "overview.review_queue" in dashboard + assert "需單位價覆核" in dashboard assert "filter='ai_picks'" in dashboard + assert "filter='pchome_review'" in dashboard assert "AI 挑品清單" in dashboard + assert "比價覆核隊列" in dashboard + assert "覆核動作" in dashboard assert "dashboard-ai-summary-grid" in dashboard assert "AI 建議" in dashboard assert "/api/export/excel/ai-picks" in dashboard diff --git a/web/static/css/page-dashboard-v2.css b/web/static/css/page-dashboard-v2.css index 869a866..606e820 100644 --- a/web/static/css/page-dashboard-v2.css +++ b/web/static/css/page-dashboard-v2.css @@ -285,6 +285,32 @@ border: 1px solid var(--momo-border-light); } + .dashboard-focus-chip.is-review { + color: var(--momo-warning-text); + background: var(--momo-warning-bg); + border: 1px solid rgba(161, 111, 35, 0.24); + } + + .dashboard-focus-action { + display: inline-flex; + width: fit-content; + align-items: center; + margin-top: 12px; + padding: 6px 10px; + color: var(--momo-text-inverse); + background: var(--momo-ink); + border: 1px solid var(--momo-ink); + border-radius: 4px; + font-size: 12px; + font-weight: 800; + text-decoration: none; + } + + .dashboard-focus-action:hover { + color: var(--momo-text-inverse); + background: var(--momo-ink-soft); + } + .dashboard-filter-card { padding: 12px 16px; } @@ -464,6 +490,10 @@ min-width: 1460px; } + .dashboard-table.is-review { + min-width: 1460px; + } + .dashboard-table th { padding: 11px 14px; color: var(--momo-text-tertiary); @@ -738,6 +768,19 @@ -webkit-line-clamp: 3; } + .dashboard-review-card { + display: grid; + min-width: 190px; + gap: 6px; + } + + .dashboard-review-note { + color: var(--momo-warning-text); + font-size: 10px; + font-weight: 800; + line-height: 1.45; + } + .dashboard-history-button { display: inline-flex; align-items: center;