新闻智能跟踪系统 v1.0.0: AI资讯自动采集+智能分析+邮件通知+网页管理台
This commit is contained in:
+131
@@ -0,0 +1,131 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
新闻智能跟踪系统 - 邮件通知
|
||||
支持 plain / starttls / ssl 三种 SMTP 模式,发送实时重要资讯与每日汇总。
|
||||
"""
|
||||
import smtplib
|
||||
from email.header import Header
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formataddr
|
||||
|
||||
import config
|
||||
import db
|
||||
|
||||
|
||||
def get_mail_cfg():
|
||||
cfg = dict(config.MAIL_DEFAULTS)
|
||||
cfg.update(db.get_all_settings().get("mail", {}))
|
||||
return cfg
|
||||
|
||||
|
||||
def send_email(subject, html, to=None):
|
||||
cfg = get_mail_cfg()
|
||||
to = to or cfg["email_to"]
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["From"] = formataddr((str(Header(cfg.get("sender_name", "新闻智能跟踪"), "utf-8")), cfg["smtp_user"]))
|
||||
msg["To"] = to
|
||||
msg["Subject"] = Header(subject, "utf-8")
|
||||
msg.attach(MIMEText(html, "html", "utf-8"))
|
||||
|
||||
mode = cfg.get("smtp_mode", "plain")
|
||||
if mode == "ssl":
|
||||
server = smtplib.SMTP_SSL(cfg["smtp_host"], int(cfg["smtp_port"]), timeout=20)
|
||||
else:
|
||||
server = smtplib.SMTP(cfg["smtp_host"], int(cfg["smtp_port"]), timeout=20)
|
||||
if mode == "starttls":
|
||||
server.starttls()
|
||||
try:
|
||||
server.login(cfg["smtp_user"], cfg["smtp_pass"])
|
||||
server.sendmail(cfg["smtp_user"], [to], msg.as_string())
|
||||
finally:
|
||||
server.quit()
|
||||
return True
|
||||
|
||||
|
||||
def _score_color(score):
|
||||
if score >= 80:
|
||||
return "#e74c3c"
|
||||
if score >= 60:
|
||||
return "#e67e22"
|
||||
return "#7f8c8d"
|
||||
|
||||
|
||||
def _card(art):
|
||||
ents = "、".join(art.get("entities") or [])
|
||||
return f"""
|
||||
<div style="border:1px solid #e5e7eb;border-left:4px solid {_score_color(art.get('total_score',0))};
|
||||
border-radius:6px;padding:10px 14px;margin:8px 0;">
|
||||
<div style="font-size:14px;font-weight:bold;color:#1f2937;">
|
||||
<a href="{art.get('url','#')}" style="color:#1f2937;text-decoration:none;">{art['title']}</a>
|
||||
</div>
|
||||
<div style="font-size:12px;color:#6b7280;margin:4px 0;">
|
||||
综合分 {art.get('total_score',0)} · 相关度 {art.get('relevance',0)} ·
|
||||
{art.get('source_name','')} · {art.get('published_at','')}
|
||||
</div>
|
||||
<div style="font-size:13px;color:#374151;">{art.get('summary') or art.get('content','')[:120]}</div>
|
||||
<div style="font-size:12px;color:#9ca3af;">涉及:{ents if ents else '—'}</div>
|
||||
{('<div style="font-size:12px;color:#b45309;background:#fef3c7;padding:4px 8px;border-radius:4px;margin-top:4px;">💡 ' + art.get('analysis','') + '</div>') if art.get('analysis') else ''}
|
||||
</div>"""
|
||||
|
||||
|
||||
def _html_wrap(title, body):
|
||||
return f"""<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"></head>
|
||||
<body style="font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif;background:#f9fafb;padding:20px;">
|
||||
<div style="max-width:720px;margin:0 auto;background:#ffffff;border-radius:10px;padding:24px;box-shadow:0 1px 3px rgba(0,0,0,.08);">
|
||||
<h2 style="margin:0 0 4px;color:#111827;">{title}</h2>
|
||||
{body}
|
||||
<div style="margin-top:24px;padding-top:12px;border-top:1px solid #e5e7eb;font-size:12px;color:#9ca3af;">
|
||||
由 新闻智能跟踪系统 自动生成 · {config.SERVICE_NAME}
|
||||
</div>
|
||||
</div>
|
||||
</body></html>"""
|
||||
|
||||
|
||||
def send_realtime(articles):
|
||||
"""实时重要资讯通知(一次一批)"""
|
||||
if not articles:
|
||||
return 0
|
||||
cards = "".join(_card(a) for a in articles)
|
||||
subject = f"🔥 重要AI资讯 {len(articles)}条 · {articles[0]['published_at'][:16]}"
|
||||
html = _html_wrap(
|
||||
"重要资讯实时提醒",
|
||||
f"<p style='color:#374151;font-size:13px;'>以下 {len(articles)} 条资讯对您重点关注领域很重要,已自动甄别:</p>{cards}",
|
||||
)
|
||||
send_email(subject, html)
|
||||
ids = [a["id"] for a in articles]
|
||||
for aid in ids:
|
||||
db.update_article(aid, notified=1)
|
||||
db.add_log("realtime", subject, len(articles), ids, status="ok", detail="实时通知")
|
||||
return len(articles)
|
||||
|
||||
|
||||
def send_daily_summary(articles, window_label):
|
||||
"""每日汇总(默认每天10点):昨天至今的重要/相关资讯"""
|
||||
if not articles:
|
||||
return 0
|
||||
top = articles[: int(db.get_setting("max_summary_items", config.AUTO_DEFAULTS["max_summary_items"]))]
|
||||
cards = "".join(_card(a) for a in top)
|
||||
# 分领域统计
|
||||
from collections import Counter
|
||||
dom_cnt = Counter(a.get("domain") or "未分类" for a in articles)
|
||||
stats = " · ".join(f"{k} {v}条" for k, v in dom_cnt.most_common(6))
|
||||
subject = f"📰 AI资讯日报 {window_label} · 共{len(articles)}条 重点{len(top)}条"
|
||||
html = _html_wrap(
|
||||
"AI 重要资讯日报",
|
||||
f"""
|
||||
<p style="color:#374151;font-size:13px;">汇总时段:{window_label}</p>
|
||||
<p style="color:#6b7280;font-size:12px;">领域分布:{stats}</p>
|
||||
<p style="color:#374151;font-size:13px;margin-top:16px;">重点资讯(按综合分排序):</p>
|
||||
{cards}
|
||||
""",
|
||||
)
|
||||
send_email(subject, html)
|
||||
ids = [a["id"] for a in articles if a["id"]]
|
||||
db.add_log("summary", subject, len(articles), ids, status="ok", detail=f"汇总{len(top)}条")
|
||||
for aid in ids:
|
||||
art = db.get_article(aid)
|
||||
if art and art.get("status") != "summarized":
|
||||
db.update_article(aid, status="summarized")
|
||||
return len(top)
|
||||
Reference in New Issue
Block a user