dsh-zotero 0.2.1 → 0.3.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.
Files changed (58) hide show
  1. package/README.en.md +27 -7
  2. package/README.md +55 -38
  3. package/lib/client.js +1754 -44
  4. package/lib/client.js.map +4 -4
  5. package/lib/config.d.ts +3 -0
  6. package/lib/config.d.ts.map +1 -1
  7. package/lib/config.js +1 -0
  8. package/lib/config.js.map +1 -1
  9. package/lib/constants.d.ts +9 -0
  10. package/lib/constants.d.ts.map +1 -1
  11. package/lib/constants.js +7 -0
  12. package/lib/constants.js.map +1 -1
  13. package/lib/contract.d.ts +18 -0
  14. package/lib/contract.d.ts.map +1 -1
  15. package/lib/contract.js +28 -3
  16. package/lib/contract.js.map +1 -1
  17. package/lib/normalize.d.ts +0 -1
  18. package/lib/normalize.d.ts.map +1 -1
  19. package/lib/normalize.js +10 -6
  20. package/lib/normalize.js.map +1 -1
  21. package/lib/presentation-meta.d.ts +256 -0
  22. package/lib/presentation-meta.d.ts.map +1 -0
  23. package/lib/presentation-meta.js +219 -0
  24. package/lib/presentation-meta.js.map +1 -0
  25. package/lib/prompt.d.ts +20 -3
  26. package/lib/prompt.d.ts.map +1 -1
  27. package/lib/prompt.js +33 -15
  28. package/lib/prompt.js.map +1 -1
  29. package/lib/provider-local.d.ts.map +1 -1
  30. package/lib/provider-local.js +2 -2
  31. package/lib/provider-local.js.map +1 -1
  32. package/lib/remote.d.ts +8 -1
  33. package/lib/remote.d.ts.map +1 -1
  34. package/lib/remote.js +24 -0
  35. package/lib/remote.js.map +1 -1
  36. package/lib/service.js +1 -1
  37. package/lib/service.js.map +1 -1
  38. package/lib/settings-namespace.d.ts +5 -5
  39. package/lib/settings-namespace.js +5 -5
  40. package/lib/tools/attachment.d.ts.map +1 -1
  41. package/lib/tools/attachment.js +2 -0
  42. package/lib/tools/attachment.js.map +1 -1
  43. package/lib/tools/export.d.ts.map +1 -1
  44. package/lib/tools/export.js +5 -1
  45. package/lib/tools/export.js.map +1 -1
  46. package/lib/tools/get.d.ts.map +1 -1
  47. package/lib/tools/get.js +2 -0
  48. package/lib/tools/get.js.map +1 -1
  49. package/lib/tools/retrieve.d.ts.map +1 -1
  50. package/lib/tools/retrieve.js +2 -0
  51. package/lib/tools/retrieve.js.map +1 -1
  52. package/lib/tools/search.d.ts.map +1 -1
  53. package/lib/tools/search.js +16 -21
  54. package/lib/tools/search.js.map +1 -1
  55. package/lib/typert.d.ts.map +1 -1
  56. package/lib/typert.js +5 -0
  57. package/lib/typert.js.map +1 -1
  58. package/package.json +10 -4
package/README.en.md CHANGED
@@ -119,16 +119,25 @@ All values are `Config` fields changeable from the bundle's `config` block (e.g.
119
119
  | `maxExportRefs` | `1000` | Upper bound for refs in one `zotero_export` call; keeps the request line under the server's HTTP header limit. |
120
120
  | `defaultStyle` | `apa` | CSL style for citation/bibliography formats. |
121
121
  | `defaultLocale` | `en-US` | CSL locale for citation/bibliography formats. |
122
+ | `webEnabled` | `true` | Enables the dedicated Zotero conversation tab; the gate is read once per page load. |
122
123
 
123
124
  ### Web configuration
124
125
 
125
- The plugin registers a "Zotero" card in dsh web's **Settings → Plugins → Plugin configuration** page listing all 19 fields above. The card binds the `zotero` settings namespace: writes land in the `zotero:` section of `$DSH_HOME/settings.yaml` (layered over the patch entry's `config`, user layer wins), and **saves apply live** — the transport and the provider rebuild on the new values, so the next tool call or `/zotero status` uses them without a dsh restart.
126
+ The plugin registers a "Zotero" card in dsh web's **Settings → Plugins → Plugin configuration** page listing all 20 fields above. The card binds the `zotero` settings namespace: writes land in the `zotero:` section of `$DSH_HOME/settings.yaml` (layered over the patch entry's `config`, user layer wins), and **saves apply live** — the transport and the provider rebuild on the new values, so the next tool call or `/zotero status` uses them without a dsh restart.
126
127
 
