# dsh-web-search-local 为 [DeepSeek Harness](https://www.deepseek.com/harness/)(dsh)`ctx.web` 接口提供的**无密钥多引擎网页搜索与抓取 provider**。适用于**任何模型后端——包括完全本地模型**,**无需 API Key,也不依赖 DeepSeek 的服务端搜索**。 [![npm 版本](https://img.shields.io/npm/v/@gausszhou/dsh-web-search-local.svg)](https://www.npmjs.com/package/@gausszhou/dsh-web-search-local) [![npm 月下载量](https://img.shields.io/npm/dm/@gausszhou/dsh-web-search-local.svg)](https://www.npmjs.com/package/@gausszhou/dsh-web-search-local) [![累计下载量](https://img.shields.io/npm/dt/@gausszhou/dsh-web-search-local.svg)](https://www.npmjs.com/package/@gausszhou/dsh-web-search-local) [![开源协议](https://img.shields.io/npm/l/@gausszhou/dsh-web-search-local.svg)](./LICENSE) ## 为什么需要它 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 会自动解析代理: 1. `proxyUrl` 配置(显式指定,或设为 `'off'` 强制直连) 2. `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` 环境变量 3. 探测常见本地 HTTP 代理端口(`7890` Clash、`7897`、`10809` v2rayN、`1080`、……) 代理**只作用于国外层引擎**(`google`、`duckduckgo`、`mojeek`)——它们才是大陆网络下需要隧道的一方。**国内层引擎(`bing`、`baidu`、`sogou`、`360`)和私有 SearXNG 实例始终直连**(只有国外层引擎走 CONNECT 隧道;隧道在传输层挂掉时这些请求同样回退直连)。这个拆分很关键:把国内引擎塞进国外节点 IP 会触发百度的安全验证墙、搜狗的反爬验证码和 360 的 302 跳转;把自己的 SearXNG 实例也走 VPN 节点则可能拿到陈旧或空的结果集。 ## 安装 ### 通过 npm > 已发布至 [npm registry](https://www.npmjs.com/package/@gausszhou/dsh-web-search-local)。 ```bash npm install @gausszhou/dsh-web-search-local ``` 或者用 dsh CLI 一步注册并激活插件: ```bash 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 层之后)中覆盖: ```yaml - 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\\.dsh\profiles\web\plugins\web-search-local\`)。然后在你的 profile 的 `cordis.patch.yml`(web profile 即 `$DSH_HOME/profiles/web/cordis.patch.yml`)中加入: ```yaml - id: web config: searchProvider: local-multi fetchProvider: local-fetch - id: web-search-deepseek disabled: true - insert: - id: web-search-local name: 'file:///C:/Users//.dsh/profiles/web/plugins/web-search-local/index.js' config: engines: [searxng, google, duckduckgo, mojeek, bing, baidu, sogou, 360] ``` 3. 重启 dsh。`web_search` 现在返回纯来源列表(无服务端摘要),且适用于任何模型。 ## 配置 ```yaml 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>' ``` 默认引擎列表**分三层、按序执行,同层引擎并行请求并合并**: 1. **searxng**——配置了 `searxngBaseUrl` 的私有 SearXNG 实例本身就是元搜索聚合,有结果就直接返回,跳过下面两层 2. **国外层**——Google、DuckDuckGo、Mojeek(大陆需要代理;无代理时整体直接跳过,见 `skipWithoutProxy`) 3. **国内层**——Bing、Baidu、Sogou、360(直连可用,无需 VPN/代理) 一层无结果(空、被墙或跳过的引擎)就降级到下一层,所以国外层永远不会拖垮可直连的国内层。`google` 引擎易被反爬(consent 墙、`sorry/` 机器人检测、`enablejs` JS 墙);要稳定拿到 Google 结果,使用启用了 google 引擎的 SearXNG 实例。在开放网络(全球引擎可直连)上,把 `skipWithoutProxy` 设为 `[]`。 私有 [SearXNG](https://docs.searxng.org/) 实例(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" 原因,而不是反复冲击被墙端点。 ## 模型指定引擎 模型可以在每次搜索时指定用哪个引擎,两种途径: 1. **工具**——在官方 `web_search` 之外,本插件注册了 `web_search_engine`,带两个可选参数: - `engine`:单个引擎——`searxng`、`google`、`duckduckgo`、`mojeek`、`bing`、`baidu`、`sogou`、`360` - `engines`:有序的引擎优先级列表 两者都不传时,调用降级为配置的默认三层引擎链,与 `web_search` 完全一致。 2. **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`;自带的 `standard` agent 预设默认是 `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