← Back to home@aooyoo

dsh-web-search-ddg

Zero-token DuckDuckGo search provider for the DeepSeek Harness (DSH) web seam — local headless browser, no API key, no model billing

Stars
1
Language
JavaScript
Created
Aug 16, 2026
Updated
Aug 26, 2026

Introduction

dsh-web-search-ddg

中文 | English

Zero-token web search provider for the DeepSeek Harness (DSH) web capability seam (ctx.web).

DSH's shipped search route (deepseek-official) performs every web_search as a full billed model round trip on deepseek-v4-flash — even when your session model is something else entirely. This plugin replaces that with two zero-cost engines tried in order, first success wins:

  1. Bing — plain fetch against Bing's HTML endpoint (with a cookie bootstrap). No browser needed, sub-second when healthy.
  2. DuckDuckGo — drives your local Chrome/Edge/Chromium headless against DuckDuckGo's HTML endpoint and parses the dumped DOM.
  • Zero model tokens per search — no API key, no auxiliary model request
  • Zero dependencies — Node builtins only; no Playwright/Puppeteer download
  • Engine fallback — if one engine is blocked or its markup changes, the other answers; a failure is reported only when every engine misses
  • Keeps the shipped provider registered — switching back is a one-line config change, not an uninstall

Requirements

  • A DSH host (≥ 0.1.0-rc) providing the ctx.web seam
  • For the DuckDuckGo engine: a local Chromium-family browser, detected automatically on macOS (Chrome, Edge, Chromium) and Linux (/usr/bin/chromium, /usr/bin/google-chrome); override with chromePath. The Bing engine needs no browser.
  • Node.js ≥ 18 (AbortSignal.any used for fetch timeouts when available)

Install

In your DSH profile directory (e.g. ~/.dsh/profiles/web), install the package as an out-of-tree plugin:

pnpm add dsh-web-search-ddg

Then edit the profile's cordis.patch.yml to mount it and make it the default search provider. Note that a patch row replaces the target row's whole config (no deep merge), so the web row must restate every key — the shipped row owns only searchProvider:

# Select this provider for the model-facing web_search tool.
- id: web
  config:
    searchProvider: ddg-browser

# Mount the plugin (registers provider id `ddg-browser`).
- insert:
    - id: web-search-ddg
      name: dsh-web-search-ddg

The shipped web-search-deepseek row stays untouched: its provider remains registered and available, so switching back is one line (searchProvider: deepseek-official). DSH's selection is a single explicit id, not a priority chain — there is no silent fallback by design.

Verify the composed tree without starting the host:

dsh --profile web --dump-config | grep -A2 searchProvider

Restart the host to apply (host-side plugin rows do not hot-reload).

Configuration

All keys optional; the row config goes to the insert entry above.

KeyDefaultMeaning
engines["bing", "duckduckgo"]Engine execution order. Supported: "bing", "duckduckgo". First success answers.
chromePathfirst detected browserAbsolute path to a Chromium-family executable (DuckDuckGo engine only).
timeoutMs20000Per-attempt budget for the DuckDuckGo engine. On timeout, buffered DOM output still counts as success (Chrome's --dump-dom process often lingers after printing). Two attempts run per search; keep 2 × timeoutMs under tool-web's searchTimeoutMs (DSH ships 60s).
virtualTimeBudgetMs8000Chrome's --virtual-time-budget — how long the page may settle before the DOM is dumped.

Behavior notes

  • Latency: the Bing engine typically answers in well under a second; the DuckDuckGo engine takes ~10–20s (the DOM dump is ready quickly, but Chrome often fails to exit on its own, so results land at the timeout guard — buffered output is still accepted).
  • Bing quality needs cookies. Without a bootstrapped cookie jar Bing serves degraded results that ignore most of the query. The plugin visits the Bing homepage once per session and replays those cookies; if results come back empty it re-bootstraps once.
  • Search engines rate-limit aggressive IPs. Heavy automated searching from one machine can earn connection resets or challenge pages from every engine at once. When that happens the plugin fails with an aggregated message naming each engine's failure — switch networks or wait for the flag to decay (hours), or switch searchProvider back to deepseek-official. It never silently degrades.
  • The headless UA is overridden with a plain desktop Chrome UA — DuckDuckGo keys on the HeadlessChrome marker and blocks it otherwise.
  • Each browser attempt runs in a throwaway --user-data-dir under the OS temp dir, cleaned up best-effort after the process dies.

How it works

Bing engine — fetch the SERP with desktop-Chrome headers and bootstrapped cookies; parse <li class="b_algo"> blocks for title + snippet; unwrap result links (/ck/a?…&u=a1<base64url>) into real target URLs.

DuckDuckGo engine — spawn the browser headless (--headless --dump-dom --virtual-time-budget, UA overridden) against https://html.duckduckgo.com/html/?q=<query>; read the serialized DOM with a kill guard (buffered output at timeout still counts); parse <a class="result__a"> titles and <a class="result__snippet"> snippets, decoding the real URL from each redirect's uddg parameter and pairing on it.

Both engines return { sources: [{ url, title?, snippet? }], truncated: false } through the seam — the model-facing web_search tool and result cards work unchanged.

Troubleshooting

ddg-browser: all search engines failed (…) names every engine's last error:

Message fragmentMeaningRemedy
connection reset / rate-limitedYour IP is temporarily flagged by the engineSwitch networks (e.g. phone hotspot) or wait hours; flags decay
anomaly/challenge pageDuckDuckGo anti-bot challenge servedSame as above; also check the UA string still matches a current Chrome
no parsable results/linksThe engine's markup changedUpdate this plugin, or file an issue
browser did not finish within NmsBrowser hung without producing DOMRaise timeoutMs; check the browser launches at all

Development

npm test          # end-to-end: registers on a stub ctx.web and runs one real search
CHROME_PATH=/path/to/browser npm test
TEST_QUERY="something else" npm test

License

MIT