dsh-molbio-tools 0.22.0 → 0.22.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/CHANGELOG.md CHANGED
@@ -10,6 +10,127 @@
10
10
  改为按**包名**引用后模块缓存问题随之消失——没有拷贝,也就没有需要保持同步的版本目录。
11
11
  下文历史条目里出现的 `vN` 目录保持原样,作为当时的记录。
12
12
 
13
+ ## [0.22.2] — 2026-10-30(DSH 0.2.1-rc.5 适配;契约守卫兼容桌面版)
14
+
15
+ **工具行为零变化(60 个)。** 0.22.1 的验收基线是 **0.2.1-rc.3**;这一版把基线推到
16
+ **0.2.1-rc.5**,并按 rc.5 的两种安装形态(npm 包 / 桌面 `app.asar`)各跑了一遍。
17
+
18
+ ### 1. 为什么"补丁号在范围内"不等于适配
19
+
20
+ `engines.dsh` 的 `>=0.2.1-rc.2 <0.3.0-0` 本来就覆盖 rc.5,DSH 的 `unmetRequirements`
21
+ (`@deepseek-ai/dsh-app-boot`)用 `includePrerelease` 判定也通过;但按本仓库的规矩
22
+ ([store-evidence.md](docs/store-evidence.md)),范围只是安装粗筛,**逐版本结论要写进
23
+ `dsh.compatibility.dshReleases`**。rc.3 之后上游发过 rc.4 / rc.5,这一版补上。
24
+
25
+ ### 2. rc.5 上真正跑过的证据
26
+
27
+ - 桌面版 0.2.1-rc.5(Electron 44.7.0 / Node 24.21.0)的完整流程:`npm pack`(115 文件)→
28
+ `dsh plugin add` → `--dump-config`(bundle 层与 preset 行都在)→ 启动 HTTP 200 →
29
+ `__DSH_BOOT__` 含 `dsh-molbio-tools/client.js` → `dsh plugin remove`(0 残留)。
30
+ - `agentPresets/list` 的 `broken` 为空,`pluginInventory/list` 里 molbio-lab 的
31
+ `tool-molbio` 是 `fiberPhase: "active"`(rules.md 规则 1 的判据,说明 preset 里 60 个工具
32
+ 真的挂上了,而不是"组合树看着对")。
33
+ - 上游 `dsh-web-app/presets/standard.patch.yml` 在 rc.3 与 rc.5 之间**逐字节一致**
34
+ (6799 B,同一 sha256),因此 preset 行清单不需要重抄。
35
+ - `preset-health` 30/30 行、`drift-probe` 16/16、`smoke` 60 工具、`bench --offline` 61 题
36
+ 全绿;`contract` 17/17(见下)。
37
+
38
+ ### 3. contract:桌面版不再报"假红"
39
+
40
+ `app.asar` 为省体积裁掉了 `lib/types/*.d.ts`(`dsh-fs` 整个 `lib/types` 目录都没有),
41
+ 于是 0.22.1 的 contract 在桌面版上有两条以 ENOENT 失败——那是**打包差异**,不是契约变化
42
+ (npm tarball 里声明仍在,运行时行为也一致)。这一版把声明守卫改成
43
+ `declarationEvidence()`:先读 `lib/types/*.d.ts`,缺失时读同一份声明的内嵌副本
44
+ (`dsh-llm/lib/typert.host.js`、`dsh-fs-local` / `dsh-attachment-local` 的 runtime),
45
+ 两条断言改由同一组谓词驱动;新增第 17 条检查用**突变文本**驱动这些谓词,证明回退
46
+ 不是"永远通过"(rules.md 规则 9)。npm 安装与桌面安装现在都是 17/17。
47
+
48
+ ### 4. Node 下限对齐 DSH 桌面运行时
49
+
50
+ `engines.node` 从 `>=26` 降到 **`>=24`**,CI 矩阵从 26 改到 24:DSH 0.2.1-rc.5 桌面版
51
+ 内嵌 **Node 24.21.0**,而 `engines.node` 是给 npm/pnpm 看的(DSH 自己只读 `engines.dsh`)。
52
+ 全套测试与 61 题离线 benchmark 都在 24.21.0 上跑过。
53
+
54
+ ### 5. 声明与版本
55
+
56
+ - 两个包 `dshReleases`:`0.2.1-rc.5: compatible`、`0.2.1-rc.4: unknown`(未装机验证)。
57
+ - CI(`test.yml` / `release.yml`)钉 `@deepseek-ai/dsh@0.2.1-rc.5`,Node 24。
58
+ - 根包 `0.22.1 → 0.22.2`;面板包 `0.1.6 → 0.1.7`(只改清单声明,`lib/client.js` 未变)。
59
+ - 基线文档(maintainer / roadmap / workflow / store-evidence / history)同步到 rc.5。
60
+
61
+ ## [0.22.1] — 2026-10-25(PubMed key 改走 DSH 凭据库;环境变量降为后备)
62
+
63
+ **工具数不变(60);没有凭据库的组合,行为与 0.22.0 相同。** 0.22.0 只从 `api_key` 参数与
64
+ `process.env` 读 key;这一版把 DSH 的凭据 seam(`ctx.credentials`)插到两者之间,并把环境变量
65
+ 降为最后一级。
66
+
67
+ ### 1. 环境变量为什么不够好
68
+
69
+ 环境变量会被**每个子进程继承**——包括 agent 自己的 shell 工具:一句 `echo $env:NCBI_API_KEY`
70
+ 就能把 key 读进对话;它还会出现在进程环境块、崩溃转储与用户的注册表环境里(Windows 的
71
+ `HKCU\Environment` 是明文)。对任何云端凭证这都是不必要的暴露面;NCBI 的 key 虽然只用于
72
+ 归属与限速,泄漏仍意味着别人可以用你的配额、顶着你的身份发请求。
73
+
74
+ DSH 自带的凭据库(`@deepseek-ai/dsh-credentials-local`)就是为这件事存在的:值存在
75
+ `~/.dsh/.credentials.yaml` 里,**不注入环境**、文件仅属主可读、改动热重载,
76
+ `describe()` 只报告是否已设置与来源、从不回显值,而且**存储值优先于启动环境**。
77
+
78
+ ### 2. 新的解析顺序
79
+
80
+ | 优先级 | 来源 | 说明 |
81
+ | --- | --- | --- |
82
+ | 1 | `api_key` 参数 | 单次调用显式指定;会出现在对话记录与工具卡片里 |
83
+ | 2 | 凭据库 `NCBI_API_KEY` | 推荐;不进入环境,agent 的 shell 子进程读不到 |
84
+ | 3 | 凭据库 `MOLBIO_NCBI_API_KEY` | 同上,插件专用命名 |
85
+ | 4 | 进程环境 `NCBI_API_KEY` / `MOLBIO_NCBI_API_KEY` | 兼容 0.22.0 的用法;暴露面最大 |
86
+
87
+ 实现上调用 `ctx.credentials.resolve(ref)`(存在性检查后才调用),因此凭据库自身的回退层
88
+ (启动环境、项目 `.env`、harness home 的 `.env`)也一并继承;provider 不存在或拒绝该引用时
89
+ **静默回退到 `process.env`**,没有凭据库的组合行为不变。
90
+
91
+ ### 3. key 放进凭据库的三种方式
92
+
93
+ 1. **插件详情页(推荐)**:插件 → 已安装插件 → `dsh-molbio-tools` → **PubMed API key** 一节,
94
+ 粘贴后点 **Save**;见 §4。
95
+ 2. 编辑 `~/.dsh/.credentials.yaml`(产品创建、仅属主可读、热重载):在 `refs:` 下加一行
96
+ `NCBI_API_KEY: 你的key`;下一次工具调用即生效,无需重启。
97
+ 3. 在 DSH 的设置界面保存同名凭据(该界面只显示“是否已设置/来源”,不显示值)。
98
+
99
+ > **这不是对 agent 的硬隔离。** 凭据库自己的文档写得很清楚:agent 的工具进程以同一个 OS 用户
100
+ > 运行,刻意去读那个文件仍然读得到。它解决的是**自动泄漏**:环境变量会被每个 shell 子进程
101
+ > 无条件继承,而凭据库不会。真正要把密钥挡在 agent 之外,需要换提供方(OS 钥匙串/KMS),
102
+ > 当前产品没有随附这类 provider。
103
+
104
+ ### 4. 图形入口:插件详情页里的 PubMed API key
105
+
106
+ 「改 YAML」对没有计算机基础的用户不是可接受的入口。浏览器半因此注册了一个
107
+ `plugins.detail.section` 条目(`id: molbio-ncbi-key`),只在**本包的 bundle 页**与它声明的**行页**
108
+ 上渲染,其它插件返回 `null`:
109
+
110
+ - 输入框是 `type=password`,只写不读;状态行来自 `ctx.remote.credentials.describe(['NCBI_API_KEY'])`,
111
+ 只显示「已配置 / 未配置 / 来源」,**不回显值**。
112
+ - **Save** 走 `ctx.remote.credentials.set('NCBI_API_KEY', value)`,**Remove** 走 `unset`;
113
+ 保存后输入框立即清空,成功/拒绝消息原样显示(`credential/rejected` 的 provider 原文)。
114
+ - 它和 CLI/工具读的是**同一个**凭据引用,所以两边不需要同步;没有凭据 provider 的部署会显示
115
+ Unavailable,而工具仍按 §2 的顺序回退到环境变量。
116
+
117
+ > **测试中发现并修掉的一处真实缺陷(同一版内)**:第一版把 `ctx.remote.credentials` 当成普通
118
+ > 属性直接访问,而它是 **fiber injection**——官方设置页在 `inject` 列表里显式声明
119
+ > `remote.credentials`。没有声明时该命名空间是 `undefined`,点 Save 什么也不会写(状态看起来
120
+ > 只是"没保存")。现在整段的注册被 `ctx.inject(['remote.credentials'], …)` 门控:有命名空间才
121
+ > 出现,没有则整段不注册(面板不受影响);写入之后还会重新 `describe`,若 Host 仍报"未配置"
122
+ > 会把这句话显示出来,而不是假装成功。
123
+
124
+ ### 5. 边界与验证
125
+
126
+ - 默认路径(无 key)仍是 web 检索;有 key 仍是 esearch + esummary + efetch,速率上限不变。
127
+ - 客户端产物因此变了:`lib/client.js` 与 `packages/molbio-panel/lib/client.js` 重新构建,
128
+ 面板包 `0.1.5 → 0.1.6`。
129
+ - smoke 新增三条:**凭据库压过环境变量**、**显式参数压过凭据库**、**provider 抛错时回退到环境**;
130
+ 原有的“无 key 不碰 E-utilities”与“坏 key 报错”保留。`test/client.mjs` 新增详情页座位的
131
+ 等待/注册、本包 subject 渲染、其它 subject 返回 null,以及凭据视图/失败信息三个纯函数。
132
+ - `npm test` 全绿;`client-bundle.mjs --check` 与 `preset-patch.mjs --check` 与源一致。
133
+
13
134
  ## [0.22.0] — 2026-10-25(DSH 0.2.1-rc.3 适配;PubMed 官方 API + NCBI API key)
