Files
ewoooc/services/openclaw_bot/menu_keyboards.py
OoO 822789c810
All checks were successful
CD Pipeline / deploy (push) Successful in 2m58s
feat(p49): Telegram 補完 9 頁對應 + daily summary 加商業面未跟進警示
M-B: Telegram 對應從 6/9 → 9/9
新增 3 個 cmd handler,對應 Phase 45-48 的 3 個新觀測頁:
- cmd:obs_overview — 一頁式總覽(三主機 24h + AI 呼叫 + 月成本 + 待審 episode)
- cmd:obs_orchestration — Agent 編排矩陣(4 Agent × Models 24h 數字)
  本地 Ollama % / RAG 命中 % / 錯誤率 + cost
- cmd:obs_business — 商業面 × AI(價格決策 7d by strategy
  + 未跟進機會 + Outcomes verdict 30d)

services/openclaw_bot/menu_keyboards.py::_submenu_observability 升級為 9 項

M-C: daily summary(每日 09:30)加商業面警示
- 從 ai_price_recommendations × action_plans 跨表 JOIN
  偵測 high-confidence (≥0.7) 卻無對應 action_plan 的「機會流失」
- 7d 內若有未跟進,daily summary 自動標 ⚠️ 警示
- 對應 Phase 48 business_intel 頁同個邏輯,閉環推送

inline keyboard 升級:日報附 6 個入口(總覽/編排/商業面/主機/AI/預算),
不再只有 4 個

Phase 38→49 累計 14 commits。觀測台戰役完整收官:
- 9 頁全部對應 Telegram cmd
- DB 22/22 = 100% 全覆蓋
- 6 個 L2 一鍵 + 3 種主動推送(即時/異常/日常)
- 日報含商業面警示

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

300 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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,
}