127
128
  - Invalid values (a non-loopback `baseUrl`, a non-positive limit) are refused before the write; the card reports the failed save and keeps the draft, and the plugin keeps running on the last valid value.
128
129
  - Every field shows its effective value; fields overridden by the settings document carry an "Overridden" badge and offer a one-click reset (clears the user layer, back to the patch entry value).
129
130
  - External edits to the settings document (e.g. editing `settings.yaml` directly) hot-apply too.
130
131
  - Compositions without a settings service (pure headless) never register the namespace, and the plugin behaves exactly as if unconfigured.
131
132
 
133
+ ### Web view
134
+
135
+ The dsh web session view is a tab ring (Chat, Trajectory, …). The plugin registers a dedicated **Zotero** tab (`conversation.view`, id `zotero`, after Trajectory and dsh-context) and leaves dsh's built-in chat and trajectory display untouched:
136
+
137
+ - A **connection strip** leads the tab: one status probe on mount, another per explicit Refresh (request-driven, no polling timers); it shows the connection state, API/schema versions, Server ID (Zotero 10+), and the last-checked time, with the diagnosis when Zotero is unavailable.
138
+ - Below it, the session's **Zotero tool activity**: every search, read, retrieve, attachment, and export call renders as a rich card (expandable, copyable refs, evidence passages labeled by source), fully replay-driven from the conversation snapshot — the same transcript renders the same cards, and missing meta degrades to the raw content.
139
+ - The **Web → Session tool cards** toggle in the settings page (`webEnabled`, default on) controls the tab's registration; the gate is read once per page load, so a toggle change applies after the page reloads. When off, Zotero calls show as dsh's built-in generic cards in the trajectory.
140
+
132
141
  ## Limits
133
142
 
134
143
  - Read-only library: no path modifies items, notes, tags, or collections.
@@ -176,7 +185,14 @@ pnpm dsh web --patch ./dsh-zotero/dev.cordis.yml
176
185
 
177
186
  #### With the npm-installed dsh
178
187
 
179
- **Resident instance**: pack a tarball and install it into a profile. The plugin runs from the tarball copy; code updates require re-packing and re-installing. Verify with the production-stack smoke:
188
+ This plugin builds in two halves: the **Node side** (`lib/`, emitted by `tsc`, holds the service, tools, provider, and other logic) and the **browser side** (`lib/client.js`, emitted by `esbuild`, holds the dsh web configuration card and the Zotero tab view). The three flows below cover the common cases.
189
+
190
+ - `npm run build` emits both halves; `npm run build:client` rebuilds only the browser side.
191
+ - The rest of this section assumes `npm run build` has been run at least once so `lib/` exists.
192
+
193
+ **① Resident instance verification (tarball install)**
194
+
195
+ Pack a tarball and install it into a profile. The plugin runs from the tarball's built artifacts; code updates require re-packing and re-installing. Verify with the production-stack smoke after install:
180
196
 
181
197
  ```sh
182
198
  npm pack
@@ -185,19 +201,23 @@ cd ~/.dsh/profiles/<name>
185
201
  node --input-type=module < /path/to/dsh-zotero/scripts/smoke.mjs
186
202
  ```
187
203
 
188
- Run the smoke inside the profile directory, so bare imports resolve from the profile's flat `node_modules`. It verifies `status`, `search`, `get`, `retrieve`, `export`, the policy prompt section, and tool registration; `SMOKE PASS` indicates the packed plugin passes the installed-path checks.
204
+ The smoke must be run inside the profile directory, so bare imports resolve from the profile's flat `node_modules`. It verifies `status`, `search`, `get`, `retrieve`, `export`, the policy prompt section, and the registration of all five tools; `SMOKE PASS` indicates the packed plugin passes the installed-path checks.
189
205
 
