dsh-searxng-search
让 DSH 内置的 web_search 工具改走 SearXNG,替代随 DSH 一起提供的 DeepSeek 搜索通道。适合自建/内网 SearXNG 的场景。
- 零依赖:只用 Node 全局
fetch/URL/AbortSignal,不 import 任何包,无构建步骤。 - 不改工具契约:复用 DSH 的 web 能力 seam(
ctx.web),模型侧仍然是原来的web_search工具,参数、结果卡片、引用格式全部不变。 - 不影响
web_fetch:抓取 URL 仍由随附的dsh-web-fetch-http提供。 - 不绑定 surface:代码只注册 seam,桌面端与
dsh web/dsh tui/dsh headless都能用;装进哪个 profile,就只有那个 surface 生效,见 3. 安装 的方式 E / F。 - 可审计:
package.json声明dsh.seams: ["web"],自测锁定它与inject一致;dsh-vet在每次 push / PR 上审计,并把报告与等级公示在dsh-vet/report分支,见 5. 验证。 - 离线自测:17 项检查(
npm test),不需要网络、不需要安装依赖,其中一项专门拒绝带 UTF-8 BOM 的发布文件。
免责声明:这是第三方项目,与 DeepSeek 官方无关,也不代表官方背书。 DSH 目前是 developer preview,本插件针对
0.2.0-rc.2验证过;它依赖的 seam API(ctx.web.registerSearchProvider)可能随 DSH 版本变化。
兼容性
| 项 | 要求 |
|---|---|
| DSH | 0.2.0-rc.2(已验证);package.json 的 peerDependencies 声明为 ^0.2.0-rc.2 |
| Node | >= 20.3.0 —— 用到 AbortSignal.any()(Node 20.3.0 引入),CI 在 20.3 / 22 / 24 上验证 |
| 运行依赖 | 无 |
| 构建步骤 | 无 |
目录
1. 原理
DSH 的 web 访问是一个可插拔的 seam(@deepseek-ai/dsh-web,即 ctx.web):
| 组件 | 角色 |
|---|---|
@deepseek-ai/dsh-tool-web |
面向模型的 web_search / web_fetch 工具 |
@deepseek-ai/dsh-web |
seam 本身:注册搜索/抓取提供方,并按 id 选择提供方 |
@deepseek-ai/dsh-web-search-deepseek |
随附的搜索提供方(id:deepseek-official) |
@deepseek-ai/dsh-web-fetch-http |
随附的抓取提供方(id:http) |
dsh-base 的默认配置把 seam 固定到了 searchProvider: deepseek-official。DSH 没有内置 SearXNG 提供方,所以接入只需要两件事:
- 挂载一个注册
searxng搜索提供方的小插件(本组合包); - 把
web行的searchProvider改成searxng。
这两件事都在本包的 cordis.patch.yml 里完成(组合包 patch 层在 dsh-base / dsh-web-app 之后应用,同一行最后写入者生效,因此可以覆盖默认值)。
⚠️ 必须显式固定
searchProvider:deepseek-official只要存在resolveApiKey解析器就报告「可用」,所以如果两个提供方同时可用,web_search会以WEB_PROVIDER_AMBIGUOUS失败。本包的 patch 已经替你固定好了。
2. SearXNG 侧的准备
这一步最容易踩坑,装了插件却搜不出结果,九成问题在这里。
2.1 打开 JSON API(最常见的坑)
SearXNG 默认只输出 HTML。在你的 settings.yml 里加上 json:
search:
formats:
- html
- json
改完重启 SearXNG。
没开 JSON 时,SearXNG 对 format=json 返回 HTTP 403(同一个地址用 HTML 搜索却是 200)——官方文档的原话是 "Requesting an unset format will return a 403 Forbidden error."。插件会把这一点写进错误消息,而不是静默返回空结果。format=csv / format=rss 也会一起 403,可以拿它们对照确认。
2.2 放行 DSH 主机的请求
SearXNG 默认开启限速/机器人检测,程序化请求容易被 429/403:
server:
limiter: false # 自用最省事
# 或保留 limiter,把 DSH 主机的 IP(或网段)加入 botdetection 的 pass_ip / 白名单
2.3 确认 DSH 所在机器能直接访问该实例
把地址换成你自己的实例(下面的 searxng.example:8888 是占位符):
curl -s -o /dev/null -w '%{http_code}\n' 'http://searxng.example:8888/search?q=test&format=json'
返回 200 就说明服务端就绪(返回 403 就回到 2.1)。
建议用
curl而不是 PowerShell 的Invoke-RestMethod:后者在报错时可能不显示 响应体,排查时容易被误导。想看完整响应头用curl -s -i。
3. 安装
安装前先改好 cordis.patch.yml 里的 baseURL(见 4. 配置项)——
仓库里的值是占位符,不改的话插件的 available() 会返回 false,web_search 报
WEB_PROVIDER_CONFIGURED_UNAVAILABLE。
DSH 插件管理器的输入框接受:包名、Git 地址、压缩包、本地绝对路径。
方式 A:插件管理器 + 本地路径
- 打开 DSH Web 界面 → 侧栏 插件(Plugins)→ 添加插件。
- 粘贴本仓库所在目录的绝对路径,例如
D:\path\to\dsh-searxng-search。 - 点 安装,等 pnpm 跑完。
- 完全重启 DSH(见下面的「装完必做」)。
方式 B:插件管理器 + Git 地址
在同一个输入框里粘贴 Git 地址即可,不需要发布到 npm:
https://github.com/mimoxh/dsh-searxng-search
方式 C:npm(尚未发布)
本包目前没有发布到 npm。发布之后可以这样装:
dsh plugin --profile <profile> add dsh-searxng-search
方式 D:手工写入 profile(不推荐)
Plugin Manager 做的就是这几步,手工做需要自己承担风险。以 profile desktop 为例:
编辑
<DSH_HOME>/profiles/desktop/package.json:{ "dependencies": { "dsh-searxng-search": "link:D:/path/to/dsh-searxng-search" }, "dsh": { "profile": { "bundles": ["...", "dsh-searxng-search"] } } }dsh.profile.bundles数组末尾追加包名(保证它的 patch 层最后应用)。在 profile 目录里用 DSH 捆绑的 pnpm 安装:
node "<DSH 安装目录>/resources/runtime/pnpm/bin/pnpm.cjs" install完全重启 DSH。
方式 E:命令行装到任意 profile(其他 surface 首选)
桌面端之外还有三个 surface,各自对应 $DSH_HOME/profiles 下的一个同名 profile:
| surface | profile | 备注 |
|---|---|---|
| 桌面 App | desktop |
Electron 专用:dsh --profile desktop ... 会被直接拒绝(error: profile "desktop" is managed exclusively by the Electron application),只能按方式 A 在 App 里装 |
dsh web / dsh --profile web |
web |
浏览器 surface |
dsh tui |
tui |
终端界面 |
dsh headless "…" |
headless |
一次性任务,跑完退出 |
profile 之间互不相干:dsh.profile.bundles 各写各的,本包装进哪一个,就只有那个
surface 生效。DSH 自带的安装入口会跑 pnpm,并在包声明了 dsh.bundle 时把它追加进该
profile 的 bundles(参数直接透传给 pnpm):
dsh plugin --profile web add "D:/path/to/dsh-searxng-search" # 本地目录(link:)
dsh plugin --profile web add github:mimoxh/dsh-searxng-search # Git(pnpm 的 github: 简写)
目标 profile 还不存在时,用 --from-default-profile <shipped-template> 从 shipped 模板
初始化(dsh --help 里的例子是 dsh rescue --from-default-profile web)。
装完仍是那一件事:让 web 行指向 searxng。组合包 patch 已经做了(只要包在 bundles
里就会应用),所以通常只差实例地址——用下面的 overlay,或按 4. 配置项 写进
该 profile 自己的 cordis.patch.yml。
方式 F:用 --patch overlay 覆盖配置(不改 profile 文件)
dsh 支持在 profile 层之后再叠一层 patch,而且可重复:
export SEARXNG_BASE_URL=http://192.168.1.20:8086
dsh web --patch ./examples/dsh-searxng.overlay.yml
dsh tui --patch ./examples/dsh-searxng.overlay.yml
dsh headless --patch ./examples/dsh-searxng.overlay.yml "搜索今天的新闻"
examples/dsh-searxng.overlay.yml 只重述两行
已经存在的 row 的 config(web 与 web-search-searxng),不插入新 row——row 由
组合包 patch 插入,所以前提是该 profile 里已装本包(方式 E 的安装那一步)。
它的 baseURL 写成 !!js process.env.SEARXNG_BASE_URL ?? '<占位>',在组合期取环境
变量,同一个文件就能在不同机器/不同实例间复用,不必改任何文件;变量没设时占位地址会让
provider 报 WEB_PROVIDER_CONFIGURED_UNAVAILABLE,而不是静默换一条搜索通道。
装到其他 surface 时的两个坑
web_search这个工具的归属,各 surface 不一样。dsh-base把tool-web放在 host plane(dsh tui/dsh headless走这条);dsh-web-app则把它disabled: true掉,改由每个 agent preset 各自组合(桌面端与dsh web走这条)。 所以在 Web 类 surface 上,除了 provider,还要确认该 session 用的 preset 挂了tool-web;否则 seam 里注册了提供方,也没有工具去调它。- 生效/重启语义不同。 桌面端是常驻 host,装或改组合包必须完全退出并重启(见
7. 已知的 DSH 侧问题);
dsh tui/dsh web/dsh headless每次启动都重新组合 profile,所以下一次启动即生效。
装完必做
- 改
cordis.patch.yml的baseURL,指向你的实例。 - 完全退出并重新启动 DSH(关窗口不算,要确认进程退出)。
组合包的 patch 层是在启动时组合进 profile 的。新装/改动组合包后不重启,运行中的
DSH 仍然在用旧的组合——表现就是「插件卡片上显示组件已关闭,web_search 还在走
DeepSeek」。
4. 配置项
配置在 cordis.patch.yml 中 web-search-searxng 行的 config 里:
| 字段 | 默认 | 说明 |
|---|---|---|
baseURL |
无(可用环境变量 SEARXNG_BASE_URL 兜底) |
SearXNG 地址,含协议/端口/路径前缀,不要结尾斜杠 |
apiPath |
/search |
JSON API 路径,仅当实例挂在别的路径下才改 |
language |
不发送 | SearXNG language,如 zh-CN、en-US、all |
categories |
不发送 | 如 general、general,news |
engines |
不发送 | 限定引擎,如 google,bing |
timeRange |
不发送 | day / week / month / year |
safeSearch |
不发送 | 0 / 1 / 2 |
timeoutMs |
15000 |
单次搜索超时(工具层还有 60s 预算) |
maxResults |
10 |
本提供方自己的上限;seam 还会再按 web_search 请求的上限截断 |
userAgent |
dsh-searxng-search/<包版本> |
请求 UA(SearXNG 会拒绝空 UA);默认值跟随 package.json 的 version |
headers |
{} |
额外请求头,例如反向代理前的 Authorization: Bearer <token> |
改动 baseURL 等配置后需要让组合包重新加载:桌面端完全重启 DSH,dsh web / dsh tui /
dsh headless 下次启动即生效。同样的字段也可以写在 --patch overlay 里(见
3. 安装 的方式 F),此时 baseURL 可用 SEARXNG_BASE_URL 兜底。
为什么没有配置 schema
本插件故意不声明配置 schema。声明 schema 能换来 DSH 在加载时校验配置、以及在设置
界面里渲染配置控件;但代价是必须 import @deepseek-ai/schemastery,而本项目刻意保持
零 import:
- 零依赖让自测可以离线跑、不需要
npm install,CI 里连安装步骤都不用; - 它不依赖 DSH 内部如何解析随安装包一起分发的模块;
- DSH 升级
schemastery时,schema 可能被判为Config is not a native Schemastery schema,从而直接拒绝该行——多一个失败面。
因此字段写错不会在加载时报错,而是在第一次搜索时报出带端点和处理建议的错误。如果你 更想要加载时校验,可以自行加 schema。
结果映射
SearXNG 的 results[].url/title/content/publishedDate → seam 的
url/title/snippet/publishedAt(按 URL 去重),answers[] → 给模型的直接答案
(content)。
5. 验证
5.1 离线自测(不依赖 DSH、不需要网络)
npm test # 等价于 node test/smoke.mjs
17 项检查覆盖:注册 id、可用性判断、User-Agent 与版本一致、dsh.seams 与 inject
一致、发布文件不带 UTF-8 BOM、URL 构造、结果映射/去重/截断、真实 HTTP 往返
(内置 mock SearXNG)、HTML(未开 JSON)报错、403(未开 JSON 格式)报错、429 报错、
取消、不可达报错。
5.2 在 DSH 里验证
- 让模型调用一次
web_search(例如「搜索 deepseek harness」)。 - 看结果卡片的来源是否来自你的 SearXNG 引擎;若报错,错误消息里会带已解析的端点和 处理建议。
- 若来源看起来不对或报
WEB_PROVIDER_CONFIGURED_MISSING,检查 profile 里web行 是否仍写着deepseek-official,以及是否忘了重启 DSH。
5.3 安全自审(dsh-vet)
本包在每次 push / PR 上跑 dsh-vet:
.github/workflows/dsh-vet.yml 审计的是 npm pack
出来的产物(也就是 files 列出来的东西),并用 strict: true 让 high 及以上、且置信度
≥ medium 的发现项直接让 CI 失败。
审计产物而不是整仓是刻意的:整仓扫描会把 test/smoke.mjs 也算进能力面(它读 JSON、开
mock HTTP 服务),多出三条与插件运行时无关的发现项——fs 读取计入
perm.seam-mismatch(medium/medium,会把整仓扫描的等级压到 C)、node:http 仅 import、
以及不可达文件。CI 因此只对产物定级。
本地复现同一份扫描:
npm pack --pack-destination .dsh-vet-pack
tar -xzf .dsh-vet-pack/*.tgz -C .dsh-vet-pack
npx --yes dsh-vet@0.4.0 --json .dsh-vet-pack/package
0.2.0 的实测结果是 A 级,报告里仍留着两条发现项——这是真实的剩余状态,不是清干净的 样板:
| 严重度 / 置信度 | 规则 | 说明 |
|---|---|---|
| high / low | egress.secret-adjacent |
index.js 读 process.env.SEARXNG_BASE_URL,而同一文件里有出网调用。按 dsh-vet 的契约,低置信度不参与定级 |
| info / high | perm.network-client |
声明 dsh.seams: ["web"] 后的正常清点项。未声明时它是 medium/high,等级会掉到 C |
想彻底消掉那条 egress.secret-adjacent,只能改成经 DSH 的
@deepseek-ai/dsh-launch-environment 读启动环境(对方 dsh-searxng 正是这么做的)——代价是
本项目"零 import"的底线,所以这里选择保留并写明。
等级徽章读 dsh-vet/report 分支上的报告(shields.io 直接读该分支的 badge.json,没有徽章
服务),因此它要等仓库推送、workflow 至少跑过一次之后才会显示。
6. 排错
| 现象 / 错误码 | 原因与处理 |
|---|---|
WEB_PROVIDER_AMBIGUOUS(多个可用提供方) |
web 行的 searchProvider 没被改成 searxng(patch 层顺序被更高层覆盖)。检查 profile 的 cordis.patch.yml 是否也写了 web 行;或在其中显式加 searchProvider: searxng |
WEB_PROVIDER_CONFIGURED_UNAVAILABLE |
searxng 已注册但 baseURL 为空或不是合法 URL。填好 baseURL(或设 SEARXNG_BASE_URL)后重启 |
WEB_PROVIDER_CONFIGURED_MISSING |
组合包没被启用/没装成功,提供方没注册;确认已重启 DSH |
SearXNG returned HTTP 403 + 提示 search.formats |
首查这个:SearXNG 没开 json 输出格式(此时 HTML 搜索照常 200)。按 2.1 加上 - json 并重启 SearXNG |
SearXNG returned HTTP 403(JSON 已开,仍被拒) |
limiter / botdetection 拦截 —— 关掉 server.limiter 或把 DSH 主机 IP 加白 |
SearXNG returned HTTP 429 |
limiter 限速 —— 同上 |
SearXNG returned HTTP 404 |
baseURL / apiPath 不对 |
报错提到 formats: [html, json] |
SearXNG 返回的是 HTML —— 按 2.1 打开 JSON API 并重启 |
| 请求超时 | 提高 timeoutMs;确认 DSH 主机到 SearXNG 的网络与 DNS |
| 搜索有结果但模型看不到 | 确认用的是 web_search 而不是某个 MCP 工具;结果里的来源来自 seam 的 sources[] |
| 结果为空 | SearXNG 的引擎本身没返回(可能是出网受限、engines 限制过窄、或 categories 不匹配)。先用 2.3 的 curl 直接验证 SearXNG |
| 插件卡片显示组件「已关闭」 | 运行中的 DSH 是在装插件之前启动的。完全重启 DSH |
dsh tui / dsh headless 上 web_search 仍走 DeepSeek |
该 profile 里没装本包——profile 之间互不相干,见 3. 安装 的方式 E |
overlay 启动后报 WEB_PROVIDER_CONFIGURED_UNAVAILABLE |
SEARXNG_BASE_URL 没设(占位地址不是真实例),或该 profile 里还没装本包(overlay 只覆盖已有 row 的 config) |
dsh --profile desktop --dump-config 报 profile 被独占 |
桌面端 profile 只能由 App 启动(managed exclusively by the Electron application);换 dsh web / dsh tui 诊断,或用 App 里的插件管理器 |
7. 已知的 DSH 侧问题
在 Desktop 版的常驻 host 里,对已安装组合包的组件点「启用」开关,可能报:
dsh: profile reload requires the root Include entry
这是 DSH 侧的 profile 热重载失败(@deepseek-ai/dsh-app-boot 的
reconcileProfilePatches() 找不到根 Include 条目),与本插件无关。组合包的行本来
就会在启动时组合,所以完全重启 DSH 即可,不需要(也无法)用那个开关。
补充一条同源的差别:desktop profile 由 Electron 独占——dsh --profile desktop …
(包括 --dump-config)会直接报 error: profile "desktop" is managed exclusively by the Electron application。所以桌面端只能走 App 内的插件管理器,CLI 的那些诊断手段要用在
dsh web / dsh tui 上(见 3. 安装 的方式 E、F)。
8. 备选方案:MCP
如果不想写代码,也可以用配置型组合包把 SearXNG 的 MCP server 挂进来(需要能拿到该
server,比如 mcp-searxng):
- insert:
- id: searxng-mcp
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: searxng
transport: stdio
command: node
args: ['C:\\path\\to\\mcp-searxng\\dist\\index.js']
env:
SEARXNG_URL: http://searxng.example:8888
failOnStartupError: true
区别:模型拿到的是 mcp__searxng__searxng_web_search 这样的独立工具,不是
web_search,也不会复用 DSH 的搜索结果卡片;而且要多跑一个 MCP 进程、多依赖一份第三方
代码。离线/内网场景下,本组合包(零依赖)更干净。
9. 开发与测试
| 文件 | 作用 |
|---|---|
index.js |
提供方实现:apply / inject、配置规整、URL 构造、结果映射、错误码 |
cordis.patch.yml |
组合包 patch:固定 web.searchProvider = searxng + 挂载提供方行 |
package.json |
组合包清单(dsh.bundle.patch、dsh.seams、展示元信息) |
examples/dsh-searxng.overlay.yml |
dsh --patch 用的 overlay(见 3. 安装 的方式 F) |
locale/zh.json、locale/en.json |
插件管理器卡片上的标题与描述 |
test/smoke.mjs |
离线自测(内置 mock SearXNG) |
.github/workflows/ci.yml |
CI:Node 20.3/22/24 × Linux/Windows,另有 tarball 内容检查 |
.github/workflows/dsh-vet.yml |
安全自审:扫 npm pack 产物并公示报告与等级(见 5. 验证) |
本地开发:
npm test
不需要 npm install:本项目没有任何依赖,测试只用 Node 内置模块。
修改 index.js 里的 PACKAGE_VERSION 时记得同步 package.json 的 version——
自测里有一项专门检查这两者是否一致。
CI 与 dsh-vet 徽标都要等仓库推送、对应 workflow 至少跑过一次之后才会显示:CI 徽标来自
ciworkflow,dsh-vet 徽标来自 workflow 发布到dsh-vet/report分支的报告。
No comments yet. Be the first to write one.