diff --git a/routes/openclaw_bot_routes.py b/routes/openclaw_bot_routes.py index df0513a..63c2cce 100644 --- a/routes/openclaw_bot_routes.py +++ b/routes/openclaw_bot_routes.py @@ -1890,6 +1890,7 @@ def _ppt_ai_analysis(prompt_data: str, report_type: str = '') -> str: is_forecast = '檔期前瞻' in report_type or 'forecast' in report_type is_promo_cmp = '多活動' in report_type or 'promo_compare' in report_type is_new_prod = '新品' in report_type or 'new_product' in report_type + is_market_intel = '市場情報' in report_type or 'market_intel' in report_type # ── 格式鐵律(所有 prompt 共用後綴)──────────────────────── FORMAT_RULES = ( @@ -2047,6 +2048,41 @@ def _ppt_ai_analysis(prompt_data: str, report_type: str = '') -> str: + FORMAT_RULES ) max_tokens = 1400 + elif is_market_intel: + sys_instruction = ( + "你身兼 (1) 行銷情報分析師(精通競品監控、消費趨勢、社群口碑分析)" + "(2) BU 主管(策略決策層級的市場敏感度)。\n" + "你的客戶是 momo CEO/BU 主管/行銷主管,會用本報告了解外部市場大局," + "做下週的廣告投放、檔期備戰、競品阻擊決策。\n\n" + f"請針對以下{report_type}(外部資料彙整:節慶日曆、季節情境、電商新聞、" + "Google Trends、Dcard、YouTube、天氣、匯率)輸出戰略洞察:\n\n" + "【本週市場大事】(3-4 句)\n" + "(1) 即將到來的關鍵檔期與其對 momo 業績的影響預估\n" + "(2) 當前最熱門的消費賽道(依 Google Trends + Dcard + YouTube 信號)\n" + "(3) 本週外部最大風險(如競品大檔期、不利天氣、匯率波動)。\n\n" + "【消費者情緒與口碑解讀】(3-4 句)\n" + "Dcard 熱門討論主題反映什麼消費焦慮(價格 / 安全 / 認證 / 永續);" + "YouTube 爆紅商品的特徵(顏值 / 解決痛點 / 名人推薦 / IP 聯名);" + "建議 momo 在哪些品類加碼選品或行銷投入。\n\n" + "【競爭態勢與差異化】(3-4 句)\n" + "蝦皮、PChome、酷澎、博客來等競品本週動態(依電商新聞);" + "momo 應該強化哪些差異化武器(會員訂閱 / 直播帶貨 / 富邦銀行折扣);" + "若競品有大檔期,給出阻擊策略(價格戰避戰 / 服務力差異 / 限時加碼)。\n\n" + "【行動建議 — SMART 框架】\n" + "■ 本週立即執行(3 條,✅ 開頭):廣告投放調整 / 商品上架 / 競品比價\n" + "■ 下週預備(3 條,✅ 開頭):檔期商品規劃 / 行銷檔期協作 / 庫存備援\n" + "■ 本月戰略(2 條,✅ 開頭):消費賽道選品 / 行銷主題定位\n" + "每條必須 SMART:具體商品/品類 + 量化目標 + 期限。\n\n" + "【最大三大外部風險】(2-3 句)\n" + "(a) 政策法規變動(如電商營業稅、進口商品法規)\n" + "(b) 匯率波動(影響進口商品成本)\n" + "(c) 競品大檔期或新平台進入(如 Temu、酷澎激進補貼)\n\n" + "要求:每段引用具體外部信號(Trend 關鍵字、Dcard 主題、新聞標題等)," + "全文 800~1100 字,禁用模糊用詞。" + + MARKET_TREND_2026 + + FORMAT_RULES + ) + max_tokens = 2000 elif is_new_prod: sys_instruction = ( "你身兼 (1) PM 商品經理(精通新品上架 / 商品生命週期 / SKU 健康度)" @@ -2791,7 +2827,7 @@ def _generate_ppt_cmd(sub_type: str, sub_arg: str, _chat_id: int, target: str, generate_vendor_ppt, generate_period_review_ppt, generate_category_deep_ppt, generate_customer_analytics_ppt, generate_forecast_pre_event_ppt, generate_promo_compare_ppt, - generate_new_product_ppt, + generate_new_product_ppt, generate_market_intel_weekly_ppt, check_pptx_available ) except ImportError: @@ -3316,6 +3352,62 @@ def _generate_ppt_cmd(sub_type: str, sub_arg: str, _chat_id: int, target: str, }) return ppt_path + elif sub_type in ('market_intel', 'intel', '市場情報', '情報週報'): + # /ppt market_intel 本週市場情報 + from datetime import datetime as _dt, timedelta as _td + today = now.date() if hasattr(now, 'date') else now + # 對齊週一作為週起點 + week_start = today - _td(days=today.weekday()) + week_label = f"{week_start.strftime('%Y/%m/%d')} 起一週" + + params = {'report_type': 'market_intel', 'week': week_label} + cached, cached_ai = _load_cached_ppt_path_and_analysis('market_intel', params) + if cached: + return cached + + # 抓 mcp_collector 各 API(容錯:失敗段落填預設文字) + from services.mcp_collector_service import mcp_collector + from services.mcp_context_service import ( + get_ecommerce_news, get_taiwan_trends, get_dcard_trends, + get_youtube_trending, get_taiwan_weather, get_twbank_exchange_rates, + ) + + def _safe(fn, default='(本次擷取失敗或無資料)'): + try: + r = fn() + return r.strip() if r and r.strip() else default + except Exception as e: + sys_log.warning(f"[market_intel] {fn.__name__} fail: {e}") + return default + + sections = { + 'holiday': mcp_collector.get_holiday_context(), + 'seasonal': mcp_collector.get_seasonal_context(), + 'ecommerce_news': _safe(get_ecommerce_news), + 'google_trends': _safe(get_taiwan_trends), + 'dcard': _safe(get_dcard_trends), + 'youtube': _safe(get_youtube_trending), + 'weather': _safe(get_taiwan_weather), + 'exchange': _safe(get_twbank_exchange_rates), + } + + # 組 data summary 給 AI + data_summary_parts = [f"【本週】{week_label}"] + for k, v in sections.items(): + data_summary_parts.append(f"\n【{k}】\n{v[:600]}") + data_summary = '\n'.join(data_summary_parts) + + ai_text = cached_ai or _ppt_ai_analysis(data_summary, '市場情報週報') + if not cached_ai and _ppt_needs_fallback(ai_text): + ai_text = _ppt_fallback_insight('市場情報', data_summary, '') + + ppt_path = generate_market_intel_weekly_ppt(week_label, sections, ai_text) + _store_ppt_cache('market_intel', params, ppt_path, { + 'report_type': 'market_intel', 'parameters': params, + 'data_summary': data_summary, 'analysis': ai_text, 'mcp': '', + }) + return ppt_path + elif sub_type in ('new_product', 'newproduct', '新品', '新品追蹤'): # /ppt new_product 預設 30 天追蹤 # /ppt new_product 14 自訂追蹤天數 diff --git a/services/openclaw_bot/menu_keyboards.py b/services/openclaw_bot/menu_keyboards.py index 60f4a5b..4f5aed7 100644 --- a/services/openclaw_bot/menu_keyboards.py +++ b/services/openclaw_bot/menu_keyboards.py @@ -220,7 +220,8 @@ def _submenu_reports(): ('👥 客戶/訂單分析', 'cmd:ppt:customer')), _row(('🎯 檔期前瞻報告', 'await:forecast_event'), ('🆚 多活動比較', 'await:promo_compare')), - _row(('🆕 新品 30 天追蹤', 'cmd:ppt:new_product'),), + _row(('🆕 新品 30 天追蹤', 'cmd:ppt:new_product'), + ('🌐 市場情報週報', 'cmd:ppt:market_intel')), ]) diff --git a/services/ppt_generator.py b/services/ppt_generator.py index 560104f..517cc85 100644 --- a/services/ppt_generator.py +++ b/services/ppt_generator.py @@ -62,6 +62,7 @@ TEMPLATE_VERSIONS = { 'forecast_pre_event': 'v3.1.0', # 2026-05-03 檔期前瞻報(baseline × lift_factor 預測 + 去年同檔期) 'promo_compare': 'v3.1.0', # 2026-05-03 多活動 ROI 並排比較 'new_product': 'v3.1.0', # 2026-05-03 新品 30 天追蹤(PM/採購) + 'market_intel': 'v3.1.0', # 2026-05-03 市場情報週報(外部資料彙整) } @@ -2952,6 +2953,164 @@ def generate_vendor_ppt(yr, mo, db_data, ai_text: str) -> str: return path +# ── 市場情報週報(外部資料彙整)──────────────────────────────────────── +def generate_market_intel_weekly_ppt(week_label: str, db_data: dict, ai_text: str) -> str: + """市場情報週報 v3.1(CEO/BU 主管 / 行銷主管 三方共讀) + 將 mcp_collector 的所有外部資料彙整成內部參考簡報 + P1 封面:本週市場大事三句話 + P2 節慶/檔期日曆(當週 + 下兩週) + P3 季節情境與消費行為趨勢 + P4 電商新聞動態(Gemini Grounding) + P5 Google Trends 熱搜 + Dcard 口碑 + P6 YouTube 熱門商品 + P7 天氣與匯率影響 + P8 AI 整合洞察與行動建議 + P9 附錄 + """ + from pptx import Presentation + from pptx.util import Cm + + prs = Presentation() + prs.slide_width = Cm(33.87) + prs.slide_height = Cm(19.05) + W = 33.87 + + sections = db_data or {} + + # ── P1 封面 ─────────────────────────────────────────── + slide = prs.slides.add_slide(prs.slide_layouts[6]) + H = 19.05 + _add_rect(slide, 0, 0, W, H, _BG_PAPER) + _add_rect(slide, 0, 0, 3.0, H, _KPI_HONEY) + _add_rect(slide, 2.85, 0, 0.15, H, _BRAND_OG2) + _add_rect(slide, W - 6.0, 0, 6.0, 0.45, _BRAND_OG2) + _add_rect(slide, W - 6.0, 0.45, 6.0, 0.12, _KPI_HONEY) + _add_rect(slide, 4.0, 8.4, 22.0, 0.06, _KPI_HONEY) + + _add_rect(slide, 3.8, 1.4, 4.8, 0.85, _BRAND_OG2) + _add_text(slide, "OPENCLAW", 3.8, 1.42, 4.8, 0.81, + bold=True, size=12, color=_WHITE, align="center", valign="middle", + latin_font=_FONT_LABEL) + _add_text(slide, "MARKET INTELLIGENCE WEEKLY · EXTERNAL SIGNALS", + 3.8, 2.45, 22, 0.55, + bold=True, size=10, color=_BRAND_OG2, + latin_font=_FONT_LABEL) + _add_text(slide, f"市場情報週報\n{week_label}", + 3.8, 3.2, 25, 5.0, + bold=True, size=42, color=_DARK_TEXT, + latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA) + _add_rect(slide, W - 9.0, 3.4, 5.0, 1.1, _KPI_HONEY) + _add_text(slide, "外部信號整合", + W - 9.0, 3.45, 5.0, 1.0, + bold=True, size=14, color=_WHITE, align="center", valign="middle", + ea_font=_FONT_BODY_EA) + _add_text(slide, + f"來源:節慶日曆 · Google Trends · Dcard · YouTube · " + f"電商新聞 · 天氣 · 匯率", + 3.8, 8.7, 27, 0.85, + size=12, color=_BRAND_OG2, + latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA) + + # 三句話本週要點(從 AI text 抽前三段) + ai_lines = [l.strip() for l in (ai_text or '').split('\n') + if l.strip() and not l.strip().startswith(('【', '■', '✅'))][:3] + pitch_y = 10.2 + for i, (color, label) in enumerate([ + ("C96442", "🎯 本週重點"), + ("B88416", "📊 市場機會"), + ("8F4530", "⚠ 風險警訊"), + ]): + py = pitch_y + i * 1.9 + _add_rect(slide, 3.8, py, 0.45, 1.5, color) + _add_text(slide, label, 4.4, py + 0.1, 27, 0.55, + bold=True, size=11, color=color, + ea_font=_FONT_BODY_EA, latin_font=_FONT_LABEL) + body = ai_lines[i] if i < len(ai_lines) else "(待 AI 補充)" + _add_text(slide, body[:100], + 4.4, py + 0.7, 27, 0.75, + size=12, color=_DARK_TEXT, + latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA) + + _add_text(slide, "Generated by OpenClaw AI Agent", + W - 7.5, H - 1.4, 7.0, 0.5, + size=9, color=_SUBTEXT, align="right", latin_font=_FONT_LABEL) + _add_text(slide, f"📅 {datetime.now().strftime('%Y/%m/%d %H:%M')}", + W - 7.5, H - 1.95, 7.0, 0.5, + bold=True, size=11, color=_BRAND_OG2, align="right", + latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA) + _add_footer(slide, W) + + # 內容卡片頁 helper(每頁 2 卡) + def _intel_double_card(prs, title_text, card1_title, card1_body, card1_color, + card2_title, card2_body, card2_color): + s = prs.slides.add_slide(prs.slide_layouts[6]) + _add_rect(s, 0, 0, W, _SLIDE_H, _BG_PAPER) + _add_header(s, title_text) + col_w = (W - 1.2) / 2 + col1_x = 0.4 + col2_x = 0.4 + col_w + 0.4 + + for x, t, b, color in [(col1_x, card1_title, card1_body, card1_color), + (col2_x, card2_title, card2_body, card2_color)]: + _add_rect(s, x, 1.95, col_w, 13.4, _WHITE, line_hex=_SUBTLE) + _add_rect(s, x, 1.95, col_w, 0.85, color) + _add_text(s, t, x + 0.4, 2.05, col_w - 0.6, 0.65, + bold=True, size=13, color=_WHITE, valign="middle", + ea_font=_FONT_BODY_EA) + body_text = (b or '').strip() or "(暫無資料)" + _add_text(s, body_text, + x + 0.5, 3.0, col_w - 1.0, 12.2, + size=12, color=_DARK_TEXT, wrap=True, + latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA) + _add_footer(s, W) + + # ── P2: 節慶日曆 + 季節情境 ────────────────────────── + _intel_double_card(prs, "📅 節慶檔期 + 季節情境", + "🎉 本月+下月關鍵檔期", + sections.get('holiday', '(暫無檔期資料)'), + _BRAND_OG, + "🌿 季節消費情境", + sections.get('seasonal', '(暫無季節資料)'), + _KPI_MAHOGANY) + + # ── P3: 電商新聞 + Google Trends ──────────────────── + _intel_double_card(prs, "📰 電商新聞 + 🔥 Google 熱搜", + "📰 電商產業新聞", + sections.get('ecommerce_news', '(無資料)'), + _KPI_HONEY, + "🔥 Google 台灣熱搜", + sections.get('google_trends', '(無資料)'), + _BRAND_OG) + + # ── P4: Dcard + YouTube ───────────────────────────── + _intel_double_card(prs, "💬 Dcard 口碑 + ▶️ YouTube 熱門", + "💬 Dcard 熱門討論", + sections.get('dcard', '(無資料)'), + "8F4530", + "▶️ YouTube 爆紅商品", + sections.get('youtube', '(無資料)'), + _KPI_EARTH) + + # ── P5: 天氣 + 匯率 ────────────────────────────────── + _intel_double_card(prs, "🌤 天氣 + 💱 匯率(影響消費行為)", + "🌤 台灣近日天氣", + sections.get('weather', '(無資料)'), + "2D5D80", + "💱 台幣匯率(跨境採購成本)", + sections.get('exchange', '(無資料)'), + "2A7A3F") + + # ── P6: AI 洞察 ─────────────────────────────────────── + _ai_insight_slide(prs, ai_text) + + # ── P7: 附錄 ────────────────────────────────────────── + _appendix_slide(prs, 'market_intel_weekly', week_label) + + path = _new_path("market_intel") + prs.save(path) + return path + + # ── 新品 30 天追蹤報告 ────────────────────────────────────────────────── def generate_new_product_ppt(db_data: dict, ai_text: str) -> str: """新品 30 天追蹤報告 v3.1(PM/採購用)