190
- **Dev instance (hot swap)**: the `dev-lib.cordis.yml` overlay disables the profile's tarball copy (id `zotero`), inserts `zotero-dev` at this checkout's `lib/index.js`, and re-enables HMR. The production web profile disables loader HMR, and the HMR watch base sits in the profile directory, so the overlay sets `base` explicitly. When the build output changes, HMR disposes the old instance and re-constructs the plugin in the same process; dsh keeps running:
206
+ **② Node-side hot-swap development**
207
+
208
+ The `dev-lib.cordis.yml` overlay disables the profile's tarball row (id `zotero`), inserts a `zotero-dev` row pointing at this checkout's `lib/index.js`, and re-enables HMR. The production web profile disables loader HMR by default, and HMR's watch root lives in the profile directory, so the overlay sets `base` explicitly. When the build output changes, HMR disposes the old instance and reconstructs the plugin in the same process — no dsh restart needed:
191
209
 
192
210
  ```sh
193
- cd /Volumes/Work/deepseek-harness/dsh-zotero
211
+ cd ./dsh-zotero # from the deepseek-harness checkout
194
212
  npm run dev & # tsc --watch: rebuild lib on src changes
195
213
  dsh web --patch ./dev-lib.cordis.yml --port 3307
196
214
  ```
197
215
 
198
- Hot swap affects only the instance started with `--patch`; the resident instance keeps running the tarball version.
216
+ Hot swap only affects the instance started with `--patch`; the resident instance keeps running the tarball version, independently.
217
+
218
+ **③ Browser-side development**
199
219
 
200
- **Developing the browser half**: the web frontend only scans loader rows whose `name` is a bare package name (resolvable to `package.json`) — the absolute-path row in `dev-lib.cordis.yml` has no browser half, so the card does not appear in the dev instance. To develop the card, install this checkout into the profile (`npm install <this repo path>` as a file: dependency, or pack and install the tarball), then pair `npm run dev:client` (esbuild watch) with the hot-swap overlay: browser-bundle changes make HMR re-fetch `/plugins/dsh-zotero/client.js`.
220
+ The web frontend only scans loader rows whose `name` is a bare package name (npm-resolvable to `package.json`) to load the browser-side bundle. `dev-lib.cordis.yml` uses an absolute-path row, which does not trigger browser-side loading, so the card does not appear in the ② dev instance. To develop the card, first install this checkout into the profile (`npm install <this repo path>` as a `file:` dependency, or pack and install the tarball), then pair `npm run dev:client` (esbuild watch) with the hot-swap overlay: browser-bundle changes make HMR re-fetch `/plugins/dsh-zotero/client.js`.
201
221
 
202
222
  ## License
203
223
 
package/README.md CHANGED
@@ -98,44 +98,53 @@ allowBuilds:
98
98
 
99
99
  所有值都是 `Config` 字段,可在 bundle 的 `config` 块中修改(例如通过 `dsh plugin config`)。以下为默认值。
100
100
 
