dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
dsh-web-search — Multi-provider web search for DeepSeek Harness: routes the native web_search tool through a configurable provider fallback chain (Tavily/Brave/Exa/Firecrawl/Jina/Kagi/SearXNG/DuckDuckGo), replacing the built-in deepseek-official backend, with a settings page for key management and ordering. Approach adapted from OMP.
English · 简体中文
@ian_p/dsh-web-search is a Cordis plugin for the DeepSeek Harness that replaces the native web_search back end with a configurable, multi-provider chain. Each query walks the configured provider order and falls back to the next on failure or empty results; DuckDuckGo needs no key and closes the chain.
The browser half adds a dedicated Search providers settings page for keys, connection tests and drag-to-reorder.
Design reference: the multi-provider approach is adapted from Oh My Pi (OMP).
| Capability | What it changes |
|---|---|
| 8 providers, one chain | Tavily, Brave, Exa, Firecrawl, Jina, Kagi, SearXNG, DuckDuckGo — any order, any subset. |
Native web_search integration | The patch override routes the harness's own web_search tool through this chain. |
| Fail-loud | With the patch not applied the native tool reports WEB_PROVIDER_AMBIGUOUS instead of silently degrading. |
| Host-native plugin page | Localized (en/zh) through the host locale service and styled with the host's own control contract (--dsw-* design tokens), so it follows the host language and light/dark theme. |
| Auditable fallback | One host-log line per search names every provider that was skipped or failed, then the one that served. |
Node.js ^22.19 or >=24
DeepSeek Harness 0.2.0-rc.2 — this plugin hand-writes its Typert Remote descriptors, so it targets one DSH train:
npm install --global @deepseek-ai/dsh@0.2.0-rc.2
Every @deepseek-ai/dsh* piece it uses comes from that host build. The three the plugin imports or is gated on are declared as peers at the same train version (^0.2.0-rc.2): dsh-web (the ctx.web seam), dsh-typert-protocol (the Remote descriptors) and dsh-api-remotes (named in dsh.client.inject); credentials is reached through ctx.get('credentials') and is not declared. The components declared on their own:
| Component | Version | Role |
|---|---|---|
@deepseek-ai/dsh | 0.2.0-rc.2 | Host runtime (source of every @deepseek-ai/dsh* peer) |
@deepseek-ai/cordis | ~4.0.4 | Plugin/context framework (peer + dev, the range the train itself declares) |
react | ^18.2 | Browser half only (dev) |
typescript | ^7.0.2 | Type check over src/host-core.js (dev) |
From npm (the package declares dsh.bundle.patch, so dsh plugin add also activates the profile bundle):
dsh plugin --profile web add @ian_p/dsh-web-search
The command resolves the latest dist-tag, which currently points at the 0.2.0-rc.1 prerelease; pin it when you need a reproducible install (… add @ian_p/dsh-web-search@0.2.0-rc.1). A host still on DSH 0.1.5-rc.3 is not compatible with that version and needs @ian_p/dsh-web-search@0.1.5-rc.3 instead — the two trains are not interchangeable. Per-version notes live on GitHub Releases.
Local development — run inside the harness source workspace; the overlay loads src/index.js directly, so no build step is needed:
pnpm dsh web --patch /path/to/dsh-web-search/patch.web.yml
web is the launcher's alias for --profile web. The overlay (patch.web.yml) inserts the plugin row and sets searchProvider: dsh-web-search on the native web row; the browser half is discovered through the dsh.client manifest plus exports["./client"].
Linked checkout (dsh plugin --profile web add "link:/path/to/checkout") loads the published shape instead, i.e. dist/ — run pnpm run build after editing src/ (pnpm install also builds it through the prepare script). A host restart is required either way.
Order comes from the settings page (drag to reorder) and is stored in the dsh-web-search/config credential record. Resolution: your ordered, non-excluded providers first, then every remaining provider in built-in order.
For each provider, in order:
available() — a local check, no network call: keyed providers need a credential record, SearXNG needs its endpoint, DuckDuckGo is always available. Not available ⇒ skipped.search() with a per-provider timeout (default 60 s, timeout in the config record).If every provider fails, the error names the last provider tried, and the plugin then retries the native deepseek-official provider before giving up.
Each search logs one line to the host log:
[14:32:07] [dsh-web-search] firecrawl: HTTP 429: ... → tavily: served (8 sources, 4437ms)
[14:32:09] [dsh-web-search] trying exa
[14:33:01] [dsh-web-search] brave: not configured → ... → all providers failed
A trying line with no completion line after it means that provider is still in flight (the harness log itself is not timestamped, so the plugin stamps its own lines).
| ID | Label | Kind | How to activate |
|---|---|---|---|
tavily | Tavily | API key | Save a Tavily API key |
brave | Brave | API key | Save a Brave API key |
exa | Exa | API key | Save an Exa API key |
firecrawl | Firecrawl | API key | Save a Firecrawl API key |
jina | Jina | API key | Save a Jina API key |
kagi | Kagi | API key | Save a Kagi API key |
searxng | SearXNG | Endpoint | Save a SearXNG instance endpoint |
duckduckgo | DuckDuckGo | None | Always available (default final fallback) |
Per-provider details worth knowing:
results[].content is a page chunk, not a description, so the request asks for chunks_per_source: 1 (one relevant passage instead of three chunks of page chrome) and include_published_date: true.success: false plus a warning; that is surfaced as the provider's failure reason instead of looking like an empty result set.week recency maps to month because the instance only understands day/month/year.All provider secrets live in harness credential records under the dsh-web-search/ scope, managed from the settings page — no environment variables. (Environment-variable credential refs would shadow saved values and stop the page from working.)
API-key providers — an api-key record, e.g. dsh-web-search/tavily.
SearXNG — a grant record carrying the instance endpoint.
DuckDuckGo — no key.
Plugin config — a grant record at dsh-web-search/config:
order: [firecrawl, tavily, brave, exa, jina, kagi, searxng, duckduckgo]
exclude: []
timeout: 60 # seconds, per provider
The settings page writes order; exclude and timeout currently only via the record.
Registered as a plugin page on the plugins.item slot (id web-search-providers, order 12) inside the Plugins page, separate from the native web-search page (which keeps its own web-search page). It lists providers in effective fallback order and lets you save or clear a key/endpoint, run a connection test, and drag the cards to reorder the chain. It talks to the host over the plugin's websearch Remote namespace (list / setKey / unsetKey / setOrder / testProvider).
The page renders two views from one component: a one-line summary for the Plugins-page list card, and the full provider list on the detail page.
The page registers en / zh dictionaries with the host locale service (sidebar label included) and paints itself with --dsw-* tokens using the host's own button, input and card geometry, so both language and theme follow the host.
web_search results are shaped to match the native path:
url, title?, snippet?, publishedAt? only. Snippets are cleaned (markdown headings stripped, whitespace collapsed, chunk separators removed) and held to 150 characters — the documented ceiling of the native citation excerpt — with a cut marked by ….content, bounded to 400 characters, and rendered as Markdown above the source list. The native provider never sends one.truncated: false; the harness caps it to request.maxResults and sets truncated: true, which is what surfaces the "sources truncated" notice to both the user and the model.dsh-web-search/
├── patch.web.yml # --patch overlay for local development (relative ./src/index.js)
├── cordis.patch.yml # published bundle patch (package specifier), declared via dsh.bundle.patch
├── src/ # source of truth (never published)
│ ├── index.js # host entry: ctx.web provider, chain orchestration, credential RPC ops
│ ├── host-core.js # pure functions: query parsing, per-provider request building and
│ │ # response normalization, snippet/answer policy
│ ├── remote.js # websearch Remote namespace (WebSearchController)
│ └── client/bundle.js # browser half: hand-written __ModuleLoader__ factory bundle (no bundler):
│ # the settings page and its single-copy state machine
├── scripts/build.mjs # build: clean copy of src/ → dist/ (the publishable tree)
├── dist/ # build output — published to npm, git-ignored
├── tests/ # 140 pure + 19 environment-dependent tests (see below)
├── docs/DESIGN.md # host contracts this plugin depends on, and why the code is shaped this way
└── package.json # main/exports → dist/, files: ["dist/", …], prepare builds dist/
pnpm install # installs peers/dev deps and runs `prepare`, which builds dist/
pnpm run build # stage dist/ from src/ (clean copy, no bundler, no new dependency)
pnpm test # 140 pure-function tests (node:test, zero dependencies)
pnpm run test:rpc # 19 environment-dependent tests (resolves the 0.2.0-rc.2 peers)
pnpm run typecheck # tsc -p tsconfig.types.json (JSDoc types of src/host-core.js)
pnpm run prepublishOnly # build + both test tiers + typecheck, before a publish
| Tier | Suite | Count | What it covers |
|---|---|---|---|
| Pure | tests/manifest.test.mjs | 6 | single-train manifest guards: peer ranges, the dead dsh-tools declaration, no runtime version sniffing, unchanged bundle mechanisms |
| Pure | tests/host-core.test.mjs | 97 | query parsing, provider request bodies, response normalization, snippet/answer policy, credential-record helpers |
| Pure | tests/interaction.test.mjs | 37 | the settings page state machine, run against the shipped client bundle |
| Env | tests/remote-contract.test.mjs | 18 | the compatibility gate against the installed runtime, the Typert Remote contribution, and the client bundle's strict codecs on both registry faces |
| Env | tests/client-bundle.smoke.mjs | 1 | bundle registration, the plugins.item registration, apply(), and the injected stylesheet |
src/index.js (the host entry: transport, chain, credential ops, ctx.web injection) has no test coverage — it needs the harness runtime; docs/DESIGN.md records what that leaves unpinned.
MIT © ForeverYoungPp