From 2888bac597c30a97a4a07dce64779b91091503a8 Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 26 Jun 2026 11:49:07 +0800 Subject: [PATCH] fix: align pchome growth comparison and promo watch --- config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 2 + docs/guides/pchome_growth_ui_ux_guardrails.md | 2 + routes/admin_observability_routes.py | 29 +++- routes/price_comparison_routes.py | 55 +++++- scripts/observability_contract.py | 2 +- templates/admin/business_intel.html | 142 ++++++++++++++- templates/ai_intelligence.html | 116 +++++++++++-- templates/price_comparison.html | 163 ++++++++++++++++-- tests/test_frontend_v2_assets.py | 5 + tests/test_pchome_revenue_growth_service.py | 10 +- tests/test_price_comparison_routes.py | 15 +- 12 files changed, 508 insertions(+), 35 deletions(-) diff --git a/config.py b/config.py index 01b1073..8f46c46 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.704" +SYSTEM_VERSION = "V10.705" 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 07097e5..5eb097e 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -782,3 +782,5 @@ POSTGRES_HOST=momo-db | 2026-06-26 | 關鍵決策框架不得因資料不足整段消失 | V10.703 起 AI 流量頁的「情境 × 知識命中矩陣」改為永遠顯示;資料不足時顯示營運空狀態,避免使用者看不到頁面應該如何判讀。 | | 2026-06-26 | 使用者頁不得把提交、分支、JSON/API 或 raw caller 當主訊息 | V10.704 起成本治理、AI 分工、AI 健康檢查與登入頁再收斂:raw caller/server 改成使用情境與服務元件,JSONL/API/PostgreSQL/Session/CSRF 等工程語改成檢查紀錄、健康檢查服務、資料服務與工作階段。 | | 2026-06-26 | 全站 UI/UX 工作重點必須文件化並納入入口索引 | V10.704 起新增 `docs/guides/pchome_growth_ui_ux_guardrails.md` 並由 `AGENTS.md` 索引;所有前台頁面以「提升 PChome 業績、快速判斷、直接下一步」為共同目標,避免後續工作再偏回局部文案修補。 | +| 2026-06-26 | 待確認商品必須能並排比較兩家賣場 | V10.705 起 `ai_intelligence` 與 `price_comparison` 的 MOMO 待確認候選都要以 PChome/MOMO 兩欄比較卡呈現,並提供「同時開兩家賣場」主要操作;不得只顯示候選摘要或只放單一平台連結。 | +| 2026-06-26 | 外部促銷活動要進商業情報與 PChome 解法 | V10.705 起商業情報頁新增外部促銷活動監控,從 24h 外部價格/折扣訊號推導外部低價壓力或促銷訊號,並用守價、組合、曝光、會員四類 PChome 業績提升解法承接。 | diff --git a/docs/guides/pchome_growth_ui_ux_guardrails.md b/docs/guides/pchome_growth_ui_ux_guardrails.md index 39c2583..7c4e486 100644 --- a/docs/guides/pchome_growth_ui_ux_guardrails.md +++ b/docs/guides/pchome_growth_ui_ux_guardrails.md @@ -11,6 +11,7 @@ 3. 比價、缺貨、匯入、AI 建議、觀測與服務健康頁,都要用同一套 PChome 業績成長語言:主推、守價、補比價、供貨風險、成長缺口、建議路徑、使用情境、服務元件。 4. 資料不足時不能整段消失,要顯示可理解的空狀態與下一步。 5. 不得把工作視窗溝通、部署交接、工程判斷或維護工作摘要搬到前台。 +6. 外部促銷活動、折扣、價格壓力與平台活動訊號,必須被整理成「PChome 現況對比」與「業績提升解法」,不能只顯示外部事件本身。 ## 每次 UI/UX 修改的驗收 @@ -20,6 +21,7 @@ - 相關業務測試:`tests/test_pchome_revenue_growth_service.py` - 正式 smoke:檢查 `/health` 版本、核心頁 HTTP 200、可見文案無 raw terms、靜態資源 HTTP 200 - 如果頁面有 PChome/MOMO 商品比較,必須能一眼看到兩平台價格與可同時開啟的外部賣場連結 +- 如果頁面有外部促銷或競品活動訊號,必須至少提供守價、組合、曝光或會員回饋等 PChome 可執行解法 ## 判斷標準 diff --git a/routes/admin_observability_routes.py b/routes/admin_observability_routes.py index aa380c4..cdbfaf9 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -821,7 +821,7 @@ def business_intel_dashboard(): sa_text(""" SELECT cph.sku, cph.competitor_product_name, cph.price, cph.momo_price, cph.discount_pct, cph.match_score, - cph.crawled_at + cph.crawled_at, cph.source FROM competitor_price_history cph WHERE cph.crawled_at >= NOW() - INTERVAL '24 hours' AND cph.match_score >= 0.7 @@ -839,10 +839,35 @@ def business_intel_dashboard(): 'match_score': round(float(r[5] or 0), 3), 'gap': (float(r[3]) - float(r[2])) if (r[2] and r[3]) else None, 'crawled_at': r[6].strftime('%m-%d %H:%M') if r[6] else '', + 'source': r[7] or '外部電商', } for r in recent_competitor ] + promo_watch_rows = [] + for item in recent_competitor_prices: + discount_pct = float(item.get('discount_pct') or 0) + gap = item.get('gap') + gap_value = float(gap) if gap is not None else 0.0 + is_external_pressure = gap_value < 0 + is_discount_signal = discount_pct >= 5 + if not (is_external_pressure or is_discount_signal): + continue + if is_external_pressure: + pressure_label = '外部低價壓力' + recommended_action = '檢查 PChome 售價、折扣券、組合包與商品頁主賣點' + else: + pressure_label = '外部促銷訊號' + recommended_action = '比對活動條件後,安排 PChome 主推曝光或會員回饋' + promo_watch_rows.append({ + **item, + 'pressure_label': pressure_label, + 'recommended_action': recommended_action, + 'gap_abs': abs(gap_value), + }) + if len(promo_watch_rows) >= 8: + break + # 7. 高 confidence 但未 follow-through (recommendation 沒對應 action_plan) unfollowed = session.execute( sa_text(f""" @@ -870,6 +895,7 @@ def business_intel_dashboard(): verdict_stats=verdict_stats, match_stats=match_stats, recent_competitor_prices=recent_competitor_prices, + promo_watch_rows=promo_watch_rows, unfollowed_count=unfollowed_count, error=None, ) @@ -879,6 +905,7 @@ def business_intel_dashboard(): active_page='obs_business_intel', days=days, rec_by_strategy=[], latest_recommendations=[], loop_records=[], verdict_stats=[], match_stats=[], recent_competitor_prices=[], + promo_watch_rows=[], unfollowed_count=0, error='商業 AI 資料暫時不可用,已切換安全空狀態。', ) diff --git a/routes/price_comparison_routes.py b/routes/price_comparison_routes.py index 548e636..3bf89c8 100644 --- a/routes/price_comparison_routes.py +++ b/routes/price_comparison_routes.py @@ -30,6 +30,55 @@ def _candidate_auto_compare_type(item: dict) -> str: return "manual_review" +def _build_pchome_product_url(product_id: str) -> str: + product_id = str(product_id or "").strip() + return f"https://24h.pchome.com.tw/prod/{product_id}" if product_id else "" + + +def _humanize_targeted_review_reasons(candidate: dict) -> list[str]: + labels = [] + score = candidate.get("target_match_score") + try: + score_pct = round(float(score or 0) * 100) + except (TypeError, ValueError): + score_pct = 0 + if score_pct > 0: + labels.append(f"可信度 {score_pct}%") + + reason_map = { + "variant_selection_review": "需確認色號", + "strong_exact_spec_match": "規格接近", + "strong_product_line_match": "系列接近", + "count_conflict": "組合數量需確認", + "unit_comparable": "可用單位價判斷", + "makeup_catalog_selection_gap": "款式需確認", + } + for reason in candidate.get("target_match_reasons") or []: + label = reason_map.get(str(reason or "").strip()) + if label and label not in labels: + labels.append(label) + return labels or ["需人工確認同款"] + + +def _present_momo_review_candidate(candidate: dict) -> dict: + """Return only user-facing review candidate fields for the UI.""" + pchome_product_id = str(candidate.get("target_pchome_product_id") or "").strip() + return { + "product_id": candidate.get("product_id") or candidate.get("goodsCode") or candidate.get("id"), + "name": candidate.get("name") or candidate.get("title") or "", + "price": candidate.get("price"), + "product_url": candidate.get("product_url") or candidate.get("url") or "", + "image_url": candidate.get("image_url") or "", + "target_pchome_product_id": pchome_product_id, + "target_pchome_name": candidate.get("target_pchome_name") or pchome_product_id, + "target_pchome_price": candidate.get("target_pchome_price"), + "target_pchome_url": candidate.get("target_pchome_url") or _build_pchome_product_url(pchome_product_id), + "target_gap_pct": candidate.get("target_gap_pct"), + "target_match_score": candidate.get("target_match_score"), + "target_match_reason_labels": _humanize_targeted_review_reasons(candidate), + } + + def _sync_targeted_candidates_to_external_offers(candidates: list[dict]) -> dict: from database.manager import DatabaseManager from services.external_market_offer_service import sync_targeted_momo_candidates_to_external_offers @@ -239,10 +288,14 @@ def fetch_momo_for_pchome_products(): item for item in products if _candidate_auto_compare_type(item) == "unit_price" ] - review_candidates = [ + raw_review_candidates = [ item for item in products if _candidate_auto_compare_type(item) not in {"total_price", "unit_price"} ] + review_candidates = [ + _present_momo_review_candidate(item) + for item in raw_review_candidates + ] external_offer_sync = { "success": True, "status": "not_requested", diff --git a/scripts/observability_contract.py b/scripts/observability_contract.py index 6981937..df32c71 100644 --- a/scripts/observability_contract.py +++ b/scripts/observability_contract.py @@ -49,7 +49,7 @@ OBSERVABILITY_PAGES = ( "/observability/business_intel", "商業面 × AI", "商業", - ("商業面 × AI", "AI", "競品"), + ("外部促銷活動監控", "PChome 解法", "競品"), ), ObservabilityPage( "templates/admin/host_health.html", diff --git a/templates/admin/business_intel.html b/templates/admin/business_intel.html index a5dddbd..a6cd7a4 100644 --- a/templates/admin/business_intel.html +++ b/templates/admin/business_intel.html @@ -177,6 +177,101 @@ font-weight: 850; } +.biz-promo-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: .75rem; +} + +.biz-solution-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: .6rem; + margin-bottom: .85rem; +} + +.biz-solution-card { + min-height: 92px; + padding: .72rem; + border: 1px solid rgba(42, 37, 32, 0.09); + border-radius: 16px; + background: rgba(250, 247, 240, 0.68); +} + +.biz-solution-card strong { + display: block; + color: var(--biz-ink); + font-size: .82rem; + font-weight: 950; +} + +.biz-solution-card span { + display: block; + margin-top: .35rem; + color: var(--biz-muted); + font-size: .72rem; + font-weight: 780; + line-height: 1.35; +} + +.biz-promo-card { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(150px, .42fr); + gap: .8rem; + align-items: start; + padding: .95rem; + border: 1px solid rgba(201, 100, 66, 0.16); + border-radius: 18px; + background: rgba(255, 250, 243, 0.9); +} + +.biz-promo-source { + display: flex; + flex-wrap: wrap; + gap: .45rem; + align-items: center; + margin-bottom: .45rem; +} + +.biz-promo-title { + color: var(--biz-ink); + font-size: .95rem; + font-weight: 950; + line-height: 1.35; +} + +.biz-promo-solution { + margin-top: .55rem; + color: var(--biz-muted); + font-size: .82rem; + font-weight: 780; + line-height: 1.5; +} + +.biz-promo-metrics { + display: grid; + gap: .45rem; +} + +.biz-promo-metric { + padding: .55rem; + border-radius: 14px; + background: rgba(201, 100, 66, 0.07); +} + +.biz-promo-metric strong { + display: block; + color: var(--biz-ink); + font-family: var(--momo-font-mono, monospace); + font-size: .92rem; +} + +.biz-promo-metric span { + color: var(--biz-muted); + font-size: .68rem; + font-weight: 900; +} + .biz-layout { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(340px, .9fr); @@ -400,7 +495,10 @@ .biz-signal-grid, .biz-strategy-grid, - .biz-decision-card { + .biz-solution-grid, + .biz-decision-card, + .biz-promo-grid, + .biz-promo-card { grid-template-columns: 1fr; } @@ -500,6 +598,48 @@ {% endif %} +
+
+
+

