dsh-zotero 0.1.0 → 0.2.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 (96) hide show
  1. package/README.en.md +56 -33
  2. package/README.md +56 -33
  3. package/lib/ask.d.ts +37 -0
  4. package/lib/ask.d.ts.map +1 -0
  5. package/lib/ask.js +130 -0
  6. package/lib/ask.js.map +1 -0
  7. package/lib/attachments.d.ts.map +1 -1
  8. package/lib/attachments.js +2 -10
  9. package/lib/attachments.js.map +1 -1
  10. package/lib/client.js +15350 -153
  11. package/lib/client.js.map +7 -1
  12. package/lib/config.d.ts +10 -1
  13. package/lib/config.d.ts.map +1 -1
  14. package/lib/config.js +8 -1
  15. package/lib/config.js.map +1 -1
  16. package/lib/constants.d.ts +2 -0
  17. package/lib/constants.d.ts.map +1 -1
  18. package/lib/constants.js +2 -0
  19. package/lib/constants.js.map +1 -1
  20. package/lib/contract.d.ts +57 -0
  21. package/lib/contract.d.ts.map +1 -0
  22. package/lib/contract.js +109 -0
  23. package/lib/contract.js.map +1 -0
  24. package/lib/errors.d.ts +7 -6
  25. package/lib/errors.d.ts.map +1 -1
  26. package/lib/errors.js +21 -12
  27. package/lib/errors.js.map +1 -1
  28. package/lib/{client.d.ts → http-client.d.ts} +2 -2
  29. package/lib/http-client.d.ts.map +1 -0
  30. package/lib/http-client.js +230 -0
  31. package/lib/http-client.js.map +1 -0
  32. package/lib/json.d.ts +13 -0
  33. package/lib/json.d.ts.map +1 -0
  34. package/lib/json.js +22 -0
  35. package/lib/json.js.map +1 -0
  36. package/lib/normalize.d.ts +27 -13
  37. package/lib/normalize.d.ts.map +1 -1
  38. package/lib/normalize.js +118 -37
  39. package/lib/normalize.js.map +1 -1
  40. package/lib/prompt.d.ts.map +1 -1
  41. package/lib/prompt.js +6 -3
  42. package/lib/prompt.js.map +1 -1
  43. package/lib/provider-local.d.ts +53 -7
  44. package/lib/provider-local.d.ts.map +1 -1
  45. package/lib/provider-local.js +238 -59
  46. package/lib/provider-local.js.map +1 -1
  47. package/lib/refs.d.ts.map +1 -1
  48. package/lib/refs.js +2 -1
  49. package/lib/refs.js.map +1 -1
  50. package/lib/remote.d.ts +50 -0
  51. package/lib/remote.d.ts.map +1 -0
  52. package/lib/remote.js +104 -0
  53. package/lib/remote.js.map +1 -0
  54. package/lib/service.d.ts +28 -2
  55. package/lib/service.d.ts.map +1 -1
  56. package/lib/service.js +106 -24
  57. package/lib/service.js.map +1 -1
  58. package/lib/settings-namespace.d.ts +14 -0
  59. package/lib/settings-namespace.d.ts.map +1 -0
  60. package/lib/settings-namespace.js +14 -0
  61. package/lib/settings-namespace.js.map +1 -0
  62. package/lib/tools/attachment.d.ts.map +1 -1
  63. package/lib/tools/attachment.js +2 -1
  64. package/lib/tools/attachment.js.map +1 -1
  65. package/lib/tools/export.d.ts +9 -3
  66. package/lib/tools/export.d.ts.map +1 -1
  67. package/lib/tools/export.js +16 -8
  68. package/lib/tools/export.js.map +1 -1
  69. package/lib/tools/get.d.ts +20 -3
  70. package/lib/tools/get.d.ts.map +1 -1
  71. package/lib/tools/get.js +24 -12
  72. package/lib/tools/get.js.map +1 -1
  73. package/lib/tools/present.d.ts +16 -0
  74. package/lib/tools/present.d.ts.map +1 -0
  75. package/lib/tools/present.js +18 -0
  76. package/lib/tools/present.js.map +1 -0
  77. package/lib/tools/retrieve.d.ts +26 -5
  78. package/lib/tools/retrieve.d.ts.map +1 -1
  79. package/lib/tools/retrieve.js +36 -22
  80. package/lib/tools/retrieve.js.map +1 -1
  81. package/lib/tools/search.d.ts +12 -3
  82. package/lib/tools/search.d.ts.map +1 -1
  83. package/lib/tools/search.js +38 -23
  84. package/lib/tools/search.js.map +1 -1
  85. package/lib/tools/validate.d.ts +17 -0
  86. package/lib/tools/validate.d.ts.map +1 -0
  87. package/lib/tools/validate.js +24 -0
  88. package/lib/tools/validate.js.map +1 -0
  89. package/lib/typert.d.ts +16 -0
  90. package/lib/typert.d.ts.map +1 -0
  91. package/lib/typert.js +51 -0
  92. package/lib/typert.js.map +1 -0
  93. package/lib/types.d.ts +17 -2
  94. package/lib/types.d.ts.map +1 -1
  95. package/package.json +42 -7
  96. package/lib/client.d.ts.map +0 -1
