"""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'), ('🛰 AI 觀測台', 'menu:observability'), ('❓ 使用說明', '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')), ]) def _submenu_observability(): """Phase 38-49 AI 觀測台 — 對應 /observability/* 9 頁。""" return _menu_with_back([ _row(('🛰 觀測台總覽 (24h)', 'cmd:obs_overview'), ('🌐 Agent 編排矩陣', 'cmd:obs_orchestration')), _row(('💼 商業面 × AI', 'cmd:obs_business'),), _row(('📊 AI 呼叫總覽 (24h)', 'cmd:obs_ai_calls'), ('🏥 主機健康狀態', 'cmd:obs_health')), _row(('💰 預算控管 (當月)', 'cmd:obs_budget'), ('💬 反饋趨勢 (30d)', 'cmd:obs_quality')), ]) _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, 'observability': _submenu_observability, }