feat: add admin dashboard, pipeline service, lightbox, and update dependencies
This commit is contained in:
+79
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user