af229e2a7b
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
116 lines
4.5 KiB
Python
116 lines
4.5 KiB
Python
"""AlertHit[] → 飞书群机器人消息。
|
||
|
||
提供两个格式化函数:
|
||
- format_alert_message: 纯文本(保留,已有集成测试依赖)。
|
||
- format_alert_post: 富文本 post(行式明细:时间|手机|版本|原因|trace 超链接)。
|
||
|
||
按触发类型分组,每类给计数 + 明细(trace/版本/原因)。两级截断防报警风暴:单类型超
|
||
max_detail_per_type 只列前 N + 「另有 M 条」;本期总量超 max_total 只给各类型计数、提示去分析库查。
|
||
标题含关键词「比价失败报警」——飞书自定义机器人用关键词验证,消息必须含它,否则被拒收。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from app.services.compare_alert import ALERT_TYPE_LABELS, AlertHit
|
||
|
||
ALERT_KEYWORD = "比价失败报警"
|
||
|
||
_TYPE_ORDER = ("T1", "T6", "T2", "T5")
|
||
|
||
|
||
def _detail_line(h: AlertHit) -> str:
|
||
ver = h.app_version or "?"
|
||
return f" - trace {h.trace_id} | {ver} | {h.reason}"
|
||
|
||
|
||
def format_alert_message(
|
||
hits: list[AlertHit],
|
||
*,
|
||
window_label: str,
|
||
max_detail_per_type: int,
|
||
max_total: int,
|
||
) -> str:
|
||
total = len(hits)
|
||
header = f"🚨 {ALERT_KEYWORD} · {window_label} · 本期触发 {total} 条"
|
||
|
||
grouped: dict[str, list[AlertHit]] = {t: [] for t in _TYPE_ORDER}
|
||
for h in hits:
|
||
grouped.setdefault(h.alert_type, []).append(h)
|
||
|
||
lines = [header]
|
||
counts_only = total > max_total
|
||
for t in _TYPE_ORDER:
|
||
bucket = grouped.get(t) or []
|
||
if not bucket:
|
||
continue
|
||
lines.append(f"• {ALERT_TYPE_LABELS[t]} {len(bucket)} 条")
|
||
if counts_only:
|
||
continue
|
||
shown = bucket[:max_detail_per_type]
|
||
lines.extend(_detail_line(h) for h in shown)
|
||
if len(bucket) > max_detail_per_type:
|
||
lines.append(f" …另有 {len(bucket) - max_detail_per_type} 条")
|
||
if counts_only:
|
||
lines.append(f"(本期命中超 {max_total} 条,仅列计数,明细见分析库 comparison_record)")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def format_alert_post(
|
||
hits: list[AlertHit],
|
||
*,
|
||
window_label: str,
|
||
phone_map: dict[int, str],
|
||
max_detail_per_type: int,
|
||
max_total: int,
|
||
) -> tuple[str, list]:
|
||
"""行式富文本:返回 (title, content)。title 含 ALERT_KEYWORD(飞书关键词验证)。
|
||
|
||
content: 摘要段(各类型计数) + 表头段 + 明细行(每条「时间|手机|版本|原因|」+ trace 超链接 a 元素)。
|
||
phone_map: {user_id: phone};明细手机号取 phone_map.get(hit.user_id) or "-"。
|
||
截断规则:总命中 > max_total → 只出摘要+各类型计数(不列明细);
|
||
否则明细最多列前 max_detail_per_type 条,超出加「…另有 N 条」。
|
||
"""
|
||
total = len(hits)
|
||
title = f"🚨 {ALERT_KEYWORD} · {window_label}"
|
||
|
||
grouped: dict[str, list[AlertHit]] = {t: [] for t in _TYPE_ORDER}
|
||
for h in hits:
|
||
grouped.setdefault(h.alert_type, []).append(h)
|
||
|
||
# 摘要段:各类型计数
|
||
summary_parts = [f"{ALERT_TYPE_LABELS[t]} {len(grouped[t])}" for t in _TYPE_ORDER if grouped.get(t)]
|
||
summary_text = f"合计 {total} 条:" + " | ".join(summary_parts)
|
||
content: list[list[dict]] = [
|
||
[{"tag": "text", "text": summary_text}],
|
||
]
|
||
|
||
counts_only = total > max_total
|
||
if counts_only:
|
||
content.append([{"tag": "text", "text": f"(本期命中超 {max_total} 条,仅列计数,明细见分析库 comparison_record)"}])
|
||
return title, content
|
||
|
||
# 表头段
|
||
content.append([{"tag": "text", "text": "时间 | 手机号 | 版本 | 失败原因 | trace"}])
|
||
|
||
# 明细行(按类型顺序展开,每条一段)
|
||
shown_count = 0
|
||
for t in _TYPE_ORDER:
|
||
bucket = grouped.get(t) or []
|
||
if not bucket:
|
||
continue
|
||
for h in bucket[:max_detail_per_type]:
|
||
time_str = h.created_at.strftime("%m-%d %H:%M") if h.created_at else "-"
|
||
phone = phone_map.get(h.user_id) if h.user_id is not None else None
|
||
phone = phone or "-"
|
||
ver = h.app_version or "-"
|
||
row: list[dict] = [{"tag": "text", "text": f"{time_str} | {phone} | {ver} | {h.reason} | "}]
|
||
if h.trace_url:
|
||
row.append({"tag": "a", "text": "trace", "href": h.trace_url})
|
||
else:
|
||
row.append({"tag": "text", "text": h.trace_id[:16]})
|
||
content.append(row)
|
||
shown_count += 1
|
||
if len(bucket) > max_detail_per_type:
|
||
content.append([{"tag": "text", "text": f"…另有 {len(bucket) - max_detail_per_type} 条"}])
|
||
|
||
return title, content
|