""" services/ppt_generator.py 緊急復原版 (2026-04-18) — 原始檔案 4/17 間遺失 依 openclaw_bot_routes.py 呼叫約定提供 7 個 function: check_pptx_available, generate_daily_ppt, generate_weekly_ppt, generate_monthly_ppt, generate_strategy_ppt, generate_competitor_ppt, generate_promo_ppt 每個 generate_* 回傳 pptx 檔案路徑 (str)。 """ import os import uuid from datetime import datetime from pathlib import Path REPORTS_DIR = Path(os.environ.get("REPORTS_DIR", "/app/data/reports")) REPORTS_DIR.mkdir(parents=True, exist_ok=True) def check_pptx_available() -> bool: try: import pptx # noqa: F401 return True except ImportError: return False def _new_path(kind: str) -> str: rid = uuid.uuid4().hex[:8] return str(REPORTS_DIR / f"ocbot_{kind}_{rid}.pptx") def _format_data(data) -> str: if data is None: return "(無資料)" if isinstance(data, dict): if not data: return "(空 dict)" lines = [] for k, v in list(data.items())[:30]: line = f"• {k}: {v}" lines.append(line[:200]) return "\n".join(lines) if isinstance(data, list): if not data: return "(空 list)" lines = [] for item in data[:20]: lines.append(f"• {str(item)[:200]}") return "\n".join(lines) return str(data)[:2000] def _build_ppt(filename_kind: str, title: str, subtitle: str, sections: list) -> str: """sections = [(heading, body), ...]""" from pptx import Presentation from pptx.util import Pt prs = Presentation() # 標題頁 s = prs.slides.add_slide(prs.slide_layouts[0]) if s.shapes.title is not None: s.shapes.title.text = title if len(s.placeholders) > 1: s.placeholders[1].text = subtitle # 內容頁 for heading, body in sections: s = prs.slides.add_slide(prs.slide_layouts[1]) if s.shapes.title is not None: s.shapes.title.text = heading body_text = body if isinstance(body, str) else _format_data(body) body_text = body_text[:3500] if len(s.placeholders) > 1: tf = s.placeholders[1].text_frame tf.text = body_text for p in tf.paragraphs: for r in p.runs: r.font.size = Pt(14) # 產生路徑並存檔 path = _new_path(filename_kind) prs.save(path) return path def generate_daily_ppt(date_str: str, db_data, ai_text: str) -> str: sections = [ ("本日銷售概況", _format_data(db_data)), ("AI 洞察", ai_text or "(暫無 AI 分析)"), ] return _build_ppt( "daily_daily", f"日報 {date_str}", f"生成時間 {datetime.now().strftime('%Y-%m-%d %H:%M')}", sections, ) def generate_weekly_ppt(db_data, ai_text: str) -> str: sections = [ ("本週銷售概況", _format_data(db_data)), ("AI 洞察", ai_text or "(暫無 AI 分析)"), ] return _build_ppt( "weekly", "週報", f"生成時間 {datetime.now().strftime('%Y-%m-%d %H:%M')}", sections, ) def generate_monthly_ppt(yr, mo, db_data, ai_text: str) -> str: sections = [ (f"{yr}/{mo} 月度概況", _format_data(db_data)), ("AI 洞察", ai_text or "(暫無 AI 分析)"), ] return _build_ppt( "monthly", f"月報 {yr}/{mo}", f"生成時間 {datetime.now().strftime('%Y-%m-%d %H:%M')}", sections, ) def generate_strategy_ppt(date_str: str, db_data, ai_text: str) -> str: sections = [ ("策略資料", _format_data(db_data)), ("AI 洞察", ai_text or "(暫無 AI 分析)"), ] return _build_ppt( "strategy", f"策略報告 {date_str}", f"生成時間 {datetime.now().strftime('%Y-%m-%d %H:%M')}", sections, ) def generate_competitor_ppt(period_label: str, db_data, ai_text: str) -> str: sections = [ (f"競品資料 ({period_label})", _format_data(db_data)), ("AI 洞察", ai_text or "(暫無 AI 分析)"), ] return _build_ppt( "competitor", f"競品分析 {period_label}", f"生成時間 {datetime.now().strftime('%Y-%m-%d %H:%M')}", sections, ) def generate_promo_ppt(promo_label: str, data, ai_text: str) -> str: sections = [ (f"促銷資料 ({promo_label})", _format_data(data)), ("AI 洞察", ai_text or "(暫無 AI 分析)"), ] return _build_ppt( "promo", f"促銷報告 {promo_label}", f"生成時間 {datetime.now().strftime('%Y-%m-%d %H:%M')}", sections, )