@gausszhou/dsh-web-search-local
v0.2.1
Published
Keyless multi-engine web search + page fetch providers for the dsh web seam (no DeepSeek dependency).
Maintainers
Readme
dsh-web-search-local
为 DeepSeek Harness(dsh)ctx.web 接口提供的无密钥多引擎网页搜索与抓取 provider。适用于任何模型后端——包括完全本地模型,无需 API Key,也不依赖 DeepSeek 的服务端搜索。
为什么需要它
dsh 内置的 web_search 工具与模型无关:它只调用 ctx.web.search()。依赖 DeepSeek 的是它的默认搜索 provider(dsh-web-search-deepseek),该 provider 会把每次查询带上 DEEPSEEK_API_KEY 发给 DeepSeek 的 web_search_20250305 服务端工具。一旦切换到本地模型(如 Ollama),这个 provider 没有 key,搜索就失效了。
本包注册两个由插件自己发 HTTP 请求的 provider:
| provider id | 能力 | 引擎 |
| --- | --- | --- |
| local-multi | web_search | 三层顺序执行——SearXNG(配置时)→ Google/DuckDuckGo/Mojeek(国外层)→ Bing/Baidu/Sogou/360(国内层);同层引擎并行请求并 round-robin 合并结果;一层无结果则降级到下一层 |
| local-fetch | web_fetch | 直接 GET,字符集感知解码(含 gbk),返回 html/text 正文 |
代理 / VPN 支持
Node 进程不会自动使用操作系统/浏览器的代理。如果 DuckDuckGo 等引擎在你的网络环境下不可达,provider 会自动解析代理:
proxyUrl配置(显式指定,或设为'off'强制直连)HTTPS_PROXY/HTTP_PROXY/ALL_PROXY环境变量- 探测常见本地 HTTP 代理端口(
7890Clash、7897、10809v2rayN、1080、……)
代理只作用于国外层引擎(google、duckduckgo、mojeek)——它们才是大陆网络下需要隧道的一方。国内层引擎(bing、baidu、sogou、360)和私有 SearXNG 实例始终直连(只有国外层引擎走 CONNECT 隧道;隧道在传输层挂掉时这些请求同样回退直连)。这个拆分很关键:把国内引擎塞进国外节点 IP 会触发百度的安全验证墙、搜狗的反爬验证码和 360 的 302 跳转;把自己的 SearXNG 实例也走 VPN 节点则可能拿到陈旧或空的结果集。
安装
通过 npm
已发布至 npm registry。
npm install @gausszhou/dsh-web-search-local或者用 dsh CLI 一步注册并激活插件:
dsh plugin add @gausszhou/dsh-web-search-local本包内置 dsh.bundle patch,因此 dsh plugin add 会安装并自动激活插件:
web profile 会自动切换为 searchProvider: local-multi / fetchProvider: local-fetch,
并禁用内置的 web-search-deepseek provider,无需手工编辑 cordis.patch.yml。
如需调整 bundle 应用的默认值,可在你的 profile 的 cordis.patch.yml
(web profile 即 $DSH_HOME/profiles/web/cordis.patch.yml,位于 bundle 层之后)中覆盖:
- id: web
config:
searchProvider: local-multi
fetchProvider: local-fetch
- id: web-search-deepseek
disabled: true
- insert:
- id: web-search-local
name: '@gausszhou/dsh-web-search-local'
config:
engines: [searxng, google, duckduckgo, mojeek, bing, baidu, sogou, 360]本地目录 / 文件路径
把本包放到 dsh 进程可读的任意位置,例如 $DSH_HOME/profiles/web/plugins/web-search-local/(Windows:C:\Users\<you>\.dsh\profiles\web\plugins\web-search-local\)。然后在你的 profile 的 cordis.patch.yml(web profile 即 $DSH_HOME/profiles/web/cordis.patch.yml)中加入:
- id: web
config:
searchProvider: local-multi
fetchProvider: local-fetch
- id: web-search-deepseek
disabled: true
- insert:
- id: web-search-local
name: 'file:///C:/Users/<you>/.dsh/profiles/web/plugins/web-search-local/index.js'
config:
engines: [searxng, google, duckduckgo, mojeek, bing, baidu, sogou, 360]- 重启 dsh。
web_search现在返回纯来源列表(无服务端摘要),且适用于任何模型。
配置
config:
engines: [searxng, google, duckduckgo, mojeek, bing, baidu, sogou, 360] # 成员列表(执行按层:searxng → 国外 → 国内,层内并行)
skipWithoutProxy: [google, duckduckgo, mojeek] # 无代理时直接跳过的引擎([] = 总是尝试)
searxngBaseUrl: 'http://127.0.0.1:8080' # 可选;设置后优先执行
proxyUrl: '' # '' 自动 | 'off' 直连 | 'http://host:port' 显式指定
searchTimeoutMs: 12000
fetchTimeoutMs: 20000
maxFetchBytes: 1048576
maxSources: 12
cacheTtlMs: 300000 # 内存结果缓存
engineMinIntervalMs: 1500 # 引擎请求最小间隔(防限流)
engineCooldownMs: 600000 # 验证墙/验证码后熔断冷却(0 = 关闭)
engineRetryCooldownMs: 60000 # 普通失败后冷却(0 = 关闭)
userAgent: '<浏览器风格的 UA>'默认引擎列表分三层、按序执行,同层引擎并行请求并合并:
- searxng——配置了
searxngBaseUrl的私有 SearXNG 实例本身就是元搜索聚合,有结果就直接返回,跳过下面两层 - 国外层——Google、DuckDuckGo、Mojeek(大陆需要代理;无代理时整体直接跳过,见
skipWithoutProxy) - 国内层——Bing、Baidu、Sogou、360(直连可用,无需 VPN/代理)
一层无结果(空、被墙或跳过的引擎)就降级到下一层,所以国外层永远不会拖垮可直连的国内层。google 引擎易被反爬(consent 墙、sorry/ 机器人检测、enablejs JS 墙);要稳定拿到 Google 结果,使用启用了 google 引擎的 SearXNG 实例。在开放网络(全球引擎可直连)上,把 skipWithoutProxy 设为 []。
私有 SearXNG 实例(Docker:docker run -p 8080:8080 searxng/searxng)是所有引擎中最稳健的:元搜索聚合、JSON API、无需逐引擎爬取。
限流韧性
搜索引擎(尤其是 DuckDuckGo)会限流脚本。以下三个机制让单引擎配置也能稳定使用:
- 节流——按引擎计:同一引擎在
engineMinIntervalMs内不会被调用两次(防限流),同层不同引擎则一起启动。 - 熔断——引擎出现机器人墙(
blocked by captcha/anomaly check/ 百度的verification wall,或 HTTP 403/429)时,在engineCooldownMs(默认 10 分钟)内跳过;普通失败(传输、HTTP 错误)只触发更短的engineRetryCooldownMs(默认 60 秒)。冷却期间引擎被跳过,原因会聚合进错误信息。 - DuckDuckGo lite 兜底——
html.duckduckgo.com端点被机器人墙拦截时,同一查询会改走lite.duckduckgo.com/lite/重试一次(该端点对脚本更宽容)。若 lite 端点也被墙,引擎会报告blocked by anomaly check (html and lite)并触发长冷却engineCooldownMs,而不是每次搜索都反复冲击两个端点。
被墙的引擎不会让整个搜索失败(前提是还有其他引擎);单引擎模式下会快速失败并给出 "cooling down" 原因,而不是反复冲击被墙端点。
模型指定引擎
模型可以在每次搜索时指定用哪个引擎,两种途径:
- 工具——在官方
web_search之外,本插件注册了web_search_engine,带两个可选参数:engine:单个引擎——searxng、google、duckduckgo、mojeek、bing、baidu、sogou、360engines:有序的引擎优先级列表 两者都不传时,调用降级为配置的默认三层引擎链,与web_search完全一致。
- provider 请求——任何直接调用
ctx.web.search({ query, engine })或ctx.web.search({ query, engines })的调用方都获得同样的覆盖;未知引擎名会抛WEB_PROVIDER_ERROR并列出合法 id。
显式覆盖会完全替换配置的引擎链(包括 SearXNG 自动前置)——模型的明确选择优先。指定的引擎同样按 searxng / 国外 / 国内三层分组、层内并行合并(与默认链一致);只指定一个引擎就是单跑。节奏控制、熔断和 skipWithoutProxy 对指定引擎同样生效,所以指定了但不可达的引擎会快速失败,而不会拖垮整个搜索。
回退到 DeepSeek 搜索
从 cordis.patch.yml 中移除 web 覆盖项、web-search-deepseek 禁用项以及插入的那一行即可。
注意事项
- 引擎靠正则抓取纯 HTML;上游改版可能导致某个引擎失效——链路会自动落到下一个引擎。所有引擎的错误会聚合进抛出的异常信息。搜狗的
/link?url=加密跳转会在服务端解析(跳转页正文内嵌真实地址);360 的跳转链接在锚点的data-mdurl属性里直接暴露真实地址,解析器直接读取。 google引擎用双布局解析器抓取 HTML 结果页(基础gbv=1标记与现代 JS 时代标记),并发送 CONSENT/SOCS Cookie 绕过欧盟 consent 墙。Google 经常对脚本返回"需要启用 JavaScript"墙(/httpservice/retry/enablejs)或sorry/验证码而不是结果——两者都会被检测并触发长熔断冷却(带明确原因),国外层随即降级到国内层。要稳定拿到 Google 结果,使用启用了 google 引擎的 SearXNG 实例。- 返回结构与官方 provider 一致:
web_search返回{ sources: [{ url, title?, snippet?, publishedAt? }], truncated }。层内引擎并行请求,来源round-robin 合并、去重、截断到maxSources条(合并结果超过上限时truncated置 true);一层无结果则降级到下一层。publishedAt是尽力而为的YYYY-MM-DD日期,当引擎页面出现日期时填充(SearXNG 的publishedDate,或 Bing/Baidu/Sogou/360 结果块中的日期文本),否则省略该字段——与官方page_age相同的可选语义。 - 无第三方运行时依赖:只用
fetch+node:http/https/net/tls,外加 dsh 自带的@deepseek-ai/dsh-web(以peerDependency声明;每个 dsh profile 都已内置)。 - 错误遵循 seam 的 provider 契约:失败时抛
WebError,code 为WEB_PROVIDER_ERROR(引擎/传输/超时,引擎错误会聚合进 message)或WEB_ABORTED(调用方取消)——与官方 provider 使用同一套错误词汇。 web_fetch需要tool-web的fetch: true;自带的standardagent 预设默认是fetch: false——把预设复制到$DSH_HOME/.agent-presets/并在那里打开开关。
配置与设置集成
插件声明了一个 schemastery Config 架构(字段与 defaultConfig() 一一对应)并通过 dsh 的 settings 服务注册了一个设置命名空间(web-search-local)——内置的 web-search-deepseek、shell、agent-loop 等插件用的就是这套机制。这带来:
- 配置经过校验与规范化,可通过 dsh 的 settings 服务持久化。
- provider 每次搜索/抓取时读取实时生效的配置段:通过设置 UI 改动的值会在下一次调用立即生效,无需重启。(
Config与SETTINGS_NAMESPACE均作为具名导出。) - 在没有 settings 服务的 profile 里,行为与原来完全一致:仍走
cordis.patch.yml传给apply(ctx, config)的组合配置。
说明:在“插件配置”面板中看得见的那张卡片是一个客户端 React 组件——内置插件(终端 / Agent 循环 / 网页搜索)的卡片都硬编码在
dsh-client-ui-settings-plugins这个打包客户端包里。要让本插件在这套面板里出现可编辑卡片,还需要随包提供一个./client半端来注册settings.plugin.item卡片(见后文)。本插件的配置界面 = 上面的服务端 schema 集成 + 客户端卡片两部分。
许可证
MIT