14
135
 
15
136
  **工具数不变(60)。新增一条通路:`molbio_pubmed_search` / `molbio_pubmed_abstract` 可以带着
package/README.md CHANGED
@@ -55,6 +55,10 @@
55
55
  装完点 **立即启用**(Enable now)——关掉对话框只会装好、不会启用,之后也可以在插件页打开它的开关。
56
56
  3. 重启该 profile,然后在预设选择器里选 "Molecular Biology Lab" 新建会话。
57
57
 
58
+ **(可选)PubMed API key**:重启后打开 **插件 → 已安装插件 → `dsh-molbio-tools` 详情页**,
59
+ 在 **PubMed API key** 一节粘贴 NCBI key 并点 **Save**。它写进 DSH 凭据库(`~/.dsh/.credentials.yaml`),
60
+ 不需要编辑文件、也不进环境变量,下一次检索即生效。没有 key 时检索仍走原来的 web 检索。
61
+
58
62
  ### 次选:命令行安装
59
63
 
60
64
  没有图形界面时(例如服务器上只用 CLI),用这条命令;它和插件页安装等价,都从 npm registry 取包:
@@ -224,7 +228,7 @@ dsh plugin --profile <profile> add dsh-molbio-panel
224
228
 
225
229
  | 工具 | 用途 |
