# @tonydua/dsh-web-search-exa

[English](README.md) | **简体中文**

[![npm version](https://img.shields.io/npm/v/@tonydua/dsh-web-search-exa)](https://www.npmjs.com/package/@tonydua/dsh-web-search-exa)
[![npm downloads](https://img.shields.io/npm/dm/@tonydua/dsh-web-search-exa)](https://www.npmjs.com/package/@tonydua/dsh-web-search-exa)
[![License](https://img.shields.io/npm/l/@tonydua/dsh-web-search-exa)](LICENSE)
[![GitHub stars](https://img.shields.io/github/stars/TonyDua/dsh-web-search-exa)](https://github.com/TonyDua/dsh-web-search-exa)
[![GitHub issues](https://img.shields.io/github/issues/TonyDua/dsh-web-search-exa)](https://github.com/TonyDua/dsh-web-search-exa)
[![Node](https://img.shields.io/badge/node-%3E%3D18-339933?logo=node.js&logoColor=white)](package.json)
[![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)

> 为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)（dsh）提供**零配置**的 [Exa](https://exa.ai) 网页搜索：
> **无需 API key** —— 一个 `ctx.web` seam 的 `WebSearchProvider`，内置匿名 MCP 兜底 + 带 key 的 REST 路径。

使用 [deepseek-v4-flash](https://api-docs.deepseek.com) 在 DeepSeek Harness（dsh）内开发。

## 当前支持的版本

`@tonydua/dsh-web-search-exa@0.1.4` 已针对以下版本测试并支持：

- `@deepseek-ai/dsh` `0.1.2-rc.1`（当前 npm `latest` 发布线）
- `@deepseek-ai/dsh-web` `0.1.2-rc.1`
- `@deepseek-ai/dsh-settings` `0.1.2-rc.1`（可选；用于启用实时 Settings 集成）
- `@deepseek-ai/dsh-launch-environment` `0.1.2-rc.1`
- `@deepseek-ai/cordis` `4.0.2`
- Node.js `>=18`

dsh `0.1.2-rc.1` 是本版本的兼容基线；`0.1.5-alpha.1` alpha 线不在本版本的测试支持矩阵内。

## 特性

- 🆓 **零配置、默认免 key** —— 搜索经由 Exa 官方托管的 MCP 服务器（`mcp.exa.ai/mcp`），**完全不携带凭据**（Exa 官方提供的免认证公共 MCP，有限流）。
- 🔑 **配 key 自动升级 REST** —— 设置 `EXA_API_KEY` 后自动切换到 Exa `POST /search` REST API（额度更高，行为不变）。
- 🔌 **即插即用** —— 注册进 dsh `ctx.web` seam；模型侧的 `web_search` / `web_fetch` 工具、提示词区段与结果卡片无需任何改动。
- 🎛️ **`providerId` 开关** —— 可与官方 `@deepseek-ai/dsh-web-search-exa` 在同一 profile 共存（不撞 id、无黑箱覆盖）。
- 📦 **可直接发布** —— MIT、ESM、内置类型声明、`files` 仅含 `lib/`。

## 为什么有这个包（与官方包的差异）

DeepSeek Harness 自带官方 Exa 提供方 [`@deepseek-ai/dsh-web-search-exa`](https://www.npmjs.com/package/@deepseek-ai/dsh-web-search-exa)。本包是它的**零配置变体**：补上了官方没有的匿名 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`，且想要官方维护的包** → 用 `@deepseek-ai/dsh-web-search-exa`，它是官方标准实现。
- **想零配置、免 key、无成本负担地试用 Exa 搜索** → 用本包。优雅降级：默认匿名 MCP，出现 key 自动走 REST。
- **两个都想要** → 一起装，用 `providerId` 开关（见[与官方包共存](#与官方包共存)）。

## 工作原理

| 条件 | 路径 | 端点 |
|---|---|---|
| 配置了 `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 限流：HTTP 429 会以 `WEB_PROVIDER_ERROR` 呈现，并提示配置 API key（配置后自动切换到 REST 路径）。

## 安装（装入 dsh profile）

**一条命令从 npm 安装**（v0.1.4+ 自带 `dsh.bundle` manifest——bundle patch 会自动插入 provider 行，无需手动改 patch）：

```powershell
dsh plugin --profile web add @tonydua/dsh-web-search-exa
```

重启 `dsh web` 生效。**无 API key 时**官方 DeepSeek 搜索提供方不可用，seam 会自动选中本插件——完全零配置。**配了 key 时**，需在你的 `$DSH_HOME/profiles/web/cordis.patch.yml`（在 bundle patch 之后应用）里显式选中 Exa：

```yaml
- id: web
  name: '@deepseek-ai/dsh-web'
  config:
    searchProvider: exa
```

…或用环境变量 `$DSH_WEB_SEARCH_PROVIDER=exa` 在运行时选中。

**本地开发目录：**

```powershell
dsh plugin --profile web add ../plugins/dsh-web-search-exa
```

然后启用并选中该提供方。合并进 `$DSH_HOME/profiles/web/cordis.patch.yml`（持久生效）：

```yaml
- id: web-search-exa
  name: '@tonydua/dsh-web-search-exa'
  config:
    apiKeyEnv: EXA_API_KEY
- id: web
  name: '@deepseek-ai/dsh-web'
  config:
    searchProvider: exa
```

也可以不修改配置，直接用环境变量 `$DSH_WEB_SEARCH_PROVIDER=exa` 在运行时选中该提供方。

重启 `dsh web` 生效。模型侧的 `web_search` 工具随即走该提供方，无需改任何工具配置。

### 运行时单例兼容性

`@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')` 失败。

## 配置

| 配置键 | 默认值 | 含义 |
|---|---|---|
| `providerId` | `exa` | 注册进 `ctx.web` 的提供方 id。仅当本包与官方包同时安装时才需要改（见下一节）。 |
| `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 句子数。 |

## 与官方包共存

两个包默认在 `ctx.web` 下注册**相同的 provider id（`exa`）**，cordis 插件名也都是 `web-search-exa`。seam 会拒绝重复 id（`WEB_DUPLICATE_PROVIDER`），所以**不改配置就把两个包装进同一个 profile 会在启动时报错**。

**没有黑箱覆盖**——共存必须显式配置，通过 `providerId` 开关完成：

1. 官方包保持 `exa`（它的 id 固定）。
2. 给本包一个不同 id——在本插件 `config` 里设 `providerId: exa-anon`（任意唯一字符串）。
3. 在 `web` seam 上显式选中匿名变体：`searchProvider: exa-anon`（或用环境变量 `$DSH_WEB_SEARCH_PROVIDER=exa-anon`）；若还想用官方包，再配 `searchProvider: exa` 切换。

```yaml
- 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 只装其中一个包，默认配置即可直接使用。

## 在 Web 面板中的呈现

**状态：本版本的配置入口在 profile 补丁层，不在 Web UI —— 没有可编辑的界面入口。** Settings UI 只渲染客户端插件为固定命名空间（`shell`、`agent-loop`、`web-search-deepseek`）手工注册的卡片，对任意插件命名空间没有通用表单。当前实际情况：

- **插件清单**（Settings → Plugins）：启用后自动出现 `web-search-exa`（`@tonydua/dsh-web-search-exa`）条目 —— 清单直接读取 Cordis loader 的实时条目，无需额外代码。
- **设置命名空间**（服务端）：插件通过当前的 `ctx.settings.installSection` API 注册了 `web-search-exa` 段，数据层可写——但**没有任何客户端卡片绑定它**，所以界面上不显示。内置的 "Web search" 卡片编辑的是官方 `web-search-deepseek` 命名空间，与本插件无关。
- **现在怎么改配置**：编辑 `$DSH_HOME/profiles/web/cordis.patch.yml` 里本插件的 `config`（字段与默认值见上方配置表），重启 `dsh web`；或用环境变量 `EXA_API_KEY` / `$DSH_WEB_SEARCH_PROVIDER`。`apiKey` 标记了 `role('secret')`，任何 `describe()` 响应都不会暴露其值。
- **搜索结果卡片**：`web_search` 调用经 `dsh-tool-web` 照常渲染 `web` 结果卡片（来源、摘要、日期），与提供方无关 —— 匿名 Exa 的结果与 DeepSeek 搜索显示完全一致。

**路线图（下一版本）**：新增注册到 `settings.plugin.item` slot 的客户端卡片，绑定 `web-search-exa` 命名空间，让上表所有字段可以在 Settings → Plugins 里实时编辑（与官方卡片同机制）。

## 常见问题（FAQ）

**Q: 需要 Exa API key 吗？**
不需要。无 key 时走 Exa 免费匿名托管 MCP；配 key 后走 REST API 获得更高额度。

**Q: 遇到 HTTP 429 / 限流怎么办？**
这是 Exa 匿名 MCP 的限流。配置 `EXA_API_KEY`（或 `apiKey` 字段），提供方会自动切到 REST 路径。

**Q: 能和官方 Exa 提供方一起装吗？**
可以——给本包一个不同的 `providerId` 并显式选中即可（见[与官方包共存](#与官方包共存)）。

**Q: 为什么 Web UI 里没有设置入口？**
本版本只在服务端注册了 `web-search-exa` 设置命名空间；UI 卡片计划在下一版本提供。现阶段通过 `cordis.patch.yml` 或环境变量配置（见[在 Web 面板中的呈现](#在-web-面板中的呈现)）。

**Q: 支持哪些 dsh 版本？**
本版本支持 dsh `0.1.2-rc.1` 及其匹配的 `dsh-web`、`dsh-settings`、`dsh-launch-environment` 包；`0.1.5-alpha.1` 线未经本版本测试。

## 致谢（Acknowledgements）

匿名 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 接入。

同时感谢 **[Exa](https://exa.ai)** 提供并运营这个**免费、免认证的托管 MCP 服务器**（`mcp.exa.ai/mcp`）——正是它让本包的零配置默认路径成为可能。Exa 托管 MCP 是 Exa 的官方产品；匿名使用有限流（见 FAQ）。

## 更新日志（Changelog）

所有变更见 [CHANGELOG.md](CHANGELOG.md)。

## 许可证

MIT —— 见 [LICENSE](LICENSE)。
