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.
- package/LICENSE +1380 -0
- package/NOTICE +17 -0
- package/README.md +875 -0
- package/README.zh-CN.md +857 -0
- package/dist/api-docs.html +878 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/_basePickBy-C0n1uo4u.js +1 -0
- package/dist/assets/_baseUniq-BRNmhFkc.js +1 -0
- package/dist/assets/arc-DOMBmC_A.js +1 -0
- package/dist/assets/architectureDiagram-VXUJARFQ-CtMWglbD.js +36 -0
- package/dist/assets/blockDiagram-VD42YOAC-DK_gTkJy.js +122 -0
- package/dist/assets/c4Diagram-YG6GDRKO-DrDAvsn7.js +10 -0
- package/dist/assets/channel-BFOE7OyC.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-CGbhdZcN.js +1 -0
- package/dist/assets/chunk-55IACEB6-sGeFSxen.js +1 -0
- package/dist/assets/chunk-B4BG7PRW-RxgHAU_P.js +165 -0
- package/dist/assets/chunk-DI55MBZ5-CvHUXZrx.js +220 -0
- package/dist/assets/chunk-FMBD7UC4-Sn7cXVNU.js +15 -0
- package/dist/assets/chunk-QN33PNHL-bDKOHppN.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-B9kN5LGm.js +1 -0
- package/dist/assets/chunk-TZMSLE5B-CCBe6len.js +1 -0
- package/dist/assets/classDiagram-2ON5EDUG-DQoMNYlY.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-DQoMNYlY.js +1 -0
- package/dist/assets/clone-BulQSblH.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-r6sLiHBW.js +1 -0
- package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
- package/dist/assets/dagre-6UL2VRFP-AbsaR1Oa.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-PSM6KHXK-DjCvdUyy.js +24 -0
- package/dist/assets/diagram-QEK2KX5R-CqiocXkH.js +43 -0
- package/dist/assets/diagram-S2PKOQOG-D5ALeoII.js +24 -0
- package/dist/assets/erDiagram-Q2GNP2WA-C1AeOsiz.js +60 -0
- package/dist/assets/flowDiagram-NV44I4VS-C98ve_DO.js +162 -0
- package/dist/assets/ganttDiagram-JELNMOA3-3zdLvcxD.js +267 -0
- package/dist/assets/gitGraphDiagram-V2S2FVAM-nDuDMsHh.js +65 -0
- package/dist/assets/graph-Blm-emkR.js +1 -0
- package/dist/assets/index-B8Yutsnm.css +1 -0
- package/dist/assets/index-Cf6gyhc3.js +654 -0
- package/dist/assets/infoDiagram-HS3SLOUP-CmzE3q0R.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/journeyDiagram-XKPGCS4Q-CYaW7LrP.js +139 -0
- package/dist/assets/kanban-definition-3W4ZIXB7-mkmW7p0j.js +89 -0
- package/dist/assets/layout-Ct2kFIId.js +1 -0
- package/dist/assets/linear-CKG9EwIJ.js +1 -0
- package/dist/assets/mermaid.core-CaKcUnnd.js +250 -0
- package/dist/assets/mindmap-definition-VGOIOE7T-Bct-pYin.js +68 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-ADFJNKIX-C8wFD4Jr.js +30 -0
- package/dist/assets/quadrantDiagram-AYHSOK5B-CuiE_f46.js +7 -0
- package/dist/assets/requirementDiagram-UZGBJVZJ-C4v1CqU8.js +64 -0
- package/dist/assets/sankeyDiagram-TZEHDZUN-keAqqs-Y.js +10 -0
- package/dist/assets/sequenceDiagram-WL72ISMW-ClNbvKTB.js +145 -0
- package/dist/assets/stateDiagram-FKZM4ZOC-D8We-2vq.js +1 -0
- package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +1 -0
- package/dist/assets/timeline-definition-IT6M3QCI-DsvEMT5T.js +61 -0
- package/dist/assets/treemap-GDKQZRPO-TJrzN7Bj.js +162 -0
- package/dist/assets/vendor-codemirror-ansM-eig.js +33 -0
- package/dist/assets/vendor-react-BWF6YQQ3.js +8 -0
- package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
- package/dist/assets/xychartDiagram-PRI3JC2R-BdATmY5F.js +7 -0
- package/dist/clear-cache.html +85 -0
- package/dist/convert-icons.md +53 -0
- package/dist/dr-claw.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/generate-icons.js +49 -0
- package/dist/icons/claude-ai-icon.svg +1 -0
- package/dist/icons/codex-white.svg +3 -0
- package/dist/icons/codex.svg +3 -0
- package/dist/icons/cursor-white.svg +12 -0
- package/dist/icons/cursor.svg +1 -0
- package/dist/icons/file.svg +16997 -0
- package/dist/icons/generate-icons.md +19 -0
- package/dist/icons/icon-128x128.png +0 -0
- package/dist/icons/icon-144x144.png +0 -0
- package/dist/icons/icon-152x152.png +0 -0
- package/dist/icons/icon-192x192.png +0 -0
- package/dist/icons/icon-384x384.png +0 -0
- package/dist/icons/icon-512x512.png +0 -0
- package/dist/icons/icon-72x72.png +0 -0
- package/dist/icons/icon-96x96.png +0 -0
- package/dist/icons/news/arxiv.svg +1 -0
- package/dist/icons/news/huggingface.svg +8 -0
- package/dist/icons/news/x-black.png +0 -0
- package/dist/icons/news/x-white.png +0 -0
- package/dist/icons/news/xiaohongshu.png +0 -0
- package/dist/index.html +51 -0
- package/dist/leveraged-cognition.png +0 -0
- package/dist/manifest.json +61 -0
- package/dist/screenshots/chat.png +0 -0
- package/dist/screenshots/new_claw.png +0 -0
- package/dist/screenshots/news_dashboard.png +0 -0
- package/dist/screenshots/project_dashboard.png +0 -0
- package/dist/screenshots/research_lab.png +0 -0
- package/dist/screenshots/skill_library.png +0 -0
- package/dist/screenshots/task_list.png +0 -0
- package/dist/screenshots/terminal_example.png +0 -0
- package/dist/screenshots/terminal_example1.png +0 -0
- package/dist/sw.js +58 -0
- package/dist/wechat-group-qr.jpg +0 -0
- package/package.json +133 -0
- package/public/api-docs.html +878 -0
- package/public/clear-cache.html +85 -0
- package/public/convert-icons.md +53 -0
- package/public/dr-claw.png +0 -0
- package/public/favicon.png +0 -0
- package/public/generate-icons.js +49 -0
- package/public/icons/claude-ai-icon.svg +1 -0
- package/public/icons/codex-white.svg +3 -0
- package/public/icons/codex.svg +3 -0
- package/public/icons/cursor-white.svg +12 -0
- package/public/icons/cursor.svg +1 -0
- package/public/icons/file.svg +16997 -0
- package/public/icons/generate-icons.md +19 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/icons/news/arxiv.svg +1 -0
- package/public/icons/news/huggingface.svg +8 -0
- package/public/icons/news/x-black.png +0 -0
- package/public/icons/news/x-white.png +0 -0
- package/public/icons/news/xiaohongshu.png +0 -0
- package/public/leveraged-cognition.png +0 -0
- package/public/manifest.json +61 -0
- package/public/screenshots/chat.png +0 -0
- package/public/screenshots/new_claw.png +0 -0
- package/public/screenshots/news_dashboard.png +0 -0
- package/public/screenshots/project_dashboard.png +0 -0
- package/public/screenshots/research_lab.png +0 -0
- package/public/screenshots/skill_library.png +0 -0
- package/public/screenshots/task_list.png +0 -0
- package/public/screenshots/terminal_example.png +0 -0
- package/public/screenshots/terminal_example1.png +0 -0
- package/public/sw.js +58 -0
- package/public/wechat-group-qr.jpg +0 -0
- package/scripts/export-skills-catalog-v2.mjs +746 -0
- package/scripts/fix-node-pty.js +45 -0
- package/scripts/gemini_image_core.py +470 -0
- package/scripts/install-python-tools.js +64 -0
- package/server/AGENTS.md +73 -0
- package/server/__tests__/gemini-legacy-thought.test.mjs +88 -0
- package/server/__tests__/runtimePorts.test.mjs +115 -0
- package/server/__tests__/session-delete.test.mjs +166 -0
- package/server/bin/compute-node +70 -0
- package/server/claude-sdk.js +799 -0
- package/server/cli-chat.js +488 -0
- package/server/cli.js +354 -0
- package/server/compute-node.js +439 -0
- package/server/constants/config.js +5 -0
- package/server/cursor-cli.js +340 -0
- package/server/database/auth.db +0 -0
- package/server/database/db.js +1852 -0
- package/server/database/init.sql +194 -0
- package/server/gemini-cli.js +1352 -0
- package/server/index.js +3154 -0
- package/server/load-env.js +71 -0
- package/server/local-gpu.js +860 -0
- package/server/middleware/auth.js +100 -0
- package/server/openai-codex.js +724 -0
- package/server/openrouter.js +890 -0
- package/server/project-token-usage.js +407 -0
- package/server/projects.js +3973 -0
- package/server/routes/agent.js +1272 -0
- package/server/routes/auth.js +128 -0
- package/server/routes/auto-research.js +558 -0
- package/server/routes/cli-auth.js +958 -0
- package/server/routes/codex.js +344 -0
- package/server/routes/commands.js +601 -0
- package/server/routes/compute.js +358 -0
- package/server/routes/cursor.js +795 -0
- package/server/routes/git.js +1348 -0
- package/server/routes/mcp-utils.js +48 -0
- package/server/routes/mcp.js +618 -0
- package/server/routes/news.js +711 -0
- package/server/routes/projects.js +633 -0
- package/server/routes/references.js +338 -0
- package/server/routes/settings.js +285 -0
- package/server/routes/skills.js +914 -0
- package/server/routes/taskmaster.js +2566 -0
- package/server/routes/telemetry.js +89 -0
- package/server/routes/user.js +155 -0
- package/server/scripts/fix_session_names.js +57 -0
- package/server/scripts/research-news/common_words.py +19 -0
- package/server/scripts/research-news/link_keywords.py +298 -0
- package/server/scripts/research-news/scan_existing_notes.py +266 -0
- package/server/scripts/research-news/scoring_utils.py +227 -0
- package/server/scripts/research-news/search_arxiv.py +842 -0
- package/server/scripts/research-news/search_huggingface.py +438 -0
- package/server/scripts/research-news/search_x.py +490 -0
- package/server/scripts/research-news/search_xiaohongshu.py +610 -0
- package/server/taskmaster-templates/README.md +150 -0
- package/server/taskmaster-templates/README.zh-CN.md +150 -0
- package/server/taskmaster-templates/ai-research-dataset.json +230 -0
- package/server/taskmaster-templates/ai-research-method-model.json +230 -0
- package/server/taskmaster-templates/ai-research-position-paper.json +230 -0
- package/server/telemetry.js +121 -0
- package/server/templates/AGENTS.md +103 -0
- package/server/templates/CLAUDE.md +104 -0
- package/server/templates/GEMINI.md +103 -0
- package/server/templates/cursor-project.md +108 -0
- package/server/templates/index.js +37 -0
- package/server/utils/__tests__/sessionTokenUsage.test.js +82 -0
- package/server/utils/auto-research-mailer.js +57 -0
- package/server/utils/cliResolution.js +170 -0
- package/server/utils/commandParser.js +303 -0
- package/server/utils/cursorCommand.js +61 -0
- package/server/utils/geminiApiKey.js +28 -0
- package/server/utils/gitConfig.js +24 -0
- package/server/utils/imageAttachmentFiles.js +35 -0
- package/server/utils/mcp-detector.js +198 -0
- package/server/utils/parsers/bibtex-parser.js +207 -0
- package/server/utils/permissions.js +100 -0
- package/server/utils/runtimePorts.js +212 -0
- package/server/utils/sessionFormatting.js +68 -0
- package/server/utils/sessionIndex.js +100 -0
- package/server/utils/sessionMode.js +95 -0
- package/server/utils/sessionTokenUsage.js +105 -0
- package/server/utils/taskmaster-websocket.js +129 -0
- package/server/utils/zotero-client.js +136 -0
- package/shared/errorClassifier.js +102 -0
- package/shared/geminiThinkingSupport.d.ts +22 -0
- package/shared/geminiThinkingSupport.js +96 -0
- package/shared/geminiThoughtParser.d.ts +8 -0
- package/shared/geminiThoughtParser.js +30 -0
- 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())
|