diff --git a/config.py b/config.py
index 3ae8011..443ead2 100644
--- a/config.py
+++ b/config.py
@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
-SYSTEM_VERSION = "V10.648"
+SYSTEM_VERSION = "V10.649"
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 417fcc5..dea4c8e 100644
--- a/docs/AI_INTELLIGENCE_MODULE_SOT.md
+++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md
@@ -84,6 +84,7 @@
- V10.646 起 `/ai_intelligence` 的商品明細必須提供搜尋與排序;搜尋至少涵蓋商品、分類、商品編號與 MOMO 候選資訊,排序至少支援優先級、近 7 天業績、價差、下滑幅度與可信度。搜尋/排序後的行動摘要與明細列表必須使用同一批結果。
- V10.647 起 `/ai_intelligence` 的商品明細每一筆都必須能打開單品作戰詳情,詳情需顯示商品、建議動作、近 7 天業績、業績變化、PChome/MOMO 價格證據、價差、可信度、判斷原因與下一步操作;不得只讓使用者看一排文字後自行猜測。
- V10.648 起 `/ai_intelligence` 的商品明細上方必須提供分類策略看板,把商品依分類彙總成可點擊的數據條列;每列至少顯示分類、近 7 天業績、商品數、價格壓力、價格優勢、缺比價、待確認與建議下一步。點擊分類後必須切到該分類商品明細。
+- V10.649 起 `/ai_intelligence` 必須提供銷售策略建議看板,把商品分成價格防守、主推曝光、組合/單位價、資料補齊等營運路徑;每張策略卡需顯示件數、近 7 天業績、代表商品與可點擊下一步,點擊後必須切到對應商品明細。
## 零之一、12 Agent 決策信封(2026-05-24)
diff --git a/templates/ai_intelligence.html b/templates/ai_intelligence.html
index 8a80ba7..2e17cf6 100644
--- a/templates/ai_intelligence.html
+++ b/templates/ai_intelligence.html
@@ -907,6 +907,122 @@
text-align: right;
}
+ .growth-playbook-board {
+ border: 1px solid rgba(42, 37, 32, 0.1);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.7);
+ margin-bottom: 10px;
+ padding: 10px;
+ }
+
+ .growth-playbook-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 8px;
+ }
+
+ .growth-playbook-title {
+ color: var(--momo-text-strong);
+ font-size: 0.84rem;
+ font-weight: 950;
+ margin: 0;
+ }
+
+ .growth-playbook-note {
+ color: var(--momo-text-muted);
+ font-size: 0.68rem;
+ font-weight: 820;
+ text-align: right;
+ }
+
+ .growth-playbook-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 8px;
+ }
+
+ .growth-playbook-card {
+ border: 1px solid rgba(42, 37, 32, 0.09);
+ border-radius: 8px;
+ background: rgba(250, 247, 240, 0.58);
+ color: var(--momo-text-strong);
+ display: grid;
+ gap: 7px;
+ min-height: 142px;
+ padding: 10px;
+ text-align: left;
+ width: 100%;
+ }
+
+ .growth-playbook-card:hover,
+ .growth-playbook-card:focus,
+ .growth-playbook-card.is-active {
+ border-color: rgba(172, 92, 58, 0.3);
+ background: rgba(255, 248, 232, 0.82);
+ outline: 0;
+ }
+
+ .growth-playbook-card.is-defense {
+ border-color: rgba(185, 79, 58, 0.2);
+ }
+
+ .growth-playbook-card.is-boost {
+ border-color: rgba(47, 143, 102, 0.2);
+ }
+
+ .growth-playbook-card.is-bundle {
+ border-color: rgba(216, 161, 58, 0.22);
+ }
+
+ .growth-playbook-card.is-data {
+ border-color: rgba(92, 111, 135, 0.2);
+ }
+
+ .growth-playbook-label {
+ color: var(--momo-text-muted);
+ font-size: 0.68rem;
+ font-weight: 950;
+ line-height: 1.2;
+ }
+
+ .growth-playbook-value {
+ color: var(--momo-text-strong);
+ font-family: var(--momo-font-mono);
+ font-size: 1.22rem;
+ font-weight: 950;
+ line-height: 1;
+ }
+
+ .growth-playbook-sales,
+ .growth-playbook-product,
+ .growth-playbook-action {
+ color: var(--momo-text-muted);
+ font-size: 0.68rem;
+ font-weight: 820;
+ line-height: 1.35;
+ }
+
+ .growth-playbook-action {
+ color: #8f4d33;
+ font-weight: 950;
+ }
+
+ .growth-playbook-track {
+ background: rgba(42, 37, 32, 0.08);
+ border-radius: 999px;
+ height: 7px;
+ overflow: hidden;
+ }
+
+ .growth-playbook-bar {
+ background: #ac5c3a;
+ border-radius: inherit;
+ display: block;
+ height: 100%;
+ }
+
.growth-decision-panel {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
@@ -1768,6 +1884,23 @@
text-align: left;
}
+ .growth-playbook-head {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .growth-playbook-note {
+ text-align: left;
+ }
+
+ .growth-playbook-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .growth-playbook-card {
+ min-height: 0;
+ }
+
.growth-detail-price-grid {
grid-template-columns: 1fr;
}
@@ -1972,6 +2105,15 @@
整理分類中...
+
+
+
銷售策略建議
+ 把比價結果轉成可執行動作
+
+
+
正在整理處理建議
@@ -2335,6 +2477,10 @@ function bindActionDelegation() {
showGrowthCategoryDetail(growthButton.dataset.categoryName || '');
return;
}
+ if (growthButton.dataset.growthAction === 'show-playbook-detail') {
+ showGrowthPlaybookDetail(growthButton.dataset.playbookKind || '');
+ return;
+ }
if (growthButton.dataset.growthAction === 'review-candidate') {
focusReviewCandidate(growthButton.dataset.productKey || '');
return;
@@ -2864,6 +3010,7 @@ function growthDetailConfig(kind) {
review: ['MOMO 候選待確認', '候選已找到,確認同款後才會進入價格判斷。'],
risk: ['MOMO 更便宜商品', 'MOMO 參考價較低,優先檢查 PChome 售價、券或組合。'],
advantage: ['PChome 價格優勢商品', 'PChome 目前較有價格優勢,適合檢查曝光與主推位置。'],
+ bundle: ['組合 / 單位價商品', '需要檢查組合包、入數、容量或單位價,避免只看總價誤判。'],
source: ['外部價格來源明細', '只列出已接到 MOMO 外部參考價的商品。'],
decline: ['業績下滑商品', '近 7 天比前 7 天下滑的商品。'],
category: [selectedCategory ? `${selectedCategory} 商品明細` : '分類商品明細', '這個分類內的商品與價格狀態。'],
@@ -2883,6 +3030,7 @@ function growthDetailRows(kind) {
if (kind === 'review') return Boolean(row.review_candidate) || actionCode === 'review_external_candidate';
if (kind === 'risk') return Boolean(price) && (actionCode === 'review_price_or_promo' || (gap !== null && gap < -5));
if (kind === 'advantage') return Boolean(price) && (actionCode === 'amplify_price_advantage' || (gap !== null && gap > 5));
+ if (kind === 'bundle') return isBundleOrUnitRow(row);
if (kind === 'source') return Boolean(price);
if (kind === 'decline') return Number(row.sales_delta_pct || 0) < 0;
if (kind === 'category') return selectedCategory && row.category === selectedCategory;
@@ -3182,6 +3330,95 @@ function showGrowthCategoryDetail(category) {
showGrowthDetail('category');
}
+function isBundleOrUnitRow(row) {
+ const price = row?.external_price || null;
+ const name = String(row?.product_name || '');
+ return Boolean(price) && (
+ price.price_basis === 'unit_price'
+ || /組|入|套|盒|包|ml|g|kg|公升|毫升/.test(name)
+ );
+}
+
+function growthPlaybookRows(kind) {
+ const rows = Array.isArray(latestGrowthRows) ? latestGrowthRows : [];
+ if (kind === 'defense') return rows.filter((row) => classifyGrowthRow(row).risk);
+ if (kind === 'boost') return rows.filter((row) => classifyGrowthRow(row).advantage);
+ if (kind === 'bundle') return rows.filter(isBundleOrUnitRow);
+ if (kind === 'data') return rows.filter((row) => {
+ const flags = classifyGrowthRow(row);
+ return flags.needs || flags.review;
+ });
+ return [];
+}
+
+function playbookTargetKind(kind) {
+ if (kind === 'defense') return 'risk';
+ if (kind === 'boost') return 'advantage';
+ if (kind === 'bundle') return 'bundle';
+ if (kind === 'data') return 'needs';
+ return 'all';
+}
+
+function renderGrowthPlaybookBoard() {
+ const board = document.getElementById('growthPlaybookBoard');
+ if (!board) return;
+ const grid = board.querySelector('.growth-playbook-grid');
+ if (!grid) return;
+
+ const playbooks = [
+ {
+ kind: 'defense',
+ className: 'is-defense',
+ label: '價格防守',
+ action: '檢查售價 / 券 / 活動',
+ },
+ {
+ kind: 'boost',
+ className: 'is-boost',
+ label: '主推曝光',
+ action: '加強主推位置與文案',
+ },
+ {
+ kind: 'bundle',
+ className: 'is-bundle',
+ label: '組合 / 單位價',
+ action: '檢查組合包與單位價',
+ },
+ {
+ kind: 'data',
+ className: 'is-data',
+ label: '資料補齊',
+ action: '補候選或確認同款',
+ },
+ ];
+
+ const summaries = playbooks.map((item) => {
+ const rows = growthPlaybookRows(item.kind).sort((a, b) => Number(b.sales_7d || 0) - Number(a.sales_7d || 0));
+ const sales = rows.reduce((sum, row) => sum + Number(row.sales_7d || 0), 0);
+ return { ...item, rows, sales, topProduct: rows[0]?.product_name || '目前沒有商品' };
+ });
+ const maxSales = Math.max(1, ...summaries.map((item) => item.sales));
+
+ grid.innerHTML = summaries.map((item) => {
+ const pct = clampPercent((item.sales / maxSales) * 100);
+ const targetKind = playbookTargetKind(item.kind);
+ const activeClass = activeGrowthDetailKind === targetKind ? ' is-active' : '';
+ return ``;
+ }).join('');
+}
+
+function showGrowthPlaybookDetail(kind) {
+ const target = playbookTargetKind(kind);
+ showGrowthDetail(target);
+}
+
function renderGrowthStrategySummary() {
const box = document.getElementById('growthStrategyGrid');
if (!box) return;
@@ -3250,6 +3487,11 @@ function growthDecisionConfig(kind) {
copy: 'PChome 有價格優勢,適合排主推、加強文案與提高曝光位置。',
actionLabel: '看主推清單',
},
+ bundle: {
+ title: '檢查組合包與單位價',
+ copy: '先確認容量、入數與單位價,再決定要做組合、加券或調整主圖文案。',
+ actionLabel: '看組合商品',
+ },
review: {
title: '先確認候選是否同款',
copy: '確認同款後才會進入價格判斷;色號、容量或組合不一致就排除。',
@@ -3359,6 +3601,7 @@ function renderGrowthDetail(kind = activeGrowthDetailKind) {
});
renderGrowthStrategySummary();
renderGrowthCategoryBoard();
+ renderGrowthPlaybookBoard();
const [title, subtitle] = growthDetailConfig(activeGrowthDetailKind);
const rows = growthDetailRows(activeGrowthDetailKind);
diff --git a/tests/test_pchome_revenue_growth_service.py b/tests/test_pchome_revenue_growth_service.py
index d594a34..bafb187 100644
--- a/tests/test_pchome_revenue_growth_service.py
+++ b/tests/test_pchome_revenue_growth_service.py
@@ -479,6 +479,10 @@ def test_ai_intelligence_template_uses_pchome_growth_name_and_endpoint():
assert "分類策略看板" in template
assert "showGrowthCategoryDetail" in template
assert "data-growth-action=\"show-category-detail\"" in template
+ assert "growthPlaybookBoard" in template
+ assert "銷售策略建議" in template
+ assert "showGrowthPlaybookDetail" in template
+ assert "組合 / 單位價" in template
assert "scrollToPanel('externalPricePanel')" in template
assert "備援資料檢查" in template
assert "外部報價預檢" not in template