feat: add growth daily action plan
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s

This commit is contained in:
ogt
2026-06-24 21:00:05 +08:00
parent 9610b4da18
commit fa71897158
4 changed files with 236 additions and 1 deletions

View File

@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.649"
SYSTEM_VERSION = "V10.650"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -85,6 +85,7 @@
- V10.647 起 `/ai_intelligence` 的商品明細每一筆都必須能打開單品作戰詳情,詳情需顯示商品、建議動作、近 7 天業績、業績變化、PChome/MOMO 價格證據、價差、可信度、判斷原因與下一步操作;不得只讓使用者看一排文字後自行猜測。
- V10.648 起 `/ai_intelligence` 的商品明細上方必須提供分類策略看板,把商品依分類彙總成可點擊的數據條列;每列至少顯示分類、近 7 天業績、商品數、價格壓力、價格優勢、缺比價、待確認與建議下一步。點擊分類後必須切到該分類商品明細。
- V10.649 起 `/ai_intelligence` 必須提供銷售策略建議看板,把商品分成價格防守、主推曝光、組合/單位價、資料補齊等營運路徑;每張策略卡需顯示件數、近 7 天業績、代表商品與可點擊下一步,點擊後必須切到對應商品明細。
- V10.650 起 `/ai_intelligence` 必須提供「今日策略動作」清單,從作戰商品中挑出前 5 件具體行動;每列需顯示處理順序、動作、商品、近 7 天業績、原因與可點擊的詳情/處理入口,避免使用者只看到分類與策略後仍不知道下一步要做哪一件商品。
## 零之一、12 Agent 決策信封2026-05-24

View File

