diff --git a/config.py b/config.py index 585e74a..76b4e5e 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.651" +SYSTEM_VERSION = "V10.652" 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 6d2bc2e..4140773 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -87,6 +87,7 @@ - V10.649 起 `/ai_intelligence` 必須提供銷售策略建議看板,把商品分成價格防守、主推曝光、組合/單位價、資料補齊等營運路徑;每張策略卡需顯示件數、近 7 天業績、代表商品與可點擊下一步,點擊後必須切到對應商品明細。 - V10.650 起 `/ai_intelligence` 必須提供「今日策略動作」清單,從作戰商品中挑出前 5 件具體行動;每列需顯示處理順序、動作、商品、近 7 天業績、原因與可點擊的詳情/處理入口,避免使用者只看到分類與策略後仍不知道下一步要做哪一件商品。 - V10.651 起從「今日策略動作」或其他非明細列入口打開單品作戰詳情時,商品明細列表中的對應商品仍必須標示為目前選取;使用者需能看出詳情與明細列的關聯。 +- V10.652 起正式首頁 `/` 必須導向「PChome 業績成長自動化作戰系統」,舊商品看板僅保留在 `/dashboard` 或 `/product-dashboard`;「今日策略動作」必須放在首屏任務摘要後方,不能只藏在商品明細區;每列必須直接顯示價格證據,至少包含 PChome、MOMO、差距與可信度四格。候選待確認或缺資料時需以待確認/待補呈現,不得要求使用者先打開詳情才知道判斷依據。 ## 零之一、12 Agent 決策信封(2026-05-24) diff --git a/routes/dashboard_routes.py b/routes/dashboard_routes.py index 176730a..8c3669a 100644 --- a/routes/dashboard_routes.py +++ b/routes/dashboard_routes.py @@ -15,7 +15,7 @@ import pickle import threading from datetime import datetime, timezone, timedelta from types import SimpleNamespace -from flask import Blueprint, request, render_template, jsonify +from flask import Blueprint, request, render_template, jsonify, redirect, url_for from sqlalchemy import func, and_, text, bindparam from sqlalchemy.orm import joinedload @@ -2624,6 +2624,14 @@ def get_pchome_review_queue_api(): @dashboard_bp.route('/') @login_required def index(): + """正式首頁:PChome 業績成長自動化作戰系統。""" + return redirect(url_for('ai.ai_intelligence')) + + +@dashboard_bp.route('/dashboard') +@dashboard_bp.route('/product-dashboard') +@login_required +def product_dashboard(): """商品看板首頁""" db = DatabaseManager() session = db.get_session() diff --git a/templates/ai_intelligence.html b/templates/ai_intelligence.html index dcfd3b1..af98a3e 100644 --- a/templates/ai_intelligence.html +++ b/templates/ai_intelligence.html @@ -1119,6 +1119,40 @@ justify-content: flex-end; } + .growth-action-evidence { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 5px; + margin-top: 7px; + } + + .growth-action-evidence-chip { + border: 1px solid rgba(42, 37, 32, 0.08); + border-radius: 8px; + background: rgba(255, 255, 255, 0.64); + min-width: 0; + padding: 5px 6px; + } + + .growth-action-evidence-label { + color: var(--momo-text-muted); + display: block; + font-size: 0.6rem; + font-weight: 950; + line-height: 1.2; + } + + .growth-action-evidence-value { + color: var(--momo-text-strong); + display: block; + font-family: var(--momo-font-mono); + font-size: 0.7rem; + font-weight: 950; + line-height: 1.25; + margin-top: 2px; + overflow-wrap: anywhere; + } + .growth-decision-panel { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -2018,6 +2052,10 @@ flex: 1 1 100%; } + .growth-action-evidence { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .growth-detail-price-grid { grid-template-columns: 1fr; } @@ -2092,6 +2130,16 @@ +
+
+

今日策略動作

+ 照順序處理最可能影響業績的商品 +
+
+
整理動作中...
+
+
+
@@ -2231,15 +2279,6 @@
整理策略中...
-
-
-

今日策略動作

- 照順序處理最可能影響業績的商品 -
-
-
整理動作中...
-
-

正在整理處理建議

@@ -3606,6 +3645,40 @@ function growthActionPlanForRow(row) { }; } +function growthActionEvidence(row) { + const price = row?.external_price || null; + const candidate = row?.review_candidate || null; + const gap = price && price.gap_pct !== null && price.gap_pct !== undefined ? Number(price.gap_pct) : null; + const quality = Math.round(rowQualityScore(row)); + return [ + { + label: 'PChome', + value: price ? formatGrowthDetailPrice(price, 'pchome') : candidate ? formatPriceAmount(candidate.pchome_price) : '待補', + }, + { + label: 'MOMO', + value: price ? formatGrowthDetailPrice(price, 'momo') : candidate ? formatPriceAmount(candidate.momo_price) : '待補', + }, + { + label: '差距', + value: candidate ? '候選待確認' : gap === null ? '待判斷' : formatGapDisplay(gap), + }, + { + label: '可信度', + value: quality ? `${quality}%` : '待補', + }, + ]; +} + +function renderGrowthActionEvidence(row) { + return `
+ ${growthActionEvidence(row).map((item) => ` + ${escapeHtml(item.label)} + ${escapeHtml(item.value)} + `).join('')} +
`; +} + function renderGrowthActionBoard() { const board = document.getElementById('growthActionBoard'); if (!board) return; @@ -3639,6 +3712,7 @@ function renderGrowthActionBoard() {
${escapeHtml(plan.label)}

近 7 天 ${escapeHtml(formatMoney(row.sales_7d))}

+ ${renderGrowthActionEvidence(row)}
diff --git a/tests/test_pchome_revenue_growth_service.py b/tests/test_pchome_revenue_growth_service.py index d64ffdb..dda9834 100644 --- a/tests/test_pchome_revenue_growth_service.py +++ b/tests/test_pchome_revenue_growth_service.py @@ -57,6 +57,7 @@ def _seed_growth_external_offers(engine): source_product_id TEXT, source_offer_key TEXT, title TEXT, + product_url TEXT, price REAL, observed_at TEXT, expires_at TEXT, @@ -73,13 +74,13 @@ def _seed_growth_external_offers(engine): conn.execute(text(""" INSERT INTO external_offers ( id, source_code, platform_code, source_product_id, source_offer_key, - title, price, observed_at, expires_at, ingestion_method, + title, product_url, price, observed_at, expires_at, ingestion_method, pchome_product_id, momo_sku, match_status, quality_score, data_quality_status, quality_notes_json, raw_payload_json ) VALUES ( 1, 'momo_reference', 'momo', 'MOMO-NEW', 'momo_reference:MOMO-NEW:PCH-1', - 'MOMO 新資料層商品', 870, '2026-06-14 12:00:00', NULL, 'legacy_competitor_cache', + 'MOMO 新資料層商品', 'https://www.momoshop.com.tw/goods/MOMO-NEW', 870, '2026-06-14 12:00:00', NULL, 'legacy_competitor_cache', 'PCH-1', 'MOMO-NEW', 'verified', 92, 'verified', '["自動同步"]', '{"pchome_public_price": 1000, "pchome_public_name": "PChome 公開商品"}' @@ -97,6 +98,7 @@ def _seed_growth_unit_price_external_offer(engine): source_product_id TEXT, source_offer_key TEXT, title TEXT, + product_url TEXT, price REAL, observed_at TEXT, expires_at TEXT, @@ -113,13 +115,13 @@ def _seed_growth_unit_price_external_offer(engine): conn.execute(text(""" INSERT INTO external_offers ( id, source_code, platform_code, source_product_id, source_offer_key, - title, price, observed_at, expires_at, ingestion_method, + title, product_url, price, observed_at, expires_at, ingestion_method, pchome_product_id, momo_sku, match_status, quality_score, data_quality_status, quality_notes_json, raw_payload_json ) VALUES ( 1, 'momo_reference', 'momo', 'MOMO-UNIT', 'momo_reference:MOMO-UNIT:PCH-1:unit_price', - 'MOMO 單位價商品', 468, '2026-06-14 12:00:00', NULL, 'targeted_momo_search', + 'MOMO 單位價商品', 'https://www.momoshop.com.tw/goods/MOMO-UNIT', 468, '2026-06-14 12:00:00', NULL, 'targeted_momo_search', 'PCH-1', 'MOMO-UNIT', 'verified', 82, 'verified', '["自動單位價比較"]', '{"price_basis": "unit_price", "pchome_public_price": 920, "pchome_public_name": "PChome 公開商品", "tags": ["identity_v2", "price_basis_unit_price"], "unit_price_comparison": {"unit_label": "ml", "momo_unit_price": 11.7, "competitor_unit_price": 23.0, "momo_total_quantity": 40, "competitor_total_quantity": 40, "unit_gap_pct": -49.13}}' @@ -487,6 +489,8 @@ def test_ai_intelligence_template_uses_pchome_growth_name_and_endpoint(): assert "今日策略動作" in template assert "renderGrowthActionBoard" in template assert "growthActionPlanForRow" in template + assert "growthActionEvidence" in template + assert "growth-action-evidence-chip" in template assert "document.querySelectorAll('.growth-detail-row')" in template assert "scrollToPanel('externalPricePanel')" in template assert "備援資料檢查" in template @@ -495,3 +499,14 @@ def test_ai_intelligence_template_uses_pchome_growth_name_and_endpoint(): assert "鎖定商品" in template assert "無法比價" in template assert "補齊比價資料" in template + + +def test_formal_homepage_routes_to_growth_command_center(): + from pathlib import Path + + route_source = Path("routes/dashboard_routes.py").read_text(encoding="utf-8") + + assert "@dashboard_bp.route('/')" in route_source + assert "url_for('ai.ai_intelligence')" in route_source + assert "@dashboard_bp.route('/dashboard')" in route_source + assert "@dashboard_bp.route('/product-dashboard')" in route_source