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 技能)。
关键架构事实(源码确认)
- web seam 单选无回退:
packages/web/web/src/index.tsresolveProvider()——配置 id 不可用报WEB_PROVIDER_CONFIGURED_UNAVAILABLE;多个可用无配置报WEB_PROVIDER_AMBIGUOUS;0 可用报WEB_PROVIDER_UNAVAILABLE。无 fallback/retry 逻辑。 WebSearchProvider接口极简:{ id: string; available(): boolean; search(request, signal): Promise<WebSearchResult> }(web/src/types.ts)。- loader 支持相对路径:
vendor/loader/src/config/tree.tsimport(name, baseUrl)——baseUrl= profile 目录(app-boot/index.ts:769pathToFileURL(dirname(configPath)).href + '/')。patch 行name: './plugins/xxx/index.js'即可加载本地插件,无需 pnpm、无需进 node_modules。 - patch 插入语法:profile 的
cordis.patch.yml插入新行用- insert: [{id, name, config}];修改已有行用- id: xxx+ name + config(整体替换该行)。改已有行时若 id 不存在会 warn 但继续。 - 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 配置模板:
- 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 教训(端到端验证抓到)
- key resolver 调用方式不一致:
buildKeyResolver返回{sync, resolve}对象,但BaseProvider._key()按函数调用this._getApiKey(signal)→ TypeError。修:this._getApiKey.resolve(signal)。 available()语义错误:同步检查sync()(只看 process.env)→ key 在 credentials seam 里被判"不可用"→WEB_PROVIDER_CONFIGURED_UNAVAILABLE。修:available()只检查配置来源存在(configured():literal 或 apiKeyEnv 已声明),实际解析留给 search 时异步做——与官方 DeepSeek provider 语义一致(检查配置完备性而非值存在性)。- credentials 获取方式:
webCtx.credentials直接属性访问可能抛错;官方用ctx.get('credentials')安全获取(未注入返回 undefined),无 seam 时回退launchEnvironmentOf(ctx).get(apiKeyEnv)。
验证方法(先直测再 UI)
- 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:/...。 - UI 端到端:
dsh web起服务 → 浏览器打开http://127.0.0.1:3080→ 新会话 → 发"用 web_search 搜索 X" → 对话流出现Code/Search/Think工具行 → 点击 Search 行展开详情卡片(显示**搜索调用链**+ 每 provider 状态 +← 实际使用+ Sources 列表)。 - 重启服务:改插件代码后必须重启
dsh web(ESM 缓存旧代码);重启前确认 3080 端口释放(kill 后残留 node 进程会 EADDRINUSE,用netstat -ano | grep :3080查 pid + PowerShellStop-Process -Id <pid> -Force)。
Common Pitfalls
- patch 插入语法错误:
- id: web-search-fallback直接写 →patch: entry not found。新行必须包在- insert:数组里。 - 目录 import 失败:
name: './plugins/web-search-fallback'→ERR_UNSUPPORTED_DIR_IMPORT(Node ESM 不解析目录 main)。必须写./plugins/web-search-fallback/index.js显式文件路径。 - 默认模型无 key 混淆验证:
settings.yaml的agent-default-model若指向无 key 的 provider(如 deepseek-official),会话报 MISSING_CREDENTIAL——会误以为是搜索插件问题。排查时先看 settings.yaml 的 provider 指向,再看~/.dsh/.credentials.yaml有没有对应 key。 - 会话级模型覆盖默认:改
agent-default-model后旧会话仍用旧模型,需"新会话"按钮新建才生效。 - 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 含调用链标注(模型可判断来源可靠性)