226
230
  | --- | --- |
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 检索 |
231
+ | `molbio_pubmed_search` | 搜 PubMed 文献。提供 NCBI API key(`api_key` 参数、**DSH 凭据库**的 `NCBI_API_KEY`、或环境变量)时走**官方 E-utilities**(esearch + esummary),返回 PMID/标题/期刊/年份/作者/DOI;无 key 时回退到 harness 的 web 检索 |
228
232
  | `molbio_pubmed_abstract` | 按 PMID 拉取摘要(efetch;同样支持 NCBI API key,依赖部署提供 web fetch 能力) |
229
233
  | `molbio_paper_add` / `molbio_paper_list` / `molbio_paper_update` / `molbio_paper_remove` | 阅读库(工作区 `papers.json`):加/列/改/删,按 PMID→URL→标题+年份去重 |
230
234
  | `molbio_paper_export_bibtex` | `papers.json` → BibTeX `.bib`(可按 tag 过滤) |
@@ -557,10 +561,16 @@ mutations remain deferred")。所以插件无法在工作区里合法地落一
557
561
 
558
562
  - 检索与摘要走 harness 的 **web** 服务(部署不提供时工具会明确报错,而不是假装搜到)。
559
563
  - 提供 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 检索通路,行为不变。
564
+ 拿题录(标题/期刊/年份/作者/DOI),`molbio_pubmed_abstract` 用 efetch 拿摘要。key 的解析顺序是
565
+ **`api_key` 参数 → DSH 凭据库(`NCBI_API_KEY`,其次 `MOLBIO_NCBI_API_KEY`)→ 进程环境变量**。
566
+ NCBI 的速率上限是**无 key 3 次/秒、有 key 10 次/秒**,一次带 key 的检索是 2 个请求
567
+ (esearch + esummary)。没有 key 时检索仍走原来的 web 检索通路,行为不变。
568
+ - **优先把 key 放凭据库,而不是环境变量。** 凭据库是 `~/.dsh/.credentials.yaml`(`refs:` 下写
569
+ `NCBI_API_KEY: …`;图形入口是**插件 → 已安装插件 → dsh-molbio-tools → PubMed API key**):
570
+ 不进环境、仅属主可读、改完热重载、下一次调用即生效。环境变量会被**每个子进程继承**——包括 agent 自己的 shell 工具,`echo $env:NCBI_API_KEY`
571
+ 就能读出来,而且明文存在用户环境里;内联 `api_key` 还会进入对话记录与工具卡片。
572
+ > 凭据库不是对 agent 的硬隔离(agent 与你是同一个 OS 用户,刻意去读文件仍可能),但它消除了
573
+ > 「所有 shell 子进程自动继承」这条泄漏路径——这正是环境变量的主要问题。
564
574
  - 阅读库/协议库/实验日志是工作区里的 `papers.json` / `protocols.json` / `experiments.json`,
565
575
  写入遵循会话的**沙箱政策**,与其它文件工具同权;面板(Papers tab)直接读 `papers.json`。
566
576
  - `molbio_paper_export_bibtex` 可选按 tag 过滤导出。
@@ -655,10 +665,12 @@ node <包目录>\preset\install.mjs --profile web --check # 只报告,不组
655
665
  `amplicon_min/max` 或 `tm_min/max` 放宽一点即可。
656
666
 
657
667
  **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
- 长期用请设环境变量。
668
+ 推荐用**插件详情页**:插件 → 已安装插件 → `dsh-molbio-tools` → **PubMed API key**,粘贴后点 Save
669
+ (写入 DSH 凭据库的 `NCBI_API_KEY`,不进环境变量,下一次检索生效)。也可以手动在
670
+ `~/.dsh/.credentials.yaml` 的 `refs:` 下加一行 `NCBI_API_KEY: 你的key`。
671
+ 也可以用环境变量 `NCBI_API_KEY`,或在单次调用里传 `api_key`,但两者都更容易泄漏:**环境变量会被
672
+ agent 的 shell 子进程继承**,内联参数会出现在对话记录与工具卡片里。有 key 时走官方 E-utilities
673
+ (PubMed 自己的题录,含 DOI),没有 key 时仍走 harness 的 web 检索。
662
674
 
