# Dsh Web Search Fallback

> Use when 配置/排查 dsh 搜索 provider 或写自定义 web provider。

- Skill: `yyyyyhhhhh0639/dsh-web-search-fallback` (Agent Skill)
- Install (CLI): `npx skillmds@latest add yyyyyhhhhh0639/dsh-web-search-fallback`
- Raw SKILL.md: https://api.skillmd.com/api/skills/yyyyyhhhhh0639/dsh-web-search-fallback/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: yyyyyhhhhh0639 (https://skillmd.com/u/yyyyyhhhhh0639)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/yyyyyhhhhh0639/dsh-web-search-fallback

---


# dsh Web Search Fallback（自定义搜索回退链插件）

## Overview

DeepSeek Harness（dsh）的 web seam（`ctx.web`）原生**单选 provider 且不做回退**——`resolveProvider()` 只按配置 id 或唯一可用 provider 路由，失败直接抛 `WEB_PROVIDER_*` 错误。要"同时注册多个 provider + 出错自动回退"，唯一架构内路径是**自定义聚合 provider**（实现 `WebSearchProvider` 接口，内部按优先级持有多个子 provider）。

本技能记录已落地的插件（`~/.dsh/profiles/web/plugins/web-search-fallback/`）：安装机制、4 个 provider 实现、3 个实测 bug 教训、验证方法。

## When to Use

- 配置/排查 dsh 的网络搜索（`web_search` 工具报 provider 错误）
- 添加新搜索后端（Firecrawl/Tavily/Parallel/自定义 API）到回退链
- 理解 dsh 插件如何安装（无 pnpm 时的手动 patch 路径）
- dsh 报 `configured web provider "fallback" is registered but unavailable` 等错误

Don't use for: Hermes 自身的搜索配置（那是 web-ddgs 插件 + web-search 技能）。

## 关键架构事实（源码确认）

1. **web seam 单选无回退**：`packages/web/web/src/index.ts` `resolveProvider()`——配置 id 不可用报 `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`；多个可用无配置报 `WEB_PROVIDER_AMBIGUOUS`；0 可用报 `WEB_PROVIDER_UNAVAILABLE`。**无 fallback/retry 逻辑**。
2. **`WebSearchProvider` 接口极简**：`{ id: string; available(): boolean; search(request, signal): Promise<WebSearchResult> }`（`web/src/types.ts`）。
3. **loader 支持相对路径**：`vendor/loader/src/config/tree.ts` `import(name, baseUrl)`——`baseUrl` = profile 目录（`app-boot/index.ts:769` `pathToFileURL(dirname(configPath)).href + '/'`）。patch 行 `name: './plugins/xxx/index.js'` 即可加载本地插件，**无需 pnpm、无需进 node_modules**。
4. **patch 插入语法**：profile 的 `cordis.patch.yml` 插入新行用 `- insert: [{id, name, config}]`；修改已有行用 `- id: xxx` + name + config（整体替换该行）。改已有行时若 id 不存在会 warn 但继续。
5. **credentials seam**：key 存 `~/.dsh/.credentials.yaml`（`NAME: value` 格式，外部编辑会被观察）；插件里用 `ctx.get('credentials').resolve(ref)` **异步获取**（`.value` 字段），官方 provider 每次 search 重新解析（支持轮转）。**不是** `ctx.credentials` 直接属性访问（可能抛错）。

## 插件结构（已落地）

```
~/.dsh/profiles/web/
├── cordis.patch.yml                    # insert 插件行 + 改 web 行 searchProvider: fallback
└── plugins/web-search-fallback/
    ├── package.json                    # { type: "module", main: "index.js" }
    └── index.js                        # 全部实现（~370 行，零依赖，只用全局 fetch）
```

patch 配置模板：

```yaml
- insert:
    - id: web-search-fallback
      name: './plugins/web-search-fallback/index.js'
      config:
        chain:
          - id: exa
            apiKeyEnv: EXA_API_KEY
          - id: firecrawl
            apiKeyEnv: FIRECRAWL_API_KEY
          - id: tavily
            apiKeyEnv: TAVILY_API_KEY
          - id: parallel
            apiKeyEnv: PARALLEL_API_KEY
- id: web
  name: '@deepseek-ai/dsh-web'
  config:
    searchProvider: fallback
```

## 4 个 provider API 速查（实测可用）

| Provider | 端点 | 认证头 | 请求体关键字段 |
|---|---|---|---|
| Exa | `POST https://api.exa.ai/search` | `x-api-key` + `authorization: Bearer`（双头，官方同款） | `query`, `numResults`, `contents.highlights.highlightsPerUrl` |
| Firecrawl | `POST https://api.firecrawl.dev/v1/search` | `authorization: Bearer` | `query`, `limit`；响应 `data[]` |
| Tavily | `POST https://api.tavily.com/search` | `authorization: Bearer` | `query`, `max_results`；响应 `results[]` + `answer` |
| Parallel | `POST https://api.parallel.ai/alpha/search` | `x-api-key` | `objective`, `search_queries[]`, `max_results`, `max_chars_per_result`；响应 `results[]` |

