sunx16963-design/dsh-web-search-thirdparty

dsh-web-search-thirdparty

A configurable third-party web-search plugin for DSH (DeepSeek Harness)/一个用于DSH(DeepSeek Harness)的可配置第三方网络搜索插件

Uncategorizeddeepseekdshsearch

dsh-web-search-thirdparty

English | 简体中文

A third-party web search plugin for DSH (DeepSeek Harness). It replaces the built-in DeepSeek-only search with configurable search engines, gives each engine its own settings, and can fetch full pages so the model can both search and read.

Built for DeepSeek Harness.

Version compatibility

Supports the latest DSH, and stays compatible with the older settings API (from 0.1.0-rc.6):

DSH settings serviceHow to recognise itWhat this plugin does
≥ 0.1.7-alpha.1dsh-settings exports SettingsForms; the settings document serves each profile entry's own Config by entry id and projects only .volatile() fieldsevery editable Config field is .volatile(); the client uses ctx.configForms.get('web-search-thirdparty'); the namespace is the composed entry id
0.1.2 – 0.1.6-alpha.2exports SettingsProvider with installSectionConfig is not volatile; the client uses ctx.settingsScope.bind({ namespace: 'dsh-web-search-thirdparty' })
≤ 0.1.1-rc.2top-level installSettingsSection helpersame as above, registration goes through the helper

The generation is detected at runtime (settingsGeneration()) and both paths are covered by tests. Note: if your DSH is ≥ 0.1.7 while @deepseek-ai/schemastery is below 3.18.3 (.volatile() only exists from 3.18.3), the plugin logs an explicit warning and the settings page cannot save.

Troubleshooting: the API key looks “cleared” after saving

Check the runtime status line under the key input (served by GET /api/web-search-thirdparty/config):

  • “Runtime status: key usable ✓” — the save worked. Secrets are never echoed back, so the input box being empty is intentional; leaving it blank means “keep the current value”.
  • “Not configured” — the key never reached the host. Check, in order: a schemastery version too old warning in the log; whether the plugin is actually listed in the profile's dsh.profile.bundles; and whether dsh web was restarted as the installer requires.

Requirements

  • DSH (DeepSeek Harness) 0.1.0-rc.6 or later, with a working dsh web profile.
  • Node.js >= 22.19 (same as DSH itself; building from source needs Node ^22.18 or >=24.11 for build:client).
  • Network access to at least one configured search engine / API.

What it does

By default DSH mounts only the official DeepSeek search provider, mounts no fetch provider, and keeps web_fetch disabled (upstream's rationale: the model picks the request target, and SSRF protection is the fetch provider's job). This plugin lets you use the search source you actually want — a self-hosted SearXNG, Tavily, Bing, Brave, Serper, or Google — and ships an SSRF-guarded fetch provider, so web_search and web_fetch are both enabled. Everything is configured from the browser settings page.

Upstream also ships optional official providers: @deepseek-ai/dsh-web-search-exa, @deepseek-ai/dsh-web-search-perplexity, and @deepseek-ai/dsh-web-fetch-http. If Exa/Perplexity is all you need, use those. This plugin's difference is "several engines (including keyless SearXNG) + per-engine settings + self-hosted instance / internal proxy support + circuit breaker and usage stats".

Features

  • Six built-in engines behind one facade: SearXNG, Tavily, Serper, Brave, Bing, Google CSE.
  • A browser settings page: choose the provider, enter the API key, and tune per-engine options.
  • A custom endpoint / baseURL per provider (self-hosted SearXNG, internal proxy).
  • Custom request headers and retry with backoff (429 / 5xx only, honouring Retry-After).
  • Automatic fallback when a provider fails, plus optional multi-source merge with deduplication.
  • Domain deduplication, relevance sorting, result-count control, and an honest truncated flag.
  • A TTL result cache (stampede guard, bounded size, hit-rate counters).
  • A "test connection" action that reports latency, result count, and the first title.
  • A web_fetch provider (HTML cleaned to Markdown, every redirect hop re-checked against SSRF).
  • An open provider-registration API so other plugins can add their own search source.
  • Per-source circuit breaker and usage statistics (settings page panel and GET /api/web-search-thirdparty/stats, including cache counters and circuit state).
  • Runtime configuration self-check: GET /api/web-search-thirdparty/config reports, per engine, whether it is actually usable right now (through the same credential chain real searches use); the settings page renders it as the runtime status line.

Supported engines

idserviceAPI keyconfig
searxng (default)SearXNG (self-hosted or public)nosearxngBaseURL
tavilyTavilyyestavilyApiKey / TAVILY_API_KEY
serperSerper (Google SERP)yesserperApiKey / SERPER_API_KEY
braveBrave SearchyesbraveApiKey / BRAVE_API_KEY
bingBing Web SearchyesbingApiKey / BING_SEARCH_API_KEY
google-cseGoogle Custom Searchyes (key + cx)googleApiKey + googleSearchEngineId

