feat: enhance UI, refactor services, improve templates and tests

- Replace image_extractor with pdf_image_extractor service
- Enhance pi_client with expanded API capabilities
- Improve summarizer service with additional features
- Update admin routes with more endpoints
- Add login page template
- Enhance detail page with comprehensive layout
- Improve search and trends pages
- Update base template with additional elements
- Refactor tests for better coverage
- Add validate_summary script
- Update project configuration and dependencies
This commit is contained in:
2026-06-07 19:38:58 +08:00
parent 4a72c35452
commit 0d293422ac
32 changed files with 2003 additions and 586 deletions
+67 -17
View File
@@ -1,11 +1,12 @@
"""管理接口 — 抓取、总结、清理、删除、日志,需要 ADMIN_TOKEN 鉴权。"""
"""管理接口 — 抓取、总结、清理、删除、日志,需要登录鉴权。"""
from __future__ import annotations
import hashlib
from datetime import date, datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi import APIRouter, Depends, Form, HTTPException, Query, Request
from fastapi.responses import RedirectResponse
from pydantic import BaseModel, field_validator
from sqlalchemy import select
from sqlalchemy.orm import Session
@@ -19,16 +20,65 @@ from app.services.summarizer import summarize_batch, summarize_single
from app.utils import release_lock, templates, today_str
router = APIRouter(prefix="/admin", tags=["admin"])
security = HTTPBearer()
async def verify_admin(
credentials: HTTPAuthorizationCredentials = Depends(security),
) -> str:
"""验证 ADMIN_TOKEN。"""
if credentials.credentials != settings.ADMIN_TOKEN:
raise HTTPException(status_code=401, detail="Invalid admin token")
return credentials.credentials
# ── 认证 ──────────────────────────────────────────────────────────────
def _check_password(password: str) -> bool:
"""校验密码,支持明文或 sha256 哈希。"""
stored = settings.ADMIN_PASSWORD
if not stored:
return False
if password == stored:
return True
# 也支持存 sha256 哈希
return hashlib.sha256(password.encode()).hexdigest() == stored
async def verify_admin(request: Request) -> None:
"""检查 session 中的登录状态,未登录则重定向到登录页。"""
if not request.session.get("is_admin"):
raise HTTPException(status_code=303, headers={"Location": "/admin/login"})
def verify_admin_page(request: Request) -> None:
"""页面级认证:未登录重定向到登录页(同步版本,用于模板路由)。"""
if not request.session.get("is_admin"):
raise HTTPException(status_code=303, headers={"Location": "/admin/login"})
# ── 登录 / 登出 ──────────────────────────────────────────────────────
@router.get("/login")
async def admin_login_page(request: Request):
"""显示登录页面。已登录则直接跳转管理页。"""
if request.session.get("is_admin"):
return RedirectResponse("/admin/logs", status_code=303)
return templates.TemplateResponse(request, "login.html", {"error": None})
@router.post("/login")
async def admin_login_submit(
request: Request,
username: str = Form(""),
password: str = Form(""),
):
"""处理登录表单提交。"""
if username == settings.ADMIN_USERNAME and _check_password(password):
request.session["is_admin"] = True
return RedirectResponse("/admin/logs", status_code=303)
return templates.TemplateResponse(
request, "login.html", {"error": "用户名或密码错误"}
)
@router.post("/logout")
async def admin_logout(request: Request):
"""退出登录,清除 session。"""
request.session.clear()
return RedirectResponse("/admin/login", status_code=303)
# ── 请求模型 ──────────────────────────────────────────────────────────
@@ -53,7 +103,7 @@ class DeleteRequest(BaseModel):
@router.post("/crawl")
async def admin_crawl(
_admin: str = Depends(verify_admin),
_admin: None = Depends(verify_admin),
db: Session = Depends(get_db),
date: str | None = Query(None, description="YYYY-MM-DD,默认今天"),
):
@@ -92,7 +142,7 @@ async def admin_crawl(
@router.post("/summarize")
async def admin_summarize_batch(
_admin: str = Depends(verify_admin),
_admin: None = Depends(verify_admin),
db: Session = Depends(get_db),
):
"""批量总结所有 pending 论文。"""
@@ -107,7 +157,7 @@ async def admin_summarize_batch(
@router.post("/summarize/{arxiv_id}")
async def admin_summarize_single(
arxiv_id: str,
_admin: str = Depends(verify_admin),
_admin: None = Depends(verify_admin),
db: Session = Depends(get_db),
):
"""总结或重跑单篇论文。"""
@@ -122,7 +172,7 @@ async def admin_summarize_single(
@router.post("/cleanup")
async def admin_cleanup(
_admin: str = Depends(verify_admin),
_admin: None = Depends(verify_admin),
db: Session = Depends(get_db),
):
"""清理 data/tmp/ 中超过 24 小时的临时文件。"""
@@ -159,7 +209,7 @@ async def admin_cleanup(
@router.post("/delete")
async def admin_delete(
body: DeleteRequest,
_admin: str = Depends(verify_admin),
_admin: None = Depends(verify_admin),
db: Session = Depends(get_db),
):
"""删除指定日期范围内的论文(需要 confirm='DELETE' 二次确认)。"""
@@ -181,7 +231,7 @@ async def admin_delete(
@router.get("/logs")
async def admin_logs(
request: Request,
_admin: str = Depends(verify_admin),
_admin: None = Depends(verify_admin),
db: Session = Depends(get_db),
page: int = Query(1, ge=1),
per_page: int = Query(20, ge=1, le=100),