v1.4.0: 通知日志分页+筛选 / 仪表盘TOP10+更多链接 / 数据源走web-capture-api抓取(获取方式与参数可编辑) / 系统错误邮件通知(频率+静默时段)
This commit is contained in:
@@ -31,8 +31,8 @@ def index():
|
||||
@app.route("/dashboard")
|
||||
def dashboard():
|
||||
stats = db.article_stats()
|
||||
latest = db.list_articles(limit=12, order="a.collected_at DESC")
|
||||
important = db.list_articles(is_important=1, limit=12, order="a.total_score DESC")
|
||||
latest = db.list_articles(limit=10, order="a.collected_at DESC")
|
||||
important = db.list_articles(is_important=1, limit=10, order="a.total_score DESC")
|
||||
dom_rows = db.get_conn().execute(
|
||||
"SELECT domain, COUNT(*) c FROM articles GROUP BY domain ORDER BY c DESC").fetchall()
|
||||
domain_stats = [{"name": r["domain"] or "未分类", "count": r["c"]} for r in dom_rows]
|
||||
@@ -86,13 +86,62 @@ def profile():
|
||||
|
||||
@app.route("/logs")
|
||||
def logs():
|
||||
return render_template("logs.html", logs=db.list_logs(limit=100))
|
||||
f_type = request.args.get("type", "")
|
||||
f_status = request.args.get("status", "")
|
||||
q = request.args.get("q", "")
|
||||
page = max(1, int(request.args.get("page", 1)))
|
||||
per = 20
|
||||
total, logs = db.list_logs(page=page, page_size=per,
|
||||
type_=f_type or None, status=f_status or None, q=q or None)
|
||||
pages = max(1, (total + per - 1) // per)
|
||||
return render_template("logs.html", logs=logs, f_type=f_type, f_status=f_status,
|
||||
q=q, page=page, pages=pages, total=total)
|
||||
|
||||
|
||||
@app.route("/api/logs")
|
||||
def api_logs():
|
||||
"""通知日志分页查询(JSON)
|
||||
GET /api/logs?page=&page_size=&type=&status=&q=
|
||||
"""
|
||||
f_type = request.args.get("type", "")
|
||||
f_status = request.args.get("status", "")
|
||||
q = request.args.get("q", "")
|
||||
page = max(1, request.args.get("page", 1, type=int))
|
||||
page_size = min(100, max(1, request.args.get("page_size", 20, type=int)))
|
||||
total, rows = db.list_logs(page=page, page_size=page_size,
|
||||
type_=f_type or None, status=f_status or None, q=q or None)
|
||||
return jsonify({"ok": True, "logs": rows, "page": page, "page_size": page_size,
|
||||
"total": total, "pages": max(1, (total + page_size - 1) // page_size)})
|
||||
|
||||
|
||||
@app.route("/api/errors")
|
||||
def api_errors():
|
||||
"""系统错误日志(最近 N 条)"""
|
||||
limit = min(200, request.args.get("limit", 100, type=int))
|
||||
return jsonify({"ok": True, "errors": db.list_system_errors(limit=limit)})
|
||||
|
||||
|
||||
@app.route("/settings")
|
||||
def settings_page():
|
||||
return render_template("settings.html", auto=db.get_all_settings(),
|
||||
providers=db.list_providers())
|
||||
auto = db.get_all_settings()
|
||||
# 归一化(兼容老库嵌套数据),并补齐默认值,保证模板字段齐全
|
||||
a0 = auto.get("auto")
|
||||
if not isinstance(a0, dict) or "auto_collect" not in a0:
|
||||
a0 = {}
|
||||
for k in config.AUTO_DEFAULTS:
|
||||
a0[k] = (auto.get("auto") or {}).get(k, config.AUTO_DEFAULTS[k])
|
||||
auto["auto"] = a0
|
||||
for k, dft in (("mail", config.MAIL_DEFAULTS), ("custom", config.CUSTOM_DEFAULTS),
|
||||
("webcapture", config.WEBCAPTURE_DEFAULTS),
|
||||
("errnotify", config.ERRNOTIFY_DEFAULTS)):
|
||||
v = auto.get(k)
|
||||
if not isinstance(v, dict):
|
||||
auto[k] = dict(dft)
|
||||
else:
|
||||
merged = dict(dft)
|
||||
merged.update(v)
|
||||
auto[k] = merged
|
||||
return render_template("settings.html", auto=auto, providers=db.list_providers())
|
||||
|
||||
|
||||
# ---------------- API ----------------
|
||||
@@ -118,7 +167,9 @@ def api_sources():
|
||||
data.get("description", ""), float(data.get("weight", 1.0)),
|
||||
kind=data.get("kind", "normal"),
|
||||
monitor_standard=data.get("monitor_standard", ""),
|
||||
scan_interval_min=int(data.get("scan_interval_min", 0) or 0))
|
||||
scan_interval_min=int(data.get("scan_interval_min", 0) or 0),
|
||||
fetch_method=data.get("fetch_method", "auto") or "auto",
|
||||
capture_params=data.get("capture_params", "{}") or "{}")
|
||||
return jsonify({"ok": True, "id": sid})
|
||||
if action == "update":
|
||||
db.update_source(data["id"], name=data.get("name"), type=data.get("type"),
|
||||
@@ -127,6 +178,8 @@ def api_sources():
|
||||
kind=data.get("kind", "normal"),
|
||||
monitor_standard=data.get("monitor_standard", ""),
|
||||
scan_interval_min=int(data.get("scan_interval_min", 0) or 0),
|
||||
fetch_method=data.get("fetch_method", "auto") or "auto",
|
||||
capture_params=data.get("capture_params", "{}") or "{}",
|
||||
enabled=1 if data.get("enabled") else 0)
|
||||
return jsonify({"ok": True})
|
||||
if action == "delete":
|
||||
@@ -257,6 +310,14 @@ def api_settings():
|
||||
cur = db.get_all_settings().get("custom", {})
|
||||
cur.update(data["custom"])
|
||||
db.set_setting("custom", cur)
|
||||
if "webcapture" in data and isinstance(data["webcapture"], dict):
|
||||
cur = db.get_all_settings().get("webcapture", {})
|
||||
cur.update(data["webcapture"])
|
||||
db.set_setting("webcapture", cur)
|
||||
if "errnotify" in data and isinstance(data["errnotify"], dict):
|
||||
cur = db.get_all_settings().get("errnotify", {})
|
||||
cur.update(data["errnotify"])
|
||||
db.set_setting("errnotify", cur)
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@@ -358,6 +419,28 @@ def api_actions():
|
||||
return jsonify({"ok": True, "msg": "测试邮件已发送"})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e)})
|
||||
if action == "test_error_mail":
|
||||
try:
|
||||
notifier.test_error_notify()
|
||||
return jsonify({"ok": True, "msg": "测试错误通知邮件已发送"})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e)})
|
||||
if action == "test_webcapture":
|
||||
try:
|
||||
cfg = dict(config.WEBCAPTURE_DEFAULTS)
|
||||
cfg.update(db.get_all_settings().get("webcapture", {}))
|
||||
base = (cfg.get("api_url") or "").rstrip("/")
|
||||
r = requests.post(f"{base}/api/capture", json={"url": "https://example.com",
|
||||
"action": "text", "wait_time": 800},
|
||||
timeout=30)
|
||||
r.raise_for_status()
|
||||
d = r.json()
|
||||
if d.get("success"):
|
||||
return jsonify({"ok": True, "title": d.get("title", ""),
|
||||
"text": (d.get("text") or "")[:80], "api_url": base})
|
||||
return jsonify({"ok": False, "error": d.get("error", "接口返回失败")})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e)})
|
||||
return jsonify({"ok": False, "error": "unknown action"})
|
||||
|
||||
|
||||
@@ -366,12 +449,30 @@ def not_found(e):
|
||||
return render_template("404.html"), 404
|
||||
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
def handle_exception(e):
|
||||
"""未捕获异常 → 记录系统错误并邮件通知(频率/静默由设置控制),返回 500 页"""
|
||||
from werkzeug.exceptions import HTTPException
|
||||
if isinstance(e, HTTPException):
|
||||
return e
|
||||
try:
|
||||
notifier.report_error("系统", "未捕获异常", f"{type(e).__name__}: {e}")
|
||||
except Exception:
|
||||
pass
|
||||
return render_template("500.html"), 500
|
||||
|
||||
|
||||
# ---------------- 启动 ----------------
|
||||
def main():
|
||||
db.init_db()
|
||||
# 确保定制监控机制配置存在(老库升级)
|
||||
if db.get_setting("custom") is None:
|
||||
db.set_setting("custom", dict(config.CUSTOM_DEFAULTS))
|
||||
# 确保网页提取服务 / 错误通知配置存在(老库升级)
|
||||
if db.get_setting("webcapture") is None:
|
||||
db.set_setting("webcapture", dict(config.WEBCAPTURE_DEFAULTS))
|
||||
if db.get_setting("errnotify") is None:
|
||||
db.set_setting("errnotify", dict(config.ERRNOTIFY_DEFAULTS))
|
||||
# 首次初始化:写入默认数据源 / 兴趣画像 / 默认设置 / 模拟数据
|
||||
if db.get_setting("initialized") != 1:
|
||||
for s in config.DEFAULT_SOURCES:
|
||||
|
||||
Reference in New Issue
Block a user