dr-claw 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/LICENSE +1380 -0
  2. package/NOTICE +17 -0
  3. package/README.md +875 -0
  4. package/README.zh-CN.md +857 -0
  5. package/dist/api-docs.html +878 -0
  6. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  7. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  8. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  11. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  12. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  17. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  18. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  21. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  22. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  23. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  25. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  26. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  28. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  29. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  30. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  31. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  32. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  33. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  34. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  35. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  36. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  37. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  38. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  48. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  49. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  50. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  51. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  52. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  53. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  54. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  55. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  56. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  57. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  58. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  59. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  60. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  61. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  62. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  63. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  64. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  65. package/dist/assets/_basePickBy-C0n1uo4u.js +1 -0
  66. package/dist/assets/_baseUniq-BRNmhFkc.js +1 -0
  67. package/dist/assets/arc-DOMBmC_A.js +1 -0
  68. package/dist/assets/architectureDiagram-VXUJARFQ-CtMWglbD.js +36 -0
  69. package/dist/assets/blockDiagram-VD42YOAC-DK_gTkJy.js +122 -0
  70. package/dist/assets/c4Diagram-YG6GDRKO-DrDAvsn7.js +10 -0
  71. package/dist/assets/channel-BFOE7OyC.js +1 -0
  72. package/dist/assets/chunk-4BX2VUAB-CGbhdZcN.js +1 -0
  73. package/dist/assets/chunk-55IACEB6-sGeFSxen.js +1 -0
  74. package/dist/assets/chunk-B4BG7PRW-RxgHAU_P.js +165 -0
  75. package/dist/assets/chunk-DI55MBZ5-CvHUXZrx.js +220 -0
  76. package/dist/assets/chunk-FMBD7UC4-Sn7cXVNU.js +15 -0
  77. package/dist/assets/chunk-QN33PNHL-bDKOHppN.js +1 -0
  78. package/dist/assets/chunk-QZHKN3VN-B9kN5LGm.js +1 -0
  79. package/dist/assets/chunk-TZMSLE5B-CCBe6len.js +1 -0
  80. package/dist/assets/classDiagram-2ON5EDUG-DQoMNYlY.js +1 -0
  81. package/dist/assets/classDiagram-v2-WZHVMYZB-DQoMNYlY.js +1 -0
  82. package/dist/assets/clone-BulQSblH.js +1 -0
  83. package/dist/assets/cose-bilkent-S5V4N54A-r6sLiHBW.js +1 -0
  84. package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
  85. package/dist/assets/dagre-6UL2VRFP-AbsaR1Oa.js +4 -0
  86. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  87. package/dist/assets/diagram-PSM6KHXK-DjCvdUyy.js +24 -0
  88. package/dist/assets/diagram-QEK2KX5R-CqiocXkH.js +43 -0
  89. package/dist/assets/diagram-S2PKOQOG-D5ALeoII.js +24 -0
  90. package/dist/assets/erDiagram-Q2GNP2WA-C1AeOsiz.js +60 -0
  91. package/dist/assets/flowDiagram-NV44I4VS-C98ve_DO.js +162 -0
  92. package/dist/assets/ganttDiagram-JELNMOA3-3zdLvcxD.js +267 -0
  93. package/dist/assets/gitGraphDiagram-V2S2FVAM-nDuDMsHh.js +65 -0
  94. package/dist/assets/graph-Blm-emkR.js +1 -0
  95. package/dist/assets/index-B8Yutsnm.css +1 -0
  96. package/dist/assets/index-Cf6gyhc3.js +654 -0
  97. package/dist/assets/infoDiagram-HS3SLOUP-CmzE3q0R.js +2 -0
  98. package/dist/assets/init-Gi6I4Gst.js +1 -0
  99. package/dist/assets/journeyDiagram-XKPGCS4Q-CYaW7LrP.js +139 -0
  100. package/dist/assets/kanban-definition-3W4ZIXB7-mkmW7p0j.js +89 -0
  101. package/dist/assets/layout-Ct2kFIId.js +1 -0
  102. package/dist/assets/linear-CKG9EwIJ.js +1 -0
  103. package/dist/assets/mermaid.core-CaKcUnnd.js +250 -0
  104. package/dist/assets/mindmap-definition-VGOIOE7T-Bct-pYin.js +68 -0
  105. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  106. package/dist/assets/pieDiagram-ADFJNKIX-C8wFD4Jr.js +30 -0
  107. package/dist/assets/quadrantDiagram-AYHSOK5B-CuiE_f46.js +7 -0
  108. package/dist/assets/requirementDiagram-UZGBJVZJ-C4v1CqU8.js +64 -0
  109. package/dist/assets/sankeyDiagram-TZEHDZUN-keAqqs-Y.js +10 -0
  110. package/dist/assets/sequenceDiagram-WL72ISMW-ClNbvKTB.js +145 -0
  111. package/dist/assets/stateDiagram-FKZM4ZOC-D8We-2vq.js +1 -0
  112. package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +1 -0
  113. package/dist/assets/timeline-definition-IT6M3QCI-DsvEMT5T.js +61 -0
  114. package/dist/assets/treemap-GDKQZRPO-TJrzN7Bj.js +162 -0
  115. package/dist/assets/vendor-codemirror-ansM-eig.js +33 -0
  116. package/dist/assets/vendor-react-BWF6YQQ3.js +8 -0
  117. package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
  118. package/dist/assets/xychartDiagram-PRI3JC2R-BdATmY5F.js +7 -0
  119. package/dist/clear-cache.html +85 -0
  120. package/dist/convert-icons.md +53 -0
  121. package/dist/dr-claw.png +0 -0
  122. package/dist/favicon.png +0 -0
  123. package/dist/generate-icons.js +49 -0
  124. package/dist/icons/claude-ai-icon.svg +1 -0
  125. package/dist/icons/codex-white.svg +3 -0
  126. package/dist/icons/codex.svg +3 -0
  127. package/dist/icons/cursor-white.svg +12 -0
  128. package/dist/icons/cursor.svg +1 -0
  129. package/dist/icons/file.svg +16997 -0
  130. package/dist/icons/generate-icons.md +19 -0
  131. package/dist/icons/icon-128x128.png +0 -0
  132. package/dist/icons/icon-144x144.png +0 -0
  133. package/dist/icons/icon-152x152.png +0 -0
  134. package/dist/icons/icon-192x192.png +0 -0
  135. package/dist/icons/icon-384x384.png +0 -0
  136. package/dist/icons/icon-512x512.png +0 -0
  137. package/dist/icons/icon-72x72.png +0 -0
  138. package/dist/icons/icon-96x96.png +0 -0
  139. package/dist/icons/news/arxiv.svg +1 -0
  140. package/dist/icons/news/huggingface.svg +8 -0
  141. package/dist/icons/news/x-black.png +0 -0
  142. package/dist/icons/news/x-white.png +0 -0
  143. package/dist/icons/news/xiaohongshu.png +0 -0
  144. package/dist/index.html +51 -0
  145. package/dist/leveraged-cognition.png +0 -0
  146. package/dist/manifest.json +61 -0
  147. package/dist/screenshots/chat.png +0 -0
  148. package/dist/screenshots/new_claw.png +0 -0
  149. package/dist/screenshots/news_dashboard.png +0 -0
  150. package/dist/screenshots/project_dashboard.png +0 -0
  151. package/dist/screenshots/research_lab.png +0 -0
  152. package/dist/screenshots/skill_library.png +0 -0
  153. package/dist/screenshots/task_list.png +0 -0
  154. package/dist/screenshots/terminal_example.png +0 -0
  155. package/dist/screenshots/terminal_example1.png +0 -0
  156. package/dist/sw.js +58 -0
  157. package/dist/wechat-group-qr.jpg +0 -0
  158. package/package.json +133 -0
  159. package/public/api-docs.html +878 -0
  160. package/public/clear-cache.html +85 -0
  161. package/public/convert-icons.md +53 -0
  162. package/public/dr-claw.png +0 -0
  163. package/public/favicon.png +0 -0
  164. package/public/generate-icons.js +49 -0
  165. package/public/icons/claude-ai-icon.svg +1 -0
  166. package/public/icons/codex-white.svg +3 -0
  167. package/public/icons/codex.svg +3 -0
  168. package/public/icons/cursor-white.svg +12 -0
  169. package/public/icons/cursor.svg +1 -0
  170. package/public/icons/file.svg +16997 -0
  171. package/public/icons/generate-icons.md +19 -0
  172. package/public/icons/icon-128x128.png +0 -0
  173. package/public/icons/icon-144x144.png +0 -0
  174. package/public/icons/icon-152x152.png +0 -0
  175. package/public/icons/icon-192x192.png +0 -0
  176. package/public/icons/icon-384x384.png +0 -0
  177. package/public/icons/icon-512x512.png +0 -0
  178. package/public/icons/icon-72x72.png +0 -0
  179. package/public/icons/icon-96x96.png +0 -0
  180. package/public/icons/news/arxiv.svg +1 -0
  181. package/public/icons/news/huggingface.svg +8 -0
  182. package/public/icons/news/x-black.png +0 -0
  183. package/public/icons/news/x-white.png +0 -0
  184. package/public/icons/news/xiaohongshu.png +0 -0
  185. package/public/leveraged-cognition.png +0 -0
  186. package/public/manifest.json +61 -0
  187. package/public/screenshots/chat.png +0 -0
  188. package/public/screenshots/new_claw.png +0 -0
  189. package/public/screenshots/news_dashboard.png +0 -0
  190. package/public/screenshots/project_dashboard.png +0 -0
  191. package/public/screenshots/research_lab.png +0 -0
  192. package/public/screenshots/skill_library.png +0 -0
  193. package/public/screenshots/task_list.png +0 -0
  194. package/public/screenshots/terminal_example.png +0 -0
  195. package/public/screenshots/terminal_example1.png +0 -0
  196. package/public/sw.js +58 -0
  197. package/public/wechat-group-qr.jpg +0 -0
  198. package/scripts/export-skills-catalog-v2.mjs +746 -0
  199. package/scripts/fix-node-pty.js +45 -0
  200. package/scripts/gemini_image_core.py +470 -0
  201. package/scripts/install-python-tools.js +64 -0
  202. package/server/AGENTS.md +73 -0
  203. package/server/__tests__/gemini-legacy-thought.test.mjs +88 -0
  204. package/server/__tests__/runtimePorts.test.mjs +115 -0
  205. package/server/__tests__/session-delete.test.mjs +166 -0
  206. package/server/bin/compute-node +70 -0
  207. package/server/claude-sdk.js +799 -0
  208. package/server/cli-chat.js +488 -0
  209. package/server/cli.js +354 -0
  210. package/server/compute-node.js +439 -0
  211. package/server/constants/config.js +5 -0
  212. package/server/cursor-cli.js +340 -0
  213. package/server/database/auth.db +0 -0
  214. package/server/database/db.js +1852 -0
  215. package/server/database/init.sql +194 -0
  216. package/server/gemini-cli.js +1352 -0
  217. package/server/index.js +3154 -0
  218. package/server/load-env.js +71 -0
  219. package/server/local-gpu.js +860 -0
  220. package/server/middleware/auth.js +100 -0
  221. package/server/openai-codex.js +724 -0
  222. package/server/openrouter.js +890 -0
  223. package/server/project-token-usage.js +407 -0
  224. package/server/projects.js +3973 -0
  225. package/server/routes/agent.js +1272 -0
  226. package/server/routes/auth.js +128 -0
  227. package/server/routes/auto-research.js +558 -0
  228. package/server/routes/cli-auth.js +958 -0
  229. package/server/routes/codex.js +344 -0
  230. package/server/routes/commands.js +601 -0
  231. package/server/routes/compute.js +358 -0
  232. package/server/routes/cursor.js +795 -0
  233. package/server/routes/git.js +1348 -0
  234. package/server/routes/mcp-utils.js +48 -0
  235. package/server/routes/mcp.js +618 -0
  236. package/server/routes/news.js +711 -0
  237. package/server/routes/projects.js +633 -0
  238. package/server/routes/references.js +338 -0
  239. package/server/routes/settings.js +285 -0
  240. package/server/routes/skills.js +914 -0
  241. package/server/routes/taskmaster.js +2566 -0
  242. package/server/routes/telemetry.js +89 -0
  243. package/server/routes/user.js +155 -0
  244. package/server/scripts/fix_session_names.js +57 -0
  245. package/server/scripts/research-news/common_words.py +19 -0
  246. package/server/scripts/research-news/link_keywords.py +298 -0
  247. package/server/scripts/research-news/scan_existing_notes.py +266 -0
  248. package/server/scripts/research-news/scoring_utils.py +227 -0
  249. package/server/scripts/research-news/search_arxiv.py +842 -0
  250. package/server/scripts/research-news/search_huggingface.py +438 -0
  251. package/server/scripts/research-news/search_x.py +490 -0
  252. package/server/scripts/research-news/search_xiaohongshu.py +610 -0
  253. package/server/taskmaster-templates/README.md +150 -0
  254. package/server/taskmaster-templates/README.zh-CN.md +150 -0
  255. package/server/taskmaster-templates/ai-research-dataset.json +230 -0
  256. package/server/taskmaster-templates/ai-research-method-model.json +230 -0
  257. package/server/taskmaster-templates/ai-research-position-paper.json +230 -0
  258. package/server/telemetry.js +121 -0
  259. package/server/templates/AGENTS.md +103 -0
  260. package/server/templates/CLAUDE.md +104 -0
  261. package/server/templates/GEMINI.md +103 -0
  262. package/server/templates/cursor-project.md +108 -0
  263. package/server/templates/index.js +37 -0
  264. package/server/utils/__tests__/sessionTokenUsage.test.js +82 -0
  265. package/server/utils/auto-research-mailer.js +57 -0
  266. package/server/utils/cliResolution.js +170 -0
  267. package/server/utils/commandParser.js +303 -0
  268. package/server/utils/cursorCommand.js +61 -0
  269. package/server/utils/geminiApiKey.js +28 -0
  270. package/server/utils/gitConfig.js +24 -0
  271. package/server/utils/imageAttachmentFiles.js +35 -0
  272. package/server/utils/mcp-detector.js +198 -0
  273. package/server/utils/parsers/bibtex-parser.js +207 -0
  274. package/server/utils/permissions.js +100 -0
  275. package/server/utils/runtimePorts.js +212 -0
  276. package/server/utils/sessionFormatting.js +68 -0
  277. package/server/utils/sessionIndex.js +100 -0
  278. package/server/utils/sessionMode.js +95 -0
  279. package/server/utils/sessionTokenUsage.js +105 -0
  280. package/server/utils/taskmaster-websocket.js +129 -0
  281. package/server/utils/zotero-client.js +136 -0
  282. package/shared/errorClassifier.js +102 -0
  283. package/shared/geminiThinkingSupport.d.ts +22 -0
  284. package/shared/geminiThinkingSupport.js +96 -0
  285. package/shared/geminiThoughtParser.d.ts +8 -0
  286. package/shared/geminiThoughtParser.js +30 -0
  287. package/shared/modelConstants.js +139 -0