101
- | 字段 | 默认值 | 含义 |
102
- | ---------------------- | ---------------------------- | ------------------------------------------------------------------------- |
103
- | `baseUrl` | `http://127.0.0.1:23119/api` | 本地 API 基础 URL。仅支持纯回环 HTTP。 |
104
- | `provider` | `local` | 要选择的 provider id。 |
105
- | `timeoutMs` | `5000` | 每个请求的 provider 超时时间。 |
106
- | `maxSearchResults` | `20` | `zotero_search` `limit` 的上限。 |
107
- | `maxNoteScanRecords` | `200` | `zotero_search` 补扫笔记正文的笔记数量上限。 |
108
- | `maxEvidenceChars` | `6000` | 检索证据的总字符预算。 |
109
- | `maxEvidencePassages` | `4` | 证据片段数量的上限。 |
110
- | `maxDetailChars` | `3000` | `zotero_get` 摘要预览的字符预算。 |
111
- | `maxNoteBodyChars` | `30000` | `zotero_get` 返回 note 条目自身正文的字符预算。 |
112
- | `maxNoteChars` | `2000` | `zotero_get` 单条笔记预览的字符预算。 |
113
- | `maxNoteRecords` | `50` | `zotero_get` 返回笔记数量的上限。 |
114
- | `maxAnnotationRecords` | `100` | `zotero_get` 返回批注数量的上限。 |
115
- | `fulltextChunkWords` | `200` | 进入证据排序的全文片段词数。 |
116
- | `maxFulltextChars` | `250000` | 进入证据排序的全文大小上限。 |
117
- | `maxResponseBytes` | `16777216` | 每个 API 响应的流式字节上限。 |
118
- | `maxExportChars` | `1000000` | 导出输出的硬上限。不会中途截断。 |
119
- | `maxExportRefs` | `1000` | 单次 `zotero_export` 的 refs 数量上限,保护请求行不超服务器 HTTP 头限制。 |
120
- | `defaultStyle` | `apa` | 引用/参考文献使用的 CSL 样式。 |
121
- | `defaultLocale` | `en-US` | 引用/参考文献使用的 CSL locale。 |
101
+ | 字段 | 默认值 | 含义 |
102
+ | ---------------------- | ---------------------------- | -------------------------------------------------------------------------------------- |
103
+ | `baseUrl` | `http://127.0.0.1:23119/api` | 本地 API 基础 URL。仅支持纯回环 HTTP。 |
104
+ | `provider` | `local` | 要选择的 provider id。 |
105
+ | `timeoutMs` | `5000` | 每个请求的 provider 超时时间。 |
106
+ | `maxSearchResults` | `20` | `zotero_search` `limit` 的上限。 |
107
+ | `maxNoteScanRecords` | `200` | `zotero_search` 补扫笔记正文的笔记数量上限。 |
108
+ | `maxEvidenceChars` | `6000` | 检索证据的总字符预算。 |
109
+ | `maxEvidencePassages` | `4` | 证据片段数量的上限。 |
110
+ | `maxDetailChars` | `3000` | `zotero_get` 摘要预览的字符预算。 |
111
+ | `maxNoteBodyChars` | `30000` | `zotero_get` 返回 note 条目自身正文的字符预算。 |
112
+ | `maxNoteChars` | `2000` | `zotero_get` 单条笔记预览的字符预算。 |
113
+ | `maxNoteRecords` | `50` | `zotero_get` 返回笔记数量的上限。 |
114
+ | `maxAnnotationRecords` | `100` | `zotero_get` 返回批注数量的上限。 |
115
+ | `fulltextChunkWords` | `200` | 进入证据排序的全文片段词数。 |
116
+ | `maxFulltextChars` | `250000` | 进入证据排序的全文大小上限。 |
117
+ | `maxResponseBytes` | `16777216` | 每个 API 响应的流式字节上限。 |
118
+ | `maxExportChars` | `1000000` | 导出输出的硬上限。不会中途截断。 |
119
+ | `maxExportRefs` | `1000` | 单次 `zotero_export` 的 refs 数量上限,保护请求行不超服务器 HTTP 头限制。 |
120
+ | `defaultStyle` | `apa` | 引用/参考文献使用的 CSL 样式。 |
121
+ | `defaultLocale` | `en-US` | 引用/参考文献使用的 CSL locale。 |
122
+ | `webEnabled` | `true` | 是否在会话顶部显示 Zotero 专属标签页;开关在每次页面加载时读取,切换后需刷新页面生效。 |
122
123
 
123
124
  ### Web 配置
124
125
 
125
- 插件在 dsh web 的 **设置 → 插件 → 插件配置** 页注册了一张 "Zotero" 卡片,列出上表全部 19 个字段。卡片绑定 `zotero` 设置命名空间:写入的内容落在 `$DSH_HOME/settings.yaml` 的 `zotero:` 段(与补丁 entry 的 `config` 叠层,用户段优先),**保存即热生效**——传输层与 provider 会按新值重建,下一个工具调用或 `/zotero status` 立即使用新配置,无需重启 dsh。
126
+ 插件在 dsh web 的 **Settings → Plugins → Plugin configuration** 页面注册一张 "Zotero" 卡片,列出上表全部字段。卡片绑定 `zotero` 设置命名空间:写入落在 `$DSH_HOME/settings.yaml` 的 `zotero:` 小节(叠加在 patch 条目 `config` 之上,用户层优先),**保存即时生效**——传输层与 provider 按新值重建,下一次工具调用或 `/zotero status` 无需重启 dsh 即可使用。
126
127
 
