diff --git a/README.md b/README.md index 34bc06e..50bbf41 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 一款面向 AI 领域的**自动采集 → 智能分析 → 邮件推送**的新闻跟踪系统。后台定时从数据源采集资讯,先规则打分、再 LLM 深度分析,把与你兴趣画像相关且重要的资讯**实时发邮件提醒**,并每天 10:00 发一份**AI 资讯日报**。带完整网页管理台,兴趣画像/数据源/通知参数全部可配。 -> 当前版本 **v1.3.0**:数据源**独立采集周期**(每源可覆盖全局)+ **定制监控与新闻监控完全分离**(独立间隔/独立汇总)+ **历史采样留档与提取**。 +> 当前版本 **v1.4.1**:修复系统错误邮件被退信(补 Date 头)+ 数据源一键启动/停用开关(状态列直接点击)。 --- @@ -236,4 +236,4 @@ cd works/news-tracker ## Git - 仓库:`hz4th_coder/news-tracker` -- 版本:`v1.0.0`(初版)→ `v1.0.1`(补 README)→ `v1.1.0`(真实网页采集+全文入库 / 大模型接口多预置一键切换 / 数据源可编辑)→ `v1.2.0`(数据源完整编辑模态框 / 定制监控类型:无权重+推送标准+LLM判断推送+测试按钮)→ **`v1.3.0`**(每源独立采集周期 / 定制监控与新闻监控分离独立配置+独立汇总 / 历史采样留档+查看+提取API)→ **`v1.4.0`**(通知日志分页+筛选 / 仪表盘 TOP10+更多链接 / 数据源走 web-capture-api 抓取+获取方式与参数可编辑 / 系统错误邮件通知:频率+静默时段) +- 版本:`v1.0.0`(初版)→ `v1.0.1`(补 README)→ `v1.1.0`(真实网页采集+全文入库 / 大模型接口多预置一键切换 / 数据源可编辑)→ `v1.2.0`(数据源完整编辑模态框 / 定制监控类型:无权重+推送标准+LLM判断推送+测试按钮)→ **`v1.3.0`**(每源独立采集周期 / 定制监控与新闻监控分离独立配置+独立汇总 / 历史采样留档+查看+提取API)→ **`v1.4.0`**(通知日志分页+筛选 / 仪表盘 TOP10+更多链接 / 数据源走 web-capture-api 抓取+获取方式与参数可编辑 / 系统错误邮件通知:频率+静默时段)→ **`v1.4.1`**(修复邮件退信:发信补 `Date` 头,mail.tphai.com amavisd 强制要求否则 554 退信 / 数据源状态列新增一键启动/停用开关,点击即生效无需刷新) diff --git a/notifier.py b/notifier.py index 05e4b5f..de7c766 100644 --- a/notifier.py +++ b/notifier.py @@ -8,7 +8,7 @@ from datetime import datetime from email.header import Header from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -from email.utils import formataddr +from email.utils import formataddr, formatdate import config import db @@ -27,6 +27,8 @@ def send_email(subject, html, to=None): msg["From"] = formataddr((str(Header(cfg.get("sender_name", "新闻智能跟踪"), "utf-8")), cfg["smtp_user"])) msg["To"] = to msg["Subject"] = Header(subject, "utf-8") + # mail.tphai.com 的 amavisd 强制要求 Date 头,缺失会退信(554 5.6.0 BAD HEADER) + msg["Date"] = formatdate(localtime=True) msg.attach(MIMEText(html, "html", "utf-8")) mode = cfg.get("smtp_mode", "plain") diff --git a/static/style.css b/static/style.css index 985e7d4..0cecebe 100644 --- a/static/style.css +++ b/static/style.css @@ -68,6 +68,14 @@ a { color:#2563eb; text-decoration:none; } .tag.ok { background:#ecfdf5; color:#059669; } .tag.err { background:#fef2f2; color:#dc2626; } +/* 一键启停开关 */ +.switch { position:relative; display:inline-block; width:38px; height:20px; vertical-align:middle; cursor:pointer; } +.switch input { opacity:0; width:0; height:0; } +.switch .slider { position:absolute; inset:0; background:#d1d5db; border-radius:20px; transition:.2s; } +.switch .slider:before { content:""; position:absolute; height:14px; width:14px; left:3px; top:3px; background:#fff; border-radius:50%; transition:.2s; } +.switch input:checked + .slider { background:#059669; } +.switch input:checked + .slider:before { transform:translateX(18px); } + .filters { display:flex; gap:10px; margin-bottom:16px; flex-wrap:wrap; align-items:center; } .filters select, .filters input { padding:8px 10px; border:1px solid #d1d5db; border-radius:8px; background:#fff; font-size:13px; } .filters input[type=text]{ width:260px; } diff --git a/templates/sources.html b/templates/sources.html index 5dc7d68..7931466 100644 --- a/templates/sources.html +++ b/templates/sources.html @@ -20,13 +20,19 @@ {{ s.type }} {% if s.kind == 'custom' %}{% else %}{{ s.weight }}{% endif %} {% if s.scan_interval_min %}{{ s.scan_interval_min }}分{% else %}默认{% endif %} - {% if s.enabled %}启用{% else %}停用{% endif %} + + + {{ '启用' if s.enabled else '停用' }} + {{ s.last_fetch or '—' }} {{ s.last_count }} - + @@ -291,7 +297,25 @@ async function openHistArticles(){ ).join('') + ''; } -async function toggleSource(id){ await API.json('/api/sources', {action:'toggle', id}); location.reload(); } +async function toggleSource(id, cb){ + cb = cb || document.querySelector(`tr[data-sid="${id}"] .switch input`); + if (!cb) return; + const orig = cb.checked; + const r = await API.json('/api/sources', {action:'toggle', id}); + if (!r.ok){ toast('操作失败', false); cb.checked = orig; return; } + const on = cb.checked; + const tag = document.getElementById('st-'+id); + if (tag){ tag.textContent = on ? '启用' : '停用'; tag.className = 'tag ' + (on ? 'ok' : ''); } + const nameEl = document.querySelector(`tr[data-sid="${id}"] b`); + const name = nameEl ? nameEl.textContent : ''; + toast((on ? '✅ 已启用「' : '⏸ 已停用「') + name + '」', true); +} +function toggleFromButton(id){ + const cb = document.querySelector(`tr[data-sid="${id}"] .switch input`); + if (!cb) return; + cb.checked = !cb.checked; + toggleSource(id, cb); +} async function delSource(id){ if (!confirm('确认删除该数据源?')) return; await API.json('/api/sources', {action:'delete', id}); toast('✅ 已删除'); setTimeout(()=>location.reload(), 400);