663
675
  **"帮我把这段序列分析一下" 它却问我要参数?**
664
676
  把目标说具体即可:要引物还是探针、产物多长、Tm 多少、哪个区域。工具默认值都是常规实验条件
@@ -32,8 +32,12 @@
32
32
  import { createElement as h, useEffect, useMemo, useRef, useState } from 'react';
33
33
  import {
34
34
  LIBRARY_FILE,
35
+ NCBI_KEY_REF,
35
36
  childPath,
36
37
  classifyEntry,
38
+ credentialInfo,
39
+ isMolbioDetailSubject,
40
+ remoteFailure,
37
41
  decodeWorkspaceBytes,
38
42
  filterPapers,
39
43
  isOpenable,
@@ -489,6 +493,142 @@ function PapersPanel({ sessionId, remote, useSessions }) {
489
493
  h('div', { style: styles.main }, ...detail));
490
494
  }
491
495
 
496
+ /** The state of the PubMed key, read from the host's credential store. */
497
+ async function readNcbiKey(remote) {
498
+ if (remote?.credentials === undefined || typeof remote.credentials.describe !== 'function') {
499
+ return { phase: 'error', configured: false, writable: false, message: 'This deployment exposes no credentials Remote (the settings controller is not mounted).' };
500
+ }
501
+ let response;
502
+ try {
503
+ response = await remote.credentials.describe([NCBI_KEY_REF]);
504
+ } catch (error) {
505
+ response = { ok: false, error: { message: error instanceof Error ? error.message : String(error) } };
506
+ }
507
+ const failure = remoteFailure(response);
508
+ if (failure !== '') return { phase: 'error', configured: false, writable: false, message: failure };
509
+ return { phase: 'ready', ...credentialInfo(response, NCBI_KEY_REF), message: '' };
510
+ }
511
+
512
+ const keyStyles = {
513
+ section: { borderTop: '1px solid rgba(127, 127, 127, 0.28)', marginTop: 16, paddingTop: 12, display: 'grid', gap: 8 },
514
+ title: { margin: 0, fontSize: 14 },
515
+ note: { margin: 0, fontSize: 12, opacity: 0.75, lineHeight: 1.5 },
516
+ status: { margin: 0, fontSize: 12 },
517
+ row: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' },
518
+ input: { flex: '1 1 240px', minWidth: 200, padding: '4px 8px' },
519
+ message: { margin: 0, fontSize: 12 },
520
+ };
521
+
522
+ /**
523
+ * The Plugins → installed plugin → detail section that stores the NCBI key.
524
+ *
525
+ * It renders only for this package's own subject (bundle page, or one of the
526
+ * row pages the bundle declares) and returns null everywhere else, which is the
527
+ * contract `plugins.detail.section` asks a contribution to honour. The value is
528
+ * write-only from here: `credentials.describe` answers whether a key is
529
+ * configured and where it came from, never the key itself.
530
+ */
531
+ function NcbiKeySection({ subject, remote }) {
532
+ const [draft, setDraft] = useState('');
533
+ const [state, setState] = useState({ phase: 'loading', configured: false, writable: true, message: '' });
534
+ const subjectToken = isMolbioDetailSubject(subject)
535
+ ? `${subject.kind}:${subject.pkg.name}:${subject.row?.rowId ?? ''}`
536
+ : '';
537
+ useEffect(() => {
538
+ if (subjectToken === '') return undefined;
539
+ let live = true;
540
+ const load = () => readNcbiKey(remote).then((next) => { if (live) setState(next); });
541
+ load();
542
+ // Another surface may rotate or clear the same reference (Settings, the
543
+ // file watcher); follow the host event instead of showing a stale badge.
544
+ const off = typeof remote?.$on === 'function'
545
+ ? remote.$on('credentials/reference-updated', (ref) => { if (ref === NCBI_KEY_REF) load(); })
546
+ : undefined;
547
+ return () => {
548
+ live = false;
549
+ if (typeof off === 'function') off();
550
+ };
551
+ }, [remote, subjectToken]);
552
+ if (!isMolbioDetailSubject(subject)) return null;
553
+ const busy = state.phase === 'saving';
554
+ const save = async () => {
555
+ const value = draft.trim();
556
+ if (value === '') return;
557
+ if (remote?.credentials === undefined || typeof remote.credentials.set !== 'function') {
558
+ setState((previous) => ({ ...previous, phase: 'error', message: 'This deployment exposes no credentials Remote (the settings controller is not mounted).' }));
559
+ return;
560
+ }
561
+ setState((previous) => ({ ...previous, phase: 'saving', message: '' }));
562
+ let response;
563
+ try {
564
+ response = await remote.credentials.set(NCBI_KEY_REF, value);
565
+ } catch (error) {
566
+ response = { ok: false, error: { message: error instanceof Error ? error.message : String(error) } };
567
+ }
568
+ const failure = remoteFailure(response);
569
+ setDraft('');
570
+ const next = await readNcbiKey(remote);
571
+ setState(failure !== ''
572
+ ? { ...next, phase: 'error', message: failure }
573
+ : next.phase === 'error'
574
+ ? next
575
+ : next.configured === true
576
+ ? { ...next, message: 'Saved. The next PubMed call uses this key.' }
577
+ : { ...next, phase: 'error', message: 'The host accepted the write but still reports no key configured. Restart the profile and try again.' });
578
+ };
579
+ const remove = async () => {
580
+ if (remote?.credentials === undefined || typeof remote.credentials.unset !== 'function') {
581
+ setState((previous) => ({ ...previous, phase: 'error', message: 'This deployment exposes no credentials Remote (the settings controller is not mounted).' }));
582
+ return;
583
+ }
584
+ setState((previous) => ({ ...previous, phase: 'saving', message: '' }));
585
+ let response;
586
+ try {
587
+ response = await remote.credentials.unset(NCBI_KEY_REF);
588
+ } catch (error) {
589
+ response = { ok: false, error: { message: error instanceof Error ? error.message : String(error) } };
590
+ }
591
+ const failure = remoteFailure(response);
592
+ const next = await readNcbiKey(remote);
593
+ setState(failure !== ''
594
+ ? { ...next, phase: 'error', message: failure }
595
+ : next.phase === 'error'
596
+ ? next
597
+ : next.configured === false
598
+ ? { ...next, message: 'Removed. PubMed search falls back to web search.' }
599
+ : { ...next, phase: 'error', message: 'The host still reports a key configured after Remove.' });
600
+ };
601
+ const status = state.phase === 'loading'
602
+ ? 'Checking the credential store…'
603
+ : state.phase === 'error'
604
+ ? 'Unavailable'
605
+ : state.configured
606
+ ? `Configured${state.source === undefined ? '' : ` (source: ${state.source})`}`
607
+ : 'Not configured';
608
+ return h('section', { 'data-molbio-ncbi-key': '', style: keyStyles.section },
609
+ h('h4', { style: keyStyles.title }, 'PubMed API key'),
610
+ h('p', { style: keyStyles.note },
611
+ 'Stored in the harness credential store. molbio_pubmed_search and molbio_pubmed_abstract use it for the official PubMed E-utilities API; without it they fall back to web search.'),
612
+ h('p', { 'data-molbio-key-status': state.phase, style: keyStyles.status }, status),
613
+ h('div', { style: keyStyles.row },
614
+ h('input', {
615
+ type: 'password',
616
+ value: draft,
617
+ placeholder: 'NCBI API key (36 characters)',
618
+ autoComplete: 'off',
619
+ spellCheck: false,
620
+ 'aria-label': 'NCBI API key',
621
+ style: keyStyles.input,
622
+ onChange: (event) => setDraft(event.target.value),
623
+ }),
624
+ h('button', { type: 'button', disabled: busy || state.writable === false || draft.trim() === '', onClick: save },
625
+ busy ? 'Saving…' : 'Save'),
626
+ h('button', { type: 'button', disabled: busy || state.writable === false || state.configured !== true, onClick: remove }, 'Remove')),
627
+ state.message === '' ? null : h('p', { 'data-molbio-key-message': '', role: 'status', style: keyStyles.message }, state.message),
628
+ h('p', { style: keyStyles.note },
629
+ 'Create a key under NCBI → Account Settings → API Key Management. Prefer this over an environment variable: the store is not inherited by shell commands.'));
630
+ }
631
+
492
632
  /** Required browser services. */