Note 1: Microsoft retired the Bing Web Search API in 2025. The bing engine is kept for endpoints that stay protocol-compatible (Azure Grounding gateways or self-hosted mirrors) via bingEndpoint.

Note 2: the default https://searx.be is only a public-instance example. Public instances commonly disable JSON output or run bot detection — a self-hosted SearXNG is recommended (add json to search.formats in its settings.yml) with searxngBaseURL pointed at it.

Keys can be set in the settings UI, stored in the DSH credentials service, or exported as environment variables; resolution order is literal → credentials service → launch environment.

Install

Installing through the plugin-manager CLI (dshpm) is recommended: it runs a quality gate and health checks (full dependency scan, bundle patch validation, rollback on failure). dshpm comes from dsh-web-plugin-manager — install the manager first if you have not.

# Option 1: from npm
dshpm install dsh-web-search-thirdparty --profile web
# Option 2: from GitHub (lib/ is committed, no local build needed)
dshpm install github:sunx16963-design/dsh-web-search-thirdparty --profile web
# Option 3: build locally (copy the build INTO the profile first — see the warning below)
git clone https://github.com/sunx16963-design/dsh-web-search-thirdparty.git
cd dsh-web-search-thirdparty
npm install
npm run build
# Crucial: the target must live inside the profile, or the link cannot reach the host packages
cp -r . "$DSH_HOME/profiles/web/vendor/dsh-web-search-thirdparty"
dshpm install "$DSH_HOME/profiles/web/vendor/dsh-web-search-thirdparty" --profile web

Restart dsh web after installing so the settings page appears. Upgrade / uninstall:

dshpm update dsh-web-search-thirdparty --profile web
dshpm remove dsh-web-search-thirdparty --profile web

Two install gotchas

1) git / local sources make the quality gate pause and ask for keys — just answer empty. This plugin needs no keys at install or build time (keys are entered in the settings page). But the manager scans READMEs for XXX_API_KEY / xxxApiKey-shaped names (capped at 8, which is why exactly tavily/serper/brave/bing show up), treats them as install-time env requirements, and pauses. Re-run with --env KEY= (empty = skip), e.g.:

dshpm install github:sunx16963-design/dsh-web-search-thirdparty --profile web \
  --env tavilyApiKey= --env TAVILY_API_KEY= --env serperApiKey= --env SERPER_API_KEY= \
  --env braveApiKey= --env BRAVE_API_KEY= --env bingApiKey= --env BING_SEARCH_API_KEY=

