Some checks failed
CD Pipeline / deploy (push) Failing after 59s
- 建立 Gitea Actions CD pipeline (.gitea/workflows/cd.yaml) - 部署模式: rsync Python 檔案至 188 → docker restart (volume mount) - Dockerfile/requirements 變動時自動重建 Docker image - 部署通知: Telegram (開始/成功/失敗) - 健康檢查: https://mo.wooo.work/health (最多 5 次重試) - 同步最新 CLAUDE.md / ADR-008 / memory (2026-04-19) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
163 lines
4.6 KiB
Python
163 lines
4.6 KiB
Python
"""
|
|
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,
|
|
)
|