From 3f6abe7ad66a0821ccfa9a044d9dde64b48bd838 Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Mon, 31 Aug 2026 12:27:35 +0800 Subject: [PATCH] =?UTF-8?q?v1.4.2:=20=E4=BF=AE=E5=A4=8D=E6=97=A5=E6=8A=A5/?= =?UTF-8?q?=E5=AE=9A=E5=88=B6=E6=B1=87=E6=80=BB=E9=87=8D=E5=A4=8D=E5=8F=91?= =?UTF-8?q?=E9=80=81=E5=88=B7=E5=B1=8F(=5Fmaybe=5Fdaily=E5=85=83=E7=BB=84?= =?UTF-8?q?=E5=B5=8C=E5=A5=97bug+=E5=8F=91=E9=80=81=E6=97=A5=E6=9C=9F?= =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96)=20+=20=E4=B8=80=E9=94=AE=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E5=90=AF=E7=94=A8/=E5=81=9C=E7=94=A8=20+=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=BA=90=E5=BF=AB=E9=80=9F=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +-- app.py | 6 +++++ db.py | 10 +++++++ scheduler.py | 24 ++++++++++------- templates/sources.html | 61 +++++++++++++++++++++++++++++++++++++++--- 5 files changed, 91 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 50bbf41..886491a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 一款面向 AI 领域的**自动采集 → 智能分析 → 邮件推送**的新闻跟踪系统。后台定时从数据源采集资讯,先规则打分、再 LLM 深度分析,把与你兴趣画像相关且重要的资讯**实时发邮件提醒**,并每天 10:00 发一份**AI 资讯日报**。带完整网页管理台,兴趣画像/数据源/通知参数全部可配。 -> 当前版本 **v1.4.1**:修复系统错误邮件被退信(补 Date 头)+ 数据源一键启动/停用开关(状态列直接点击)。 +> 当前版本 **v1.4.2**:修复日报/定制汇总重复发送刷屏 + 数据源一键全部启用/停用 + 数据源快速筛选。 --- @@ -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.4.1`**(修复邮件退信:发信补 `Date` 头,mail.tphai.com amavisd 强制要求否则 554 退信 / 数据源状态列新增一键启动/停用开关,点击即生效无需刷新) +- 版本:`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 退信 / 数据源状态列新增一键启动/停用开关,点击即生效无需刷新)→ **`v1.4.2`**(**修复日报/定制汇总刷屏**:`_maybe_daily` 日期判断元组嵌套 bug 导致每天只发一次的逻辑失效、到点后每 30s 重复发;改为纯字符串 + 发送日期持久化到 settings,重启也不重复 / **一键全部启用/停用**:数据源页顶部两个按钮 + `/api/sources` 新 action enable_all/disable_all / **快速筛选**:关键词搜索 + 监控方式 + 启用状态 三条件过滤,重置一键还原) diff --git a/app.py b/app.py index 842eb2c..d4faf7b 100644 --- a/app.py +++ b/app.py @@ -189,6 +189,12 @@ def api_sources(): s = db.get_source(data["id"]) db.update_source(data["id"], enabled=0 if s["enabled"] else 1) return jsonify({"ok": True}) + if action == "enable_all": + n = db.set_all_sources_enabled(1) + return jsonify({"ok": True, "count": n}) + if action == "disable_all": + n = db.set_all_sources_enabled(0) + return jsonify({"ok": True, "count": n}) if action == "test_standard": # 定制监控:用大模型测试「推送标准」是否好使(给一段示例内容看判不判得出) standard = (data.get("monitor_standard") or "").strip() diff --git a/db.py b/db.py index ac7af2f..97dedb2 100644 --- a/db.py +++ b/db.py @@ -290,6 +290,16 @@ def delete_source(sid): conn.close() +def set_all_sources_enabled(val): + """一键全部启用/停用。返回受影响的数据源数。""" + conn = get_conn() + cur = conn.execute("UPDATE sources SET enabled=?", (1 if val else 0,)) + conn.commit() + n = cur.rowcount + conn.close() + return n + + def update_source_fetch(sid, status="ok", count=0): conn = get_conn() conn.execute( diff --git a/scheduler.py b/scheduler.py index 453e265..dbd3bd9 100644 --- a/scheduler.py +++ b/scheduler.py @@ -147,11 +147,16 @@ def send_custom_summary(): return notifier.send_custom_summary(articles, label) -def _maybe_daily(now, last_day, time_key, send_fn, kind): - """到点触发的通用逻辑。time_key 存 'HH:MM',send_fn 返回发送条数。""" +def _maybe_daily(now, last_day, time_key, send_fn, kind, persist_key): + """到点触发的通用逻辑。time_key 存 'HH:MM',send_fn 返回发送条数。 + last_day 存上次发送的日期字符串 'YYYY-MM-DD'(None=尚未发送)。 + 修复:每天只发一次——last_day 必须是纯字符串,不能嵌套元组; + 并把已发送日期持久化到 settings,服务重启后也不会当天重复发。""" day_key = now.strftime("%Y-%m-%d") - if last_day[0] == day_key: - return last_day + if last_day is None: + last_day = db.get_setting(persist_key, "") + if last_day == day_key: + return last_day # 今天已发过,不再发 hm = str(db.get_setting(time_key, config.AUTO_DEFAULTS.get(time_key) or config.CUSTOM_DEFAULTS.get(time_key) or "10:00")) try: @@ -161,7 +166,8 @@ def _maybe_daily(now, last_day, time_key, send_fn, kind): if now.strftime("%H:%M") >= hm and now.hour >= hh: try: send_fn() - last_day = (day_key,) + db.set_setting(persist_key, day_key) + return day_key except Exception as e: db.add_log(kind, f"{time_key} 汇总异常", 0, [], status="error", detail=str(e)) notifier.report_error("汇总", f"{time_key} 汇总发送异常", str(e)[:300]) @@ -174,10 +180,10 @@ def scheduler_loop(stop_event): while not stop_event.is_set(): now = datetime.now() try: - last_summary_day = _maybe_daily(now, (last_summary_day,), "summary_time", - send_daily_summary, "summary") - last_custom_summary_day = _maybe_daily(now, (last_custom_summary_day,), "custom_summary_time", - send_custom_summary, "custom_summary") + last_summary_day = _maybe_daily(now, last_summary_day, "summary_time", + send_daily_summary, "summary", "summary_last_day") + last_custom_summary_day = _maybe_daily(now, last_custom_summary_day, "custom_summary_time", + send_custom_summary, "custom_summary", "custom_summary_last_day") except Exception as e: notifier.report_error("汇总", "定时汇总检查异常", str(e)[:300]) # 采集到期数据源(每源独立周期) diff --git a/templates/sources.html b/templates/sources.html index 7931466..bbe4929 100644 --- a/templates/sources.html +++ b/templates/sources.html @@ -4,15 +4,37 @@ {% block content %}