2) A local path outside the profile directory breaks the whole profile at boot. dshpm install /abs/path/outside records link:/that/path; the plugin resolves @deepseek-ai/* from its own location (the host provides them through profiles/node_modules), so an out-of-profile target cannot reach them and boot fails with failed to import loader entry … Cannot find package '@deepseek-ai/cordis' — the profile will not start. Copy the build into profiles/<name>/vendor/ before installing (as option 3 does). Also note: when the package name is published on npm, a github: source is mapped to the npm version (the log says "installed from npm"), so a stale npm release wins over the repo; and a .tgz path is not supported by this manager version.

Configuration

What you save in the settings page lands in the current profile's patch document. The namespace depends on the DSH generation:

  • DSH ≥ 0.1.7: the namespace is the composed entry id, i.e. web-search-thirdparty.
  • DSH ≤ 0.1.6: the namespace is the plugin's registered namespace, i.e. dsh-web-search-thirdparty (written to settings.yaml).
web-search-thirdparty:          # dsh-web-search-thirdparty on ≤ 0.1.6
  provider: searxng
  searxngBaseURL: https://searx.be   # or your own instance
  maxResults: 8
  mergeResults: false
  maxPerDomain: 2
  relevanceSort: false
  cacheEnabled: true
  cacheTtlMs: 60000
  retryCount: 1
  retryBackoffMs: 250
  # default true: register the bundled web_fetch provider.
  # Set false to hand web_fetch back to the host's provider
  # (also remove the `fetchProvider` line from this plugin's bundle patch).
  enableFetchProvider: true

On DSH ≥ 0.1.7 every configuration field is volatile(), so a settings-page write takes effect on the next search without a restart.

Config-file-only knobs

The settings form covers the common options (provider, keys/instance URL, result count, merge, per-domain cap, relevance sort, cache, and each engine's advanced params). These advanced options are not in the form and only take effect from settings.yaml / the composition layer (they are also included in the "restore defaults" reset list):

timeoutMs, snippetMaxLength, retryCount, retryBackoffMs, extraHeadersJson, fallbackProviders, maxProviderQueries, maxProviderConcurrency, circuitEnabled, circuitFailureLimit, circuitCooldownMs, statsEnabled, fetchAllowPrivate, fetchMaxBodyChars, fetchTimeoutMs, fetchUserAgent.

Security

  • web_fetch refuses private / loopback / link-local / cloud-metadata (169.254.169.254) targets and special-purpose ranges: IPv4 0.0.0.0/8, 100.64.0.0/10 (CGNAT), 192.0.0.0/24, 192.88.99.0/24, 198.18.0.0/15, 240.0.0.0/4; IPv6 ::1, fc00::/7, fe80::/10, ff00::/8, 2002::/16 (6to4), 2001::/32 (Teredo), 2001:db8::/32, 64:ff9b::/96 (NAT64), and every IPv4-mapped form.
  • Redirects are followed manually hop by hop, and every hop is re-validated (plain follow does not re-check Location).
  • A failed DNS resolution is treated as a refusal (inconsistent resolution is the rebinding entry point).
  • Set fetchAllowPrivate: true explicitly if you need to fetch from an internal network.
  • The "test connection" route also runs the SSRF check on a form-supplied SearXNG instance URL.
  • Known residual risk: a DNS re-resolution window remains between validation and the actual connection (the 0-TTL rebinding TOCTOU). Closing it requires pinning the validated IP at the connect layer (i.e. a custom undici dispatcher); this implementation deliberately adds no runtime dependency, so the window stays. Do not enable fetchAllowPrivate with untrusted input.

Developer

npm install
npm run build:host     # compiles the host plugin (lib/index.js)
npm run build:client   # bundles the browser UI (lib/client.js)
npm run typecheck
```sh
npm install
npm run build:host     # compiles the host plugin (lib/index.js)
npm run build:client   # bundles the browser UI (lib/client.js)
npm run typecheck
npm test
npm pack

Unit tests use the repo's own platform shims, so they cannot catch an upstream API removal. This script loads the build into a real cordis + dsh-web + dsh-settings-file stack and asserts: apply does not throw, the settings namespace registers, ctx.web.search() normalizes, and ctx.web.fetch() retrieves:

mkdir -p /tmp/dsh-e2e && cd /tmp/dsh-e2e
npm init -y >/dev/null && npm pkg set type=module
npm i --legacy-peer-deps @deepseek-ai/cordis@4 @deepseek-ai/dsh-web@0.1.5-rc.2 \
  @deepseek-ai/dsh-settings@0.1.5-rc.2 @deepseek-ai/dsh-settings-file@0.1.5-rc.2 \
  @deepseek-ai/dsh-credentials@0.1.5-rc.2 @deepseek-ai/dsh-launch-environment@0.1.5-rc.2 \
  @deepseek-ai/dsh-llm@0.1.5-rc.2 @deepseek-ai/schemastery@3
# peers are not installed with dependencies: add whatever @deepseek-ai/* package it reports
node ./scripts/e2e-real-host.mjs /tmp/dsh-e2e

Point it at 0.1.1-rc.2 and friends to exercise the legacy settings path (the script prints which generation the stack uses).

Sources are split by responsibility (src/index.ts keeps engines, facade, fetch provider, routes, apply):

filerole
src/engine-spec.tssingle engine table (providers, labels, credential inputs, endpoints, advanced params, test-route mapping, reset fields)
src/config.tsconfig shape and schemastery schema
src/text.ts, src/html.tsresult normalization, snippet cleaning, entity decoding, HTML→Markdown (pure functions)
src/net.tsSSRF validation and hop-by-hop redirects
src/state.tsresult cache, concurrency, circuit breaker, usage stats
src/settings-compat.tstwo-generation settings-registration compatibility layer

A fresh clone builds with public npm packages only: @deepseek-ai/* compile-time types come from the repo's ambient shims, while the runtime packages are provided by DSH (linked through the profile, so no second copy appears).

Adding an engine

Engine metadata lives in a single shared table, src/engine-spec.ts (it drives the provider list, labels, credential inputs, endpoints, advanced-params form, test-route mapping and reset fields on both host and browser sides). Adding an engine takes two steps:

  1. write the implementation in src/index.ts and add it to ENGINES;
  2. add a spec row in src/engine-spec.ts.

tests/engine-spec.test.ts cross-checks both directions to prevent drift.

Provider API

Other Cordis plugins can inject the web-search-thirdparty service to register their own search source (built-in engine ids are a reserved namespace — third parties should use their own id):

export const inject = ['web-search-thirdparty']

function apply(ctx) {
  ctx.get('web-search-thirdparty').register({
    id: 'my-source',
    label: 'My Source',
    search: async ({ query, maxResults, config }, signal) => ({
      sources: [{ url, title, snippet }],
      content: 'optional answer',
    }),
  })
}

License

BSD-3-Clause. See LICENSE.