package/README.en.md CHANGED
@@ -10,15 +10,13 @@ Describe what you need in a session and the Agent calls the tools below as neede
10
10
 
11
11
  ## Tools
12
12
 
13
- | Tool | Purpose |
14
- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
15
- | `zotero_search` | Discover: search the library by title/creator/year, or indexed full text with `everything`; scope to a collection or saved search. |
16
- | `zotero_get` | Inspect: read one item's structured core metadata, optionally with manifests and previews of its notes, annotations, and attachments. |
17
- | `zotero_retrieve` | Evidence: return the most relevant bounded evidence passages (annotations, notes, abstract, full-text chunks) for a query. |
18
- | `zotero_attachment` | Source: resolve an item or attachment ref to the original attachment's verified on-disk path or linked URL. An item ref yields the best attachment Zotero itself picks; an attachment ref pinpoints one. |
19
- | `zotero_export` | Cite: let Zotero's own citation/export machinery produce citations, a CSL bibliography, or `bibtex` / `biblatex` / `ris` / `csljson`. |
20
-
21
- Every tool returns reusable refs of the form `zotero://user/0/<item|attachment|annotation|collection|search>/<KEY>`, optionally qualified with `?server=<id>`. Later calls chain through these refs. The Zotero 10+ `server` qualifier binds a ref to the database that produced it, so a database switch blocks stale refs instead of misreading them.
13
+ | Tool | Purpose |
14
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
15
+ | `zotero_search` | Discover: search by title/creator/year, or indexed full text with `everything`; optionally scope to a collection or saved search. |
16
+ | `zotero_get` | Inspect: read one item's structured core metadata, optionally with manifests and previews of its notes, annotations, and attachments. |
17
+ | `zotero_retrieve` | Evidence: return the most relevant bounded evidence passages (annotations, notes, abstract, full-text chunks) for a query. |
18
+ | `zotero_attachment` | Source: resolve an item or attachment ref to the original attachment's verified on-disk path or linked URL. |
19
+ | `zotero_export` | Cite: let Zotero's own citation/export machinery produce citations, a CSL bibliography, or `bibtex` / `biblatex` / `ris` / `csljson`. |
22
20
 
23
21
  ## Usage example
24
22
 
@@ -46,10 +44,17 @@ The Agent moves down the ladder as a request deepens. A typical conversation:
46
44
 
47
45
  `/zotero status` reports connectivity, API/schema versions, and the database identity (Server ID, Zotero 10+). This is the only health check. Ordinary calls fail with typed domain errors.
48
46
 
47
+ ## On-demand work and connectivity-failure interaction
48
+
49
+ - The plugin is resident but strictly request-driven: loading, idling, and unloading never issue a request (no probes, no polling, no background work). Only two entry points touch Zotero: the five tools, invoked when the user explicitly asks about their library, and the explicitly invoked `/zotero status` command.
50
+ - When a tool call fails with a connectivity error (`ZOTERO_NOT_RUNNING` not running / `ZOTERO_API_DISABLED` local API disabled / `ZOTERO_API_VERSION` unsupported version / `ZOTERO_TIMEOUT` timed out), the plugin asks the user how to proceed through an interactive question card: the first option is the recommended action marked `(Recommended)` (e.g. "I started Zotero, retry (Recommended)"); choosing it re-runs the same request once, and a second failure or the "Abort this query" choice surfaces the original typed error — never a second question.
51
+ - Without an interactive provider (headless compositions), the ask is skipped and the typed error is returned as-is; a failing question mechanism never masks the original connectivity error.
52
+
49
53
  ## Limits
50
54
 
51
- - Read-only library: V1 has no path that modifies items, notes, tags, or collections.
55
+ - Read-only library: no path modifies items, notes, tags, or collections.
52
56
  - Full-text evidence depends on Zotero's index: `everything` search and `retrieve` full-text passages both require indexing.
57
+ - Note-content search is a client-side scan: library/collection scopes and the first result page only, bounded by `maxNoteScanRecords`; notes beyond the cap never match.
53
58
  - Attachment depth depends on the harness composition: `zotero_attachment` returns the file location; reading that PDF further needs a matching file/PDF capability.
54
59
  - Evidence ranking is term-based relevance, not embedding or semantic search.
55
60
 
@@ -67,7 +72,7 @@ The Agent moves down the ladder as a request deepens. A typical conversation:
67
72
  dsh plugin --profile <name> add dsh-zotero