📡 数据源管理

- +
+ + + +
+
+ +
+
+ + + + +
- +
{% for s in sources %} - + @@ -304,11 +326,14 @@ async function toggleSource(id, cb){ const r = await API.json('/api/sources', {action:'toggle', id}); if (!r.ok){ toast('操作失败', false); cb.checked = orig; return; } const on = cb.checked; + const row = document.querySelector(`tr[data-sid="${id}"]`); + if (row) row.setAttribute('data-enabled', on ? '1' : '0'); 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); + applyFilter(); } function toggleFromButton(id){ const cb = document.querySelector(`tr[data-sid="${id}"] .switch input`); @@ -316,6 +341,36 @@ function toggleFromButton(id){ cb.checked = !cb.checked; toggleSource(id, cb); } + +// ---------- 快速筛选 ---------- +function applyFilter(){ + const kw = (document.getElementById('f_keyword').value || '').trim().toLowerCase(); + const kind = document.getElementById('f_kind').value; + const en = document.getElementById('f_enabled').value; + document.querySelectorAll('#srcTable tbody tr').forEach(tr => { + const text = ((tr.dataset.name||'') + ' ' + (tr.dataset.type||'') + ' ' + (tr.dataset.url||'') + ' ' + (tr.dataset.desc||'')).toLowerCase(); + const okKw = !kw || text.includes(kw); + const okKind = !kind || (tr.dataset.kind||'normal') === kind; + const okEn = en === '' || (tr.dataset.enabled||'1') === en; + tr.style.display = (okKw && okKind && okEn) ? '' : 'none'; + }); +} +function clearFilter(){ + document.getElementById('f_keyword').value = ''; + document.getElementById('f_kind').value = ''; + document.getElementById('f_enabled').value = ''; + applyFilter(); +} + +// ---------- 一键全部启用 / 全部停用 ---------- +async function setAllSources(val){ + const action = val ? 'enable_all' : 'disable_all'; + if (!confirm(val ? '确定要【全部启用】所有数据源吗?' : '确定要【全部停用】所有数据源吗?')) return; + const r = await API.json('/api/sources', {action}); + if (!r.ok){ toast('操作失败', false); return; } + toast((val ? '✅ 已全部启用 ' : '⏸ 已全部停用 ') + (r.count||0) + ' 个数据源', true); + setTimeout(()=>location.reload(), 400); +} async function delSource(id){ if (!confirm('确认删除该数据源?')) return; await API.json('/api/sources', {action:'delete', id}); toast('✅ 已删除'); setTimeout(()=>location.reload(), 400);
ID名称监控方式获取方式类型权重采集周期状态最近采样条数操作
{{ s.id }} {{ s.name }}
{{ s.description }}
{{ s.url }}
{% if s.kind == 'custom' %}🎯 定制监控{% else %}普通{% endif %}