Files
2026-08-26 12:34:39 +08:00

140 lines
5.1 KiB
Python

#!/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()
body = "\n".join(lines)
# 告知压制历史
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()
body = "\n".join(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