v1.4.0: 通知日志分页+筛选 / 仪表盘TOP10+更多链接 / 数据源走web-capture-api抓取(获取方式与参数可编辑) / 系统错误邮件通知(频率+静默时段)
This commit is contained in:
+125
@@ -4,6 +4,7 @@
|
||||
支持 plain / starttls / ssl 三种 SMTP 模式,发送实时重要资讯与每日汇总。
|
||||
"""
|
||||
import smtplib
|
||||
from datetime import datetime
|
||||
from email.header import Header
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
@@ -43,6 +44,130 @@ def send_email(subject, html, to=None):
|
||||
return True
|
||||
|
||||
|
||||
# ============ 系统错误邮件通知(频率 + 静默时段) ============
|
||||
|
||||
def _esc(s):
|
||||
return str(s or "").replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
||||
|
||||
|
||||
def _errnotify_cfg():
|
||||
cfg = dict(config.ERRNOTIFY_DEFAULTS)
|
||||
cfg.update(db.get_all_settings().get("errnotify", {}))
|
||||
return cfg
|
||||
|
||||
|
||||
def _in_quiet_period(now=None):
|
||||
"""是否处于静默时段。支持多段、跨午夜(如 23:00-07:30)。"""
|
||||
cfg = _errnotify_cfg()
|
||||
if not cfg.get("quiet_enabled"):
|
||||
return False
|
||||
now = now or datetime.now()
|
||||
cur_min = now.hour * 60 + now.minute
|
||||
for p in cfg.get("quiet_periods") or []:
|
||||
p = (p or "").strip()
|
||||
if not p or "-" not in p:
|
||||
continue
|
||||
try:
|
||||
s, e = p.split("-")
|
||||
sh, sm = map(int, s.strip().split(":"))
|
||||
eh, em = map(int, e.strip().split(":"))
|
||||
except Exception:
|
||||
continue
|
||||
s_min, e_min = sh * 60 + sm, eh * 60 + em
|
||||
if s_min <= e_min:
|
||||
if s_min <= cur_min < e_min:
|
||||
return True
|
||||
else: # 跨午夜
|
||||
if cur_min >= s_min or cur_min < e_min:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _send_error_digest(errors):
|
||||
"""发送一封错误通知邮件(受调用方冷却/静默控制),并记录通知日志"""
|
||||
rows = "".join(
|
||||
f"<div style='border:1px solid #fecaca;border-left:4px solid #dc2626;border-radius:6px;"
|
||||
f"padding:10px 14px;margin:8px 0;'>"
|
||||
f"<div style='font-weight:bold;color:#b91c1c;'>{_esc(e.get('source', ''))} · 发生 {e.get('count', 1)} 次</div>"
|
||||
f"<div style='font-size:13px;color:#374151;margin-top:2px;'>{_esc(e.get('message', ''))}</div>"
|
||||
f"<div style='font-size:12px;color:#6b7280;'>{_esc(e.get('detail', ''))}</div>"
|
||||
f"<div style='font-size:11px;color:#9ca3af;'>最近: {_esc(e.get('last_seen', ''))} · "
|
||||
f"首次: {_esc(e.get('first_seen', ''))}</div></div>"
|
||||
for e in errors
|
||||
)
|
||||
subject = f"⚠️ 系统异常通知 · {len(errors)} 类错误"
|
||||
html = _html_wrap(
|
||||
"系统运行异常提醒",
|
||||
f"<p style='color:#374151;font-size:13px;'>新闻智能跟踪系统检测到以下异常,请及时处理:</p>{rows}",
|
||||
)
|
||||
send_email(subject, html)
|
||||
db.add_log("error", subject, len(errors), [], status="ok", detail="系统错误邮件通知")
|
||||
|
||||
|
||||
def _try_send_pending():
|
||||
"""按通知策略发送待通知错误(受静默时段 + 冷却频率控制)。返回发送条数。"""
|
||||
cfg = _errnotify_cfg()
|
||||
if not cfg.get("enabled") or cfg.get("mode") == "off":
|
||||
return 0
|
||||
if _in_quiet_period():
|
||||
return 0 # 静默时段不发,等待调度器在静默结束后 flush
|
||||
# 冷却频率:两次错误邮件最小间隔
|
||||
last_sent = db.get_err_last_send()
|
||||
if last_sent:
|
||||
try:
|
||||
lt = datetime.strptime(last_sent, "%Y-%m-%d %H:%M:%S")
|
||||
cooldown = max(0, int(cfg.get("cooldown_min", 60) or 0))
|
||||
if cooldown > 0 and (datetime.now() - lt).total_seconds() < cooldown * 60:
|
||||
return 0 # 冷却中
|
||||
except Exception:
|
||||
pass
|
||||
errors = db.pending_system_errors(limit=int(cfg.get("max_items", 10) or 10))
|
||||
if not errors:
|
||||
return 0
|
||||
try:
|
||||
_send_error_digest(errors)
|
||||
except Exception:
|
||||
# 邮件发送失败:保留 pending,下次重试
|
||||
return 0
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
db.mark_errors_notified([e["id"] for e in errors], now)
|
||||
db.set_err_last_send(now)
|
||||
return len(errors)
|
||||
|
||||
|
||||
def report_error(source, message, detail=""):
|
||||
"""记录一条系统错误;immediate 模式下尝试立即发送(受冷却/静默控制)。"""
|
||||
try:
|
||||
db.add_system_error(source, message, detail)
|
||||
except Exception:
|
||||
return None
|
||||
cfg = _errnotify_cfg()
|
||||
if not cfg.get("enabled") or cfg.get("mode") not in ("immediate", "cooldown"):
|
||||
return None
|
||||
if cfg.get("mode") == "immediate":
|
||||
try:
|
||||
_try_send_pending()
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
def flush_pending_errors():
|
||||
"""后台调度器定时调用:把待通知错误按策略发送(冷却 + 静默控制)"""
|
||||
return _try_send_pending()
|
||||
|
||||
|
||||
def test_error_notify():
|
||||
"""发送一封测试错误邮件(无视冷却/静默,用于设置页测试按钮)"""
|
||||
_send_error_digest([{
|
||||
"source": "测试", "message": "这是一封测试错误通知邮件",
|
||||
"detail": "如果你收到了这封邮件,说明系统错误邮件通知链路正常。",
|
||||
"count": 1, "first_seen": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"last_seen": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}])
|
||||
return True
|
||||
|
||||
|
||||
def _score_color(score):
|
||||
if score >= 80:
|
||||
return "#e74c3c"
|
||||
|
||||
Reference in New Issue
Block a user