#!/usr/bin/env python3 """alert_helper.py — MoFin 统一告警网关(信噪比控制) 原则(老爸 2026-07-21 定): - 真正有意义的信息(重点推荐操作/买入信号/需人工处理)绝不能被淹没 - 纯通知型信息(系统报备/日常状态)必须控制频率和篇幅 两级通道: - ACTION(行动级):买入信号、重点推荐、需人工核查的故障 → 直通,不限速,🚨 醒目前缀,独立成条 - INFO(通知级):部署报备、卫生审计、修复报备、螺旋嫌疑 → 同类 30 分钟内最多 1 条;篇幅 ≤8 行;24h 内容去重(同一问题不重复报) 所有系统消息统一 📟【MoFin系统·类别】前缀,与知微本人消息一眼区分。 用法: from alert_helper import notify, ACTION, INFO notify("信号", "📈 300308 买入信号...", level=ACTION) # 直通 notify("部署守卫", "自动部署完成...", level=INFO) # 限速 """ import json, os, time, hashlib from datetime import datetime ACTION = "action" INFO = "info" STATE_FILE = "/home/hmo/MoFin/gateway/logs/alert_state.json" LOG = "/home/hmo/MoFin/gateway/logs/alert_helper.log" INFO_MIN_INTERVAL = 1800 # 同类 info 30 分钟最多 1 条 INFO_MAX_LINES = 8 # info 篇幅上限 ACTION_MAX_LINES = 30 # action 篇幅上限(宽松但不失控) ACTION_DEDUP_SEC = 300 # action 相同内容 5 分钟内不重复发(防同秒双发/竞态) DEDUP_SEC = 24 * 3600 # 相同内容 24h 不重复 def _log(msg): line = f"[{datetime.now().isoformat(timespec='seconds')}] {msg}" print(line, flush=True) try: os.makedirs(os.path.dirname(LOG), exist_ok=True) with open(LOG, "a", encoding="utf-8") as f: f.write(line + "\n") except Exception: pass def _load_state(): try: with open(STATE_FILE, encoding="utf-8") as f: return json.load(f) except Exception: return {} def _save_state(st): try: # 只保留最近 100 个类别条目 if len(st) > 100: st = dict(sorted(st.items(), key=lambda kv: kv[1].get("last_ts", 0))[-100:]) with open(STATE_FILE, "w", encoding="utf-8") as f: json.dump(st, f) except Exception: pass def _send(body): import urllib.request req = urllib.request.Request( "http://127.0.0.1:5805/", data=json.dumps({"body": body, "to": "hmo@yoin.fun", "type": "chat"}).encode(), headers={"Content-Type": "application/json"}) urllib.request.urlopen(req, timeout=5) def notify(category, body, level=INFO): """统一告警入口。返回 True=已发送, False=被限速/去重静默。""" now = time.time() st = _load_state() entry = st.get(category, {"last_ts": 0, "last_hash": "", "suppressed": 0}) body_hash = hashlib.md5(body.encode()).hexdigest() if level == INFO: # 24h 内容去重:同一问题不重复报 if body_hash == entry.get("last_hash") and now - entry.get("last_ts", 0) < DEDUP_SEC: _log(f"[{category}] 内容重复(24h内已报),静默") return False # 频率限制:同类 30min 最多 1 条 if now - entry.get("last_ts", 0) < INFO_MIN_INTERVAL: entry["suppressed"] = entry.get("suppressed", 0) + 1 st[category] = entry _save_state(st) _log(f"[{category}] 30min 频率限制,静默(累计压制{entry['suppressed']}条)") return False # 篇幅截断 lines = body.splitlines() if len(lines) > INFO_MAX_LINES: body = "\n".join(lines[:INFO_MAX_LINES]) + \ f"\n…(共{len(lines)}行,详见 gateway/logs/)" # 告知压制历史 if entry.get("suppressed", 0) > 0: body += f"\n(注: 上次以来另有 {entry['suppressed']} 条同类通知已按频率策略静默)" entry["suppressed"] = 0 prefix = f"📟【MoFin系统·{category}】(非知微本人)" else: # ACTION: 不限速,但相同内容 5 分钟内去重(防竞态双发) if body_hash == entry.get("last_hash") and now - entry.get("last_ts", 0) < ACTION_DEDUP_SEC: _log(f"[{category}] ACTION 内容重复(5min内),静默") return False lines = body.splitlines() if len(lines) > ACTION_MAX_LINES: body = "\n".join(lines[:ACTION_MAX_LINES]) + f"\n…(共{len(lines)}行)" prefix = f"🚨【MoFin·{category}】" # 写入 broadcast_messages(统一消息源,告警类) try: from messenger import _classify, DB import sqlite3 as _sq3 from datetime import datetime as _dt _cat = "system_error" if level == ACTION else _classify(category, body) _conn = _sq3.connect(DB, timeout=30) _conn.execute( "INSERT INTO broadcast_messages (ts, category, title, content, source) VALUES (?,?,?,?,?)", (_dt.now().isoformat(), _cat, f"MoFin·{category}", body, category)) _conn.commit() _conn.close() except Exception: pass try: _send(f"{prefix}\n{body}") except Exception as e: _log(f"[{category}] XMPP发送失败: {e}") return False entry["last_ts"] = now entry["last_hash"] = body_hash st[category] = entry _save_state(st) _log(f"[{category}] 已发送({level})") return True