用 SearXNG 等检索 API 替换内置网页搜索
30/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/17 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add Anixuil/dsh-custom-search该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:待复验
是什么
dsh 原生插件 · search
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 16 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码v2.7.0🟢 扫描:未发现敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描未发现敏感能力:最多只发网络请求、读取自身元数据或只读文件,未发现执行命令、文件写入或敏感信息读取。
发起外部网络请求
查看扫描证据(共 1 处,展示前 1 条)
- 发起网络请求engines.js:193
const response = await fetch(url, {
扫描于 2026/11/2(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery@deepseek-ai/dsh-credentials@deepseek-ai/dsh-launch-environment@deepseek-ai/dsh-typert-protocol@deepseek-ai/dsh-web给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
README
由 DeepSeek 最新模型翻译生成dsh-custom-search
一个 DeepSeek Harness 插件,用你自己选的搜索引擎替换内置的 DeepSeek 网页搜索—— SearXNG、Tavily、Exa、Brave、Serper、Bing 或 Google CSE 都行。面向模型的 web_search 工具保留原有名称与 schema,所以提示词、智能体预设、工具列表都不用改。
用你自己的搜索源替换 DSH 内置的「仅官方 DeepSeek」搜索。web_search 工具名与参数保持不变,换的只是背后的检索通道——所以模型的提示词、工具表、Agent 预设都不用动。
第三方社区插件。 本项目与 DeepSeek 官方无隶属关系,也不属于官方插件市场。
下文「官方搜索」「官方提供方」一律指 DSH 自带的 @deepseek-ai/dsh-web-search-deepseek。
展开完整文档(README 剩余 1 节)
dsh-custom-search
一个 DeepSeek Harness 插件,用你自己选的搜索引擎替换内置的 DeepSeek 网页搜索—— SearXNG、Tavily、Exa、Brave、Serper、Bing 或 Google CSE 都行。面向模型的 web_search 工具保留原有名称与 schema,所以提示词、智能体预设、工具列表都不用改。
用你自己的搜索源替换 DSH 内置的「仅官方 DeepSeek」搜索。web_search 工具名与参数保持不变,换的只是背后的检索通道——所以模型的提示词、工具表、Agent 预设都不用动。
第三方社区插件。 本项目与 DeepSeek 官方无隶属关系,也不属于官方插件市场。
下文「官方搜索」「官方提供方」一律指 DSH 自带的 @deepseek-ai/dsh-web-search-deepseek。
它替换的到底是什么
DSH 把「联网搜索」拆成了三层,本插件只接管最底下那层:
| 层 | 包 | 职责 | 本插件是否改动 |
| 工具层 | @deepseek-ai/dsh-tool-web | 向模型暴露 web_search / web_fetch | 不改 |
| 服务层 | @deepseek-ai/dsh-web(ctx.web) | 选后端、取消、错误码、maxResults 截断 | 只改一个配置值 |
| 提供方层 | @deepseek-ai/dsh-web-search-deepseek | 真正发请求、返回结果 | 替换掉 |
服务层通过 registerSearchProvider() 收集提供方,并按配置里 pin 的 id 选一个执行。
官方提供方注册的 id 是 deepseek-official,本插件注册 custom-search——所以「换搜索引擎」在 DSH 里就是改一个字符串,不需要替换任何官方文件。
这也意味着:官方搜索的一个查询要花一整轮模型调用(DeepSeek 没有独立的检索接口),而接上 SearXNG / Tavily 这类真正的检索 API 后,搜索不再消耗会话模型的 token。
安装
桌面端面板(桌面端只有这一条路)
侧栏「插件」→「+ 添加插件」→ 填 github:Anixuil/dsh-custom-search → 安装 → 立即启用 → 重启 Harness。
左:面板把 github:Anixuil/dsh-custom-search 识别为「Git 仓库」来源,底层走 pnpm add 拉取。
右:安装完成,落点是 profile 目录 ~/.dsh/profiles/desktop,点「立即启用」。
面板那一栏填的是安装源(npm 包名 / GitHub 源 / 本地路径),不是插件名。
直接填 dsh-custom-search 会提示「未找到相关插件」——这个名字在 npm 上还没发布。
装完重启 Harness 才生效(bundle 类插件不会热加载)。桌面端用「重启 Harness」或退出重开。
改成从本地目录装(自己改代码时用这个)
同一栏填本仓库的绝对路径,例如 D:\code\self\all\dsh-custom-search。
pnpm 把它记成 link: 而不是复制,所以此后改 index.js / client.js
只要重启 Harness 就生效,不必卸载重装。改了代码要反复看效果时用这个,稳定了再换回 github:。
命令行安装(需要单独装 CLI,桌面端不自带)
dsh plugin --profile desktop add github:Anixuil/dsh-custom-search#main
DeepSeek Harness Desktop 的应用包没有声明 bin,也没有随附 dsh 可执行文件;
dsh plugin 来自单独安装的 CLI。只有桌面端的话,用上面面板那一栏,效果相同。
desktop 是桌面端的 profile 名(数据目录 ~/.dsh/profiles/desktop)。
用 CLI 或其他 profile 时换成实际名称,例如
dsh plugin --profile web add github:Anixuil/dsh-custom-search#main。
本插件没有构建步骤,github: 源可以直装。若 pnpm 仍拦下构建脚本,在 $DSH_HOME/profiles/desktop/pnpm-workspace.yaml 的 allowBuilds 里放行 dsh-custom-search。
升级:必须卸载再装
插件管理器不提供升级——不列注册表版本,也没有更新按钮。而且 pnpm-lock.yaml 会把
git 源解析出的 commit 钉死(resolution: {commit, type: git}),所以后台也不会自己挪。
面板里:卸载(要确认)→ 「+ 添加插件」填同一个 github: 地址 → 安装 → 立即启用 → 重启。
命令行同理:
dsh plugin --profile desktop remove dsh-custom-search
dsh plugin --profile desktop add github:Anixuil/dsh-custom-search#main
github: 源拉的是默认分支的当前提交,所以重装即拿到最新代码。
要省掉反复重装,就按上面说的换成本地目录安装——之后只需重启。
从 ≤0.1.0 升上来的注意:旧版的 patch 写法会让面板卸载永久失败(报 bundle-in-use),卸载那一步走不通。照「卸载」一节末尾的手工步骤摘掉旧版,再装 0.2.0。
兼容性
provider 契约(ctx.web.registerSearchProvider 与返回结构)是照着 @deepseek-ai/dsh- 0.2.0-rc.2(DeepSeek Harness Desktop 0.2.0-rc.2)的源码实现的,并对该版本做过离线契约验证。
DSH 仍在 developer preview:apply(ctx) 规范稳定,但 ctx.web 这类服务在预览版之间仍可能调整。升级 Harness 后若搜索报 WEB_PROVIDER_ 错误,先对照 @deepseek-ai/dsh-web 的 registerSearchProvider 签名和 WebRuntime.Config 字段。
依赖:为什么 peerDependencies 是必需的
宿主半侧会 import 五个 Harness 自己的包(schemastery——Config 需要它; dsh-credentials / dsh-launch-environment——解析 key;dsh-web——错误码; dsh-typert-protocol——连通性探针)。这些包一个都不在插件的 node_modules 里、也不该在:
- 重复安装一份 @deepseek-ai/cordis / Typert,会让 instanceof 判定的对象身份对不上,探针服务在宿主眼里就不是它认识的那个 Service;
- 从 npm 拉这些内部包也拉不到与当前运行时同版本的副本。
DSH 的解法是 dsh-app-boot 给 profile 装的一层模块解析拦截(profile-resolution-bootstrap.js):来自 profile 或 linked 目录的模块,其 @deepseek-ai/ 导入会被路由到当前安装的那一份。
但它只对 package.json 的 peerDependencies 里声明过的名字生效——linked 安装( link:D:\...)走 routeLinked,逐个祖先目录读 package.json 的 peerDependencies 才决定拦截。
没声明就是解析不到,而失败方式极其安静:插件照常加载、搜索照常能用,只有那个 import 背后的功能凭空消失。
这正是 0.3.x 的真实故障:@deepseek-ai/dsh-typert-protocol 没声明,installProbe 的 await import() 抛 ERR_MODULE_NOT_FOUND,探针静默退出,界面只显示「宿主没有提供探针」——看起来像"部署不支持",实际是清单少了一行。
dev/validate.mjs 现在会拿源码里的 @deepseek-ai/ 导入和 peerDependencies 对账,两边任一侧多一条少一条都直接失败。
范围写成 是刻意的:dsh-app-boot 会拿 @deepseek-ai/dsh- 的 peer 去比运行版本,填具体范围等于每次 Harness 升级都要用户去开一次 exact-version 豁免。
@deepseek-ai/schemastery 不在该检查范围内,但一并写 保持一致。
这份清单是重启即生效,不用重装。 readPeerNames() 是每次解析时现读磁盘上的 package.json(readFileSync,无缓存),而"这是 linked 目录"这个事实来自 profile 的 link: 依赖,本来就在——所以本地目录安装改完 package.json 重启一次即可。
只有 github: 源安装才需要卸载重装(它拉的是仓库快照,本地改什么都不影响它)。
声明和导入对上了还不够:声明的那一份得真的在安装包里,否则路由过去同样 ERR_MODULE_NOT_FOUND。这个只有当前安装包能回答,所以有 dev/peer-closure.py ——它把安装包的依赖闭包从 app.asar 里按宿主的走法重算一遍(0.2.0-rc.2 上是 513 个包),再逐个核对本插件的 peer:
python dev/peer-closure.py # 全部 5 个 peer 都能被当前安装包供给 ✓
python dev/peer-closure.py --all # 顺带列出作用域里所有 @deepseek-ai/
(node_modules/ 里那份 @deepseek-ai/{cosmokit,schemastery} 只是离线跑 dev/ 测试用的, .gitignore 忽略、files 白名单排除。声明了 peer 之后,运行时会优先走上面的拦截、取安装包那一份,所以不会出现"插件自己带了一份旧 schemastery"这种漂移。)
配置
在插件页改(推荐)。侧栏「插件」,三个入口,进去是同一张表单:
- 点开 dsh-custom-search 组合包 —— 配置表单就在包详情页里。这是最稳的一个入口,因为它和这一行在同一个页面上。
- 「官方」区块里的「自定义搜索引擎」卡片 —— 点开是一个独立配置页。
- 或者点开组合包 → 组件行 custom-search 右侧的配置按钮,进行详情页。
表单里 12 个字段全在,引擎还能用一排胶囊一键切换。点保存即写入 profile 的 patch 层并由 Loader 应用,不用重启。
装完要重启一次 Harness,卡片才会出现。浏览器半侧是前端启动时加载的模块:安装是在前端启动之后才落地的,热重载不会把它补进来。
API Key 不进设置文件:它经凭证服务写入,引用名默认取该引擎约定的环境变量名(Brave 是 BRAVE_API_KEY),落点是 ~/.dsh/.credentials.yaml 的 refs: 段——和模型 provider 的 VOLCENGINE_API_KEY 并排。填一次就持久保存,profile patch 里只留引用名,key 不会进版本库。
测一下配好没有
表单最下面有个测试连接按钮,点一下就知道"我配的到底能不能用"。它分两层:
1. 本地检查(不联网,立即出结果):引擎 id 是否有效、这个引擎要不要 key、key 到底配上了没有 (问凭证服务,不是看输入框)、endpoint 是否是合法的 http(s) 地址、兜底链里有没有拼错的引擎、数值有没有越界。这些正是真实请求只会回一个含糊厂商错误的地方。
2. 真实请求:用已保存的配置走一遍 agent 实际使用的 provider,真跑一次搜索。会消耗 1 次搜索配额。
结果按 ✓ / ✗ / ! 逐条列出,真实请求那一段是明细,不是一句"成功/失败":
✓ Brave Search 返回 3 条结果,用时 312 ms。
密钥来源:凭据服务(BRAVE_API_KEY) GET https://api.search.brave.com/res/v1/web/search HTTP 200 · 298 ms
1. 第一条结果的标题
2. …
明细里的每一项都对应一个只有它才能回答的问题:
| 明细 | 它能回答的问题 |
| 方法与端点(已去掉查询串) | 我真的在打这个厂商、这个路径吗?自建 SearXNG 打的是不是我的实例? |
| HTTP 200 / 401 / 403 / 429 / 5xx | key 无效(401/403)、触发限流(429)、还是厂商挂了(5xx)? |
| 往返耗时 | 是超时设置太紧,还是网络真的慢? |
| 密钥来源 | 配置里的字面密钥 / 凭据服务 / 启动环境 / 进程环境变量 / 哪里都没找到——"没配 key"和"配了但这次没解析到"是两个完全不同的故障,修法也不同 |
| 每一跳各一条 | 兜底链生效时,主引擎那一跳为什么被跳过(返回 0 条,还是报错) |
兜底链里每个被尝试过的引擎都会留一条记录,所以"回退成功了"不会掩盖"主引擎其实是坏的"。
表单里有未保存的改动时会另行提示——本地检查看的是你的输入,真实请求用的是已保存的配置。
按钮下面那行说"只剩本地检查"时,它会点名原因。 页面能看见自己的四种失败,所以四种各说各的: $mount 不存在(这个 Harness 构建不对)、挂载被 Gateway 拒绝(把 Gateway 的错误原文带出来)、挂载成功但读不到命名空间(列出 ctx.remote 上实际有什么)、挂载了但没有这个方法。
这句含糊话以前是一句包打天下,而且把责任推给宿主——宿主恰恰是页面看不见的那一方。
宿主侧的原因另有去处:宿主进程控制台会打 [dsh-custom-search] 连接探针未安装:… 并点名缺哪个包。
这是有意的降级:探针永远不影响搜索本身,最坏情况是少一个按钮。
没有界面时:直接改 profile 的 patch 层
$DSH_HOME/profiles/desktop/cordis.patch.yml 在所有 bundle 层之后应用,优先级高于插件自带的默认值:
- id: custom-search
name: dsh-custom-search
config:
engine: brave
- 只写要改的字段。没写的字段回落到 Config schema 的默认值(每个字段都有默认值)。
- 手改后重启 Harness。
- 同一个 id 已存在于 profile patch 时改那一条,不要重复添加。
为什么配置页要插件自带? DSH 的插件页只渲染插件自己注册了配置页的插件——官方那四个配置页(shell 执行器、agent loop、子智能体、DeepSeek 搜索提供方)各来自一个伴生包,设置里的「插件」分区只是只读清单。
本插件把这一半放在 client.js,三个入口全注册:
| 槽位 | 寻址字段 | 渲染成 |
| plugins.bundle.config | key = 包名(keyed 槽) | 包详情页里的配置表单 |
| plugins.item | id(list 槽) | 插件页上一张能直接点开的卡片,带独立页面 |
| plugins.row.config | key = <包名>#<行 id>(keyed 槽) | 行详情里的配置表单 |
三个都得注册,少一个就少一个入口。 插件页对每个槽都是"没注册就整块不渲染",而不是报错:包详情页的配置区由 ledger.bundles.has(包名) 控制,为假时整个 section 直接从树里拿掉。0.3.1 之前只注册了后两个,所以从组合包点进去看到的是一个白板。
三者都只在宿主在服务 custom-search 这个设置命名空间时才挂载(configForms.whileServed),所以没装/没启用的部署上看不到任何痕迹。
没有它们,这一行就只有启停开关——这正是本插件 0.1.0 的状态。
字段
| 字段 | 默认 | 说明 |
| engine | searxng | 主搜索引擎,可选 searxng / tavily / exa / brave / serper / bing / google |
| fallback | 空 | 逗号分隔的备用引擎,按顺序兜底(主引擎报错或返回空结果时启用) |
| endpoint | 空 | 主引擎的接口基址。留空则用该引擎的官方地址;自建 SearXNG 填 http://127.0.0.1:8080 |
| endpoints | 空 | 按引擎分别指定基址的 JSON,例如 {"tavily":"https://gw.internal/tavily"},优先级高于 endpoint |
| apiKey | 空 | API key 字面量。标记为 secret,不会回显、不进模型上下文 |
| apiKeyEnv | 空 | 凭证引用名。留空则用该引擎约定的环境变量(见下表) |
| googleCx | 空 | 仅 Google CSE 需要:Programmable Search 的搜索引擎 ID |
| searchDepth | basic | 仅 Tavily:basic / advanced(advanced 消耗更多额度) |
| maxResults | 8 | 单次搜索最多返回多少条来源,上限 20 |
| timeoutMs | 20000 | 单次搜索超时(毫秒) |
| language | zh-CN | 语言/地区提示,各引擎会映射成自己的参数。Brave 例外:它的 search_lang 只认 50 个固定标签(中文须写 zh-hans / zh-hant,日语是 jp、挪威语是 nb),本插件会自动换算;换算不出来的语言会省略该参数并在连接报告里说明,而不是发出去等 422 |
| safeSearch | moderate | off / moderate / strict,引擎不支持时忽略 |
各引擎怎么准备
| 引擎 | 是否要 key | 约定的环境变量 | 备注 |
| searxng | 否 | — | 自建或公共实例,必须在实例的 settings.yml 里开启 json 输出格式,否则接口返回 403 |
| tavily | 是 | TAVILY_API_KEY | 为 AI 检索设计,按次计费 |
| exa | 是 | EXA_API_KEY | 语义检索,可直接拿到正文片段 |
| brave | 是 | BRAVE_API_KEY | 独立索引,免费额度较宽裕 |
| serper | 是 | SERPER_API_KEY | Google 结果的低延迟代理 |
| bing | 是 | BING_API_KEY | Azure 认知服务 Web Search |
| google | 是 | GOOGLE_API_KEY | 还需填 googleCx |
key 的解析顺序:apiKey 字面量 → 凭证服务(设置页写入的) → 启动环境 → 进程环境变量。
apiKey / apiKeyEnv 只作用于当前选中的主引擎。fallback 里的引擎是别的厂商,各自从自己的环境变量或凭证服务取 key——例如 engine: searxng + fallback: tavily 只需要额外准备 TAVILY_API_KEY。
反过来说:apiKeyEnv 留空才是常态。留空时引用名跟着主引擎走,换引擎会自动跟着换;一旦显式填了引用名,它就压过引擎约定名(engine: tavily + apiKeyEnv: BRAVE_API_KEY 就会拿着 Brave 的引用名去解 Tavily 的 key)。所以不要在配置里硬写引擎专属的引用名,除非你确实要把 key 存在别的名字下。
常见组合
全本地、不花一分钱(推荐先跑通这个)
engine: searxng
endpoint: http://127.0.0.1:8080
要更稳,主引擎挂了自动兜底
engine: searxng
endpoint: http://127.0.0.1:8080
fallback: tavily
安装后的实际改动
cordis.patch.yml 里有两类条目,区别很关键:
- insert: # ① 新行:本组合包「拥有」的行
- id: custom-search
name: dsh-custom-search
config: { ... }
② 覆盖行:改的是别的组合包已经声明的行(dsh-base 声明了它们)
- id: web
name: '@deepseek-ai/dsh-web'
config:
searchProvider: custom-search
fetchProvider: http # 必须保留:patch 是整行替换,不是深合并
- id: web-search-deepseek
name: '@deepseek-ai/dsh-web-search-deepseek'
disabled: true
为什么这两行不能放进 insert:。 insert: 的语义是「插入新行」—— loader 以空列表为基准套用所有层,所以 insert: 里的行会被直接追加进树。
而 web / web-search-deepseek 这两行由 @deepseek-ai/dsh-base 声明,id 已经被占用;再 insert: 一次只会多出同名副本,不会覆盖原来那行。
更隐蔽的后果在卸载:插件管理器把「insert: 里的行」记为本组合包的组件(bundleRows),只要其中任何一行还挂着,就拒绝卸载并报 bundle-in-use(「其他配置仍在使用这个组合包的组件,请先停用它们」)。
web 那行的正本属于 dsh-base,永远不会下线——于是本组合包会永远卸不掉。
写成顶层覆盖就没有这个问题:覆盖行不被算作本组合包的组件,卸载时随层一起失效。
两个容易踩的点:
1. patch 是整行替换。web 那一行必须把 fetchProvider 一起写回去,否则 web_fetch 会失去后端。
2. 关掉官方提供方是为了避免歧义。服务层在没 pin id 时会要求「恰好一个可用提供方」;两个都在就会报 WEB_PROVIDER_AMBIGUOUS。想让官方搜索和本插件并存,把 disabled: true 改成 false,并确保 searchProvider 始终有值。
回到官方搜索
最省事:在插件页把 dsh-custom-search 这个组合包整个关掉。它的 patch 层随之失效,那两条覆盖一起消失——web 的 searchProvider 回落、web-search-deepseek 不再被 disabled,官方搜索自动回来。
想保留插件、只临时切回官方搜索:在 profile 的 cordis.patch.yml 里加两条覆盖:
- id: web
name: '@deepseek-ai/dsh-web'
config:
searchProvider: deepseek-official
fetchProvider: http # 整行替换,必须复述,否则 web_fetch 失去后端
- id: web-search-deepseek
name: '@deepseek-ai/dsh-web-search-deepseek'
disabled: false # 必须放开,否则 pin 的提供方本身是关闭的
卸载
面板里点组合包卡片上的卸载(要确认)。用命令行的话:
dsh plugin --profile desktop remove dsh-custom-search
两条路都会连组合包一起摘掉,它携带的两条覆盖随之消失,不需要手工清理。
要清理的是你自己写进 profile patch 的那些条目(见上面的「配置」)——组合包没了以后 id: custom-search 寻址不到任何一行,会被静默跳过;留在文件里只是垃圾。
如果面板报「其他配置仍在使用这个组合包的组件」
这条报错对应宿主的 bundle-in-use:管理器认为该组合包还有行挂在运行时里。
0.2.0 之前的版本一定会撞上它——那时 web / web-search-deepseek 被写在 insert: 里,而它们的正本属于 dsh-base,永远不会下线,卸载检查因此永远过不去(见「安装后的实际改动」)。
0.2.0 起改成顶层覆盖,面板卸载正常。
已经被卡住的旧版:不用拆 profile,换掉那一份文件就够。 卸载检查读的是 已安装副本里的 cordis.patch.yml,把它换成 0.2.0 的,判据立刻从 3 行降到 1 行:
cd ~/.dsh/profiles/desktop/node_modules/dsh-custom-search
cp "D:\code\self\all\dsh-custom-search\cordis.patch.yml" .
然后回面板点卸载 —— 它会自己跑 pnpm remove 并整理 package.json / lockfile
想先确认再动手,用 dev/bundle-rows.mjs 对着这份文件跑一次:它照宿主的语义算出「贡献行」,凡不是本包的行都会被标出来 —— 那正是 bundle-in-use 的判据。
如果报的是另一个码 —— reasonStopProfile(「这个 profile 没有启用 HMR,正在使用的包要停止后用 dsh plugin 卸载」)—— 说明该组合包是本次启动时加载的。
先重启 Harness 让 startedBundles 复位,再卸载。
两条路都不通时的兜底(桌面端没有 dsh 命令行,只能改文件):
1) 停掉 Harness
2) 从依赖里删掉这一行 ~/.dsh/profiles/desktop/package.json 的 dependencies.dsh-custom-search
3) 删掉 profile patch 里 id: custom-search 那个条目(若有) ~/.dsh/profiles/desktop/cordis.patch.yml
4) 删目录,并删掉 dsh.profile.bundles 里的 dsh-custom-search
rm -rf ~/.dsh/profiles/desktop/node_modules/dsh-custom-search
5) 重启 Harness —— 官方搜索自动回来
安全说明
- 插件以 Harness 进程的权限运行,安装前请自行审阅源码——这是所有 DSH 插件的通用前提。
- 携带 API key 的请求使用 redirect: 'error',重定向会在发出前被拒绝,避免凭证被转发到未核实的主机。免 key 的 SearXNG 允许跟随重定向(常见的 http→https)。
- 返回的 URL 只接受绝对 http(s) 地址,其他一律丢弃,不会出现在给模型的结果里。
- apiKey 标记为 secret:设置页不回显,也不会进入模型上下文。
- 搜索结果是外部不可信内容,工具层会照常标注为 untrusted。
已知限制
- 「测试连接」的探测层可能缺席。本地检查(字段合法性、凭证状态,不联网)永远可用;「真实请求」要经 Remote 调用到宿主,靠 Typert 的源模式发现,宿主没挂载那套包时只剩本地检查。
两条路都不影响搜索本身——最坏情况是少一个按钮。面板会说明是哪一种缺席(见上文「测一下配好没有」),清单层面的原因见「依赖:为什么 peerDependencies 是必需的」。
- 探针走的是 Typert 的源模式兜底(手写 descriptor + 原型标记),因为本包没有构建步骤、产不出 dsh-typert-generator 的那两份文件。这是 DSH 的内部机制,大版本升级后可能要跟着调;调不动时的表现是面板点名说明,而不是搜索坏掉。
- 明细里的端点是去掉查询串的 scheme://host/path:Google CSE 把 key= 拼在 URL 上,时限报错也会把整条 URL 写进消息,所以报告生成前统一脱敏(engines.js 的 publicEndpoint / redactUrls,smoke.mjs 有对应断言)。
- 换引擎不会迁移密钥。apiKeyEnv 留空时引用名跟着主引擎走(brave → BRAVE_API_KEY),所以从 Brave 换到 Tavily 要在表单里另填一个 key;旧的那个仍留在凭证服务里。
- engine 在 schema 里是 union,表单上是文本框 + 一排胶囊。填了 union 之外的值保存会失败,表单会保留草稿让你改(SettingsForm 的既定行为)。
- 仍可以把密钥明文写进 profile patch 的 apiKey 字段(只在本机),但推荐走表单/凭证服务。
- ctx.web 的搜索请求只有 query 和 maxResults 两个参数,所以没有时间范围、域名过滤、站点限定这类 provider 级控制(这是服务层的既定边界,不是本插件的取舍)。
- search() 是每次查询一次请求。fallback 里多个引擎串行尝试,最坏情况耗时是各超时之和。
- available() 必须是同步、离线的——服务层在选中 custom-search 时就是这么调它的——所以它只能回答「有没有声明一个凭证引用」,回答不了「引用背后有没有值」:那个答案只有异步的凭证服务有。
声明了引用就算可用,判的是「有没有解析器」而不是「解析器当前有没有值」;官方 DeepSeek 搜索提供方用的也是这个判据(它检查自己的 resolver 闭包是否存在)。代价是缺 key 会推迟到 搜索时才报错——报的是引擎名与引用名,比在请求发出前被拦下更有用。process.env 里有值不是必要条件,因为 key 的正常落点在凭证服务;未知引擎与不合理的 timeoutMs 仍判为不可用, SearXNG 实例挂了同样体现为搜索报错。
目录结构
dsh-custom-search/
├── index.js 宿主半侧:Config schema、provider 注册、凭证解析、兜底链、连通性探针 ├── client.js 浏览器半侧:插件页的配置表单(三个入口)、「测试连接」按钮与缺席诊断 ├── engines.js 引擎适配表:每个引擎的 build(怎么发请求)+ parse(怎么读响应)+ 脱敏 ├── cordis.patch.yml 组合包 patch:1 行 insert(本插件的行)+ 2 条对既有行的覆盖 ├── package.json dsh.bundle.patch + dsh.client 声明、peerDependencies、icon、locale、files 白名单 ├── locale/ 面板显示用的中英文标题与描述 ├── icon.svg ├── docs/ README 用的安装截图 └── dev/ 离线验证套件(不随插件分发,files 白名单已排除) ├── smoke.mjs 20 项:provider 契约、可用判据、归一化、去重、兜底、取消、错误码、Brave 语言映射与参数省略、探针报告与脱敏 ├── validate.mjs 57 项:清单、patch 方言、行归属(insert / 覆盖)、patch 与 Config 对齐、peerDependencies 对账、Brave 语言枚举、可用判据(引用即解析器)、浏览器半侧的契约与渲染、探针两端一致性与缺席文案 ├── degraded-probe.mjs 子进程跑的降级用例:没有 Typert 包时探针缺席但 provider 照常 ├── stubs/ 只在 dev 里顶替 @deepseek-ai/dsh-typert-protocol(含注册用的 resolve hook) ├── bundle-rows.mjs 对着任一份 patch 复现卸载判据(bundle-in-use),非本包的行会标出来 ├── peer-closure.py 用安装包的依赖闭包核对 peerDependencies 是否真的可供给 └── asar.py 从 DSH 安装包里抽源码/依赖的解析脚本
开发
装成本地目录,之后改代码只需重启 Harness,不必反复卸载重装:面板「+ 添加插件」填本仓库的绝对路径,或
dsh plugin --profile desktop add D:\code\self\all\dsh-custom-search
本地目录是 link: 而不是复制,所以 index.js(宿主半侧)改完重启生效, client.js(浏览器半侧)改完重启后刷新页面即可。改稳定了再换回 github: 地址。
新增一个搜索引擎 = 在 engines.js 的 ENGINES 里加一个条目,声明 build 和 parse,再到 index.js 的 Config.engine union 里加上这个 id,最后在 client.js 的 ENGINES 里补一行(浏览器半侧够不到宿主半侧的源码,这张表是刻意重复的, validate.mjs 会盯着两边不许漂移)。
验证
dev/ 下的套件直接 import 上级目录的真实代码,没有副本需要同步:
cd dev
node smoke.mjs # 起本地 mock server,用 stub ctx.web 驱动真实插件模块(含探针报告与脱敏)
node validate.mjs # 清单、patch 方言、两端声明一致性,以及浏览器半侧的契约与渲染
node bundle-rows.mjs <某份 cordis.patch.yml> # 复现宿主的卸载判据,定位「卸不掉」
python peer-closure.py # 安装包到底供不供得起本插件声明的每个 peer(读 app.asar)
degraded-probe.mjs 不用手动跑——smoke.mjs 会用子进程拉它起来(必须另起进程,因为 resolve hook 是按进程注册的)。
validate.mjs 里那条 Brave 语言断言值得单说:language 是本插件唯一一个发给外部服务、却被对方用封闭枚举校验的参数,而 Brave 对不在枚举里的值直接回 422 —— 整个搜索失败,不只是丢一个提示。默认值 zh-CN 恰好是最容易踩雷的形状(Brave 没有裸 zh)。
所以断言的不是「每个映射对不对」这一件事,而是更硬的不变量:*braveSearchLang() 的返回值
要么是 undefined,要么必须是枚举成员,用 40 个越界探针(含 id-ID、xx-YY、ja、 no、pt、空串、null、对象)去撞。想验证这条护栏真的会咬,就把 braveSearchLang 改回 language.split('-')[0] —— 两个套件都会当场失败。
validate.mjs 里另一条宿主机侧的不变量是可用判据。服务层选中 custom-search 时会同步调一次 available()(dsh-web 的 resolveProvider 里就是 if (!provider.available())),而那一问只能回答「有没有声明一个凭证引用」——凭证服务是异步的,且到那时已经没有第二个提供方可退。原实现在这条判据上多加了一句:引擎约定的引用名还必须在 process.env 里有值。于是 把 key 存在凭证服务里这个最常见、也正是本插件设置页自己写出来的部署判成了不可用: configured web provider "custom-search" is registered but unavailable,请求一行都没发出去。
所以断言两条不变量:每个需 key 引擎都必须声明约定的引用名(枚举 ENGINES 逐个查,将来加引擎自动纳入),以及 available() 的实现里不得出现 process.env(只看代码——注释里讨论这个坑不算)。参照系是官方的 dsh-web-search-deepseek:它的 available() 检查的是自己的 resolver 闭包存不存在,而不是去解出值。想看这条护栏会不会咬,就把 available() 改回要求 process.env 有值 —— 两个套件都会当场失败(validate 报「不得以进程环境为前提」, smoke 报「未配置任何东西时也应自认可用」)。
validate.mjs 里浏览器半侧那段最值得一提:它真的把 client.js 跑了两遍——先在 vm 里用 stub 的 require 物化工厂,拿一份假 ctx 调 apply(ctx),检查它到底注册了什么;再拿一份假 React(会真建元素树、真展开函数组件)把配置页组件渲染一遍。
第一遍断言注册的形状:三个槽位各自的寻址字段(plugins.bundle.config 用包名、 plugins.item 用 id、plugins.row.config 用 <包名>#<行 id>)、卡片 id 不与官方占用的四个撞车、三个入口共用同一份表单、页面组件不自带 React hook、注入的 hooks.<key> 与组件读取的 props.use<Key>() 拼写一致。这些全都只会静默失败——写错一个字符,插件行就只剩启停开关,界面上没有任何报错。
第二遍渲染才是抓真 bug 的那一遍:组件抛错时槽运行时会把它从那一格退役,渲染成空白,不留任何信息。所以「能不能渲染」必须真的渲染才知道。0.3.1 修的那个 bug(connectionPanel 的签名漏了 state,函数体里 state?.writable 于是成了未声明标识符——可选链只护 null,不护未声明变量)正是靠这一遍抓出来的。渲染这遍还覆盖了三条分支:带探针、不带探针、以及探针已出报告(检查行 + 来源列表 + 逐次尝试明细)。
最后那条分支的断言值一提:validate.mjs 从 client.js 里读出真实的中文模板当渲染时的 t,再把渲染结果与模板对起来。用恒等函数当 t 的话占位符根本不会被填充,明细渲染成
key: {ref} 这种半成品也照样「渲染成功」——那样的断言等于没写。同时它先把函数组件展开一遍
拿到文本(textOf),再渲染一遍(stageReport 可重复调用),所以同一份报告会被走两次,次数多算一次就会当场露馅。
探针缺席的理由也照同一套断言。 那四种成因各有各的句子,脚本逐个渲染、逐个比对模板,再断言四句话互不相同——所以「用一句含糊话糊过去」是过不了的。它还把 describeProbeAbsence 会报的成因与 ABSENCE_KEY_OF 的句子双向对账:报了没句子的成因而渲染出裸键、或写了句子却没人用,两种都当场失败。
探针那几项是同类问题的另一面。 测试按钮要经 Remote 调用才能碰到宿主,而这个包没有构建步骤, @deepseek-ai/dsh-typert-generator 本该产出的那两份声明只能手写;写错命名空间不会报错,只会让面板安静地退回「只有本地检查」。所以校验脚本把两半对起来看:命名空间与包名、descriptor 的 id / service / namespace / method、参数与结果的 codec 是否满足下面那三套校验器、宿主方法的参数名与 descriptor 的 wire(源模式是从函数源码文本里解析参数名的,所以不能有默认值、解构或 rest),以及原型标记键是否与 Gateway 读取的那个常量逐字相同。
探针的 codec 值得单独说,因为它让我猜错过两轮。 $mount 看起来是一道闸门,其实它后面还站着两道,三套校验器对「一个 strict codec 该长什么样」的要求并不一致:
| 校验器 | 要求 |
| Gateway requireStrictInputs | 只看 codec.mode !== 'strict' |
| Typert registry validateCodec | 另要求 typeSymbol 非空、create 是函数 |
| Typert loader requireStrictCodec | 同上,且 decode / encode 若写了必须是函数 |
只写裸 { mode: 'strict' } 能过第一道、卡在第二道,而它报出来的是 Cannot read properties of undefined (reading 'length') —— validateNonempty 去读 typeSymbol.length,一个字都不提是哪个字段,所以看起来像个内部错误。validate.mjs 的 assertStrictCodec 就是把这套要求就地转录成断言,下次 DSH 收紧规则能在离线套件里当场发现,而不是又靠用户截图。
顺带说明 create() 产出的 schema 是原样放行:宿主给 SRC 来源的参数合成的是 codec.mode === 'src-json',而 decode 只在 strict 分支才取用 codec —— 这条路上工厂根本不会被调用,它只是为了让三道校验器都满意。
smoke.mjs 补的是另一半,而且两条路都跑真代码:进程内 register() 一个 resolve hook (dev/stubs/hooks.mjs),只把它一个 specifier 顶替掉,于是 installProbe 真的挂载起来,探针出报告那一串(HTTP 状态、脱敏端点、往返耗时、每跳成败、密钥来源与引用名)才有东西可断言;另一半「压根没有 Typert 包」的降级场景交给子进程 degraded-probe.mjs,它不带这个 hook,所以 import() 失败得和真实部署一样。断言的是:apply 照常注册 provider 并返回 disposer, console.warn 恰好报一次且原因里点名缺失的包——探针缺席不牵连搜索。
顶替品做成 node:module 的 hook 而不是往 node_modules/ 里塞个假包,是因为后者会真的遮蔽真包(哪天本机装上了 Typert,测试会继续跑在假货上却毫无提示)。hook 只重定向一个 specifier, @deepseek-ai/schemastery 走的还是真包。
行归属那几项同理:脚本会照 applyEntryPatches 的语义把各层合成一遍,断言 insert: 里只有本包自己的行、合成后 web / web-search-deepseek 各只有一份。
把覆盖行误写进 insert: 时,运行期只表现为「卸载按钮报错」,这里会当场失败。
bundle-rows.mjs 是这条断言的现场版:同一套语义,但可以指向任意一份 patch ——包括 profile 里那份已安装的副本,所以线上被卡住时也能直接查,不必先复现安装。
这些脚本需要 js-yaml、@deepseek-ai/schemastery、@deepseek-ai/cosmokit (已放在本目录的 node_modules/,被 .gitignore 忽略)。
@deepseek-ai/ 那几个是 DSH 自带的 vendor 包,可从本机安装里抽:
python dev/asar.py list "node_modules/@deepseek-ai/schemastery"
python dev/asar.py extract "node_modules/@deepseek-ai/schemastery" node_modules
asar.py 默认指向 %LOCALAPPDATA%\Programs\DeepSeek Harness\resources\app.asar;换机器时改脚本顶部的 ASAR 常量即可。
许可证
MIT预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
Anixuil/dsh-custom-search
用 SearXNG 等检索 API 替换内置网页搜索
相关插件 · 搜索 / 抓取
LLM 知识平台Tencent/WeKnora
概览 ·快速开始 ·新特性 ·功能 ·客户端 ·文档 ·开发
★ 36,161已汉化DSH
72.1
智能体记忆系统MemTensor/MemOS
MemOS 2.0 Stardust星尘
★ 12,195已汉化DSH
71.2
跨代理记忆库zilliztech/memsearch
跨平台语义记忆,专为 AI 编程智能体打造。
★ 2,799已汉化DSH
70.9
技能语料库EverMind-AI/SkillCorpus
SkillCorpus 能给你什么
★ 694已汉化DSH
69.7
代码图谱记忆syncable-dev/memtrace-public
你的智能体值得拥有结构化记忆。
★ 509已汉化MCP
67.9
AnySearch 搜索插件anysearch-team/anysearch-dsh
AnySearch 网页搜索服务商,为 DeepSeek Harness(DSH)提供高级搜索工具
★ 457已汉化DSH
66.6