127
- - 非法值(如非回环的 `baseUrl`、非正整数的限制)在写入前被拒绝,卡片提示保存失败并保留草稿,插件继续运行于上一个合法值。
128
- - 每个字段显示当前生效值;被设置文档覆盖的字段带「已覆盖」标记,可一键恢复默认(清除用户段,回到补丁 entry 值)。
129
- - 设置文档被外部编辑(如直接改 `settings.yaml`)时同样会热生效。
130
- - 无 settings 服务的组合(纯 headless)不注册命名空间,插件行为与未配置时完全一致。
128
+ - 非法值(非回环 `baseUrl`、非正数上限)在写入前被拒绝;卡片提示保存失败并保留草稿,插件继续运行在最后一个合法值上。
129
+ - 每个字段显示有效值;被设置文档覆盖的字段带有 "Overridden" 徽标,提供一键重置(清除用户层,回到 patch 条目值)。
130
+ - 直接编辑设置文档(如手工修改 `settings.yaml`)同样热生效。
131
+ - 没有设置服务的组合(纯 headless)不会注册命名空间,插件行为与未配置时完全一致。
131
132
 
132
- ## 限制
133
+ ### Web 视图
133
134
 
134
- - 对文献库只读:没有任何修改条目、笔记、标签、分类等文献库数据的路径。
135
- - 全文证据依赖 Zotero 的全文索引:`everything` 搜索和 `retrieve` 的全文片段都以索引为前提。
136
- - 笔记正文搜索是插件侧补扫:仅库/分类范围、仅结果首页、受 `maxNoteScanRecords` 上限约束,超出上限的笔记不参与匹配。
137
- - 附件深度分析取决于当前 Harness 配置:`zotero_attachment` 返回文件位置,能否继续读取该 PDF 由 composition 里是否有相应文件/PDF 能力决定。
138
- - 证据排序是词项相关度检索,不是 embedding 或语义搜索。
135
+ dsh web 的会话视图是标签页环(Chat、Trajectory、…)。插件注册一个专属 **Zotero** 标签页(`conversation.view`,id `zotero`,位于 Trajectory 与 dsh-context 之后),不触碰 dsh 自带的聊天与轨迹视图:
136
+
137
+ - 标签页顶部是**连接条**:挂载时探测一次、每次手动刷新再探测一次(请求驱动,无轮询定时器);显示连接状态、API/Schema 版本、Server ID(Zotero 10+)与上次检查时间;Zotero 不可用时显示诊断信息。
138
+ - 下方是本会话的 **Zotero 工具活动**:每次搜索、精读、取证、附件解析与导出调用都渲染为富卡片(可展开、ref 可复制、证据段落标注来源),完全由会话快照重放驱动——同一段记录永远渲染出同样的卡片,meta 缺失时降级为原始内容。
139
+ - 设置页的 **Web → 会话工具卡片** 开关(`webEnabled`,默认开启)控制标签页的注册;开关在每次页面加载时读取一次,切换后需刷新页面生效。关闭后,Zotero 调用在轨迹中显示为 dsh 内置的通用卡片。
140
+
141
+ ### 限制
142
+
143
+ - 只读文献库:没有任何路径会修改条目、笔记、标签或合集。
144
+ - 全文证据依赖 Zotero 的索引:`everything` 搜索与 `retrieve` 的全文段落都需要已建立索引。
145
+ - 笔记正文搜索是客户端扫描:仅限 library/collection 作用域与第一页结果,受 `maxNoteScanRecords` 限制;超出上限的笔记永远不会命中。
146
+ - 附件深度取决于宿主组合:`zotero_attachment` 返回文件位置;继续阅读该 PDF 需要宿主具备对应的文件/PDF 能力。
147
+ - 证据排序是基于词项的相关性,而非向量或语义检索。
139
148
 
140
149
  ## 开发
141
150
 
@@ -176,7 +185,11 @@ pnpm dsh web --patch ./dsh-zotero/dev.cordis.yml
176
185
 
177
186
  #### 使用 npm 安装的 dsh
178
187
 
179
- **常驻实例**:打包为 tarball 并安装到 profile,插件以 tarball 中的副本运行;代码更新需重新打包安装。安装后运行生产栈 smoke 验证:
188
+ 本插件分两部分构建:**Node 端**(`lib/`,由 `tsc` 生成,包含服务、工具、provider 等逻辑)与**浏览器端**(`lib/client.js`,由 `esbuild` 生成,包含 dsh web 的配置卡片与 Zotero 标签视图)。下面三种开发流程覆盖了不同场景。
189
+
190
+ **① 常驻实例验证(tarball 安装)**
191
+
192
+ 打包为 tarball 并安装到 profile,插件以 tarball 内的构建产物运行;代码更新后需重新打包安装。安装后通过生产栈 smoke 脚本验证:
180
193
 
