[V10.289] 重排 EA HITL Telegram 告警格式 | telegram_templates.py
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s

This commit is contained in:
OoO
2026-05-19 23:35:16 +08:00
parent 645cd397c2
commit 91601da3f8
5 changed files with 254 additions and 21 deletions

View File

@@ -21,6 +21,7 @@ import io
import json
import logging
import os
import re
from html import escape
from datetime import datetime
from typing import Any, Dict, List, Optional
@@ -467,6 +468,183 @@ def meta_analysis_msg(period: str, content: str) -> str:
# 💡 洞察類模板
# ══════════════════════════════════════════════════════════════════════════════
def _short_text(value: Any, limit: int = 120) -> str:
text = str(value or "").strip()
if len(text) <= limit:
return text
return text[: max(0, limit - 1)].rstrip() + ""
def _split_action_parts(action: Any) -> List[str]:
return [
part.strip()
for part in re.split(r"\s*[|]\s*", str(action or ""))
if part and part.strip()
]
def _parse_ea_action(action: Any) -> Dict[str, Any]:
parts = _split_action_parts(action)
item: Dict[str, Any] = {
"raw": str(action or ""),
"title": parts[0] if parts else str(action or ""),
"notes": [],
}
title_match = re.match(r"^\[([^\]]+)\]\s*(.+)$", item["title"])
if title_match:
item["sku"] = title_match.group(1).strip()
item["name"] = title_match.group(2).strip()
for part in parts[1:]:
if "MOMO" in part and "PChome" in part:
item["comparison"] = part
momo = re.search(r"MOMO\s*\$([0-9,]+)", part)
pchome = re.search(r"PChome\s*\$([0-9,]+)", part)
pct = re.search(r"\(([+-]?\d+(?:\.\d+)?)%\)", part)
if momo:
item["momo_price"] = momo.group(1)
if pchome:
item["pchome_price"] = pchome.group(1)
if pct:
item["gap_pct"] = float(pct.group(1))
item["gap_pct_text"] = f"{float(pct.group(1)):+.1f}%"
elif part.startswith("PChome "):
item["pchome_id"] = part.replace("PChome", "", 1).strip()
elif part.startswith("建議"):
item["action"] = part
elif "NT$" in part or "流失" in part or "價差" in part or "價格優勢" in part:
item["impact"] = part
amount = re.search(r"NT\$\s*([0-9,]+)", part)
if amount:
try:
item["impact_amount"] = int(amount.group(1).replace(",", ""))
except ValueError:
pass
else:
item["notes"].append(part)
return item
def _format_ea_risk_summary(actions: List[Dict[str, Any]]) -> List[str]:
count = len(actions)
gap_values = [a["gap_pct"] for a in actions if isinstance(a.get("gap_pct"), (int, float))]
amounts = [a["impact_amount"] for a in actions if isinstance(a.get("impact_amount"), int)]
lines = [f"• 待審 SKU<b>{count}</b> 件"]
if gap_values:
low, high = min(gap_values), max(gap_values)
if low == high:
lines.append(f"• 價差幅度:<b>{low:+.1f}%</b>")
else:
lines.append(f"• 價差範圍:<b>{low:+.1f}%{high:+.1f}%</b>")
if amounts:
lines.append(f"• 最大單件價差:<b>NT$ {max(amounts):,}</b>")
lines.append("• 核心判斷:先確認同款 identity_v2再決定跟價、促銷或曝光")
return lines
def _format_ea_action_card(item: Dict[str, Any], index: int) -> List[str]:
sku = escape(str(item.get("sku") or ""))
name = escape(_short_text(item.get("name") or item.get("title") or "", 58))
heading = f"<b>{index}. [{sku}] {name}</b>" if sku else f"<b>{index}. {name}</b>"
lines = [heading]
momo_price = item.get("momo_price")
pchome_price = item.get("pchome_price")
if momo_price or pchome_price:
momo_text = f"${momo_price}" if momo_price else ""
pchome_text = f"${pchome_price}" if pchome_price else ""
lines.append(f" MOMO<b>{momo_text}</b> PChome<b>{pchome_text}</b>")
gap_text = item.get("gap_pct_text")
impact = item.get("impact")
if gap_text or impact:
impact_text = escape(str(impact or ""))
if gap_text and impact_text:
lines.append(f" 價差:<b>{escape(str(gap_text))}</b> {impact_text}")
elif gap_text:
lines.append(f" 價差:<b>{escape(str(gap_text))}</b>")
else:
lines.append(f" 影響:{impact_text}")
action = str(item.get("action") or "").replace("建議", "", 1).strip(" :")
if action:
lines.append(f" 動作:{escape(_short_text(action, 86))}")
if item.get("pchome_id"):
lines.append(f" PChome<code>{escape(_short_text(item['pchome_id'], 40))}</code>")
return lines
def _format_ea_escalation_alert(
*,
base_event: Dict[str, Any],
tier_label: str,
ai_summary: str,
ai_cause: Optional[str],
ai_actions: Optional[list],
) -> str:
event_type = escape(str(base_event.get("event_type", "ea_escalation")))
title = escape(str(base_event.get("title", "EA 升級審核")))
summary = escape(str(base_event.get("summary", "")))
cause_parts = [
escape(part.strip())
for part in str(ai_cause or "").split("|")
if part and part.strip()
]
parsed_actions = [_parse_ea_action(action) for action in (ai_actions or [])]
shown_actions = parsed_actions[:5]
hidden_count = max(0, len(parsed_actions) - len(shown_actions))
lines = [
f"⚡ <b>{escape(str(tier_label))}</b>",
f"📌 <b>{title}</b>",
f"<code>{event_type}</code>",
"━━━━━━━━━━━━━━━━━━━━",
"🧭 <b>決策狀態</b>",
]
if summary:
lines.append(f"{summary}")
for part in cause_parts[:3]:
lines.append(f"{part}")
if ai_summary:
lines += [
"",
"🧠 <b>背景摘要</b>",
f"{escape(_short_text(ai_summary, 280))}",
]
if parsed_actions:
lines += [
"",
"📊 <b>風險摘要</b>",
*_format_ea_risk_summary(parsed_actions),
"",
"📋 <b>TOP 待審 SKU</b>",
]
for idx, item in enumerate(shown_actions, start=1):
if idx > 1:
lines.append("")
lines.extend(_format_ea_action_card(item, idx))
if hidden_count:
lines.append(f"\n<i>另有 {hidden_count} 件,請至觀測台查看完整清單。</i>")
lines += [
"",
"✅ <b>建議處置</b>",
"• 先人工確認 PChome identity_v2 與規格一致",
"• 同款:評估跟價、組合促銷或加強 MOMO 價格優勢曝光",
"• 非同款:標記待審,避免進入自動調價或簡報決策",
]
else:
lines += [
"",
"✅ <b>建議處置</b>",
"• 先確認資料來源與最近錯誤紀錄",
"• 補齊可審核證據後再批准執行",
]
return "\n".join(lines)
def triaged_alert(base_event: Dict[str, Any], tier_label: str,
ai_summary: str, ai_cause: Optional[str] = None,
ai_actions: Optional[list] = None,
@@ -481,25 +659,35 @@ def triaged_alert(base_event: Dict[str, Any], tier_label: str,
safe_actions = [escape(str(a)) for a in (ai_actions or [])]
safe_executed = [escape(str(a)) for a in (ai_executed or [])]
lines = [
f"⚡ <b>{tier_label} · {event_type}</b>",
f"📌 {title}",
"",
]
if summary:
lines += [f"🔍 <b>概要:</b>{summary}", ""]
if safe_ai_summary:
lines += [f"🧠 <b>AI 摘要:</b>{safe_ai_summary[:400]}", ""]
if safe_ai_cause:
lines += [f"💡 <b>可能原因:</b>{safe_ai_cause}", ""]
if safe_actions:
lines += ["<b>📋 建議行動:</b>"] + [f"{a}" for a in safe_actions] + [""]
if safe_executed:
lines += ["<b>✅ 已執行:</b>"] + [f"{a}" for a in safe_executed] + [""]
if event_type == "ea_escalation":
message = _format_ea_escalation_alert(
base_event=base_event,
tier_label=tier_label,
ai_summary=str(ai_summary or ""),
ai_cause=ai_cause,
ai_actions=ai_actions,
)
else:
lines = [
f" <b>{tier_label} · {event_type}</b>",
f"📌 {title}",
"",
]
if summary:
lines += [f"🔍 <b>概要:</b>{summary}", ""]
if safe_ai_summary:
lines += [f"🧠 <b>AI 摘要:</b>{safe_ai_summary[:400]}", ""]
if safe_ai_cause:
lines += [f"💡 <b>可能原因:</b>{safe_ai_cause}", ""]
if safe_actions:
lines += ["<b>📋 建議行動:</b>"] + [f"{a}" for a in safe_actions] + [""]
if safe_executed:
lines += ["<b>✅ 已執行:</b>"] + [f"{a}" for a in safe_executed] + [""]
trace = base_event.get("trace")
if trace:
lines.append(f"<pre>{trace[-400:]}</pre>")
trace = base_event.get("trace")
if trace:
lines.append(f"<pre>{trace[-400:]}</pre>")
message = "\n".join(lines)
# ADR-012: eig=event_ignoreevent_id 截斷確保 ≤ 60 bytes留 buffer
_eid = str(event_id)[:52]
@@ -507,7 +695,7 @@ def triaged_alert(base_event: Dict[str, Any], tier_label: str,
[{"text": "🛑 忽略此事件",
"callback_data": f"momo:eig:{_eid}"}],
]}
return "\n".join(lines), keyboard
return message, keyboard
def insight_summary_msg(insights: List[Dict], period: str = "近24h") -> str: