papers.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. """论文库管理 API 路由 —— 论文保存、删除、搜索、分类与阅读状态管理."""
  2. import logging
  3. import anyio
  4. from fastapi import APIRouter, Query, BackgroundTasks, Request, Depends, HTTPException
  5. from ...utils.common import safe_http_500
  6. from ...models.schemas import (
  7. DeletePaperResponse,
  8. LibraryCategoriesResponse,
  9. Paper,
  10. PapersResponse,
  11. ReadStatus,
  12. SavePapersRequest,
  13. SavePapersResponse,
  14. LibraryGraphResponse,
  15. UpdatePaperRequest,
  16. UpdatePaperResponse,
  17. DailyPapersRequest,
  18. DailyPapersResponse,
  19. DailyRecommendFeedbackRequest,
  20. DailyRecommendFeedbackResponse,
  21. ReadingCalendarItem,
  22. ReadingLogRequest,
  23. ReadingCalendarResponse,
  24. )
  25. from ...services.papers.papers_converters import api_paper_to_litpaper, litpaper_to_api_paper
  26. from ...services.papers.papers_helpers import (
  27. daily_paper_identity_sig,
  28. )
  29. from ...services.graph.graph_service import build_library_graph
  30. from ...services.papers.papers_library_service import (
  31. build_library_pdf_response_service,
  32. delete_paper_by_id,
  33. get_library as get_library_service,
  34. get_paper_by_id,
  35. list_library_categories as list_library_categories_service,
  36. save_papers as save_papers_service,
  37. update_paper_by_id,
  38. )
  39. from ...services.daily.daily_auto_refresh import get_daily_compute_lock
  40. from ...services.daily.daily_service import (
  41. compute_daily_papers as compute_daily_service,
  42. read_daily_cached_or_204 as get_daily_cached_or_204_service,
  43. record_user_daily_feedback as record_daily_feedback_service,
  44. )
  45. from ...settings import get_settings
  46. from ..dependencies import get_database, get_db_path, get_searcher
  47. logger = logging.getLogger(__name__)
  48. router = APIRouter(prefix="/papers", tags=["文献管理"])
  49. class DailyServices:
  50. def __init__(self, db_path=Depends(get_db_path), searcher=Depends(get_searcher)):
  51. self.db_path = db_path
  52. self.searcher = searcher
  53. # ── 知识图谱 ──
  54. @router.get("/graph/library", response_model=LibraryGraphResponse)
  55. def library_graph(
  56. limit: int = Query(default=200, ge=1, le=1000),
  57. category: str | None = Query(default=None),
  58. include_authors: bool = Query(default=False),
  59. include_keywords: bool = Query(default=False),
  60. relation_edge_limit: int = Query(default=400, ge=0, le=5000),
  61. focus_paper_id: int | None = Query(default=None, ge=1),
  62. db=Depends(get_database),
  63. ):
  64. try:
  65. return build_library_graph(
  66. db=db,
  67. limit=int(limit),
  68. category=category,
  69. include_authors=bool(include_authors),
  70. include_keywords=bool(include_keywords),
  71. relation_edge_limit=int(relation_edge_limit),
  72. focus_paper_id=focus_paper_id,
  73. )
  74. except Exception as e:
  75. logger.exception("GET /api/papers/graph/library 失败")
  76. raise HTTPException(status_code=500, detail=str(e))
  77. # ── 文献库管理 ──
  78. @router.get("/library/categories", response_model=LibraryCategoriesResponse)
  79. def list_library_categories(db=Depends(get_database)):
  80. return list_library_categories_service(db=db)
  81. @router.get("/library", response_model=PapersResponse)
  82. def get_library(
  83. limit: int = Query(default=50, ge=1, le=1000),
  84. offset: int = Query(default=0, ge=0),
  85. q: str | None = None,
  86. year_from: int | None = None,
  87. year_to: int | None = None,
  88. read_status: ReadStatus | None = None,
  89. tags: str | None = Query(default=None, description="逗号分隔标签"),
  90. category: str | None = Query(default=None, description="领域筛选"),
  91. db=Depends(get_database),
  92. ):
  93. return get_library_service(
  94. db=db,
  95. litpaper_to_api_paper_fn=litpaper_to_api_paper,
  96. limit=limit,
  97. offset=offset,
  98. q=q,
  99. year_from=year_from,
  100. year_to=year_to,
  101. read_status=read_status,
  102. tags=tags,
  103. category=category,
  104. )
  105. # ── 论文保存 ──
  106. @router.post("/save", response_model=SavePapersResponse)
  107. async def save_papers(
  108. request: SavePapersRequest,
  109. background_tasks: BackgroundTasks,
  110. db=Depends(get_database),
  111. ):
  112. try:
  113. return await save_papers_service(
  114. db=db,
  115. request=request,
  116. background_tasks=background_tasks,
  117. api_to_lit_fn=api_paper_to_litpaper,
  118. litpaper_to_api_paper_fn=litpaper_to_api_paper,
  119. )
  120. except HTTPException:
  121. raise
  122. except Exception as e:
  123. raise safe_http_500("save_papers", e)
  124. # ── 每日推荐 ──
  125. @router.get("/daily")
  126. async def daily_papers_get(db_path=Depends(get_db_path)):
  127. logger.info("HTTP GET /api/papers/daily")
  128. return await get_daily_cached_or_204_service(db_path=db_path)
  129. @router.post("/daily", response_model=DailyPapersResponse)
  130. async def daily_papers(
  131. body: DailyPapersRequest,
  132. services: DailyServices = Depends(),
  133. settings=Depends(get_settings),
  134. ):
  135. logger.info(
  136. "HTTP POST /api/papers/daily force_refresh=%s",
  137. getattr(body, "force_refresh", False),
  138. )
  139. lock = get_daily_compute_lock()
  140. async with lock:
  141. try:
  142. with anyio.fail_after(180.0):
  143. resp = await compute_daily_service(
  144. body=body, db_path=services.db_path, searcher=services.searcher,
  145. daily_paper_identity_sig_fn=daily_paper_identity_sig,
  146. daily_arxiv_cs_categories=settings.get_daily_arxiv_cs_categories(),
  147. papergraph_to_api_fn=litpaper_to_api_paper, logger=logger,
  148. )
  149. except TimeoutError:
  150. err_msg = "每日论文计算超时(>180s),请稍后重试或缩小范围"
  151. raise HTTPException(status_code=504, detail=err_msg)
  152. except HTTPException:
  153. raise
  154. except Exception as e:
  155. raise safe_http_500("daily_papers", e)
  156. else:
  157. return resp
  158. # ── 阅读日志 ──
  159. @router.post("/reading/log")
  160. def log_reading_session(body: ReadingLogRequest, db_path=Depends(get_db_path)):
  161. from ...services.reading_log.log import append_session
  162. append_session(db_path, paper_id=int(body.paper_id), duration_sec=int(body.duration_sec),
  163. client_ts=int(body.client_ts) if body.client_ts is not None else None)
  164. return {"success": True}
  165. @router.get("/reading/calendar", response_model=ReadingCalendarResponse)
  166. def reading_calendar(days: int = Query(default=180, ge=7, le=366), db_path=Depends(get_db_path)):
  167. from ...services.reading_log.log import list_daily_aggregate
  168. items = list_daily_aggregate(db_path, days=int(days))
  169. return ReadingCalendarResponse(success=True, days=int(days),
  170. items=[ReadingCalendarItem(**x) for x in items])
  171. @router.get("/{paper_id}/library-pdf")
  172. async def get_paper_library_pdf(
  173. paper_id: int,
  174. request: Request,
  175. db_path=Depends(get_db_path),
  176. ):
  177. return build_library_pdf_response_service(paper_id=paper_id, request=request, db_path=db_path, logger_obj=logger)
  178. @router.get("/{paper_id}", response_model=Paper)
  179. def get_paper(paper_id: int, db=Depends(get_database)):
  180. return get_paper_by_id(db=db, paper_id=paper_id, litpaper_to_api_paper_fn=litpaper_to_api_paper)
  181. @router.put("/{paper_id}", response_model=UpdatePaperResponse)
  182. def update_paper(paper_id: int, body: UpdatePaperRequest, db=Depends(get_database)):
  183. return update_paper_by_id(db=db, paper_id=paper_id, body=body)
  184. @router.delete("/{paper_id}", response_model=DeletePaperResponse)
  185. def delete_paper(paper_id: int, db=Depends(get_database)):
  186. return delete_paper_by_id(db=db, paper_id=paper_id)
  187. @router.post("/daily/feedback", response_model=DailyRecommendFeedbackResponse)
  188. async def record_daily_recommend_feedback(
  189. body: DailyRecommendFeedbackRequest,
  190. db_path=Depends(get_db_path),
  191. ):
  192. try:
  193. return await record_daily_feedback_service(body=body, db_path=db_path)
  194. except HTTPException:
  195. raise
  196. except Exception as e:
  197. raise safe_http_500("record_daily_recommend_feedback", e)