181
194
  ```sh
182
195
  npm pack
@@ -185,19 +198,23 @@ cd ~/.dsh/profiles/<name>
185
198
  node --input-type=module < /path/to/dsh-zotero/scripts/smoke.mjs
186
199
  ```
187
200
 
188
- smoke 需在 profile 目录内运行,裸导入由此从 profile 的扁平 `node_modules` 解析。脚本依次验证 `status`、`search`、`get`、`retrieve`、`export`、策略提示词分区与五个工具注册;输出 `SMOKE PASS` 表示打包后的插件通过安装路径验证。
201
+ smoke 脚本必须在 profile 目录内运行,这样裸导入才能从 profile 的扁平 `node_modules` 中解析。脚本依次验证 `status`、`search`、`get`、`retrieve`、`export`、策略提示词分区,以及五个工具的注册情况;输出 `SMOKE PASS` 表示打包后的插件通过了安装路径验证。
202
+
203
+ **② Node 端热替换开发**
189
204
 
190
- **开发实例(热替换)**:`dev-lib.cordis.yml` 覆盖层禁用 profile 中的 tarball 副本(id `zotero`),插入 `zotero-dev` 指向本仓库的 `lib/index.js`,并重新启用 HMR。生产 web profile 默认禁用 loader HMR,且 HMR 的监视根位于 profile 目录,因此覆盖层显式设置了 `base`。构建输出变化后,HMR 在同一进程内销毁旧实例并重新构造插件,无需重启 dsh:
205
+ `dev-lib.cordis.yml` 覆盖层会禁用 profile 中的 tarball 行(id `zotero`),转而插入 `zotero-dev` 行指向本仓库的 `lib/index.js`,并重新启用 HMR。生产 web profile 默认关闭 loader HMR,且 HMR 的监视根位于 profile 目录,因此覆盖层显式设置了 `base`。构建产物变化后,HMR 会在同一进程内销毁旧实例并重新构造插件,无需重启 dsh:
191
206
 
192
207
  ```sh
193
- cd /Volumes/Work/deepseek-harness/dsh-zotero
208
+ cd ./dsh-zotero # 从 deepseek-harness checkout 进入本仓库
194
209
  npm run dev & # tsc --watch:修改 src 后自动重建 lib
195
210
  dsh web --patch ./dev-lib.cordis.yml --port 3307
196
211
  ```
197
212
 
198
- 热替换仅作用于通过 `--patch` 启动的实例;常驻实例继续运行 tarball 版本。
213
+ 热替换仅对通过 `--patch` 启动的实例生效;常驻实例仍运行 tarball 版本,互不影响。
214
+
215
+ **③ 浏览器端开发**
199
216
 
200
- **浏览器半边的开发**:Web 端只扫描 Loader 行 `name` 为裸包名(npm 可解析到 `package.json`)的条目——`dev-lib.cordis.yml` 的绝对路径行不会加载浏览器半边,因此卡片不会出现在 dev 实例中。开发卡片时把本仓库装进 profile(`npm install <本仓库路径>` 作为 file: 依赖,或 `npm pack` 后安装 tarball),再配合 `npm run dev:client`(esbuild watch)与热替换 overlay:浏览器 bundle 变化会触发 HMR 重新拉取 `/plugins/dsh-zotero/client.js`。
217
+ dsh web 只会扫描 Loader 行中 `name` 为裸包名(npm 能解析到 `package.json`)的条目来加载浏览器端 bundle。`dev-lib.cordis.yml` 使用的是绝对路径行,不会触发浏览器端加载,因此卡片不会出现在 ② 的 dev 实例中。开发卡片时需要先把本仓库装进 profile(`npm install <本仓库路径>` 作为 `file:` 依赖,或 `npm pack` 后安装 tarball),再配合 `npm run dev:client`(esbuild watch)与热替换 overlay 一起使用:浏览器 bundle 变化会触发 HMR 重新拉取 `/plugins/dsh-zotero/client.js`。
201
218
 
202
219
  ## 许可证
203
220