All checks were successful
CD Pipeline / deploy (push) Successful in 1m19s
Migration 017: - CREATE TABLE IF NOT EXISTS agent_context, action_plans, action_outcomes, agent_strategy_weights (all four ADR-012 tables were missing from production DB) - These tables are required by ElephantAlpha AutonomousEngine coordination loop telegram_templates.py: - Fix: from database.telegram_models → database.trend_models (TelegramUser has always lived in trend_models; telegram_models module does not exist) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
190 lines
6.6 KiB
Python
190 lines
6.6 KiB
Python
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"<b>🚨 {title}</b>\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"<b>⚠️ {title}</b>\n\n{summary}"
|
||
if details:
|
||
msg += "\n\n<b>細節:</b>\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"<b>📊 {title}</b> [{module}]{t_str}\n\n{content}"
|
||
|
||
def success(title: str, module: str, stats: str = "") -> str:
|
||
"""成功通知(綠色)"""
|
||
return f"<b>✅ {title}</b> [{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"<b>💰 自動降價建議</b>\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"<b>⚡ {tier_label} · {base_event.get('event_type', 'alert')}</b>\n"
|
||
f"📌 <code>{base_event.get('title')}</code>\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 += "<b>📋 建議行動:</b>\n" + "\n".join(f"• {a}" for a in ai_actions) + "\n\n"
|
||
if ai_executed:
|
||
msg += "<b>✅ 已執行:</b>\n" + "\n".join(f"• {a}" for a in ai_executed) + "\n\n"
|
||
|
||
trace = base_event.get("trace")
|
||
if trace:
|
||
msg += f"<pre>{trace[-500:]}</pre>"
|
||
|
||
# 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"<b>📊 {title}</b> ({report_type})\n"
|
||
f"期間:{period}\n\n"
|
||
f"{content_md}"
|
||
)
|
||
|
||
def success(title: str, module: str, stats: str = "") -> str:
|
||
"""成功通知(綠色)"""
|
||
return f"<b>✅ {title}</b> [{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)
|