All checks were successful
CD Pipeline / deploy (push) Successful in 3m37s
Wave 1.2:用一份 generator 解 4 種時間維度報表(季/半年/年/TTM)。 generate_period_review_ppt(services/ppt_generator.py)— 12 頁 - 期間類型徽章自動切換(季報/半年報/年報/TTM 滾動) - P1 封面:含業績狀態徽章 + elevator pitch + 期間動能 - P2 執行摘要:KPI v2 含 △% (vs 上季/上半/去年) + AI 解讀 + YoY 對比帶 - P3 月度業績走勢(matplotlib 折線:本期 + 上期 + 月均線 + 高低點)+ 4 卡指標 - P4 品類分析:橫條 + 帕雷托雙視圖 - P5-P7 TOP 50 商品(自動分頁) - P8 TOP 30 廠商 - P9 MCP 市場情報 - P10 AI 結構化洞察 - P11 附錄 query_period_summary(routes/openclaw_bot_routes.py) - 一次拉齊:kpis / monthly_breakdown / top_products(50) / top_categories(8) / top_vendors(30) - 自動算月度聚合(PostgreSQL TO_CHAR) 路由層加 4 種 sub_type 分支: - /ppt quarterly [YYYY/Q1-4] 當季或指定季 - /ppt half_yearly [YYYY/H1-2] 當半年或指定半年 - /ppt annual [YYYY] 當年或指定年 - /ppt ttm 最近 12 個月(自動算到本月底) 每種自動抓三段資料:本期 + 上期同等 + 去年同期 - quarterly: 上季 + 去年同季 - half_yearly: 上半年 + 去年同半年 - annual: 去年 + 前年 - ttm: 上一個 TTM (24-12 月前) + 同上 _ppt_ai_analysis 加 is_period 分支 - 角色:策略顧問 + BU 主管 + CFO 三合一視角 - 結構:整體解讀 / 市場趨勢對位 / 月度走勢分析 / 品類結構 / 戰略級 SMART / 風險預警 - SMART 行動分三層:下期立即啟動(30 天)/ 下期戰略(60-90 天)/ 下下期預備(6-12 月) - max_tokens 2600,字數 1000-1300 Telegram 按鈕:報表選單加 4 顆季/半/年/TTM 按鈕 bump TEMPLATE_VERSIONS:quarterly / half_yearly / annual / ttm 全 v3.1.0 煙霧測試:4 種全綠,每份 12 頁 220KB。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
277 lines
9.8 KiB
Python
277 lines
9.8 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')),
|
|
])
|
|
|
|
|
|
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,
|
|
}
|