# dsh-literature-search [English](../README.md) · **中文** > 本文件位于 `docs/` 而不是仓库根目录,是**刻意为之**:npm 用 `{README,README.*}` > 这个 glob 选包页面用的 README,并把结果排序后取第一个匹配 `*.md` 的文件。 > 在这个排序里 `README.zh-CN.md` 排在 `README.md` **前面**,所以根目录下的任何 > `README.<语言>.md` 都会把主 README 顶掉 —— 0.2.3 首次发布时 npm 页面上显示的 > 就是中文版。放到 `docs/` 后 glob 不再匹配它,英文版才成为包页面内容。 [![CI](https://github.com/Astervolans/dsh-literature-search/actions/workflows/ci.yml/badge.svg)](https://github.com/Astervolans/dsh-literature-search/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../LICENSE) [![npm](https://img.shields.io/npm/v/dsh-literature-search.svg)](https://www.npmjs.com/package/dsh-literature-search) [![Runtime dependencies: 0](https://img.shields.io/badge/runtime%20dependencies-0-brightgreen.svg)](../package.json) DeepSeek Harness(dsh)插件:通过 **PubMed 官方 E-utilities API** 与 **Google Scholar** 检索文献, 把结果统一成同一种 paper 结构返回给模型。 - **PubMed 走官方 API**:`esearch`(检索 PMID)→ `esummary`(元数据)→ `efetch`(摘要/MeSH/关键词)→ `elink`(相关文献)。 免费、无需密钥、有文档、可长期稳定运行。 - **Google Scholar 没有官方 API**:有 SerpApi key 时走 SerpApi 的 `google_scholar` 引擎; 没有 key 时直接解析 `scholar.google.com` 的 HTML(会被 Google 限流/拦截,此时**明确报错**而不是静默返回空结果)。 - **零运行时依赖**:只用 Node 内置 `fetch` / `AbortSignal`,`@deepseek-ai/*` 由 DSH 运行时提供,无需 `npm install`。 - **设置界面**:在 **设置 → 插件 → 文献检索** 里有专属配置页,可直接填 NCBI / SerpAPI 密钥、切换 Scholar 后端、调速率与返回条数,并一键测试连通性。 > 详细的 API 调研(参数、速率、返回格式、实测证据、选型理由)见 [`docs/API-RESEARCH.md`](API-RESEARCH.md)。 ## 工具 | 工具 | 上游 | 说明 | | --- | --- | --- | | `pubmed_search` | E-utilities `esearch`+`esummary`+`efetch` | 关键词/字段标签检索,支持 `sort`、发表日期窗口、`offset` 分页;返回 PMID、标题、作者、期刊、卷期页、DOI、PMC、摘要、MeSH、关键词 | | `pubmed_paper` | `efetch` + `esummary` | 按 PMID 取完整记录(含摘要、MeSH、关键词、文献类型、评论/引用行) | | `pubmed_related` | `elink` `pubmed_pubmed` | 按 NCBI 相关文献算法列出与某篇最相关的论文 | | `scholar_search` | SerpApi / HTML | 谷歌学术检索:标题、作者、期刊/会议、年份、被引次数、摘要片段、PDF 链接;支持年份区间与分页 | | `scholar_cite` | SerpApi `google_scholar_cite` | 返回 MLA/APA/Chicago/Harvard/Vancouver/BibTeX 等可粘贴引用格式(仅 SerpApi 后端) | 所有工具名与已有的 `dsh-ai4scholar` 插件(`search_pubmed` / `search_google_scholar`)**不冲突**,可以共存。 ## 安装 ### 从 npm 安装(推荐) ```bash dsh plugin --profile desktop add dsh-literature-search ``` `dsh plugin` 是在 profile 目录里转发给 pnpm 的薄封装,装完会按**已安装状态**重算 profile 的 bundle 列表:任何 manifest 里声明了 `dsh.bundle` 的依赖都会自动进入层栈,因此不需要手改配置。 跑 `dsh web` 的话把 `desktop` 换成 `web`。安装后**重启 DSH** 才会加载新工具。 卸载: ```bash dsh plugin --profile desktop remove dsh-literature-search ``` ### 从源码安装(离线) ```powershell powershell -ExecutionPolicy Bypass -File install.ps1 -Profile desktop ``` - `-Profile`:目标 profile(桌面应用为 `desktop`,`dsh web` CLI 为 `web`)。 - 脚本做两件事:把插件复制到 `$DSH_HOME\profiles\\node_modules\dsh-literature-search`, 并把 `literature-search` 配置行合并进该 profile 的 `cordis.patch.yml`(幂等,可重复执行)。 - 安装后**重启 DSH**(桌面应用或 `dsh web`)才会加载新工具。 - 卸载:`powershell -ExecutionPolicy Bypass -File uninstall.ps1 -Profile desktop`。 不想改 profile 也可以直接先跑 CLI 验证: ```powershell node cli.mjs pubmed "CRISPR base editing" --max 3 node cli.mjs paper 33301246 node cli.mjs scholar "base editing" --max 5 # 无 key 时走 HTML,时通时断,被拦会给出提示 ``` ## 设置界面(设置 → 插件 → 文献检索) 客户端插件(`lib/client.js`)在设置面板注册一个标签页,服务端提供 `/plugin/literature-search` 路由支撑它: | 路由 | 用途 | | --- | --- | | `GET /plugin/literature-search/config` | 返回 settings 命名空间(已脱敏)、修订号与状态事实(facts) | | `POST /plugin/literature-search/config` | 带 `expectedRevision` 的补丁写入;版本冲突返回 409 | | `POST /plugin/literature-search/credential` | 按 `slot`(`pubmed` / `scholar`)写入或清除密钥 | | `POST /plugin/literature-search/test` | 真跑一次 PubMed esearch 与 Scholar 检索,返回耗时与结果 | 页面能力: - **密钥**:NCBI API Key 与 SerpApi Key 通过**凭据服务**写入 `$DSH_HOME/.credentials.yaml`, 不落进 settings 文档;页面显示来源(`环境变量` = 只读 / `已保存` = 可改)与配置状态。 - **后端选择**:`scholarProvider` 三选一(auto / serpapi / html),保存后**立即生效**(客户端缓存会失效重建)。 - **速率与结果**:PubMed/Scholar 请求间隔、默认条数、单次上限、摘要字符数、超时与重试——均为即时生效。 - **一键测试**:分别探测 PubMed 与 Google Scholar,失败时给出原因(含「被墙/限流 → 建议配 SerpApi」提示)。 - **需重启项**:`enabled`、`pubmedEnabled`、`scholarEnabled`、`promptGuidance`、`promptOrder` 属于注册期开关,改动后需重启 DSH(页面上有说明)。 实现细节:`lib/web.js` 负责路由与事实采集;`lib/client.js` 是手写的模块加载器包 (`window.__ModuleLoader__.load`,`require("react")` 由 DSH 客户端运行时解析),**不需要构建步骤**。 ### 密钥存放与状态判定 每个密钥槽用**两条独立探针**判断状态,并在徽章下方打印一行非敏感诊断 (`ref=` / `describe=` / `resolve=` / `error=`): 1. **凭据服务**(`ctx.credentials`,写 `$DSH_HOME/.credentials.yaml`)——首选; 2. **设置内的密钥字段**(`pubmedApiKey` / `scholarSerpApiKey`,标了 `role('secret')`,读取时脱敏)—— 当前组合**没有**凭据服务时自动降级到这里;`resolveSecret` 也优先读取它。 所以不论组合里有没有凭据服务,密钥都会落到一个**插件确实会读到**的位置。 若两条探针结论不一致(例如 `describe` 说未配置、`resolve` 却能取到值),页面按「已配置」显示, 同时把两条原始结论留在诊断行里。 服务端还会把**不含密钥值**的判定结果写到: ``` $DSH_HOME/.dsh-literature-search/diagnostics.json ``` `GET /config`、`POST /credential`、`POST /test` 每次都会刷新它,内容含 `describe`/`resolve` 原始结论、错误文本、`DSH_HOME`/`cwd`,以及环境变量中是否存在同名密钥(只记布尔)。 排查「填了 key 仍显示未配置」时先看这个文件。 另外,服务端与工具侧都改为从**注入的上下文**取 `settings` / `credentials` (`ctx.inject(['credentials'], …)`),而不是 `ctx.get('credentials')` —— 后者在某些组合里可能取不到兄弟插件的服务,这正是密钥「写进去了却读不出来」的典型原因。 ## 密钥(可选) | 环境变量 / credential | 作用 | 不设置的后果 | | --- | --- | --- | | `NCBI_API_KEY` | E-utilities 配额从 3 请求/秒 提升到 10 请求/秒 | 仍然可用,插件自动限速到 350 ms/请求 | | `SERPAPI_API_KEY` | Google Scholar 走 SerpApi(稳定、含被引次数与引用格式) | 退回 HTML 抓取,可能被 Google 以 HTTP 429 拦截 | | `NCBI_EMAIL` | 仅 CLI 使用,填进 E-utilities 的 `email` 参数 | NCBI 官方建议填,便于他们在异常时联系 | 两种配置方式(插件按 内联配置 → 凭据服务 → 环境变量 的顺序解析): 1. 启动 DSH 的环境变量; 2. `$DSH_HOME\.credentials.yaml`(DSH 凭据服务)中写入 `NCBI_API_KEY` / `SERPAPI_API_KEY`; 3. 直接写进 `cordis.patch.yml` 的 `pubmedApiKey` / `scholarSerpApiKey`(不推荐,容易泄漏到版本库)。 ## 配置 `cordis.patch.yml` 中的 `literature-search` 行: | Key | 默认值 | 说明 | | --- | --- | --- | | `enabled` | `true` | 是否注册本插件的工具 | | `pubmedEnabled` | `true` | 是否注册 `pubmed_*` 三个工具 | | `pubmedBaseUrl` | `https://eutils.ncbi.nlm.nih.gov/entrez/eutils` | E-utilities 根地址 | | `pubmedTool` / `pubmedEmail` | `dsh-literature-search` / 空 | 每次请求都带上的 `tool` / `email` 参数(NCBI 使用政策要求) | | `pubmedApiKeyEnv` / `pubmedApiKey` | `NCBI_API_KEY` / 空 | 密钥引用 / 内联密钥 | | `pubmedRateLimitMs` | `0` | 请求最小间隔;0 = 无 key 350 ms、有 key 110 ms | | `scholarEnabled` | `true` | 是否注册 `scholar_*` 两个工具 | | `scholarProvider` | `auto` | `auto`(有 key 用 SerpApi,否则 HTML)/ `serpapi` / `html` | | `scholarBaseUrl` / `scholarSerpApiBaseUrl` | Google / SerpApi 官方地址 | 可指向自建代理 | | `scholarSerpApiKeyEnv` / `scholarSerpApiKey` | `SERPAPI_API_KEY` / 空 | 密钥引用 / 内联密钥 | | `scholarHl` | `en` | 谷歌学术界面语言 | | `scholarRateLimitMs` | `0` | 0 = HTML 2500 ms、SerpApi 250 ms | | `defaultMaxResults` / `maxResultsCap` | `10` / `50` | 模型未指定 / 最多可请求的条数 | | `abstractMaxChars` | `600` | 每篇摘要/片段字符上限,0 = 不返回摘要 | | `requestTimeoutMs` / `maxRetries` / `retryBackoffMs` | `30000` / `3` / `1000` | 单次请求超时、重试次数、退避基数(遇 429/5xx 自动重试并遵守 `Retry-After`) | | `toolTimeoutMs` | `120000` | 单次工具调用预算 | | `userAgent` | 空(内置 Chrome UA) | 覆盖默认 UA | | `promptGuidance` / `promptOrder` | `true` / `155` | 是否注册系统提示词段落及其顺序 | ## 使用示例 模型侧(自然语言即可): > 用 pubmed_search 检索 2020 年以后 “base editing” 的综述,取 5 篇,然后对第一篇用 pubmed_related 找 5 篇相关文献。 > 用 scholar_search 找 “CRISPR base editing” 被引最高的 10 篇,并把第一篇的 resultId 交给 scholar_cite 生成 BibTeX。 返回示例(`pubmed_paper 33301246`): ``` PubMed record: Safety and Efficacy of the BNT162b2 mRNA Covid-19 Vaccine. Safety and Efficacy of the BNT162b2 mRNA Covid-19 Vaccine. 2020 · The New England journal of medicine · vol 383 · no 27 · pp 2603-2615 Polack, Fernando P, Thomas, Stephen J, Kitchin, Nicholas et al. (29 authors) DOI: 10.1056/NEJMoa2034577 · PMID: 33301246 · PMC: PMC7745181 https://pubmed.ncbi.nlm.nih.gov/33301246/ Types: Clinical Trial, Phase III, Journal Article, Randomized Controlled Trial MeSH: BNT162 Vaccine, COVID-19, SARS-CoV-2, ... Abstract: BACKGROUND: ... ``` ## 测试 离线套件需要两个 DSH 运行时包(`@deepseek-ai/dsh-tools`、`@deepseek-ai/schemastery`)。 两种准备方式,任选其一: ```powershell # 路线 A:从本机 DSH 安装里抽取(无需联网) powershell -ExecutionPolicy Bypass -File setup-dev-links.ps1 # 首次运行 # 路线 B:从 npm 安装(CI 用的就是这条,机器上没装 DSH 也能跑) node tools/fetch-dev-deps.mjs # 离线(解析器 + 插件契约 + 设置路由 + 客户端 bundle + 配置漂移) node test/run-all.mjs # 在线(真实上游;需要能访问对应域名) node test/live-pubmed.mjs # eutils.ncbi.nlm.nih.gov node test/live-scholar.mjs # scholar.google.com(Google 拒绝请求时记为 SKIP) node test/probe-scholar.mjs # 连通性诊断:status / 结果块数 / 是否被反爬拦截 ``` - `setup-dev-links.ps1` 调用 `tools/extract-dev-deps.mjs`,从 `D:\DSH Desktop\resources\app.asar` 里**按需抽取**插件测试所需的 DSH 运行时闭包 (`@deepseek-ai/dsh-tools`、`schemastery`、`yaml` 及其传递依赖)到 `.dev-deps/`, 再用 junction 链到 `node_modules/`。DSH 换版本、profile 布局变化都不影响; **插件本身零 npm 依赖**,`.dev-deps/` 与 `node_modules/` 都不会被安装进 profile。 - `tools/fetch-dev-deps.mjs` 走另一条路:按 `.dev-deps/package.json` 里钉住的版本 从 npm 装同一批包,再软链到 `node_modules/`。适合没装 DSH Desktop 的机器和 CI (`.github/workflows/ci.yml` 在 Ubuntu + Windows × Node 22/24 上跑全部离线用例)。 - 离线共 **62 个用例**:MEDLINE 解析 7、Scholar 解析/分页 6、插件与工具 21(含 「settings 写入后 Scholar 即时切换后端」「凭据写入后无需重启生效」「实时条数上限」)、 设置路由 14、客户端 bundle 10、配置漂移 4。全部用桩 `fetch`/假服务,不联网。 - 客户端 bundle 套件用桩 `window.__ModuleLoader__` + 极简 React shim 真正执行并遍历渲染树, 能在没有浏览器的情况下抓出设置页里的拼写错误与空引用(已借此修掉一个 `state.drafts` 空值崩溃)。 0.2.2 起它会展开函数组件做深度渲染,因此子组件拥有的节点(如密钥字段的按钮)也能断言, 并直接守住主题回归:主按钮必须用成对的 token、不得把 `--dsw-alias-brand-primary` 当填充色。 - 在线测试把**上游不可用**(无出网 / DNS / 超时 / HTTP 429 / 反爬页)记为 SKIP 而不是 FAIL, 因为那是环境或对方策略问题;只有“页面里有结果块但解析出 0 条”才判失败。 实测:Google Scholar HTML 5/5 通过;PubMed 在本机出网时好时坏,不通时会明确打印 SKIP 与原因。 ## 目录结构 ``` dsh-literature-search/ ├── lib/ │ ├── index.js # 插件入口:Config / apply / runtime / settings 命名空间 / 提示词段落 │ ├── web.js # /plugin/literature-search 路由:配置读写、密钥读写、连通性测试 │ ├── client.js # 设置页客户端插件(window.__ModuleLoader__,免构建) │ ├── http.js # 限速闸门、超时、重试、Retry-After、JSON/文本解码 │ ├── paper.js # 统一 paper 结构、输出 schema、渲染 │ ├── medline.js # MEDLINE 文本解析(efetch rettype=medline) │ ├── pubmed.js # E-utilities 客户端 + pubmed_* 三个工具 │ └── scholar.js # Google Scholar(SerpApi / HTML)+ scholar_* 两个工具 ├── tools/ │ ├── extract-dev-deps.mjs # 从 app.asar 抽取测试所需的 DSH 运行时闭包 │ └── fetch-dev-deps.mjs # 从 npm 装同一批包(CI / 没装 DSH 的机器) ├── test/ # 离线套件(含设置路由与客户端 bundle)+ 在线冒烟 + fixtures ├── docs/ │ ├── API-RESEARCH.md # API 调研(本文件) │ └── README.zh-CN.md # 中文 README —— 刻意不放仓库根目录 ├── .github/ # CI 工作流 + Issue / PR 模板 ├── .dev-deps/package.json # 钉住离线测试用的 DSH 运行时版本(只有这一个文件入库) ├── cli.mjs # 脱离 DSH 的命令行验证 ├── install.ps1 / uninstall.ps1 / setup-dev-links.ps1 ├── cordis.patch.yml ├── LICENSE / CHANGELOG.md / CONTRIBUTING.md / SECURITY.md └── package.json ``` ## 已知限制 - **PubMed 的日期过滤按印刷出版日期(`pdat`)**:ahead-of-print 记录的 `esummary.pubdate` 可能显示为之后的年份,这是 PubMed 数据本身的行为,不是插件 bug。 - **Google Scholar 无官方 API**:HTML 后端时通时断(同一台机器可能一会儿 200、一会儿 429 或直接连接失败); 插件会抛出可读错误并给出替代方案。要稳定使用请配 SerpApi key。 - **PubMed 摘要可能受版权保护**:NCBI 免责声明要求使用者遵守版权方条款;大规模挖掘请下载 PubMed 本地副本。 - `scholar_cite` 只支持 SerpApi 后端(HTML 端点没有引用格式接口)。 ## scholar.google.com 连不上时怎么办 在被阻断的网络里,`scholar.google.com`(及其 `.com.hk` / `.co.jp` / `.de` 镜像)会直接 `UND_ERR_CONNECT_TIMEOUT`,而 `serpapi.com` 与 `eutils.ncbi.nlm.nih.gov` 正常。 最快的判断方式是设置页里的 **「测试 Google Scholar」** 按钮;命令行诊断脚本: ```powershell node test/probe-scholar.mjs ``` - `resultBlocks > 0`:页面能抓到 → HTML 后端可用(`scholarBaseUrl` 可改成任一可达镜像)。 - `blocked=true`:Google 返回反爬页 → 降低频率,或改用 SerpApi。 - `ERR UND_ERR_CONNECT_TIMEOUT`:网络层被阻断 → 下面两条路二选一。 **方案 A:配 SerpApi(推荐,稳定)** 在设置页「Google Scholar」区块粘贴 SerpApi Key 并保存(写入 `$DSH_HOME/.credentials.yaml`), 或在启动 DSH 的环境里设置 `SERPAPI_API_KEY`;`scholarProvider=auto` 会自动切到 SerpApi。 **方案 B:走本机代理** 若本机有代理(如 Clash 默认 `127.0.0.1:7897`),用 Node 24 的环境变量代理支持启动 DSH: ```powershell set NODE_USE_ENV_PROXY=1 set HTTPS_PROXY=http://127.0.0.1:7897 set HTTP_PROXY=http://127.0.0.1:7897 "D:\DSH Desktop\DSH Desktop.exe" ``` 注意:`NODE_USE_ENV_PROXY` 是 Node 24 起的特性,且必须让 **DSH 进程本身**带着这些变量启动 (插件内部的 `fetch` 会继承);若代理客户端没开(`ProxyEnable=0` 且端口无监听),这条方案无效。 PubMed 不受影响:`eutils.ncbi.nlm.nih.gov` 在同样网络下可直接访问。 ## 参与贡献 欢迎 Issue 与 PR:开发环境、测试命令和代码约定见 [`CONTRIBUTING.md`](../CONTRIBUTING.md); 变更历史见 [`CHANGELOG.md`](../CHANGELOG.md)。 发现安全问题时请不要开公开 Issue,改走 [`SECURITY.md`](../SECURITY.md) 里的私下渠道 —— 本插件会接触 NCBI / SerpApi 密钥,密钥泄漏路径都按安全问题处理。 ## License [MIT](../LICENSE)