diff --git a/app/cli.py b/app/cli.py
index 0f25c7e..ef1216c 100644
--- a/app/cli.py
+++ b/app/cli.py
@@ -31,6 +31,7 @@ def crawl(
# 确保数据库和表存在
import os
+
os.makedirs(settings.db_path.parent, exist_ok=True)
_init(engine)
typer.echo(f"📡 开始抓取 {target} ...")
@@ -63,6 +64,7 @@ def summarize(
from app.services.summarizer import summarize_batch, summarize_single
import os
+
os.makedirs(settings.db_path.parent, exist_ok=True)
_init(engine)
@@ -97,6 +99,7 @@ def init_db():
from app.models import init_db as _init
import os
+
os.makedirs(settings.db_path.parent, exist_ok=True)
_init(engine)
typer.echo(f"✅ 数据库已初始化:{settings.db_path}")
diff --git a/app/config.py b/app/config.py
index e316855..f537bb7 100644
--- a/app/config.py
+++ b/app/config.py
@@ -62,7 +62,7 @@ class Settings(BaseSettings):
# sqlite:///data/db/papers.db → data/db/papers.db
url = self.DATABASE_URL
if url.startswith("sqlite:///"):
- return BASE_DIR / url[len("sqlite:///"):]
+ return BASE_DIR / url[len("sqlite:///") :]
raise ValueError(f"Unsupported DATABASE_URL: {url}")
@property
diff --git a/app/main.py b/app/main.py
index 33ceaaf..8943786 100644
--- a/app/main.py
+++ b/app/main.py
@@ -58,7 +58,9 @@ def create_app() -> FastAPI:
# 安全警告
if settings.ADMIN_TOKEN == "change-me":
- logger.warning("⚠️ ADMIN_TOKEN is the default value 'change-me'. Please change it in .env!")
+ logger.warning(
+ "⚠️ ADMIN_TOKEN is the default value 'change-me'. Please change it in .env!"
+ )
if settings.APP_HOST not in ("127.0.0.1", "localhost", "::1"):
logger.warning(
diff --git a/app/models.py b/app/models.py
index eb5006a..378cd5b 100644
--- a/app/models.py
+++ b/app/models.py
@@ -43,13 +43,39 @@ class Paper(Base):
raw_output_path = Column(String)
summary_quality = Column(String)
- authors = relationship("PaperAuthor", back_populates="paper", cascade="all, delete-orphan")
- tags = relationship("PaperTag", back_populates="paper", cascade="all, delete-orphan")
- summary = relationship("PaperSummary", back_populates="paper", uselist=False, cascade="all, delete-orphan")
- summary_status = relationship("SummaryStatus", back_populates="paper", uselist=False, cascade="all, delete-orphan")
- bookmark = relationship("UserBookmark", back_populates="paper", uselist=False, cascade="all, delete-orphan")
- reading_status = relationship("UserReadingStatus", back_populates="paper", uselist=False, cascade="all, delete-orphan")
- note = relationship("UserNote", back_populates="paper", uselist=False, cascade="all, delete-orphan")
+ authors = relationship(
+ "PaperAuthor", back_populates="paper", cascade="all, delete-orphan"
+ )
+ tags = relationship(
+ "PaperTag", back_populates="paper", cascade="all, delete-orphan"
+ )
+ summary = relationship(
+ "PaperSummary",
+ back_populates="paper",
+ uselist=False,
+ cascade="all, delete-orphan",
+ )
+ summary_status = relationship(
+ "SummaryStatus",
+ back_populates="paper",
+ uselist=False,
+ cascade="all, delete-orphan",
+ )
+ bookmark = relationship(
+ "UserBookmark",
+ back_populates="paper",
+ uselist=False,
+ cascade="all, delete-orphan",
+ )
+ reading_status = relationship(
+ "UserReadingStatus",
+ back_populates="paper",
+ uselist=False,
+ cascade="all, delete-orphan",
+ )
+ note = relationship(
+ "UserNote", back_populates="paper", uselist=False, cascade="all, delete-orphan"
+ )
# ── paper_authors ───────────────────────────────────────────────────────
@@ -58,7 +84,9 @@ class PaperAuthor(Base):
__table_args__ = (UniqueConstraint("paper_id", "name"),)
id = Column(Integer, primary_key=True, autoincrement=True)
- paper_id = Column(Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False)
+ paper_id = Column(
+ Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False
+ )
name = Column(String, nullable=False)
position = Column(Integer, default=0)
@@ -71,7 +99,9 @@ class PaperTag(Base):
__table_args__ = (UniqueConstraint("paper_id", "tag", "source"),)
id = Column(Integer, primary_key=True, autoincrement=True)
- paper_id = Column(Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False)
+ paper_id = Column(
+ Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False
+ )
tag = Column(String, nullable=False)
source = Column(String, default="hf")
@@ -82,7 +112,9 @@ class PaperTag(Base):
class PaperSummary(Base):
__tablename__ = "paper_summaries"
- paper_id = Column(Integer, ForeignKey("papers.id", ondelete="CASCADE"), primary_key=True)
+ paper_id = Column(
+ Integer, ForeignKey("papers.id", ondelete="CASCADE"), primary_key=True
+ )
one_line = Column(Text)
difficulty = Column(String)
prerequisites_json = Column(Text)
@@ -111,7 +143,9 @@ class SummaryStatus(Base):
__table_args__ = (UniqueConstraint("paper_id"),)
id = Column(Integer, primary_key=True, autoincrement=True)
- paper_id = Column(Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False)
+ paper_id = Column(
+ Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False
+ )
status = Column(String, nullable=False, default="pending")
quality = Column(String)
error_type = Column(String)
@@ -158,7 +192,9 @@ class UserBookmark(Base):
__table_args__ = (UniqueConstraint("paper_id"),)
id = Column(Integer, primary_key=True, autoincrement=True)
- paper_id = Column(Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False)
+ paper_id = Column(
+ Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False
+ )
note = Column(Text)
created_at = Column(DateTime, nullable=False)
@@ -170,7 +206,9 @@ class UserReadingStatus(Base):
__table_args__ = (UniqueConstraint("paper_id"),)
id = Column(Integer, primary_key=True, autoincrement=True)
- paper_id = Column(Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False)
+ paper_id = Column(
+ Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False
+ )
status = Column(String, nullable=False, default="unread")
updated_at = Column(DateTime, nullable=False)
@@ -182,7 +220,9 @@ class UserNote(Base):
__table_args__ = (UniqueConstraint("paper_id"),)
id = Column(Integer, primary_key=True, autoincrement=True)
- paper_id = Column(Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False)
+ paper_id = Column(
+ Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False
+ )
content = Column(Text, nullable=False)
created_at = Column(DateTime, nullable=False)
updated_at = Column(DateTime, nullable=False)
diff --git a/app/routes/admin.py b/app/routes/admin.py
index bac16d2..8947c3a 100644
--- a/app/routes/admin.py
+++ b/app/routes/admin.py
@@ -74,7 +74,9 @@ async def admin_crawl(
db.commit()
except Exception:
db.rollback()
- raise HTTPException(status_code=409, detail=f"Crawl already running for {target_date}")
+ raise HTTPException(
+ status_code=409, detail=f"Crawl already running for {target_date}"
+ )
try:
result = await crawl_daily(db, target_date)
@@ -96,7 +98,9 @@ async def admin_summarize_batch(
"""批量总结所有 pending 论文。"""
result = await summarize_batch(db)
if result.get("status") == "conflict":
- raise HTTPException(status_code=409, detail=result.get("error", "batch already running"))
+ raise HTTPException(
+ status_code=409, detail=result.get("error", "batch already running")
+ )
return result
diff --git a/app/routes/pages.py b/app/routes/pages.py
index b249046..4c12c8d 100644
--- a/app/routes/pages.py
+++ b/app/routes/pages.py
@@ -58,10 +58,13 @@ def day_page(date_str: str, request: Request, db: Session = Depends(get_db)):
.limit(30)
.all()
)
- available_dates = [d[0].isoformat() if isinstance(d[0], date) else str(d[0]) for d in dates_raw]
+ available_dates = [
+ d[0].isoformat() if isinstance(d[0], date) else str(d[0]) for d in dates_raw
+ ]
return templates.TemplateResponse(
- request, "index.html",
+ request,
+ "index.html",
{
"papers": papers,
"current_date": date_str,
@@ -105,7 +108,8 @@ def paper_detail(arxiv_id: str, request: Request, db: Session = Depends(get_db))
images = _get_paper_images(arxiv_id)
return templates.TemplateResponse(
- request, "detail.html",
+ request,
+ "detail.html",
{
"paper": paper,
"summary_state": summary_state,
@@ -166,7 +170,11 @@ def _get_similar_papers(db: Session, arxiv_id: str, top_k: int = 6) -> list[dict
# 从 DB 加载论文信息
similar_ids = results["ids"][0]
- distances = results["distances"][0] if results["distances"] else [0.0] * len(similar_ids)
+ distances = (
+ results["distances"][0]
+ if results["distances"]
+ else [0.0] * len(similar_ids)
+ )
# 排除自身
papers_info = {}
@@ -186,13 +194,15 @@ def _get_similar_papers(db: Session, arxiv_id: str, top_k: int = 6) -> list[dict
items = []
for p in papers:
- items.append({
- "arxiv_id": p.arxiv_id,
- "title_zh": p.title_zh or p.title_en,
- "distance": papers_info.get(p.arxiv_id, 0.0),
- "paper_date": p.paper_date.isoformat() if p.paper_date else "",
- "tags": [t.tag for t in p.tags[:3]],
- })
+ items.append(
+ {
+ "arxiv_id": p.arxiv_id,
+ "title_zh": p.title_zh or p.title_en,
+ "distance": papers_info.get(p.arxiv_id, 0.0),
+ "paper_date": p.paper_date.isoformat() if p.paper_date else "",
+ "tags": [t.tag for t in p.tags[:3]],
+ }
+ )
# 按距离排序
items.sort(key=lambda x: x["distance"])
@@ -215,8 +225,10 @@ def _get_paper_images(arxiv_id: str) -> list[dict]:
images = []
for img_file in sorted(images_dir.iterdir()):
if img_file.suffix.lower() in (".png", ".jpg", ".jpeg", ".gif", ".svg"):
- images.append({
- "url": f"/papers/{arxiv_id}/images/{img_file.name}",
- "name": img_file.name,
- })
+ images.append(
+ {
+ "url": f"/papers/{arxiv_id}/images/{img_file.name}",
+ "name": img_file.name,
+ }
+ )
return images
diff --git a/app/routes/search.py b/app/routes/search.py
index 0ec43be..df4bbf4 100644
--- a/app/routes/search.py
+++ b/app/routes/search.py
@@ -34,7 +34,9 @@ def search_page(
db: Session = Depends(get_db),
):
"""搜索页面,支持 keyword 和 semantic 模式。"""
- result = search_papers(db, query=q or None, tag=tag or None, sort=sort, page=page, mode=mode)
+ result = search_papers(
+ db, query=q or None, tag=tag or None, sort=sort, page=page, mode=mode
+ )
all_tags = get_all_tags(db)
return templates.TemplateResponse(
@@ -72,7 +74,9 @@ def search_api(
db: Session = Depends(get_db),
):
"""搜索 JSON API,支持 keyword 和 semantic 模式。"""
- result = search_papers(db, query=q or None, tag=tag or None, sort=sort, page=page, mode=mode)
+ result = search_papers(
+ db, query=q or None, tag=tag or None, sort=sort, page=page, mode=mode
+ )
distances = result.get("distances", {})
items = []
@@ -170,7 +174,9 @@ def _generate_rss_xml(papers: list[Paper], base_url: str, tag: str | None) -> st
channel_title += f" — {tag}"
lines.append(f"
{escape(channel_title)} ")
lines.append(f" {escape(base_url)}")
- lines.append(" HuggingFace Daily Papers — 中文论文导览站 ")
+ lines.append(
+ " HuggingFace Daily Papers — 中文论文导览站 "
+ )
lines.append(" zh-CN ")
for paper in papers:
diff --git a/app/services/cleaner.py b/app/services/cleaner.py
index 5916a43..414b6a9 100644
--- a/app/services/cleaner.py
+++ b/app/services/cleaner.py
@@ -61,7 +61,9 @@ def cleanup_tmp(max_age_hours: int = _MAX_TMP_AGE_HOURS) -> dict:
errors.append(err_msg)
logger.warning("Failed to clean tmp dir %s: %s", entry.name, exc)
- logger.info("Tmp cleanup: scanned=%d removed=%d errors=%d", scanned, removed, len(errors))
+ logger.info(
+ "Tmp cleanup: scanned=%d removed=%d errors=%d", scanned, removed, len(errors)
+ )
return {"scanned": scanned, "removed": removed, "errors": errors}
@@ -109,7 +111,12 @@ async def delete_papers_by_date_range(
)
total = len(papers)
- logger.info("Delete papers by date range: %s ~ %s, found %d papers", date_start, date_end, total)
+ logger.info(
+ "Delete papers by date range: %s ~ %s, found %d papers",
+ date_start,
+ date_end,
+ total,
+ )
# 创建 delete job 记录
job = DataDeleteJob(
@@ -139,9 +146,12 @@ async def delete_papers_by_date_range(
# 1.5 Phase 5: 从 ChromaDB 删除语义索引
try:
from app.services.embedder import delete_paper
+
delete_paper(arxiv_id)
except Exception:
- logger.warning("Failed to delete %s from ChromaDB", arxiv_id, exc_info=True)
+ logger.warning(
+ "Failed to delete %s from ChromaDB", arxiv_id, exc_info=True
+ )
# 2. 删除本地文件 data/papers/{arxiv_id}/
paper_dir = PAPERS_DIR / arxiv_id
@@ -179,7 +189,9 @@ async def delete_papers_by_date_range(
job_status = "success"
if failed_items:
job_status = "failed" if deleted == 0 else "success"
- job_error = "; ".join(f"{f['arxiv_id']}: {f['error']}" for f in failed_items[:20])
+ job_error = "; ".join(
+ f"{f['arxiv_id']}: {f['error']}" for f in failed_items[:20]
+ )
job.status = job_status
job.paper_count = deleted
@@ -210,6 +222,10 @@ async def delete_papers_by_date_range(
}
logger.info(
"Delete job completed: date_range=%s~%s total=%d deleted=%d failed=%d",
- date_start, date_end, total, deleted, len(failed_items),
+ date_start,
+ date_end,
+ total,
+ deleted,
+ len(failed_items),
)
return result
diff --git a/app/services/crawler.py b/app/services/crawler.py
index 371c90e..33897e2 100644
--- a/app/services/crawler.py
+++ b/app/services/crawler.py
@@ -38,20 +38,29 @@ async def fetch_daily(target_date: str, top_n: int | None = None) -> list[dict]:
async with make_http_client() as client:
for attempt in range(1, settings.HTTP_MAX_RETRIES + 1):
try:
- logger.info("Fetching HF Daily Papers: date=%s attempt=%d", target_date, attempt)
+ logger.info(
+ "Fetching HF Daily Papers: date=%s attempt=%d", target_date, attempt
+ )
resp = await client.get(url, params=params)
resp.raise_for_status()
data = resp.json()
break
except (httpx.HTTPError, httpx.HTTPStatusError) as exc:
- logger.warning("Fetch failed (attempt %d/%d): %s", attempt, settings.HTTP_MAX_RETRIES, exc)
+ logger.warning(
+ "Fetch failed (attempt %d/%d): %s",
+ attempt,
+ settings.HTTP_MAX_RETRIES,
+ exc,
+ )
if attempt == settings.HTTP_MAX_RETRIES:
raise
else:
data = []
papers = data[:top_n]
- logger.info("Fetched %d papers for %s (raw=%d)", len(papers), target_date, len(data))
+ logger.info(
+ "Fetched %d papers for %s (raw=%d)", len(papers), target_date, len(data)
+ )
return papers
@@ -75,8 +84,14 @@ def _parse_paper(item: dict) -> dict:
"hf_url": f"https://huggingface.co/papers/{arxiv_id}" if arxiv_id else "",
"arxiv_url": f"https://arxiv.org/abs/{arxiv_id}" if arxiv_id else "",
"pdf_url": f"https://arxiv.org/pdf/{arxiv_id}.pdf" if arxiv_id else "",
- "authors": [a.get("name", a) if isinstance(a, dict) else a for a in paper_info.get("authors", [])],
- "tags": [t.get("name", t) if isinstance(t, dict) else t for t in (paper_info.get("tags") or [])],
+ "authors": [
+ a.get("name", a) if isinstance(a, dict) else a
+ for a in paper_info.get("authors", [])
+ ],
+ "tags": [
+ t.get("name", t) if isinstance(t, dict) else t
+ for t in (paper_info.get("tags") or [])
+ ],
}
@@ -133,15 +148,25 @@ def upsert_papers(db: Session, papers_raw: list[dict], paper_date: str) -> list[
"INSERT INTO papers_fts(rowid, title_en, abstract, authors, tags) "
"VALUES (:id, :title, :abstract, :authors, :tags)"
),
- {"id": paper.id, "title": meta["title_en"], "abstract": meta["abstract"] or "",
- "authors": authors_text, "tags": tags_text},
+ {
+ "id": paper.id,
+ "title": meta["title_en"],
+ "abstract": meta["abstract"] or "",
+ "authors": authors_text,
+ "tags": tags_text,
+ },
)
new_papers.append(paper)
logger.debug("Inserted new paper: %s", arxiv_id)
db.commit()
- logger.info("Upserted %d papers (%d new) for %s", len(papers_raw), len(new_papers), paper_date)
+ logger.info(
+ "Upserted %d papers (%d new) for %s",
+ len(papers_raw),
+ len(new_papers),
+ paper_date,
+ )
return new_papers
@@ -165,7 +190,12 @@ async def crawl_daily(db: Session, target_date: str, top_n: int | None = None) -
log_entry.papers_new = len(new_papers)
log_entry.completed_at = datetime.now(timezone.utc)
db.commit()
- return {"found": len(raw_papers), "new": len(new_papers), "status": "success", "error": None}
+ return {
+ "found": len(raw_papers),
+ "new": len(new_papers),
+ "status": "success",
+ "error": None,
+ }
except Exception as exc:
logger.exception("Crawl failed for %s", target_date)
log_entry.status = "failed"
diff --git a/app/services/embedder.py b/app/services/embedder.py
index 015a288..698931b 100644
--- a/app/services/embedder.py
+++ b/app/services/embedder.py
@@ -50,7 +50,9 @@ class ChromaManager:
"""获取或创建 papers_embeddings collection。"""
try:
col = self._client.get_collection("papers_embeddings")
- logger.info("ChromaDB collection 'papers_embeddings' loaded, count=%d", col.count())
+ logger.info(
+ "ChromaDB collection 'papers_embeddings' loaded, count=%d", col.count()
+ )
return col
except Exception:
pass
@@ -228,7 +230,9 @@ def index_paper(paper_id: str, texts_dict: dict | None = None) -> bool:
col.upsert(
ids=[arxiv_id],
embeddings=[vec],
- metadatas=[{"arxiv_id": arxiv_id, "title_zh": title_zh, "paper_date": paper_date}],
+ metadatas=[
+ {"arxiv_id": arxiv_id, "title_zh": title_zh, "paper_date": paper_date}
+ ],
)
logger.info("Indexed paper %s in ChromaDB", arxiv_id)
return True
@@ -262,7 +266,9 @@ def index_batch(paper_ids: list[str]) -> dict:
else:
failed += 1
- logger.info("Batch index: total=%d success=%d failed=%d", len(paper_ids), success, failed)
+ logger.info(
+ "Batch index: total=%d success=%d failed=%d", len(paper_ids), success, failed
+ )
return {"total": len(paper_ids), "success": success, "failed": failed}
diff --git a/app/services/pdf_downloader.py b/app/services/pdf_downloader.py
index cfb0d5a..6db50d0 100644
--- a/app/services/pdf_downloader.py
+++ b/app/services/pdf_downloader.py
@@ -78,6 +78,7 @@ async def download_source_zip(arxiv_id: str, source_url: str, dest_dir: Path) ->
except zipfile.BadZipFile:
# 可能是 tar.gz
import tarfile
+
try:
with tarfile.open(zip_path, "r:*") as tf:
tf.extractall(dest_dir, filter="data")
diff --git a/app/services/pi_client.py b/app/services/pi_client.py
index b7aae2b..b51b576 100644
--- a/app/services/pi_client.py
+++ b/app/services/pi_client.py
@@ -53,7 +53,9 @@ def write_meta_json(paper) -> Path:
"tags": tags,
"upvotes": paper.upvotes,
}
- meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
+ meta_path.write_text(
+ json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8"
+ )
return meta_path
@@ -88,9 +90,7 @@ async def call_pi(meta_path: Path, pdf_path: Path) -> str:
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
- raise PiTimeoutError(
- f"pi timed out after {settings.SUMMARY_TIMEOUT_SECONDS}s"
- )
+ raise PiTimeoutError(f"pi timed out after {settings.SUMMARY_TIMEOUT_SECONDS}s")
if proc.returncode != 0:
raise PiProcessError(proc.returncode, stderr.decode("utf-8", errors="replace"))
diff --git a/app/services/scheduler.py b/app/services/scheduler.py
index 9f80c7b..f0915ff 100644
--- a/app/services/scheduler.py
+++ b/app/services/scheduler.py
@@ -132,18 +132,26 @@ async def _daily_pipeline() -> None:
# Step 1: 抓取
logger.info("Scheduler pipeline: crawl %s", today)
crawl_result = await crawl_daily(db, today)
- logger.info("Scheduler pipeline: crawl done, found=%d new=%d",
- crawl_result.get("found", 0), crawl_result.get("new", 0))
+ logger.info(
+ "Scheduler pipeline: crawl done, found=%d new=%d",
+ crawl_result.get("found", 0),
+ crawl_result.get("new", 0),
+ )
# Step 2: 总结 pending 论文
logger.info("Scheduler pipeline: summarize batch")
summarize_result = await summarize_batch(db)
- logger.info("Scheduler pipeline: summarize done, result=%s", summarize_result)
+ logger.info(
+ "Scheduler pipeline: summarize done, result=%s", summarize_result
+ )
# Step 3: 清理临时文件
logger.info("Scheduler pipeline: cleanup tmp")
cleanup_result = cleanup_tmp()
- logger.info("Scheduler pipeline: cleanup done, removed=%d", cleanup_result.get("removed", 0))
+ logger.info(
+ "Scheduler pipeline: cleanup done, removed=%d",
+ cleanup_result.get("removed", 0),
+ )
log_entry.status = "success"
diff --git a/app/services/schemas.py b/app/services/schemas.py
index 2b2838b..9dd8fd3 100644
--- a/app/services/schemas.py
+++ b/app/services/schemas.py
@@ -132,7 +132,9 @@ def flatten_for_db(schema: SummarySchema) -> dict:
return {
"one_line": schema.one_line,
"difficulty": schema.difficulty,
- "prerequisites_json": json.dumps(schema.prerequisites.model_dump(), ensure_ascii=False),
+ "prerequisites_json": json.dumps(
+ schema.prerequisites.model_dump(), ensure_ascii=False
+ ),
"motivation_problem": schema.motivation.problem,
"motivation_goal": schema.motivation.goal,
"motivation_gap": schema.motivation.gap,
@@ -140,11 +142,19 @@ def flatten_for_db(schema: SummarySchema) -> dict:
"method_key_idea": schema.method.key_idea,
"method_steps_json": json.dumps(schema.method.steps, ensure_ascii=False),
"method_novelty": schema.method.novelty,
- "results_main_json": json.dumps(schema.results.main_findings, ensure_ascii=False),
- "results_benchmarks_json": json.dumps(schema.results.benchmarks, ensure_ascii=False),
+ "results_main_json": json.dumps(
+ schema.results.main_findings, ensure_ascii=False
+ ),
+ "results_benchmarks_json": json.dumps(
+ schema.results.benchmarks, ensure_ascii=False
+ ),
"limitations_json": json.dumps(schema.results.limitations, ensure_ascii=False),
- "weaknesses_json": json.dumps(schema.improvements.weaknesses, ensure_ascii=False),
- "future_work_json": json.dumps(schema.improvements.future_work, ensure_ascii=False),
+ "weaknesses_json": json.dumps(
+ schema.improvements.weaknesses, ensure_ascii=False
+ ),
+ "future_work_json": json.dumps(
+ schema.improvements.future_work, ensure_ascii=False
+ ),
"reproducibility": schema.improvements.reproducibility,
"full_json": schema.model_dump_json(ensure_ascii=False),
"updated_at": datetime.now(timezone.utc),
diff --git a/app/services/searcher.py b/app/services/searcher.py
index 9dd88a1..7cf1385 100644
--- a/app/services/searcher.py
+++ b/app/services/searcher.py
@@ -90,12 +90,24 @@ def search_papers(
if match_expr:
return _search_with_fts(
- db, match_expr, tag_join, tag_where, tag_params,
- sort, page, page_size, offset,
+ db,
+ match_expr,
+ tag_join,
+ tag_where,
+ tag_params,
+ sort,
+ page,
+ page_size,
+ offset,
)
else:
return _search_tag_only(
- db, tag, sort, page, page_size, offset,
+ db,
+ tag,
+ sort,
+ page,
+ page_size,
+ offset,
)
@@ -114,7 +126,11 @@ def _search_with_fts(
params = {"query": match_expr, "limit": page_size, "offset": offset}
params.update(tag_params)
- order = "bm25(papers_fts)" if sort == "relevance" else "p.paper_date DESC, p.upvotes DESC"
+ order = (
+ "bm25(papers_fts)"
+ if sort == "relevance"
+ else "p.paper_date DESC, p.upvotes DESC"
+ )
# ── 主查询:取 ID + rank + snippet ──
rows_sql = text(f"""
@@ -145,12 +161,11 @@ def _search_with_fts(
total = db.execute(count_sql, params).scalar() or 0
paper_ids = [row[0] for row in fts_rows]
- snippets = {
- row[0]: {"title_zh": row[2], "abstract": row[3]}
- for row in fts_rows
- }
+ snippets = {row[0]: {"title_zh": row[2], "abstract": row[3]} for row in fts_rows}
- papers = _load_papers_by_ids(db, paper_ids, sort, {row[0]: row[1] for row in fts_rows})
+ papers = _load_papers_by_ids(
+ db, paper_ids, sort, {row[0]: row[1] for row in fts_rows}
+ )
return {
"results": papers,
@@ -188,7 +203,10 @@ def _search_semantic(
"JOIN paper_tags pt ON pt.paper_id = p.id" if tag else "",
"AND pt.tag = :tag" if tag else "",
{"tag": tag} if tag else {},
- sort, page, page_size, (page - 1) * page_size,
+ sort,
+ page,
+ page_size,
+ (page - 1) * page_size,
)
# 按 arxiv_id 从 DB 加载完整数据
@@ -218,7 +236,7 @@ def _search_semantic(
# 分页
total = len(papers)
start = (page - 1) * page_size
- page_papers = papers[start:start + page_size]
+ page_papers = papers[start : start + page_size]
return {
"results": page_papers,
@@ -239,7 +257,11 @@ def _search_tag_only(
offset: int,
) -> dict:
"""只有标签筛选,无关键词。"""
- order = "p.paper_date DESC, p.upvotes DESC" if sort == "date" else "p.paper_date DESC, p.upvotes DESC"
+ order = (
+ "p.paper_date DESC, p.upvotes DESC"
+ if sort == "date"
+ else "p.paper_date DESC, p.upvotes DESC"
+ )
rows_sql = text(f"""
SELECT p.id
@@ -249,7 +271,9 @@ def _search_tag_only(
ORDER BY {order}
LIMIT :limit OFFSET :offset
""")
- rows = db.execute(rows_sql, {"tag": tag, "limit": page_size, "offset": offset}).fetchall()
+ rows = db.execute(
+ rows_sql, {"tag": tag, "limit": page_size, "offset": offset}
+ ).fetchall()
count_sql = text("""
SELECT COUNT(DISTINCT p.id)
diff --git a/app/services/summarizer.py b/app/services/summarizer.py
index b3450d8..94d70d5 100644
--- a/app/services/summarizer.py
+++ b/app/services/summarizer.py
@@ -191,7 +191,11 @@ async def summarize_one(
# 跳过 permanent_failure(除非 force)
if status.status == "permanent_failure" and not force:
- return {"arxiv_id": arxiv_id, "status": "skipped", "reason": "permanent_failure"}
+ return {
+ "arxiv_id": arxiv_id,
+ "status": "skipped",
+ "reason": "permanent_failure",
+ }
if semaphore:
await semaphore.acquire()
@@ -270,7 +274,9 @@ async def _do_summarize_one(db: Session, paper: Paper) -> dict:
}
index_paper(arxiv_id, texts_dict)
except Exception:
- logger.warning("Failed to index paper %s in ChromaDB", arxiv_id, exc_info=True)
+ logger.warning(
+ "Failed to index paper %s in ChromaDB", arxiv_id, exc_info=True
+ )
logger.info("Summarize done: %s quality=%s", arxiv_id, quality)
return {"arxiv_id": arxiv_id, "status": "done", "quality": quality}
@@ -430,7 +436,13 @@ async def summarize_batch(
log_entry.papers_new = 0
log_entry.completed_at = datetime.now(timezone.utc)
release_lock(db, lock)
- return {"status": "success", "done": 0, "failed": 0, "skipped": 0, "total": 0}
+ return {
+ "status": "success",
+ "done": 0,
+ "failed": 0,
+ "skipped": 0,
+ "total": 0,
+ }
# 并发控制
semaphore = asyncio.Semaphore(settings.SUMMARY_CONCURRENCY)
@@ -482,7 +494,10 @@ async def summarize_batch(
logger.info(
"Summarize batch done: total=%d done=%d failed=%d skipped=%d",
- total, done, failed, skipped,
+ total,
+ done,
+ failed,
+ skipped,
)
return {
"status": "success" if failed == 0 else "partial",
diff --git a/app/services/trends.py b/app/services/trends.py
index 51e7824..e460092 100644
--- a/app/services/trends.py
+++ b/app/services/trends.py
@@ -13,33 +13,33 @@ def get_trends_data(db: Session) -> dict:
thirty_days_ago = (date.today() - timedelta(days=30)).isoformat()
# 1. 按日论文数量(近 30 天)
- daily_rows = db.execute(text("""
+ daily_rows = db.execute(
+ text("""
SELECT paper_date, COUNT(*) as cnt
FROM papers
WHERE paper_date >= :start_date
GROUP BY paper_date
ORDER BY paper_date ASC
- """), {"start_date": thirty_days_ago}).fetchall()
- daily_counts = [
- {"date": str(row[0]), "count": row[1]}
- for row in daily_rows
- ]
+ """),
+ {"start_date": thirty_days_ago},
+ ).fetchall()
+ daily_counts = [{"date": str(row[0]), "count": row[1]} for row in daily_rows]
# 2. 热门标签 Top 20
- tag_rows = db.execute(text("""
+ tag_rows = db.execute(
+ text("""
SELECT tag, COUNT(*) as cnt
FROM paper_tags
GROUP BY tag
ORDER BY cnt DESC
LIMIT 20
- """)).fetchall()
- top_tags = [
- {"tag": row[0], "count": row[1]}
- for row in tag_rows
- ]
+ """)
+ ).fetchall()
+ top_tags = [{"tag": row[0], "count": row[1]} for row in tag_rows]
# 3. Upvotes 分布
- upvote_rows = db.execute(text("""
+ upvote_rows = db.execute(
+ text("""
SELECT
CASE
WHEN upvotes >= 100 THEN '100+'
@@ -53,25 +53,22 @@ def get_trends_data(db: Session) -> dict:
FROM papers
GROUP BY bucket
ORDER BY MIN(upvotes) DESC
- """)).fetchall()
- upvotes_dist = [
- {"range": row[0], "count": row[1]}
- for row in upvote_rows
- ]
+ """)
+ ).fetchall()
+ upvotes_dist = [{"range": row[0], "count": row[1]} for row in upvote_rows]
# 4. 总结完成率
- summary_rows = db.execute(text("""
+ summary_rows = db.execute(
+ text("""
SELECT
COALESCE(ss.status, 'none') as status,
COUNT(*) as cnt
FROM papers p
LEFT JOIN summary_status ss ON ss.paper_id = p.id
GROUP BY status
- """)).fetchall()
- summary_completion = [
- {"status": row[0], "count": row[1]}
- for row in summary_rows
- ]
+ """)
+ ).fetchall()
+ summary_completion = [{"status": row[0], "count": row[1]} for row in summary_rows]
return {
"daily_counts": daily_counts,
diff --git a/app/static/css/style.css b/app/static/css/style.css
index a8f85d1..5c16b04 100644
--- a/app/static/css/style.css
+++ b/app/static/css/style.css
@@ -14,7 +14,13 @@
--max-width: 960px;
}
-*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
body {
font-family: var(--font-sans);
@@ -24,8 +30,14 @@ body {
-webkit-font-smoothing: antialiased;
}
-a { color: var(--accent); text-decoration: none; }
-a:hover { color: var(--accent-hover); text-decoration: underline; }
+a {
+ color: var(--accent);
+ text-decoration: none;
+}
+a:hover {
+ color: var(--accent-hover);
+ text-decoration: underline;
+}
/* ── Header ─────────────────────────────────────────────────────── */
.site-header {
@@ -52,9 +64,18 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
color: var(--ink);
}
-.nav-links { display: flex; gap: 16px; margin-left: auto; }
-.nav-links a { font-size: 0.9rem; color: var(--ink-light); }
-.nav-links a:hover { color: var(--accent); }
+.nav-links {
+ display: flex;
+ gap: 16px;
+ margin-left: auto;
+}
+.nav-links a {
+ font-size: 0.9rem;
+ color: var(--ink-light);
+}
+.nav-links a:hover {
+ color: var(--accent);
+}
/* ── Container ──────────────────────────────────────────────────── */
.container {
@@ -88,7 +109,11 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
color: var(--ink-light);
transition: all 0.2s;
}
-.date-nav-btn:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
+.date-nav-btn:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+ text-decoration: none;
+}
/* ── Date Chips ─────────────────────────────────────────────────── */
.date-quick-nav {
@@ -111,11 +136,23 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
font-size: 0.8rem;
color: var(--ink-light);
}
-.date-chip:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
-.date-chip.active { background: var(--accent); color: #fff; border-color: var(--accent); }
+.date-chip:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+ text-decoration: none;
+}
+.date-chip.active {
+ background: var(--accent);
+ color: #fff;
+ border-color: var(--accent);
+}
/* ── Paper Card ─────────────────────────────────────────────────── */
-.paper-list { display: flex; flex-direction: column; gap: 16px; }
+.paper-list {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
.paper-card {
background: var(--surface);
@@ -124,7 +161,9 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
padding: 20px 24px;
transition: box-shadow 0.2s;
}
-.paper-card:hover { box-shadow: 0 2px 12px var(--shadow); }
+.paper-card:hover {
+ box-shadow: 0 2px 12px var(--shadow);
+}
.paper-card-header {
display: flex;
@@ -140,8 +179,12 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
line-height: 1.5;
flex: 1;
}
-.paper-title a { color: var(--ink); }
-.paper-title a:hover { color: var(--accent); }
+.paper-title a {
+ color: var(--ink);
+}
+.paper-title a:hover {
+ color: var(--accent);
+}
.paper-upvotes {
font-size: 0.85rem;
@@ -149,7 +192,8 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
white-space: nowrap;
}
-.paper-one-line, .paper-abstract-preview {
+.paper-one-line,
+.paper-abstract-preview {
margin-top: 8px;
color: var(--ink-light);
font-size: 0.92rem;
@@ -191,11 +235,27 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
padding: 2px 8px;
border-radius: 3px;
}
-.summary-none { background: #f0f0f0; color: #888; }
-.summary-pending { background: #fff3e0; color: #e67e22; }
-.summary-processing { background: #e3f2fd; color: #1976d2; }
-.summary-done { background: #e8f5e9; color: #388e3c; }
-.summary-failed, .summary-permanent_failure { background: #fce4ec; color: #c62828; }
+.summary-none {
+ background: #f0f0f0;
+ color: #888;
+}
+.summary-pending {
+ background: #fff3e0;
+ color: #e67e22;
+}
+.summary-processing {
+ background: #e3f2fd;
+ color: #1976d2;
+}
+.summary-done {
+ background: #e8f5e9;
+ color: #388e3c;
+}
+.summary-failed,
+.summary-permanent_failure {
+ background: #fce4ec;
+ color: #c62828;
+}
.btn-detail {
font-size: 0.85rem;
@@ -209,11 +269,19 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
padding: 60px 20px;
color: var(--ink-light);
}
-.empty-state p:first-child { font-size: 1.2rem; }
-.hint { font-size: 0.85rem; margin-top: 8px; }
+.empty-state p:first-child {
+ font-size: 1.2rem;
+}
+.hint {
+ font-size: 0.85rem;
+ margin-top: 8px;
+}
/* ── Paper Detail ───────────────────────────────────────────────── */
-.paper-detail { max-width: 780px; margin: 0 auto; }
+.paper-detail {
+ max-width: 780px;
+ margin: 0 auto;
+}
.back-link {
display: inline-block;
@@ -246,7 +314,12 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
margin-bottom: 12px;
}
-.detail-tags { margin-bottom: 12px; display: flex; gap: 6px; flex-wrap: wrap; }
+.detail-tags {
+ margin-bottom: 12px;
+ display: flex;
+ gap: 6px;
+ flex-wrap: wrap;
+}
.detail-links {
display: flex;
@@ -261,7 +334,11 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
font-size: 0.85rem;
color: var(--ink-light);
}
-.ext-link:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
+.ext-link:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+ text-decoration: none;
+}
/* ── Summary Sections ───────────────────────────────────────────── */
.summary-section {
@@ -291,8 +368,14 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
line-height: 1.6;
}
-.abstract-section { background: #faf8f5; }
-.abstract-en { font-size: 0.9rem; color: var(--ink-light); font-style: italic; }
+.abstract-section {
+ background: #faf8f5;
+}
+.abstract-en {
+ font-size: 0.9rem;
+ color: var(--ink-light);
+ font-style: italic;
+}
/* ── Summary Placeholders ───────────────────────────────────────── */
.summary-placeholder {
@@ -301,10 +384,20 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
border-radius: var(--radius);
margin-bottom: 24px;
}
-.summary-placeholder.processing { background: #e3f2fd; }
-.summary-placeholder.failed { background: #fce4ec; }
-.summary-placeholder.none { background: #f5f5f5; }
-.error-detail { font-size: 0.85rem; color: #c62828; margin-top: 8px; }
+.summary-placeholder.processing {
+ background: #e3f2fd;
+}
+.summary-placeholder.failed {
+ background: #fce4ec;
+}
+.summary-placeholder.none {
+ background: #f5f5f5;
+}
+.error-detail {
+ font-size: 0.85rem;
+ color: #c62828;
+ margin-top: 8px;
+}
.quality-warning {
padding: 10px 16px;
@@ -384,7 +477,9 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
cursor: pointer;
transition: background 0.2s;
}
-.search-btn:hover { background: var(--accent-hover); }
+.search-btn:hover {
+ background: var(--accent-hover);
+}
/* ── Tag Filter ─────────────────────────────────────────────────── */
.tag-filter {
@@ -407,8 +502,16 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
font-size: 0.8rem;
color: var(--ink-light);
}
-.tag-chip:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
-.tag-chip.active { background: var(--accent); color: #fff; border-color: var(--accent); }
+.tag-chip:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+ text-decoration: none;
+}
+.tag-chip.active {
+ background: var(--accent);
+ color: #fff;
+ border-color: var(--accent);
+}
/* ── Search Meta & Sort ─────────────────────────────────────────── */
.search-meta {
@@ -423,9 +526,18 @@ a:hover { color: var(--accent-hover); text-decoration: underline; }
color: var(--ink-light);
font-size: 0.85rem;
}
-.sort-toggle a.active { color: var(--accent); font-weight: 600; }
-.sort-toggle a:hover { color: var(--accent); text-decoration: none; }
-.sort-divider { color: var(--border); margin: 0 4px; }
+.sort-toggle a.active {
+ color: var(--accent);
+ font-weight: 600;
+}
+.sort-toggle a:hover {
+ color: var(--accent);
+ text-decoration: none;
+}
+.sort-divider {
+ color: var(--border);
+ margin: 0 4px;
+}
/* ── Search Highlight ───────────────────────────────────────────── */
mark {
@@ -464,7 +576,11 @@ mark {
font-size: 0.85rem;
color: var(--ink-light);
}
-.page-btn:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
+.page-btn:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+ text-decoration: none;
+}
.page-info {
font-size: 0.85rem;
color: var(--ink-light);
@@ -494,8 +610,16 @@ mark {
font-size: 0.85rem;
color: var(--ink-light);
}
-.filter-chip:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
-.filter-chip.active { background: var(--accent); color: #fff; border-color: var(--accent); }
+.filter-chip:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+ text-decoration: none;
+}
+.filter-chip.active {
+ background: var(--accent);
+ color: #fff;
+ border-color: var(--accent);
+}
/* ── Paper Card Footer (enhanced) ──────────────────────────────── */
.paper-footer {
@@ -528,8 +652,12 @@ mark {
transition: color 0.2s;
line-height: 1;
}
-.btn-bookmark:hover { color: var(--accent); }
-.btn-bookmark.active { color: #f0a500; }
+.btn-bookmark:hover {
+ color: var(--accent);
+}
+.btn-bookmark.active {
+ color: #f0a500;
+}
/* ── Reading Badge ──────────────────────────────────────────────── */
.reading-badge {
@@ -537,24 +665,61 @@ mark {
padding: 2px 6px;
border-radius: 3px;
}
-.reading-unread { background: #f0f0f0; color: #888; }
-.reading-skimmed { background: #e3f2fd; color: #1976d2; }
-.reading-read_summary { background: #e8f5e9; color: #388e3c; }
-.reading-read_full { background: #e8f5e9; color: #2e7d32; font-weight: 500; }
+.reading-unread {
+ background: #f0f0f0;
+ color: #888;
+}
+.reading-skimmed {
+ background: #e3f2fd;
+ color: #1976d2;
+}
+.reading-read_summary {
+ background: #e8f5e9;
+ color: #388e3c;
+}
+.reading-read_full {
+ background: #e8f5e9;
+ color: #2e7d32;
+ font-weight: 500;
+}
/* ── Responsive ─────────────────────────────────────────────────── */
@media (max-width: 640px) {
- .container { padding: 16px; }
- .nav-bar { padding: 10px 16px; }
- .nav-search-input { width: 120px; }
- .date-nav { gap: 8px; }
- .date-title { font-size: 1.2rem; }
- .paper-card { padding: 14px 16px; }
- .detail-title { font-size: 1.3rem; }
- .detail-meta { flex-direction: column; gap: 4px; }
- .search-form { flex-direction: column; }
- .reading-list-filters { gap: 4px; }
- .filter-chip { padding: 4px 10px; font-size: 0.8rem; }
+ .container {
+ padding: 16px;
+ }
+ .nav-bar {
+ padding: 10px 16px;
+ }
+ .nav-search-input {
+ width: 120px;
+ }
+ .date-nav {
+ gap: 8px;
+ }
+ .date-title {
+ font-size: 1.2rem;
+ }
+ .paper-card {
+ padding: 14px 16px;
+ }
+ .detail-title {
+ font-size: 1.3rem;
+ }
+ .detail-meta {
+ flex-direction: column;
+ gap: 4px;
+ }
+ .search-form {
+ flex-direction: column;
+ }
+ .reading-list-filters {
+ gap: 4px;
+ }
+ .filter-chip {
+ padding: 4px 10px;
+ font-size: 0.8rem;
+ }
}
/* ── Search Mode Toggle (Phase 5) ─────────────────────────────── */
@@ -575,8 +740,12 @@ mark {
align-items: center;
gap: 4px;
}
-.mode-option input[type="radio"] { display: none; }
-.mode-option:hover { background: var(--bg); }
+.mode-option input[type="radio"] {
+ display: none;
+}
+.mode-option:hover {
+ background: var(--bg);
+}
.mode-option.active {
background: var(--accent);
color: #fff;
@@ -612,12 +781,16 @@ mark {
padding: 10px 0;
border-bottom: 1px solid var(--border);
}
-.similar-paper-item:last-child { border-bottom: none; }
+.similar-paper-item:last-child {
+ border-bottom: none;
+}
.similar-paper-title a {
font-size: 0.92rem;
color: var(--ink);
}
-.similar-paper-title a:hover { color: var(--accent); }
+.similar-paper-title a:hover {
+ color: var(--accent);
+}
.similar-paper-dist {
font-size: 0.8rem;
color: var(--ink-light);
@@ -654,7 +827,9 @@ mark {
max-height: 300px;
}
@media (max-width: 768px) {
- .charts-grid { grid-template-columns: 1fr; }
+ .charts-grid {
+ grid-template-columns: 1fr;
+ }
}
/* ── Compare Page (Phase 5) ────────────────────────────────────── */
diff --git a/app/templates/admin_logs.html b/app/templates/admin_logs.html
index ce14513..805ee8b 100644
--- a/app/templates/admin_logs.html
+++ b/app/templates/admin_logs.html
@@ -1,8 +1,5 @@
-{% extends "base.html" %}
-
-{% block title %}管理日志 — HF Daily Papers{% endblock %}
-
-{% block content %}
+{% extends "base.html" %} {% block title %}管理日志 — HF Daily Papers{% endblock
+%} {% block content %}
📋 管理日志
@@ -34,21 +31,31 @@
{% for log in crawl_logs %}
{{ log.id }}
- {{ log.task }}
+
+ {{ log.task }}
+
- {% if log.status == 'success' %}✓ 成功
- {% elif log.status == 'running' %}⟳ 运行中
- {% elif log.status == 'failed' %}✗ 失败
- {% else %}{{ log.status }}{% endif %}
+ {% if log.status == 'success' %}✓ 成功 {% elif log.status ==
+ 'running' %}⟳ 运行中 {% elif log.status == 'failed' %}✗ 失败 {%
+ else %}{{ log.status }}{% endif %}
{{ log.date or '-' }}
{{ log.papers_found or 0 }}
{{ log.papers_new or 0 }}
- {{ log.started_at.strftime('%m-%d %H:%M') if log.started_at else '-' }}
- {{ log.completed_at.strftime('%m-%d %H:%M') if log.completed_at else '-' }}
- {{ log.error[:80] + '...' if log.error and log.error|length > 80 else (log.error or '-') }}
+
+ {{ log.started_at.strftime('%m-%d %H:%M') if log.started_at else
+ '-' }}
+
+
+ {{ log.completed_at.strftime('%m-%d %H:%M') if log.completed_at
+ else '-' }}
+
+
+ {{ log.error[:80] + '...' if log.error and log.error|length > 80
+ else (log.error or '-') }}
+
{% endfor %}
@@ -90,15 +97,23 @@
{{ job.paper_count or 0 }}
- {% if job.status == 'success' %}✓ 成功
- {% elif job.status == 'running' %}⟳ 运行中
- {% elif job.status == 'failed' %}✗ 失败
- {% else %}{{ job.status }}{% endif %}
+ {% if job.status == 'success' %}✓ 成功 {% elif job.status ==
+ 'running' %}⟳ 运行中 {% elif job.status == 'failed' %}✗ 失败 {%
+ else %}{{ job.status }}{% endif %}
-
{{ job.started_at.strftime('%m-%d %H:%M') if job.started_at else '-' }}
-
{{ job.completed_at.strftime('%m-%d %H:%M') if job.completed_at else '-' }}
-
{{ job.error[:80] + '...' if job.error and job.error|length > 80 else (job.error or '-') }}
+
+ {{ job.started_at.strftime('%m-%d %H:%M') if job.started_at else
+ '-' }}
+
+
+ {{ job.completed_at.strftime('%m-%d %H:%M') if job.completed_at
+ else '-' }}
+
+
+ {{ job.error[:80] + '...' if job.error and job.error|length > 80
+ else (job.error or '-') }}
+
{% endfor %}
@@ -116,16 +131,24 @@
管理操作
- 🔄 抓取今天
- 📝 批量总结
- 🧹 清理临时文件
+
+ 🔄 抓取今天
+
+
+ 📝 批量总结
+
+
+ 🧹 清理临时文件
+
-{% endblock %}
-
-{% block scripts %}
+{% endblock %} {% block scripts %}
{% endblock %}
diff --git a/app/templates/base.html b/app/templates/base.html
index 6e272f9..aab7fe7 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -1,38 +1,44 @@
-
+
-
-
-
- {% block title %}HF Daily Papers{% endblock %}
-
-
-
-
+
+
+
+ {% block title %}HF Daily Papers{% endblock %}
+
+
+
+
-
- {% block content %}{% endblock %}
-
+ {% block content %}{% endblock %}
-
+
-
-
- {% block scripts %}{% endblock %}
-
+
+
+ {% block scripts %}{% endblock %}
+
diff --git a/app/templates/compare.html b/app/templates/compare.html
index c59a934..0d9f6d0 100644
--- a/app/templates/compare.html
+++ b/app/templates/compare.html
@@ -1,16 +1,17 @@
-{% extends "base.html" %}
-
-{% block title %}{{ page_title }} — HF Daily Papers{% endblock %}
-
-{% block content %}
+{% extends "base.html" %} {% block title %}{{ page_title }} — HF Daily Papers{%
+endblock %} {% block content %}
论文对比
{# ID 输入表单 #}
@@ -18,9 +19,7 @@
- {% endif %}
-
- {% if papers %}
+ {% endif %} {% if papers %}
@@ -29,8 +28,8 @@
{% for paper in papers %}
{{ paper.arxiv_id }}
-
-
+
+
{{ paper.upvotes }} 👍 · {{ paper.paper_date }}
@@ -42,7 +41,9 @@
作者
{% for paper in papers %}
- {{ paper.authors|map(attribute='name')|join(', ') }}
+
+ {{ paper.authors|map(attribute='name')|join(', ') }}
+
{% endfor %}
@@ -58,16 +59,13 @@
{% endfor %}
- {# 结构化对比字段 #}
- {% for row in rows %}
+ {# 结构化对比字段 #} {% for row in rows %}
{{ row.label }}
{% for cell in row.cells %}
- {% if cell %}
- {{ cell }}
- {% else %}
- 暂无总结
+ {% if cell %} {{ cell }} {% else %}
+ 暂无总结
{% endif %}
{% endfor %}
diff --git a/app/templates/detail.html b/app/templates/detail.html
index 3979d6d..255ae99 100644
--- a/app/templates/detail.html
+++ b/app/templates/detail.html
@@ -1,140 +1,141 @@
-{% extends "base.html" %}
-
-{% block title %}{{ page_title }} — HF Daily Papers{% endblock %}
-
-{% block content %}
+{% extends "base.html" %} {% block title %}{{ page_title }} — HF Daily Papers{%
+endblock %} {% block content %}
- ← 返回 {{ paper.paper_date.isoformat() }}
+ ← 返回 {{ paper.paper_date.isoformat() }}
{# 标题 #}
- {{ paper.title_zh or paper.title_en }}
- {% if paper.title_zh and paper.title_en != paper.title_zh %}
+ {{ paper.title_zh or paper.title_en }} {% if paper.title_zh and
+ paper.title_en != paper.title_zh %}
{{ paper.title_en }}
{% endif %}
{# 元信息 #}
- {{ paper.authors|map(attribute='name')|join(', ') }}
- 📅 {{ paper.published_at or paper.paper_date }}
+ {{ paper.authors|map(attribute='name')|join(', ') }}
+ 📅 {{ paper.published_at or paper.paper_date }}
👍 {{ paper.upvotes }}
- {# 标签 #}
- {% if paper.tags %}
+ {# 标签 #} {% if paper.tags %}
{% for tag in paper.tags %}
{{ tag.tag }}
{% endfor %}
- {% endif %}
-
- {# 链接 #}
+ {% endif %} {# 链接 #}
- {% if paper.arxiv_url %}
arXiv {% endif %}
- {% if paper.hf_url %}
HuggingFace {% endif %}
- {% if paper.pdf_url %}
PDF {% endif %}
+ {% if paper.arxiv_url %}
arXiv {% endif %} {% if paper.hf_url %}
HuggingFace {% endif %} {% if paper.pdf_url %}
PDF {% endif %}
- {# 总结内容 — 按状态降级 #}
- {% if summary_state == 'done' and paper.summary %}
- {% if paper.summary_status and paper.summary_status.quality == 'low' %}
- ⚠️ AI 总结质量较低,仅供参考
- {% elif paper.summary_status and paper.summary_status.quality == 'degraded' %}
- 📝 总结部分字段不完整
- {% endif %}
-
- {% if paper.summary.one_line %}
-
- 一句话摘要
- {{ paper.summary.one_line }}
-
- {% endif %}
-
- {% if paper.summary.difficulty %}
-
- 难度
- {{ paper.summary.difficulty }}
-
- {% endif %}
-
+ {# 总结内容 — 按状态降级 #} {% if summary_state == 'done' and paper.summary %}
+ {% if paper.summary_status and paper.summary_status.quality == 'low' %}
+ ⚠️ AI 总结质量较低,仅供参考
+ {% elif paper.summary_status and paper.summary_status.quality == 'degraded' %}
+ 📝 总结部分字段不完整
+ {% endif %} {% if paper.summary.one_line %}
+
+ 一句话摘要
+ {{ paper.summary.one_line }}
+
+ {% endif %} {% if paper.summary.difficulty %}
+
+ 难度
+ {{ paper.summary.difficulty }}
+
+ {% endif %} {% if paper.summary.motivation_problem %}
+
+ 研究动机
{% if paper.summary.motivation_problem %}
-
- 研究动机
- {% if paper.summary.motivation_problem %}问题: {{ paper.summary.motivation_problem }}
{% endif %}
- {% if paper.summary.motivation_goal %}目标: {{ paper.summary.motivation_goal }}
{% endif %}
- {% if paper.summary.motivation_gap %}差距: {{ paper.summary.motivation_gap }}
{% endif %}
-
+ 问题: {{ paper.summary.motivation_problem }}
+ {% endif %} {% if paper.summary.motivation_goal %}
+ 目标: {{ paper.summary.motivation_goal }}
+ {% endif %} {% if paper.summary.motivation_gap %}
+ 差距: {{ paper.summary.motivation_gap }}
{% endif %}
-
- {% if paper.summary.method_key_idea %}
-
- 核心方法
- {% if paper.summary.method_overview %}{{ paper.summary.method_overview }}
{% endif %}
- 关键思路: {{ paper.summary.method_key_idea }}
- {% if paper.summary.method_novelty %}新颖性: {{ paper.summary.method_novelty }}
{% endif %}
-
+
+ {% endif %} {% if paper.summary.method_key_idea %}
+
+ 核心方法
+ {% if paper.summary.method_overview %}
+ {{ paper.summary.method_overview }}
{% endif %}
-
- {% if paper.summary.results_main_json %}
-
- 实验结果
- {{ paper.summary.results_main_json }}
-
+ 关键思路: {{ paper.summary.method_key_idea }}
+ {% if paper.summary.method_novelty %}
+ 新颖性: {{ paper.summary.method_novelty }}
{% endif %}
-
- {% if paper.summary.limitations_json %}
-
- 局限与改进
- {{ paper.summary.limitations_json }}
-
- {% endif %}
-
- {% elif summary_state == 'processing' %}
-
+
+ {% endif %} {% if paper.summary.results_main_json %}
+
+ 实验结果
+ {{ paper.summary.results_main_json }}
+
+ {% endif %} {% if paper.summary.limitations_json %}
+
+ 局限与改进
+ {{ paper.summary.limitations_json }}
+
+ {% endif %} {% elif summary_state == 'processing' %}
+
{% elif summary_state in ('failed', 'permanent_failure') %}
-
-
❌ 总结生成失败{% if paper.summary_status and paper.summary_status.error_type %}({{ paper.summary_status.error_type }}){% endif %}
- {% if paper.summary_status and paper.summary_status.error %}
-
{{ paper.summary_status.error }}
- {% endif %}
-
+
+
+ ❌ 总结生成失败{% if paper.summary_status and
+ paper.summary_status.error_type %}({{ paper.summary_status.error_type
+ }}){% endif %}
+
+ {% if paper.summary_status and paper.summary_status.error %}
+
{{ paper.summary_status.error }}
+ {% endif %}
+
{% else %}
-
- {% endif %}
-
- {# 英文摘要 — 始终显示 #}
- {% if paper.abstract %}
+
+ {% endif %} {# 英文摘要 — 始终显示 #} {% if paper.abstract %}
Abstract
{{ paper.abstract }}
- {% endif %}
-
- {# Phase 5: 图片画廊 #}
- {% if paper_images %}
+ {% endif %} {# Phase 5: 图片画廊 #} {% if paper_images %}
论文图片
{% for img in paper_images %}
-
+
{{ img.name }}
{% endfor %}
- {% endif %}
-
- {# Phase 5: 相似论文推荐 #}
- {% if similar_papers %}
+ {% endif %} {# Phase 5: 相似论文推荐 #} {% if similar_papers %}
相似论文推荐
{% for sp in similar_papers %}
@@ -142,7 +143,9 @@
{{ sp.title_zh }}
- 🎯 {{ "%.3f"|format(sp.distance) }}
+ 🎯 {{ "%.3f"|format(sp.distance) }}
{% endfor %}
diff --git a/app/templates/index.html b/app/templates/index.html
index f659ed1..58089d0 100644
--- a/app/templates/index.html
+++ b/app/templates/index.html
@@ -1,8 +1,5 @@
-{% extends "base.html" %}
-
-{% block title %}{{ page_title }} — HF Daily Papers{% endblock %}
-
-{% block content %}
+{% extends "base.html" %} {% block title %}{{ page_title }} — HF Daily Papers{%
+endblock %} {% block content %}
{% if prev_day %}
← 前一天
@@ -16,9 +13,8 @@
{% if papers %}
- {% for paper in papers %}
- {% include "partials/paper_card.html" %}
- {% endfor %}
+ {% for paper in papers %} {% include "partials/paper_card.html" %} {% endfor
+ %}
{% else %}
@@ -30,7 +26,11 @@
有数据的日期:
{% for d in available_dates[:10] %}
-
{{ d }}
+
{{ d }}
{% endfor %}
{% endblock %}
diff --git a/app/templates/partials/paper_card.html b/app/templates/partials/paper_card.html
index 79cb8aa..08c53f1 100644
--- a/app/templates/partials/paper_card.html
+++ b/app/templates/partials/paper_card.html
@@ -12,7 +12,9 @@
{% if paper.summary and paper.summary.one_line %}
{{ paper.summary.one_line }}
{% elif paper.abstract %}
-
{{ paper.abstract[:200] }}{% if paper.abstract|length > 200 %}…{% endif %}
+
+ {{ paper.abstract[:200] }}{% if paper.abstract|length > 200 %}…{% endif %}
+
{% endif %}
@@ -29,32 +31,31 @@