@@ -0,0 +1,842 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ arXiv + Semantic Scholar 混合架构论文搜索脚本
4
+ 用于 start-my-day skill,搜索最近一个月和最近一年的极火、极热门、极优质论文
5
+ """
6
+
7
+ import xml.etree.ElementTree as ET
8
+ import json
9
+ import re
10
+ import os
11
+ import sys
12
+ import time
13
+ import logging
14
+ import ssl
15
+ from datetime import datetime, timedelta
16
+ from typing import List, Dict, Set, Optional, Tuple
17
+ from pathlib import Path
18
+ import urllib.request
19
+ import urllib.parse
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ try:
24
+ import requests
25
+ HAS_REQUESTS = True
26
+ except ImportError:
27
+ HAS_REQUESTS = False
28
+ logger.warning("requests library not found, using urllib for Semantic Scholar API")
29
+
30
+ try:
31
+ import certifi
32
+ CERTIFI_CA_BUNDLE = certifi.where()
33
+ except ImportError:
34
+ CERTIFI_CA_BUNDLE = None
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # API 配置
38
+ # ---------------------------------------------------------------------------
39
+ ARXIV_NS = {
40
+ 'atom': 'http://www.w3.org/2005/Atom',
41
+ 'arxiv': 'http://arxiv.org/schemas/atom'
42
+ }
43
+
44
+ SEMANTIC_SCHOLAR_API_URL = "https://api.semanticscholar.org/graph/v1/paper/search"
45
+ SEMANTIC_SCHOLAR_FIELDS = "title,abstract,publicationDate,citationCount,influentialCitationCount,url,authors,externalIds"
46
+
47
+ ARXIV_CATEGORY_KEYWORDS = {
48
+ "cs.AI": "artificial intelligence",
49
+ "cs.LG": "machine learning",
50
+ "cs.CL": "computational linguistics natural language processing",
51
+ "cs.CV": "computer vision",
52
+ "cs.MM": "multimedia",
53
+ "cs.MA": "multi-agent systems",
54
+ "cs.RO": "robotics"
55
+ }
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # 评分常量与函数(从 scoring_utils 导入)
59
+ # ---------------------------------------------------------------------------
60
+ from scoring_utils import (
61
+ SCORE_MAX, RELEVANCE_TITLE_KEYWORD_BOOST, RELEVANCE_SUMMARY_KEYWORD_BOOST,
62
+ RELEVANCE_CATEGORY_MATCH_BOOST, RECENCY_THRESHOLDS, RECENCY_DEFAULT,
63
+ POPULARITY_INFLUENTIAL_CITATION_FULL_SCORE, WEIGHTS_NORMAL, WEIGHTS_HOT,
64
+ calculate_relevance_score, calculate_recency_score, calculate_quality_score,
65
+ calculate_recommendation_score,
66
+ )
67
+
68
+ # Semantic Scholar 速率限制等待时间(秒)
69
+ S2_RATE_LIMIT_WAIT = 30
70
+ S2_CATEGORY_REQUEST_INTERVAL = 3
71
+
72
+
73
+ def build_ssl_context() -> ssl.SSLContext:
74
+ """Build an SSL context, preferring certifi when available."""
75
+ if CERTIFI_CA_BUNDLE and os.path.exists(CERTIFI_CA_BUNDLE):
76
+ return ssl.create_default_context(cafile=CERTIFI_CA_BUNDLE)
77
+ return ssl.create_default_context()
78
+
79
+
80
+ def http_get_text(
81
+ url: str,
82
+ headers: Optional[Dict[str, str]] = None,
83
+ timeout: int = 60,
84
+ params: Optional[Dict[str, str]] = None,
85
+ ) -> str:
86
+ """Fetch text over HTTPS with an explicit CA bundle when available."""
87
+ headers = headers or {}
88
+
89
+ if HAS_REQUESTS:
90
+ request_kwargs = {
91
+ "headers": headers,
92
+ "timeout": timeout,
93
+ }
94
+ if params:
95
+ request_kwargs["params"] = params
96
+ if CERTIFI_CA_BUNDLE and os.path.exists(CERTIFI_CA_BUNDLE):
97
+ request_kwargs["verify"] = CERTIFI_CA_BUNDLE
98
+ response = requests.get(url, **request_kwargs)
99
+ response.raise_for_status()
100
+ response.encoding = response.encoding or "utf-8"
101
+ return response.text
102
+
103
+ final_url = url
104
+ if params:
105
+ query_string = urllib.parse.urlencode(params)
106
+ separator = "&" if "?" in url else "?"
107
+ final_url = f"{url}{separator}{query_string}"
108
+
109
+ request = urllib.request.Request(final_url, headers=headers)
110
+ with urllib.request.urlopen(request, timeout=timeout, context=build_ssl_context()) as response:
111
+ return response.read().decode("utf-8")
112
+
113
+
114
+ def http_get_json(
115
+ url: str,
116
+ headers: Optional[Dict[str, str]] = None,
117
+ timeout: int = 60,
118
+ params: Optional[Dict[str, str]] = None,
119
+ ) -> Dict:
120
+ """Fetch JSON over HTTPS with the same certificate handling as http_get_text."""
121
+ return json.loads(http_get_text(url, headers=headers, timeout=timeout, params=params))
122
+
123
+
124
+ def load_research_config(config_path: str) -> Dict:
125
+ """
126
+ 从 JSON 或 YAML 文件加载研究兴趣配置
127
+
128
+ Args:
129
+ config_path: 配置文件路径
130
+
131
+ Returns:
132
+ 研究配置字典
133
+ """
134
+ import json
135
+
136
+ try:
137
+ with open(config_path, 'r', encoding='utf-8-sig') as f:
138
+ if config_path.endswith('.json'):
139
+ config = json.load(f)
140
+ else:
141
+ try:
142
+ import yaml
143
+ config = yaml.safe_load(f)
144
+ except ImportError:
145
+ # Fallback: try parsing as JSON even if extension is .yaml
146
+ config = json.load(f)
147
+ return config
148
+ except Exception as e:
149
+ logger.error("Error loading config: %s", e)
150
+ # 返回默认配置
151
+ return {
152
+ "research_domains": {
153
+ "大模型": {
154
+ "keywords": [
155
+ "pre-training", "foundation model", "model architecture",
156
+ "large language model", "LLM", "transformer"
157
+ ],
158
+ "arxiv_categories": ["cs.AI", "cs.LG", "cs.CL"],
159
+ "priority": 5
160
+ }
161
+ },
162
+ "excluded_keywords": ["3D", "review", "workshop", "survey"]
163
+ }
164
+
165
+
166
+ def calculate_date_windows(target_date: Optional[datetime] = None) -> Tuple[datetime, datetime, datetime, datetime]:
167
+ """
168
+ 计算两个时间窗口:最近30天和过去一年(除去最近30天)
169
+
170
+ Args:
171
+ target_date: 基准日期,如果为 None 则使用当前日期
172
+
173
+ Returns:
174
+ (window_30d_start, window_30d_end, window_1y_start, window_1y_end)
175
+ - window_30d_start: 30天窗口开始日期
176
+ - window_30d_end: 30天窗口结束日期(即 target_date)
177
+ - window_1y_start: 一年窗口开始日期
178
+ - window_1y_end: 一年窗口结束日期(即 31天前)
179
+ """
180
+ if target_date is None:
181
+ target_date = datetime.now()
182
+
183
+ # 最近30天窗口: [target_date - 30 days, target_date]
184
+ window_30d_start = target_date - timedelta(days=30)
185
+ window_30d_end = target_date
186
+
187
+ # 过去一年窗口(除去最近30天): [target_date - 365 days, target_date - 31 days]
188
+ window_1y_start = target_date - timedelta(days=365)
189
+ window_1y_end = target_date - timedelta(days=31)
190
+
191
+ return window_30d_start, window_30d_end, window_1y_start, window_1y_end
192
+
193
+
194
+ def search_arxiv_by_date_range(
195
+ categories: List[str],
196
+ start_date: datetime,
197
+ end_date: datetime,
198
+ max_results: int = 200,
199
+ max_retries: int = 3
200
+ ) -> Tuple[List[Dict], bool]:
201
+ """
202
+ 使用 arXiv API 搜索指定日期范围内的论文
203
+
204
+ Args:
205
+ categories: arXiv 分类列表
206
+ start_date: 开始日期
207
+ end_date: 结束日期
208
+ max_results: 最大结果数
209
+ max_retries: 最大重试次数
210
+
211
+ Returns:
212
+ 论文列表
213
+ """
214
+ # 构建分类查询
215
+ category_query = "+OR+".join([f"cat:{cat}" for cat in categories])
216
+
217
+ # 构建日期范围查询 (arXiv 使用 YYYYMMDD 格式)
218
+ date_query = f"submittedDate:[{start_date.strftime('%Y%m%d')}0000+TO+{end_date.strftime('%Y%m%d')}2359]"
219
+
220
+ # 组合查询
221
+ full_query = f"({category_query})+AND+{date_query}"
222
+
223
+ # 构建 URL
224
+ url = (
225
+ f"https://export.arxiv.org/api/query?"
226
+ f"search_query={full_query}&"
227
+ f"max_results={max_results}&"
228
+ f"sortBy=submittedDate&"
229
+ f"sortOrder=descending"
230
+ )
231
+ headers = {
232
+ "User-Agent": "ResearchNews-ArxivFetcher/1.0"
233
+ }
234
+
235
+ logger.info("[arXiv] Searching papers from %s to %s", start_date.date(), end_date.date())
236
+ logger.debug("[arXiv] URL: %s...", url[:120])
237
+
238
+ last_error = None
239
+
240
+ for attempt in range(max_retries):
241
+ try:
242
+ xml_content = http_get_text(url, headers=headers, timeout=60)
243
+ papers = parse_arxiv_xml(xml_content)
244
+ logger.info("[arXiv] Found %d papers", len(papers))
245
+ return papers, True
246
+ except Exception as e:
247
+ last_error = e
248
+ logger.warning("[arXiv] Error (attempt %d/%d): %s", attempt + 1, max_retries, e)
249
+ if attempt < max_retries - 1:
250
+ wait_time = (2 ** attempt) * 2
251
+ logger.info("[arXiv] Retrying in %d seconds...", wait_time)
252
+ time.sleep(wait_time)
253
+ else:
254
+ logger.error("[arXiv] Failed after %d attempts", max_retries)
255
+ if last_error is not None:
256
+ logger.error("[arXiv] Last error: %s", last_error)
257
+ return [], False
258
+
259
+ return [], False
260
+
261
+
262
+ def search_semantic_scholar_hot_papers(
263
+ query: str,
264
+ start_date: datetime,
265
+ end_date: datetime,
266
+ top_k: int = 20,
267
+ max_retries: int = 3
268
+ ) -> List[Dict]:
269
+ """
270
+ 使用 Semantic Scholar API 搜索指定时间范围内的高影响力论文
271
+
272
+ Args:
273
+ query: 搜索关键词
274
+ start_date: 开始日期
275
+ end_date: 结束日期
276
+ top_k: 返回前 K 篇高影响力论文
277
+ max_retries: 最大重试次数
278
+
279
+ Returns:
280
+ 按高影响力引用数排序的论文列表
281
+ """
282
+ # 构建日期范围 (Semantic Scholar 使用 YYYY-MM-DD:YYYY-MM-DD 格式)
283
+ date_range = f"{start_date.strftime('%Y-%m-%d')}:{end_date.strftime('%Y-%m-%d')}"
284
+
285
+ # 构建请求参数
286
+ params = {
287
+ "query": query,
288
+ "publicationDateOrYear": date_range,
289
+ "limit": 100, # 先拉取100篇相关度最高的
290
+ "fields": SEMANTIC_SCHOLAR_FIELDS
291
+ }
292
+
293
+ headers = {
294
+ "User-Agent": "StartMyDay-PaperFetcher/1.0"
295
+ }
296
+
297
+ logger.info("[S2] Searching hot papers from %s to %s", start_date.date(), end_date.date())
298
+ logger.info("[S2] Query: '%s'", query)
299
+
300
+ for attempt in range(max_retries):
301
+ try:
302
+ data = http_get_json(
303
+ SEMANTIC_SCHOLAR_API_URL,
304
+ headers=headers,
305
+ timeout=15,
306
+ params=params,
307
+ )
308
+
309
+ papers = data.get("data", [])
310
+ if not papers:
311
+ logger.info("[S2] No papers found")
312
+ return []
313
+
314
+ # 本地二次过滤与排序
315
+ valid_papers = []
316
+ for p in papers:
317
+ # 过滤掉没有标题或摘要的无效条目
318
+ if not p.get("title") or not p.get("abstract"):
319
+ continue
320
+
321
+ # 处理可能的 None 值
322
+ inf_cit = p.get("influentialCitationCount") or 0
323
+ cit = p.get("citationCount") or 0
324
+
325
+ p["influentialCitationCount"] = inf_cit
326
+ p["citationCount"] = cit
327
+
328
+ # 标记来源
329
+ p["source"] = "semantic_scholar"
330
+ p["hot_score"] = inf_cit # 使用高影响力引用数作为热度分数
331
+
332
+ valid_papers.append(p)
333
+
334
+ # 按高影响力引用数倒序排列
335
+ sorted_papers = sorted(
336
+ valid_papers,
337
+ key=lambda x: x["influentialCitationCount"],
338
+ reverse=True
339
+ )
340
+
341
+ logger.info("[S2] Found %d valid papers, returning top %d", len(sorted_papers), top_k)
342
+ return sorted_papers[:top_k]
343
+
344
+ except Exception as e:
345
+ error_msg = str(e)
346
+ logger.warning("[S2] Error (attempt %d/%d): %s", attempt + 1, max_retries, e)
347
+
348
+ # 检查是否是 429 错误(Too Many Requests)
349
+ is_rate_limit = "429" in error_msg or "Too Many Requests" in error_msg
350
+
351
+ if attempt < max_retries - 1:
352
+ # 对于 429 错误,使用更长的等待时间
353
+ if is_rate_limit:
354
+ wait_time = S2_RATE_LIMIT_WAIT
355
+ logger.warning("[S2] Rate limit hit. Waiting %d seconds...", wait_time)
356
+ else:
357
+ wait_time = (2 ** attempt) * 2
358
+ logger.info("[S2] Retrying in %d seconds...", wait_time)
359
+ time.sleep(wait_time)
360
+ else:
361
+ logger.error("[S2] Failed after %d attempts", max_retries)
362
+ return []
363
+
364
+ return []
365
+
366
+
367
+ def search_hot_papers_from_categories(
368
+ categories: List[str],
369
+ start_date: datetime,
370
+ end_date: datetime,
371
+ top_k_per_category: int = 5
372
+ ) -> List[Dict]:
373
+ """
374
+ 为多个 arXiv 分类搜索高影响力论文
375
+
376
+ Args:
377
+ categories: arXiv 分类列表
378
+ start_date: 开始日期
379
+ end_date: 结束日期
380
+ top_k_per_category: 每个分类返回的论文数
381
+
382
+ Returns:
383
+ 合并后的高影响力论文列表
384
+ """
385
+ all_hot_papers = []
386
+ seen_arxiv_ids = set()
387
+
388
+ for category in categories:
389
+ # 获取对应的关键词
390
+ query = ARXIV_CATEGORY_KEYWORDS.get(category, category)
391
+
392
+ papers = search_semantic_scholar_hot_papers(
393
+ query=query,
394
+ start_date=start_date,
395
+ end_date=end_date,
396
+ top_k=top_k_per_category
397
+ )
398
+
399
+ # 去重(基于 arXiv ID)
400
+ for p in papers:
401
+ # 安全地从 externalIds 字典中提取 ArXiv 编号
402
+ arxiv_id = p.get("externalIds", {}).get("ArXiv") if p.get("externalIds") else None
403
+
404
+ # 统一写入 arxiv_id 字段,方便最后 Step 3 的全局去重
405
+ p["arxiv_id"] = arxiv_id
406
+
407
+ if arxiv_id and arxiv_id not in seen_arxiv_ids:
408
+ seen_arxiv_ids.add(arxiv_id)
409
+ all_hot_papers.append(p)
410
+ elif not arxiv_id:
411
+ # 没有 arXiv ID 的也保留(可能是其他来源的论文)
412
+ all_hot_papers.append(p)
413
+
414
+ time.sleep(S2_CATEGORY_REQUEST_INTERVAL)
415
+
416
+ # 最终按影响力引用数排序
417
+ all_hot_papers.sort(key=lambda x: x.get("influentialCitationCount", 0), reverse=True)
418
+
419
+ return all_hot_papers
420
+
421
+
422
+ def parse_arxiv_xml(xml_content: str) -> List[Dict]:
423
+ """
424
+ 解析 arXiv XML 结果
425
+
426
+ Args:
427
+ xml_content: XML 内容
428
+
429
+ Returns:
430
+ 论文列表,每篇论文包含 ID、标题、作者、摘要等信息
431
+ """
432
+ papers = []
433
+
434
+ try:
435
+ root = ET.fromstring(xml_content)
436
+
437
+ # 查找所有 entry 元素
438
+ for entry in root.findall('atom:entry', ARXIV_NS):
439
+ paper = {}
440
+
441
+ # 提取 ID
442
+ id_elem = entry.find('atom:id', ARXIV_NS)
443
+ if id_elem is not None:
444
+ paper['id'] = id_elem.text
445
+ # 提取 arXiv ID(从 URL 中提取)
446
+ match = re.search(r'arXiv:(\d+\.\d+)', paper['id'])
447
+ if match:
448
+ paper['arxiv_id'] = match.group(1)
449
+ else:
450
+ match = re.search(r'/(\d+\.\d+)$', paper['id'])
451
+ if match:
452
+ paper['arxiv_id'] = match.group(1)
453
+
454
+ # 提取标题
455
+ title_elem = entry.find('atom:title', ARXIV_NS)
456
+ if title_elem is not None:
457
+ paper['title'] = title_elem.text.strip()
458
+
459
+ # 提取摘要
460
+ summary_elem = entry.find('atom:summary', ARXIV_NS)
461
+ if summary_elem is not None:
462
+ paper['summary'] = summary_elem.text.strip()
463
+
464
+ # 提取作者
465
+ authors = []
466
+ for author in entry.findall('atom:author', ARXIV_NS):
467
+ name_elem = author.find('atom:name', ARXIV_NS)
468
+ if name_elem is not None:
469
+ authors.append(name_elem.text)
470
+ paper['authors'] = authors
471
+
472
+ # 提取发布日期
473
+ published_elem = entry.find('atom:published', ARXIV_NS)
474
+ if published_elem is not None:
475
+ paper['published'] = published_elem.text
476
+ # 解析日期
477
+ try:
478
+ paper['published_date'] = datetime.fromisoformat(
479
+ paper['published'].replace('Z', '+00:00')
480
+ )
481
+ except (ValueError, TypeError):
482
+ paper['published_date'] = None
483
+
484
+ # 提取更新日期
485
+ updated_elem = entry.find('atom:updated', ARXIV_NS)
486
+ if updated_elem is not None:
487
+ paper['updated'] = updated_elem.text
488
+
489
+ # 提取分类
490
+ categories = []
491
+ for category in entry.findall('atom:category', ARXIV_NS):
492
+ term = category.get('term')
493
+ if term:
494
+ categories.append(term)
495
+ paper['categories'] = categories
496
+
497
+ # 提取 PDF 链接
498
+ for link in entry.findall('atom:link', ARXIV_NS):
499
+ if link.get('title') == 'pdf':
500
+ paper['pdf_url'] = link.get('href')
501
+ break
502
+
503
+ # 提取主页面链接
504
+ if 'id' in paper:
505
+ paper['url'] = paper['id']
506
+
507
+ # 标记来源
508
+ paper['source'] = 'arxiv'
509
+
510
+ papers.append(paper)
511
+
512
+ except ET.ParseError as e:
513
+ logger.error("Error parsing XML: %s", e)
514
+ raise
515
+
516
+ return papers
517
+
518
+
519
+ def filter_and_score_papers(
520
+ papers: List[Dict],
521
+ config: Dict,
522
+ target_date: Optional[datetime] = None,
523
+ is_hot_paper_batch: bool = False
524
+ ) -> List[Dict]:
525
+ """
526
+ 筛选和评分论文
527
+
528
+ Args:
529
+ papers: 论文列表
530
+ config: 研究配置
531
+ target_date: 目标日期(用于计算新近性)
532
+ is_hot_paper_batch: 是否是高影响力论文批次
533
+
534
+ Returns:
535
+ 筛选和评分后的论文列表
536
+ """
537
+ domains = config.get('research_domains', {})
538
+ excluded_keywords = config.get('excluded_keywords', [])
539
+
540
+ scored_papers = []
541
+
542
+ for paper in papers:
543
+ # 计算相关性
544
+ relevance, matched_domain, matched_keywords = calculate_relevance_score(
545
+ paper, domains, excluded_keywords
546
+ )
547
+
548
+ # 如果相关性为0,跳过
549
+ if relevance == 0:
550
+ continue
551
+
552
+ # 计算新近性
553
+ if 'published_date' in paper:
554
+ recency = calculate_recency_score(paper.get('published_date'))
555
+ else:
556
+ # 对于 Semantic Scholar 的论文,使用 publicationDate
557
+ pub_date_str = paper.get('publicationDate')
558
+ if pub_date_str:
559
+ try:
560
+ pub_date = datetime.strptime(pub_date_str, '%Y-%m-%d')
561
+ recency = calculate_recency_score(pub_date)
562
+ except (ValueError, TypeError):
563
+ recency = 0
564
+ else:
565
+ recency = 0
566
+
567
+ # 计算热门度
568
+ if is_hot_paper_batch:
569
+ # 高影响力论文:使用 influentialCitationCount
570
+ inf_cit = paper.get('influentialCitationCount', 0)
571
+ popularity = min(
572
+ inf_cit / (POPULARITY_INFLUENTIAL_CITATION_FULL_SCORE / SCORE_MAX),
573
+ SCORE_MAX,
574
+ )
575
+ else:
576
+ # 普通论文:基于摘要推断
577
+ summary = paper.get('summary', '') if 'summary' in paper else paper.get('abstract', '')
578
+ popularity = calculate_quality_score(summary) # 临时使用质量评分作为热门度
579
+
580
+ # 计算质量
581
+ summary = paper.get('summary', '') if 'summary' in paper else paper.get('abstract', '')
582
+ quality = calculate_quality_score(summary)
583
+
584
+ # 计算综合推荐评分
585
+ recommendation_score = calculate_recommendation_score(
586
+ relevance, recency, popularity, quality, is_hot_paper_batch
587
+ )
588
+
589
+ # 添加评分信息
590
+ paper['scores'] = {
591
+ 'relevance': round(relevance, 2),
592
+ 'recency': round(recency, 2),
593
+ 'popularity': round(popularity, 2),
594
+ 'quality': round(quality, 2),
595
+ 'recommendation': recommendation_score
596
+ }
597
+ # Flat score fields for the frontend
598
+ paper['relevance_score'] = round(relevance, 2)
599
+ paper['recency_score'] = round(recency, 2)
600
+ paper['popularity_score'] = round(popularity, 2)
601
+ paper['quality_score'] = round(quality, 2)
602
+ paper['final_score'] = recommendation_score
603
+ # Ensure 'abstract' field exists for the frontend
604
+ if 'abstract' not in paper and 'summary' in paper:
605
+ paper['abstract'] = paper['summary']
606
+ paper['matched_domain'] = matched_domain
607
+ paper['matched_keywords'] = matched_keywords
608
+ paper['is_hot_paper'] = is_hot_paper_batch
609
+
610
+ scored_papers.append(paper)
611
+
612
+ # 按推荐评分排序
613
+ scored_papers.sort(key=lambda x: x['scores']['recommendation'], reverse=True)
614
+
615
+ return scored_papers
616
+
617
+
618
+ def main():
619
+ """主函数"""
620
+ import argparse
621
+
622
+ default_config = os.environ.get('OBSIDIAN_VAULT_PATH', '')
623
+ if default_config:
624
+ default_config = os.path.join(default_config, '99_System', 'Config', 'research_interests.yaml')
625
+
626
+ parser = argparse.ArgumentParser(description='Search and filter arXiv papers with Semantic Scholar integration')
627
+ parser.add_argument('--config', type=str,
628
+ default=default_config or None,
629
+ help='Path to research interests config file (or set OBSIDIAN_VAULT_PATH env var)')
630
+ parser.add_argument('--output', type=str, default='arxiv_filtered.json',
631
+ help='Output JSON file path')
632
+ parser.add_argument('--max-results', type=int, default=200,
633
+ help='Maximum number of results to fetch from arXiv')
634
+ parser.add_argument('--top-n', type=int, default=10,
635
+ help='Number of top papers to return')
636
+ parser.add_argument('--target-date', type=str, default=None,
637
+ help='Target date (YYYY-MM-DD) for filtering')
638
+ parser.add_argument('--categories', type=str,
639
+ default='cs.AI,cs.LG,cs.CL,cs.CV,cs.MM,cs.MA,cs.RO',
640
+ help='Comma-separated list of arXiv categories')
641
+ parser.add_argument('--skip-hot-papers', action='store_true',
642
+ help='Skip searching hot papers from Semantic Scholar')
643
+
644
+ args = parser.parse_args()
645
+
646
+ logging.basicConfig(
647
+ level=logging.INFO,
648
+ format='%(asctime)s [%(levelname)s] %(message)s',
649
+ datefmt='%H:%M:%S',
650
+ stream=sys.stderr,
651
+ )
652
+
653
+ if not args.config:
654
+ logger.error("未指定配置文件路径。请通过 --config 参数或 OBSIDIAN_VAULT_PATH 环境变量设置。")
655
+ return 1
656
+
657
+ logger.info("Loading config from: %s", args.config)
658
+ config = load_research_config(args.config)
659
+
660
+ # 解析目标日期
661
+ target_date = None
662
+ if args.target_date:
663
+ try:
664
+ target_date = datetime.strptime(args.target_date, '%Y-%m-%d')
665
+ logger.info("Target date: %s", args.target_date)
666
+ except ValueError:
667
+ logger.error("Invalid target date format: %s", args.target_date)
668
+ return 1
669
+ else:
670
+ target_date = datetime.now()
671
+ logger.info("Using current date: %s", target_date.strftime('%Y-%m-%d'))
672
+
673
+ window_30d_start, window_30d_end, window_1y_start, window_1y_end = calculate_date_windows(target_date)
674
+ logger.info("Date windows:")
675
+ logger.info(" Recent 30 days: %s to %s", window_30d_start.date(), window_30d_end.date())
676
+ logger.info(" Past year (31-365 days): %s to %s", window_1y_start.date(), window_1y_end.date())
677
+
678
+ # 解析分类
679
+ categories = args.categories.split(',')
680
+
681
+ all_scored_papers = []
682
+ recent_papers = []
683
+ hot_papers = []
684
+
685
+ def _write_intermediate(papers_so_far, stage, total_recent=0, total_hot=0):
686
+ """Write intermediate results to the output file so the UI can poll progress."""
687
+ sorted_papers = sorted(papers_so_far, key=lambda x: x.get('scores', {}).get('recommendation', 0), reverse=True)
688
+ top = sorted_papers[:args.top_n]
689
+ intermediate = {
690
+ 'target_date': args.target_date or target_date.strftime('%Y-%m-%d'),
691
+ 'search_date': target_date.strftime('%Y-%m-%d'),
692
+ 'stage': stage,
693
+ 'total_found': total_recent + total_hot,
694
+ 'total_filtered': len(papers_so_far),
695
+ 'top_papers': top,
696
+ }
697
+ try:
698
+ with open(args.output, 'w', encoding='utf-8') as f:
699
+ json.dump(intermediate, f, ensure_ascii=False, indent=2, default=str)
700
+ except Exception:
701
+ pass
702
+
703
+ def _build_output(top_papers, total_unique):
704
+ total_found = len(recent_papers) + len(hot_papers)
705
+ return {
706
+ 'target_date': args.target_date or target_date.strftime('%Y-%m-%d'),
707
+ 'search_date': target_date.strftime('%Y-%m-%d'),
708
+ 'date_windows': {
709
+ 'recent_30d': {
710
+ 'start': window_30d_start.strftime('%Y-%m-%d'),
711
+ 'end': window_30d_end.strftime('%Y-%m-%d')
712
+ },
713
+ 'past_year': {
714
+ 'start': window_1y_start.strftime('%Y-%m-%d'),
715
+ 'end': window_1y_end.strftime('%Y-%m-%d')
716
+ }
717
+ },
718
+ 'total_found': total_found,
719
+ 'total_filtered': total_unique,
720
+ 'total_recent': len(recent_papers),
721
+ 'total_hot': len(hot_papers),
722
+ 'total_unique': total_unique,
723
+ 'top_papers': top_papers,
724
+ }
725
+
726
+ # ========== 第一步:搜索最近30天的论文(arXiv)==========
727
+ logger.info("=" * 70)
728
+ logger.info("Step 1: Searching recent papers (last 30 days) from arXiv")
729
+ logger.info("=" * 70)
730
+
731
+ recent_papers, recent_fetch_succeeded = search_arxiv_by_date_range(
732
+ categories=categories,
733
+ start_date=window_30d_start,
734
+ end_date=window_30d_end,
735
+ max_results=args.max_results
736
+ )
737
+
738
+ if not recent_fetch_succeeded:
739
+ return 1
740
+
741
+ if recent_papers:
742
+ scored_recent = filter_and_score_papers(
743
+ papers=recent_papers,
744
+ config=config,
745
+ target_date=target_date,
746
+ is_hot_paper_batch=False
747
+ )
748
+ logger.info("Scored %d recent papers", len(scored_recent))
749
+ all_scored_papers.extend(scored_recent)
750
+ _write_intermediate(all_scored_papers, 'recent_done', total_recent=len(recent_papers))
751
+ else:
752
+ logger.warning("No recent papers found")
753
+
754
+ # ========== 第二步:搜索过去一年的高影响力论文(Semantic Scholar)==========
755
+ # NOTE: Disabled — currently only fetching from arXiv directly.
756
+ # if not args.skip_hot_papers:
757
+ # logger.info("=" * 70)
758
+ # logger.info("Step 2: Searching hot papers (past year) from Semantic Scholar")
759
+ # logger.info("=" * 70)
760
+ #
761
+ # hot_papers = search_hot_papers_from_categories(
762
+ # categories=categories,
763
+ # start_date=window_1y_start,
764
+ # end_date=window_1y_end,
765
+ # top_k_per_category=5
766
+ # )
767
+ #
768
+ # if hot_papers:
769
+ # scored_hot = filter_and_score_papers(
770
+ # papers=hot_papers,
771
+ # config=config,
772
+ # target_date=target_date,
773
+ # is_hot_paper_batch=True
774
+ # )
775
+ # logger.info("Scored %d hot papers", len(scored_hot))
776
+ # all_scored_papers.extend(scored_hot)
777
+ # _write_intermediate(all_scored_papers, 'hot_done', total_recent=len(recent_papers), total_hot=len(hot_papers))
778
+ # else:
779
+ # logger.warning("No hot papers found from Semantic Scholar")
780
+ # else:
781
+ # logger.info("Skipping hot paper search (disabled by user)")
782
+ logger.info("Skipping Semantic Scholar hot paper search (disabled)")
783
+
784
+ # ========== 第三步:合并结果并排序 ==========
785
+ logger.info("=" * 70)
786
+ logger.info("Step 3: Merging and ranking results")
787
+ logger.info("=" * 70)
788
+
789
+ # 按推荐评分排序
790
+ all_scored_papers.sort(key=lambda x: x['scores']['recommendation'], reverse=True)
791
+
792
+ # 去重(基于 arXiv ID)
793
+ seen_ids = set()
794
+ unique_papers = []
795
+ for p in all_scored_papers:
796
+ arxiv_id = p.get('arxiv_id') or p.get('arxivId')
797
+ if arxiv_id:
798
+ if arxiv_id not in seen_ids:
799
+ seen_ids.add(arxiv_id)
800
+ unique_papers.append(p)
801
+ else:
802
+ # 没有 arXiv ID 的,使用标题去重
803
+ title = p.get('title', '')
804
+ if title not in seen_ids:
805
+ seen_ids.add(title)
806
+ unique_papers.append(p)
807
+
808
+ logger.info("Total unique papers after deduplication: %d", len(unique_papers))
809
+
810
+ if len(unique_papers) == 0:
811
+ logger.warning("No papers matched the criteria!")
812
+ output = _build_output([], 0)
813
+ with open(args.output, 'w', encoding='utf-8') as f:
814
+ json.dump(output, f, ensure_ascii=False, indent=2, default=str)
815
+ logger.info("Results saved to: %s", args.output)
816
+ print(json.dumps(output, ensure_ascii=True, indent=2, default=str))
817
+ return 0
818
+
819
+ # 取前 N 篇
820
+ top_papers = unique_papers[:args.top_n]
821
+
822
+ # 准备输出
823
+ output = _build_output(top_papers, len(unique_papers))
824
+
825
+ # 保存结果
826
+ with open(args.output, 'w', encoding='utf-8') as f:
827
+ json.dump(output, f, ensure_ascii=False, indent=2, default=str)
828
+
829
+ logger.info("Results saved to: %s", args.output)
830
+ logger.info("Top %d papers:", len(top_papers))
831
+ for i, p in enumerate(top_papers, 1):
832
+ hot_marker = " [HOT]" if p.get('is_hot_paper') else ""
833
+ logger.info(" %d. %s... (Score: %s)%s", i, p.get('title', 'N/A')[:60], p['scores']['recommendation'], hot_marker)
834
+
835
+ # 同时输出到 stdout
836
+ print(json.dumps(output, ensure_ascii=True, indent=2, default=str))
837
+
838
+ return 0
839
+
840
+
841
+ if __name__ == '__main__':
842
+ sys.exit(main())