493
633
  const inject = ['slots', 'sidebarRightTabs', 'remote'];
494
634
 
@@ -572,6 +712,25 @@ function apply(ctx) {
572
712
  key: toolName,
573
713
  }, MolbioMapCard)), `molbio panel: ${toolName} card`);
574
714
  }
715
+ // Plugins -> the installed plugin's detail page: the one place a user can
716
+ // store the NCBI key without editing a YAML file or exporting a variable.
717
+ //
718
+ // `remote.credentials` is a FIBER INJECTION, not merely a property on the
719
+ // Remote carrier: the shipped settings pages declare it in `inject` for the
720
+ // same reason. Without the declaration `ctx.remote.credentials` is undefined,
721
+ // which is exactly how the first build of this section made Save a no-op.
722
+ // The gate keeps the panel working in deployments that expose no credentials
723
+ // namespace: the section simply never registers there.
724
+ ctx.effect(() => {
725
+ const gate = ctx.inject(['remote.credentials'], (keyCtx) => {
726
+ keyCtx.effect(() => keyCtx.slots.inject('plugins.detail.section', () => keyCtx.slots.register({
727
+ name: 'plugins.detail.section',
728
+ id: 'molbio-ncbi-key',
729
+ order: 100,
730
+ }, (props) => h(NcbiKeySection, { ...props, remote: keyCtx.remote }))), 'molbio panel: NCBI key section');
731
+ });
732
+ return () => { gate.dispose(); };
733
+ }, 'molbio panel: NCBI key section gate');
575
734
  }
576
735
 
577
736
  export { apply, inject };
@@ -390,3 +390,41 @@ export function paperLink(paper) {
390
390
  if (typeof paper.pmid === 'string' && paper.pmid !== '') return `https://pubmed.ncbi.nlm.nih.gov/${paper.pmid}/`;
391
391
  return undefined;
392
392
  }