68
73
  ```
69
74
 
70
- The tarball ships the built `lib/`; no local build is needed.
75
+ The tarball ships the built `lib/` (the node half plus the browser half `lib/client.js`); no local build is needed. The browser half is the configuration card: dsh web scans the package's `dsh.client` manifest and mounts it automatically, with no extra setup.
71
76
 
72
77
  ### From a local tarball
73
78
 
@@ -100,39 +105,55 @@ The plugin mounts as id `zotero` and takes effect on the next dsh start. After i
100
105
 
101
106
  All values are `Config` fields changeable from the bundle's `config` block (e.g. via `dsh plugin config`). Defaults are shown.
102
107
 
103
- | Field | Default | Meaning |
104
- | ---------------------- | ---------------------------- | ------------------------------------------------------------ |
105
- | `baseUrl` | `http://127.0.0.1:23119/api` | Local API base URL. Plain loopback HTTP only. |
106
- | `provider` | `local` | Provider id to select. |
107
- | `timeoutMs` | `5000` | Per-request provider deadline. |
108
- | `maxSearchResults` | `20` | Upper bound for `zotero_search` `limit`. |
109
- | `maxEvidenceChars` | `6000` | Total character budget for retrieved evidence. |
110
- | `maxEvidencePassages` | `4` | Upper bound for evidence passage counts. |
111
- | `maxDetailChars` | `3000` | Character budget for `zotero_get` abstract previews. |
112
- | `maxNoteChars` | `2000` | Character budget per note preview in `zotero_get`. |
113
- | `maxNoteRecords` | `50` | Upper bound for note records returned by `zotero_get`. |
114
- | `maxAnnotationRecords` | `100` | Upper bound for annotation records returned by `zotero_get`. |
115
- | `fulltextChunkWords` | `200` | Word count per full-text passage entering evidence ranking. |
116
- | `maxFulltextChars` | `250000` | Full text accepted into evidence ranking. |
117
- | `maxResponseBytes` | `16777216` | Streaming byte bound for every API response. |
118
- | `maxExportChars` | `1000000` | Export output hard limit. Never mid-truncated. |
119
- | `defaultStyle` | `apa` | CSL style for citation/bibliography formats. |
120
- | `defaultLocale` | `en-US` | CSL locale for citation/bibliography formats. |
108
+ | Field | Default | Meaning |
109
+ | ---------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------- |
110
+ | `baseUrl` | `http://127.0.0.1:23119/api` | Local API base URL. Plain loopback HTTP only. |
111
+ | `provider` | `local` | Provider id to select. |
112
+ | `timeoutMs` | `5000` | Per-request provider deadline. |
113
+ | `maxSearchResults` | `20` | Upper bound for `zotero_search` `limit`. |
114
+ | `maxNoteScanRecords` | `200` | Upper bound for note records scanned for body matches by `zotero_search`. |
115
+ | `maxEvidenceChars` | `6000` | Total character budget for retrieved evidence. |
116
+ | `maxEvidencePassages` | `4` | Upper bound for evidence passage counts. |
117
+ | `maxDetailChars` | `3000` | Character budget for `zotero_get` abstract previews. |
118
+ | `maxNoteBodyChars` | `30000` | Character budget for a note item's own body returned by `zotero_get`. |
119
+ | `maxNoteChars` | `2000` | Character budget per note preview in `zotero_get`. |
120
+ | `maxNoteRecords` | `50` | Upper bound for note records returned by `zotero_get`. |
121
+ | `maxAnnotationRecords` | `100` | Upper bound for annotation records returned by `zotero_get`. |
122
+ | `fulltextChunkWords` | `200` | Word count per full-text passage entering evidence ranking. |
123
+ | `maxFulltextChars` | `250000` | Full text accepted into evidence ranking. |
124
+ | `maxResponseBytes` | `16777216` | Streaming byte bound for every API response. |
125
+ | `maxExportChars` | `1000000` | Export output hard limit. Never mid-truncated. |
126
+ | `maxExportRefs` | `1000` | Upper bound for refs in one `zotero_export` call; keeps the request line under the server's HTTP header limit. |
127
+ | `defaultStyle` | `apa` | CSL style for citation/bibliography formats. |
128
+ | `defaultLocale` | `en-US` | CSL locale for citation/bibliography formats. |
129
+
130
+ ### Web configuration
131
+
132
+ 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.
133
+
134
+ - 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.
135
+ - 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).
136
+ - External edits to the settings document (e.g. editing `settings.yaml` directly) hot-apply too.
137
+ - Compositions without a settings service (pure headless) never register the namespace, and the plugin behaves exactly as if unconfigured.
121
138
 
122
139
  ## Development
123
140
 
124
141
  ### Commands
125
142
 
126
143
  ```sh
127
- npm install # uses a local npm cache
128
- npm test # unit tests (mock Zotero server)
144
+ npm install # uses a local npm cache (see the workspace note below)
145
+ npm test # unit tests (mock Zotero server + browser card tests)
129
146
  npm run test:coverage # 100% coverage gate on src/
130
- npm run typecheck # tsc --noEmit, app + test projects
131
- npm run build # emits lib/
147
+ npm run typecheck # tsc --noEmit, node / test / client projects
148
+ npm run build # tsc emits the node half into lib/; esbuild emits the browser half lib/client.js
149
+ npm run build:client # rebuild the browser half only (self-checks the loader handoff)
150
+ npm run dev:client # watch the browser half (pair with the hot-swap overlay)
132
151
  npm run format # prettier --write across the repo
133
152
  npm run format:check # verify formatting (run before committing)
134
153
  ```
