dsh-search-tools 0.1.0 → 0.1.1

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 (100) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +0 -0
  3. package/README.en.md +728 -0
  4. package/README.md +695 -77
  5. package/README.zh.md +5 -88
  6. package/SECURITY.md +0 -0
  7. package/THIRD_PARTY_NOTICES.md +0 -0
  8. package/cordis.patch.yml +0 -0
  9. package/icon.svg +0 -0
  10. package/lib/client.js +2 -2
  11. package/lib/client.js.map +1 -1
  12. package/lib/index.js +3 -3
  13. package/lib/index.js.map +1 -1
  14. package/lib/types/client/ActivationHint.d.ts +0 -0
  15. package/lib/types/client/ActivationHint.d.ts.map +0 -0
  16. package/lib/types/client/ResearchCard.d.ts +0 -0
  17. package/lib/types/client/ResearchCard.d.ts.map +0 -0
  18. package/lib/types/client/SettingsPage.d.ts +0 -0
  19. package/lib/types/client/SettingsPage.d.ts.map +0 -0
  20. package/lib/types/client/TakeoverWarning.d.ts +0 -0
  21. package/lib/types/client/TakeoverWarning.d.ts.map +0 -0
  22. package/lib/types/client/api.d.ts +0 -0
  23. package/lib/types/client/api.d.ts.map +0 -0
  24. package/lib/types/client/components/Field.d.ts +0 -0
  25. package/lib/types/client/components/Field.d.ts.map +0 -0
  26. package/lib/types/client/diagnostic-text.d.ts +0 -0
  27. package/lib/types/client/diagnostic-text.d.ts.map +0 -0
  28. package/lib/types/client/index.d.ts +0 -0
  29. package/lib/types/client/index.d.ts.map +0 -0
  30. package/lib/types/client/issue-text.d.ts +0 -0
  31. package/lib/types/client/issue-text.d.ts.map +0 -0
  32. package/lib/types/client/locales.d.ts +0 -0
  33. package/lib/types/client/locales.d.ts.map +0 -0
  34. package/lib/types/client/styles.d.ts +0 -0
  35. package/lib/types/client/styles.d.ts.map +0 -0
  36. package/lib/types/config.d.ts +0 -0
  37. package/lib/types/config.d.ts.map +0 -0
  38. package/lib/types/core/diagnostics.d.ts +0 -0
  39. package/lib/types/core/diagnostics.d.ts.map +0 -0
  40. package/lib/types/core/dispatchers.d.ts +0 -0
  41. package/lib/types/core/dispatchers.d.ts.map +0 -0
  42. package/lib/types/core/host-types.d.ts +0 -0
  43. package/lib/types/core/host-types.d.ts.map +0 -0
  44. package/lib/types/core/service.d.ts +0 -0
  45. package/lib/types/core/service.d.ts.map +0 -0
  46. package/lib/types/core/types.d.ts +0 -0
  47. package/lib/types/core/types.d.ts.map +0 -0
  48. package/lib/types/fetch/adapters/readable-http/adapter.d.ts +0 -0
  49. package/lib/types/fetch/adapters/readable-http/adapter.d.ts.map +0 -0
  50. package/lib/types/fetch/adapters/readable-http/manifest.d.ts +0 -0
  51. package/lib/types/fetch/adapters/readable-http/manifest.d.ts.map +0 -0
  52. package/lib/types/fetch/adapters/readable-http/provider.d.ts +0 -0
  53. package/lib/types/fetch/adapters/readable-http/provider.d.ts.map +0 -0
  54. package/lib/types/fetch/adapters/readable-http/readability.d.ts +0 -0
  55. package/lib/types/fetch/adapters/readable-http/readability.d.ts.map +0 -0
  56. package/lib/types/fetch/adapters/readable-http/schema.d.ts +0 -0
  57. package/lib/types/fetch/adapters/readable-http/schema.d.ts.map +0 -0
  58. package/lib/types/host/routes.d.ts +0 -0
  59. package/lib/types/host/routes.d.ts.map +0 -0
  60. package/lib/types/index.d.ts +0 -0
  61. package/lib/types/index.d.ts.map +0 -0
  62. package/lib/types/research/pipeline.d.ts +0 -0
  63. package/lib/types/research/pipeline.d.ts.map +0 -0
  64. package/lib/types/research/prompt.d.ts +0 -0
  65. package/lib/types/research/prompt.d.ts.map +0 -0
  66. package/lib/types/research/render.d.ts +0 -0
  67. package/lib/types/research/render.d.ts.map +0 -0
  68. package/lib/types/research/tool.d.ts +0 -0
  69. package/lib/types/research/tool.d.ts.map +0 -0
  70. package/lib/types/search/adapters/searxng/adapter.d.ts +0 -0
  71. package/lib/types/search/adapters/searxng/adapter.d.ts.map +0 -0
  72. package/lib/types/search/adapters/searxng/client.d.ts +0 -0
  73. package/lib/types/search/adapters/searxng/client.d.ts.map +0 -0
  74. package/lib/types/search/adapters/searxng/manifest.d.ts +0 -0
  75. package/lib/types/search/adapters/searxng/manifest.d.ts.map +0 -0
  76. package/lib/types/search/adapters/searxng/parse-html.d.ts +0 -0
  77. package/lib/types/search/adapters/searxng/parse-html.d.ts.map +0 -0
  78. package/lib/types/search/adapters/searxng/parse-json.d.ts +0 -0
  79. package/lib/types/search/adapters/searxng/parse-json.d.ts.map +0 -0
  80. package/lib/types/search/adapters/searxng/schema.d.ts +0 -0
  81. package/lib/types/search/adapters/searxng/schema.d.ts.map +0 -0
  82. package/lib/types/shared/defaults.d.ts +0 -0
  83. package/lib/types/shared/defaults.d.ts.map +0 -0
  84. package/lib/types/shared/diff.d.ts +0 -0
  85. package/lib/types/shared/diff.d.ts.map +0 -0
  86. package/lib/types/shared/errors.d.ts +0 -0
  87. package/lib/types/shared/errors.d.ts.map +0 -0
  88. package/lib/types/shared/limit.d.ts +0 -0
  89. package/lib/types/shared/limit.d.ts.map +0 -0
  90. package/lib/types/shared/localized.d.ts +0 -0
  91. package/lib/types/shared/localized.d.ts.map +0 -0
  92. package/lib/types/shared/path.d.ts +0 -0
  93. package/lib/types/shared/path.d.ts.map +0 -0
  94. package/lib/types/shared/url.d.ts +0 -0
  95. package/lib/types/shared/url.d.ts.map +0 -0
  96. package/lib/types/shared/validation.d.ts +0 -0
  97. package/lib/types/shared/validation.d.ts.map +0 -0
  98. package/locale/en.json +0 -0
  99. package/locale/zh.json +0 -0
  100. package/package.json +2 -2
