| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- """论文库管理 API 路由 —— 论文保存、删除、搜索、分类与阅读状态管理."""
- import logging
- import anyio
- from fastapi import APIRouter, Query, BackgroundTasks, Request, Depends, HTTPException
- from ...utils.common import safe_http_500
- from ...models.schemas import (
- DeletePaperResponse,
- LibraryCategoriesResponse,
- Paper,
- PapersResponse,
- ReadStatus,
- SavePapersRequest,
- SavePapersResponse,
- LibraryGraphResponse,
- UpdatePaperRequest,
- UpdatePaperResponse,
- DailyPapersRequest,
- DailyPapersResponse,
- DailyRecommendFeedbackRequest,
- DailyRecommendFeedbackResponse,
- ReadingCalendarItem,
- ReadingLogRequest,
- ReadingCalendarResponse,
- )
- from ...services.papers.papers_converters import api_paper_to_litpaper, litpaper_to_api_paper
- from ...services.papers.papers_helpers import (
- daily_paper_identity_sig,
- )
- from ...services.graph.graph_service import build_library_graph
- from ...services.papers.papers_library_service import (
- build_library_pdf_response_service,
- delete_paper_by_id,
- get_library as get_library_service,
- get_paper_by_id,
- list_library_categories as list_library_categories_service,
- save_papers as save_papers_service,
- update_paper_by_id,
- )
- from ...services.daily.daily_auto_refresh import get_daily_compute_lock
- from ...services.daily.daily_service import (
- compute_daily_papers as compute_daily_service,
- read_daily_cached_or_204 as get_daily_cached_or_204_service,
- record_user_daily_feedback as record_daily_feedback_service,
- )
- from ...settings import get_settings
- from ..dependencies import get_database, get_db_path, get_searcher
- logger = logging.getLogger(__name__)
- router = APIRouter(prefix="/papers", tags=["文献管理"])
- class DailyServices:
- def __init__(self, db_path=Depends(get_db_path), searcher=Depends(get_searcher)):
- self.db_path = db_path
- self.searcher = searcher
- # ── 知识图谱 ──
- @router.get("/graph/library", response_model=LibraryGraphResponse)
- def library_graph(
- limit: int = Query(default=200, ge=1, le=1000),
- category: str | None = Query(default=None),
- include_authors: bool = Query(default=False),
- include_keywords: bool = Query(default=False),
- relation_edge_limit: int = Query(default=400, ge=0, le=5000),
- focus_paper_id: int | None = Query(default=None, ge=1),
- db=Depends(get_database),
- ):
- try:
- return build_library_graph(
- db=db,
- limit=int(limit),
- category=category,
- include_authors=bool(include_authors),
- include_keywords=bool(include_keywords),
- relation_edge_limit=int(relation_edge_limit),
- focus_paper_id=focus_paper_id,
- )
- except Exception as e:
- logger.exception("GET /api/papers/graph/library 失败")
- raise HTTPException(status_code=500, detail=str(e))
- # ── 文献库管理 ──
- @router.get("/library/categories", response_model=LibraryCategoriesResponse)
- def list_library_categories(db=Depends(get_database)):
- return list_library_categories_service(db=db)
- @router.get("/library", response_model=PapersResponse)
- def get_library(
- limit: int = Query(default=50, ge=1, le=1000),
- offset: int = Query(default=0, ge=0),
- q: str | None = None,
- year_from: int | None = None,
- year_to: int | None = None,
- read_status: ReadStatus | None = None,
- tags: str | None = Query(default=None, description="逗号分隔标签"),
- category: str | None = Query(default=None, description="领域筛选"),
- db=Depends(get_database),
- ):
- return get_library_service(
- db=db,
- litpaper_to_api_paper_fn=litpaper_to_api_paper,
- limit=limit,
- offset=offset,
- q=q,
- year_from=year_from,
- year_to=year_to,
- read_status=read_status,
- tags=tags,
- category=category,
- )
- # ── 论文保存 ──
- @router.post("/save", response_model=SavePapersResponse)
- async def save_papers(
- request: SavePapersRequest,
- background_tasks: BackgroundTasks,
- db=Depends(get_database),
- ):
- try:
- return await save_papers_service(
- db=db,
- request=request,
- background_tasks=background_tasks,
- api_to_lit_fn=api_paper_to_litpaper,
- litpaper_to_api_paper_fn=litpaper_to_api_paper,
- )
- except HTTPException:
- raise
- except Exception as e:
- raise safe_http_500("save_papers", e)
- # ── 每日推荐 ──
- @router.get("/daily")
- async def daily_papers_get(db_path=Depends(get_db_path)):
- logger.info("HTTP GET /api/papers/daily")
- return await get_daily_cached_or_204_service(db_path=db_path)
- @router.post("/daily", response_model=DailyPapersResponse)
- async def daily_papers(
- body: DailyPapersRequest,
- services: DailyServices = Depends(),
- settings=Depends(get_settings),
- ):
- logger.info(
- "HTTP POST /api/papers/daily force_refresh=%s",
- getattr(body, "force_refresh", False),
- )
- lock = get_daily_compute_lock()
- async with lock:
- try:
- with anyio.fail_after(180.0):
- resp = await compute_daily_service(
- body=body, db_path=services.db_path, searcher=services.searcher,
- daily_paper_identity_sig_fn=daily_paper_identity_sig,
- daily_arxiv_cs_categories=settings.get_daily_arxiv_cs_categories(),
- papergraph_to_api_fn=litpaper_to_api_paper, logger=logger,
- )
- except TimeoutError:
- err_msg = "每日论文计算超时(>180s),请稍后重试或缩小范围"
- raise HTTPException(status_code=504, detail=err_msg)
- except HTTPException:
- raise
- except Exception as e:
- raise safe_http_500("daily_papers", e)
- else:
- return resp
- # ── 阅读日志 ──
- @router.post("/reading/log")
- def log_reading_session(body: ReadingLogRequest, db_path=Depends(get_db_path)):
- from ...services.reading_log.log import append_session
- append_session(db_path, paper_id=int(body.paper_id), duration_sec=int(body.duration_sec),
- client_ts=int(body.client_ts) if body.client_ts is not None else None)
- return {"success": True}
- @router.get("/reading/calendar", response_model=ReadingCalendarResponse)
- def reading_calendar(days: int = Query(default=180, ge=7, le=366), db_path=Depends(get_db_path)):
- from ...services.reading_log.log import list_daily_aggregate
- items = list_daily_aggregate(db_path, days=int(days))
- return ReadingCalendarResponse(success=True, days=int(days),
- items=[ReadingCalendarItem(**x) for x in items])
- @router.get("/{paper_id}/library-pdf")
- async def get_paper_library_pdf(
- paper_id: int,
- request: Request,
- db_path=Depends(get_db_path),
- ):
- return build_library_pdf_response_service(paper_id=paper_id, request=request, db_path=db_path, logger_obj=logger)
- @router.get("/{paper_id}", response_model=Paper)
- def get_paper(paper_id: int, db=Depends(get_database)):
- return get_paper_by_id(db=db, paper_id=paper_id, litpaper_to_api_paper_fn=litpaper_to_api_paper)
- @router.put("/{paper_id}", response_model=UpdatePaperResponse)
- def update_paper(paper_id: int, body: UpdatePaperRequest, db=Depends(get_database)):
- return update_paper_by_id(db=db, paper_id=paper_id, body=body)
- @router.delete("/{paper_id}", response_model=DeletePaperResponse)
- def delete_paper(paper_id: int, db=Depends(get_database)):
- return delete_paper_by_id(db=db, paper_id=paper_id)
- @router.post("/daily/feedback", response_model=DailyRecommendFeedbackResponse)
- async def record_daily_recommend_feedback(
- body: DailyRecommendFeedbackRequest,
- db_path=Depends(get_db_path),
- ):
- try:
- return await record_daily_feedback_service(body=body, db_path=db_path)
- except HTTPException:
- raise
- except Exception as e:
- raise safe_http_500("record_daily_recommend_feedback", e)
|