关于 paper-fetch
此命令行工具可根据 DOI 或标题解析论文,并按顺序尝试多个来源下载 PDF。它还支持批量任务及适用于 Agent 工作流的结构化输出。
主要功能
- 01通过 DOI 或标题解析论文
- 02按优先级尝试 PDF 来源
- 03批量下载多篇论文
- 04输出 JSON 和进度事件
- 05提供机器可读的 CLI Schema
适用场景
- 01通过 DOI 获取论文 PDF
- 02根据论文标题查找 PDF
- 03批量下载论文清单
- 04依据结构化错误安排重试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents365-ai/ paper-fetch --skill paper-fetch安装后在 Claude Code 中输入/paper-fetch确认已加载。
附带文件scripts/cloak_pdf.pyscripts/fetch.py
详细介绍
paper-fetch
根据 DOI(或标题)获取论文的 PDF。按优先级依次尝试多个来源,并在找到第一个结果后停止。
解析顺序
- Unpaywall —
https://api.unpaywall.org/v2/{doi}?email=$UNPAYWALL_EMAIL,读取best_oa_location.url_for_pdf(若未设置UNPAYWALL_EMAIL,则跳过) - Semantic Scholar —
https://api.semanticscholar.org/graph/v1/paper/DOI:{doi}?fields=openAccessPdf,externalIds - arXiv — 若存在
externalIds.ArXiv,则使用https://arxiv.org/pdf/{arxiv_id}.pdf - PubMed Central OA — 若存在 PMCID,则使用
https://www.ncbi.nlm.nih.gov/pmc/articles/{pmcid}/pdf/ - bioRxiv / medRxiv — 若 DOI 前缀为
10.1101,则查询https://api.biorxiv.org/details/{server}/{doi}获取最新版本的 PDF URL - 出版商直连 (仅限机构模式 —
PAPER_FETCH_INSTITUTIONAL=1) — 将 DOI 前缀映射到出版商 PDF 模板(Nature / Science / Wiley / Springer / ACS / PNAS / NEJM / Sage / T&F / Elsevier)。授权下载的是调用方自己的订阅 IP / Cookie / EZproxy;未授权的响应无法通过%PDF检查,因此会继续尝试第 7 步。 - Sci-Hub 镜像 (默认启用;使用
PAPER_FETCH_NO_SCIHUB=1禁用) — 最后的备用方案。按顺序尝试PAPER_FETCH_SCIHUB_MIRRORS中的镜像列表(或内置默认列表sci-hub.ru、sci-hub.st、sci-hub.su、sci-hub.box、sci-hub.red、sci-hub.al、sci-hub.mk、sci-hub.ee);若全部失败,则在每个进程中抓取一次https://www.sci-hub.pub/,获取最新镜像。验证码 / 缺少论文的页面没有 PDF iframe,因此会静默跳过。 - 否则 → 报告失败,并附上标题 / 作者信息,以便用户通过馆际互借(ILL)申请
CloakBrowser 备用方案(下载层,可选启用 — PAPER_FETCH_CLOAK=1)。 这不是单独的数据源:它位于下载瓶颈处,因此适用于上述任意来源。当 Cloudflare 阻止已解析出的 PDF URL 时——HTTP 403/429,或返回“Just a moment…” HTML 中间页而非文件——且操作员已选择启用时,会通过 cloak_pdf.py 辅助程序使用 CloakBrowser(能够通过 JS 验证的隐身 Chromium)重试该 URL。返回的字节会通过相同的 %PDF 魔数 + 50 MB 检查进行重新验证;成功后,结果会包含 via: "cloak"。默认关闭,且失败时会安全关闭(缺少 CloakBrowser 时静默跳过);Agent 无法选择启用——参见下文的 CloakBrowser access。
如果只提供标题,请通过 --title "<title>" 直接传入。解析链如下:
- Crossref
query.title— 首选;涵盖所有主要期刊 / 会议 DOI - Semantic Scholar
/paper/search/match— 当 Crossref 的首选匹配置信度较低(match_score < 40)或与次选结果的分差小于< 3时,作为备用方案。关键在于,S2 能覆盖仅有 arXiv 的预印本(没有 Crossref DOI)。当 S2 找到的论文只有 arXiv id 时,会合成规范的10.48550/arXiv.<id>,以保持下载链统一。 - Crossref 的最佳猜测(低置信度) — 仅当两个解析器都难以确定时使用。结果封套会设置
meta.title_resolution.low_confidence: true,并包含low_confidence_reason(score_below_threshold/ambiguous_runner_up),以便 Agent 决定放弃或通过--dry-run确认。
无论哪种情况,解析出的 DOI、胜出的解析器、完整的 resolvers_tried 列表以及排名靠前的候选匹配都会显示在 meta.title_resolution 下。
如果已注册 semanticscholar-skill,它可以作为更丰富的标题 → DOI 解析前置步骤——适用于还需要相关性排序、摘要搜索或引文上下文,而不仅仅是 DOI 的情况。Agent 编写 Python 脚本,使用该技能的 match_title() 读取 externalIds.DOI,然后运行 paper-fetch <doi>。如果结果只有 ArXiv id(没有 DOI),则合成 10.48550/arXiv.<ArXiv> 并传给 paper-fetch。
如果只需要 DOI,--title 是单命令路径——paper-fetch 内置的 Crossref → S2 链可以处理大多数情况。
用法
python scripts/fetch.py <DOI> [options]
python scripts/fetch.py --title "<paper title>" [options]
python scripts/fetch.py --batch <FILE|-> [options]
python scripts/fetch.py schema # machine-readable self-description
参数
以下参数是 Agent 在常规使用中会组合使用的参数。完整契约(包括 --dry-run、--pretty、--stream、--overwrite、--timeout、--version,以及参数类型和退出码映射)请运行 python scripts/fetch.py schema(机器可读,并通过 schema_version 检查是否存在差异)。
| 参数 | 默认值 | 描述 |
|---|---|---|
doi | — | 要获取的 DOI(位置参数)。使用 - 从标准输入读取单个 DOI |
--title TITLE | — | 论文标题;下载前会通过 Crossref 解析为 DOI。不能与位置参数 DOI / --batch 同时使用 |
--batch FILE | — | 每行包含一个 DOI 的文件,用于批量下载。使用 - 从标准输入读取 |
--out DIR | pdfs | 输出目录 |
--format | auto | 面向 Agent 使用 json,面向用户使用 text。自动检测:当 stdout 不是 TTY 时使用 json,是 TTY 时使用 text |
--idempotency-key KEY | — | 安全重试键。使用相同的键重新运行时,会从 <out>/.paper-fetch-idem/ 重放原始信封,不会进行网络 I/O |
Agent 发现:schema 子命令
python scripts/fetch.py schema
在 stdout 上输出 CLI 的完整机器可读描述(不进行网络访问)。包含 cli_version、schema_version、参数类型、退出码、错误码、信封结构和环境变量。Agent 应读取一次,将其与 schema_version 一同缓存;当缓存版本不匹配时重新读取。
输出契约
stdout 输出单个 JSON 信封。每个信封都包含一个 meta 槽位。
成功(所有 DOI 均已解析):
{
"ok": true,
"data": {
"results": [
{
"doi": "10.1038/s41586-021-03819-2",
"success": true,
"source": "unpaywall",
"pdf_url": "https://www.nature.com/articles/s41586-021-03819-2.pdf",
"file": "pdfs/Jumper_2021_Highly_accurate_protein_structure_predic.pdf",
"meta": {"title": "Highly accurate protein structure prediction with AlphaFold", "year": 2021, "author": "Jumper"},
"sources_tried": ["unpaywall"]
}
],
"summary": {"total": 1, "succeeded": 1, "failed": 0},
"next": []
},
"meta": {
"request_id": "req_a908f5156fc1",
"latency_ms": 2036,
"schema_version": "1.9.0",
"cli_version": "0.13.1",
"sources_tried": ["unpaywall"]
}
}
部分成功(批量模式——部分 DOI 获取失败,退出码反映失败类别):
{
"ok": "partial",
"data": {
"results": [
{ "doi": "10.1038/s41586-021-03819-2", "success": true, "source": "unpaywall", ... },
{
"doi": "10.1234/nonexistent",
"success": false,
"source": null,
"pdf_url": null,
"file": null,
"meta": {},
"sources_tried": ["unpaywall", "semantic_scholar"],
"error": {
"code": "not_found",
"message": "No open-access PDF found",
"retryable": true,
"retry_after_hours": 168,
"reason": "OA availability changes over time; retry after embargo lifts or preprint appears"
}
}
],
"summary": {"total": 2, "succeeded": 1, "failed": 1},
"next": ["paper-fetch 10.1234/nonexistent --out pdfs"]
},
"meta": { ... }
}
next 槽位是建议后续执行的命令数组:重新调用这些命令只会重试失败的子集。结合 --idempotency-key 使用,可安全重试整个批次,而不会重新下载已成功的项目。
失败(参数错误,退出码为 3):
{
"ok": false,
"error": {
"code": "validation_error",
"message": "Provide a DOI or --batch file",
"retryable": false
},
"meta": { ... }
}
单项跳过(目标文件已存在,且未指定 --overwrite):
{
"doi": "10.1038/s41586-021-03819-2",
"success": true,
"source": "unpaywall",
"pdf_url": "https://...",
"file": "pdfs/Jumper_2021_...pdf",
"skipped": true,
"skip_reason": "file_exists",
"sources_tried": ["unpaywall"]
}
幂等重放(使用相同的 --idempotency-key 重新运行):
缓存的信封会原样返回,但 meta.request_id 和 meta.latency_ms 会针对当前调用重新盖戳,并设置 meta.replayed_from_idempotency_key。不会进行网络 I/O。
Stderr 进度(NDJSON)
当使用 --format json 时,stderr 会逐行输出一个 JSON 对象,用于表示运行状态:
{"event": "session", "request_id": "req_...", "elapsed_ms": 0, "cli_version": "0.13.1", "schema_version": "1.9.0"}
{"event": "start", "request_id": "req_...", "elapsed_ms": 2, "doi": "10.1038/..."}
{"event": "source_try", "request_id": "req_...", "elapsed_ms": 2, "doi": "...", "source": "unpaywall"}
{"event": "source_hit", "request_id": "req_...", "elapsed_ms": 2036, "doi": "...", "source": "unpaywall", "pdf_url": "..."}
{"event": "download_ok", "request_id": "req_...", "elapsed_ms": 4120, "doi": "...", "file": "..."}
事件类型:session、start、source_try、source_hit、source_miss、source_skip、source_enrich、source_enrich_failed、download_ok、download_error、download_skip、dry_run、not_found、resolve_error。所有事件都包含 request_id 和 elapsed_ms,使编排器能够关联 stderr 中的进度与最终的 stdout 信封。每次调用都会在处理任何 DOI 或进行任何网络 I/O 之前触发一次 session 事件,并携带 cli_version / schema_version,因此 Agent 无需等待最终信封即可将缓存副本与当前版本比较,检测架构是否存在差异。
当调用 Semantic Scholar 仅用于补齐缺失的 author / title,而另一个来源已提供 PDF URL 时,会触发 source_enrich;其 fields 数组会准确列出已补齐的字段。若该补充信息调用失败,则会触发 source_enrich_failed——仍会使用 Unpaywall PDF URL,文件名则回退为 unknown_<year>_…。
当使用 --format text 时,stderr 会输出人类可读的文本。
退出代码
| 代码 | 含义 | 可重试类别 |
|---|---|---|
0 | 所有 DOI 均已解析/预览 | — |
1 | 未解析——一个或多个 DOI 没有开放获取副本;没有传输故障 | 暂不重试(在 retry_after_hours 后重试) |
2 | 保留用于身份验证错误(当前未使用) | — |
3 | 验证错误(参数错误、缺少输入) | 否 |
4 | 传输错误(网络/下载/IO 故障) | 是 |
此分类法让编排器能够确定性地处理故障:退出代码 4 值得立即重试,退出代码 1 不值得重试,退出代码 3 则是调用方的错误。
JSON 中的错误代码
每个可重试错误都会在错误对象中附带 retry_after_hours 提示,编排器无需猜测即可安排重试。
| 代码 | 含义 | 可重试 | retry_after_hours |
|---|---|---|---|
validation_error | 参数错误或输入为空 | 否 | — |
title_resolve_failed | Crossref 未返回与给定 --title 查询匹配的条目(请尝试使用更完整/更规范的标题,或直接传入 DOI) | 否 | — |
not_found | 未找到开放获取 PDF | 是 | 168(一周——开放获取资源通常会按禁运期/预印本的时间尺度上线) |
resolve_network_error | 元数据解析器遇到传输错误(超时/5xx/403);开放获取状态未知——应重试,而不是视为 not_found。映射为退出代码 4。 | 是 | 1 |
download_network_error | 下载期间发生网络故障 | 是 | 1 |
download_not_a_pdf | 响应不是 PDF(而是 HTML 落地页) | 否 | — |
download_host_not_allowed | PDF URL 未通过 SSRF 安全检查(私有 IP/非 http(s)/非 80、443 端口/被屏蔽的元数据主机/主机名解析到私有地址空间/不安全的重定向跳转) | 否 | — |
download_size_exceeded | 响应超过 50 MB 限制 | 是 | 24 |
download_io_error | 本地文件系统写入失败 | 是 | 1 |
internal_error | 意外错误 | 否 | — |
规范映射位于 scripts/fetch.py 中的 RETRY_AFTER_HOURS,并通过 schema.error_codes 提供。
示例
# Single DOI (JSON output when piped; text when in a terminal)
python scripts/fetch.py 10.1038/s41586-020-2649-2
# Single title (resolved to DOI via Crossref, then downloaded)
python scripts/fetch.py --title "Highly accurate protein structure prediction with AlphaFold"
# Dry-run preview (resolve without downloading)
python scripts/fetch.py 10.1038/s41586-020-2649-2 --dry-run
# Title + dry-run — preview the resolved DOI and candidate matches
python scripts/fetch.py --title "Attention Is All You Need" --dry-run
# Force JSON (for agents even inside a terminal)
python scripts/fetch.py 10.1038/s41586-020-2649-2 --format json
# Human-readable with pretty colors in a pipeline
python scripts/fetch.py 10.1038/s41586-020-2649-2 --format text
# Batch download, safely retriable
python scripts/fetch.py --batch dois.txt --out ./papers \
--idempotency-key monday-review-batch
# Pipe DOIs from another tool
zot -F ids.json query ... | jq -r '.[].doi' | python scripts/fetch.py --batch -
# Agent discovery
python scripts/fetch.py schema --pretty
# Streaming mode — one result per line as each DOI resolves
python scripts/fetch.py --batch dois.txt --stream
# Works without UNPAYWALL_EMAIL (skips Unpaywall, uses remaining 4 sources)
python scripts/fetch.py 10.1038/s41586-020-2649-2
环境
| 变量 | 默认值 | 用途 |
|---|---|---|
UNPAYWALL_EMAIL | 未设置 | 用于 Unpaywall API 的联系邮箱。可选,但建议设置。未设置时会跳过 Unpaywall(其余来源仍可正常使用)。 |
PAPER_FETCH_INSTITUTIONAL | 未设置 | 设置为任意值(例如 1)即可启用机构模式——激活 1 req/s 速率限制和出版商直连回退方案。见下文。 |
PAPER_FETCH_NO_SCIHUB | 未设置 | 设置为任意值即可禁用 Sci-Hub 回退方案(第 7 步)。 |
PAPER_FETCH_SCIHUB_MIRRORS | 未设置 | 按优先级顺序尝试的镜像主机名,以逗号分隔(例如 sci-hub.ru,sci-hub.st,sci-hub.su)。覆盖内置默认值。 |
PAPER_FETCH_CLOAK | 未设置 | 设置为任意值即可启用 CloakBrowser 回退方案——通过隐身 Chromium 重试被 Cloudflare 拦截的 PDF(HTTP 403/429 或非 PDF 中间页)。见下文的 CloakBrowser 访问。 |
CLOAKBROWSER_PYTHON | 自动 | 可用于 import cloakbrowser 的 Python 路径,供隐身回退方案使用。自动检测顺序:此变量 → ~/github/CloakBrowser/.venv/bin/python → 当前解释器。 |
PAPER_FETCH_CLOAK_HEADED | 未设置 | 设置为任意值即可启动有头(可见)浏览器,而非无头浏览器。较难应对的 Cloudflare 验证(例如 science.org)会使无头模式失效,只有在真实窗口中才能通过——如果隐身回退方案持续返回 HTTP 403 / "Just a moment…",请设置此变量。需要显示设备。 |
CloakBrowser 访问(选择加入)
一些出版商(例如 science.org)位于 Cloudflare 后面,Cloudflare 会向普通 HTTP 客户端返回 403/429,或显示“Just a moment…”JS 挑战页面,而不是 PDF。因此,即使浏览器可以正常访问该 URL,默认的 urllib 下载也无法通过。CloakBrowser 是一种能够通过这些挑战的隐身 Chromium。此技能借鉴了 cloakFetch 的实现方式。
选择加入: export PAPER_FETCH_CLOAK=1(还需要可导入 cloakbrowser 的 Python——请参阅 CLOAKBROWSER_PYTHON)。
工作原理:回退逻辑位于下载层,而不是作为新的来源,因此适用于任何已解析的 URL(Unpaywall、出版商直链、Sci-Hub,等等)。遇到 Cloudflare 拦截时,fetch.py 会通过已解析的 Python 调用配套脚本 cloak_pdf.py;CloakBrowser 会加载 PDF 主机的源站以解决 JS 挑战,然后使用页面内的 fetch() 获取 PDF(这样请求会携带浏览器的真实指纹和 cf_clearance Cookie),并通过 stdout 返回字节。fetch.py 本身仅使用标准库——绝不会导入 cloakbrowser。
**无头与有头模式。**配套脚本默认以无头模式运行。某些挑战(例如 science.org)会让无头 Chromium 无法通过,并一直卡在“Just a moment…”页面——设置 PAPER_FETCH_CLOAK_HEADED=1 可使用可见窗口,从而通过挑战。已验证:普通客户端访问 www.science.org/doi/pdf/… PDF 会返回 403,而通过有头 Cloak 回退则可以顺利下载。
**仅限同源。**页面内的 fetch 请求仅限同源,因此当已解析的 URL 是被拦截主机上的 PDF 直链时,回退机制可以正常工作(例如 www.science.org/doi/pdf/…)。如果 URL 会重定向到其他源(例如直接使用 doi.org/… 链接),或者旧版主机上的挑战始终无法通过,则会安全失败,并转而尝试下一个来源。
保持不变的部分:
- 返回的字节会通过相同的
%PDF魔数检查和 50 MB 大小上限进行重新验证。浏览器启动前,URL 仍须通过 SSRF 防护检查。 - **不会解决 CAPTCHA。**CloakBrowser 可以通过自动化 JS 挑战,但无法处理交互式挑战;交互式 Turnstile 仍会安全失败并转而尝试下一个来源。
- **安全失败。**没有可导入
cloakbrowser的 Python、缺少辅助脚本或出现任何错误时,都会静默转而尝试下一个来源。绝不会告知 Agent 被拦截的下载已成功。 - Agent 无法自行选择加入——必须由人工操作员设置
PAPER_FETCH_CLOAK。其信任边界与机构模式相同。 - 成功时,每个结果对象都会携带
via: "cloak",以便编排器识别已使用回退机制。
**成本:**触发回退时会启动一个真实浏览器(约需 20–40 s;首次下载 Chromium 约为 200 MB)。只有在普通下载被拦截后才会触发,因此正常情况下不受影响。
机构访问(主动启用)
许多研究人员通过所在机构的 IP 范围(在校园内或通过 VPN)合法地拥有订阅访问权限。Paper-fetch 可以利用这项访问权限:由出版商自己的身份验证机制(你的 IP、你的会话 Cookie)决定是否提供 PDF。
两种模式的主机可达性没有区别——公共模式已经信任 OA API(Unpaywall、Semantic Scholar、bioRxiv、PMC)返回的 URL,并会获取任何通过 SSRF 防护的 HTTPS 主机。机构模式增加了两项功能:(1)直接访问出版商的回退机制(上文第 6 步):当所有 OA 来源都未找到内容时,根据 DOI 前缀构建出版商一侧的 PDF URL,以便你的机构 IP/Cookie 授权获取;(2)1 次/秒的速率限制,避免批量任务导致你的 IP 因“系统性下载”而受到限流或封禁。
主动启用: export PAPER_FETCH_INSTITUTIONAL=1
机构模式的变化:
| 方面 | 公共(默认) | 机构 |
|---|---|---|
| 主机可达性 | 任何通过 SSRF 防护的公共 HTTPS 主机 | 相同 |
| SSRF 防护 | 已启用(私有 IP/非 http(s)/非 80、443 端口/云元数据均会被拦截) | 已启用——规则相同 |
| 直接访问出版商的回退机制 | 关闭 | 开启——根据 DOI 前缀 → 出版商 PDF URL,在所有 OA 来源均未找到内容后作为最后手段 |
| 速率限制 | 无 | 1 次/秒令牌桶(所有出站请求) |
meta.auth_mode | "public" | "institutional" |
保持不变的内容:
%PDF魔数字节检查和 50 MB 大小上限(防止 HTML 落地页和超大响应混入)- 绝不破解 CAPTCHA。如果出版商显示验证挑战,响应就不会以
%PDF开头,paper-fetch 会转而尝试下一个来源。 - 机构模式本身不使用浏览器自动化、Playwright 或隐身功能——它通过出版商直连 URL 执行普通 HTTP 获取。(浏览器自动化是单独启用的功能:上文所述的 CloakBrowser 回退机制,由
PAPER_FETCH_CLOAK控制。) - Agent 不能自行启用此模式——
PAPER_FETCH_INSTITUTIONAL必须由人工操作员在 shell 环境中设置。这就是信任边界。
当 paper-fetch 找不到 OA 副本且你处于公共模式时,错误封装中会包含 suggest_institutional: true,以及提示用户设置环境变量的信息。Agent 可以原样转述,而不是悄无声息地失败。
服务条款提示: 几乎所有出版商的订阅条款都禁止“系统性下载”。1 次/秒的速率限制和现有的单文件幂等机制旨在让个人研究用途保持在可接受范围内。运行多个并行的 paper-fetch 进程或取消速率限制,都可能触发出版商对整个机构的 IP 封禁。请勿这样做。
注意事项
- 身份验证由外部处理。 Agent 绝不会运行登录子命令。人工用户或编排器会在环境中设置
UNPAYWALL_EMAIL;Agent 会继承该变量。若缺少邮箱,则会平稳降级,仅使用其余 4 个来源。 - 信任具有方向性。 CLI 参数只在入口处验证一次。SSRF 防护、
%PDF魔数字节检查和 50 MB 大小上限由环境层强制执行,而非由 Agent 请求决定。Agent 无法通过传入标志放宽安全限制——通过环境变量启用机构模式(及其速率限制风险配置)是操作员的行为。 - 下载天然具备幂等性。 使用相同的
--out重新运行时,会跳过已存在的文件(确定性文件名:{first_author}_{year}_{journal_abbrev}_{short_title}.pdf;若元数据不含期刊/来源,则省略期刊部分)。搭配--idempotency-key,还可在不进行任何网络 I/O 的情况下重放完全相同的封装。 - 默认输出目录:
./pdfs/。