package/README.en.md ADDED
@@ -0,0 +1,728 @@
1
+ # dsh-search-tools
2
+
3
+ [English](README.en.md) · [简体中文](https://github.com/ThzxxArt/dsh-search-tools/blob/main/README.md)
4
+
5
+ [![npm](https://img.shields.io/npm/v/dsh-search-tools?label=npm)](https://www.npmjs.com/package/dsh-search-tools)
6
+ [![CI](https://github.com/ThzxxArt/dsh-search-tools/actions/workflows/ci.yml/badge.svg)](https://github.com/ThzxxArt/dsh-search-tools/actions/workflows/ci.yml)
7
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ThzxxArt/dsh-search-tools/blob/main/LICENSE)
8
+
9
+ Adapter-first web search and readable page fetching for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH). Everything is configured in the DSH Web GUI under **Settings → Search Tools**; no hand-edited YAML is required.
10
+
11
+ > **Current version: 0.1.1 (2026-10-22).** This is a documentation and metadata refresh after 0.1.0. The feature surface is unchanged; the primary README is now complete Simplified Chinese and this file is the English counterpart. 0.1.0 was verified against a real DSH instance, a real SearXNG instance, real model tool turns, and the Web GUI; build, test, pack, and publint gates pass. Evidence is recorded in the [M0 verification note](https://github.com/ThzxxArt/dsh-search-tools/blob/main/docs/m0-verification.md).
12
+
13
+ ---
14
+
15
+ ## Contents
16
+
17
+ - [What it solves](#what-it-solves)
18
+ - [Features](#features)
19
+ - [How it works](#how-it-works)
20
+ - [Requirements](#requirements)
21
+ - [Install and upgrade](#install-and-upgrade)
22
+ - [Quick start](#quick-start)
23
+ - [Settings page](#settings-page)
24
+ - [Configuration reference](#configuration-reference)
25
+ - [Where configuration lives](#where-configuration-lives)
26
+ - [SearXNG search settings](#searxng-search-settings)
27
+ - [Readability HTTP fetch settings](#readability-http-fetch-settings)
28
+ - [web_research settings](#web_research-settings)
29
+ - [Takeover settings](#takeover-settings)
30
+ - [Model tools](#model-tools)
31
+ - [Security model](#security-model)
32
+ - [Adapter development](#adapter-development)
33
+ - [Local development](#local-development)
34
+ - [Release process](#release-process)
35
+ - [Troubleshooting](#troubleshooting)
36
+ - [Known limitations](#known-limitations)
37
+ - [License and third-party notices](#license-and-third-party-notices)
38
+
39
+ ---
40
+
41
+ ## What it solves
42
+
43
+ DSH ships built-in `web_search` and `web_fetch`, but the default backends do not fit every deployment. Some users already run SearXNG, some need search traffic to go through a proxy, and some want readable article text instead of raw HTML.
44
+
45
+ `dsh-search-tools` separates the backend from DSH core:
46
+
47
+ - Users pick a backend, enter instances, and test it in the Web settings page.
48
+ - The plugin registers **stable provider ids**, so swapping a backend never changes DSH's `web.searchProvider`.
49
+ - Adding a backend means registering an adapter; the settings page renders its manifest without page-code changes.
50
+ - An extra `web_research` tool performs search + top-N page fetch in one call for turns that need page content, not just links.
51
+
52
+ It does **not** provide a public search API, bypass logins or paywalls, execute page JavaScript, render a browser, or replace DSH's permission and sandbox model.
53
+
54
+ ## Features
55
+
56
+ | Capability | Description |
57
+ |---|---|
58
+ | Stable search/fetch providers | `dsh-search-tools` and `dsh-search-tools-fetch` never change with the backend |
59
+ | Built-in search adapter | SearXNG: multiple instances, ordered/round-robin, failover, JSON/HTML, GET/POST, Bearer/Basic auth, proxy, cache, pagination, domain filters |
60
+ | Built-in fetch adapter | Readability HTTP: reuses DSH safe HTTP, extracts article text with Readability, can fall back to raw content |
61
+ | Research tool | `web_research`: search, fetch top N concurrently, per-page/total clipping, sources + readable content |
62
+ | Web settings page | Overview, Search, Fetch, Research, Advanced; inline tests, diagnostics, cache clear, keyboard save |
63
+ | Credential isolation | Search tokens go to DSH credentials, never YAML, logs, or diagnostic text |
64
+ | Takeover and restore | Optionally route built-in `web_search` / `web_fetch` through this plugin; restore the previous provider when disabled |
65
+ | External adapters | Register backends through `ctx.searchTools`; the settings page renders manifest fields generically |
66
+
67
+ ## How it works
68
+
69
+ ```text
70
+ DSH ctx.web
71
+ searchProvider = "dsh-search-tools" (stable)
72
+ fetchProvider = "dsh-search-tools-fetch" (stable)
73
+ │
74
+ ▼
75
+ SearchDispatcher / FetchDispatcher
76
+ │ reads config.search.adapter / config.fetch.adapter
77
+ ▼
78
+ ctx.searchTools (adapter registry)
79
+ ├── searxng built-in search adapter
80
+ ├── readable-http built-in fetch adapter
81
+ └── future adapters from independent packages
82
+ ```
83
+
84
+ Key decisions:
85
+
86
+ 1. **Stable provider ids, swappable adapters.** Adding a backend does not change `web.searchProvider` or require re-registering providers.
87
+ 2. **Open config sections.** `search.adapters` and `fetch.adapters` are open dictionaries; each adapter owns defaults and validation, so the core schema does not grow per backend.
88
+ 3. **Manifest-driven UI.** Host routes send adapter manifests to the browser; the page renders their `fields` generically. New adapters do not require React changes.
89
+ 4. **Resolve config at call time.** The dispatcher selects the current adapter per call; switching backends does not require provider re-registration.
90
+
91
+ See [docs/architecture.md](https://github.com/ThzxxArt/dsh-search-tools/blob/main/docs/architecture.md) for details.
92
+
93
+ ## Requirements
94
+
95
+ | Item | Requirement |
96
+ |---|---|
97
+ | DSH | `>=0.2.1-rc.2 <0.3.0` (verified against `0.2.1-rc.2`) |
98
+ | Node.js | `^22.19.0 || >=24.0.0` |
99
+ | Search backend | A reachable SearXNG instance; or keep DSH's default search |
100
+ | Fetch backend | Public `http(s)` pages; private addresses are blocked by DSH's SSRF protection |
101
+ | Browser | DSH Web GUI (the settings section is Web-only) |
102
+
103
+ DSH itself is preview software and its interfaces can change. The plugin pins its peer range to the verified `0.2.1-rc.2` line; read the [CHANGELOG](https://github.com/ThzxxArt/dsh-search-tools/blob/main/CHANGELOG.md) before upgrading DSH.
104
+
105
+ ## Install and upgrade
106
+
107
+ ### Option 1: Web UI (recommended)
108
+
109
+ 1. Open the DSH Web GUI.
110
+ 2. Go to **Plugins**.
111
+ 3. Click **Add plugin**.
112
+ 4. Enter the package name:
113
+
114
+ ```text
115
+ dsh-search-tools
116
+ ```
117
+
118
+ 5. Enable the bundle after installation.
119
+ 6. Open **Settings → Search Tools**.
120
+
121
+ If the settings section appears, both the host and browser halves loaded.
122
+
123
+ ### Option 2: CLI
124
+
125
+ ```sh
126
+ dsh plugin --profile <profile> add dsh-search-tools
127
+ ```
128
+
129
+ Replace `<profile>` with the target profile, then start it:
130
+
131
+ ```sh
132
+ dsh --profile <profile>
133
+ ```
134
+
135
+ ### Option 3: Offline tarball
136
+
137
+ Download `dsh-search-tools-0.1.1.tgz` from [GitHub Releases](https://github.com/ThzxxArt/dsh-search-tools/releases), then:
138
+
139
+ ```sh
140
+ dsh plugin --profile <profile> add ./dsh-search-tools-0.1.1.tgz
141
+ ```
142
+
143
+ The tarball is byte-identical to the npm artifact and works on isolated networks.
144
+
145
+ ### Upgrade
146
+
147
+ DSH plugins do **not** auto-update. To upgrade:
148
+
149
+ 1. Turn off search/fetch takeover under **Settings → Search Tools → Overview** if it is enabled.
150
+ 2. Remove the old version on the **Plugins** page.
151
+ 3. Add `dsh-search-tools` again, or install the new tarball.
152
+ 4. Re-enable it and check that configuration survived (it lives in the profile's YAML patch and normally survives a reinstall).
153
+ 5. Re-run the search and fetch tests.
154
+
155
+ CLI equivalent:
156
+
157
+ ```sh
158
+ dsh plugin --profile <profile> remove dsh-search-tools
159
+ dsh plugin --profile <profile> add dsh-search-tools
160
+ ```
161
+
162
+ > Do not uninstall while takeover is still enabled. Built-in `web_search` would point at a provider that no longer exists until you reinstall the plugin or restore `web.searchProvider` manually.
163
+
164
+ ### Post-install checks
165
+
166
+ - **Settings → Search Tools** shows all five tabs.
167
+ - **Advanced** shows the version, config path, and cache stats.
168
+ - The model should see `web_research` when the tool is enabled.
169
+ - Enable search takeover only after the search test passes.
170
+
171
+ ## Quick start
172
+
173
+ ### 1. Prepare SearXNG
174
+
175
+ You need a reachable SearXNG instance. Confirm:
176
+
177
+ - the address is an absolute `http(s)` URL, for example `https://searx.example.com`;
178
+ - for JSON API use, `search.formats` includes `json`;
179
+ - HTML-only instances work too: set **Response format** to `html`, or leave `auto`;
180
+ - if the instance requires auth, prepare a Bearer token or `username:password`;
181
+ - if it rate-limits your network, prepare multiple instances or a proxy.
182
+
183
+ The plugin trims a trailing `/search`, query, and hash from the instance URL. Embedded usernames/passwords are rejected.
184
+
185
+ ### 2. Configure search
186
+
187
+ 1. Open **Settings → Search Tools → Search**.
188
+ 2. Choose `SearXNG` under **Backend**.
189
+ 3. Click **Add instance** and enter the instance URL; the label is optional.
190
+ 4. Adjust engines, categories, language, safe search, pages, result count, and timeouts as needed.
191
+ 5. If the instance requires auth, choose `Bearer` or `Basic` and save the credential.
192
+ 6. Click **Test** and confirm structured sources appear.
193
+ 7. Click **Save and apply**.
194
+
195
+ ### 3. Configure fetch (optional)
196
+
197
+ 1. Open the **Fetch** tab.
198
+ 2. Keep **Readability HTTP**.
199
+ 3. Test a public page with the defaults, for example `https://example.com`.
200
+ 4. If extraction is poor, adjust article bounds, response limits, or timeout; or disable Readability to use raw content.
201
+ 5. Save after **Test** passes.
202
+
203
+ ### 4. Enable takeover (optional)
204
+
205
+ On **Overview**:
206
+
207
+ - **Take over built-in search**: built-in `web_search` uses this plugin's search provider. The page runs a search test first; it will not take over if the test fails.
208
+ - **Take over built-in fetch**: built-in `web_fetch` uses this plugin's fetch provider.
209
+
210
+ Takeover writes DSH's `web.searchProvider` / `web.fetchProvider`. Turning it off restores the previous provider recorded in `activation.previousSearchProvider` / `previousFetchProvider`.
211
+
212
+ `web_research` does **not** require takeover. It calls the current DSH search/fetch providers; without takeover, it uses DSH defaults.
213
+
214
+ ### 5. Use the model tools
215
+
216
+ In a model session:
217
+
218
+ - `web_search`: built-in DSH tool; uses this plugin after takeover;
219
+ - `web_fetch`: built-in DSH tool; uses this plugin after takeover;
220
+ - `web_research`: registered by this plugin; works without takeover but uses the current providers.
221
+
222
+ Example `web_research` arguments:
223
+
224
+ ```json
225
+ {
226
+ "query": "DeepSeek Harness release notes",
227
+ "max_results": 8,
228
+ "fetch_top": 3,
229
+ "max_chars": 12000
230
+ }
231
+ ```
232
+
233
+ The result contains `sources`, `fetched`, and `truncated`; the conversation card shows source count, successful fetches, fetch details, and truncation state.
234
+
235
+ ## Settings page
236
+
237
+ ### Overview
238
+
239
+ - Current search/fetch providers.
240
+ - Takeover state and quick toggles.
241
+ - Backend availability, instance count, cache size.
242
+ - Quick search test and fetch preview.
243
+ - Empty-state guide when no instance exists.
244
+
245
+ ### Search
246
+
247
+ - Card-based adapter picker with description, capabilities, and configuration state.
248
+ - Instance editor: add, remove, move up/down, enable/disable, per-instance test.
249
+ - Manifest-rendered fields.
250
+ - Inline test with elapsed time, source title/link/snippet, and **Search notes** (for example, suspended SearXNG engines).
251
+ - Validation errors are field-scoped; a broken search draft does not block a fetch test.
252
+
253
+ ### Fetch
254
+
255
+ - Fetch adapter picker and fields.
256
+ - Public URL preview.
257
+ - Status code, body kind, character count, truncation flag, and content preview.
258
+ - Private addresses are rejected by SSRF protection; this is expected.
259
+
260
+ ### Research
261
+
262
+ - Enable/disable `web_research`.
263
+ - Default result count, max fetch count, default fetch count, per-page/total character budgets, concurrency, timeout.
264
+ - **Model-facing tool prompt** preview generated from the current draft.
265
+ - Disabling `web_research` removes the tool from the model after saving.
266
+
267
+ ### Advanced
268
+
269
+ - Config file path.
270
+ - Cache size and hit/miss counts.
271
+ - Clear cache.
272
+ - Recent errors (ring buffer).
273
+ - Copy redacted diagnostics and clear errors.
274
+ - Current version.
275
+
276
+ ### Keyboard and state
277
+
278
+ - `Cmd/Ctrl+S`: save and apply when the draft is valid and dirty.
279
+ - Tabs support `←` / `→` / `Home` / `End`.
280
+ - Save status, test results, and diagnostic actions are announced through `aria-live`.
281
+ - The page is read-only when a higher configuration layer overrides it; external edits produce a conflict and require reload.
282
+
283
+ ## Configuration reference
284
+
285
+ ### Where configuration lives
286
+
287
+ Configuration is written through DSH's native settings service into the current profile's YAML patch:
288
+
289
+ ```text
290
+ $DSH_HOME/profiles/<profile>/cordis.patch.yml
291
+ ```
292
+
293
+ The shape is roughly:
294
+
295
+ ```yaml
296
+ - id: dsh-search-tools
297
+ name: dsh-search-tools
298
+ config:
299
+ search:
300
+ adapter: searxng
301
+ adapters:
302
+ searxng:
303
+ enabled: true
304
+ instances:
305
+ - url: https://searx.example.com
306
+ enabled: true
307
+ label: primary
308
+ engines: []
309
+ categories: [general]
310
+ language: all
311
+ safesearch: 1
312
+ pages: 1
313
+ maxResults: 10
314
+ strategy: ordered
315
+ method: GET
316
+ format: auto
317
+ auth:
318
+ type: none
319
+ requestTimeoutMs: 12000
320
+ deadlineMs: 45000
321
+ retries: 1
322
+ cacheTtlMs: 300000
323
+ cacheMaxEntries: 200
324
+ userAgent: dsh-search-tools/0.1.1 (+https://github.com/ThzxxArt/dsh-search-tools)
325
+ proxy: ""
326
+ includeDomains: []
327
+ excludeDomains: []
328
+ fetch:
329
+ adapter: readable-http
330
+ adapters:
331
+ readable-http:
332
+ enabled: true
333
+ readability: true
334
+ minArticleChars: 200
335
+ maxArticleChars: 200000
336
+ maxResponseBytes: 5000000
337
+ maxBodyChars: 2000000
338
+ timeoutMs: 20000
339
+ maxRedirects: 5
340
+ userAgent: dsh-search-tools/0.1.1 (+https://github.com/ThzxxArt/dsh-search-tools)
341
+ failOpen: true
342
+ research:
343
+ enabled: true
344
+ maxResults: 8
345
+ maxFetch: 8
346
+ defaultFetchTop: 3
347
+ maxCharsPerPage: 12000
348
+ maxTotalChars: 40000
349
+ concurrency: 3
350
+ timeoutMs: 120000
351
+ activation:
352
+ previousSearchProvider: ""
353
+ previousFetchProvider: ""
354
+ ```
355
+
356
+ **Do not hand-edit this file to configure the plugin.** The settings page handles validation, section-scoped diffs, secret isolation, and conflicts. Hand edits can break YAML or be overridden by a higher layer.
357
+
358
+ Secrets do not enter YAML. The plugin resolves them through DSH credentials:
359
+
360
+ | Credential ref | Purpose | Format |
361
+ |---|---|---|
362
+ | `SEARXNG_TOKEN` | Bearer auth | raw token |
363
+ | `SEARXNG_BASIC_AUTH` | Basic auth | `username:password` |
364
+
365
+ Secret fields are write-only: the page shows only configured/unconfigured state and source, never the value. Tests use saved credentials; unsaved input is not sent.
366
+
367
+ ### SearXNG search settings
368
+
369
+ | Field | Default | Range/values | Notes |
370
+ |---|---:|---|---|
371
+ | `enabled` | `true` | boolean | Disables the adapter |
372
+ | `instances` | `[]` | list | Tried in order; each has `url`, `enabled`, `label` |
373
+ | `engines` | `[]` | comma-separated | Empty uses instance defaults; e.g. `google, bing` |
374
+ | `categories` | `["general"]` | comma-separated | Empty omits the parameter and uses instance defaults |
375
+ | `language` | `all` | string | e.g. `zh-CN`, `en-US` |
376
+ | `safesearch` | `1` | `0` / `1` / `2` | Safe-search level |
377
+ | `timeRange` | empty | `day` / `week` / `month` / `year` | Time range |
378
+ | `pages` | `1` | 1–3 | Concurrent page requests; results are merged and deduplicated |
379
+ | `maxResults` | `10` | 1–20 | Maximum results returned per search |
380
+ | `strategy` | `ordered` | `ordered` / `round-robin` | Instance preference or rotation |
381
+ | `method` | `GET` | `GET` / `POST` | SearXNG request method |
382
+ | `format` | `auto` | `auto` / `json` / `html` | `auto` tries JSON, then HTML |
383
+ | `auth.type` | `none` | `none` / `bearer` / `basic` | Authentication mode |
384
+ | `requestTimeoutMs` | `12000` | 1000–120000 | Per-request timeout |
385
+ | `deadlineMs` | `45000` | 1000–300000 | Whole-search budget; must be at least the request timeout |
386
+ | `retries` | `1` | 0–5 | Retries per instance for retryable errors |
387
+ | `cacheTtlMs` | `300000` | 0–86400000 | Search cache TTL; `0` disables |
388
+ | `cacheMaxEntries` | `200` | 0–10000 | Cache capacity; `0` disables |
389
+ | `userAgent` | versioned default | non-empty, no newlines | Request UA |
390
+ | `proxy` | empty | `http(s)` URL | Proxy; must not contain credentials |
391
+ | `includeDomains` | `[]` | domain list | Keep matching domains; subdomain-aware |
392
+ | `excludeDomains` | `[]` | domain list | Drop matching domains; subdomain-aware |
393
+
394
+ Behavior:
395
+
396
+ - **Failover**: a failed instance falls through to the next; an error is raised only when all fail.
397
+ - **Retries and budget**: retries are bounded by `retries` and `deadlineMs`; no new instance is attempted after the budget expires.
398
+ - **JSON / HTML**: `auto` inspects the response, then falls back to HTML for instances that return an HTML challenge with HTTP 200.
399
+ - **Search notes**: SearXNG `unresponsive_engines` are preserved as notes. Zero results often means suspended engines or CAPTCHA, not a truly empty query.
400
+ - **Cache key**: query, instance list, engines, categories, language, safe search, time range, pages, strategy, method, format, and domain filters. Changing those naturally invalidates entries.
401
+ - **Domain filters**: applied before results become `sources`; `example.com` also matches `www.example.com`.
402
+
403
+ ### Readability HTTP fetch settings
404
+
405
+ | Field | Default | Range | Notes |
406
+ |---|---:|---|---|
407
+ | `enabled` | `true` | boolean | Disables the adapter |
408
+ | `readability` | `true` | boolean | Use Readability for article extraction |
409
+ | `minArticleChars` | `200` | 0–100000 | Below this is not considered article text |
410
+ | `maxArticleChars` | `200000` | 1000–5000000 | Article cap; must be at least the minimum |
411
+ | `maxResponseBytes` | `5000000` | 1024–50000000 | Response byte cap |
412
+ | `maxBodyChars` | `2000000` | 1024–20000000 | Decoded character cap; must be at least the article minimum |
413
+ | `timeoutMs` | `20000` | 1000–120000 | Fetch timeout |
414
+ | `maxRedirects` | `5` | 0–10 | Redirect limit |
415
+ | `userAgent` | versioned default | non-empty, no newlines | Request UA |
416
+ | `failOpen` | `true` | boolean | Fall back to raw content when extraction fails |
417
+
418
+ Fetch behavior:
419
+
420
+ - Reuses DSH's safe HTTP provider, intended to block SSRF and private addresses.
421
+ - Handles `http(s)` URLs only.
422
+ - Parses text/HTML only; PDFs, images, and binaries are not converted to article text.
423
+ - Does not execute JavaScript; client-rendered pages may have no readable body.
424
+ - With `failOpen=true`, a Readability failure returns raw text/HTML instead of failing the whole fetch.
425
+
426
+ ### web_research settings
427
+
428
+ | Field | Default | Range | Notes |
429
+ |---|---:|---|---|
430
+ | `enabled` | `true` | boolean | When off, the model does not see the tool |
431
+ | `maxResults` | `8` | 1–20 | Source cap; upper bound for `max_results` |
432
+ | `maxFetch` | `8` | 1–10 | Fetch cap; upper bound for `fetch_top` |
433
+ | `defaultFetchTop` | `3` | 0–10 | Default when the model omits `fetch_top`; must not exceed `maxFetch` |
434
+ | `maxCharsPerPage` | `12000` | 1000–200000 | Per-page cap; upper bound for `max_chars` |
435
+ | `maxTotalChars` | `40000` | 1000–500000 | Total content budget; must be at least the per-page cap |
436
+ | `concurrency` | `3` | 1–8 | Concurrent fetches |
437
+ | `timeoutMs` | `120000` | 5000–600000 | Tool timeout budget |
438
+
439
+ `web_research` clamps model arguments to these bounds and distributes `maxTotalChars` across pages. A failed page does not fail the whole call; it carries an `error` field, and the card still shows sources and successful fetches.
440
+
441
+ > **Timeout note:** the current `timeoutMs` value is used in the tool prompt preview; the registered tool timeout is fixed at `120000` ms. This is listed under known limitations.
442
+
443
+ ### Takeover settings
444
+
445
+ Takeover is optional.
446
+
447
+ | Setting | Effect |
448
+ |---|---|
449
+ | `web.searchProvider = dsh-search-tools` | Built-in `web_search` uses this plugin's search provider |
450
+ | `web.fetchProvider = dsh-search-tools-fetch` | Built-in `web_fetch` uses this plugin's fetch provider |
451
+ | `activation.previousSearchProvider` | Search provider recorded before takeover; restored when disabled |
452
+ | `activation.previousFetchProvider` | Fetch provider recorded before takeover; restored when disabled |
453
+
454
+ Rules:
455
+
456
+ - Enabling search takeover runs a search test with the saved config first; it does not take over on failure.
457
+ - Fetch takeover does not force a test; test a public page on the Fetch tab first.
458
+ - If no previous provider was recorded, disabling takeover unsets the setting and returns to the DSH default.
459
+ - Disable takeover before disabling or uninstalling the bundle. The plugin detail page shows a takeover warning with a direct "disable takeover" action.
460
+
461
+ ## Model tools
462
+
463
+ ### Built-in `web_search` / `web_fetch`
464
+
465
+ These are DSH tools. The plugin does not replace their names or argument schemas; it only routes their providers:
466
+
467
+ - takeover off: they keep using DSH's providers, unaffected by this plugin;
468
+ - takeover on: requests enter this plugin's dispatcher and the selected adapter;
469
+ - takeover off again: the previous provider is restored.
470
+
471
+ ### `web_research`
472
+
473
+ Registered by this plugin for turns that need page content, not only links.
474
+
475
+ | Argument | Type | Required | Notes |
476
+ |---|---|---|---|
477
+ | `query` | string | yes | Search query |
478
+ | `max_results` | number | no | Source count, bounded by `maxResults` |
479
+ | `fetch_top` | number | no | Fetch top N; `0` returns sources only |
480
+ | `max_chars` | number | no | Per-page character budget, bounded by `maxCharsPerPage` |
481
+
482
+ Result shape:
483
+
484
+ ```json
485
+ {
486
+ "query": "example",
487
+ "sources": [
488
+ { "url": "https://example.com", "title": "Example", "snippet": "..." }
489
+ ],
490
+ "fetched": [
491
+ {
492
+ "url": "https://example.com",
493
+ "title": "Example",
494
+ "content": " readable markdown ...",
495
+ "truncated": false
496
+ }
497
+ ],
498
+ "truncated": false
499
+ }
500
+ ```
501
+
502
+ Behavior:
503
+
504
+ - Searches through the current `ctx.web.search` and fetches through `ctx.web.fetch`.
505
+ - Fetches the top `fetch_top` results with `concurrency` workers.
506
+ - HTML pages go through Readability and Markdown conversion; plain text is used directly.
507
+ - Each page is clipped to `max_chars`; pages share `maxTotalChars`.
508
+ - A failed page carries `error` and does not fail the rest.
509
+ - The conversation card shows sources, successful/failed fetches, character counts, and truncation.
510
+ - External page content is **untrusted data**, not instructions. Neither the model nor the user should execute instructions found in a page.
511
+
512
+ ## Security model
513
+
514
+ - **Authenticated settings routes.** Every `/api/dsh-search-tools/*` route passes DSH's `connection.requestRejection` fence; unauthenticated requests get 401/403.
515
+ - **Bounded request bodies.** JSON bodies are capped at 64 KiB; non-JSON requests get 415.
516
+ - **SSRF protection.** Fetching reuses DSH's `HttpFetchProvider`; private/reserved addresses are rejected. This is not a custom network stack.
517
+ - **No redirects for searches.** SearXNG requests use `redirect: 'error'`, so auth headers cannot follow a cross-origin redirect.
518
+ - **Credentials stay out of YAML.** Bearer/Basic secrets go to DSH credentials; YAML stores only `auth.type`. Diagnostics are redacted.
519
+ - **Proxies carry no credentials.** `proxy` must be an `http(s)` URL without username/password.
520
+ - **Limited error text.** Instance hostnames may appear in diagnostics, but query strings and credentials are stripped.
521
+ - **No telemetry.** The plugin sends no usage data to the author or a third party; search requests go only to the instances you configure.
522
+
523
+ Security still depends on DSH and your deployment. Do not expose the settings page to an untrusted network, and do not treat secrets in a shared profile as public configuration.
524
+
525
+ ## Adapter development
526
+
527
+ > The external adapter contract is experimental in 0.1.x. Interfaces may change in a minor release; pin `dsh-search-tools` and run integration tests if you depend on it.
528
+
529
+ An adapter package does three things:
530
+
531
+ 1. Inject `ctx.searchTools`.
532
+ 2. Provide a manifest (for the settings page) and a runtime (actual search/fetch).
533
+ 3. Register through `ctx.effect` so unload cleanup is automatic.
534
+
535
+ ```ts
536
+ import type { Context } from '@deepseek-ai/cordis'
537
+
538
+ export const inject = ['searchTools']
539
+
540
+ export function apply(ctx: Context): void {
541
+ ctx.effect(
542
+ () => ctx.searchTools.registerSearchAdapter(myAdapter),
543
+ 'my-search-adapter',
544
+ )
545
+ }
546
+ ```
547
+
548
+ `myAdapter` looks like:
549
+
550
+ ```ts
551
+ const myAdapter = {
552
+ manifest: {
553
+ kind: 'search',
554
+ id: 'my-search',
555
+ label: { en: 'My Search', zh: '我的搜索' },
556
+ description: { en: '...', zh: '...' },
557
+ defaults: { endpoint: 'https://search.example.com' },
558
+ capabilities: ['json', 'probe'],
559
+ fields: [
560
+ { kind: 'text', key: 'endpoint', label: { en: 'Endpoint', zh: '地址' } },
561
+ ],
562
+ },
563
+ validate(rawConfig) {
564
+ return { ok: true, issues: [] }
565
+ },
566
+ create(ctx, getConfig) {
567
+ return {
568
+ available: () => true,
569
+ async search(request, signal) {
570
+ return { sources: [], truncated: false }
571
+ },
572
+ }
573
+ },
574
+ }
575
+ ```
576
+
577
+ Conventions:
578
+
579
+ - Search adapter `id` values must be unique; `kind` is `search` or `fetch`.
580
+ - Config lives under `config.search.adapters.<id>` (or `fetch.adapters.<id>`); the adapter normalizes and validates it.
581
+ - `fields` supports `text`, `url`, `password`, `number`, `boolean`, `select`, `tags`, and `instances`.
582
+ - Declare secrets with `secretRefs`; values go through DSH credentials and must not be in manifest defaults.
583
+ - New adapters do not require settings-page code; the page renders manifest fields.
584
+ - v0.1.x does not support a fully custom adapter settings UI. An adapter can register its own `settings.section` in the future, but that is outside the unified page.
585
+ - External adapter localization currently uses the manifest's `label` / `description` text; built-in adapters have `zh` / `en` dictionaries.
586
+
587
+ `ctx.effect` returns a disposer. Do not keep global registration state outside `apply`; duplicate ids are rejected.
588
+
589
+ ## Local development
590
+
591
+ ```sh
592
+ pnpm install --frozen-lockfile --registry=https://registry.npmjs.org
593
+ pnpm typecheck
594
+ pnpm test
595
+ pnpm build
596
+ pnpm release
597
+ ```
598
+
599
+ `pnpm build` emits:
600
+
601
+ ```text
602
+ lib/index.js Host half
603
+ lib/client.js Browser lazy-CJS factory
604
+ lib/types/** TypeScript declarations
605
+ ```
606
+
607
+ Checks:
608
+
609
+ | Command | What it covers |
610
+ |---|---|
611
+ | `pnpm typecheck` | TypeScript |
612
+ | `pnpm test` | Vitest: host units, settings page/research card components, pipeline, publint policy |
613
+ | `pnpm build` | Host, browser, and declarations |
614
+ | `pnpm pack:check` | build + `pnpm pack --dry-run` + publint |
615
+ | `pnpm release` | typecheck + test + pack:check |
616
+
617
+ CI runs typecheck, test, build, pack dry-run, and publint on Ubuntu/Windows × Node 22/24. Publint ignores only `FILE_INVALID_FORMAT` for `lib/client.js`; DSH loads it as a browser CJS factory, not a Node ESM entry. Every other diagnostic fails.
618
+
619
+ ### Project layout
620
+
621
+ ```text
622
+ src/core/ adapter types, registry, stable dispatchers, diagnostics
623
+ src/search/adapters/ SearXNG adapter (manifest/schema/client/parser)
624
+ src/fetch/adapters/ Readability HTTP adapter
625
+ src/research/ web_research pipeline, prompt, render, tool
626
+ src/host/ authenticated settings routes
627
+ src/client/ Settings → Search Tools + research card
628
+ src/shared/ config, validation, diff, URL, limits
629
+ tests/ host units + settings/research card component tests
630
+ docs/ architecture, design, M0 verification, release checklist
631
+ ```
632
+
633
+ ### Why linkedom instead of jsdom
634
+
635
+ Readability runs inside the Host. `jsdom`'s `tr46` dependency calls `createRequire(...).resolve.paths`, which DSH's profile-resolution hook does not implement. `linkedom` is smaller and works inside the Host. `jsdom` remains a dev-only dependency for component tests.
636
+
637
+ ## Release process
638
+
639
+ Before releasing:
640
+
641
+ - `package.json` version is `0.1.1`;
642
+ - `CHANGELOG.md` has the matching entry;
643
+ - `pnpm release` passes;
644
+ - the worktree is clean;
645
+ - npm auth works, or the repository has an `NPM_TOKEN` secret.
646
+
647
+ Local check:
648
+
649
+ ```sh
650
+ pnpm install --frozen-lockfile --registry=https://registry.npmjs.org
651
+ pnpm release
652
+ pnpm pack --pack-destination dist
653
+ ```
654
+
655
+ Tag and push:
656
+
657
+ ```sh
658
+ git tag -a v0.1.1 -m "dsh-search-tools v0.1.1"
659
+ git push origin main --follow-tags
660
+ ```
661
+
662
+ Pushing a `v*` tag triggers `.github/workflows/release.yml`:
663
+
664
+ 1. re-runs `pnpm release`;
665
+ 2. checks whether the version is already published and skips if so;
666
+ 3. publishes to npm with provenance using `NPM_TOKEN`;
667
+ 4. attaches the tarball to the GitHub Release.
668
+
669
+ Manual publish:
670
+
671
+ ```sh
672
+ npm publish --provenance --access public
673
+ ```
674
+
675
+ After publishing, smoke-test a fresh profile: install, enable, configure SearXNG, run search and fetch tests, and make one `web_research` call. See the [v0.1.1 release checklist](https://github.com/ThzxxArt/dsh-search-tools/blob/main/docs/release-v0.1.1.md).
676
+
677
+ ## Troubleshooting
678
+
679
+ | Symptom | Likely cause | Fix |
680
+ |---|---|---|
681
+ | No "Search Tools" settings section | Bundle disabled; DSH version mismatch; client load failure | Enable it on the Plugins page; confirm DSH is `>=0.2.1-rc.2 <0.3.0`; check browser console/plugin logs |
682
+ | Search test 401/403 | Instance needs auth; credential not saved; Basic format is not `username:password` | Choose the right auth mode, save the secret, retry; confirm `Source: env` is expected |
683
+ | Search test 429 | Instance rate limit | Slow down, switch instance, raise cache TTL, or use another backend |
684
+ | Test passes but returns 0 results | SearXNG default engines are rate-limited/CAPTCHA | Check **Search notes**; set engines (for example `bing`) or switch instance |
685
+ | JSON parse failure | Instance does not serve JSON | Add `json` to SearXNG `search.formats`, or set format to `html` |
686
+ | `web_research is disabled` | Research tool off | Settings → Research → enable `web_research`, save |
687
+ | Research returns sources without content | `fetch_top=0`; fetch failed; page not readable | Raise `fetch_top`; inspect fetch `error`; test the URL on the Fetch tab |
688
+ | Private URL fetch fails | SSRF protection | Expected; do not try to bypass |
689
+ | Built-in search breaks after takeover | Bundle disabled/removed while takeover still points at it | Re-enable the plugin or disable takeover; the settings/plugin detail page has recovery actions |
690
+ | Save conflict | Config changed elsewhere or overridden by a higher layer | Click **Reload**; inspect the profile's `cordis.patch.yml` and overlays |
691
+ | Config is read-only | Higher layer overrides it | The page explains; edit the overriding layer or remove it |
692
+ | Duplicate instance URL | Same URL after normalization | Remove the duplicate; trailing `/search`, query, and hash are stripped |
693
+ | Proxy validation error | Not `http(s)`, or contains credentials | Use `http(s)://host:port` without username/password |
694
+ | Cache results are stale | TTL not expired | Clear cache on Advanced, or wait for TTL |
695
+ | No `web_research` after install | Tool disabled; model has not refreshed tools | Enable and save; start a new session or reload |
696
+ | npm install fails | Registry/network/peer mismatch | Check DSH/Node versions and npm registry; use the offline tarball |
697
+
698
+ Suggested diagnosis order:
699
+
700
+ 1. **Settings → Advanced → Recent errors**;
701
+ 2. **Search notes** (engine-level errors);
702
+ 3. reproduce with **Test**, not only a model turn;
703
+ 4. confirm takeover state and the active provider;
704
+ 5. then inspect DSH and instance logs.
705
+
706
+ ## Known limitations
707
+
708
+ - **One search adapter and one fetch adapter are built in.** The architecture supports more; 0.1.x does not ship Google/Bing/Exa or other commercial backends.
709
+ - **No auto-update.** Upgrade by removing and reinstalling.
710
+ - **The external adapter contract is experimental.** It may change in 0.1.x; pin versions for production integrations.
711
+ - **No browser rendering.** No JavaScript execution, login state, SPA, paywall, or CAPTCHA handling.
712
+ - **Text/HTML only.** PDFs, images, audio, and video are not converted to article text.
713
+ - **`web_research` is not a crawler.** It fetches the top N results only; no site traversal, graph deduplication, or deep crawling.
714
+ - **The research prompt preview is read-only.** No copy button; the tool description stays English to match what the model sees.
715
+ - **The research timeout field is not wired to tool registration.** `research.timeoutMs` currently appears only in the prompt budget preview; the actual `web_research` registration timeout is fixed at `120000` ms. Deployments that need a different timeout must wait for a later release or patch the code.
716
+ - **Takeover is a global provider switch.** It affects every `ctx.web` consumer in the DSH profile, not only this plugin.
717
+ - **Cache is in-process memory.** It is lost on restart and not shared across processes.
718
+ - **Narrow DSH preview compatibility.** Pinned to the `0.2.1-rc.2` line; run a regression before upgrading DSH.
719
+
720
+ ## License and third-party notices
721
+
722
+ MIT. See [LICENSE](https://github.com/ThzxxArt/dsh-search-tools/blob/main/LICENSE).
723
+
724
+ Third-party components and licenses are listed in [THIRD_PARTY_NOTICES.md](https://github.com/ThzxxArt/dsh-search-tools/blob/main/THIRD_PARTY_NOTICES.md). Main runtime dependencies include `@mozilla/readability`, `linkedom`, `cheerio`, `turndown`, `@joplin/turndown-plugin-gfm`, `undici`, `lru-cache`, and `normalize-url`.
725
+
726
+ ---
727
+
728
+ If this plugin helps you, open an issue or discussion on GitHub. For security reports, do not post tokens, instance passwords, or full configuration publicly.