From 2cad7a29763f0334626040e4d8e17d451687dd94 Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Sat, 29 Aug 2026 19:04:32 +0800 Subject: [PATCH] =?UTF-8?q?v1.2.0:=20=E6=95=B0=E6=8D=AE=E6=BA=90=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E7=BC=96=E8=BE=91(=E6=A8=A1=E6=80=81=E6=A1=86?= =?UTF-8?q?=E6=89=80=E6=9C=89=E9=85=8D=E7=BD=AE=E5=8F=AF=E6=94=B9)=20+=20?= =?UTF-8?q?=E5=AE=9A=E5=88=B6=E7=9B=91=E6=8E=A7=E7=B1=BB=E5=9E=8B(?= =?UTF-8?q?=E6=97=A0=E6=9D=83=E9=87=8D,=E6=8E=A8=E9=80=81=E6=A0=87?= =?UTF-8?q?=E5=87=86=E7=94=B1=E5=A4=A7=E6=A8=A1=E5=9E=8B=E5=88=A4=E6=96=AD?= =?UTF-8?q?=E6=8E=A8=E9=80=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 15 ++-- analysis.py | 61 +++++++++++++++- app.py | 33 ++++++++- config.py | 4 ++ db.py | 23 ++++-- notifier.py | 6 +- static/style.css | 14 ++++ templates/sources.html | 157 ++++++++++++++++++++++++++++++----------- 8 files changed, 255 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 7c2a938..3aca103 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 一款面向 AI 领域的**自动采集 → 智能分析 → 邮件推送**的新闻跟踪系统。后台定时从数据源采集资讯,先规则打分、再 LLM 深度分析,把与你兴趣画像相关且重要的资讯**实时发邮件提醒**,并每天 10:00 发一份**AI 资讯日报**。带完整网页管理台,兴趣画像/数据源/通知参数全部可配。 -> 当前版本 **v1.1.0**:真实网页采集(可读正文入库)+ 大模型接口多预置一键切换 + 数据源可编辑。 +> 当前版本 **v1.2.0**:数据源**完整编辑**(所有配置可改,模态框)+ **定制监控类型**(无权重,按「推送标准」由大模型判断是否推送)。 --- @@ -39,9 +39,10 @@ - 🧠 **两级智能分析**:规则打分秒级响应 + LLM 深度分析(重要度/相关度/分类/结论) - 🤖 **大模型接口可配置**:设置页可增删改/测试大模型接口,预置 SiliconFlow(默认)/ DeepSeek 官方 / Autodl / Local Qwen 四个,**一键切换即时生效**,激活接口失败自动切换下一个可用接口 - 🔥 **实时重要资讯邮件**:综合分达到阈值自动推送,单批最多 10 条 +- 🎯 **定制监控数据源**:数据源可选「定制监控」方式——**不设权重**,填写「推送标准」后由大模型逐条判断是否达到标准,达到即实时邮件推送(页面可直接「🧪 测试推送标准」);命中推送的资讯在邮件/列表中带「🎯 定制监控命中」标识 - 📰 **每日 AI 资讯日报**:默认每天 10:00,按领域统计 + 重点资讯卡片 - 🎯 **兴趣画像管理**:关键词(带权重)、领域、关注公司,全部可在网页维护,改完一键「重新打分」 -- 🔗 **数据源管理**:7 个内置数据源模板,**可编辑**(名称/类型/URL/权重/描述)、启停、删除,每源带权重 +- 🔗 **数据源管理**:7 个内置数据源模板,**完整编辑**(模态框一次性改名称/类型/URL/权重/描述/监控方式/推送标准)、启停、删除;普通源带权重,定制监控源填「推送标准」 - 📊 **网页管理台**:仪表盘(总数/重要/待分析/趋势/领域分布)、资讯列表(筛选/搜索/分页)、详情页(含全文)、通知日志 - 📧 **邮件通知**:支持 plain / starttls / ssl 三种 SMTP 模式,可测试发送 - 🛠 **手动操作台**:一键采集 / 一键 LLM 分析 / 一键发日报 / 一键造数据 / 重新打分 / 测试邮件 @@ -144,7 +145,7 @@ cd works/news-tracker | `/dashboard` | 仪表盘:总资讯/重要资讯/待分析/已通知 + 近7天趋势 + 领域分布 | | `/news` | 资讯列表:按领域/重要/关键词搜索,分页 | | `/news/` | 资讯详情:规则分/LLM分/实体/分析结论 + **页面可读全文** | -| `/sources` | 数据源管理:**编辑**/启停/删除/权重,真实源状态可见 | +| `/sources` | 数据源管理:**完整编辑**(模态框)/启停/删除,支持普通(权重)与定制监控(推送标准),真实源状态可见 | | `/profile` | 兴趣画像:关键词/领域/公司维护 | | `/logs` | 通知日志(实时/日报发送记录) | | `/settings` | 设置:自动化参数 + 邮件配置 + **大模型接口管理(一键切换/测试)** | @@ -156,7 +157,7 @@ cd works/news-tracker | 接口 | 方法 | 说明 | |------|------|------| | `/api/stats` | GET | 统计数据(总数/重要/趋势/领域分布) | -| `/api/sources` | POST | `action=add\|update\|delete\|toggle` 数据源管理 | +| `/api/sources` | POST | `action=add\|update\|delete\|toggle\|test_standard` 数据源管理(update 可改全部配置;test_standard=用大模型试测推送标准) | | `/api/profile` | POST | `action=add\|delete`,`kind=keyword\|domain\|company` 画像维护 | | `/api/settings` | POST | 更新 `auto` / `mail` 设置 | | `/api/llm` | POST | 大模型接口:`add\|update\|delete\|switch\|toggle\|test`(switch=一键切换) | @@ -168,7 +169,7 @@ cd works/news-tracker ## 数据模型 -- **sources**:数据源(name/type/url/weight/enabled/status/last_fetch/last_count;status 标记真实源采集 ok/error) +- **sources**:数据源(name/type/url/weight/**kind**/**monitor_standard**/enabled/status/last_fetch/last_count;`kind`=normal 普通按权重打分 / custom 定制监控按推送标准 LLM 判断;`monitor_standard`=定制监控的推送标准说明;status 标记真实源采集 ok/error) - **articles**:资讯(title/url/content/summary/**full_text(页面可读全文)**/domain/entities/importance/relevance/total_score/is_important/analysis/llm_status/notified/status/published_at/collected_at) - **keywords** / **domains** / **companies**:兴趣画像 - **llm_providers**:大模型接口(name/base_url/api_key/model/active/enabled;active=当前激活) @@ -177,6 +178,8 @@ cd works/news-tracker `llm_status`:`pending`(待分析) → `done`(完成) / `skipped`(未达标跳过) / `error`(失败) +**定制监控(kind=custom)**:无权重(规则打分时权重项按 0 处理,仅作展示);采集后**无条件进入 LLM 分析**(不受 `llm_threshold` 限制),LLM 按该源的 `monitor_standard`(推送标准)判断 `meets_standard`,达到 → `is_important=1` → 实时邮件推送;未达到 → 不推送。命中推送的资讯也会纳入每日日报。 + **综合分公式**:`total_score = 0.4 × 相关度 + 0.6 × 重要度`(LLM 分析后按 `0.6×规则分 + 0.4×LLM分量` 融合) --- @@ -208,4 +211,4 @@ cd works/news-tracker ## Git - 仓库:`hz4th_coder/news-tracker` -- 版本:`v1.0.0`(初版)→ `v1.0.1`(补 README)→ **`v1.1.0`**(真实网页采集+全文入库 / 大模型接口多预置一键切换 / 数据源可编辑) +- 版本:`v1.0.0`(初版)→ `v1.0.1`(补 README)→ `v1.1.0`(真实网页采集+全文入库 / 大模型接口多预置一键切换 / 数据源可编辑)→ **`v1.2.0`**(数据源完整编辑模态框 / 定制监控类型:无权重+推送标准+LLM判断推送+测试按钮) diff --git a/analysis.py b/analysis.py index 3247f55..9a4becb 100644 --- a/analysis.py +++ b/analysis.py @@ -142,9 +142,21 @@ def _source_weight(a): s = db.get_source(sid) if not s: return 0 + if s.get("kind") == "custom": + # 定制监控无权重,是否推送由大模型按「推送标准」判断 + return 0 return int(round(s["weight"] * 10)) +def source_is_custom(a): + """该资讯所属数据源是否为定制监控类型""" + sid = a.get("source_id") or 0 + if not sid: + return False + s = db.get_source(sid) + return bool(s and s.get("kind") == "custom") + + def analyze_article(aid): """规则打分(立即生效)""" a = db.get_article(aid) @@ -167,6 +179,14 @@ def analyze_article(aid): total = min(100, int(round(0.4 * rel + 0.6 * imp))) + # 定制监控:规则分仅供展示,是否推送完全由大模型按「推送标准」判断(初始标记为不推送,等 LLM 结论) + if source_is_custom(a): + db.update_article(aid, domain=domain, entities=entities, relevance=rel, + total_score=total, is_important=0) + return {"id": aid, "domain": domain, "entities": entities, "relevance": rel, + "importance_rule": imp, "total_score": total, "is_important": 0, + "custom": True} + threshold = int(db.get_setting("realtime_threshold", config.AUTO_DEFAULTS["realtime_threshold"])) is_important = 1 if (total >= threshold or (rel >= 65 and imp >= 70)) else 0 @@ -226,10 +246,14 @@ def _llm_chat(prompt): def llm_analyze(aid): - """LLM 深度分析单条:重要度 1-10 + 相关度 + 结论。失败则标记 error 不阻塞。""" + """LLM 深度分析单条。 + 普通源:重要度/相关度/结论;定制监控源:按「推送标准」判断是否达到推送条件。""" a = db.get_article(aid) if not a: return None + if source_is_custom(a): + src = db.get_source(a.get("source_id") or 0) + return _llm_standard_check(a, src) profile = _profile_text() prompt = ( "你是一位资深科技资讯分析师,专注AI领域。\n" @@ -269,6 +293,36 @@ def llm_analyze(aid): return {"id": aid, "error": str(e)} +def _llm_standard_check(a, src): + """定制监控源:按推送标准让大模型判断该条资讯是否达到推送条件。 + 达到 → is_important=1 → 实时邮件推送;未达到 → 不推送。""" + standard = (src.get("monitor_standard") or "").strip() or "重要资讯" + prompt = ( + "你是一位资讯监控专员。用户配置了一个定制监控数据源,并设定了「推送标准」。\n" + f"【推送标准】\n{standard}\n\n" + f"【资讯标题】{a['title']}\n" + f"【资讯内容】{a.get('content') or a.get('summary')}\n\n" + "请严格对照推送标准判断:这条资讯是否达到应推送的程度?\n" + "只输出一个 JSON 对象(不要任何其他文字),格式:\n" + '{"meets_standard": true或false, "reason": "判断理由(40字内中文)"}' + ) + try: + content, _provider = _llm_chat(prompt) + parsed = json.loads(content) + meets = 1 if parsed.get("meets_standard") else 0 + reason = parsed.get("reason", "") + db.update_article( + a["id"], is_important=meets, analysis=reason, llm_status="done", + importance=7 if meets else 1, + ) + return {"id": a["id"], "meets_standard": bool(meets), "reason": reason, + "total_score": a.get("total_score", 0), "is_important": meets, + "custom": True} + except Exception as e: + db.update_article(a["id"], llm_status="error") + return {"id": a["id"], "error": str(e)} + + def _profile_text(): kws = "、".join(k["keyword"] for k in db.list_keywords() if k["enabled"]) comps = "、".join(c["name"] for c in db.list_companies() if c["enabled"]) @@ -277,12 +331,13 @@ def _profile_text(): def batch_llm_analyze(limit=10): - """后台线程:对 pending 且规则分达标的资讯做 LLM 深度分析""" + """后台线程:对 pending 的资讯做 LLM 深度分析 + 普通源:规则分达 llm_threshold 才分析;定制监控源:无条件分析(是否推送由大模型决定)。""" threshold = int(db.get_setting("llm_threshold", config.AUTO_DEFAULTS["llm_threshold"])) arts = db.pending_llm_articles(limit=limit) results = {"done": 0, "error": 0, "skipped": 0} for a in arts: - if a.get("total_score", 0) < threshold: + if a.get("total_score", 0) < threshold and not source_is_custom(a): db.update_article(a["id"], llm_status="skipped") results["skipped"] += 1 continue diff --git a/app.py b/app.py index 7edf9e1..584726f 100644 --- a/app.py +++ b/app.py @@ -6,6 +6,8 @@ API:采集 / LLM分析 / 汇总 / 画像维护 / 设置维护 """ from datetime import datetime, timedelta +import json + from flask import Flask, render_template, request, jsonify, redirect, url_for import requests @@ -113,12 +115,16 @@ def api_sources(): action = data.get("action") if action == "add": sid = db.add_source(data.get("name", ""), data.get("type", ""), data.get("url", ""), - data.get("description", ""), float(data.get("weight", 1.0))) + data.get("description", ""), float(data.get("weight", 1.0)), + kind=data.get("kind", "normal"), + monitor_standard=data.get("monitor_standard", "")) return jsonify({"ok": True, "id": sid}) if action == "update": db.update_source(data["id"], name=data.get("name"), type=data.get("type"), url=data.get("url"), description=data.get("description"), weight=float(data.get("weight", 1.0)), + kind=data.get("kind", "normal"), + monitor_standard=data.get("monitor_standard", ""), enabled=1 if data.get("enabled") else 0) return jsonify({"ok": True}) if action == "delete": @@ -128,6 +134,27 @@ 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 == "test_standard": + # 定制监控:用大模型测试「推送标准」是否好使(给一段示例内容看判不判得出) + standard = (data.get("monitor_standard") or "").strip() + sample = (data.get("sample") or "").strip() + if not standard: + return jsonify({"ok": False, "error": "请先填写推送标准"}) + prompt = ( + "你是一位资讯监控专员。用户配置了一个定制监控数据源,并设定了「推送标准」。\n" + f"【推送标准】\n{standard}\n\n" + f"【待判断内容】\n{sample or '(未提供示例内容,请自行用一句典型的需推送场景作答,说明是否达到标准)'}\n\n" + "请判断该内容是否达到推送标准。只输出一个 JSON 对象(不要任何其他文字):\n" + '{"meets_standard": true或false, "reason": "判断理由(40字内中文)"}' + ) + try: + content, _name = analysis._llm_chat(prompt) + parsed = json.loads(content) + return jsonify({"ok": True, + "meets": bool(parsed.get("meets_standard")), + "reason": parsed.get("reason", "")}) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}) return jsonify({"ok": False, "error": "unknown action"}) @@ -282,7 +309,9 @@ def main(): # 首次初始化:写入默认数据源 / 兴趣画像 / 默认设置 / 模拟数据 if db.get_setting("initialized") != 1: for s in config.DEFAULT_SOURCES: - db.add_source(s["name"], s["type"], s["url"], s["description"], s["weight"]) + db.add_source(s["name"], s["type"], s["url"], s["description"], s["weight"], + kind=s.get("kind", "normal"), + monitor_standard=s.get("monitor_standard", "")) for kw, w in config.DEFAULT_KEYWORDS: db.add_keyword(kw, w) for d, w in config.DEFAULT_DOMAINS: diff --git a/config.py b/config.py index d52b73b..c28857e 100644 --- a/config.py +++ b/config.py @@ -119,4 +119,8 @@ DEFAULT_SOURCES = [ "description": "行业数据报告、市场调研(模拟源,可改真实 URL)", "weight": 0.7}, {"name": "科技投资动态", "type": "投融资", "url": "https://example.com/funding", "description": "AI 领域融资、并购、IPO 动态(模拟源,可改真实 URL)", "weight": 0.9}, + # 定制监控示例:无权重,按「推送标准」由大模型判断是否推送 + {"name": "英伟达重大异动监控", "type": "定制监控", "url": "https://example.com/custom-nvda", + "description": "定制监控示例:英伟达重大异动(可改真实 URL)", "kind": "custom", + "monitor_standard": "仅推送英伟达(NVIDIA)重大事件:股价单日涨跌幅超10%,或重大产品发布/回购/分拆/收购等实质性公告,或分析师大幅上调目标价。普通行情波动、日常新闻一律不推送。"}, ] diff --git a/db.py b/db.py index 416a8ec..3221644 100644 --- a/db.py +++ b/db.py @@ -18,6 +18,8 @@ CREATE TABLE IF NOT EXISTS sources ( url TEXT DEFAULT '', description TEXT DEFAULT '', weight REAL DEFAULT 1.0, + kind TEXT DEFAULT 'normal', -- normal 普通(按权重打分) / custom 定制监控(按推送标准LLM判断) + monitor_standard TEXT DEFAULT '', -- 定制监控:推送标准说明 enabled INTEGER DEFAULT 1, status TEXT DEFAULT 'ok', -- ok / error last_fetch TEXT DEFAULT '', @@ -111,6 +113,12 @@ def init_db(): os.makedirs(config.DATA_DIR, exist_ok=True) conn = get_conn() conn.executescript(_SCHEMA) + # 迁移:旧库补充 sources.kind / monitor_standard(定制监控类型) + cols = [r["name"] for r in conn.execute("PRAGMA table_info(sources)").fetchall()] + if "kind" not in cols: + conn.execute("ALTER TABLE sources ADD COLUMN kind TEXT DEFAULT 'normal'") + if "monitor_standard" not in cols: + conn.execute("ALTER TABLE sources ADD COLUMN monitor_standard TEXT DEFAULT ''") # 迁移:旧库补充 full_text 列(存页面可读正文) cols = [r["name"] for r in conn.execute("PRAGMA table_info(articles)").fetchall()] if "full_text" not in cols: @@ -179,11 +187,12 @@ def get_source(sid): return dict(row) if row else None -def add_source(name, type_, url, desc, weight=1.0): +def add_source(name, type_, url, desc, weight=1.0, kind="normal", monitor_standard=""): conn = get_conn() cur = conn.execute( - "INSERT INTO sources(name,type,url,description,weight,created_at) VALUES(?,?,?,?,?,?)", - (name, type_, url, desc, weight, now_str()), + "INSERT INTO sources(name,type,url,description,weight,kind,monitor_standard,created_at) " + "VALUES(?,?,?,?,?,?,?,?)", + (name, type_, url, desc, weight, kind, monitor_standard, now_str()), ) conn.commit() conn.close() @@ -191,7 +200,7 @@ def add_source(name, type_, url, desc, weight=1.0): def update_source(sid, **fields): - allowed = {"name", "type", "url", "description", "weight", "enabled"} + allowed = {"name", "type", "url", "description", "weight", "kind", "monitor_standard", "enabled"} sets, vals = [], [] for k, v in fields.items(): if k in allowed: @@ -361,13 +370,15 @@ def article_stats(): def latest_articles_for_summary(window_hours=24): - """汇总窗口内(含未通知、未汇总的)资讯""" + """汇总窗口内(含未通知、未汇总的)资讯 + 普通源按 total_score>=50;定制监控命中推送标准(is_important=1)的也纳入,不看分数。""" conn = get_conn() rows = conn.execute( "SELECT a.*, s.name AS source_name FROM articles a " "LEFT JOIN sources s ON a.source_id=s.id " "WHERE a.published_at >= datetime('now','localtime','-%d hours') " - "AND a.total_score>=50 ORDER BY a.total_score DESC LIMIT 200" % int(window_hours) + "AND (a.total_score>=50 OR (s.kind='custom' AND a.is_important=1)) " + "ORDER BY a.total_score DESC LIMIT 200" % int(window_hours) ).fetchall() conn.close() out = [] diff --git a/notifier.py b/notifier.py index d94450c..61dca32 100644 --- a/notifier.py +++ b/notifier.py @@ -53,11 +53,15 @@ def _score_color(score): def _card(art): ents = "、".join(art.get("entities") or []) + src = db.get_source(art.get("source_id") or 0) if art.get("source_id") else None + custom = bool(src and src.get("kind") == "custom") + badge = ('🎯 定制监控命中') if custom else "" return f"""
- {art['title']} + {badge}{art['title']}
综合分 {art.get('total_score',0)} · 相关度 {art.get('relevance',0)} · diff --git a/static/style.css b/static/style.css index e7aea53..20bfc40 100644 --- a/static/style.css +++ b/static/style.css @@ -110,5 +110,19 @@ textarea { width:100%; } .toast.ok { background:#059669; } .toast.err { background:#dc2626; } +/* 模态框(数据源新增/编辑) */ +.modal-mask { position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(17,24,39,.55); display:flex; align-items:center; justify-content:center; z-index:50; } +.modal { background:#fff; border-radius:14px; width:560px; max-width:92vw; max-height:88vh; overflow:auto; box-shadow:0 10px 30px rgba(0,0,0,.2); } +.modal-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid #f3f4f6; } +.modal-head h3 { font-size:16px; } +.modal-x { cursor:pointer; font-size:18px; color:#9ca3af; padding:4px; } +.modal-x:hover { color:#dc2626; } +.modal-body { padding:16px 20px; } +.modal-body label { display:block; font-size:12px; color:#6b7280; margin:12px 0 4px; } +.modal-body input, .modal-body select, .modal-body textarea { width:100%; padding:8px 10px; border:1px solid #d1d5db; border-radius:8px; font-size:13px; } +.modal-body textarea { resize:vertical; } +.modal-foot { display:flex; justify-content:flex-end; gap:8px; padding:14px 20px; border-top:1px solid #f3f4f6; } +.modal-mask[hidden] { display:none !important; } + /* 页面可读全文(干净排版) */ .full-text { margin-top:8px; padding:14px 16px; background:#f9fafb; border:1px solid #e5e7eb; border-radius:8px; font-size:13px; line-height:1.9; color:#374151; white-space:pre-wrap; word-break:break-word; } diff --git a/templates/sources.html b/templates/sources.html index 6f1386d..236fb4f 100644 --- a/templates/sources.html +++ b/templates/sources.html @@ -4,36 +4,25 @@ {% block content %}

📡 数据源管理

- -
- -
- + {% for s in sources %} - + + - + @@ -41,40 +30,128 @@ {% endfor %}
ID名称类型权重状态最近采集新增条数操作
ID名称监控方式类型权重状态最近采集新增条数操作
{{ s.id }} {{ s.name }}
{{ s.description }}
{{ s.url }}
{% if s.kind == 'custom' %}🎯 定制监控{% else %}普通{% endif %} {{ s.type }}{{ s.weight }}{% if s.kind == 'custom' %}{% else %}{{ s.weight }}{% endif %} {% if s.enabled %}启用{% else %}停用{% endif %} {{ s.last_fetch or '—' }} {{ s.last_count }} - +
-
💡 真实 URL 源会自动抓取页面并清洗可读正文入库;example.com 等占位地址走模拟数据。
+
+ 💡 真实 URL 源自动抓取页面并清洗可读正文入库;example.com 等占位地址走模拟数据。
+ 🎯 定制监控:不设权重,填写「推送标准」后由大模型逐条判断是否达到标准,达到即实时邮件推送。 +
+
+ + + {% endblock %} {% block script %} {% endblock %}