dsh-web-search-keyless
DSH 宿主插件:给 ctx.web 换上一个不需要密钥、也不消耗模型回合的搜索 provider。
Bing RSS 为主,DuckDuckGo Lite 为备,命中失败自动顺延。
为什么
DSH 自带的 dsh-web-search-deepseek 走的是 "Anthropic Messages + 原生 web_search
服务端工具":DeepSeek 没有独立检索端点,所以每搜一次就是一次完整的模型回合,按
token 计费,而且每个 query 文本不同、前缀从第一个差异处断开,缓存几乎必然不命中。
实测一次会话里 93 个 query 就是 93 次官方 API 请求、93 次未命中缓存的输入。
真实检索本来不需要模型参与。本插件把这一层换成两个普通 HTTP GET,成本为零:
| 引擎 | 端点 | 输出 |
|---|
bing | https://www.bing.com/search?q=…&format=rss | RSS,<item> 里的 title / link / description / pubDate |
ddg | https://lite.duckduckgo.com/lite/?q=… | 表格 HTML,a.result-link + td.result-snippet + span.timestamp |
DDG 的 href 是 //duckduckgo.com/l/?uddg=<encoded> 形式的跳转壳,unwrapDdgHref
会解出真实地址。
安装
dsh plugin --profile web add github:s11phere/dsh-web-search-keyless
装完重启 dsh web(新增 bundle 不进热更新),刷新页面。卸载把 add 换成 remove,
同样要重启。
别用 scp 风格的 git@github.com:... —— pnpm 会把它误解析成「包名 git + 版本」,
装出一个叫 git 的空依赖,插件不会生效。用上面的 github:<owner>/<repo> 写法。
也可以 dsh plugin --profile web add <本地目录>,但本包不依赖任何 @deepseek-ai/*
裸导入(见下),所以跨盘、跨 profile 都不会出链接问题。
为什么没有任何 @deepseek-ai/* 导入
以 link: / github: 装进 profile 时,Node 按插件的真实路径解析裸导入,
@deepseek-ai/* 不在本包的祖先目录链上,因此 import { WebError } from '@deepseek-ai/dsh-web' 会直接 ERR_MODULE_NOT_FOUND(同 dsh-paper-reader 对
defineTool 的处理)。本包因此只依赖 node: 与自己的 ./backends.js:
lib/host.js 里带一个形状对齐 seam 契约的本地 WebError(message + code + cause)。
唯一代价是宿主 dsh-tools 只对 instanceof HarnessError 的错误暴露结构化错误码,
所以本 provider 的失败以文本形式到达模型 —— 消息里逐条列出了每个引擎的失败原因。
生效方式
cordis.patch.yml 随包发布,包含两条:注册 loader 入口,以及把
web.searchProvider 指到 keyless。放在包里是为了让安装保持原子——profile 开了
patchReload: live,若把 provider 选择单独写进 profile 配置,保存瞬间宿主就会去解析
一个尚未注册的 provider,重启前 web_search 会一直报 WEB_PROVIDER_CONFIGURED_MISSING。
profile 自己的 cordis.patch.yml 在 bundle 之后应用,随时可以覆盖回去:
- id: web
name: '@deepseek-ai/dsh-web'
config:
searchProvider: deepseek-official # 换回 DeepSeek 官方搜索
配置
| 字段 | 默认 | 含义 |
|---|
id | keyless | 注册到 seam 的 provider id,供 web.searchProvider 引用 |
engines | ['bing','ddg'] | 引擎尝试顺序;前一个抛错、返回零条或结果全部无关才换下一个 |
timeoutMs | 15000 | 单个引擎的超时 |
userAgent | 桌面 Chrome UA | Bing/DDG 对空 UA 会返回空结果集,别清空 |
relevanceGuard | true | 丢掉与 query 无关的结果;关掉就退回"静默转发降级结果",只建议排查解析问题时临时用 |
fallbackProvider | deepseek-official | 免费引擎全部失败后改用的已注册 provider;空字符串关闭回退 |
minIntervalMs | 800 | 同一引擎两次请求的最小间隔(并行 query 会排队) |
cooldownMs | 300000 | 引擎遇到 403/429/传输失败后的冷却时长 |
debug | false | 打印命中的引擎、丢弃条数与冷却/回退事件 |
- id: dsh-web-search-keyless
name: 'dsh-web-search-keyless'
config:
engines: ['bing']
timeoutMs: 10000
minIntervalMs: 1500
debug: true
request.maxResults 会作为 count 传给 Bing;截断本身由 seam 负责,provider 一律
返回 truncated: false。全部引擎失败才抛 WebError('WEB_PROVIDER_ERROR'),错误文本里
带每个引擎的失败原因(超时 / HTTP 状态 / 零结果 / 未知引擎 / 结果无关 / 冷却中)。
免费优先、失败才付费的回退
免费引擎全部失败(连接失败 / 403 / 零结果 / 结果全被判为无关 / 冷却中)之后,
provider 会调用 fallbackProvider 指定的另一个已注册 provider —— 默认就是 DSH 自带的
deepseek-official。所以正常的常见查询依然零成本,只有冷门调研才会花钱,而且花的那次
会照常写 web/deepseek-search-llm-request 审计事件。
三个设计决定:
- 复用注册表,不抄实现。回退是直接调用
ctx.web 里已注册的那个 provider,而不是
把"Anthropic Messages + 原生 web_search"再实现一遍:密钥解析、错误语义、审计事件
都只有一份,不会漂移。代价是 searchProviders 在 dsh-web 的类型里是 private
(运行时是个普通 Map),因此代码里一律按"拿不到就不回退"处理 —— DSH 改了内部结构,
本插件只是退回纯免费行为,不会抛错。
- 回退结果不过相关度闸门。付费路径的服务器端检索本身做了相关度,而且它实测不带
snippet,用为免费引擎设计的规则去判它只会误杀。
- 失败仍然是一次失败。连回退也失败时,抛出的错误里同时列出免费引擎和回退的失败
原因,不会静默降级成空结果。
代价是延迟:最坏情况是 Bing 超时 + DDG 超时之后才轮到付费搜索。工具侧
tool-web.searchTimeoutMs 默认 60s,通常够用;要提高回退命中率可以调小
timeoutMs(引擎失败得越快,越早进入付费路径)。
效果与付费路径的差异(实测)
这不是付费搜索的等价替代。 差异分三层,都有实测支撑:
-
元数据反而更全。付费路径(DeepSeek 原生搜索)经本仓库实测的 200 条结果里
0 条带摘要、0 条带日期(web_search_result 的 snippet 来自引用片段
cited_text,实际很少命中),模型只拿到标题 + URL。本插件每条都带标题、摘要、
发布日期,模型据此更容易判断该不该 web_fetch。
-
长尾查询会静默降级。Bing 对冷门查询会返回 HTTP 200、channel 标题正确回显
query,然后给出完全无关的热门内容。实测(真实查询,节流 9s/次):
| query | 结果 |
|---|
PyMuPDF latest version | 6 条,全部相关 |
docling picture caption matching nearest caption below layout model | 8 条,全部相关 |
Nougat OCR arXiv PDF math LaTeX formula accuracy | 8 条,全部相关 |
PDFFigures 2.0 mining figures from research papers… | 失败(Bing 返回 8 条无关内容) |
FigureSeer parsing result-figures research papers ECCV 2016… | 失败(同上) |
CERMINE figure caption zone classification SVM… | 失败(同上) |
同一批 query 走付费路径时,返回的是 dl.acm / Springer 论文页、github.com/CeON/CERMINE
源码、docs.rs 文档这一级的深链——冷门方法名、论文标题这类调研,付费路径明显更强。
所以 relevanceGuard 默认开着:与其让模型自信引用垃圾,不如让这次搜索明确失败。
-
降级是客户端信誉问题,不是查询语义。同一时刻直接抓 Bing 的 HTML 结果页
(/search?q=…,不走 RSS)返回的同样是无关内容(实测给出 "Assistant Principal jobs
in Georgia"),说明两个接口都在被反爬降级。短时间高频请求会显著加剧:一次约 170
个请求的压测之后,DDG 从可用变成 403、再变成连接超时,Bing 的长尾查询全面降级。
因此本插件默认串行 + 最小间隔 + 冷却,且失败后不回落到付费路径。
结论:命名实体、常见库/项目类查询,免费路径够用且更快;冷门方法名、论文标题、
细分算法调研,建议要么接一个为程序化调用设计的检索 API(Tavily / Brave / Exa /
Perplexity——本插件的 provider 接口与之同形,改 backends.js 即可),要么在 profile 里
把 web.searchProvider 临时切回 deepseek-official。
已知取舍
- 质量:Bing RSS / DDG Lite 是检索结果,不是摘要;没有
content 字段,只有
sources[]。模型拿到的是 URL + 标题 + 摘要,需要细节就自己 web_fetch。
- 可用性:两个引擎都是公开页面,可能被限流或改版。任一失败会顺延到下一个,
两个都失败就是一次工具错误——不再回落到付费路径,这是有意的。
- 条款:Bing RSS 的版权声明限定 "personal, non-commercial use within an RSS
aggregator"。多租户或商用请换成有正式 API 的检索后端,见上一节。
测试
node --test # 纯离线:解析器 + 相关度闸门 + 节流/冷却 + 注册
DSH_KEYLESS_LIVE=1 node --test # 额外打真实端点,验证引擎出结果