135
154
 
155
+ > This checkout sits inside the deepseek-harness workspace tree: the parent `package.json` declares `workspaces`, so npm walks up to it and tries to install the whole workspace. Run `npm install --no-workspaces` instead (or drop a `.npmrc` with `workspaces=false` in this repository).
156
+
136
157
  Integration tests run against a live Zotero and stay skipped unless enabled:
137
158
 
138
159
  ```sh
@@ -175,6 +196,8 @@ dsh web --patch ./dev-lib.cordis.yml --port 3307
175
196
 
176
197
  Hot swap affects only the instance started with `--patch`; the resident instance keeps running the tarball version.
177
198
 
199
+ **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`.
200
+
178
201
  ## License
179
202
 
180
203
  MIT. See [LICENSE](./LICENSE).
package/README.md CHANGED
@@ -10,15 +10,13 @@
10
10
 
11
11
  ## 工具
12
12
 
13
- | 工具 | 用途 |
14
- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
15
- | `zotero_search` | 发现:按标题/作者/年份搜索库里的资料,`everything` 模式连全文索引一起搜;可限定某个分类或已保存搜索。 |
16
- | `zotero_get` | 检查:读取一条资料的结构化核心元数据,可选检查笔记、注释、附件的清单与预览。 |
17
- | `zotero_retrieve` | 取证:按问题返回最相关的有界证据片段(注释、笔记、摘要、全文分块)。 |
18
- | `zotero_attachment` | 原文:解析条目或附件 ref,返回原始附件已验证的磁盘路径或链接 URL。条目 ref 取 Zotero 自选的最佳附件,附件 ref 指定单个附件。 |
19
- | `zotero_export` | 引用:让 Zotero 按自己的 citation/export 能力生成结果(引用、CSL 参考文献表、`bibtex` / `biblatex` / `ris` / `csljson`)。 |
20
-
21
- 每个工具都返回形如 `zotero://user/0/<item|attachment|annotation|collection|search>/<KEY>` 的可复用 ref,后续操作都通过它串联。Zotero 10+ 的 `?server=<id>` 限定符把 ref 绑定到产生它的数据库身份,数据库切换时阻止误读旧 ref。
13
+ | 工具 | 用途 |
14
+ | ------------------- | -------------------------------------------------------------------------------------------------------------------------- |
15
+ | `zotero_search` | 发现:按标题/作者/年份搜索库里的资料,`everything` 模式连全文索引一起搜;可限定某个分类或已保存搜索 |
16
+ | `zotero_get` | 检查:读取一条资料的结构化核心元数据,可选检查笔记、注释、附件的清单与预览。 |
17
+ | `zotero_retrieve` | 取证:按问题返回最相关的有界证据片段(注释、笔记、摘要、全文分块) |
18
+ | `zotero_attachment` | 原文:解析条目或附件 ref,返回原始附件已验证的磁盘路径或链接 URL |
19
+ | `zotero_export` | 引用:让 Zotero 按自己的 citation/export 能力生成结果(引用、CSL 参考文献表、`bibtex` / `biblatex` / `ris` / `csljson`)。 |
22
20
 
23
21
  ## 使用示例
24
22
 
@@ -46,10 +44,17 @@ Agent 按需求逐层深入,一段典型对话:
46
44
 
47
45
  `/zotero status` 报告连通性、API/schema 版本和数据库身份标识(Server ID,Zotero 10+)。这是唯一的健康检查。普通调用失败时返回带类型的领域错误。
48
46
 
47
+ ## 按需工作与连接失败交互
48
+
49
+ - 插件常驻但完全请求驱动:加载、闲置、卸载都不会发起任何请求(无探测、无轮询、无后台任务)。只有两种入口会触达 Zotero:Agent 在用户明确要求时调用五个工具,或用户手动执行 `/zotero status`。
50
+ - 工具调用遇到连接类失败(`ZOTERO_NOT_RUNNING` 未运行 / `ZOTERO_API_DISABLED` 本地 API 被禁用 / `ZOTERO_API_VERSION` 版本过旧 / `ZOTERO_TIMEOUT` 超时)时,会通过交互式问题卡片询问用户怎么处理:第一个选项是带 `(Recommended)` 的推荐操作(英文文案,与错误消息一致,如 "I started Zotero, retry (Recommended)"),选择后插件按原参数重试一次;再失败或选择 "Abort this query" 时返回原类型化错误,绝不反复询问。
51
+ - 无交互能力的环境(headless 组合、无 UI provider)自动降级:不询问,直接返回类型化错误。询问机制自身故障也绝不掩盖原始连接错误。
52
+
49
53
  ## 限制
