ctx.web 接缝的零配置 Exa 网页搜索提供方:无 API key 时走匿名 MCP 兜底,配 key 时走 REST 搜索。
安装
# npm 包(预构建)
dsh plugin --profile web add @tonydua/dsh-web-search-exa
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:TonyDua/dsh-web-search-exa
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
English | 简体中文
给 DeepSeek Harness(dsh)加上 Exa 网页搜索。
dsh plugin --profile web add @tonydua/dsh-web-search-exa
重启 dsh web 就能用。不用配 API key,不用改配置,不用选 provider。
背景,了解即可:
- Exa 是一个搜索 API。它按关键词或语义检索网页,返回可引用的来源和摘要,不生成答案。它提供 REST API,也运营一个免认证的公共 MCP 服务器。
- 官方的
dsh-web-search-exa是 dsh 的 Exa 搜索提供方。它走 Exa 的 REST API,必须配置 API key 才有用。 - 本包基于官方包改的。 REST 路径的实现与官方一致,补充了一条免 key 通道:没有 key 时改走 Exa 的公共 MCP 服务器,配了 key 仍走 REST。匿名接入方式参考了 oh-my-pi 项目,见致谢。
默认情况下不用管这几件事。只有同时用官方包,或 dsh 报错说 provider 有歧义时,才需要看选中提供方。
使用 deepseek-v4-flash 在 DeepSeek Harness(dsh)内开发。
特性
- 免 key 可用。搜索经由 Exa 的公共 MCP 服务器(
mcp.exa.ai/mcp),不携带任何凭据。 - 配 key 自动升级。设置
EXA_API_KEY后自动切到 ExaPOST /searchREST API,额度更高,行为不变。 - 即插即用。注册进 dsh
ctx.webseam,模型侧的web_search和web_fetch工具、提示词区段、结果卡片都无需改动。 - 装上就能用。不装官方包时不需要选 provider,默认自动生效。
- 失败时能退让。匿名通道连续失败后,插件会把自己标记为不可用,让 dsh 有机会换别的 provider,而不是每次搜索都硬失败,详见搜索失败时会发生什么。
安装
三种方式选一种。方式只决定代码从哪来,装完都一样。
从 npm 安装。 v0.1.4 起自带 dsh.bundle manifest,bundle patch 会自动插入 provider 行,无需手动改 patch。
dsh plugin --profile web add @tonydua/dsh-web-search-exa
从 GitHub Release 安装。 同一份 tarball,npm 不可达时用。
dsh plugin --profile web add https://github.com/TonyDua/dsh-web-search-exa/releases/latest/download/dsh-web-search-exa.tgz
从仓库安装。 跟随 main,包含尚未发布的改动。
dsh plugin --profile web add github:TonyDua/dsh-web-search-exa
本地开发目录的装法相同,把包名换成路径即可:dsh plugin --profile web add ../plugins/dsh-web-search-exa。
装完重启 dsh web。多数情况下这就是全部步骤。
选中提供方
不装官方包时不用看这一节。
dsh 的 seam 每次搜索前会挑一个可用 provider。只有一个可用时自动选中,多于一个时抛 WEB_PROVIDER_AMBIGUOUS,要求你指定。所以只有在下面两种情况才需要动手:
- 同时装了官方包:两个包都注册 provider id
exa,dsh web启动就会报WEB_DUPLICATE_PROVIDER。必须先给本包改一个 id,见与官方包共存。 - 报
WEB_PROVIDER_AMBIGUOUS:说明有另一个可用 provider。指定一个即可。
指定方式二选一:
# $DSH_HOME/profiles/web/cordis.patch.yml
- id: web
name: '@deepseek-ai/dsh-web'
config:
searchProvider: exa
或用环境变量 $DSH_WEB_SEARCH_PROVIDER=exa。
改完重启 dsh web。模型侧的 web_search 工具会自动走选中的 provider,不用改工具配置。
发布产物。 CI 打包本版本的 tarball,在每一个受支持的 dsh 版本上验证,挂到 GitHub Release,并把这个产物本身发布到 npm。所以 Release 附件和 npm 上的 tarball 是同一个文件,而不是两次恰好一致的构建。
profile 安装告警。 dsh profile 默认 autoInstallPeers: false,而 harness 自身的服务由 dsh 宿主在运行时提供,不经 pnpm 解析。如果 dsh plugin add 报 peer 警告,把下面这段加进 profile 的 pnpm-workspace.yaml:
peerDependencyRules:
ignoreMissing:
- '@deepseek-ai/cordis'
- '@deepseek-ai/dsh-*'
配置
| 配置键 | 默认值 | 含义 |
|---|---|---|
apiKey |
未设置 | Exa API 密钥字面值。为空或缺失时启用匿名 MCP 路径。 |
apiKeyEnv |
EXA_API_KEY |
未设置字面 apiKey 时读取的环境变量名。 |
baseURL |
https://api.exa.ai |
Exa API 基础 URL。带 key 的 REST 路径会追加 /search,与官方 dsh 提供方一致。 |
apiURL |
未设置 | 已弃用的完整 REST 端点别名,设置后优先于 baseURL。 |
mcpURL |
https://mcp.exa.ai/mcp |
Exa 托管 MCP 端点,匿名路径使用。 |
searchType |
auto |
REST 检索模式:auto、keyword 或 neural。 |
numResults |
未设置 | 请求未携带 maxResults 时的默认结果数。 |
highlightsPerResult |
1 |
REST 路径每个结果请求的 highlight 句子数。 |
providerId |
exa |
注册进 ctx.web 的提供方 id。仅当本包与官方包同时安装时才需要改,见与官方包共存。 |
配置写在哪里:编辑 $DSH_HOME/profiles/web/cordis.patch.yml 里本插件的 config,然后重启 dsh web。也可以用环境变量 EXA_API_KEY 和 $DSH_WEB_SEARCH_PROVIDER。apiKey 标记了 role('secret'),任何 describe() 响应都不会暴露它的值。
在 Web 面板中的呈现
当前版本的配置入口在 profile 补丁层,不在 Web UI,没有可编辑的界面入口。Settings UI 只渲染客户端插件为固定命名空间(shell、agent-loop、web-search-deepseek)手工注册的卡片,对任意插件命名空间没有通用表单。当前实际情况:
- 插件清单(Settings → Plugins):启用后自动出现
web-search-exa条目。清单直接读取 Cordis loader 的实时条目,无需额外代码。 - 设置命名空间(服务端):插件通过
ctx.settings.installSectionAPI 注册了web-search-exa段,数据层可写。但没有任何客户端卡片绑定它,所以界面上不显示。内置的 Web search 卡片编辑的是官方web-search-deepseek命名空间,与本插件无关。 - 搜索结果卡片:
web_search调用经dsh-tool-web照常渲染web结果卡片(来源、摘要、日期),与提供方无关。匿名 Exa 的结果和 DeepSeek 搜索显示一致。
路线图:下一版本会新增注册到 settings.plugin.item slot 的客户端卡片,绑定 web-search-exa 命名空间,让上表所有字段可以在 Settings → Plugins 里实时编辑。
工作原理
| 条件 | 路径 | 端点 |
|---|---|---|
配置了 apiKey / EXA_API_KEY |
REST POST /search,Authorization: Bearer |
https://api.exa.ai/search(可用 baseURL 配置) |
| 未配置任何 key | 匿名 MCP tools/call web_search_exa(JSON-RPC 2.0,无凭据) |
https://mcp.exa.ai/mcp(可配置) |
匿名 MCP 路径不发送任何凭据,来源标识通过 x-exa-source: dsh-anything 头携带。结果按 seam 的 WebSearchSource 形状规范化(url、title、snippet、publishedAt),maxResults 由 seam 在返回路径上强制执行。
限流
匿名通道是 Exa 提供的公共端点,有限流。触发时搜索会失败,错误码是 WEB_RATE_LIMITED,错误信息里写明要配 EXA_API_KEY。这个码是本插件定的,方便你和模型区分“被限流”和“网络坏了”。
配置 key 后走 REST 路径,不受这个限制。
搜索失败时会发生什么
你会看到什么:
- 匿名通道被限流:错误码
WEB_RATE_LIMITED,提示配置 key。 - 匿名通道连续失败 3 次:本插件会把自己标记为不可用,冷却 5 分钟。这期间
available()返回false。 - 冷却期内你写死了
searchProvider: exa:搜索报WEB_PROVIDER_CONFIGURED_UNAVAILABLE。 - 冷却期内你没写
searchProvider:seam 跳过本插件,去找别的 provider。没有别的可用 provider 时,报WEB_PROVIDER_UNAVAILABLE。 - 配了 key 走 REST 路径:不受上面任何一条影响,失败会照常抛给你。
为什么会这样。 seam 每次搜索前会调用 available() 决定用哪个 provider。如果本插件永远回答“可用”,端点挂掉时每次搜索都会硬失败,用户看到的是一个坏掉的 dsh。所以本插件加了一个熔断器:连续 3 次瞬时失败就承认自己暂时不可用,让 seam 有机会选别人。这是本插件的设计,Exa 没有这个机制。
计数规则。 只统计重试可能成功的失败:5xx、429、网络错误、响应体无法解析。满 3 次后冷却 5 分钟,任意一次成功搜索立即清零。
429 以外的 4xx 不计入。那是配置错误,重试多少次都一样,藏进冷却期只会把同一个错误推迟 5 分钟再报给你。
这是有代价的取舍。 Exa 挂掉的 5 分钟里,写死了 searchProvider: exa 的 profile 会直接报错,而不是继续尝试。插件无法替你选:
- 写死
searchProvider: exa:平时行为确定,但熔断打开时没有退路。 - 不写
searchProvider:熔断时能退到别的 provider,代价是多个 provider 同时可用时,seam 会报WEB_PROVIDER_AMBIGUOUS,需要你再显式指定一个。
想要回退能力就选后者,并且只装一个备选 provider。
与官方包比较
DeepSeek Harness 有一个官方 Exa 提供方 @deepseek-ai/dsh-web-search-exa,需要单独安装,dsh 默认不带。本包是它的零配置变体:补上了官方没有的匿名 MCP 兜底,同时保留配置 key 后的相同 REST 行为。
官方 @deepseek-ai/dsh-web-search-exa |
本包 @tonydua/dsh-web-search-exa |
|
|---|---|---|
REST 路径(POST /search) |
✅ 唯一路径 | ✅ 配置 key 时使用 |
| 必须有 API key | ✅ 是,key 为空则不可用 | ❌ 不需要,无 key 走匿名 MCP 兜底 |
匿名 MCP(mcp.exa.ai/mcp) |
❌ 未实现 | ✅ 无 key 时的默认路径 |
| 零配置安装 | ❌ | ✅ |
| Provider id | exa(固定) |
默认 exa,可用 providerId 配置 |
| Cordis 插件名 | web-search-exa |
web-search-exa |
| 配置键 | apiKey、baseURL、searchType、numResults、highlightsPerResult |
apiKey、apiKeyEnv、baseURL、apiURL(旧版)、mcpURL、searchType、numResults、highlightsPerResult、providerId |
该用哪个:
- 你有
EXA_API_KEY,且想用官方维护的包:用官方包,它是标准实现。 - 想零配置、免 key 试用 Exa 搜索:用本包。默认走匿名 MCP,出现 key 后自动走 REST。
- 两个都想要:一起装,用
providerId区分,见下节。
与官方包共存
两个包默认在 ctx.web 下注册相同的 provider id(exa),cordis 插件名也都是 web-search-exa。seam 会拒绝重复 id,报 WEB_DUPLICATE_PROVIDER。所以不改配置就把两个包装进同一个 profile,会在启动时报错。
共存必须显式配置,通过 providerId 开关完成:
- 官方包保持
exa,它的 id 固定。 - 给本包一个不同 id。在本插件的
config里设providerId: exa-anon,任意唯一字符串即可。 - 在
webseam 上显式选中一个。用searchProvider: exa-anon选匿名变体,或用searchProvider: exa选官方包。也可以用环境变量$DSH_WEB_SEARCH_PROVIDER。
- insert:
- id: web-search-exa
name: '@tonydua/dsh-web-search-exa'
config:
providerId: exa-anon
- id: web
name: '@deepseek-ai/dsh-web'
config:
searchProvider: exa-anon
最简单的替代方案是每个 profile 只装其中一个包,默认配置即可直接用。
排查
dsh web 启动时报 duplicate loader entry id: web。 这是 0.1.2 的 bug,0.1.4 起已修复,升级本插件即可。如果已在 0.1.4 或更新版本上遇到,请带上 dsh --version 和你的 cordis.patch.yml 提 issue,因为用户补丁里插入 web 行也会产生同样的错误。
启动时报 Cannot read properties of undefined (reading 'prepare')。 @deepseek-ai/dsh-tools 是 dsh 的运行时单例包,一个 profile 中必须解析到同一份物理包实例。本插件不依赖它。常见原因是 profile 里其他第三方插件把它声明成了普通嵌套依赖,而不是 peer dependency。先修正那个插件的依赖声明,或让 profile 的包管理器统一解析到共享实例,再排查搜索错误。
搜索报 WEB_PROVIDER_AMBIGUOUS。 同时存在多个可用 provider。按选中提供方显式指定一个。
搜索报 WEB_PROVIDER_CONFIGURED_UNAVAILABLE。 你写死的 provider 当前不可用。免 key 通道熔断时会这样,见搜索失败时会发生什么。
Web UI 里找不到设置入口。 本版本没有 UI 卡片,用 cordis.patch.yml 或环境变量配置,见在 Web 面板中的呈现。
版本兼容性
0.1.2-alpha.2 到 0.1.7-alpha.1 之间每一个已发布的 dsh 版本都实测过。实测包含三件事:独立安装该版本、用该版本自己的类型声明做类型检查、用 npm 严格安装一次本插件。最后一步最容易失败,因为 npm 的 peer 规则比 pnpm 严。复现命令:bash scripts/compat-matrix.sh。
| dsh 版本线 | 实测 | 说明 |
|---|---|---|
0.1.2-alpha.2 … 0.1.2-alpha.5 |
✅ | 最老的受支持基线 |
0.1.2-rc.1 |
✅ | |
0.1.3-alpha.2 |
✅ | |
0.1.5-alpha.1、0.1.5-alpha.2 |
✅ | |
0.1.5-rc.1、0.1.5-rc.2、0.1.5-rc.3 |
✅ | 0.1.5-rc.2 另有端到端验证:无 API key 时用真实的 dsh --profile headless 走通匿名 MCP |
0.1.6-alpha.1、0.1.6-alpha.2 |
✅ | |
0.1.7-alpha.1 |
✅ | settings 服务换了形态,见下 |
peer 范围里的 >=0.1.8 用来承接之后的稳定版,但这些版本尚未实测。
各版本之间差在哪
我逐个探测了 14 个版本的真实导出面。结论是 ctx.web seam 完全稳定:WebError 始终由 dsh-web 导出且继承 HarnessError,launchEnvironmentOf 始终存在,ctx.settings 在每个版本都被挂载。真正有差异的只有两处。
其一,0.1.7-alpha.1 换掉了 settings API。SettingsProvider.installSection 被移除,服务变成 SettingsForms,它直接从 Loader 已持有的 Config schema 派生配置页(SettingsDescriptor.schema、autoGenerate)。旧代码无条件调用该方法,会在这个版本上抛 TypeError:插件能加载,但会失败。现在改为先探测方法,存在才调用,不存在则什么都不做。在 0.1.7+ 上由 Loader 的 schema 驱动表单,插件无需注册任何东西。
其二,0.1.7-alpha.1 依赖 @deepseek-ai/cordis ^4.0.3,而 cordis 的 latest dist-tag 仍指向 4.0.2。4.0.3 已发布,只是 tag 落后。搭配 0.1.7 宿主时请安装 @deepseek-ai/cordis@4.0.3。矩阵脚本已按版本固化这一点。
同样支持 @deepseek-ai/dsh-web、dsh-settings(可选)和 dsh-launch-environment,覆盖上述整个范围。Node.js 需要 >=22.19.0,与 harness 自身的下限一致。
"@deepseek-ai/dsh-web": ">=0.1.2-alpha.2 || >=0.1.3-alpha.2 || >=0.1.4-0 || >=0.1.5-alpha.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1 || >=0.1.8"
这串枚举是在 pnpm 和 npm 下都能装遍所有已发布版本的唯一写法。原因是 semver 的一条规则:
prerelease 版本要满足某个范围,该范围中必须有一个比较器,它的 prerelease 落在相同的
major.minor.patch三段上。
所以 >=0.1.2-rc.1 匹配不到 0.1.5-rc.2,两者三段不同。单一开区间下界覆盖不了“以一串 prerelease 发布的项目”,而 * 会连未来的破坏性 1.0 一起放行。凡是发布过 prerelease 的 0.1.x 版本线,都需要自己的比较器。>=0.1.8 承接之后的稳定版,所以只有 dsh 开出新的 0.1.x prerelease 线时才需要追加条目。
在真实发布物上实测的结果:
| 范围 | npm 可安装版本数 | pnpm |
|---|---|---|
>=0.1.2-rc.1(先前写法) |
1 / 14 | 14 / 14 |
| 枚举写法(当前) | 14 / 14 | 14 / 14 |
这个结论是测出来的。开区间在 pnpm 下没问题,而 dsh plugin add 用的正是 pnpm。但在 npm 下,它会让 14 个版本中的 13 个报 ERESOLVE。如果你用 npm 安装旧版本的本插件时遇到该错误,升级即可,或临时加 --legacy-peer-deps。
从源码构建
pnpm install
pnpm run build # tsdown -> lib/index.js + lib/index.d.ts
pnpm run typecheck # tsc --noEmit
pnpm test # 先构建,再对 lib/ 跑 node:test 套件
src/ 是唯一的源文件目录。lib/ 仍然提交进仓库,因为 npm 发布包和基于 git 的安装都依赖它。
致谢
匿名 MCP 接入方式参考了 can1357/oh-my-pi 的 web_search 实现(packages/coding-agent/src/web/search/providers/exa.ts 和 src/exa/mcp-client.ts)以及 @oh-my-pi/exa 插件:同样的“有 key 走 REST、无 key 走免凭据 mcp.exa.ai/mcp”策略、同样的 x-exa-source 来源头、同样的 Title: 分节响应解析。感谢 oh-my-pi(omp)项目最先做出零配置的 Exa 接入。
同时感谢 Exa 提供并运营这个免费、免认证的托管 MCP 服务器(mcp.exa.ai/mcp),正是它让本包的零配置默认路径成为可能。Exa 托管 MCP 是 Exa 的官方产品,匿名使用有限流,见限流。
更新日志
所有变更见 CHANGELOG.md。
许可证
MIT,见 LICENSE。
链接
同类插件
Tencent/BrowserSkill#dsh-plugin-browserskill★ 7561
BrowserSkill 的 DeepSeek Harness 浏览器自动化桥接插件,通过原生浏览器工具控制可见的 Chrome 和 Edge Agent Window,支持可访问性与 VOM 页面观察、截图、隔离的多会话控制和 Web UI 实时观察浮层。
omdsh-dev/dsh-browser#packages/browser/bridge-browser★ 734
Chrome 侧边栏扩展,让 DSH 直接操控你的浏览器,无需视觉能力。
liustack/modsearch★ 559
纯文本 agent 的联网搜索桥:搜索网页与 X,返回结构化 JSON 证据(search/fetch/引用)。
DDDMUC/dsh-free-search★ 265
DSH 免费搜索插件:7 个引擎(DuckDuckGo/Bing/SearXNG 免费 + Exa/Perplexity/DeepSeek 付费)、自动回退、设置页 UI(API key 输入 + 官网链接)、web_fetch、引擎测试工具。
Tabbit-Browser/dsh-tabbit★ 101
让 DeepSeek Harness 能够控制 Tabbit 浏览器:安装即自动加载 tabbit-browser skill,检测国际版 Tabbit 与国内版 Tabbit Browser 正式版(>= 1.9.0),检查 tabbit-cli 常驻运行时,按平台诊断调用 CLI 所需的 DSH sandbox 模式,并在没有合格版本时通过后台任务下载与系统地区匹配的正式版安装包。
wqty123/dsh-browser★ 83
共享真实浏览器:用户可观看并随时接管的原生 Electron 窗口,agent 通过 CDP 驱动,内置 20 个 browser_* 工具(打开/快照/执行/填表/截图/下载/登录态);任务级会话隔离、登录态持久化、人机验证识别,纯 `dsh web` 无需桌面外壳即可自托管。
社区评论
评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。