@tonydua/dsh-web-search-exa 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.zh.md CHANGED
@@ -2,138 +2,212 @@
2
2
 
3
3
  [English](README.md) | **简体中文**
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/@tonydua/dsh-web-search-exa)](https://www.npmjs.com/package/@tonydua/dsh-web-search-exa)
6
- [![npm downloads](https://img.shields.io/npm/dm/@tonydua/dsh-web-search-exa)](https://www.npmjs.com/package/@tonydua/dsh-web-search-exa)
5
+ [![npm 版本](https://img.shields.io/npm/v/@tonydua/dsh-web-search-exa?label=npm)](https://www.npmjs.com/package/@tonydua/dsh-web-search-exa)
6
+ [![GitHub release](https://img.shields.io/github/release/TonyDua/dsh-web-search-exa?label=release)](https://github.com/TonyDua/dsh-web-search-exa/releases/latest)
7
+ [![npm 下载量](https://img.shields.io/npm/dm/@tonydua/dsh-web-search-exa)](https://www.npmjs.com/package/@tonydua/dsh-web-search-exa)
7
8
  [![License](https://img.shields.io/npm/l/@tonydua/dsh-web-search-exa)](LICENSE)
9
+ [![dsh](https://img.shields.io/badge/dsh-0.1.2--alpha.2%20%E2%80%93%200.1.7--alpha.1-4c6?logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
10
+ [![Node](https://img.shields.io/badge/node-%3E%3D22.19.0-339933?logo=node.js&logoColor=white)](package.json)
8
11
  [![GitHub stars](https://img.shields.io/github/stars/TonyDua/dsh-web-search-exa)](https://github.com/TonyDua/dsh-web-search-exa)
9
12
  [![GitHub issues](https://img.shields.io/github/issues/TonyDua/dsh-web-search-exa)](https://github.com/TonyDua/dsh-web-search-exa)
10
- [![Node](https://img.shields.io/badge/node-%3E%3D18-339933?logo=node.js&logoColor=white)](package.json)
11
- [![dsh](https://img.shields.io/badge/dsh-0.1.2--rc.1-4c6?logo=deepseek&logoColor=white)](https://www.npmjs.com/package/@deepseek-ai/dsh)
12
13
 
13
- > 为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)提供**零配置**的 [Exa](https://exa.ai) 网页搜索:
14
- > **无需 API key** —— 一个 `ctx.web` seam 的 `WebSearchProvider`,内置匿名 MCP 兜底 + 带 key 的 REST 路径。
14
+ 给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)加上 [Exa](https://exa.ai) 网页搜索。
15
15
 
16
- 使用 [deepseek-v4-flash](https://api-docs.deepseek.com) 在 DeepSeek Harness(dsh)内开发。
17
-
18
- ## 当前支持的版本
19
-
20
- `@tonydua/dsh-web-search-exa@0.1.4` 已针对以下版本测试并支持:
16
+ ```powershell
17
+ dsh plugin --profile web add @tonydua/dsh-web-search-exa
18
+ ```
21
19
 
22
- - `@deepseek-ai/dsh` `0.1.2-rc.1`(当前 npm `latest` 发布线)
23
- - `@deepseek-ai/dsh-web` `0.1.2-rc.1`
24
- - `@deepseek-ai/dsh-settings` `0.1.2-rc.1`(可选;用于启用实时 Settings 集成)
25
- - `@deepseek-ai/dsh-launch-environment` `0.1.2-rc.1`
26
- - `@deepseek-ai/cordis` `4.0.2`
27
- - Node.js `>=18`
20
+ 重启 `dsh web` 就能用。不用配 API key,不用改配置,不用选 provider。
28
21
 
29
- dsh `0.1.2-rc.1` 是本版本的兼容基线;`0.1.5-alpha.1` alpha 线不在本版本的测试支持矩阵内。
22
+ 背景,了解即可:
30
23
 
31
- ## 特性
24
+ - **Exa** 是一个搜索 API。它按关键词或语义检索网页,返回可引用的来源和摘要,不生成答案。它提供 REST API,也运营一个免认证的公共 MCP 服务器。
25
+ - **官方的 [`dsh-web-search-exa`](https://github.com/deepseek-ai/deepseek-harness/blob/HEAD/packages/web/web-search-exa/README.zh.md)** 是 dsh 的 Exa 搜索提供方。它走 Exa 的 REST API,必须配置 API key 才有用。
26
+ - **本包基于官方包改的。** REST 路径的实现与官方一致,补充了一条免 key 通道:没有 key 时改走 Exa 的公共 MCP 服务器,配了 key 仍走 REST。匿名接入方式参考了 oh-my-pi 项目,见[致谢](#致谢)。
32
27
 
33
- - 🆓 **零配置、默认免 key** —— 搜索经由 Exa 官方托管的 MCP 服务器(`mcp.exa.ai/mcp`),**完全不携带凭据**(Exa 官方提供的免认证公共 MCP,有限流)。
34
- - 🔑 **配 key 自动升级 REST** —— 设置 `EXA_API_KEY` 后自动切换到 Exa `POST /search` REST API(额度更高,行为不变)。
35
- - 🔌 **即插即用** —— 注册进 dsh `ctx.web` seam;模型侧的 `web_search` / `web_fetch` 工具、提示词区段与结果卡片无需任何改动。
36
- - 🎛️ **`providerId` 开关** —— 可与官方 `@deepseek-ai/dsh-web-search-exa` 在同一 profile 共存(不撞 id、无黑箱覆盖)。
37
- - 📦 **可直接发布** —— MIT、ESM、内置类型声明、`files` 仅含 `lib/`。
28
+ 默认情况下不用管这几件事。只有同时用官方包,或 dsh 报错说 provider 有歧义时,才需要看[选中提供方](#选中提供方)。
38
29
 
39
- ## 为什么有这个包(与官方包的差异)
30
+ 使用 [deepseek-v4-flash](https://api-docs.deepseek.com) 在 DeepSeek Harness(dsh)内开发。
40
31
 
41
- DeepSeek Harness 自带官方 Exa 提供方 [`@deepseek-ai/dsh-web-search-exa`](https://www.npmjs.com/package/@deepseek-ai/dsh-web-search-exa)。本包是它的**零配置变体**:补上了官方没有的匿名 MCP 兜底,同时保留配置 key 后的相同 REST 行为。
32
+ ## 特性
42
33
 
43
- | | 官方 `@deepseek-ai/dsh-web-search-exa` | 本包 `@tonydua/dsh-web-search-exa` |
44
- |---|---|---|
45
- | REST 路径(`POST /search`) | ✅ 唯一路径 | ✅ 配置 key 时使用 |
46
- | 必须有 API key | ✅ **是——key 为空则不可用** | ❌ 不需要——无 key 走匿名 MCP 兜底 |
47
- | 匿名 MCP(`mcp.exa.ai/mcp`) | ❌ 未实现 | ✅ 无 key 时默认路径 |
48
- | 零配置安装 | ❌ | ✅ |
49
- | Provider id | `exa`(固定) | 默认 `exa`,**可用 `providerId` 配置** |
50
- | Cordis 插件名 | `web-search-exa` | `web-search-exa` |
51
- | 配置键 | `apiKey`、`baseURL`、`searchType`、`numResults`、`highlightsPerResult` | `apiKey`、`apiKeyEnv`、`baseURL`、`apiURL`(旧版)、`mcpURL`、`searchType`、`numResults`、`highlightsPerResult`、`providerId` |
34
+ - 免 key 可用。搜索经由 Exa 的公共 MCP 服务器(`mcp.exa.ai/mcp`),不携带任何凭据。
35
+ - 这条免 key 通道返回结构化结果。它调用 `web_search_advanced_exa`,输出是采用 REST 字段词表的 JSON,因此 source 直接带上真实的 highlight 摘要,无需文本解析。
36
+ - 配 key 自动升级。设置 `EXA_API_KEY` 后自动切到 Exa `POST /search` REST API,额度更高,行为不变。
37
+ - 即插即用。注册进 dsh `ctx.web` seam,模型侧的 `web_search` 和 `web_fetch` 工具、提示词区段、结果卡片都无需改动。
38
+ - 装上就能用。不装官方包时不需要选 provider,默认自动生效。
39
+ - 失败时能退让。匿名通道连续失败后,插件会把自己标记为不可用,让 dsh 有机会换别的 provider,而不是每次搜索都硬失败,详见[搜索失败时会发生什么](#搜索失败时会发生什么)。
52
40
 
53
- ## 我该用哪个?
41
+ ## 安装
54
42
 
55
- - **你有 `EXA_API_KEY`,且想要官方维护的包** → 用 `@deepseek-ai/dsh-web-search-exa`,它是官方标准实现。
56
- - **想零配置、免 key、无成本负担地试用 Exa 搜索** → 用本包。优雅降级:默认匿名 MCP,出现 key 自动走 REST。
57
- - **两个都想要** → 一起装,用 `providerId` 开关(见[与官方包共存](#与官方包共存))。
43
+ 三种方式选一种。方式只决定代码从哪来,装完都一样。
58
44
 
59
- ## 工作原理
45
+ **从 npm 安装。** v0.1.4 起自带 `dsh.bundle` manifest,bundle patch 会自动插入 provider 行,无需手动改 patch。
60
46
 
61
- | 条件 | 路径 | 端点 |
62
- |---|---|---|
63
- | 配置了 `apiKey` / `EXA_API_KEY` | REST `POST /search`,`Authorization: Bearer` | `https://api.exa.ai/search`(可用 `baseURL` 配置) |
64
- | 未配置任何 key | 匿名 MCP `tools/call web_search_exa`(JSON-RPC 2.0,无凭据) | `https://mcp.exa.ai/mcp`(可配置) |
47
+ ```powershell
48
+ dsh plugin --profile web add @tonydua/dsh-web-search-exa
49
+ ```
65
50
 
66
- 匿名 MCP 路径不发送任何凭据,来源标识通过 `x-exa-source: dsh-anything` 头携带。结果按 seam 的 `WebSearchSource` 形状规范化(`url`、`title`、`snippet`、`publishedAt`),`maxResults` 由 seam 在返回路径上强制执行。匿名使用受 Exa 限流:HTTP 429 会以 `WEB_PROVIDER_ERROR` 呈现,并提示配置 API key(配置后自动切换到 REST 路径)。
51
+ **从 GitHub Release 安装。** 同一份 tarball,npm 不可达时用。
67
52
 
68
- ## 安装(装入 dsh profile)
53
+ ```powershell
54
+ dsh plugin --profile web add https://github.com/TonyDua/dsh-web-search-exa/releases/latest/download/dsh-web-search-exa.tgz
55
+ ```
69
56
 
70
- **一条命令从 npm 安装**(v0.1.4+ 自带 `dsh.bundle` manifest——bundle patch 会自动插入 provider 行,无需手动改 patch):
57
+ **从仓库安装。** 跟随 `main`,包含尚未发布的改动。
71
58
 
72
59
  ```powershell
73
- dsh plugin --profile web add @tonydua/dsh-web-search-exa
60
+ dsh plugin --profile web add github:TonyDua/dsh-web-search-exa
74
61
  ```
75
62
 
76
- 重启 `dsh web` 生效。**无 API key 时**官方 DeepSeek 搜索提供方不可用,seam 会自动选中本插件——完全零配置。**配了 key 时**,需在你的 `$DSH_HOME/profiles/web/cordis.patch.yml`(在 bundle patch 之后应用)里显式选中 Exa:
63
+ 本地开发目录的装法相同,把包名换成路径即可:`dsh plugin --profile web add ../plugins/dsh-web-search-exa`。
77
64
 
78
- ```yaml
79
- - id: web
80
- name: '@deepseek-ai/dsh-web'
81
- config:
82
- searchProvider: exa
83
- ```
65
+ 装完重启 `dsh web`。多数情况下这就是全部步骤。
84
66
 
85
- …或用环境变量 `$DSH_WEB_SEARCH_PROVIDER=exa` 在运行时选中。
67
+ ### 选中提供方
86
68
 
87
- **本地开发目录:**
69
+ **不装官方包时不用看这一节。**
88
70
 
89
- ```powershell
90
- dsh plugin --profile web add ../plugins/dsh-web-search-exa
91
- ```
71
+ dsh 的 seam 每次搜索前会挑一个可用 provider。只有一个可用时自动选中,多于一个时抛 `WEB_PROVIDER_AMBIGUOUS`,要求你指定。所以只有在下面两种情况才需要动手:
72
+
73
+ - **同时装了官方包**:两个包都注册 provider id `exa`,`dsh web` 启动就会报 `WEB_DUPLICATE_PROVIDER`。必须先给本包改一个 id,见[与官方包共存](#与官方包共存)。
74
+ - **报 `WEB_PROVIDER_AMBIGUOUS`**:说明有另一个可用 provider。指定一个即可。
92
75
 
93
- 然后启用并选中该提供方。合并进 `$DSH_HOME/profiles/web/cordis.patch.yml`(持久生效):
76
+ 指定方式二选一:
94
77
 
95
78
  ```yaml
96
- - id: web-search-exa
97
- name: '@tonydua/dsh-web-search-exa'
98
- config:
99
- apiKeyEnv: EXA_API_KEY
79
+ # $DSH_HOME/profiles/web/cordis.patch.yml
100
80
  - id: web
101
81
  name: '@deepseek-ai/dsh-web'
102
82
  config:
103
83
  searchProvider: exa
104
84
  ```
105
85
 
106
- 也可以不修改配置,直接用环境变量 `$DSH_WEB_SEARCH_PROVIDER=exa` 在运行时选中该提供方。
86
+ 或用环境变量 `$DSH_WEB_SEARCH_PROVIDER=exa`。
87
+
88
+ 改完重启 `dsh web`。模型侧的 `web_search` 工具会自动走选中的 provider,不用改工具配置。
89
+
90
+ <details>
91
+ <summary>发布产物与安装告警(一般不用看)</summary>
107
92
 
108
- 重启 `dsh web` 生效。模型侧的 `web_search` 工具随即走该提供方,无需改任何工具配置。
93
+ **发布产物。** CI 打包本版本的 tarball,在每一个受支持的 dsh 版本上验证,挂到 GitHub Release,并把这个产物本身发布到 npm。所以 Release 附件和 npm 上的 tarball 是同一个文件,而不是两次恰好一致的构建。
109
94
 
110
- ### 运行时单例兼容性
95
+ **profile 安装告警。** dsh profile 默认 `autoInstallPeers: false`,而 harness 自身的服务由 dsh 宿主在运行时提供,不经 pnpm 解析。如果 `dsh plugin add` 报 peer 警告,把下面这段加进 profile 的 `pnpm-workspace.yaml`:
96
+
97
+ ```yaml
98
+ peerDependencyRules:
99
+ ignoreMissing:
100
+ - '@deepseek-ai/cordis'
101
+ - '@deepseek-ai/dsh-*'
102
+ ```
111
103
 
112
- `@deepseek-ai/dsh-tools` 是 dsh 的运行时单例包,一个 profile 中必须解析到同一份物理包实例。本插件本身不依赖它;这是宿主 profile 的依赖约束。如果 profile 中的其他第三方插件把 `@deepseek-ai/dsh-tools` 错误声明成普通嵌套依赖,而不是 peer dependency,应先修正该插件的依赖声明,或让 profile 的包管理器统一解析到共享实例,再排查搜索错误。否则 dsh agent loop 可能在 provider 被调用前就因 `Cannot read properties of undefined (reading 'prepare')` 失败。
104
+ </details>
113
105
 
114
106
  ## 配置
115
107
 
116
108
  | 配置键 | 默认值 | 含义 |
117
109
  |---|---|---|
118
- | `providerId` | `exa` | 注册进 `ctx.web` 的提供方 id。仅当本包与官方包同时安装时才需要改(见下一节)。 |
119
- | `apiKey` | 未设置 | Exa API 密钥字面值。为空/缺失时启用匿名 MCP 路径。 |
110
+ | `apiKey` | 未设置 | Exa API 密钥字面值。为空或缺失时启用匿名 MCP 路径。 |
120
111
  | `apiKeyEnv` | `EXA_API_KEY` | 未设置字面 `apiKey` 时读取的环境变量名。 |
121
- | `baseURL` | `https://api.exa.ai` | Exa API 基础 URL;带 key 的 REST 路径会追加 `/search`,与官方 dsh 提供方一致。 |
122
- | `apiURL` | 未设置 | 已弃用的完整 REST 端点别名;设置后优先于 `baseURL`。 |
123
- | `mcpURL` | `https://mcp.exa.ai/mcp` | Exa 托管 MCP 端点(匿名路径使用)。 |
124
- | `searchType` | `auto` | REST 检索模式:`auto` / `keyword` / `neural`。 |
112
+ | `baseURL` | `https://api.exa.ai` | Exa API 基础 URL。带 key 的 REST 路径会追加 `/search`,与官方 dsh 提供方一致。 |
113
+ | `apiURL` | 未设置 | 已弃用的完整 REST 端点别名,设置后优先于 `baseURL`。 |
114
+ | `mcpURL` | `https://mcp.exa.ai/mcp?tools=web_search_exa,web_search_advanced_exa` | Exa 托管 MCP 端点,匿名路径使用。默认值里的 `tools` 查询参数是必需的:结构化工具没有它就无法被调用。你自己的 URL 里不写这个参数,插件会补上。 |
115
+ | `mcpTool` | `web_search_advanced_exa` | 匿名路径调用哪个 MCP 工具:默认是返回结构化 JSON 的那个;填 `web_search_exa` 则回到旧的 `Title:` 分节文本。见[工作原理](#工作原理)。 |
116
+ | `searchType` | `auto` | REST 检索模式:`auto`、`keyword` 或 `neural`。只在 REST 路径上读取。 |
125
117
  | `numResults` | 未设置 | 请求未携带 `maxResults` 时的默认结果数。 |
126
118
  | `highlightsPerResult` | `1` | REST 路径每个结果请求的 highlight 句子数。 |
119
+ | `providerId` | `exa` | 注册进 `ctx.web` 的提供方 id。仅当本包与官方包同时安装时才需要改,见[与官方包共存](#与官方包共存)。 |
120
+
121
+ 配置写在哪里:编辑 `$DSH_HOME/profiles/web/cordis.patch.yml` 里本插件的 `config`,然后重启 `dsh web`。也可以用环境变量 `EXA_API_KEY` 和 `$DSH_WEB_SEARCH_PROVIDER`。`apiKey` 标记了 `role('secret')`,任何 `describe()` 响应都不会暴露它的值。
122
+
123
+ ### 在 Web 面板中的呈现
124
+
125
+ 当前版本的配置入口在 profile 补丁层,不在 Web UI,没有可编辑的界面入口。Settings UI 只渲染客户端插件为固定命名空间(`shell`、`agent-loop`、`web-search-deepseek`)手工注册的卡片,对任意插件命名空间没有通用表单。当前实际情况:
126
+
127
+ - **插件清单**(Settings → Plugins):启用后自动出现 `web-search-exa` 条目。清单直接读取 Cordis loader 的实时条目,无需额外代码。
128
+ - **设置命名空间**(服务端):插件通过 `ctx.settings.installSection` API 注册了 `web-search-exa` 段,数据层可写。但没有任何客户端卡片绑定它,所以界面上不显示。内置的 Web search 卡片编辑的是官方 `web-search-deepseek` 命名空间,与本插件无关。
129
+ - **搜索结果卡片**:`web_search` 调用经 `dsh-tool-web` 照常渲染 `web` 结果卡片(来源、摘要、日期),与提供方无关。匿名 Exa 的结果和 DeepSeek 搜索显示一致。
130
+
131
+ 路线图:下一版本会新增注册到 `settings.plugin.item` slot 的客户端卡片,绑定 `web-search-exa` 命名空间,让上表所有字段可以在 Settings → Plugins 里实时编辑。
132
+
133
+ ## 工作原理
134
+
135
+ | 条件 | 路径 | 端点 |
136
+ |---|---|---|
137
+ | 配置了 `apiKey` / `EXA_API_KEY` | REST `POST /search`,`Authorization: Bearer` | `https://api.exa.ai/search`(可用 `baseURL` 配置) |
138
+ | 未配置任何 key | 匿名 MCP `tools/call web_search_advanced_exa`(JSON-RPC 2.0,无凭据) | `https://mcp.exa.ai/mcp?tools=…`(可配置) |
139
+
140
+ 匿名 MCP 路径不发送任何凭据,来源标识通过 `x-exa-source: dsh-anything` 头携带。结果按 seam 的 `WebSearchSource` 形状规范化(`url`、`title`、`snippet`、`publishedAt`),`maxResults` 由 seam 在返回路径上强制执行。
141
+
142
+ 匿名路径默认调用 `web_search_advanced_exa`。它的文本内容是一份结构化的 JSON 搜索结果,条目的字段名与 REST API 一致,因此可以直接映射成 source,全程不涉及 `Title:` 分节文本解析。有两点需要知道:
143
+
144
+ - **只有 URL 带上 `?tools=…` 时,这个结构化工具才会被服务。** 裸端点会返回 `MCP error -32602: Tool web_search_advanced_exa not found`,这就是该查询参数写进 `mcpURL` 默认值的原因,也是插件会把它补进任何缺少该参数的 `mcpURL` 的原因。
145
+ - **它每条结果都返回整页正文,而 highlights 需要我们单独索取。** 不传 `enableHighlights` 时端点只返回纯文本条目,每条结果都拿不到摘要,搜索会整体返回空。正文随后被丢弃:摘要永远是真实的 highlight 句子,既不生成、也不从正文里截取。
146
+
147
+ 这里**不会**转发 `searchType`。本插件的这个配置用的是 REST 词表(`auto`、`keyword`、`neural`),而该工具只接受自己的词表(`auto`、`fast`、`instant`);转发会让配置了 `keyword` 或 `neural` 的用户触发参数校验失败,把整条匿名路径一起打死。工具的默认行为本就等同于 `auto`。
148
+
149
+ 把 `mcpTool` 固定为 `web_search_exa` 可以回到旧的文本块路径,同样结果以 `Title:` 开头的分节形式返回。若 Exa 改动了结构化工具的输出形状,可以这样切换;而返回体若不是预期的 JSON,插件本身就会自动回退到分节解析。
150
+
151
+ 由于结构化工具每条结果都带整页正文,响应体可能很大。匿名响应上限为 256 KiB:会先检查声明的 `content-length` 再读取正文,正文若持续增长则在传输中途中断。超限响应按可重试错误失败,而不是被截断或静默解析。
152
+
153
+ ### 限流
154
+
155
+ 匿名通道是 Exa 提供的公共端点,有限流。触发时搜索会失败,错误码是 `WEB_RATE_LIMITED`,错误信息里写明要配 `EXA_API_KEY`。这个码是本插件定的,方便你和模型区分“被限流”和“网络坏了”。
156
+
157
+ 配置 key 后走 REST 路径,不受这个限制。
158
+
159
+ ### 搜索失败时会发生什么
160
+
161
+ **你会看到什么:**
162
+
163
+ - 匿名通道被限流:错误码 `WEB_RATE_LIMITED`,提示配置 key。
164
+ - 匿名通道连续失败 3 次:本插件会把自己标记为不可用,冷却 5 分钟。这期间 `available()` 返回 `false`。
165
+ - 冷却期内你写死了 `searchProvider: exa`:搜索报 `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`。
166
+ - 冷却期内你没写 `searchProvider`:seam 跳过本插件,去找别的 provider。没有别的可用 provider 时,报 `WEB_PROVIDER_UNAVAILABLE`。
167
+ - 配了 key 走 REST 路径:不受上面任何一条影响,失败会照常抛给你。
168
+
169
+ **为什么会这样。** seam 每次搜索前会调用 `available()` 决定用哪个 provider。如果本插件永远回答“可用”,端点挂掉时每次搜索都会硬失败,用户看到的是一个坏掉的 dsh。所以本插件加了一个熔断器:连续 3 次瞬时失败就承认自己暂时不可用,让 seam 有机会选别人。这是本插件的设计,Exa 没有这个机制。
170
+
171
+ **计数规则。** 只统计重试可能成功的失败:5xx、429、网络错误、响应体无法解析。满 3 次后冷却 5 分钟,任意一次成功搜索立即清零。
172
+
173
+ 429 以外的 4xx 不计入。那是配置错误,重试多少次都一样,藏进冷却期只会把同一个错误推迟 5 分钟再报给你。
174
+
175
+ **这是有代价的取舍。** Exa 挂掉的 5 分钟里,写死了 `searchProvider: exa` 的 profile 会直接报错,而不是继续尝试。插件无法替你选:
176
+
177
+ - 写死 `searchProvider: exa`:平时行为确定,但熔断打开时没有退路。
178
+ - 不写 `searchProvider`:熔断时能退到别的 provider,代价是多个 provider 同时可用时,seam 会报 `WEB_PROVIDER_AMBIGUOUS`,需要你再显式指定一个。
179
+
180
+ 想要回退能力就选后者,并且只装一个备选 provider。
181
+
182
+ ## 与官方包比较
183
+
184
+ DeepSeek Harness 有一个官方 Exa 提供方 [`@deepseek-ai/dsh-web-search-exa`](https://www.npmjs.com/package/@deepseek-ai/dsh-web-search-exa),需要单独安装,dsh 默认不带。本包是它的零配置变体:补上了官方没有的匿名 MCP 兜底,同时保留配置 key 后的相同 REST 行为。
185
+
186
+ | | 官方 `@deepseek-ai/dsh-web-search-exa` | 本包 `@tonydua/dsh-web-search-exa` |
187
+ |---|---|---|
188
+ | REST 路径(`POST /search`) | ✅ 唯一路径 | ✅ 配置 key 时使用 |
189
+ | 必须有 API key | ✅ 是,key 为空则不可用 | ❌ 不需要,无 key 走匿名 MCP 兜底 |
190
+ | 匿名 MCP(`mcp.exa.ai/mcp`) | ❌ 未实现 | ✅ 无 key 时的默认路径 |
191
+ | 零配置安装 | ❌ | ✅ |
192
+ | Provider id | `exa`(固定) | 默认 `exa`,可用 `providerId` 配置 |
193
+ | Cordis 插件名 | `web-search-exa` | `web-search-exa` |
194
+ | 配置键 | `apiKey`、`baseURL`、`searchType`、`numResults`、`highlightsPerResult` | `apiKey`、`apiKeyEnv`、`baseURL`、`apiURL`(旧版)、`mcpURL`、`mcpTool`、`searchType`、`numResults`、`highlightsPerResult`、`providerId` |
195
+
196
+ 该用哪个:
197
+
198
+ - 你有 `EXA_API_KEY`,且想用官方维护的包:用官方包,它是标准实现。
199
+ - 想零配置、免 key 试用 Exa 搜索:用本包。默认走匿名 MCP,出现 key 后自动走 REST。
200
+ - 两个都想要:一起装,用 `providerId` 区分,见下节。
127
201
 
128
202
  ## 与官方包共存
129
203
 
130
- 两个包默认在 `ctx.web` 下注册**相同的 provider id(`exa`)**,cordis 插件名也都是 `web-search-exa`。seam 会拒绝重复 id(`WEB_DUPLICATE_PROVIDER`),所以**不改配置就把两个包装进同一个 profile 会在启动时报错**。
204
+ 两个包默认在 `ctx.web` 下注册相同的 provider id(`exa`),cordis 插件名也都是 `web-search-exa`。seam 会拒绝重复 id,报 `WEB_DUPLICATE_PROVIDER`。所以不改配置就把两个包装进同一个 profile,会在启动时报错。
131
205
 
132
- **没有黑箱覆盖**——共存必须显式配置,通过 `providerId` 开关完成:
206
+ 共存必须显式配置,通过 `providerId` 开关完成:
133
207
 
134
- 1. 官方包保持 `exa`(它的 id 固定)。
135
- 2. 给本包一个不同 id——在本插件 `config` 里设 `providerId: exa-anon`(任意唯一字符串)。
136
- 3. 在 `web` seam 上显式选中匿名变体:`searchProvider: exa-anon`(或用环境变量 `$DSH_WEB_SEARCH_PROVIDER=exa-anon`);若还想用官方包,再配 `searchProvider: exa` 切换。
208
+ 1. 官方包保持 `exa`,它的 id 固定。
209
+ 2. 给本包一个不同 id。在本插件的 `config` 里设 `providerId: exa-anon`,任意唯一字符串即可。
210
+ 3. 在 `web` seam 上显式选中一个。用 `searchProvider: exa-anon` 选匿名变体,或用 `searchProvider: exa` 选官方包。也可以用环境变量 `$DSH_WEB_SEARCH_PROVIDER`。
137
211
 
138
212
  ```yaml
139
213
  - insert:
@@ -147,46 +221,105 @@ dsh plugin --profile web add ../plugins/dsh-web-search-exa
147
221
  searchProvider: exa-anon
148
222
  ```
149
223
 
150
- 最简单的替代方案:每个 profile 只装其中一个包,默认配置即可直接使用。
224
+ 最简单的替代方案是每个 profile 只装其中一个包,默认配置即可直接用。
151
225
 
152
- ## 在 Web 面板中的呈现
226
+ ## 排查
153
227
 
154
- **状态:本版本的配置入口在 profile 补丁层,不在 Web UI —— 没有可编辑的界面入口。** Settings UI 只渲染客户端插件为固定命名空间(`shell`、`agent-loop`、`web-search-deepseek`)手工注册的卡片,对任意插件命名空间没有通用表单。当前实际情况:
228
+ **`dsh web` 启动时报 `duplicate loader entry id: web`。** 这是 0.1.2 的 bug,0.1.4 起已修复,升级本插件即可。如果已在 0.1.4 或更新版本上遇到,请带上 `dsh --version` 和你的 `cordis.patch.yml` 提 issue,因为用户补丁里插入 `web` 行也会产生同样的错误。
155
229
 
156
- - **插件清单**(Settings → Plugins):启用后自动出现 `web-search-exa`(`@tonydua/dsh-web-search-exa`)条目 —— 清单直接读取 Cordis loader 的实时条目,无需额外代码。
157
- - **设置命名空间**(服务端):插件通过当前的 `ctx.settings.installSection` API 注册了 `web-search-exa` 段,数据层可写——但**没有任何客户端卡片绑定它**,所以界面上不显示。内置的 "Web search" 卡片编辑的是官方 `web-search-deepseek` 命名空间,与本插件无关。
158
- - **现在怎么改配置**:编辑 `$DSH_HOME/profiles/web/cordis.patch.yml` 里本插件的 `config`(字段与默认值见上方配置表),重启 `dsh web`;或用环境变量 `EXA_API_KEY` / `$DSH_WEB_SEARCH_PROVIDER`。`apiKey` 标记了 `role('secret')`,任何 `describe()` 响应都不会暴露其值。
159
- - **搜索结果卡片**:`web_search` 调用经 `dsh-tool-web` 照常渲染 `web` 结果卡片(来源、摘要、日期),与提供方无关 —— 匿名 Exa 的结果与 DeepSeek 搜索显示完全一致。
230
+ **启动时报 `Cannot read properties of undefined (reading 'prepare')`。** `@deepseek-ai/dsh-tools` 是 dsh 的运行时单例包,一个 profile 中必须解析到同一份物理包实例。本插件不依赖它。常见原因是 profile 里其他第三方插件把它声明成了普通嵌套依赖,而不是 peer dependency。先修正那个插件的依赖声明,或让 profile 的包管理器统一解析到共享实例,再排查搜索错误。
160
231
 
161
- **路线图(下一版本)**:新增注册到 `settings.plugin.item` slot 的客户端卡片,绑定 `web-search-exa` 命名空间,让上表所有字段可以在 Settings → Plugins 里实时编辑(与官方卡片同机制)。
232
+ **搜索报 `WEB_PROVIDER_AMBIGUOUS`。** 同时存在多个可用 provider。按[选中提供方](#选中提供方)显式指定一个。
162
233
 
163
- ## 常见问题(FAQ)
234
+ **搜索报 `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`。** 你写死的 provider 当前不可用。免 key 通道熔断时会这样,见[搜索失败时会发生什么](#搜索失败时会发生什么)。
164
235
 
165
- **Q: 需要 Exa API key 吗?**
166
- 不需要。无 key 时走 Exa 免费匿名托管 MCP;配 key 后走 REST API 获得更高额度。
236
+ **Web UI 里找不到设置入口。** 本版本没有 UI 卡片,用 `cordis.patch.yml` 或环境变量配置,见[在 Web 面板中的呈现](#在-web-面板中的呈现)。
167
237
 
168
- **Q: 遇到 HTTP 429 / 限流怎么办?**
169
- 这是 Exa 匿名 MCP 的限流。配置 `EXA_API_KEY`(或 `apiKey` 字段),提供方会自动切到 REST 路径。
238
+ ## 版本兼容性
170
239
 
171
- **Q: 能和官方 Exa 提供方一起装吗?**
172
- 可以——给本包一个不同的 `providerId` 并显式选中即可(见[与官方包共存](#与官方包共存))。
240
+ `0.1.2-alpha.2` 到 `0.2.1-alpha.1` 之间每一个已发布的 dsh 版本都实测过。实测包含三件事:独立安装该版本、用该版本自己的类型声明做类型检查、用 npm 严格安装一次本插件。最后一步最容易失败,因为 npm 的 peer 规则比 pnpm 严。复现命令:`bash scripts/compat-matrix.sh`。
241
+
242
+ | dsh 版本线 | 实测 | 说明 |
243
+ |---|---|---|
244
+ | `0.1.2-alpha.2` … `0.1.2-alpha.5` | ✅ | 最老的受支持基线 |
245
+ | `0.1.2-rc.1` | ✅ | |
246
+ | `0.1.3-alpha.2` | ✅ | |
247
+ | `0.1.5-alpha.1`、`0.1.5-alpha.2` | ✅ | |
248
+ | `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 |
249
+ | `0.1.6-alpha.1`、`0.1.6-alpha.2` | ✅ | |
250
+ | `0.1.7-alpha.1` | ✅ | settings 服务换了形态,见下 |
251
+ | `0.2.0-rc.1`、`0.2.0-rc.2` | ✅ | 在宿主旁做 npm 严格安装,再跑一次真实的免 key 搜索 |
252
+ | `0.2.1-alpha.1` | ✅ | 同上;也是唯一需要 `@deepseek-ai/cordis@4.0.5-alpha.1` 的版本 |
253
+
254
+ 从 `0.2.0-rc.1` 这条线起,各版本还在 `package.json` 的 `dsh.compatibility.dshReleases` 里逐条声明。目录类审核需要的是这种逐版本精确记录,光有 peer 范围不构成可安装证据。
255
+
256
+ ### 各版本之间差在哪
257
+
258
+ 我逐个探测了各版本的真实导出面。结论是 `ctx.web` seam 完全稳定:`WebError` 始终由 `dsh-web` 导出且继承 `HarnessError`,`launchEnvironmentOf` 始终存在,`ctx.settings` 在每个版本都被挂载。真正有差异的只有两处。
259
+
260
+ 其一,`0.1.7-alpha.1` 换掉了 settings API。`SettingsProvider.installSection` 被移除,服务变成 `SettingsForms`,它直接从 Loader 已持有的 Config schema 派生配置页(`SettingsDescriptor.schema`、`autoGenerate`)。旧代码无条件调用该方法,会在这个版本上抛 `TypeError`:插件能加载,但会失败。现在改为先探测方法,存在才调用,不存在则什么都不做。在 `0.1.7+` 上由 Loader 的 schema 驱动表单,插件无需注册任何东西。
261
+
262
+ 其二,`@deepseek-ai/cordis` 跟着 dsh 走,而且是一路穿过 prerelease 走的:`0.1.5`/`0.1.6` 是 `^4.0.2`(`0.1.5-rc.3` 为精确 `4.0.2`),`0.1.7` 是 `^4.0.3`,`0.2.0` 是 `~4.0.4`,`0.2.1-alpha.1` 是 `~4.0.5-alpha.1`。宿主要求哪个就装哪个,但钉法要当心:`^4.0.2` 会解析到 `4.0.4`,而 `0.1.5` 那几个版本并非针对它发布的,本插件随后就无法严格安装在它们旁边。矩阵脚本读取该范围后,把它的**下界**作为具体版本钉住。本插件自己的 peer 范围也需要补上 `>=4.0.5-alpha.1` 比较器,否则 `0.2.1-alpha.1` 根本装不上。
263
+
264
+ 同样支持 `@deepseek-ai/dsh-web`、`dsh-settings`(可选)和 `dsh-launch-environment`,覆盖上述整个范围。Node.js 需要 `>=22.19.0`,与 harness 自身的下限一致。
265
+
266
+ <details>
267
+ <summary>为什么 peer 范围长这样</summary>
268
+
269
+ ```jsonc
270
+ "@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 || >=0.2.0-rc.1 || >=0.2.1-alpha.1"
271
+ ```
272
+
273
+ 这串枚举是**在 pnpm 和 npm 下都能装遍所有已发布版本**的唯一写法。原因是 semver 的一条规则:
274
+
275
+ > prerelease 版本要满足某个范围,该范围中必须有一个比较器,它的 prerelease 落在**相同的 `major.minor.patch` 三段**上。
276
+
277
+ 所以 `>=0.1.2-rc.1` **匹配不到** `0.1.5-rc.2`,两者三段不同。单一开区间下界覆盖不了“以一串 prerelease 发布的项目”,而 `*` 会连未来的破坏性 `1.0` 一起放行。
278
+
279
+ 这个写法有两点很容易搞错,而这两点我都踩过:
280
+
281
+ - **这里的 `||` 不是拓宽范围,而是在挑下界。** 每个比较器都是开区间的 `>=`,所以整个表达式等于“大于等于各自 X”的并集——也就是“大于等于最大的那个 X”。为**更低**版本追加比较器是白费:在一个以 `>=0.1.8` 结尾的范围后再接 `|| >=0.2.0-rc.1`,会把 `0.1.8` 以及它到 `0.2.0-rc.1` 之间的所有版本一起踢出去,因为按上面那条三段规则,此时已经没有 0.1.8 的比较器可匹配。这一点是拿范围去跑真实发布列表才发现的,读是读不出来的。
282
+ - **每条 prerelease 线都需要落在自己三段上的比较器。** `0.2.0-rc.2` 满足 `>=0.2.0-rc.1`,但不满足 `>=0.2.1-alpha.1`,所以 `0.2.1-alpha.1` 需要第二个条目。同一条规则也适用于 `@deepseek-ai/cordis`——它自己的线走到了 `4.0.5-alpha.1`,而 `>=4.0.2` 把它排除在外,所以实际发布的范围是 `>=4.0.2 || >=4.0.5-alpha.1`。这一条不是好看不好看的问题:没有它,在 `0.2.1-alpha.1` 宿主旁用 `npm install` 装本插件会直接 `ERESOLVE` 失败。
283
+
284
+ 在真实发布物上实测的结果:
285
+
286
+ | 范围 | npm 可安装 |
287
+ |---|---|
288
+ | `>=0.1.2-rc.1`(最早的写法) | 它想覆盖的 14 个版本里只有 **1** 个 |
289
+ | 以 `0.1.8` 结尾的枚举 | **14 / 14** |
290
+ | 当前枚举 | 从 `0.1.2-alpha.2` 到 `0.2.1-alpha.1` 全部已发布版本 **20 / 20** |
291
+
292
+ 这个结论是测出来的。开区间在 pnpm 下没问题,而 `dsh plugin add` 用的正是 pnpm。但在 npm 下,它会让最初那 14 个版本中的 13 个报 `ERESOLVE`。如果你用 npm 安装旧版本的本插件时遇到该错误,升级即可,或临时加 `--legacy-peer-deps`。
293
+
294
+ **能解析**不等于**被测过**:矩阵覆盖的是每条版本线一个版本,外加任何改动了所依赖 seam 的版本——解析侧是上面这 20 个,而表格里是 17 行。`0.1.7-alpha.2`、`0.1.7-rc.1`、`0.1.7-rc.2` 能正常解析、也预期可用,但实际跑过的只有 `0.1.7-alpha.1`。
295
+
296
+ 插件自己的 `dsh.compatibility.dshReleases` 是另一份记录,不影响依赖解析:它逐个完整版本声明本次构建验证过哪些 dsh 发行版,供那些要求精确逐版本证据、不接受范围的目录使用。
297
+
298
+ </details>
299
+
300
+ ### 从源码构建
301
+
302
+ ```sh
303
+ pnpm install
304
+ pnpm run build # tsdown -> lib/index.js + lib/index.d.ts
305
+ pnpm run typecheck # tsc --noEmit
306
+ pnpm test # 先构建,再对 lib/ 跑 node:test 套件
307
+ ```
173
308
 
174
- **Q: 为什么 Web UI 里没有设置入口?**
175
- 本版本只在服务端注册了 `web-search-exa` 设置命名空间;UI 卡片计划在下一版本提供。现阶段通过 `cordis.patch.yml` 或环境变量配置(见[在 Web 面板中的呈现](#在-web-面板中的呈现))。
309
+ `src/` 是唯一的源文件目录。`lib/` 仍然提交进仓库,因为 npm 发布包和基于 git 的安装都依赖它。
176
310
 
177
- **Q: 支持哪些 dsh 版本?**
178
- 本版本支持 dsh `0.1.2-rc.1` 及其匹配的 `dsh-web`、`dsh-settings`、`dsh-launch-environment` 包;`0.1.5-alpha.1` 线未经本版本测试。
311
+ ## 致谢
179
312
 
180
- ## 致谢(Acknowledgements)
313
+ 匿名 MCP 接入方式参考了 [can1357/oh-my-pi](https://github.com/can1357/oh-my-pi) 的 `web_search` 实现(`packages/coding-agent/src/web/search/providers/exa.ts` 和 `src/exa/mcp-client.ts`)以及 [`@oh-my-pi/exa`](https://www.npmjs.com/package/@oh-my-pi/exa) 插件:同样的“有 key 走 REST、无 key 走免凭据 `mcp.exa.ai/mcp`”策略、同样的 `x-exa-source` 来源头、同样的 `Title:` 分节响应解析。感谢 oh-my-pi(omp)项目最先做出零配置的 Exa 接入。
181
314
 
182
- 匿名 MCP 接入方式参考了 [can1357/oh-my-pi](https://github.com/can1357/oh-my-pi) 的 `web_search` 实现(`packages/coding-agent/src/web/search/providers/exa.ts` 与 `src/exa/mcp-client.ts`)以及 [`@oh-my-pi/exa`](https://www.npmjs.com/package/@oh-my-pi/exa) 插件:同样的"有 key 走 REST、无 key 走免凭据 `mcp.exa.ai/mcp`"策略、同样的 `x-exa-source` 来源头、同样的 `Title:` 分节响应解析。感谢 oh-my-pi(omp)项目率先打通了零配置的 Exa 接入。
315
+ 同时感谢 **[Exa](https://exa.ai)** 提供并运营这个免费、免认证的托管 MCP 服务器(`mcp.exa.ai/mcp`),正是它让本包的零配置默认路径成为可能。Exa 托管 MCP 是 Exa 的官方产品,匿名使用有限流,见[限流](#限流)。
183
316
 
184
- 同时感谢 **[Exa](https://exa.ai)** 提供并运营这个**免费、免认证的托管 MCP 服务器**(`mcp.exa.ai/mcp`)——正是它让本包的零配置默认路径成为可能。Exa 托管 MCP 是 Exa 的官方产品;匿名使用有限流(见 FAQ)。
317
+ 感谢 **[@kahlos](https://github.com/kahlos)**([PR #1](https://github.com/TonyDua/dsh-web-search-exa/pull/1)):他发现 `web_search_advanced_exa` 返回的是结构化结果,并且端点在没有 `?tools=` 查询参数时根本不会服务这个工具。这两点都没有文档记载,只能靠实测得出。匿名路径现在默认走这个工具,旧的 `Title:` 分节路径保留为回退。
185
318
 
186
- ## 更新日志(Changelog)
319
+ ## 更新日志
187
320
 
188
321
  所有变更见 [CHANGELOG.md](CHANGELOG.md)。
189
322
 
190
323
  ## 许可证
191
324
 
192
- MIT —— 见 [LICENSE](LICENSE)。
325
+ MIT,见 [LICENSE](LICENSE)。