Files
ewoooc/services/openclaw_bot/menu_keyboards.py
OoO 8b76e3872f
All checks were successful
CD Pipeline / deploy (push) Successful in 2m49s
feat(ppt): competitor v4 — 5-forces strategic analysis (Wave 4 partial)
Wave 4 部分完成:competitor v4 五力升級(戰略視角)

generate_competitor_v4_ppt — 7 頁
- P1 封面:含整體領先/勢均力敵/落後徽章 + 三句話戰略指引
- P2 五力雷達圖(matplotlib polar)+ 右側 6 維度分數明細表(含雙條視覺化)
- P3 商品力 + 價格力(雙卡)
- P4 行銷力 + 服務力(雙卡)
- P5 品牌力 + 財務力(雙卡含 momo 8454/PChome 8044/蝦皮 SE/酷澎基本面)
- P6 AI 戰略整合(差異化建議)
- P7 附錄

新增 helper:_mpl_radar_png()
- matplotlib polar projection 五力雷達
- momo 焦糖橘 + 競品蜂蜜金
- 0-10 分尺度,含格線/標籤/圖例

query_competitor_5forces — 半實作
- 商品力:momo SKU 數從 DB 算 / 競品靜態 fallback
- 價格力:靜態(待擴 competitor_price_history 整合)
- 行銷力 / 服務力:靜態知識(電視購物頻道 / 24h 物流 / 訂閱制)
- 品牌力:靜態 + 預留 mcp_collector 整合空間
- 財務力:上市公司公開資訊(momo 8454 富邦集團、PChome 8044、SEA、酷澎)

每個維度自動算 momo - 競品差異 → 識別最大優勢力與最大劣勢力

_ppt_ai_analysis 加 is_5forces 分支
- 角色:BCG/麥肯錫資深戰略顧問
- 結構:整體競爭態勢 / 優勢加碼 / 劣勢補強或避戰 / SMART 三層 / 競爭風險
- max_tokens 2400

路由:
- /ppt competitor_v4         vs PChome(預設)
- /ppt competitor_v4 蝦皮    vs 蝦皮
- /ppt competitor_v4 酷澎    vs 酷澎

Telegram 按鈕:「⚔️ 競業五力 v4」

bump TEMPLATE_VERSIONS['competitor_v4'] = v4.0.0(新類型獨立版本)

簡化限制:商品力/價格力/品牌力的競品具體數據需後續擴 mcp_collector
(PChome SKU API、Dcard 品牌討論度量化等),靜態 fallback 已維持結構完整性

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:10:15 +08:00

285 lines
10 KiB
Python

