feat: add admin dashboard, pipeline service, lightbox, and update dependencies

This commit is contained in:
2026-06-09 09:32:10 +08:00
parent 0d293422ac
commit 32978b3fc5
50 changed files with 4054 additions and 1618 deletions
+79 -2
View File
@@ -2,10 +2,14 @@
from __future__ import annotations
from datetime import datetime, timezone
import json
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo
import bleach
import httpx
from fastapi.templating import Jinja2Templates
@@ -35,12 +39,36 @@ templates = _Templates(directory="app/templates")
# ── 时区工具 ──────────────────────────────────────────────────────────
def utc_now() -> datetime:
"""当前 UTC 时间(替代 datetime.now(timezone.utc) 的简写)。"""
return datetime.now(timezone.utc)
def today_str() -> str:
"""当前日期字符串(按 APP_TIMEZONE)。"""
tz = ZoneInfo(settings.APP_TIMEZONE)
return datetime.now(tz).strftime("%Y-%m-%d")
def yesterday_str() -> str:
"""昨天日期字符串(按 APP_TIMEZONE)。"""
tz = ZoneInfo(settings.APP_TIMEZONE)
yesterday = datetime.now(tz).date() - timedelta(days=1)
return yesterday.isoformat()
def latest_paper_date(db) -> str:
"""查询数据库中最新的 paper_date,无数据时回退到 today_str()。"""
from sqlalchemy import func, select
from app.models import Paper
result = db.scalar(select(func.max(Paper.paper_date)))
if result is not None:
return result.isoformat() if isinstance(result, date) else str(result)
return today_str()
# ── 锁释放 ────────────────────────────────────────────────────────────
@@ -48,7 +76,7 @@ def release_lock(db, lock) -> None:
"""释放 TaskLock。"""
try:
lock.status = "finished"
lock.released_at = datetime.now(timezone.utc)
lock.released_at = utc_now()
db.commit()
except Exception:
db.rollback()
@@ -83,3 +111,52 @@ def make_http_client(
if sync:
return httpx.Client(**defaults)
return httpx.AsyncClient(**defaults)
# ── JSON 安全解析 ──────────────────────────────────────────────────────
def safe_json_loads(text: str | None, default: Any = None) -> Any:
"""安全解析 JSON 字符串,解析失败返回 default 值(不会抛异常)。"""
if not text:
return default
try:
return json.loads(text)
except (json.JSONDecodeError, TypeError, ValueError):
return default
# ── HTML 清洗 ──────────────────────────────────────────────────────────
# AI 生成内容中允许的 HTML 标签和属性
_ALLOWED_TAGS = {
"p", "br", "strong", "b", "em", "i", "u", "s", "del",
"h3", "h4", "h5", "h6",
"ul", "ol", "li",
"a", "code", "pre", "blockquote",
"table", "thead", "tbody", "tr", "th", "td",
"sup", "sub", "span",
}
_ALLOWED_ATTRS = {
"a": {"href", "title"},
"th": {"colspan", "rowspan"},
"td": {"colspan", "rowspan"},
"span": {"class"},
}
def sanitize_html(text: str | None) -> str:
"""清洗 AI 生成的 HTML,移除危险标签但保留安全的富文本。
- 移除: <script>, <iframe>, on* 事件属性, javascript: 链接
- 保留: 段落、加粗、列表、表格、链接等排印元素
"""
if not text:
return ""
cleaned = bleach.clean(
text,
tags=_ALLOWED_TAGS,
attributes=_ALLOWED_ATTRS,
strip=True,
)
return cleaned