dsh-molbio-tools 0.21.1 → 0.22.0
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/CHANGELOG.md +72 -0
- package/README.md +13 -2
- package/benchmark/tasks/08-records.json +5 -1
- package/benchmark/verifications.mjs +5 -0
- package/build/release-preflight.mjs +1 -1
- package/docs/history.md +25 -0
- package/docs/maintainer.md +1 -1
- package/docs/roadmap.md +6 -2
- package/docs/store-evidence.md +19 -19
- package/docs/store-preset-exemption.md +4 -0
- package/docs/workflow.md +1 -1
- package/index.mjs +64 -10
- package/package.json +2 -2
- package/papers.mjs +151 -0
- package/preset/molbio-lab/agent.cordis.yml +5 -4
- package/test/smoke.mjs +109 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,78 @@
|
|
|
10
10
|
改为按**包名**引用后模块缓存问题随之消失——没有拷贝,也就没有需要保持同步的版本目录。
|
|
11
11
|
下文历史条目里出现的 `vN` 目录保持原样,作为当时的记录。
|
|
12
12
|
|
|
13
|
+
## [0.22.0] — 2026-10-25(DSH 0.2.1-rc.3 适配;PubMed 官方 API + NCBI API key)
|
|
14
|
+
|
|
15
|
+
**工具数不变(60)。新增一条通路:`molbio_pubmed_search` / `molbio_pubmed_abstract` 可以带着
|
|
16
|
+
NCBI API key 走 PubMed 官方 E-utilities。** 没有 key 时两条工具的行为与 0.21.1 相同——检索仍走
|
|
17
|
+
harness 的 web 检索,摘要仍走 `ctx.web.fetch`。
|
|
18
|
+
|
|
19
|
+
### 1. 为什么要有官方通路
|
|
20
|
+
|
|
21
|
+
原来的检索完全依赖 harness 的 web 检索服务:它返回的是**搜索引擎索引到的页面**,不是 PubMed 的
|
|
22
|
+
题录。三个具体代价:命中的 PMID 要从 URL 里猜、拿不到期刊/年份/作者/DOI 这些结构化字段、也
|
|
23
|
+
无法用 NCBI 的 API key 提升速率上限(无 key 3 次/秒,有 key 10 次/秒)。官方 API 是两步:
|
|
24
|
+
`esearch`(term → PMID 列表)→ `esummary`(PMID → 题录),摘要另走 `efetch`。
|
|
25
|
+
|
|
26
|
+
### 2. 通路选择与 key 的来源
|
|
27
|
+
|
|
28
|
+
| 条件 | 检索通路 | 结果字段 |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| 有 key(参数或环境变量) | 官方 E-utilities:esearch + esummary | PMID、标题、期刊、年份、作者、DOI,另含 `total_matches` |
|
|
31
|
+
| 无 key | 原来的 harness web 检索(行为不变) | 标题、URL、snippet、从 URL 提取的 PMID |
|
|
32
|
+
|
|
33
|
+
key 的优先级是 **`api_key` 参数 > `NCBI_API_KEY` > `MOLBIO_NCBI_API_KEY`**;空白值视为未提供
|
|
34
|
+
(不会发出 `api_key=` 这种被 NCBI 以 400 拒绝的空参数)。一次带 key 的检索是 **2 个请求**,
|
|
35
|
+
`molbio_pubmed_abstract` 是 1 个 efetch 请求。
|
|
36
|
+
|
|
37
|
+
> **内联传 key 会出现在对话记录和工具调用卡片里**,这是参数通路的固有代价;长期使用应设环境
|
|
38
|
+
> 变量。工具的输出、渲染和错误信息都不回显 key。
|
|
39
|
+
|
|
40
|
+
### 3. 实现
|
|
41
|
+
|
|
42
|
+
- `papers.mjs` 增加纯函数:`pubmedApiKey`(参数/环境变量优先级)、`pubmedSearchUrl` /
|
|
43
|
+
`pubmedSummaryUrl` / `pubmedFetchUrl`(三个 URL 构造器,统一带 `tool=dsh-molbio-tools`)、
|
|
44
|
+
`parseEsearch`(ids + count)、`parseEsummaryRecords`(按 `uids` 顺序归一化题录,
|
|
45
|
+
DOI 从 `articleids` 里取 `idtype: doi`)、`esummarySource`(转成工具返回的 source 形状)。
|
|
46
|
+
- 传输仍走 `ctx.web.fetch`:官方 API 不是一条绕开沙箱/同意机制的新网络通路。没有 `web.fetch`
|
|
47
|
+
而用户又给了 key 时,工具明确报「需要 ctx.web.fetch」,而不是悄悄退回 web 检索。
|
|
48
|
+
- 错误处理:NCBI 的 `{"error":"API key invalid"}` 会变成 `PubMed E-utilities error: API key
|
|
49
|
+
invalid`,**不会被当成「没有结果」**——「搜不到」和「key 错了」是两件事。空结果返回
|
|
50
|
+
`sources: []` 与 `total_matches: 0`。
|
|
51
|
+
- 输出 schema 扩展:`backend`(`pubmed-eutils` / `web-search`)、`total_matches`,以及 source 上的
|
|
52
|
+
`doi` / `authors` / `journal` / `year`;全部为可选字段,旧调用的输出形状不变。
|
|
53
|
+
|
|
54
|
+
### 4. rc.3 适配:这次上游什么都没动,但仍要走完流程
|
|
55
|
+
|
|
56
|
+
- `@deepseek-ai/dsh-web-app@0.2.1-rc.3` 的 `standard.patch.yml` 与 rc.2 **逐字节一致**(`git diff
|
|
57
|
+
--no-index` 验证),所以 preset 行清单、`cordis.patch.yml`、`preset-health` 的期望都没有改。
|
|
58
|
+
- 在 rc.3 上全套守卫全绿:`preset-health`(30 行)、`drift-probe`(14 条突变)、`contract`(16 项,
|
|
59
|
+
含客户端入口与 `presentationMeta` 的行为断言)、`benchmark-profile`(12 行 / 60 工具)。
|
|
60
|
+
- 声明:`engines.dsh` 仍为 `>=0.2.1-rc.2 <0.3.0-0`(rc.2 仍可用);`dshReleases` 把 rc.3 改成
|
|
61
|
+
`compatible`;CI 的 test/release 两条工作流钉到 `0.2.1-rc.3`;面板包 `0.1.4 → 0.1.5`(清单的
|
|
62
|
+
兼容声明变了,客户端产物字节未变,因此只 bump 面板版本线)。
|
|
63
|
+
- 证据用 rc.3 的 CLI 重跑:`docs/store-evidence.md`(0.22.0 tarball 安装 → 启动 → 卸载)。
|
|
64
|
+
|
|
65
|
+
### 5. benchmark 的处理(以及为什么没有重跑模型)
|
|
66
|
+
|
|
67
|
+
- 默认通路(无 key)的输出与 0.21.1 相同,`bench --offline` 的 61 题期望值逐条照跑。
|
|
68
|
+
- `literature-search` 仍是 `NETWORK_TASKS` 里唯一的一条:它断言的是模型的最终回答(PMID、标题、
|
|
69
|
+
摘要措辞),工具结果本来就是**当天的 live PubMed 数据**,没有可以钉住的值。
|
|
70
|
+
- **带 key 的新通路由 `test/smoke.mjs` 覆盖,而不是 benchmark**:benchmark 不能要求真实 key、
|
|
71
|
+
也不该把 live 记录冻结成期望值。smoke 用 mock 的 esearch/esummary/efetch 断言了四件事——
|
|
72
|
+
key 同时出现在两个请求里、题录字段解析正确、环境变量能回退、无效 key 会报错;另有一条
|
|
73
|
+
「无 key 时不碰 E-utilities 传输」的对照。
|
|
74
|
+
|
|
75
|
+
### 6. 版本与验证
|
|
76
|
+
|
|
77
|
+
- 根包 `0.21.1 → 0.22.0`;面板 `0.1.4 → 0.1.5`。
|
|
78
|
+
- `npm test`(DSH 0.2.1-rc.3)全绿:smoke、画面四套、客户端四套、contract 16、drift-probe 14 条
|
|
79
|
+
突变、preset-health 30 行、release 13 项、benchmark 四道守卫 + `bench --offline` 61 题。
|
|
80
|
+
- `node build/preset-patch.mjs --check` 与 `node build/client-bundle.mjs --check` 均报告与源一致
|
|
81
|
+
(rc.3 适配没有动任何生成物)。
|
|
82
|
+
- 一次性 Profile:tarball **115 个文件 / 约 1.0 MB**;安装 571 ms;启动 `HTTP 200`、38180 字节,
|
|
83
|
+
`__DSH_BOOT__` 含 `dsh-molbio-tools/client.js&rev=a25c3919676b`;卸载 421 ms,`molbio` 引用 0、
|
|
84
|
+
lockfile 与 `node_modules` 无残留。
|
|
13
85
|
## [0.21.1] — 2026-10-25(DSH STORE 提交前置:一个 patch、逐版本兼容声明、一个 tarball 一个清单)
|
|
14
86
|
|
|
15
87
|
**工具数不变(60),插件运行时行为零改动。** 这一版不加任何计算能力,只把"这个包以什么形状提交"
|
package/README.md
CHANGED
|
@@ -224,8 +224,8 @@ dsh plugin --profile <profile> add dsh-molbio-panel
|
|
|
224
224
|
|
|
225
225
|
| 工具 | 用途 |
|
|
226
226
|
| --- | --- |
|
|
227
|
-
| `molbio_pubmed_search` |
|
|
228
|
-
| `molbio_pubmed_abstract` | 按 PMID
|
|
227
|
+
| `molbio_pubmed_search` | 搜 PubMed 文献。提供 NCBI API key(`api_key` 参数,或环境变量 `NCBI_API_KEY` / `MOLBIO_NCBI_API_KEY`)时走**官方 E-utilities**(esearch + esummary),返回 PMID/标题/期刊/年份/作者/DOI;无 key 时回退到 harness 的 web 检索 |
|
|
228
|
+
| `molbio_pubmed_abstract` | 按 PMID 拉取摘要(efetch;同样支持 NCBI API key,依赖部署提供 web fetch 能力) |
|
|
229
229
|
| `molbio_paper_add` / `molbio_paper_list` / `molbio_paper_update` / `molbio_paper_remove` | 阅读库(工作区 `papers.json`):加/列/改/删,按 PMID→URL→标题+年份去重 |
|
|
230
230
|
| `molbio_paper_export_bibtex` | `papers.json` → BibTeX `.bib`(可按 tag 过滤) |
|
|
231
231
|
| `molbio_protocol_add` / `molbio_protocol_list` / `molbio_protocol_update` | 协议库(`protocols.json`):步骤列表、自由参数、来源文献 |
|
|
@@ -556,6 +556,11 @@ mutations remain deferred")。所以插件无法在工作区里合法地落一
|
|
|
556
556
|
### 文献与记录
|
|
557
557
|
|
|
558
558
|
- 检索与摘要走 harness 的 **web** 服务(部署不提供时工具会明确报错,而不是假装搜到)。
|
|
559
|
+
- 提供 NCBI API key 时,`molbio_pubmed_search` 用官方 E-utilities:esearch 拿 PMID、esummary
|
|
560
|
+
拿题录(标题/期刊/年份/作者/DOI),`molbio_pubmed_abstract` 用 efetch 拿摘要。key 的来源优先级是
|
|
561
|
+
`api_key` 参数 > `NCBI_API_KEY` > `MOLBIO_NCBI_API_KEY`;**长期使用请用环境变量**,内联传参会让 key
|
|
562
|
+
出现在对话记录与工具卡片里。NCBI 的速率上限是**无 key 3 次/秒、有 key 10 次/秒**,一次带 key 的
|
|
563
|
+
检索是 2 个请求(esearch + esummary)。没有 key 时检索仍走原来的 web 检索通路,行为不变。
|
|
559
564
|
- 阅读库/协议库/实验日志是工作区里的 `papers.json` / `protocols.json` / `experiments.json`,
|
|
560
565
|
写入遵循会话的**沙箱政策**,与其它文件工具同权;面板(Papers tab)直接读 `papers.json`。
|
|
561
566
|
- `molbio_paper_export_bibtex` 可选按 tag 过滤导出。
|
|
@@ -649,6 +654,12 @@ node <包目录>\preset\install.mjs --profile web --check # 只报告,不组
|
|
|
649
654
|
工具会返回 `notes` 说明放宽哪个约束(Tm 窗口、GC 窗口、扩增子长度、探针长度…)。通常是把
|
|
650
655
|
`amplicon_min/max` 或 `tm_min/max` 放宽一点即可。
|
|
651
656
|
|
|
657
|
+
**PubMed 的 API key 怎么配?**
|
|
658
|
+
在启动 DSH 的环境里设 `NCBI_API_KEY`(或 `MOLBIO_NCBI_API_KEY`),插件会自动把它带进 E-utilities 请求;
|
|
659
|
+
也可以在单次调用里传 `api_key`。有 key 时 `molbio_pubmed_search` 走官方 API(PubMed 自己的题录,
|
|
660
|
+
含 DOI),没有 key 时仍走 harness 的 web 检索。注意:**内联传的 key 会出现在对话记录和工具调用卡片里**,
|
|
661
|
+
长期用请设环境变量。
|
|
662
|
+
|
|
652
663
|
**"帮我把这段序列分析一下" 它却问我要参数?**
|
|
653
664
|
把目标说具体即可:要引物还是探针、产物多长、Tm 多少、哪个区域。工具默认值都是常规实验条件
|
|
654
665
|
(例如引物 18–28 nt / Tm 55–65 °C / GC 40–60%),不特别说明时按默认来。
|
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
"whatever PubMed answers today and there is no recorded value to pin. Its",
|
|
14
14
|
"assertions are therefore all `where: \"answer\"` and structural — a PMID",
|
|
15
15
|
"appears, a title appears — and `benchmark/verifications.mjs` deliberately",
|
|
16
|
-
"declares no offline invocation for it. See NETWORK_TASKS there."
|
|
16
|
+
"declares no offline invocation for it. See NETWORK_TASKS there.",
|
|
17
|
+
"",
|
|
18
|
+
"The 0.22.0 NCBI-API-key path is NOT exercised here: a benchmark cannot",
|
|
19
|
+
"require a real key or pin live PubMed records. test/smoke.mjs covers it with",
|
|
20
|
+
"mocked esearch/esummary/efetch responses (including the invalid-key error)."
|
|
17
21
|
],
|
|
18
22
|
"tasks": [
|
|
19
23
|
{
|
|
@@ -402,5 +402,10 @@ export function invocationsFor(task) {
|
|
|
402
402
|
* answer (structural facts: a PMID appears, a title appears), which is why this
|
|
403
403
|
* module returns no invocations for them and why the offline gate tolerates that
|
|
404
404
|
* specific case while failing every other task that forgets to declare one.
|
|
405
|
+
*
|
|
406
|
+
* The NCBI-API-key path added in 0.22.0 is covered by `test/smoke.mjs` instead:
|
|
407
|
+
* a keyed call is two E-utilities requests (esearch + esummary) whose responses
|
|
408
|
+
* the mock returns, including the invalid-key error. A benchmark must not need a
|
|
409
|
+
* real NCBI key or pin live PubMed data, so this task stays network-only.
|
|
405
410
|
*/
|
|
406
411
|
export const NETWORK_TASKS = new Set(['literature-search']);
|
|
@@ -201,7 +201,7 @@ export function tagVersion(tag) {
|
|
|
201
201
|
* `rc.10 < rc.2`, so a release of `rc.2` after `rc.10` would look like a
|
|
202
202
|
* DOWNGRADE and the gate would refuse a legitimate publish (or, in the other
|
|
203
203
|
* direction, wave through a stale one). This repository does cut prereleases
|
|
204
|
-
* (the harness baseline itself is `0.2.1-rc.
|
|
204
|
+
* (the harness baseline itself is `0.2.1-rc.3`), so the rule is implemented
|
|
205
205
|
* rather than approximated.
|
|
206
206
|
*
|
|
207
207
|
* Semver's rule: split on dots; numeric identifiers compare numerically and rank
|
package/docs/history.md
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
| 包版本 | preset 版本 | 内容 | 工具数 |
|
|
18
18
|
| --- | --- | --- | --- |
|
|
19
|
+
| 0.22.0 | — | **DSH 0.2.1-rc.3 适配 + PubMed 官方 API**:`molbio_pubmed_search`/`_abstract` 支持 NCBI API key(E-utilities),无 key 行为不变 | 60 |
|
|
19
20
|
| 0.21.1 | — | **DSH STORE 提交前置**:单文件 bundle patch、逐版本兼容声明;根 tarball 只带一个插件清单(工具行为零变化) | 60 |
|
|
20
21
|
| 0.21.0 | — | **DSH 0.2.1-rc.2 适配**:preset 行清单逐行重抄;contract 两条断言重锚到行为;两条 suspended 记录用真实模型重录 | 60 |
|
|
21
22
|
| 0.20.1 | — | 插件专属图标(透明底双螺旋 SVG),运行时零改动 | 60 |
|
|
@@ -44,6 +45,30 @@
|
|
|
44
45
|
|
|
45
46
|
---
|
|
46
47
|
|
|
48
|
+
## v0.22.0(2026-10-25,包 0.22.0)
|
|
49
|
+
|
|
50
|
+
一半是适配(rc.3),一半是新能力(PubMed 官方 API + key)。三条教训。
|
|
51
|
+
|
|
52
|
+
**(1) 「上游没变」不是可以跳过流程的理由,而是一个需要被证明的结论。**
|
|
53
|
+
rc.3 的 `standard.patch.yml` 与 rc.2 逐字节相同,所以 preset 行、生成物、`preset-health` 的期望
|
|
54
|
+
一行都没动——但这是先 diff 过、再把 `preset-health` / `drift-probe` / `contract` /
|
|
55
|
+
`benchmark-profile` 全套在 rc.3 上跑绿之后才敢下的结论。上一次(0.21.0)同一个文件在补丁级版本里
|
|
56
|
+
改了 12 处结构;把「这次应该没事」当选版本的理由,就是在赌下一次。
|
|
57
|
+
|
|
58
|
+
**(2) 官方 API 不是「另一条网络通路」,它仍然是一次 `ctx.web.fetch`。**
|
|
59
|
+
PubMed E-utilities 的加入没有引入 `fetch` 依赖,也没有绕开沙箱/同意机制:工具只是把 URL 构造成
|
|
60
|
+
esearch/esummary/efetch,再交给同一个 `ctx.web` 缝。两个细节必须做对:**空结果与鉴权失败是两件
|
|
61
|
+
事**(`{"error":"API key invalid"}` 不能渲染成「没有搜到」);**key 的优先级要可预测**
|
|
62
|
+
(参数 > `NCBI_API_KEY` > `MOLBIO_NCBI_API_KEY`,空白值视为缺席)。另外,内联传 key 会让它出现在
|
|
63
|
+
对话记录与工具卡片里——这是参数通路的固有代价,所以文档把「长期使用请设环境变量」写在工具描述里,
|
|
64
|
+
而不是只写在 README。
|
|
65
|
+
|
|
66
|
+
**(3) 需要真实凭证/实时数据的路径,归 smoke,不归 benchmark。**
|
|
67
|
+
benchmark 的 `literature-search` 一直是 `NETWORK_TASKS`:它只断言最终回答的结构,因为工具返回的
|
|
68
|
+
是当天的 PubMed 数据,没有可冻结的值。带 key 的新通路同样不能被 benchmark 钉住(既不能要求一个
|
|
69
|
+
真 key,也不该把 live 记录写进 fixture),于是用 mock 的 esearch/esummary/efetch 在 `test/smoke.mjs`
|
|
70
|
+
里断言「key 真的进了两个请求」「字段解析正确」「环境变量能回退」「坏 key 会报错」,并保留一条
|
|
71
|
+
「无 key 时不碰 E-utilities」的对照——benchmark 负责模型会不会用,smoke 负责这条分支本身对不对。
|
|
47
72
|
## v0.21.1(2026-10-25,包 0.21.1)
|
|
48
73
|
|
|
49
74
|
上架前置版:一个工具都没改,只改「提交的形状」。三条教训都关于**契约不止一份**。
|
package/docs/maintainer.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
- **包**:`dsh-molbio-tools`,零依赖 DSH 插件包,**60 个 `molbio_*` 工具**(其中
|
|
34
34
|
`molbio_plot` 一个工具承载 8 种图表)。
|
|
35
|
-
- **基线**:DSH **0.2.1-rc.
|
|
35
|
+
- **基线**:DSH **0.2.1-rc.3**;`package.json` 版本 0.22.0。
|
|
36
36
|
- **两条分发渠道**:根包(工具 + 面板)与 `packages/molbio-panel`(只面板),
|
|
37
37
|
**两个独立的 npm 条目、两条独立的版本线**,必须分别发布。
|
|
38
38
|
- **工具只在专属模式里出现**(设计如此):bundle 的 `cordis.patch.yml` 只插一行**惰性宿主锚点**
|
package/docs/roadmap.md
CHANGED
|
@@ -10,12 +10,12 @@ L = 需要新机制或外部资源)。
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
## 1. 当前状态(0.
|
|
13
|
+
## 1. 当前状态(0.22.0)
|
|
14
14
|
|
|
15
15
|
- 60 个 `molbio_*` 工具,零依赖,随 preset 分发(见 [README](../README.md))。
|
|
16
16
|
- 离线测试金字塔 + 组合漂移守卫全绿;`benchmark/` 提供**模型使用**维度的评估
|
|
17
17
|
(见 [benchmark/README.md](../benchmark/README.md))。
|
|
18
|
-
- 基线:DSH **0.2.1-rc.
|
|
18
|
+
- 基线:DSH **0.2.1-rc.3**。
|
|
19
19
|
- **CI 已在位**(`.github/workflows/test.yml`,0.18.2 起):push / PR / 手动都跑全套;harness 装
|
|
20
20
|
**钉死版本**,并用 `npm prefix -g` 显式导出 `DSH_HARNESS_ROOT`。0.18.4 起**同时跑
|
|
21
21
|
`ubuntu-latest` 与 `windows-latest`**——3.6 已修,套件不再依赖宿主分隔符。
|
|
@@ -37,6 +37,10 @@ L = 需要新机制或外部资源)。
|
|
|
37
37
|
`dshReleases`、根 tarball 去掉 `packages/`(一个包只带一个插件清单)。剩下的两条预检规则
|
|
38
38
|
不是改包能解决的,证据与请求见 [store-preset-exemption.md](store-preset-exemption.md)。
|
|
39
39
|
下一版若要动工具,按本文件第 2 节挑候选;若只等商城结论,则不必发版。
|
|
40
|
+
- **0.22.0 是 rc.3 适配 + 文献检索升级**:DSH 基线 0.2.1-rc.2 → 0.2.1-rc.3(上游
|
|
41
|
+
`standard` 行清单**逐字节未变**,`preset-health` / `drift-probe` / `contract` 在 rc.3 上
|
|
42
|
+
全绿,因此这一半只改声明与证据);`molbio_pubmed_search` / `molbio_pubmed_abstract` 增加
|
|
43
|
+
NCBI API key 通路,走官方 E-utilities(esearch + esummary),无 key 时保持原有 web 检索。
|
|
40
44
|
|
|
41
45
|
---
|
|
42
46
|
|
package/docs/store-evidence.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"dshReleases": {
|
|
19
19
|
"0.2.1-rc.1": "unknown",
|
|
20
20
|
"0.2.1-rc.2": "compatible",
|
|
21
|
-
"0.2.1-rc.3": "
|
|
21
|
+
"0.2.1-rc.3": "compatible"
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
}
|
|
@@ -29,19 +29,19 @@
|
|
|
29
29
|
`@deepseek-ai/dsh-app-boot` 的 `unmetRequirements`):带 prerelease 选项时 `<0.3.0`
|
|
30
30
|
会放行 `0.3.0-alpha.1`,而 `-0` 不会。
|
|
31
31
|
|
|
32
|
-
### 为什么 rc.2 是 compatible、rc.1
|
|
32
|
+
### 为什么 rc.2 / rc.3 是 compatible、rc.1 是 unknown
|
|
33
33
|
|
|
34
34
|
| 版本 | 记录 | 依据 |
|
|
35
35
|
| --- | --- | --- |
|
|
36
36
|
| 0.2.1-rc.1 | unknown | 早于本包基线(0.20.1 钉 `0.2.0-rc.2`),未在此版本上运行过;同时也不在 `engines.dsh` 范围内 |
|
|
37
|
-
| 0.2.1-rc.2 | compatible |
|
|
38
|
-
| 0.2.1-rc.3 |
|
|
37
|
+
| 0.2.1-rc.2 | compatible | 0.21.0 / 0.21.1 的发布基线;本文件第 3 节的一次性 Profile 流程在 rc.2 上跑过;`contract` / `preset-health` / `smoke` 均通过 |
|
|
38
|
+
| 0.2.1-rc.3 | compatible | 0.22.0 的发布基线(CI 钉该版本);同一套一次性 Profile 流程在 rc.3 上重跑通过(安装 → 启动 HTTP 200 → 卸载无残留);`contract` / `preset-health` / `drift-probe` / `benchmark-profile` / `smoke` 在 rc.3 上全绿。上游 `standard` 行清单与 rc.2 **逐字节一致** |
|
|
39
39
|
|
|
40
40
|
## 2. 环境
|
|
41
41
|
|
|
42
42
|
| 项 | 值 |
|
|
43
43
|
| --- | --- |
|
|
44
|
-
| DSH | `0.2.1-rc.
|
|
44
|
+
| DSH | `0.2.1-rc.3` |
|
|
45
45
|
| Node.js | `v26.12.1` |
|
|
46
46
|
| pnpm | `11.22.0` |
|
|
47
47
|
| 平台 | Windows(win32 x64) |
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
export DSH_HOME=<workspace>/.tmp/dsh-store-check
|
|
55
|
-
npm pack --pack-destination "$DSH_HOME" # 得到 dsh-molbio-tools-0.
|
|
55
|
+
npm pack --pack-destination "$DSH_HOME" # 得到 dsh-molbio-tools-0.22.0.tgz
|
|
56
56
|
dsh --profile molbio-tgz-check --from-default-profile web --dump-config
|
|
57
|
-
dsh plugin --profile molbio-tgz-check add "$DSH_HOME/dsh-molbio-tools-0.
|
|
57
|
+
dsh plugin --profile molbio-tgz-check add "$DSH_HOME/dsh-molbio-tools-0.22.0.tgz"
|
|
58
58
|
dsh --profile molbio-tgz-check --dump-config # 安装后的组合树
|
|
59
59
|
dsh --profile molbio-tgz-check --no-open --port 0 # 启动,打印 URL
|
|
60
60
|
curl "http://127.0.0.1:<port>/?token=<token>" # 读回页面
|
|
@@ -65,20 +65,20 @@ dsh --profile molbio-tgz-check --dump-config # 卸载后的组合树
|
|
|
65
65
|
安装用 **tarball** 而不是目录,因为 tarball 就是发布物本身:它能同时证明 `files`
|
|
66
66
|
白名单带全了运行文件(`host.mjs`、`lib/client.js`、单个 `cordis.patch.yml` 的两个 insert、preset)。
|
|
67
67
|
|
|
68
|
-
tarball 是 **115 个文件 / 约
|
|
69
|
-
|
|
68
|
+
tarball 是 **115 个文件 / 约 1.0 MB**(`npm pack` 实测 1,004,017 B,解包约 3.2 MB;再改文档会让字节数
|
|
69
|
+
小幅浮动,文件数不变),且 `package/packages/**` 为 **0**:根包不再内嵌
|
|
70
70
|
`packages/molbio-panel`,一个 tarball 里只有一个插件清单(面板包按自己的地址单独发布)。
|
|
71
71
|
面板本身没有因此消失——根包的 `lib/client.js` 就带着它,第 3.2 节的回读可以作证。
|
|
72
72
|
|
|
73
|
-
下面这次是在 **0.
|
|
74
|
-
|
|
73
|
+
下面这次是在 **0.22.0 的 tarball** 上、用 **dsh 0.2.1-rc.3** 的 CLI 重跑的(2026-10-25),
|
|
74
|
+
文件数与发布物一致;更早的记录只差版本号与文件数,流程未变。
|
|
75
75
|
|
|
76
76
|
### 3.1 安装
|
|
77
77
|
|
|
78
78
|
```
|
|
79
|
-
+ dsh-molbio-tools file:D:/.../dsh-molbio-tools-0.
|
|
79
|
+
+ dsh-molbio-tools file:D:/.../dsh-molbio-tools-0.22.0.tgz
|
|
80
80
|
Packages: +1
|
|
81
|
-
Done in
|
|
81
|
+
Done in 571ms
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
安装后的 `--dump-config` 出现 bundle 层,且 `host.mjs` 解析到**已安装包内**:
|
|
@@ -98,19 +98,19 @@ Done in 631ms
|
|
|
98
98
|
### 3.2 启动
|
|
99
99
|
|
|
100
100
|
```
|
|
101
|
-
dsh web: http://127.0.0.1:
|
|
101
|
+
dsh web: http://127.0.0.1:50071/?token=...
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
读回结果:`HTTP 200`、
|
|
105
|
-
`dsh-molbio-tools/client.js&rev=
|
|
104
|
+
读回结果:`HTTP 200`、38180 字节;页面里的 `window.__DSH_BOOT__` 图包含
|
|
105
|
+
`dsh-molbio-tools/client.js&rev=a25c3919676b`,即浏览器半被宿主 Loader 发现并进了客户端图——
|
|
106
106
|
这是"启动成功"的可观察证据,而不只是进程没退出。(先用 token 换 cookie,再带 cookie 读 `/`,
|
|
107
107
|
否则第一跳是 303。)
|
|
108
108
|
|
|
109
109
|
### 3.3 卸载
|
|
110
110
|
|
|
111
111
|
```
|
|
112
|
-
- dsh-molbio-tools file:D:/.../dsh-molbio-tools-0.
|
|
113
|
-
Done in
|
|
112
|
+
- dsh-molbio-tools file:D:/.../dsh-molbio-tools-0.22.0.tgz
|
|
113
|
+
Done in 421ms
|
|
114
114
|
```
|
|
115
115
|
|
|
116
116
|
卸载后四项同时回到初始状态:
|
|
@@ -133,7 +133,7 @@ pnpm 的 hoisted linker 会在 `node_modules/` 留下一个指向仓库目录的
|
|
|
133
133
|
|
|
134
134
|
```
|
|
135
135
|
node test/contract.mjs → 16 checks passed(host 契约、client 产物、tarball 覆盖)
|
|
136
|
-
node test/preset-health.mjs → all 30 rows load on dsh 0.2.1-rc.
|
|
136
|
+
node test/preset-health.mjs → all 30 rows load on dsh 0.2.1-rc.3
|
|
137
137
|
node test/smoke.mjs → registered 60 tools, all smoke tests passed
|
|
138
138
|
```
|
|
139
139
|
|
|
@@ -43,6 +43,10 @@ or a fixed-source manual review.
|
|
|
43
43
|
}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
> 这段是**提交时那个固定 commit** 的原文(所以 rc.3 当时是 `unknown`)。0.22.0 起 rc.3 已在其上
|
|
47
|
+
> 跑过同一套一次性 Profile 与守卫,`dshReleases` 已改为 `compatible`,见
|
|
48
|
+
> [store-evidence.md](store-evidence.md) 第 1–2 节。
|
|
49
|
+
|
|
46
50
|
同一个 patch 文件里的两个 insert(顺序即应用顺序),分工固定:
|
|
47
51
|
|
|
48
52
|
```yaml
|
package/docs/workflow.md
CHANGED
|
@@ -456,7 +456,7 @@ git status --short # lib/client.js 与 packages/molbio-panel/lib/clien
|
|
|
456
456
|
### DSH 升级后:preset 组合的维护(必做)
|
|
457
457
|
|
|
458
458
|
`preset/molbio-lab/agent.cordis.yml` 是官方 `standard` 预设的**行清单**副本 + 末尾一行
|
|
459
|
-
`tool-molbio`(当前基线:**dsh 0.2.1-rc.
|
|
459
|
+
`tool-molbio`(当前基线:**dsh 0.2.1-rc.3**)。它不会自动跟随 DSH 升级,因此每次升级
|
|
460
460
|
DSH 后:
|
|
461
461
|
|
|
462
462
|
1. 取新版的 shipped `standard`:
|
package/index.mjs
CHANGED
|
@@ -104,9 +104,16 @@ import {
|
|
|
104
104
|
} from './methylation.mjs';
|
|
105
105
|
import {
|
|
106
106
|
addPapers,
|
|
107
|
+
esummarySource,
|
|
107
108
|
libraryPath,
|
|
108
109
|
loadLibrary,
|
|
110
|
+
parseEsearch,
|
|
111
|
+
parseEsummaryRecords,
|
|
109
112
|
pmidFromUrl,
|
|
113
|
+
pubmedApiKey,
|
|
114
|
+
pubmedFetchUrl,
|
|
115
|
+
pubmedSearchUrl,
|
|
116
|
+
pubmedSummaryUrl,
|
|
110
117
|
removePaper,
|
|
111
118
|
saveLibrary,
|
|
112
119
|
updatePaper,
|
|
@@ -6888,14 +6895,34 @@ const phylogeneticTreeTool = (ctx) => define({
|
|
|
6888
6895
|
},
|
|
6889
6896
|
});
|
|
6890
6897
|
|
|
6898
|
+
/** Fetch one E-utilities URL through the harness web service and parse JSON. */
|
|
6899
|
+
async function eutilsRequest(web, url, signal, label) {
|
|
6900
|
+
let result;
|
|
6901
|
+
try {
|
|
6902
|
+
result = await web.fetch({ url }, signal);
|
|
6903
|
+
} catch (error) {
|
|
6904
|
+
throw new MolbioInputError(`${label} could not be reached: ${error instanceof Error ? error.message : String(error)}`);
|
|
6905
|
+
}
|
|
6906
|
+
const body = result?.body?.content;
|
|
6907
|
+
if (typeof body !== 'string' || body === '') {
|
|
6908
|
+
throw new MolbioInputError(`${label} returned no content (status ${result?.statusCode ?? 'unknown'})`);
|
|
6909
|
+
}
|
|
6910
|
+
try {
|
|
6911
|
+
return JSON.parse(body);
|
|
6912
|
+
} catch {
|
|
6913
|
+
throw new MolbioInputError(`${label} returned a non-JSON response (status ${result?.statusCode ?? 'unknown'})`);
|
|
6914
|
+
}
|
|
6915
|
+
}
|
|
6916
|
+
|
|
6891
6917
|
const pubmedAbstractTool = (ctx) => define({
|
|
6892
6918
|
name: 'molbio_pubmed_abstract',
|
|
6893
|
-
description: 'Fetch PubMed abstracts for PMIDs via NCBI E-utilities (efetch). Requires the deployment to provide the web fetch capability; otherwise it reports that clearly. Use it to pull abstracts for papers found by molbio_pubmed_search before adding them to the library.',
|
|
6919
|
+
description: 'Fetch PubMed abstracts for PMIDs via NCBI E-utilities (efetch). Requires the deployment to provide the web fetch capability; otherwise it reports that clearly. Pass api_key (or set NCBI_API_KEY / MOLBIO_NCBI_API_KEY) to attribute the request to your NCBI key and raise the rate limit. Use it to pull abstracts for papers found by molbio_pubmed_search before adding them to the library.',
|
|
6894
6920
|
parameters: {
|
|
6895
6921
|
type: 'object',
|
|
6896
6922
|
required: ['pmids'],
|
|
6897
6923
|
properties: {
|
|
6898
6924
|
pmids: { type: 'array', items: { type: 'string' }, description: 'PubMed IDs, e.g. ["37607951"].' },
|
|
6925
|
+
api_key: { type: 'string', description: 'Optional NCBI API key for E-utilities. Prefer setting NCBI_API_KEY in the environment: an inline key appears in the conversation transcript.' },
|
|
6899
6926
|
},
|
|
6900
6927
|
},
|
|
6901
6928
|
outputSchema: {
|
|
@@ -6939,7 +6966,8 @@ const pubmedAbstractTool = (ctx) => define({
|
|
|
6939
6966
|
if (web === undefined || typeof web.fetch !== 'function') {
|
|
6940
6967
|
throw new MolbioInputError('this deployment provides no web fetch capability, so PubMed abstracts cannot be retrieved; molbio_pubmed_search still works for titles and snippets');
|
|
6941
6968
|
}
|
|
6942
|
-
const
|
|
6969
|
+
const apiKey = pubmedApiKey(args);
|
|
6970
|
+
const url = pubmedFetchUrl(pmids, apiKey);
|
|
6943
6971
|
const result = await web.fetch({ url }, exec?.signal);
|
|
6944
6972
|
const body = result?.body?.content;
|
|
6945
6973
|
if (typeof body !== 'string' || body === '') {
|
|
@@ -7290,21 +7318,24 @@ function paperServiceDeps(ctx, exec, args) {
|
|
|
7290
7318
|
|
|
7291
7319
|
const pubmedSearchTool = (ctx) => define({
|
|
7292
7320
|
name: 'molbio_pubmed_search',
|
|
7293
|
-
description: 'Search
|
|
7321
|
+
description: 'Search PubMed for literature. With an NCBI API key (the api_key argument, or NCBI_API_KEY / MOLBIO_NCBI_API_KEY in the environment) it queries the official E-utilities API and returns PubMed records with PMIDs, titles, journal/year, authors and DOI; without a key it falls back to the harness web-search service. Add the papers you keep to the reading library with molbio_paper_add.',
|
|
7294
7322
|
parameters: {
|
|
7295
7323
|
type: 'object',
|
|
7296
7324
|
required: ['query'],
|
|
7297
7325
|
properties: {
|
|
7298
7326
|
query: requiredString('Search query, e.g. "CRISPR base editing review 2024" or "site:pubmed.ncbi.nlm.nih.gov KRAS G12D".'),
|
|
7299
7327
|
max_results: { type: 'integer', description: 'Maximum results (1-20, default 8).' },
|
|
7328
|
+
api_key: { type: 'string', description: 'Optional NCBI API key for the official PubMed E-utilities API. Prefer setting NCBI_API_KEY in the environment: an inline key appears in the conversation transcript.' },
|
|
7300
7329
|
},
|
|
7301
7330
|
},
|
|
7302
7331
|
outputSchema: {
|
|
7303
7332
|
type: 'object',
|
|
7304
7333
|
additionalProperties: false,
|
|
7305
|
-
required: ['query', 'sources'],
|
|
7334
|
+
required: ['query', 'backend', 'sources'],
|
|
7306
7335
|
properties: {
|
|
7307
7336
|
query: { type: 'string' },
|
|
7337
|
+
backend: { type: 'string', enum: ['pubmed-eutils', 'web-search'] },
|
|
7338
|
+
total_matches: { type: 'integer' },
|
|
7308
7339
|
answer: { type: 'string' },
|
|
7309
7340
|
sources: {
|
|
7310
7341
|
type: 'array',
|
|
@@ -7317,14 +7348,22 @@ const pubmedSearchTool = (ctx) => define({
|
|
|
7317
7348
|
url: { type: 'string' },
|
|
7318
7349
|
snippet: { type: 'string' },
|
|
7319
7350
|
pmid: { type: 'string' },
|
|
7351
|
+
doi: { type: 'string' },
|
|
7352
|
+
authors: { type: 'string' },
|
|
7353
|
+
journal: { type: 'string' },
|
|
7354
|
+
year: { type: 'string' },
|
|
7320
7355
|
},
|
|
7321
7356
|
},
|
|
7322
7357
|
},
|
|
7323
7358
|
},
|
|
7324
7359
|
},
|
|
7325
7360
|
render(value) {
|
|
7326
|
-
|
|
7327
|
-
|
|
7361
|
+
const via = value.backend === 'pubmed-eutils' ? ' via PubMed E-utilities' : '';
|
|
7362
|
+
if (value.sources.length === 0) return `no results for "${value.query}"${via}`;
|
|
7363
|
+
const matches = value.total_matches === undefined
|
|
7364
|
+
? ''
|
|
7365
|
+
: ` — ${value.total_matches} matching record${value.total_matches === 1 ? '' : 's'}`;
|
|
7366
|
+
const lines = [`results for "${value.query}"${via}${matches}:`];
|
|
7328
7367
|
if (value.answer !== undefined) lines.push(value.answer);
|
|
7329
7368
|
for (const source of value.sources) {
|
|
7330
7369
|
const pmid = source.pmid !== undefined ? ` [PMID ${source.pmid}]` : '';
|
|
@@ -7334,12 +7373,26 @@ const pubmedSearchTool = (ctx) => define({
|
|
|
7334
7373
|
return lines.join('\n');
|
|
7335
7374
|
},
|
|
7336
7375
|
async execute(args, exec) {
|
|
7337
|
-
const web = ctx.get('web');
|
|
7338
|
-
if (web === undefined) throw new MolbioInputError('the web search service is not available in this composition; molbio_pubmed_search needs ctx.web');
|
|
7339
7376
|
const maxResults = args.max_results ?? 8;
|
|
7340
7377
|
if (!Number.isInteger(maxResults) || maxResults < 1 || maxResults > 20) {
|
|
7341
7378
|
throw new MolbioInputError('max_results must be an integer between 1 and 20');
|
|
7342
7379
|
}
|
|
7380
|
+
const web = ctx.get('web');
|
|
7381
|
+
const apiKey = pubmedApiKey(args);
|
|
7382
|
+
if (apiKey !== undefined) {
|
|
7383
|
+
if (web === undefined || typeof web.fetch !== 'function') {
|
|
7384
|
+
throw new MolbioInputError('an NCBI API key was provided, but this deployment provides no web fetch capability; the official PubMed API needs ctx.web.fetch');
|
|
7385
|
+
}
|
|
7386
|
+
const search = await eutilsRequest(web, pubmedSearchUrl(args.query, maxResults, apiKey), exec?.signal, 'PubMed esearch');
|
|
7387
|
+
const { ids, count } = parseEsearch(search);
|
|
7388
|
+
if (ids.length === 0) {
|
|
7389
|
+
return { query: args.query, backend: 'pubmed-eutils', total_matches: count, sources: [] };
|
|
7390
|
+
}
|
|
7391
|
+
const summary = await eutilsRequest(web, pubmedSummaryUrl(ids, apiKey), exec?.signal, 'PubMed esummary');
|
|
7392
|
+
const sources = parseEsummaryRecords(summary).map(esummarySource);
|
|
7393
|
+
return { query: args.query, backend: 'pubmed-eutils', total_matches: count, sources };
|
|
7394
|
+
}
|
|
7395
|
+
if (web === undefined) throw new MolbioInputError('the web search service is not available in this composition; molbio_pubmed_search needs ctx.web (or an NCBI API key plus ctx.web.fetch)');
|
|
7343
7396
|
const result = await web.search({ query: args.query, maxResults }, exec?.signal);
|
|
7344
7397
|
const sources = result.sources.map((source) => {
|
|
7345
7398
|
const pmid = pmidFromUrl(source.url ?? '');
|
|
@@ -7352,6 +7405,7 @@ const pubmedSearchTool = (ctx) => define({
|
|
|
7352
7405
|
});
|
|
7353
7406
|
return {
|
|
7354
7407
|
query: args.query,
|
|
7408
|
+
backend: 'web-search',
|
|
7355
7409
|
...result.content !== undefined ? { answer: result.content } : {},
|
|
7356
7410
|
sources,
|
|
7357
7411
|
};
|
|
@@ -7520,9 +7574,9 @@ const PROMPT_SECTION = `Molecular-biology tools (dsh-molbio-tools) are available
|
|
|
7520
7574
|
- Sequences & files: molbio_align (local alignment with a readable match line), molbio_msa_align (progressive multiple sequence alignment — pass an array of sequences or a workspace FASTA), molbio_conservation (consensus + per-column identity/conservation + variable positions; accepts aligned rows or raw sequences it aligns first), molbio_sequence_logo (draws the same alignment as a sequence logo SVG — pass the molbio_msa_align output rows as the alignment argument; letters are sized by per-position information content), molbio_fasta_fastq (FASTA/FASTQ stats/extract/convert/QC on workspace files), molbio_batch_analyze (run one summary/ORF/enzyme analysis over every matching FASTA/FASTQ/GenBank/.dna file in a directory and write one CSV/TSV table), molbio_extract_region (pull a CDS/promoter/region from a plasmid file by feature label or coordinates).
|
|
7521
7575
|
- CRISPR: molbio_grna_design (family-aware guide RNAs over a sequence or plasmid file: cas_family spcas9/sacas9/cas12a/cas13a/cas13b/cas13d selects the PAM side, crRNA length, seed and target type — Cas12a uses a 5' TTTV PAM and 23 nt crRNA; Cas13 targets RNA and reports a PFS preference rather than a PAM. Both strands are scanned for DNA families, the sense strand by default for Cas13; guides are filtered on GC/poly-T/self-complementarity and ranked, with a mismatch-tolerant off-target search over the same sequence — reaching a whole genome simply means passing it as the target). Pass the top candidates on for ordering with save_path (CSV) or map_path (SVG map); the ranking is a heuristic and says so, so present it as candidate ranking, never as a validated efficiency prediction.
|
|
7522
7576
|
- Map extras: pass gc_skew: true for the GC skew ring, show_unique_cutters: true to mark single-cutting enzymes on the map.
|
|
7523
|
-
- Literature & records: molbio_pubmed_abstract (fetch abstracts by PMID —
|
|
7577
|
+
- Literature & records: molbio_pubmed_abstract (fetch abstracts by PMID — needs web fetch; pass api_key or set NCBI_API_KEY to use your NCBI key), molbio_paper_export_bibtex (papers.json → .bib), molbio_protocol_add / molbio_protocol_list / molbio_protocol_update (protocols.json), molbio_experiment_log / molbio_experiment_list (experiments.json).
|
|
7524
7578
|
- Bench analysis (v19): molbio_fastq_qc (read-level QC for a FASTQ file: per-base quality with quartiles, per-read quality, base content, GC distribution, length, exact duplication, over-represented sequences, adapter screen, plus a report SVG — use it for "is this run good"), molbio_codon_usage (CAI/RSCU/Nc/GC3, rare-codon list and CDS CpG against E. coli/yeast/human — use it for "will this gene express"; it explains, molbio_codon_optimize rewrites), molbio_phylogenetic_tree (distance method: p-distance/JC/K2P/Tamura-Nei, UPGMA or neighbour-joining, bootstrap support, consensus, Newick file and an SVG tree — it is NOT a maximum-likelihood or Bayesian analysis, and bootstrap percentages are not p-values), molbio_pcr_simulate (where a primer pair amplifies: product sizes and coordinates on both strands, mismatch detail, the 3'-end exact-match requirement, circular templates, screening other templates, and an expected gel — use it for "what band will I see"), molbio_gc_composition (sliding-window GC, CpG islands under the Gardiner-Garden or Takai criteria, GC/AT skew with the cumulative-skew origin/terminus indication, dinucleotide obs/exp, entropy, complexity, N50/L50, plus a figure — use it for "is this a promoter or an origin").
|
|
7525
|
-
- Literature: molbio_pubmed_search (
|
|
7579
|
+
- Literature: molbio_pubmed_search — with an NCBI API key (api_key argument, or NCBI_API_KEY / MOLBIO_NCBI_API_KEY in the environment) it uses the official PubMed E-utilities API and returns PMIDs, titles, journal/year, authors and DOI; without a key it falls back to the harness web search. The reading library is molbio_paper_add / molbio_paper_list / molbio_paper_update / molbio_paper_remove (papers.json in the workspace).
|
|
7526
7580
|
|
|
7527
7581
|
Cloning workflow rules (follow them to avoid slow re-derivation):
|
|
7528
7582
|
- To clone a sequence into a plasmid, call molbio_clone_simulate FIRST — pass the BARE insert with add_flanks: true and the tool adds the enzyme recognition sites itself. The tool also validates single cutting, insert orientation, and internal sites itself: never pre-verify enzymes, cutting counts, or flank construction by thinking. Its output already contains final_sequence AND the remapped feature coordinates (features field). NEVER recompute feature coordinates or insert positions by reasoning — always take them from the tool output.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-molbio-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Zero-dependency molecular-biology plugin bundle for the DeepSeek Harness: sequence analysis, primer design (incl. cross-intron qPCR primers, TaqMan hydrolysis/MGB probes with ordering CSV and multiplex compatibility), in-silico PCR, cloning simulation, SnapGene .dna parsing and plasmid maps, Sanger verification, protein tools (incl. helical wheel and hydropathy plots), methylation-aware restriction analysis with double-digest buffer checks, qPCR analysis and plotting, multiple sequence alignment that conserves every input residue, conservation analysis, sequence logos, codon-usage analysis, phylogenetic trees with bootstrap support, Newick I/O and long-name label wrapping, CpG-island/GC-skew composition analysis, read-level FASTQ QC, batch file analysis with CSV export, silent restriction-site mutagenesis, family-aware CRISPR gRNA design (SpCas9/SaCas9/Cas12a/Cas13) with off-target search, an opt-in hand-off that shows a generated plot to the model as an attached image, plus a browser-side right-sidebar panel (plasmid maps and sequence logos drawn in the Web GUI) and a literature/protocol/experiment record system.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"dshReleases": {
|
|
97
97
|
"0.2.1-rc.1": "unknown",
|
|
98
98
|
"0.2.1-rc.2": "compatible",
|
|
99
|
-
"0.2.1-rc.3": "
|
|
99
|
+
"0.2.1-rc.3": "compatible"
|
|
100
100
|
}
|
|
101
101
|
},
|
|
102
102
|
"bundle": {
|
package/papers.mjs
CHANGED
|
@@ -132,6 +132,157 @@ export function pmidFromUrl(url) {
|
|
|
132
132
|
return match?.[1];
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// ── PubMed E-utilities (official API) ───────────────────────────────────────
|
|
136
|
+
//
|
|
137
|
+
// `molbio_pubmed_search` used to depend entirely on the harness web-search
|
|
138
|
+
// service. The official API is a different thing: it answers with PubMed's own
|
|
139
|
+
// records (PMIDs, indexed titles, journal/year/DOI) instead of whatever a
|
|
140
|
+
// general search engine indexed, and an NCBI API key raises the rate limit from
|
|
141
|
+
// 3 to 10 requests per second. The helpers below build the request URLs and
|
|
142
|
+
// normalize the responses; the tool keeps the transport (ctx.web.fetch) so the
|
|
143
|
+
// existing sandbox/consent rules still apply.
|
|
144
|
+
|
|
145
|
+
/** The E-utilities endpoint prefix. */
|
|
146
|
+
export const EUTILS_BASE = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils';
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The NCBI API key for this call: an explicit tool argument first, then the two
|
|
150
|
+
* environment variables a deployment can set once. A blank value counts as
|
|
151
|
+
* absent, so an empty `api_key` falls back instead of sending `api_key=` (NCBI
|
|
152
|
+
* rejects the empty form with a 400 that blames the query).
|
|
153
|
+
*/
|
|
154
|
+
export function pubmedApiKey(args, env = process.env) {
|
|
155
|
+
for (const candidate of [args?.api_key, env?.NCBI_API_KEY, env?.MOLBIO_NCBI_API_KEY]) {
|
|
156
|
+
if (typeof candidate === 'string' && candidate.trim() !== '') return candidate.trim();
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function eutilsUrl(endpoint, params) {
|
|
162
|
+
const pairs = Object.entries(params)
|
|
163
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
164
|
+
.map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`);
|
|
165
|
+
return `${EUTILS_BASE}/${endpoint}?${pairs.join('&')}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** esearch: the PMIDs matching a query, relevance-ranked. */
|
|
169
|
+
export function pubmedSearchUrl(query, maxResults, apiKey) {
|
|
170
|
+
return eutilsUrl('esearch.fcgi', {
|
|
171
|
+
db: 'pubmed',
|
|
172
|
+
term: query,
|
|
173
|
+
retmax: maxResults,
|
|
174
|
+
retmode: 'json',
|
|
175
|
+
sort: 'relevance',
|
|
176
|
+
tool: 'dsh-molbio-tools',
|
|
177
|
+
api_key: apiKey,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** esummary: title/authors/journal/year/DOI for the PMIDs esearch returned. */
|
|
182
|
+
export function pubmedSummaryUrl(pmids, apiKey) {
|
|
183
|
+
return eutilsUrl('esummary.fcgi', {
|
|
184
|
+
db: 'pubmed',
|
|
185
|
+
id: pmids.join(','),
|
|
186
|
+
retmode: 'json',
|
|
187
|
+
tool: 'dsh-molbio-tools',
|
|
188
|
+
api_key: apiKey,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** efetch: the full XML records (abstracts live here, not in esummary). */
|
|
193
|
+
export function pubmedFetchUrl(pmids, apiKey) {
|
|
194
|
+
return eutilsUrl('efetch.fcgi', {
|
|
195
|
+
db: 'pubmed',
|
|
196
|
+
id: pmids.join(','),
|
|
197
|
+
retmode: 'xml',
|
|
198
|
+
tool: 'dsh-molbio-tools',
|
|
199
|
+
api_key: apiKey,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The PMIDs and total hit count in an esearch JSON response. */
|
|
204
|
+
export function parseEsearch(payload) {
|
|
205
|
+
if (typeof payload?.error === 'string' && payload.error !== '') {
|
|
206
|
+
throw new MolbioInputError(`PubMed E-utilities error: ${payload.error}`);
|
|
207
|
+
}
|
|
208
|
+
const result = payload?.esearchresult;
|
|
209
|
+
if (result === undefined || result === null || typeof result !== 'object') {
|
|
210
|
+
throw new MolbioInputError('PubMed esearch returned no esearchresult — is the response really E-utilities JSON?');
|
|
211
|
+
}
|
|
212
|
+
if (typeof result.ERROR === 'string' && result.ERROR !== '') {
|
|
213
|
+
throw new MolbioInputError(`PubMed esearch failed: ${result.ERROR}`);
|
|
214
|
+
}
|
|
215
|
+
const ids = Array.isArray(result.idlist)
|
|
216
|
+
? result.idlist.map((id) => String(id).trim()).filter((id) => /^\d+$/.test(id))
|
|
217
|
+
: [];
|
|
218
|
+
const count = Number.parseInt(String(result.count ?? ''), 10);
|
|
219
|
+
return { ids, count: Number.isFinite(count) ? count : ids.length };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Normalize the records in an esummary JSON response.
|
|
224
|
+
*
|
|
225
|
+
* The response puts a `uids` array beside one object per PMID; walking `uids`
|
|
226
|
+
* (rather than Object.keys) keeps the esearch order, which is relevance order.
|
|
227
|
+
*/
|
|
228
|
+
export function parseEsummaryRecords(payload) {
|
|
229
|
+
if (typeof payload?.error === 'string' && payload.error !== '') {
|
|
230
|
+
throw new MolbioInputError(`PubMed E-utilities error: ${payload.error}`);
|
|
231
|
+
}
|
|
232
|
+
const result = payload?.result;
|
|
233
|
+
if (result === undefined || result === null || typeof result !== 'object') {
|
|
234
|
+
throw new MolbioInputError('PubMed esummary returned no result — is the response really E-utilities JSON?');
|
|
235
|
+
}
|
|
236
|
+
if (typeof result.error === 'string' && result.error !== '') {
|
|
237
|
+
throw new MolbioInputError(`PubMed esummary failed: ${result.error}`);
|
|
238
|
+
}
|
|
239
|
+
const uids = Array.isArray(result.uids) ? result.uids.map(String) : [];
|
|
240
|
+
const records = [];
|
|
241
|
+
for (const uid of uids) {
|
|
242
|
+
const row = result[uid];
|
|
243
|
+
if (row === undefined || row === null || typeof row !== 'object') continue;
|
|
244
|
+
const title = typeof row.title === 'string' ? row.title.trim() : '';
|
|
245
|
+
const authors = Array.isArray(row.authors)
|
|
246
|
+
? row.authors
|
|
247
|
+
.map((author) => (typeof author?.name === 'string' ? author.name.trim() : ''))
|
|
248
|
+
.filter((name) => name !== '')
|
|
249
|
+
: [];
|
|
250
|
+
const articleIds = Array.isArray(row.articleids) ? row.articleids : [];
|
|
251
|
+
const doi = articleIds.find((id) => id?.idtype === 'doi' && typeof id.value === 'string')?.value ?? '';
|
|
252
|
+
const pubdate = typeof row.pubdate === 'string' ? row.pubdate : '';
|
|
253
|
+
const year = /\b(?:1[89]|20)\d{2}\b/.exec(pubdate)?.[0] ?? '';
|
|
254
|
+
const journal = typeof row.fulljournalname === 'string' && row.fulljournalname !== ''
|
|
255
|
+
? row.fulljournalname
|
|
256
|
+
: (typeof row.source === 'string' ? row.source : '');
|
|
257
|
+
records.push({
|
|
258
|
+
pmid: String(row.uid ?? uid),
|
|
259
|
+
title,
|
|
260
|
+
authors,
|
|
261
|
+
journal,
|
|
262
|
+
pubdate,
|
|
263
|
+
year,
|
|
264
|
+
doi,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return records;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** One esummary record as a search source (the shape molbio_pubmed_search returns). */
|
|
271
|
+
export function esummarySource(record) {
|
|
272
|
+
const source = {
|
|
273
|
+
title: record.title === '' ? `PMID ${record.pmid}` : record.title,
|
|
274
|
+
url: `https://pubmed.ncbi.nlm.nih.gov/${record.pmid}/`,
|
|
275
|
+
pmid: record.pmid,
|
|
276
|
+
};
|
|
277
|
+
const authorText = record.authors.length > 2 ? `${record.authors[0]} et al.` : record.authors.join(', ');
|
|
278
|
+
const snippet = [record.journal, record.year, authorText].filter((part) => part !== '').join(' · ');
|
|
279
|
+
if (snippet !== '') source.snippet = snippet;
|
|
280
|
+
if (record.authors.length > 0) source.authors = record.authors.join(', ');
|
|
281
|
+
if (record.journal !== '') source.journal = record.journal;
|
|
282
|
+
if (record.year !== '') source.year = record.year;
|
|
283
|
+
if (record.doi !== '') source.doi = record.doi;
|
|
284
|
+
return source;
|
|
285
|
+
}
|
|
135
286
|
/** Escape a string for BibTeX. */
|
|
136
287
|
function bibtexEscape(text) {
|
|
137
288
|
return String(text)
|
|
@@ -48,14 +48,15 @@
|
|
|
48
48
|
# list carries no comments of its own, so none appear below; provenance and the
|
|
49
49
|
# upgrade rules live in this header.
|
|
50
50
|
#
|
|
51
|
-
# Baseline: the `standard` preset of dsh 0.2.1-rc.
|
|
51
|
+
# Baseline: the `standard` preset of dsh 0.2.1-rc.3 (harness root
|
|
52
52
|
# `%APPDATA%\npm\node_modules\@deepseek-ai\dsh`, file
|
|
53
53
|
# `node_modules/@deepseek-ai/dsh-web-app/presets/standard.patch.yml`, whose
|
|
54
54
|
# rows live under the `preset-standard` row's `config.plugins:`).
|
|
55
55
|
#
|
|
56
|
-
# 0.2.1-rc.
|
|
57
|
-
#
|
|
58
|
-
#
|
|
56
|
+
# 0.2.1-rc.3 ships the SAME row list as 0.2.1-rc.2 (verified by diffing the
|
|
57
|
+
# file), so no row changed in this bump. The last upstream row change was
|
|
58
|
+
# 0.2.1-rc.2: it added `time-context`, `tool-schedule` and `tool-plugin-market`;
|
|
59
|
+
# dropped the two disabled optional subagent providers (`tool-subagent-codex`,
|
|
59
60
|
# `tool-subagent-claude-code`) and `tool-ralph`; dropped the persona `suffix` and
|
|
60
61
|
# both `backgroundMode: continuable` settings; and reworded the plan-mode
|
|
61
62
|
# section. Absorb such a bump verbatim — see workflow.md §4.
|
package/test/smoke.mjs
CHANGED
|
@@ -99,6 +99,9 @@ const memFs = {
|
|
|
99
99
|
};
|
|
100
100
|
|
|
101
101
|
const mockWeb = {
|
|
102
|
+
// Every fetch URL the tools request, so the tests can prove the NCBI key
|
|
103
|
+
// actually travels in the E-utilities query instead of merely being accepted.
|
|
104
|
+
calls: [],
|
|
102
105
|
async search({ query }) {
|
|
103
106
|
return {
|
|
104
107
|
content: `answer for ${query}`,
|
|
@@ -109,9 +112,45 @@ const mockWeb = {
|
|
|
109
112
|
truncated: false,
|
|
110
113
|
};
|
|
111
114
|
},
|
|
112
|
-
async fetch() {
|
|
115
|
+
async fetch({ url } = {}) {
|
|
116
|
+
const target = String(url ?? '');
|
|
117
|
+
this.calls.push(target);
|
|
118
|
+
const json = (payload, statusCode = 200) => ({
|
|
119
|
+
url: target,
|
|
120
|
+
statusCode,
|
|
121
|
+
body: { kind: 'text', content: JSON.stringify(payload) },
|
|
122
|
+
truncated: false,
|
|
123
|
+
});
|
|
124
|
+
if (target.includes('/esearch.fcgi')) {
|
|
125
|
+
if (target.includes('api_key=bad-key')) return json({ error: 'API key invalid' }, 400);
|
|
126
|
+
return json({ esearchresult: { count: '2', retmax: '2', idlist: ['39609418', '12345678'] } });
|
|
127
|
+
}
|
|
128
|
+
if (target.includes('/esummary.fcgi')) {
|
|
129
|
+
return json({
|
|
130
|
+
result: {
|
|
131
|
+
uids: ['39609418', '12345678'],
|
|
132
|
+
'39609418': {
|
|
133
|
+
uid: '39609418',
|
|
134
|
+
title: 'SPLICER: a highly efficient base editing toolbox',
|
|
135
|
+
pubdate: '2024 Nov',
|
|
136
|
+
source: 'Nat Biotechnol',
|
|
137
|
+
fulljournalname: 'Nature biotechnology',
|
|
138
|
+
authors: [{ name: 'Li X' }, { name: 'Wang Y' }, { name: 'Zhang Z' }],
|
|
139
|
+
articleids: [{ idtype: 'doi', value: '10.1038/s41587-024-02460-4' }],
|
|
140
|
+
},
|
|
141
|
+
'12345678': {
|
|
142
|
+
uid: '12345678',
|
|
143
|
+
title: 'An older paper',
|
|
144
|
+
pubdate: '2019',
|
|
145
|
+
source: 'Cell',
|
|
146
|
+
authors: [{ name: 'Doe J' }],
|
|
147
|
+
articleids: [],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
113
152
|
return {
|
|
114
|
-
url:
|
|
153
|
+
url: target,
|
|
115
154
|
statusCode: 200,
|
|
116
155
|
body: {
|
|
117
156
|
kind: 'text',
|
|
@@ -1891,15 +1930,83 @@ function splicedToGenomic(splicedPos, exons) {
|
|
|
1891
1930
|
|
|
1892
1931
|
// ── pubmed search (mocked web service) ──────────────────────────────────────
|
|
1893
1932
|
|
|
1933
|
+
// The keys below are cleared for the whole section: a developer machine with
|
|
1934
|
+
// NCBI_API_KEY exported must not silently change what these tests exercise.
|
|
1935
|
+
const savedNcbiEnv = {
|
|
1936
|
+
NCBI_API_KEY: process.env.NCBI_API_KEY,
|
|
1937
|
+
MOLBIO_NCBI_API_KEY: process.env.MOLBIO_NCBI_API_KEY,
|
|
1938
|
+
};
|
|
1939
|
+
delete process.env.NCBI_API_KEY;
|
|
1940
|
+
delete process.env.MOLBIO_NCBI_API_KEY;
|
|
1941
|
+
|
|
1894
1942
|
{
|
|
1943
|
+
// no key and no environment: the pre-existing web-search path
|
|
1944
|
+
mockWeb.calls.length = 0;
|
|
1895
1945
|
const out = await run('molbio_pubmed_search', { query: 'CRISPR' });
|
|
1896
1946
|
assert.equal(out.query, 'CRISPR');
|
|
1947
|
+
assert.equal(out.backend, 'web-search');
|
|
1897
1948
|
assert.equal(out.answer, 'answer for CRISPR');
|
|
1898
1949
|
assert.equal(out.sources.length, 2);
|
|
1899
1950
|
assert.equal(out.sources[0].pmid, '12345678');
|
|
1900
1951
|
assert.equal(out.sources[1].pmid, undefined);
|
|
1952
|
+
assert.equal(mockWeb.calls.length, 0, 'the web-search path must not touch the E-utilities transport');
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
{
|
|
1956
|
+
// official PubMed API: an explicit key switches the tool to E-utilities, and
|
|
1957
|
+
// the key travels in the request (esearch AND esummary) rather than merely
|
|
1958
|
+
// being accepted by the argument schema.
|
|
1959
|
+
mockWeb.calls.length = 0;
|
|
1960
|
+
const out = await run('molbio_pubmed_search', { query: 'CRISPR base editing', api_key: 'test-key-123' });
|
|
1961
|
+
assert.equal(out.backend, 'pubmed-eutils');
|
|
1962
|
+
assert.equal(out.total_matches, 2);
|
|
1963
|
+
assert.equal(out.sources.length, 2);
|
|
1964
|
+
assert.equal(out.sources[0].pmid, '39609418');
|
|
1965
|
+
assert.equal(out.sources[0].url, 'https://pubmed.ncbi.nlm.nih.gov/39609418/');
|
|
1966
|
+
assert.match(out.sources[0].title, /SPLICER/);
|
|
1967
|
+
assert.equal(out.sources[0].journal, 'Nature biotechnology');
|
|
1968
|
+
assert.equal(out.sources[0].year, '2024');
|
|
1969
|
+
assert.equal(out.sources[0].doi, '10.1038/s41587-024-02460-4');
|
|
1970
|
+
assert.match(out.sources[0].snippet, /Nature biotechnology · 2024 · Li X et al\./);
|
|
1971
|
+
assert.equal(out.sources[1].authors, 'Doe J');
|
|
1972
|
+
assert.ok(mockWeb.calls.every((url) => url.includes('api_key=test-key-123')));
|
|
1973
|
+
assert.ok(mockWeb.calls.some((url) => url.includes('/esearch.fcgi')));
|
|
1974
|
+
assert.ok(mockWeb.calls.some((url) => url.includes('/esummary.fcgi')));
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
{
|
|
1978
|
+
// the environment fallback is what makes "set the key once" possible
|
|
1979
|
+
const saved = process.env.NCBI_API_KEY;
|
|
1980
|
+
process.env.NCBI_API_KEY = 'env-key-456';
|
|
1981
|
+
try {
|
|
1982
|
+
mockWeb.calls.length = 0;
|
|
1983
|
+
const out = await run('molbio_pubmed_search', { query: 'base editing' });
|
|
1984
|
+
assert.equal(out.backend, 'pubmed-eutils');
|
|
1985
|
+
assert.ok(mockWeb.calls.length === 2 && mockWeb.calls.every((url) => url.includes('api_key=env-key-456')));
|
|
1986
|
+
} finally {
|
|
1987
|
+
delete process.env.NCBI_API_KEY;
|
|
1988
|
+
if (saved !== undefined) process.env.NCBI_API_KEY = saved;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
{
|
|
1993
|
+
// an invalid key is reported as such, not presented as "no results"
|
|
1994
|
+
await assert.rejects(() => run('molbio_pubmed_search', { query: 'x', api_key: 'bad-key' }), /API key invalid/);
|
|
1901
1995
|
}
|
|
1902
1996
|
|
|
1997
|
+
{
|
|
1998
|
+
// abstracts use the same key and the efetch endpoint
|
|
1999
|
+
mockWeb.calls.length = 0;
|
|
2000
|
+
const out = await run('molbio_pubmed_abstract', { pmids: ['12345678'], api_key: 'test-key-123' });
|
|
2001
|
+
assert.equal(out.abstracts[0].error, '');
|
|
2002
|
+
assert.equal(out.abstracts[0].title, 'Test Article Title');
|
|
2003
|
+
assert.ok(mockWeb.calls.length === 1 && mockWeb.calls[0].includes('/efetch.fcgi') && mockWeb.calls[0].includes('api_key=test-key-123'));
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// Restore whatever the developer machine had exported.
|
|
2007
|
+
if (savedNcbiEnv.NCBI_API_KEY !== undefined) process.env.NCBI_API_KEY = savedNcbiEnv.NCBI_API_KEY;
|
|
2008
|
+
if (savedNcbiEnv.MOLBIO_NCBI_API_KEY !== undefined) process.env.MOLBIO_NCBI_API_KEY = savedNcbiEnv.MOLBIO_NCBI_API_KEY;
|
|
2009
|
+
|
|
1903
2010
|
// ── paper library (mocked fs service) ───────────────────────────────────────
|
|
1904
2011
|
|
|
1905
2012
|
{
|