"""OpenClaw Telegram inline keyboard builders."""
from datetime import datetime, timedelta, timezone
TAIPEI_TZ = timezone(timedelta(hours=8))
_GOALS = {}
_latest_date_provider = lambda: None
_BACK = [{'text': '← 返回主選單', 'callback_data': 'menu:main'}]
def configure_menu_keyboards(latest_date_provider=None, goals=None, taipei_tz=None):
"""Inject runtime dependencies owned by the route module."""
global _latest_date_provider, _GOALS, TAIPEI_TZ
if latest_date_provider is not None:
_latest_date_provider = latest_date_provider
if goals is not None:
_GOALS = goals
if taipei_tz is not None:
TAIPEI_TZ = taipei_tz
def _latest_date():
return _latest_date_provider() or ''
def _yesterday_from(date_str):
if not date_str:
return ''
try:
return (
datetime.strptime(date_str.replace('/', '-'), '%Y-%m-%d')
- timedelta(days=1)
).strftime('%Y/%m/%d')
except Exception:
return ''
def _row(*buttons):
"""將 (text, callback_data) 打包成 Telegram keyboard row。"""
return [{'text': text, 'callback_data': callback_data} for text, callback_data in buttons]
def _chunk_rows(items, row_size=2):
"""將一維按鈕序列切成固定列寬。"""
rows = []
cur = []
for item in items:
cur.append({'text': item[0], 'callback_data': item[1]})
if len(cur) >= row_size:
rows.append(cur)
cur = []
if cur:
rows.append(cur)
return rows
def quick_menu_keyboard():
"""help/引導頁快速入口(精簡版)。"""
return _chunk_rows([
('📊 快速查詢', 'menu:sales'),
('🏆 熱銷與廠商', 'menu:products'),
('🎯 目標管理', 'menu:goals'),
('📈 智能分析', 'menu:analysis'),
('🧩 報表簡報', 'menu:reports'),
('🔍 競品比較', 'menu:competitor'),
], row_size=2)
def _menu_with_back(rows):
"""共用加上「返回主選單」尾巴。"""
return rows + [_BACK]
def main_menu_keyboard():
"""第一層主選單 — 主要功能入口。"""
return _chunk_rows(
[
('📊 業績查詢', 'menu:sales'),
('🏆 商品廠商', 'menu:products'),
('🎯 目標管理', 'menu:goals'),
('📈 智能分析', 'menu:analysis'),
('📄 簡報報表', 'menu:reports'),
('🌐 市場情報', 'menu:market'),
('🔍 競品日報', 'menu:competitor'),
('❓ 使用說明', 'cmd:help'),
],
row_size=2,
)
def _submenu_sales():
ld = _latest_date()
yesterday = _yesterday_from(ld)
current_month = datetime.now(TAIPEI_TZ).strftime('%Y/%m')
d_label = ld[-5:] if ld else '-'
y_label = yesterday[-5:] if yesterday else '-'
return _menu_with_back([
_row((f'📊 今日 ({d_label})', f'cmd:sales:{ld}'),
(f'⬅ 昨日 ({y_label})', f'cmd:sales:{yesterday}')),
_row(('📅 每週業績', 'cmd:trend:week'),
('📅 每月業績', f'cmd:history:{current_month}')),
_row(('📅 每季業績', 'cmd:trend:quarter'),
('📅 近半年', 'cmd:trend:half')),
_row(('📈 趨勢分析', 'menu:trend'),
('🔄 同期比較', f'cmd:compare:{ld}')),
_row(('🗂 分類業績', f'cmd:category:{ld}'),
('📅 日期/區間', 'await:date_range_sales')),
_row(('🗃 月份總覽', 'cmd:history')),
])
def _submenu_products():
ld = _latest_date()
yesterday = _yesterday_from(ld)
d_label = ld[-5:] if ld else '-'
y_label = yesterday[-5:] if yesterday else '-'
return _menu_with_back([
_row((f'🏆 熱銷商品 ({d_label})', f'cmd:top:{ld}'),
(f'🏭 熱銷廠商 ({d_label})', f'cmd:vendor:{ld}')),
_row((f'⬅ 昨日商品 ({y_label})', f'cmd:top:{yesterday}'),
('🧬 商品健康', f'cmd:health:{ld}')),
_row(('📦 補貨預測', 'cmd:restock'),
('🗂 分類鑽取', 'menu:category')),
_row(('📅 指定日期', 'await:date_top')),
])
def _submenu_goals():
dg = _GOALS.get('daily', 0)
mg = _GOALS.get('monthly', 0)
qg = _GOALS.get('quarterly', 0)
hg = _GOALS.get('half', 0)
yg = _GOALS.get('yearly', 0)
def _fmt(v):
return f'{v/10000:.0f}' if v else '未設'
return _menu_with_back([
_row(('📋 查看達成率', 'cmd:goal')),
_row((f'日目標 ({_fmt(dg)})', 'await:goal_daily'),
(f'月目標 ({_fmt(mg)})', 'await:goal_monthly')),
_row((f'季目標 ({_fmt(qg)})', 'await:goal_quarterly'),
(f'半年目標 ({_fmt(hg)})', 'await:goal_half')),
_row((f'年目標 ({_fmt(yg)})', 'await:goal_yearly')),
])
def _submenu_analysis():
ld = _latest_date()
return _menu_with_back([
_row(('🎲 策略矩陣', f'cmd:strategy:{ld}'),
('📈 業績趨勢', 'menu:trend')),
_row(('🧬 商品健康', f'cmd:health:{ld}'),
('🗂 分類業績', f'cmd:category:{ld}')),
_row(('🎉 促銷追蹤', 'await:promo_range'),
('📦 補貨預測', 'cmd:restock')),
_row(('📊 趨勢圖表', 'cmd:chart'),
('🔄 同期比較', f'cmd:compare:{ld}')),
_row(('📅 指定日期', 'await:date_analysis')),
])
def _submenu_category():
"""分類業績鑽取 — 顯示 L1 固定分類按鈕。"""
ld = _latest_date()
cats = [
('美妝保養', '💄'), ('保健食品/用品', '💊'), ('母嬰', '👶'),
('食品飲料', '🍱'), ('家電', '🏠'), ('服裝內著', '👕'),
('個人清潔', '🧴'), ('運動用品/器材', '🏃'), ('寵物', '🐾'), ('其他', '📦'),
]
rows = []
for i in range(0, len(cats), 2):
rows.append([{
'text': f'{icon} {name}',
'callback_data': f'cmd:catdetail:{name}:{ld}'
} for name, icon in cats[i:i + 2]])
rows.append([{'text': '🗂 全分類清單', 'callback_data': f'cmd:category:{ld}'}])
return _menu_with_back(rows)
def _submenu_trend():
return _menu_with_back([
_row(('📅 近7日', 'cmd:trend:7'),
('📅 近1個月', 'cmd:trend:month')),
_row(('📅 近3個月', 'cmd:trend:quarter'),
('📅 近半年', 'cmd:trend:half')),
_row(('📅 本年度', 'cmd:trend:year'),
('📅 指定月份', 'await:date_trend_month')),
_row(('📅 指定年份', 'await:date_trend_year'),
('📅 指定季度', 'await:date_trend_quarter')),
])
def _submenu_reports():
return _menu_with_back([
_row(('📄 日報', 'cmd:ppt:daily'),
('📈 週報', 'cmd:ppt:weekly')),
_row(('📅 月報', 'cmd:ppt:monthly'),
('📋 下載報表', 'cmd:report')),
_row(('🧩 策略(日)', 'cmd:ppt:strategy'),
('🧩 策略(週)', 'cmd:ppt:strategy weekly')),
_row(('🧩 策略(月)', 'cmd:ppt:strategy monthly'),
('🧩 策略(季)', 'cmd:ppt:strategy quarterly')),
_row(('🧩 策略(半年)', 'cmd:ppt:strategy half'),
('🧩 策略(年)', 'cmd:ppt:strategy yearly')),
_row(('🎉 促銷效益簡報', 'await:promo_range'),
('🔍 競品比較', 'menu:competitor')),
_row(('🏭 廠商業績報告', 'cmd:ppt:vendor'),
('📅 指定日期日報', 'await:date_ppt_daily')),
_row(('📅 指定月份月報', 'await:date_ppt_monthly'),),
_row(('📊 季報 (本季)', 'cmd:ppt:quarterly'),
('📊 半年報 (本半)', 'cmd:ppt:half_yearly')),
_row(('📊 年報 (本年)', 'cmd:ppt:annual'),
('📊 TTM 滾動 12 月', 'cmd:ppt:ttm')),
_row(('🗂 品類深度報告', 'await:category_deep'),
('👥 客戶/訂單分析', 'cmd:ppt:customer')),
_row(('🎯 檔期前瞻報告', 'await:forecast_event'),
('🆚 多活動比較', 'await:promo_compare')),
_row(('🆕 新品 30 天追蹤', 'cmd:ppt:new_product'),
('🌐 市場情報週報', 'cmd:ppt:market_intel')),
_row(('💰 價格彈性報告', 'cmd:ppt:price_elasticity'),
('⚔️ 競業五力 v4', 'cmd:ppt:competitor_v4')),
])
def _submenu_market():
return _menu_with_back([
_row(('📰 電商新聞', 'cmd:news'),
('🌤 台北天氣', 'cmd:weather')),
_row(('🔥 Google熱搜', 'cmd:trends'),
('💬 Dcard口碑', 'cmd:dcard')),
_row(('💱 台銀匯率', 'cmd:exchange'),
('📅 電商節慶', 'cmd:calendar')),
_row(('▶️ YouTube爆紅商品', 'cmd:youtube'),
('🧠 AI學習狀態', 'cmd:learn')),
_row(('🔍 關鍵字比價', 'await:search_compare'),
('📷 圖片比價說明', 'cmd:photo_search_help')),
])
def _submenu_competitor():
"""競品日報第二層:所有選項直接產 PPT。"""
today = datetime.now(TAIPEI_TZ).date()
yesterday = today - timedelta(days=1)
td_str = today.strftime('%Y/%m/%d')
yd_str = yesterday.strftime('%Y/%m/%d')
td_label = today.strftime('%m/%d')
yd_label = yesterday.strftime('%m/%d')
return _menu_with_back([
_row((f'📊 今日簡報 ({td_label})', f'cmd:ppt:competitor {td_str}'),
(f'📊 昨日簡報 ({yd_label})', f'cmd:ppt:competitor {yd_str}')),
_row(('📈 本週比較', 'cmd:ppt:competitor weekly'),
('📆 本月比較', 'cmd:ppt:competitor monthly')),
_row(('🗃 本季比較', 'cmd:ppt:competitor quarterly'),
('📅 指定日期', 'await:date_competitor')),
_row(('📄 更多週期 →', 'menu:competitor_ppt')),
])
def _submenu_competitor_ppt():
"""競品 PPT 長週期選單(第三層)— 半年/年。"""
return _menu_with_back([
_row(('📆 半年比較', 'cmd:ppt:competitor half'),
('🗓 年比較', 'cmd:ppt:competitor yearly')),
])
_SUBMENUS = {
'main': main_menu_keyboard,
'sales': _submenu_sales,
'products': _submenu_products,
'goals': _submenu_goals,
'analysis': _submenu_analysis,
'trend': _submenu_trend,
'reports': _submenu_reports,
'market': _submenu_market,
'competitor': _submenu_competitor,
'competitor_ppt': _submenu_competitor_ppt,
'category': _submenu_category,
}