## 调用链在 UI 的呈现机制

用户要求"对话流中像工具调用一样显示调用链"——实现路径（源码确认）：

```
fallback provider 返回 content（多行 markdown 调用链）
  → tool-web execute 透传 content（模型可见）
  → searchMetaFromValue() 把 content 映射为 meta.answer  (tool-web/src/search.ts)
  → presentSearchResult() 组装 card: 'web' 搜索卡片
  → client WebBlock 渲染：answer 区显示在引用列表上方
```

**限制**：npm 安装版无法注册独立 UI 条目（ConversationNodeDefinition 需 client 插件编译进 Web bundle，cookbook 明示）；工具卡片 answer 区是唯一可行呈现位。

## 3 个实测 bug 教训（端到端验证抓到）

1. **key resolver 调用方式不一致**：`buildKeyResolver` 返回 `{sync, resolve}` 对象，但 `BaseProvider._key()` 按函数调用 `this._getApiKey(signal)` → TypeError。修：`this._getApiKey.resolve(signal)`。
2. **`available()` 语义错误**：同步检查 `sync()`（只看 process.env）→ key 在 credentials seam 里被判"不可用"→ `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`。修：`available()` 只检查**配置来源存在**（`configured()`：literal 或 apiKeyEnv 已声明），实际解析留给 search 时异步做——与官方 DeepSeek provider 语义一致（检查配置完备性而非值存在性）。
3. **credentials 获取方式**：`webCtx.credentials` 直接属性访问可能抛错；官方用 `ctx.get('credentials')` 安全获取（未注入返回 undefined），无 seam 时回退 `launchEnvironmentOf(ctx).get(apiKeyEnv)`。

## 验证方法（先直测再 UI）

1. **Node 直测**（快速、无 UI）：`node -e` 或临时 .mjs 直接 import 插件模块（`file:///%USERPROFILE%/.dsh/profiles/web/plugins/web-search-fallback/index.js`），构造 `{sync: () => key, resolve: async () => key}` 伪 resolver，逐个调 `provider.search()` 验证 key/响应解析；再构造完整 chain 验证 fallback 顺序。注意 node 在 Windows 不认 MSYS `/c/` 路径，用 `C:\\Users\\...` 或 `file:///C:/...`。
2. **UI 端到端**：`dsh web` 起服务 → 浏览器打开 `http://127.0.0.1:3080` → 新会话 → 发"用 web_search 搜索 X" → 对话流出现 `Code`/`Search`/`Think` 工具行 → 点击 Search 行展开详情卡片（显示 `**搜索调用链**` + 每 provider 状态 + `← 实际使用` + Sources 列表）。
3. **重启服务**：改插件代码后必须重启 `dsh web`（ESM 缓存旧代码）；重启前确认 3080 端口释放（kill 后残留 node 进程会 EADDRINUSE，用 `netstat -ano | grep :3080` 查 pid + PowerShell `Stop-Process -Id <pid> -Force`）。

## Common Pitfalls

1. **patch 插入语法错误**：`- id: web-search-fallback` 直接写 → `patch: entry not found`。新行必须包在 `- insert:` 数组里。
2. **目录 import 失败**：`name: './plugins/web-search-fallback'` → `ERR_UNSUPPORTED_DIR_IMPORT`（Node ESM 不解析目录 main）。必须写 `./plugins/web-search-fallback/index.js` 显式文件路径。
3. **默认模型无 key 混淆验证**：`settings.yaml` 的 `agent-default-model` 若指向无 key 的 provider（如 deepseek-official），会话报 MISSING_CREDENTIAL——会误以为是搜索插件问题。排查时先看 settings.yaml 的 provider 指向，再看 `~/.dsh/.credentials.yaml` 有没有对应 key。
4. **会话级模型覆盖默认**：改 `agent-default-model` 后旧会话仍用旧模型，需"新会话"按钮新建才生效。
5. **exa 403 是真实回退场景**：EXA key 过期/风控时 exa 会 403——fallback 链应把 exa 放前面实测确认，403 会正确触发回退（当前环境实测如此）。

## Verification Checklist

- [ ] `dsh --profile web --dump-config` 能看到 `web-search-fallback` 插入行 + `web` 行 `searchProvider: fallback`
- [ ] `~/.dsh/.credentials.yaml` 含所有 chain 用到的 key 名
- [ ] Node 直测：每个子 provider 单独 search 成功或给出明确错误；完整 chain 按优先级回退
- [ ] UI 实测：对话流出现 Search 工具行，点击展开显示 `**搜索调用链**` 多行格式 + `← 实际使用` 标记
- [ ] 模型可见：搜索结果的 content 含调用链标注（模型可判断来源可靠性）