外部促銷活動監控

+

AI Agent 以外部價格與折扣訊號監控促銷壓力,先對照 PChome 現況,再提出業績提升解法。

+
+ {{ promo_watch_rows|length }} 筆訊號 +
+
+
+
守價確認售價、折扣券與毛利底線,避免被外部促銷壓住。
+
組合用組合包、贈品或加價購拉高感知價值,不只跟價。
+
曝光把 PChome 有利商品推到搜尋、首頁、EDM 或活動入口。
+
會員用會員回饋、免運或售後服務對抗短期低價。
+
+ {% if promo_watch_rows %} +
+ {% for r in promo_watch_rows %} +
+
+
+ {{ r.pressure_label }} + {{ r.source or '外部電商' }} + {{ r.crawled_at or '-' }} +
+
{{ r.product_name or r.sku }}
+
PChome 解法:{{ r.recommended_action }}
+
+
+
{{ '%.0f'|format(r.discount_pct or 0) }}%外部折扣
+
{{ '%.0f'|format(r.gap_abs or 0) }}價差壓力
+
{{ '%.2f'|format(r.match_score or 0) }}同款可信度
+
+
+ {% endfor %} +
+ {% else %} +
目前尚未捕捉到可判讀的外部促銷壓力;AI Agent 會持續觀察外部價格、折扣與同款可信度,捕捉到訊號後先進這裡,再進作戰清單。
+ {% endif %} +
+
+
diff --git a/templates/ai_intelligence.html b/templates/ai_intelligence.html index a89583e..e73fbb9 100644 --- a/templates/ai_intelligence.html +++ b/templates/ai_intelligence.html @@ -2129,12 +2129,60 @@ gap: 8px; } + .review-candidate-flow { + display: grid; + gap: 8px; + } + + .review-candidate-flow-step { + display: grid; + grid-template-columns: 28px minmax(0, 1fr); + gap: 9px; + align-items: center; + min-height: 54px; + padding: 9px 10px; + border: 1px solid rgba(42, 37, 32, 0.09); + border-radius: 8px; + background: rgba(250, 247, 240, 0.72); + } + + .review-candidate-flow-index { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 999px; + background: rgba(172, 92, 58, 0.13); + color: var(--momo-warm-rust); + font-family: var(--momo-font-mono); + font-size: 0.74rem; + font-weight: 900; + } + + .review-candidate-flow-step strong { + display: block; + color: var(--momo-text-strong); + font-size: 0.78rem; + font-weight: 900; + line-height: 1.2; + } + + .review-candidate-flow-step span:last-child { + display: block; + margin-top: 2px; + color: var(--momo-text-muted); + font-size: 0.7rem; + font-weight: 780; + line-height: 1.25; + } + .review-candidate-result { border: 1px solid rgba(42, 37, 32, 0.1); border-radius: 8px; background: rgba(255, 255, 255, 0.76); - min-height: 212px; - max-height: 360px; + min-height: 246px; + max-height: 520px; overflow: auto; padding: 10px; } @@ -2144,7 +2192,7 @@ grid-template-columns: minmax(0, 1fr) minmax(148px, auto); gap: 14px; border-bottom: 1px solid rgba(42, 37, 32, 0.08); - padding: 12px 0; + padding: 14px 0; } .review-candidate-row:last-child { @@ -2184,18 +2232,28 @@ .review-candidate-compare { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 8px; - margin-top: 8px; + gap: 10px; + margin-top: 10px; } .review-candidate-store { border: 1px solid rgba(42, 37, 32, 0.1); border-radius: 8px; background: rgba(255, 255, 255, 0.68); - padding: 10px; + padding: 11px; min-width: 0; } + .review-candidate-store.is-pchome { + border-color: rgba(46, 125, 91, 0.2); + background: rgba(235, 248, 241, 0.62); + } + + .review-candidate-store.is-momo { + border-color: rgba(201, 100, 66, 0.2); + background: rgba(255, 244, 239, 0.58); + } + .review-candidate-store strong { display: flex; align-items: center; @@ -2260,6 +2318,16 @@ font-weight: 800; } + .review-candidate-store-label { + display: inline-flex; + align-items: center; + gap: 5px; + color: var(--momo-text-muted); + font-size: 0.68rem; + font-weight: 900; + letter-spacing: 0.02em; + } + .review-candidate-pill { display: inline-flex; align-items: center; @@ -2310,6 +2378,13 @@ white-space: nowrap; } + .review-candidate-actions .review-candidate-primary { + border-color: rgba(49, 113, 234, 0.32); + background: rgba(49, 113, 234, 0.08); + color: #2557b8; + font-weight: 900; + } + @media (max-width: 1320px) { .growth-command-kpi-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -3129,7 +3204,7 @@
MOMO 待確認候選 - 先確認同款,再進價格判斷 + 並排看兩家賣場,確認同款後才進作戰
-
- 確認同款後才會進入 MOMO 價格參考;不確定色號、容量或組合時請先排除。 +
+
+ 1 + 同時看賣場先開 PChome 與 MOMO 商品頁 +
+
+ 2 + 確認規格比對品名、容量、色號與組合 +
+
+ 3 + 送進作戰同款才進價格壓力與主推判斷 +
@@ -5096,7 +5182,7 @@ function renderGrowthReviewCandidates(rows) { ? `MOMO 商品圖` : ''; const compareButton = pchomeUrl && momoUrl - ? `` + ? `` : ''; return `
@@ -5106,25 +5192,27 @@ function renderGrowthReviewCandidates(rows) { 同款可信度 ${score}%
-
+
- PChome ${pchomeLink} + PChome ${escapeHtml(pchomePrice)}

${escapeHtml(row.pchome_product_name || row.pchome_product_id || 'PChome 商品')}

+
${pchomeLink}
-
+
${momoThumb}
- MOMO ${momoLink} + MOMO ${escapeHtml(momoPrice)}

${escapeHtml(row.momo_title || row.momo_sku || '未命名候選')}

+
${momoLink}
diff --git a/templates/price_comparison.html b/templates/price_comparison.html index d1c8fd4..c361f9a 100644 --- a/templates/price_comparison.html +++ b/templates/price_comparison.html @@ -473,7 +473,14 @@ color: var(--momo-success-text); } -.price-unit-list { +.price-note.is-review { + background: rgba(255, 248, 231, 0.95); + border-color: rgba(210, 158, 58, 0.28); + color: #7a5209; +} + +.price-unit-list, +.price-review-list { display: grid; gap: 8px; margin-top: 8px; @@ -512,6 +519,80 @@ white-space: nowrap; } +.price-review-item { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + align-items: start; + padding: 10px; + border: 1px solid rgba(210, 158, 58, 0.24); + border-radius: var(--momo-radius-sm); + background: rgba(255, 255, 255, 0.58); +} + +.price-review-compare { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + min-width: 0; +} + +.price-review-store { + min-width: 0; + padding: 8px; + border: 1px solid rgba(42, 37, 32, 0.1); + border-radius: var(--momo-radius-sm); + background: rgba(250, 247, 240, 0.64); +} + +.price-review-store.is-pchome { + border-color: rgba(46, 125, 91, 0.2); + background: rgba(235, 248, 241, 0.62); +} + +.price-review-store.is-momo { + border-color: rgba(201, 100, 66, 0.2); + background: rgba(255, 244, 239, 0.58); +} + +.price-review-platform { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + color: var(--momo-text-secondary); + font-size: 0.68rem; + font-weight: 900; +} + +.price-review-name { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + min-height: 2.5em; + margin-top: 5px; + overflow: hidden; + color: var(--momo-text-primary); + font-size: 0.78rem; + font-weight: 850; + line-height: 1.25; +} + +.price-review-price { + display: block; + margin-top: 6px; + color: var(--momo-text-primary); + font-family: var(--momo-font-mono, monospace); + font-size: 0.88rem; + font-weight: 900; +} + +.price-review-actions { + display: grid; + gap: 7px; + min-width: 128px; +} + .price-table-head th { background: var(--momo-bg-paper) !important; color: var(--momo-text-secondary) !important; @@ -610,6 +691,12 @@ grid-template-columns: 1fr; } + .price-review-item, + .price-review-compare, + .price-review-actions { + grid-template-columns: 1fr; + } + .price-unit-metric { justify-content: flex-start; white-space: normal; @@ -789,7 +876,7 @@ 0 筆需確認
- +
@@ -1230,6 +1317,18 @@ La Roche-Posay 安得利防曬液 50ml,920 panel.replaceChildren(heading, list); } + function buildPchomeProductUrl(productId) { + const id = String(productId || '').trim(); + return id ? `https://24h.pchome.com.tw/prod/${encodeURIComponent(id)}` : ''; + } + + function reviewCandidateReasonLabels(item) { + const labels = Array.isArray(item?.target_match_reason_labels) + ? item.target_match_reason_labels.filter(Boolean) + : []; + return labels.length ? labels : ['需人工確認同款']; + } + function renderMomoReviewPanel() { const countBadge = document.getElementById('momoReviewCount'); const panel = document.getElementById('momoReviewPanel'); @@ -1240,16 +1339,56 @@ La Roche-Posay 安得利防曬液 50ml,920 if (!momoReviewCandidates.length) { panel.style.display = 'none'; - panel.textContent = ''; + panel.replaceChildren(); return; } - const sampleNames = momoReviewCandidates - .slice(0, 3) - .map(item => item.name || item.title || '未命名商品') - .join('、'); panel.style.display = 'block'; - panel.textContent = `找到 ${momoReviewCandidates.length} 筆需人工確認候選,可能是單品、組合或單位價差異:${sampleNames}`; + const heading = document.createElement('strong'); + heading.textContent = `待確認 ${momoReviewCandidates.length} 筆:先並排看兩家賣場`; + + const list = document.createElement('div'); + list.className = 'price-review-list'; + momoReviewCandidates.slice(0, 5).forEach(item => { + const pchomeId = String(item.target_pchome_product_id || '').trim(); + const pchomeName = item.target_pchome_name || pchomeId || 'PChome 商品'; + const momoName = item.name || item.title || item.product_id || 'MOMO 候選'; + const pchomeUrl = toSafeUrl(item.target_pchome_url || buildPchomeProductUrl(pchomeId)); + const momoUrl = toSafeUrl(item.product_url || item.url || ''); + const pchomePrice = item.target_pchome_price ? formatMoney(item.target_pchome_price) : '待補價格'; + const momoPrice = item.price ? formatMoney(item.price) : '待補價格'; + const reasons = reviewCandidateReasonLabels(item).slice(0, 2).join('、') || '請確認規格'; + + const row = document.createElement('div'); + row.className = 'price-review-item'; + row.innerHTML = ` +
+
+
+ PChome + ${pchomeUrl ? `賣場` : '待補連結'} +
+
${escapeHtml(pchomeName)}
+ ${escapeHtml(pchomePrice)} +
+
+
+ MOMO + ${momoUrl ? `賣場` : '待補連結'} +
+
${escapeHtml(momoName)}
+ ${escapeHtml(momoPrice)} +
+
+
+ ${pchomeUrl && momoUrl ? `` : ''} + ${escapeHtml(reasons)} +
+ `; + list.appendChild(row); + }); + + panel.replaceChildren(heading, list); } async function parseMomoExcel() { @@ -1721,7 +1860,7 @@ La Roche-Posay 安得利防曬液 50ml,920 ${pchomeId} - ${pchomeUrl ? `` : ''} + ${pchomeUrl ? `` : ''} @@ -1733,11 +1872,11 @@ La Roche-Posay 安得利防曬液 50ml,920 ${momoId} ${momoUrl ? `權杖<", "Agent 上下文", "RAG × MCP"], "templates/admin/observability_overview.html": ["權杖量", "RAG × MCP"], + "templates/admin/business_intel.html": ["candidate queue", "資料表"], "templates/cicd_dashboard.html": ["Pipeline Flow", "Pipeline History", "完整修復", "一鍵修復", "重啟 Registry", "舊叢集"], "templates/admin/budget.html": ["燒錢呼叫端", "migrations/025", "{{ c.caller }}"], "templates/ai_automation_smoke.html": ["JSONL", "健康檢查 API", "JSON.stringify(item.details"], diff --git a/tests/test_price_comparison_routes.py b/tests/test_price_comparison_routes.py index 3ff8367..8e45d88 100644 --- a/tests/test_price_comparison_routes.py +++ b/tests/test_price_comparison_routes.py @@ -141,8 +141,14 @@ def test_fetch_momo_for_pchome_endpoint_splits_auto_and_review_candidates(monkey "name": "組合需確認商品", "price": 468, "product_id": "REVIEW-1", + "product_url": "https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=REVIEW-1", "can_auto_compare": False, "target_review_status": "需人工確認", + "target_pchome_product_id": "PCH-1", + "target_pchome_name": "PChome B5 修復霜", + "target_pchome_price": 920, + "target_match_score": 0.97, + "target_match_reasons": ["variant_selection_review"], }, ] @@ -171,7 +177,14 @@ def test_fetch_momo_for_pchome_endpoint_splits_auto_and_review_candidates(monkey assert payload["data"]["candidate_count"] == 3 assert payload["data"]["products"][0]["product_id"] == "AUTO-1" assert payload["data"]["unit_compare_candidates"][0]["product_id"] == "UNIT-1" - assert payload["data"]["review_candidates"][0]["product_id"] == "REVIEW-1" + review_candidate = payload["data"]["review_candidates"][0] + assert review_candidate["product_id"] == "REVIEW-1" + assert review_candidate["target_pchome_product_id"] == "PCH-1" + assert review_candidate["target_pchome_name"] == "PChome B5 修復霜" + assert review_candidate["target_pchome_price"] == 920 + assert review_candidate["target_pchome_url"] == "https://24h.pchome.com.tw/prod/PCH-1" + assert review_candidate["target_match_reason_labels"] == ["可信度 97%", "需確認色號"] + assert "target_match_reasons" not in review_candidate assert payload["data"]["external_offer_sync"]["status"] == "not_requested"