50
54
 
51
- - 对文献库只读:V1 没有任何修改条目、笔记、标签、分类等文献库数据的路径。
55
+ - 对文献库只读:没有任何修改条目、笔记、标签、分类等文献库数据的路径。
52
56
  - 全文证据依赖 Zotero 的全文索引:`everything` 搜索和 `retrieve` 的全文片段都以索引为前提。
57
+ - 笔记正文搜索是插件侧补扫:仅库/分类范围、仅结果首页、受 `maxNoteScanRecords` 上限约束,超出上限的笔记不参与匹配。
53
58
  - 附件深度分析取决于当前 Harness 配置:`zotero_attachment` 返回文件位置,能否继续读取该 PDF 由 composition 里是否有相应文件/PDF 能力决定。
54
59
  - 证据排序是词项相关度检索,不是 embedding 或语义搜索。
55
60
 
@@ -67,7 +72,7 @@ Agent 按需求逐层深入,一段典型对话:
67
72
  dsh plugin --profile <name> add dsh-zotero
68
73
  ```
69
74
 
70
- tarball 内含已构建的 `lib/`,无需本地构建。
75
+ tarball 内含已构建的 `lib/`(node 半与浏览器半 `lib/client.js`),无需本地构建。浏览器半边是配置卡片:dsh web 会扫描到包内声明的 `dsh.client` 清单并自动挂载,无需额外配置。
71
76
 
72
77
  ### 本地 tarball
73
78
 
@@ -100,39 +105,55 @@ allowBuilds:
100
105
 
101
106
  所有值都是 `Config` 字段,可在 bundle 的 `config` 块中修改(例如通过 `dsh plugin config`)。以下为默认值。
102
107
 
103
- | 字段 | 默认值 | 含义 |
104
- | ---------------------- | ---------------------------- | -------------------------------------- |
105
- | `baseUrl` | `http://127.0.0.1:23119/api` | 本地 API 基础 URL。仅支持纯回环 HTTP。 |
106
- | `provider` | `local` | 要选择的 provider id。 |
107
- | `timeoutMs` | `5000` | 每个请求的 provider 超时时间。 |
108
- | `maxSearchResults` | `20` | `zotero_search` `limit` 的上限。 |
109
- | `maxEvidenceChars` | `6000` | 检索证据的总字符预算。 |
110
- | `maxEvidencePassages` | `4` | 证据片段数量的上限。 |
111
- | `maxDetailChars` | `3000` | `zotero_get` 摘要预览的字符预算。 |
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
- | `defaultStyle` | `apa` | 引用/参考文献使用的 CSL 样式。 |
120
- | `defaultLocale` | `en-US` | 引用/参考文献使用的 CSL locale。 |
108
+ | 字段 | 默认值 | 含义 |
109
+ | ---------------------- | ---------------------------- | ------------------------------------------------------------------------- |
110
+ | `baseUrl` | `http://127.0.0.1:23119/api` | 本地 API 基础 URL。仅支持纯回环 HTTP。 |
111
+ | `provider` | `local` | 要选择的 provider id。 |
112
+ | `timeoutMs` | `5000` | 每个请求的 provider 超时时间。 |
113
+ | `maxSearchResults` | `20` | `zotero_search` `limit` 的上限。 |
114
+ | `maxNoteScanRecords` | `200` | `zotero_search` 补扫笔记正文的笔记数量上限。 |
115
+ | `maxEvidenceChars` | `6000` | 检索证据的总字符预算。 |
116
+ | `maxEvidencePassages` | `4` | 证据片段数量的上限。 |
117
+ | `maxDetailChars` | `3000` | `zotero_get` 摘要预览的字符预算。 |
118
+ | `maxNoteBodyChars` | `30000` | `zotero_get` 返回 note 条目自身正文的字符预算。 |
119
+ | `maxNoteChars` | `2000` | `zotero_get` 单条笔记预览的字符预算。 |
120
+ | `maxNoteRecords` | `50` | `zotero_get` 返回笔记数量的上限。 |
121
+ | `maxAnnotationRecords` | `100` | `zotero_get` 返回批注数量的上限。 |
122
+ | `fulltextChunkWords` | `200` | 进入证据排序的全文片段词数。 |
123
+ | `maxFulltextChars` | `250000` | 进入证据排序的全文大小上限。 |
124
+ | `maxResponseBytes` | `16777216` | 每个 API 响应的流式字节上限。 |
125
+ | `maxExportChars` | `1000000` | 导出输出的硬上限。不会中途截断。 |
126
+ | `maxExportRefs` | `1000` | 单次 `zotero_export` 的 refs 数量上限,保护请求行不超服务器 HTTP 头限制。 |
127
+ | `defaultStyle` | `apa` | 引用/参考文献使用的 CSL 样式。 |
128
+ | `defaultLocale` | `en-US` | 引用/参考文献使用的 CSL locale。 |
129
+
130
+ ### Web 配置
131
+
132
+ 插件在 dsh web 的 **设置 → 插件 → 插件配置** 页注册了一张 "Zotero" 卡片,列出上表全部 19 个字段。卡片绑定 `zotero` 设置命名空间:写入的内容落在 `$DSH_HOME/settings.yaml` 的 `zotero:` 段(与补丁 entry 的 `config` 叠层,用户段优先),**保存即热生效**——传输层与 provider 会按新值重建,下一个工具调用或 `/zotero status` 立即使用新配置,无需重启 dsh。
133
+
134
+ - 非法值(如非回环的 `baseUrl`、非正整数的限制)在写入前被拒绝,卡片提示保存失败并保留草稿,插件继续运行于上一个合法值。
135
+ - 每个字段显示当前生效值;被设置文档覆盖的字段带「已覆盖」标记,可一键恢复默认(清除用户段,回到补丁 entry 值)。
136
+ - 设置文档被外部编辑(如直接改 `settings.yaml`)时同样会热生效。
137
+ - 无 settings 服务的组合(纯 headless)不注册命名空间,插件行为与未配置时完全一致。
121
138
 