393
+
394
+ /** The credential reference the PubMed tools read first. */
395
+ export const NCBI_KEY_REF = 'NCBI_API_KEY';
396
+
397
+ /**
398
+ * Whether a Plugins-page detail subject is this package's own entry.
399
+ *
400
+ * Bundles and rows both expose `pkg.name`; the key section is offered on the
401
+ * bundle page and on every row page the bundle declares, and on nothing else.
402
+ */
403
+ export function isMolbioDetailSubject(subject) {
404
+ if (subject === null || typeof subject !== 'object') return false;
405
+ if (subject.kind !== 'bundle' && subject.kind !== 'row') return false;
406
+ const pkg = subject.pkg;
407
+ return pkg !== null && typeof pkg === 'object' && pkg.name === 'dsh-molbio-tools';
408
+ }
409
+
410
+ /**
411
+ * The host failure message carried by a Remote result, or '' on success.
412
+ * Remote calls answer `{ ok: true, value }` or `{ ok: false, error }`.
413
+ */
414
+ export function remoteFailure(response) {
415
+ if (response === null || typeof response !== 'object' || response.ok !== false) return '';
416
+ const message = response.error?.message;
417
+ return typeof message === 'string' && message !== '' ? message : 'the host refused the request';
418
+ }
419
+
420
+ /** One reference's view from a `credentials.describe` response (never the value). */
421
+ export function credentialInfo(response, ref) {
422
+ const view = response !== null && typeof response === 'object' && response.ok !== false
423
+ ? response.value?.[ref]
424
+ : undefined;
425
+ return {
426
+ configured: view?.configured === true,
427
+ writable: view?.writable !== false,
428
+ ...typeof view?.source === 'string' && view.source !== '' ? { source: view.source } : {},
429
+ };
430
+ }
@@ -13,6 +13,7 @@
13
13
  | **Molbio**(右栏 tab) | 列出会话工作区里的序列文件,选中即画:`.dna`/`.gb`/`.gbk` → 质粒图谱 + 特征表;`.fa`/`.fasta`(≥2 条)→ 序列标识图 | `workspaceFiles.readAll`(二进制,含 SnapGene) |
14
14
  | **Papers**(右栏 tab) | 把 `molbio_paper_*` 工具维护的 `papers.json` 渲染成可搜索的阅读列表 + 详情面板(标题/作者/期刊/年份/PMID/URL/标签/笔记,标题链接到 PubMed 或原 URL) | `workspaceFiles.read`(文本) |
15
15
  | **图谱调用卡**(`tool.call.toolview`) | `molbio_plasmid_map` / `molbio_plasmid_map_file` 的调用卡:在对话里直接画出这次调用产出的图谱(摘要行 + 写入路径),失败时显示错误文本与 Inspect 入口 | 工具自己声明的 `output.presentationMeta`(见下) |
16
+ | **PubMed API key**(`plugins.detail.section`) | 插件 → 已安装插件 → `dsh-molbio-tools` 详情页里的一段表单:password 输入 + Save/Remove,状态只报「已配置 / 来源 / 未配置」,永不回显值 | `ctx.remote.credentials.describe` / `set` / `unset`(host 的凭据 seam) |
16
17
 
17
18
  **解析与渲染全部在浏览器里跑本仓库自己的模块**(`lib/genbank/snapgene/plasmid/msa/logo`),
18
19
  不经过任何工具调用、不落盘 SVG、不弹系统查看器。
@@ -46,7 +47,7 @@ SVG 随 meta 传输有上限(256 KB):真实载体的图谱 20-60 KB,但
46
47
  | `build/client-bundle-core.mjs` | 打包器的**生成逻辑**(可 import):`createGenerator({entry, baseDir})` → 模块图 + `renderBundle(packageName)`;`test/contract.mjs` 在进程内用它重算产物做新鲜度检查(不 spawn,受限沙箱下也能验证) |
47
48
  | `build/browser-api.mjs` | 浏览器安全面:从**包根的 `.mjs` 源文件**再导出面板可用的一切(单一事实来源) |
48
49
  | `build/panel-core.mjs` | 面板的数据通路(分类/解码/解析/渲染/文献库投影),不含 React,可在 Node 里单测 |
49
- | `build/client-entry.mjs` | 浏览器半本体:两个 tab 类型的注册、正文、标题 chip,以及两个 map 工具的调用卡 |
50
+ | `build/client-entry.mjs` | 浏览器半本体:两个 tab 类型的注册、正文、标题 chip,两个 map 工具的调用卡,以及插件详情页的 key 配置段 |
50
51
  | `lib/client.js` | **构建产物**(`exports["./client"]` 指向它;由 `npm run build:client` 生成) |
51
52
  | `packages/molbio-panel/` | 面板专用包(宿主半边空实现),面板与 57 个工具解耦的交付通道 |
52
53
  | `test/client.mjs` | 按加载器的方式执行产物 + 驱动两条数据通路(含真实 pUC118 夹具) |
@@ -131,6 +132,14 @@ seed)、禁止动态 `import()`、禁止跨插件值导入。它自己只降
131
132
  - **图片式交互**:SVG 里注入的是真实 DOM(可缩放、可选中文本),但没有点特征跳转之类的联动。
132
133
  - **CSS 内联**:产物刻意不注入样式表——`factory` 里的样式副作用要自己管拆除;
133
134
  内联样式让产物保持单文件、零生命周期负担。
