import json import logging from typing import Any, Dict, Optional from database.manager import get_session from database.trend_models import TelegramUser sys_log = logging.getLogger("TelegramTpl") # ─── 常數 ──────────────────────────────────────────────── TELEGRAM_BOT_TOKEN_ENV = "TELEGRAM_BOT_TOKEN" TELEGRAM_CHAT_IDS_ENV = "TELEGRAM_CHAT_IDS" # ─── 工具:取得 Token 與 Chat ID(容錯) ───────────────── def _get_bot_token() -> Optional[str]: from dotenv import load_dotenv load_dotenv() import os return os.getenv(TELEGRAM_BOT_TOKEN_ENV) def _get_chat_ids() -> list: token = _get_bot_token() if not token: sys_log.warning("[TelegramTpl] %s 未設定,跳過 Telegram 通知", TELEGRAM_BOT_TOKEN_ENV) return [] raw = __import__("os").getenv(TELEGRAM_CHAT_IDS_ENV, "[]") try: return json.loads(raw) except json.JSONDecodeError: sys_log.warning("[TelegramTpl] %s 格式錯誤,應為 JSON 陣列", TELEGRAM_CHAT_IDS_ENV) return [] # ─── 原始發送(內部使用) ───────────────────────────────── def _send_telegram_raw(text: str, chat_ids: Optional[list] = None, reply_markup: Optional[Dict[str, Any]] = None, parse_mode: str = "HTML") -> bool: import requests token = _get_bot_token() if not token: return False if chat_ids is None: chat_ids = _get_chat_ids() if not chat_ids: chat_ids = [-1003940688311] # fallback url = f"https://api.telegram.org/bot{token}/sendMessage" payload = { "chat_id": chat_ids[0], "text": text, "parse_mode": parse_mode, } if reply_markup: payload["reply_markup"] = json.dumps(reply_markup, ensure_ascii=False) try: r = requests.post(url, json=payload, timeout=10) if not r.ok: sys_log.warning("[TelegramTpl] sendMessage HTTP %s: %s", r.status_code, r.text[:200]) return False return True except Exception as e: sys_log.error("[TelegramTpl] send 失敗: %s", e) return False # ─── 公用模板 ───────────────────────────────────────────── def alert(title: str, content: str, actions: Optional[list] = None) -> str: """高危險警報(紅色)""" msg = f"🚨 {title}\n\n{content}" if actions: msg += "\n\n" + "\n".join(f"• {a}" for a in actions) return msg def warning(title: str, summary: str, details: Optional[dict] = None) -> str: """中風險警告(橙色)""" msg = f"⚠️ {title}\n\n{summary}" if details: msg += "\n\n細節:\n" + "\n".join(f"• {k}: {v}" for k, v in details.items()) return msg def info(title: str, module: str, content: str, time: Optional[Any] = None) -> str: """普通信息(藍色)""" t_str = f" · {time}" if time else "" return f"📊 {title} [{module}]{t_str}\n\n{content}" def success(title: str, module: str, stats: str = "") -> str: """成功通知(綠色)""" return f"✅ {title} [{module}]\n{stats}" def price_decision( product_name: str, product_sku: str, current_price: float, suggested_price: float, reason: str, insight_id: Optional[int] = None, ) -> tuple: """ 降價決策通知(含 Inline Keyboard)。 回傳 (message_text, reply_markup) """ diff = current_price - suggested_price if diff > 0: action_text = f"降價 ${diff:,.0f}" elif diff < 0: action_text = f"提價 ${-diff:,.0f}" else: action_text = "維持" message = ( f"💰 自動降價建議\n" f"商品:{product_name} (SKU: {product_sku})\n" f"現價:${current_price:,.0f} → 建議:${suggested_price:,.0f}\n" f"原因:{reason}\n" ) if insight_id: message += f"洞察 ID:{insight_id}\n" keyboard = { "inline_keyboard": [ [ {"text": "✅ 確認執行", "callback_data": f"price_decision:approve:{product_sku}"}, {"text": "❌ 拒絕", "callback_data": f"price_decision:reject:{product_sku}"}, ], [ {"text": "📊 查看洞察", "url": f"https://your-dashboard.example/insight/{insight_id}" if insight_id else "#"}, ], ] } return message, keyboard def triaged_alert( base_event: Dict[str, Any], tier_label: str, ai_summary: str, ai_cause: Optional[str] = None, ai_actions: Optional[list] = None, ai_executed: Optional[list] = None, ) -> str: """ L1/L2 整合通知(帶 AI 摘要與可執行動作)。 """ msg = ( f"⚡ {tier_label} · {base_event.get('event_type', 'alert')}\n" f"📌 {base_event.get('title')}\n\n" ) summary = base_event.get("summary", "") if summary: msg += f"🔍 概要:{summary}\n\n" if ai_summary: msg += f"🧠 AI 摘要:{ai_summary}\n\n" if ai_cause: msg += f"💡 可能原因:{ai_cause}\n\n" if ai_actions: msg += "📋 建議行動:\n" + "\n".join(f"• {a}" for a in ai_actions) + "\n\n" if ai_executed: msg += "✅ 已執行:\n" + "\n".join(f"• {a}" for a in ai_executed) + "\n\n" trace = base_event.get("trace") if trace: msg += f"
{trace[-500:]}
" # W2-D: momo: prefix 強制(共用 Bot 鐵律,ADR-011) event_id = base_event.get("id", "unknown") keyboard = { "inline_keyboard": [ [{"text": "📊 查看详情", "url": f"https://dashboard.example/event/{event_id}"}], [{"text": "🛑 忽略此事件", "callback_data": f"momo:event_ignore:{event_id}"}], ] } return msg, keyboard def report(title: str, report_type: str, period: str, content_md: str) -> str: """策略/週報模板""" return ( f"📊 {title} ({report_type})\n" f"期間:{period}\n\n" f"{content_md}" ) def success(title: str, module: str, stats: str = "") -> str: """成功通知(綠色)""" return f"✅ {title} [{module}]\n{stats}" def _send_telegram(msg: str, chat_ids: Optional[list] = None, reply_markup: Optional[Dict[str, Any]] = None) -> bool: return _send_telegram_raw(msg, chat_ids=chat_ids, reply_markup=reply_markup)