122
139
  ## 开发
123
140
 
124
141
  ### 命令
125
142
 
126
143
  ```sh
127
- npm install # 使用本地 npm 缓存
128
- npm test # 单元测试(mock Zotero server)
144
+ npm install # 使用本地 npm 缓存(见下方 workspace 说明)
145
+ npm test # 单元测试(mock Zotero server + 浏览器卡片测试)
129
146
  npm run test:coverage # 对 src/ 的 100% 覆盖率门禁
130
- npm run typecheck # tsc --noEmit,app + test 项目
131
- npm run build # 生成 lib/
147
+ npm run typecheck # tsc --noEmit,node / test / client 三个项目
148
+ npm run build # tsc 生成 node 半 lib/ + esbuild 生成浏览器半 lib/client.js
149
+ npm run build:client # 只重建浏览器半(含 loader 交接格式自检)
150
+ npm run dev:client # 浏览器半 watch 模式(配合热替换 overlay)
132
151
  npm run format # prettier --write 全仓格式化
133
152
  npm run format:check # 校验格式化(提交前执行)
134
153
  ```
135
154
 
155
+ > 本仓库位于 deepseek-harness workspace 树内:父目录 `package.json` 声明了 `workspaces`,npm 会向上找到它并尝试安装整个 workspace。请使用 `npm install --no-workspaces`(或在本仓库放置含 `workspaces=false` 的 `.npmrc`)。
156
+
136
157
  集成测试面向真实 Zotero,默认跳过,需显式开启:
137
158
 
138
159
  ```sh
@@ -175,6 +196,8 @@ dsh web --patch ./dev-lib.cordis.yml --port 3307
175
196
 
176
197
  热替换仅作用于通过 `--patch` 启动的实例;常驻实例继续运行 tarball 版本。
177
198
 
199
+ **浏览器半边的开发**: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`。
200
+
178
201
  ## 许可证
179
202
 
180
203
  本插件以 [MIT](./LICENSE) 许可证发布。