135
+ - **key 配置段只在「本包」的详情页出现**:`plugins.detail.section` 是 list 座位,条目自己判断
136
+ `subject`;本包只在 `kind: bundle|row` 且 `pkg.name === 'dsh-molbio-tools'` 时渲染,其它插件页
137
+ 返回 `null`。它依赖宿主挂载了凭据 provider(默认组合都有):没有时整段通过
138
+ `ctx.inject(['remote.credentials'], …)` 门控而不注册,工具仍会按「参数 → 凭据库 → 环境变量」
139
+ 的顺序回退,不会因此不可用。**`remote.credentials` 是 fiber injection,不是普通属性**:
140
+ 官方设置页同样在 `inject` 里声明它;第一版漏了这一步,`ctx.remote.credentials` 是 `undefined`,
141
+ 点 Save 什么也没写(0.22.1 测试中发现并修掉)。**输入框只写不读**:`describe` 的返回里没有值,
142
+ 保存成功后输入框立即清空;写后仍报未配置、或 Remove 后仍报已配置,都会把不一致显示出来。
134
143
  - **每次改源都要重新构建**:`lib/client.js` 是**产物**,改了根 `.mjs` 或 `build/*.mjs` 后
135
144
  必须重跑 `npm run build:client`,否则面板还跑旧逻辑(这与 preset 的"版本目录"规则是
136
145
  两码事:客户端产物靠 `rev` 哈希缓存失效,不靠目录名)。
@@ -146,9 +155,11 @@ seed)、禁止动态 `import()`、禁止跨插件值导入。它自己只降
146
155
  `guide`、无 `patterns`、guide `order` 40/41)、四个 keyed 座位的注册、两个正文的 `inject`
147
156
  工厂都只交出 `remote`。座位那一层从**一个座位都没声明**的最坏顺序启动(`test/slots-stub.mjs`
148
157
  复刻 shell 的 SlotCore 语义):`apply()` 必须不抛;右栏座位声明后四条注册落地;调用卡座位在
149
- ui-tool 声明之前一直**处于等待**、声明之后才落地;重声明不产生重复注册;并当场复刻
150
- "未声明座位 `register()` 必抛"的那条 SlotCore 异常——0.7.1 把整个 GUI 卡在
151
- "Failed to load plugins" 的就是它。
158
+ ui-tool 声明之前一直**处于等待**、声明之后才落地;插件详情页座位同样先等待、后注册;重声明
159
+ 不产生重复注册;并当场复刻 "未声明座位 `register()` 必抛"的那条 SlotCore 异常——0.7.1 把整个
160
+ GUI 卡在 "Failed to load plugins" 的就是它。key 段还断言了:本包 subject 渲染出
161
+ `section` + password 输入 + Save,其它包的 subject 返回 `null`,以及凭据视图/失败信息
162
+ 三个纯函数(`isMolbioDetailSubject` / `credentialInfo` / `remoteFailure`)。
152
163
  3. **组件行为**:`test/panel-render.mjs` 把**真实的面板组件**放进 Node 跑——没有 React
153
164
  (harness 里有,浏览器里的 React 由 shell 播种)也没有 DOM,所以这个文件自带一套最小
154
165
  钩子宿主(`createElement`/`useState`/`useEffect`/`useMemo`/`useRef`,语义与 React 一致:
@@ -159,7 +170,9 @@ seed)、禁止动态 `import()`、禁止跨插件值导入。它自己只降
159
170
  列目录失败的错误态、文献库的列表/搜索/标签过滤/详情/PubMed 链接回退、空库提示、坏库
160
171
  报错(**不显示成"空库"**)、以及卸载时中止在途读取。
161
172
  这一层抓到过三个真 bug:列表把不可打开的文件也列出来、`sortEntries` 的 `other` 档从不
162
- 生效、以及坏库时列表仍宣称"还没有文献"。
173
+ 生效、以及坏库时列表仍宣称"还没有文献"。插件详情页的 key 段也被真实驱动:
174
+ `describe` → 输入 → Save(trim 后写入 `NCBI_API_KEY`)→ 状态刷新 + 清空输入 → Remove →
175
+ provider 拒绝时消息原样显示。
163
176
  4. **数据通路(质粒)**:真实 pUC118 `.dna` → 记录(名称/长度/拓扑/特征,AmpR 2102-2962
164
177
  反链)→ SVG;GenBank 文本路径与 Node 工具解析结果**逐字段一致**;FASTA → 比对 → logo;
165
178
  扩展名分类、排序、base64 解码、非法输入的错误路径。
@@ -234,6 +247,9 @@ shadow(无冲突),面板的 `remote`/`slot` 服务不受影响。
234
247
 
235
248
  ## 7. 下一步
236
249
 
250
+ - **插件详情页的 key 配置**:✅ 已落地(0.22.1,见第 1 与第 4 节)。后续可把同一个
251
+ `plugins.detail.section` 模式用于其它凭据(例如文献库的机构代理),前提是它们也有一个
252
+ 稳定的环境变量名。
237
253
  - **`tool.call.toolview`**:✅ 已落地(见第 1 与第 3 节),覆盖两个 map 工具。可继续做的
238
254
  是给 `molbio_sequence_logo` / `molbio_grna_design` 之类的工具也加上卡片(同一套
239
255
  `presentationMeta` + 卡片模式)。**注意上线前先跑 `test/client.mjs` 的抢座位顺序那一层**:
package/docs/history.md CHANGED
@@ -16,6 +16,8 @@
16
16
 
17
17
  | 包版本 | preset 版本 | 内容 | 工具数 |
18
18
  | --- | --- | --- | --- |
