dsh-web-search
为 ctx.web 接入多家 web_search / web_fetch 后端(Tavily、Firecrawl,可扩展),不绑定任何 LLM 供应商
cbalaa
@cbalaa
⬇ 2
★ 1
main
安装
dsh plugin --profile web add github:cbalaa/dsh-web-search
需要可复现安装时,可在仓库后追加 #commit 固定提交。
为 ctx.web 接入多家 web_search / web_fetch 后端(Tavily、Firecrawl,可扩展),不绑定任何 LLM 供应商
该插件未提供要点说明,请参考仓库 README。
- 安装并启动 DeepSeek Harness:
npx @deepseek-ai/dsh web - 在终端执行上面的安装命令(CLI 会解析插件并核验来源)
- 用 dsh plugins list 确认已安装,必要时重启 Harness 生效
插件以当前 dsh 进程的权限运行,安装时可能执行代码。请先通读仓库源码与许可证,确认无破坏性命令与越权访问;本站只做索引,不对第三方插件安全性作担保。
| 代码仓库 | github.com/cbalaa/dsh-web-search |
| 许可证 | MIT |
| 主要语言 | main |
| 下载量 | 2 |
| GitHub 星标 | 1 |
| 最近推送 | 2026-09-07 |
| 收录日期 | 2026-09-19 |
| 分类 | 工具与能力 |
事实信息来自公开插件目录快照(2026-10-01),介绍文案由本站再加工。
以下为插件仓库 README 全文(原始内容,由公开目录抓取整理)。
# dsh-web-search
DSH web plugin: **multi-vendor `web_search` / `web_fetch` backends** for the
`ctx.web` seam. Works with any LLM provider — you no longer need DeepSeek's
native search for `web_search` to work.
| Vendor | Config name | Search provider | Fetch provider | Key lookup (in order) |
| --------- | ----------- | -------------------- | ------------------------------ | ---------------------------------------------------------------------------- |
| Tavily | `tavily` | `POST /search` | `POST /extract` (clean text) | `apiKey` → credentials service(`apiKeyEnv`) → `$TAVILY_API_KEY` → Tavily CLI config file |
| Firecrawl | `firecrawl` | `POST /v2/search` | `POST /v2/scrape` (markdown) | `apiKey` → credentials service(`apiKeyEnv`) → `$FIRECRAWL_API_KEY` |
| *yours* | — | see [Adding a vendor](#adding-a-vendor) | — | — |
The plugin registers under the **fixed** ids `dsh-web-search` (search) and
`dsh-web-fetch` (fetch), so switching vendors never touches the web-seam row —
one config line does it.
## Install
Published to npm — install straight from the DSH CLI:
```sh
# latest (or pin a version: @balababa/dsh-web-search@0.2.0)
dsh plugin --profile web add @balababa/dsh-web-search
```
`dsh plugin add` forwards its argument to `pnpm add` inside the profile
directory, then reconciles `dsh.profile.bundles`: because this package declares
`dsh.bundle.patch`, `dsh-web-search` is appended to the bundle stack
automatically — no manual `cordis.patch.yml` edit to mount it.
Restart the web profile to take effect (`dsh web`, or `dsh --profile web`).
> **Prerequisites** — Node.js >= 20. The plugin's only runtime npm dependency is
> `@deepseek-ai/schemastery`. The `@deepseek-ai/dsh-web`, `@deepseek-ai/dsh-settings`,
> and `@deepseek-ai/dsh-credentials` peers — plus `react` / `react-dom` /
> `@deepseek-ai/cordis` — are provided by the DSH web runtime, no separate
> install. The settings and credentials peers are optional: without them the
> plugin still works composition-only / env-only.
### From a local checkout (development)
Run these from the checkout root:
```sh
npm install # tsdown / react / playwright-core (dev) + schemastery (runtime)
npm run build # bundles src/client/settings-card.tsx → lib/client.js
dsh plugin --profile web add file:.
```
The repo ships a pre-built `lib/client.js`, so installation works without a
build; but **after editing `src/client/`, re-run `npm run build`** or the
settings card will still serve the old bundle. The `file:.` spec is anchored
to your invoking directory, so run it from inside the checkout.
### Verify
```sh
dsh web --port 9000
```
After restarting, the plugin is live: open **Settings → Plugins → Plugin
configuration** to see the "Web search" card, or run a `web_search` /
`web_fetch` to confirm. The bundle patch mounts the plugin and points
`searchProvider` at it (`fetchProvider` stays on the built-in anonymous `http`
provider until you opt in — free, no vendor credits).
## Configure (two layers)
Configuration resolves through two layers, **both hot-applied**:
1. **Composition** — the plugin row's cordis config in your profile's
`cordis.patch.yml`, applied at boot.
2. **Settings** (optional) — the `web-search:` section of the settings
document (`settings.yaml`), layered **over** the composition config
(`schema defaults < composition < user document`; user edits win). Editing
the document — or saving the **Settings → Plugins → Plugin configuration →
Web search** card — takes effect **without a restart**: vendor switches
dispose/re-register the fixed-id providers, and key/endpoint/parameter
edits reach the very next request.
```yaml
# your profile's cordis.patch.yml
- id: web-search
config:
search: tavily # vendor for web_search
fetch: tavily # vendor for web_fetch ("off" = none)
providers:
tavily: {} # key from credentials / $TAVILY_API_KEY / Tavily CLI config file
# firecrawl:
# apiKey: 'fc-...' # or store/export FIRECRAWL_API_KEY
- id: web
config:
searchProvider: dsh-web-search
fetchProvider: dsh-web-fetch # or keep the built-in "http"
```
The equivalent in the settings document (`settings.yaml`), saved hot:
```yaml
web-search:
search: firecrawl # switches web_search to Firecrawl immediately
providers:
firecrawl:
maxResults: 10
```
Switching search vendor = change `search:` and save. Done.
> **`fetch` migration**: the composition config used to mean "omit `fetch:` =
> no plugin fetch provider". The schema now spells that case explicitly as
> `"off"`. A profile patch that sets `fetch: tavily` keeps working unchanged;
> a user document that wrote an empty `fetch:` should now read `off`.
### The settings card
When a settings provider is mounted, the plugin registers the `web-search`
namespace and contributes a card to **Settings → Plugins → Plugin
configuration**. The card edits vendor selection and per-vendor options, plus
**capability-scoped API keys**: a **Search API key** (always shown, for the
search vendor) and a **Fetch API key** (shown only when the fetch vendor
differs from the search vendor — a shared vendor needs one key). Keys are
written through the **credentials** domain (never into `settings.yaml` or
hardcoded in the plugin), addressed by each vendor's `apiKeyEnv` reference,
and show a configured/unconfigured badge. A blank key box leaves the stored
key untouched.
> The card appears only while a settings service is running. Without one, the
> plugin still works exactly as composed (`cordis.patch.yml`), and edits to
> the settings document (`settings.yaml`) simply have no effect on it.
### Per-vendor options
Common to every vendor (all optional):
| Field | Meaning |
| ------------- | ------------------------------------------------------------------ |
| `apiKey` | inline key (prefer credentials/env/files for secrets) |
| `apiKeyEnv` | credential reference / env var name to read, per-vendor default |
| `baseURL` | endpoint root override |
| `transport` | `"auto"` (default), `"curl"`, or `"fetch"` — `auto` picks curl when a `*_proxy` env var is set (Node fetch ignores proxy env unless `NODE_USE_ENV_PROXY=1`) |
| `timeoutSec` | per-request budget, default 30 |
Tavily adds `maxResults` (default 8), `searchDepth` (`basic`|`advanced`),
`includeAnswer` (default `true`). Firecrawl adds `limit` (default result count).
> Keys are resolved **per request**, so rotating a key in the credentials
> service, the env var, or the Tavily CLI config file needs no restart. A
> request with no key anywhere fails with a readable
> `WEB_PROVIDER_CREDENTIAL_MISSING`. The curl transport passes requests via
> `-K -` stdin config: keys never appear in the process argv.
## Adding a vendor
1. Create `lib/providers/.js`:
```js
export const name = "myvendor";
export function create(configOrThunk, h) {
const readConfig = () => (typeof configOrThunk === "function" ? configOrThunk() : configOrThunk) ?? {};
const options = () => ({
apiKey: h.firstNonBlank(readConfig().apiKey) ?? h.envValue("MYVENDOR_API_KEY") ?? "",
baseURL: readConfig().baseURL ?? "https://api.myvendor.example",
});
const search = {
id: "myvendor",
available: () => options().apiKey.length > 0 && URL.canParse(options().baseURL),
async search(request, signal) {
const o = options();
const { status, bodyText } = await h.postJson(
`${o.baseURL}/search`,
{ authorization: `Bearer ${o.apiKey}` },
{ q: request.query, limit: request.maxResults },
{ transport: readConfig().transport, timeoutSec: readConfig().timeoutSec, signal },
);
if (status < 200 || status >= 300) throw h.errors.providerError(`myvendor HTTP ${status}`);
const data = h.parseJson(bodyText) ?? {};
// → { content?, sources: [{url, title?, snippet?, publishedAt?}], truncated: false }
return { sources: data.results ?? [], truncated: false };
},
};
// Optional fetch provider: { id, available(), fetch(request, signal) }
// → { url, statusCode, body: { kind: "text", content }, truncated }
return { search };
}
```
2. Register one line in `lib/registry.js` (`import` + array entry).
3. Add a `VENDOR_SCHEMAS` entry in `lib/config-schema.js` (built with
`vendorSchema()` + per-vendor extras) so the settings namespace validates
and the card can render it.
4. Add a row to the README vendor table.
That's the whole contract — helpers (`postJson`, `envValue`, `readHomeJson`,
`firstNonBlank`, `cleanSnippet`, `parseJson`, `positiveInteger`, `errors`,
`resolveCredential`) are injected, so vendor modules need no harness imports
and share the proxy-aware transport and the credentials-service key chain.
## Development & tests
```sh
npm test # node:test unit suite (mock ctx.web + settings service)
node tests/integration.mjs # real cordis + dsh-web + file settings hot-switch
node tests/verify-client.mjs # loads lib/client.js through a stub module table
```
## Notes
- Fully independent of your conversation LLM provider — custom
OpenAI-compatible gateways work out of the box.
- `web_fetch` stays on the built-in anonymous `http` provider unless you set
`fetch:` + `fetchProvider: dsh-web-fetch`; switch when you want JS rendering
/ PDF parsing (Firecrawl scrape) or clean article text (Tavily extract).
- Errors surface as typed `WebError`s (`WEB_PROVIDER_ERROR` /
`WEB_PROVIDER_CREDENTIAL_MISSING` / `WEB_ABORTED`) when
`@deepseek-ai/dsh-web` is importable, plain coded Errors otherwise.
数据来源:公开的 DeepSeek Harness 插件目录与各插件 GitHub 仓库。本站为独立第三方目录,与 DeepSeek、幻方(High-Flyer)及插件作者均无隶属或背书关系。