package/lib/ask.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Interactive recovery for Zotero connectivity failures.
3
+ *
4
+ * When a request-driven tool call fails because Zotero cannot serve it
5
+ * (not running, local API disabled, unsupported API version, timeout),
6
+ * the caller is asked how to proceed through the `userQuestions` seam,
7
+ * with the recommended action offered first. The ask happens only inside
8
+ * a tool call that actually attempted a Zotero request — loading the
9
+ * plugin never probes, and a tool that was never called never asks.
10
+ * Everything here fails closed: an absent question service, a failed
11
+ * question, or a non-retry answer all surface the original typed
12
+ * `ZoteroError`, so a broken question mechanism can never mask a broken
13
+ * Zotero connection, and a retry is attempted at most once.
14
+ * @module dsh-zotero/ask
15
+ */
16
+ import type { Context } from '@deepseek-ai/cordis';
17
+ import { type ToolRunContext } from '@deepseek-ai/dsh-tools';
18
+ /** The connectivity failure codes that warrant asking the user how to proceed. */
19
+ export declare const ASK_WORTHY_CODES: readonly ["ZOTERO_NOT_RUNNING", "ZOTERO_API_DISABLED", "ZOTERO_API_VERSION", "ZOTERO_TIMEOUT"];
20
+ export type AskWorthyCode = (typeof ASK_WORTHY_CODES)[number];
21
+ /** The parts of a tool execution `withConnectivityAsk` needs. */
22
+ export type ConnectivityAskExec = Pick<ToolRunContext, 'signal' | 'agent'>;
23
+ /**
24
+ * Run one Zotero request; on a connectivity failure, ask the user how to
25
+ * proceed and retry at most once when they choose the recommended action.
26
+ * @param ctx - the plugin context; the question service is looked up
27
+ * optionally, so headless compositions skip the ask.
28
+ * @param exec - the tool execution (signal and agent) the failure belongs to.
29
+ * @param run - the request to attempt; must be re-runnable with identical
30
+ * arguments, because the retry path calls it a second time.
31
+ * @returns the request result, or throws the original `ZoteroError` when
32
+ * the failure is not ask-worthy, no question service exists, the user
33
+ * does not choose to retry, the question itself fails, or the retry
34
+ * fails again.
35
+ */
36
+ export declare function withConnectivityAsk<T>(ctx: Context, exec: ConnectivityAskExec, run: () => Promise<T>): Promise<T>;
37
+ //# sourceMappingURL=ask.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ask.d.ts","sourceRoot":"","sources":["../src/ask.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAe1E,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,gGAKnB,CAAA;AAEV,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE7D,iEAAiE;AACjE,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAA;AA2E1E;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,EACzC,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,mBAAmB,EACzB,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACpB,OAAO,CAAC,CAAC,CAAC,CA4BZ"}
package/lib/ask.js ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Interactive recovery for Zotero connectivity failures.
3
+ *
4
+ * When a request-driven tool call fails because Zotero cannot serve it
5
+ * (not running, local API disabled, unsupported API version, timeout),
6
+ * the caller is asked how to proceed through the `userQuestions` seam,
7
+ * with the recommended action offered first. The ask happens only inside
8
+ * a tool call that actually attempted a Zotero request — loading the
9
+ * plugin never probes, and a tool that was never called never asks.
10
+ * Everything here fails closed: an absent question service, a failed
11
+ * question, or a non-retry answer all surface the original typed
12
+ * `ZoteroError`, so a broken question mechanism can never mask a broken
13
+ * Zotero connection, and a retry is attempted at most once.
14
+ * @module dsh-zotero/ask
15
+ */
16
+ import { HarnessError } from '@deepseek-ai/dsh-llm';
17
+ import { TOOL_ABORTED } from '@deepseek-ai/dsh-tools';
18
+ import { ZOTERO_API_DISABLED, ZOTERO_API_VERSION, ZOTERO_NOT_RUNNING, ZOTERO_TIMEOUT, ZoteroError, } from './errors.js';
19
+ /** The connectivity failure codes that warrant asking the user how to proceed. */
20
+ export const ASK_WORTHY_CODES = [
21
+ ZOTERO_NOT_RUNNING,
22
+ ZOTERO_API_DISABLED,
23
+ ZOTERO_API_VERSION,
24
+ ZOTERO_TIMEOUT,
25
+ ];
26
+ const ABORT_LABEL = 'Abort this query';
27
+ const ABORT_DESCRIPTION = 'Stop this operation; ask me to retry later if you still need it.';
28
+ const RETRY_DESCRIPTION = 'Re-run the query with the original parameters.';
29
+ const FAILURE_SPECS = {
30
+ [ZOTERO_NOT_RUNNING]: {
31
+ header: 'Zotero is not running',
32
+ question: 'Zotero is not running, so I cannot read your library. What should I do?',
33
+ detail: 'Start Zotero and check "Allow other applications on this computer to communicate with Zotero" under Settings → Advanced.',
34
+ retryLabel: 'I started Zotero, retry (Recommended)',
35
+ retryDescription: RETRY_DESCRIPTION,
36
+ abortLabel: ABORT_LABEL,
37
+ abortDescription: ABORT_DESCRIPTION,
38
+ },
39
+ [ZOTERO_API_DISABLED]: {
40
+ header: 'Zotero local API is disabled',
41
+ question: 'Zotero is running but rejected the local API request (403).',
42
+ detail: 'Check "Allow other applications on this computer to communicate with Zotero" under Zotero Settings → Advanced.',
43
+ retryLabel: 'I enabled the local API, retry (Recommended)',
44
+ retryDescription: RETRY_DESCRIPTION,
45
+ abortLabel: ABORT_LABEL,
46
+ abortDescription: ABORT_DESCRIPTION,
47
+ },
48
+ [ZOTERO_API_VERSION]: {
49
+ header: 'Zotero version too old',
50
+ question: 'The running Zotero does not speak local API version 3, which this plugin requires.',
51
+ detail: 'Upgrade Zotero to a version whose local API supports version 3.',
52
+ retryLabel: 'I upgraded Zotero, retry (Recommended)',
53
+ retryDescription: RETRY_DESCRIPTION,
54
+ abortLabel: ABORT_LABEL,
55
+ abortDescription: ABORT_DESCRIPTION,
56
+ },
57
+ [ZOTERO_TIMEOUT]: {
58
+ header: 'Zotero timed out',
59
+ question: 'Zotero did not respond within the timeout (it may be indexing a large library).',
60
+ detail: 'The request failed after the configured timeout.',
61
+ retryLabel: 'Retry (Recommended)',
62
+ retryDescription: 'Run the same request again.',
63
+ abortLabel: ABORT_LABEL,
64
+ abortDescription: ABORT_DESCRIPTION,
65
+ },
66
+ };
67
+ function isAskWorthyCode(code) {
68
+ return ASK_WORTHY_CODES.includes(code);
69
+ }
70
+ function questionOf(spec) {
71
+ return {
72
+ id: 'zotero-failure',
73
+ question: spec.question,
74
+ header: spec.header,
75
+ detail: spec.detail,
76
+ options: [
77
+ { label: spec.retryLabel, description: spec.retryDescription },
78
+ { label: spec.abortLabel, description: spec.abortDescription },
79
+ ],
80
+ };
81
+ }
82
+ /**
83
+ * Run one Zotero request; on a connectivity failure, ask the user how to
84
+ * proceed and retry at most once when they choose the recommended action.
85
+ * @param ctx - the plugin context; the question service is looked up
86
+ * optionally, so headless compositions skip the ask.
87
+ * @param exec - the tool execution (signal and agent) the failure belongs to.
88
+ * @param run - the request to attempt; must be re-runnable with identical
89
+ * arguments, because the retry path calls it a second time.
90
+ * @returns the request result, or throws the original `ZoteroError` when
91
+ * the failure is not ask-worthy, no question service exists, the user
92
+ * does not choose to retry, the question itself fails, or the retry
93
+ * fails again.
94
+ */
95
+ export async function withConnectivityAsk(ctx, exec, run) {
96
+ try {
97
+ return await run();
98
+ }
99
+ catch (error) {
100
+ if (!(error instanceof ZoteroError) || !isAskWorthyCode(error.code))
101
+ throw error;
102
+ const questions = ctx.get('userQuestions');
103
+ if (questions === undefined)
104
+ throw error;
105
+ const spec = FAILURE_SPECS[error.code];
106
+ let answer;
107
+ try {
108
+ const request = {
109
+ questions: [questionOf(spec)],
110
+ ...(exec.agent !== undefined ? { agent: exec.agent } : {}),
111
+ signal: exec.signal,
112
+ };
113
+ answer = await questions.ask(request);
114
+ }
115
+ catch {
116
+ // A failed question (no provider, aborted ask, delegated caller) must
117
+ // never mask the underlying connectivity failure.
118
+ if (exec.signal?.aborted)
119
+ throw new HarnessError('tool call aborted', TOOL_ABORTED);
120
+ throw error;
121
+ }
122
+ const answerItem = answer.answers.find((item) => item.id === 'zotero-failure');
123
+ const selected = answerItem?.selected ?? [];
124
+ if (!selected.includes(spec.retryLabel))
125
+ throw error;
126
+ // Outside the catch: a second failure propagates as-is, never re-asking.
127
+ return await run();
128
+ }
129
+ }
130
+ //# sourceMappingURL=ask.js.map
package/lib/ask.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ask.js","sourceRoot":"","sources":["../src/ask.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAgBH;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,2DAA2D;IAC3D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,uEAAuE;AACvE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAGjF;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,OAAO,GACZ;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAKlD;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,OAAO,GAAG,yBAAyB,CAmBlF;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,SAAS,OAAO,EAAE,EACxB,IAAI,EAAE,MAAM,GACX,yBAAyB,GAAG,SAAS,CAoBvC"}
1
+ {"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,2DAA2D;IAC3D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,uEAAuE;AACvE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAGjF;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,OAAO,GACZ;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAKlD;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,OAAO,GAAG,yBAAyB,CAmBlF;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,SAAS,OAAO,EAAE,EACxB,IAAI,EAAE,MAAM,GACX,yBAAyB,GAAG,SAAS,CAoBvC"}
@@ -9,15 +9,7 @@
9
9
  * @module dsh-zotero/attachments