19
+ | 0.22.2 | — | **DSH 0.2.1-rc.5 适配**:基线 rc.3 → rc.5(上游 standard 逐字节未变);contract 声明守卫兼容桌面版裁掉 `lib/types` 的打包形态(+突变自检);engines.node 与 CI 降到 24 | 60 |
20
+ | 0.22.1 | — | **PubMed key 改走 DSH 凭据库**:解析顺序 `api_key` → 凭据库 → 环境变量;无凭据库时行为同 0.22.0 | 60 |
19
21
  | 0.22.0 | — | **DSH 0.2.1-rc.3 适配 + PubMed 官方 API**:`molbio_pubmed_search`/`_abstract` 支持 NCBI API key(E-utilities),无 key 行为不变 | 60 |
20
22
  | 0.21.1 | — | **DSH STORE 提交前置**:单文件 bundle patch、逐版本兼容声明;根 tarball 只带一个插件清单(工具行为零变化) | 60 |
21
23
  | 0.21.0 | — | **DSH 0.2.1-rc.2 适配**:preset 行清单逐行重抄;contract 两条断言重锚到行为;两条 suspended 记录用真实模型重录 | 60 |
@@ -45,6 +47,25 @@
45
47
 
46
48
  ---
47
49
 
50
+ ## v0.22.1(2026-10-25,包 0.22.1)
51
+
52
+ **环境变量不是唯一选择。** 0.22.0 只从 `api_key` 参数与 `process.env` 读 key;这一版把 DSH 的
53
+ `ctx.credentials` 插到中间,并把环境变量降为后备。
54
+
55
+ - 新顺序:`api_key` 参数 → 凭据库(`NCBI_API_KEY` / `MOLBIO_NCBI_API_KEY`)→ 进程环境。
56
+ - 为什么:环境变量会被**每个子进程**继承——包括 agent 自己的 shell 工具。凭据库不进入环境、
57
+ 仅属主可读、热重载,`describe()` 不回显值。但 DSH 凭据库的文档也写明:agent 与你是同一个 OS
58
+ 用户,它不是硬隔离,只是消除了「自动继承」这条路径。
59
+ - 没有凭据库的组合(以及 provider 抛错的组合)静默回退到环境,行为与 0.22.0 一致;显式参数仍然
60
+ 最高优先。
61
+ - smoke 新增三条:凭据库压过环境、参数压过凭据库、provider 抛错回退。
62
+ - 0.22.1 同时补了**图形入口**:插件 → 已安装插件 → 本包详情页里的 `plugins.detail.section`
63
+ 表单(password 输入 + Save/Remove,值只写不读)。改 YAML 不该是唯一入口。
64
+ - **测试里抓到的缺陷**:`ctx.remote.credentials` 是 fiber injection,不是 Remote 载体的普通属性。
65
+ 第一版没在 `inject` 里声明它,命名空间就是 `undefined`,Save 静默地什么也没写;官方设置页
66
+ 一直把 `remote.credentials` 写在 `inject` 列表里,那就是这行声明的用途。现在用
67
+ `ctx.inject(['remote.credentials'], …)` 门控整段注册,并在写后重新 `describe` 核对结果。
68
+
48
69
  ## v0.22.0(2026-10-25,包 0.22.0)
49
70
 
50
71
  一半是适配(rc.3),一半是新能力(PubMed 官方 API + key)。三条教训。
@@ -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.3**;`package.json` 版本 0.22.0。
35
+ - **基线**:DSH **0.2.1-rc.5**;`package.json` 版本 0.22.2(DSH 桌面版内嵌 Node 24.21.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.22.0)
13
+ ## 1. 当前状态(0.22.2)
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.3**。
18
+ - 基线:DSH **0.2.1-rc.5**;DSH 桌面版内嵌 Node 24.21.0,`engines.node` 与 CI 矩阵都按这个下限。
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 已修,套件不再依赖宿主分隔符。
@@ -41,6 +41,16 @@ L = 需要新机制或外部资源)。
41
41
  `standard` 行清单**逐字节未变**,`preset-health` / `drift-probe` / `contract` 在 rc.3 上
42
42
  全绿,因此这一半只改声明与证据);`molbio_pubmed_search` / `molbio_pubmed_abstract` 增加
43
43
  NCBI API key 通路,走官方 E-utilities(esearch + esummary),无 key 时保持原有 web 检索。
44
+ - **0.22.1 是 key 存放方式的安全加固 + 图形入口**:`molbio_pubmed_search` / `molbio_pubmed_abstract`
45
+ 的 key 解析顺序变为 **`api_key` 参数 → DSH 凭据库 → 环境变量**(环境变量会被 agent 自己的 shell
46
+ 子进程继承,凭据库不会);同时插件详情页出现 **PubMed API key** 表单,用户不必改 YAML。
47
+ 没有凭据库的组合行为与 0.22.0 相同。
48
+ - **0.22.2 是 rc.5 纯适配版(工具行为零变化)**:基线 rc.3 → rc.5。上游 `standard` 行清单
49
+ **逐字节未变**;真正的变化在**打包形态**——桌面版 `app.asar` 裁掉 `lib/types`,于是
50
+ `contract` 的声明守卫改为"npm 包读 `.d.ts`,桌面版读 `typert.host.js` / runtime 内嵌副本",
51
+ 并加了"突变必须失败"的自检(17 条)。两份 `dshReleases` 增加 rc.5 `compatible`、rc.4
52
+ `unknown`;`engines.node` 与 CI 降到 DSH 桌面实际内嵌的 **24**。面板包 `0.1.6 → 0.1.7`
53
+ (只改声明,产物未变)。
44
54
 
45
55
  ---
46
56