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