10
10
  */
11
11
  import { ZOTERO_UNEXPECTED, ZoteroError } from './errors.js';
12
- const OBJECT_KEY_PATTERN = /^[A-Z0-9]{8}$/;
13
- function asRecord(value) {
14
- return typeof value === 'object' && value !== null && !Array.isArray(value)
15
- ? value
16
- : undefined;
17
- }
18
- function asString(value) {
19
- return typeof value === 'string' ? value : undefined;
20
- }
12
+ import { asRecord, asString, isObjectKey } from './json.js';
21
13
  /** Extract a Zotero object key from an API `links.attachment.href`. */
22
14
  export function extractAttachmentKey(href) {
23
15
  if (href === undefined)
@@ -43,7 +35,7 @@ export function bestAttachmentFromLinks(json) {
43
35
  export function normalizeAttachmentRecord(json) {
44
36
  const record = asRecord(json);
45
37
  const key = asString(record?.key);
46
- if (key === undefined || !OBJECT_KEY_PATTERN.test(key)) {
38
+ if (key === undefined || !isObjectKey(key)) {
47
39
  throw new ZoteroError('Zotero returned an attachment without a valid object key.', ZOTERO_UNEXPECTED);
48
40
  }
49
41
  const data = asRecord(record?.data);
@@ -1 +1 @@
1
- {"version":3,"file":"attachments.js","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"attachments.js","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"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"}