@@ -1023,6 +1023,102 @@
height: 100%;
}
.growth-action-board {
border: 1px solid rgba(42, 37, 32, 0.1);
border-radius: 8px;
background: rgba(255, 255, 255, 0.72);
margin-bottom: 10px;
padding: 10px;
}
.growth-action-board-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 8px;
}
.growth-action-board-title {
color: var(--momo-text-strong);
font-size: 0.84rem;
font-weight: 950;
margin: 0;
}
.growth-action-board-note {
color: var(--momo-text-muted);
font-size: 0.68rem;
font-weight: 820;
text-align: right;
}
.growth-action-list {
display: grid;
gap: 7px;
}
.growth-action-row {
border: 1px solid rgba(42, 37, 32, 0.08);
border-radius: 8px;
background: rgba(250, 247, 240, 0.58);
display: grid;
grid-template-columns: auto minmax(0, 1.1fr) minmax(170px, 0.55fr) auto;
gap: 10px;
align-items: center;
padding: 9px 10px;
}
.growth-action-rank {
align-items: center;
background: rgba(172, 92, 58, 0.12);
border-radius: 999px;
color: #8f4d33;
display: inline-flex;
font-family: var(--momo-font-mono);
font-size: 0.72rem;
font-weight: 950;
height: 34px;
justify-content: center;
width: 34px;
}
.growth-action-title {
color: var(--momo-text-strong);
font-size: 0.8rem;
font-weight: 950;
line-height: 1.3;
margin: 0;
}
.growth-action-meta,
.growth-action-reason {
color: var(--momo-text-muted);
font-size: 0.68rem;
font-weight: 820;
line-height: 1.35;
margin: 3px 0 0;
}
.growth-action-pill {
border: 1px solid rgba(172, 92, 58, 0.18);
border-radius: 999px;
color: #8f4d33;
display: inline-flex;
font-size: 0.68rem;
font-weight: 950;
justify-content: center;
padding: 4px 8px;
white-space: nowrap;
}
.growth-action-buttons {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: flex-end;
}
.growth-decision-panel {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
@@ -1901,6 +1997,27 @@
min-height: 0;
}
.growth-action-board-head {
align-items: flex-start;
flex-direction: column;
}
.growth-action-board-note {
text-align: left;
}
.growth-action-row {
grid-template-columns: 1fr;
}
.growth-action-buttons {
justify-content: stretch;
}
.growth-action-buttons .table-row-action {
flex: 1 1 100%;
}
.growth-detail-price-grid {
grid-template-columns: 1fr;
}
@@ -2114,6 +2231,15 @@
<div class="text-center py-3 text-muted">整理策略中...</div>
</div>
</div>
<div class="growth-action-board" id="growthActionBoard" aria-label="今日策略動作">
<div class="growth-action-board-head">
<h3 class="growth-action-board-title">今日策略動作</h3>
<span class="growth-action-board-note">照順序處理最可能影響業績的商品</span>
</div>
<div class="growth-action-list">
<div class="text-center py-3 text-muted">整理動作中...</div>
</div>
</div>
<div class="growth-decision-panel" id="growthDecisionSummary">
<div>
<h3 class="growth-decision-title">正在整理處理建議</h3>
@@ -3419,6 +3545,109 @@ function showGrowthPlaybookDetail(kind) {
showGrowthDetail(target);
}
function growthActionPlanForRow(row) {
const flags = classifyGrowthRow(row);
const next = growthRowNextAction(row);
const price = row?.external_price || null;
const gap = price && price.gap_pct !== null && price.gap_pct !== undefined ? Number(price.gap_pct) : null;
if (flags.risk) {
return {
label: '價格防守',
title: '檢查售價、券或活動',
reason: gap === null ? 'MOMO 參考價較低,先檢查價格壓力。' : formatGapDisplay(gap),
next,
};
}
if (flags.advantage) {
return {
label: '主推曝光',
title: '放大價格優勢',
reason: gap === null ? 'PChome 有外部價格優勢,適合提高曝光。' : formatGapDisplay(gap),
next,
};
}
if (isBundleOrUnitRow(row)) {
return {
label: '組合 / 單位價',
title: '檢查組合包與單位價',
reason: '先確認容量、入數與單位價,再決定組合或券。',
next,
};
}
if (flags.review) {
return {
label: '候選確認',
title: '確認 MOMO 候選是否同款',
reason: '同款確認後才會進入價格判斷。',
next,
};
}
if (flags.needs) {
return {
label: '補資料',
title: '補齊 MOMO 參考商品',
reason: '目前缺外部參考價,先補資料才有可行動建議。',
next,
};
}
if (Number(row?.sales_delta_pct || 0) < 0) {
return {
label: '找回動能',
title: '檢查下滑商品',
reason: `近 7 天業績變化 ${Number(row.sales_delta_pct || 0).toFixed(1)}%。`,
next,
};
}
return {
label: '追蹤',
title: '維持追蹤',
reason: '目前沒有明確價格或資料風險。',
next,
};
}
function renderGrowthActionBoard() {
const board = document.getElementById('growthActionBoard');
if (!board) return;
const list = board.querySelector('.growth-action-list');
if (!list) return;
const rows = (Array.isArray(latestGrowthRows) ? latestGrowthRows : [])
.map((row) => ({ row, plan: growthActionPlanForRow(row) }))
.filter((item) => item.plan.label !== '追蹤')
.sort((a, b) => Number(b.row.priority_score || 0) - Number(a.row.priority_score || 0))
.slice(0, 5);
if (!rows.length) {
list.innerHTML = `<div class="text-center py-3 text-muted">目前沒有需要立即處理的商品。</div>`;
return;
}
list.innerHTML = rows.map((item, index) => {
const row = item.row;
const plan = item.plan;
const key = escapeHtml(growthRowKey(row));
const nextAttrs = plan.next.action === 'backfill'
? 'data-growth-action="backfill"'
: `data-growth-action="${plan.next.action}" data-product-key="${key}"`;
return `<article class="growth-action-row">
<span class="growth-action-rank">${String(index + 1).padStart(2, '0')}</span>
<div>
<p class="growth-action-title">${escapeHtml(plan.title)}</p>
<p class="growth-action-meta">${escapeHtml(row.product_name || '未命名商品')}</p>
<p class="growth-action-reason">${escapeHtml(plan.reason)}</p>
</div>
<div>
<span class="growth-action-pill">${escapeHtml(plan.label)}</span>
<p class="growth-action-meta">近 7 天 ${escapeHtml(formatMoney(row.sales_7d))}</p>
</div>
<div class="growth-action-buttons">
<button type="button" class="btn btn-sm btn-outline-secondary table-row-action" data-growth-action="show-product-detail" data-product-key="${key}">詳情</button>
<button type="button" class="btn btn-sm btn-outline-primary table-row-action" ${nextAttrs}>${escapeHtml(plan.next.label)}</button>
</div>
</article>`;
}).join('');
}
function renderGrowthStrategySummary() {
const box = document.getElementById('growthStrategyGrid');
if (!box) return;
@@ -3602,6 +3831,7 @@ function renderGrowthDetail(kind = activeGrowthDetailKind) {
renderGrowthStrategySummary();
renderGrowthCategoryBoard();
renderGrowthPlaybookBoard();
renderGrowthActionBoard();
const [title, subtitle] = growthDetailConfig(activeGrowthDetailKind);
const rows = growthDetailRows(activeGrowthDetailKind);

View File

@@ -483,6 +483,10 @@ def test_ai_intelligence_template_uses_pchome_growth_name_and_endpoint():
assert "銷售策略建議" in template
assert "showGrowthPlaybookDetail" in template
assert "組合 / 單位價" in template
assert "growthActionBoard" in template
assert "今日策略動作" in template
assert "renderGrowthActionBoard" in template
assert "growthActionPlanForRow" in template
assert "scrollToPanel('externalPricePanel')" in template
assert "備援資料檢查" in template
assert "外部報價預檢" not in template