dsh-exa-search 1.0.0 → 1.0.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/CHANGELOG.md +2 -3
- package/README.en.md +93 -60
- package/README.md +60 -72
- package/index.js +11 -4
- package/package.json +3 -5
- package/scripts/repair-credentials.mjs +0 -101
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 1.0.
|
|
3
|
+
## 1.0.1
|
|
4
4
|
|
|
5
5
|
First public release: Exa-backed `web_search` / `web_fetch`, ten additional tools, and three access modes.
|
|
6
6
|
|
|
@@ -39,5 +39,4 @@ First public release: Exa-backed `web_search` / `web_fetch`, ten additional tool
|
|
|
39
39
|
|
|
40
40
|
### Notes
|
|
41
41
|
|
|
42
|
-
-
|
|
43
|
-
- Tests (`node --test`), the CI workflow and the English README are in the repository; they are not part of the npm tarball.
|
|
42
|
+
- Unit tests (`node --test`), the CI workflow and the English README live in the repository; they are not part of the npm tarball.
|
package/README.en.md
CHANGED
|
@@ -1,103 +1,136 @@
|
|
|
1
1
|
# Exa Search for DeepSeek Harness
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This plugin provides web search and page fetching for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) through [Exa](https://exa.ai). It supports three access modes — keyless, API key and Exa account — and registers tools according to the active mode.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
5
|
+
Once installed:
|
|
6
|
+
|
|
7
|
+
- `web_search` and `web_fetch` keep their existing arguments and behaviour; requests are served by Exa.
|
|
8
|
+
- The plugin registers ten additional `exa_*` tools. A tool is exposed to the model only when the active mode can call it.
|
|
9
|
+
- **Settings → Exa Search** switches modes and shows the availability of every tool under all three modes.
|
|
8
10
|
|
|
9
11
|
## Tool catalog
|
|
10
12
|
|
|
11
|
-
| Group | Tool |
|
|
13
|
+
| Group | Tool | Purpose | Keyless | API key | Exa account |
|
|
12
14
|
| --- | --- | --- | --- | --- | --- |
|
|
13
15
|
| Search | `web_search` | Harness web search, served by Exa | ✓ | ✓ | ✓ |
|
|
14
|
-
| Search | `exa_search` | Domain
|
|
16
|
+
| Search | `exa_search` | Domain, category, date, text and subpage filters | ✓ | ✓ | ✓ |
|
|
15
17
|
| Search | `exa_deep_search` | Deep Search: multi-step retrieval with a cited answer | — | ✓ | — |
|
|
16
18
|
| Contents | `web_fetch` | Harness page fetch, served by Exa | ✓ | ✓ | ✓ |
|
|
17
|
-
| Contents | `exa_fetch` | Up to
|
|
18
|
-
| Contents | `exa_find_similar` |
|
|
19
|
-
| Answer | `exa_answer` | `/answer`: cited answer to a question | — | ✓ | — |
|
|
20
|
-
| Research | `exa_research` | Exa Agent: research, list building
|
|
21
|
-
| Monitors | `exa_monitor` |
|
|
22
|
-
| Websets | `exa_webset` |
|
|
23
|
-
| Batch | `exa_batch` |
|
|
24
|
-
| Status | `exa_status` |
|
|
19
|
+
| Contents | `exa_fetch` | Up to five pages; optional highlights, summary and subpages (API key required for those three), three-way concurrency | ✓ | ✓ | ✓ |
|
|
20
|
+
| Contents | `exa_find_similar` | Expands a known URL into similar pages | — | ✓ | — |
|
|
21
|
+
| Answer | `exa_answer` | `/answer`: a cited answer to a question | — | ✓ | — |
|
|
22
|
+
| Research | `exa_research` | Exa Agent: research, list building and structured output; resume, cancel, stop | — | ✓ | per account |
|
|
23
|
+
| Monitors | `exa_monitor` | Monitors: scheduled or manual searches; list, create, update, trigger, delete | — | ✓ | — |
|
|
24
|
+
| Websets | `exa_webset` | Websets: build and read verified datasets | — | ✓* | — |
|
|
25
|
+
| Batch | `exa_batch` | Batch: submit many `/search` or `/agent/runs` requests | — | ✓* | — |
|
|
26
|
+
| Status | `exa_status` | Reports the active mode, available tools and upstream tool list | ✓ | ✓ | ✓ |
|
|
27
|
+
|
|
28
|
+
Twelve tools in total: ten registered by the plugin and two native Harness tools redirected to Exa.
|
|
29
|
+
|
|
30
|
+
\* Websets requires a Pro plan; Batch must be enabled for the team. When an API key is active, the plugin performs one free probe for each. Features the team does not have are not registered as tools; the settings page marks them as unavailable instead of letting the model receive a 401 or 403.
|
|
31
|
+
|
|
32
|
+
## Deprecated MCP tools
|
|
33
|
+
|
|
34
|
+
The Exa MCP repository keeps eight deprecated entries in `toolRegistry.ts` (disabled by default). Their capabilities are covered by the current tools, so the plugin does not register them again:
|
|
35
|
+
|
|
36
|
+
| Deprecated tool | Replacement |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `get_code_context_exa` | `exa_search` with `category=github` |
|
|
39
|
+
| `company_research_exa` | `exa_search` with `category=company` |
|
|
40
|
+
| `people_search_exa` | `exa_search` with `category=people` |
|
|
41
|
+
| `linkedin_search_exa` | `exa_search` with `category=linkedin profile` |
|
|
42
|
+
| `deep_search_exa` | `exa_deep_search` |
|
|
43
|
+
| `crawling_exa` | `exa_fetch` |
|
|
44
|
+
| `deep_researcher_start` | `exa_research` |
|
|
45
|
+
| `deep_researcher_check` | `exa_research` with `runId` |
|
|
25
46
|
|
|
26
|
-
|
|
47
|
+
The same table appears at the bottom of the settings page.
|
|
27
48
|
|
|
28
49
|
## Access modes
|
|
29
50
|
|
|
30
51
|
| Mode | Credential | Upstream | Notes |
|
|
31
52
|
| --- | --- | --- | --- |
|
|
32
|
-
| `keyless` | none | `mcp.exa.ai/mcp
|
|
33
|
-
| `apiKey` | saved
|
|
34
|
-
| `oauth` | browser sign-in | `mcp.exa.ai/mcp
|
|
53
|
+
| `keyless` | none | `https://mcp.exa.ai/mcp?tools=...` | Basic search, advanced search and fetching. Exa documents `web_search_advanced_exa` as usable without authentication, so it is enabled through the `tools` parameter. `agent_run` requires authentication and cannot be added to a keyless connection (the whole connection then returns 401). |
|
|
54
|
+
| `apiKey` | key saved in Settings, or `EXA_API_KEY` | `https://api.exa.ai` | The full REST surface: Search, Contents, Find Similar, Answer, Agent, Monitors and Websets. |
|
|
55
|
+
| `oauth` | browser sign-in | `https://mcp.exa.ai/mcp?tools=...` | MCP tools follow the account. `agent_run` is probed after sign-in; `exa_research` is registered only when it is present. |
|
|
35
56
|
|
|
36
|
-
|
|
57
|
+
“Per account” means the tool is registered according to the real `tools/list` response after sign-in, not a fixed setting.
|
|
37
58
|
|
|
38
59
|
## Implementation notes
|
|
39
60
|
|
|
40
|
-
|
|
41
|
-
- **Rate limits**: keyless answers with `_meta.ai.exa/rateLimited` or a plain text notice; both are translated into a clear error instead of being returned as results.
|
|
42
|
-
- **Deep Search** only synthesizes when `outputSchema` is present. The tool sends a `{ "type": "text" }` schema by default and returns structured JSON if you pass your own.
|
|
43
|
-
- **Websets** lives on its own mount point: `api.exa.ai/websets/v0/websets…`.
|
|
44
|
-
- **Credentials** are stored under `exa-search/api-key` / `exa-search/oauth` (the service requires `<scope>/<id>`), resolved through an injection, and writes are validated before touching the vault.
|
|
45
|
-
- **Request policy**: hard timeouts (search 30s, MCP 60s, Agent 240s), retries with backoff for 429/5xx and `Retry-After`, and one forced token refresh on a 401.
|
|
46
|
-
- **Spend estimate**: accumulated from Exa's `costDollars`; Agent runs are counted once at a terminal status. OAuth MCP search responses carry no cost field, so the estimate covers REST calls and Agent runs.
|
|
61
|
+
**MCP `tools` parameter.** The hosted Exa MCP endpoint exposes only `web_search_exa` and `web_fetch_exa` by default, and `agent_run` only on authenticated connections. The plugin builds the URL per mode:
|
|
47
62
|
|
|
48
|
-
|
|
63
|
+
- Keyless: `?tools=web_search_exa,web_fetch_exa,web_search_advanced_exa`
|
|
64
|
+
- Exa account: `?tools=...,agent_run`; if the probe fails, the request is retried without `agent_run`.
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
dsh plugin add dsh-exa-search
|
|
52
|
-
```
|
|
66
|
+
**Rate limits.** When the keyless channel is rate limited, Exa returns `_meta.ai.exa/rateLimited` or a plain “free MCP rate limit” message rather than a JSON-RPC error. The plugin converts either into an explicit error that asks for an API key or sign-in; the text is never returned as a search result.
|
|
53
67
|
|
|
54
|
-
|
|
68
|
+
**Agent continuation.** `exa_research` waits at most `timeoutSeconds` per call (default 120, maximum 600). On timeout it returns `status: "running"` and a `runId`; a later call with that `runId` continues waiting. API key mode uses `POST /agent/runs` with polling; account mode uses the MCP `agent_run` tool.
|
|
55
69
|
|
|
56
|
-
|
|
70
|
+
**Category mapping.** The REST API accepts category names such as `research paper` and `linkedin profile`, while advanced MCP search uses a strict enum. The plugin maps the former to `publication` and `people`; categories without a mapping are dropped rather than sent unchanged.
|
|
57
71
|
|
|
58
|
-
|
|
72
|
+
**Deep Search and `outputSchema`.** Exa synthesizes an answer only when the request contains `outputSchema`; without it, `type: deep` still returns only `results`. The plugin supplies `{ "type": "text" }` by default to obtain a prose answer and returns structured JSON when the caller passes an object schema.
|
|
59
73
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
| `mcpTools` | `null` | Overrides the MCP `tools` list (array or comma string). |
|
|
68
|
-
| `toolGroups` | `null` | Initial per-group tool switches, e.g. `{ "answer": false }`. |
|
|
69
|
-
| `locale` | `auto` | Message language: `auto` / `en` / `zh`. |
|
|
74
|
+
**Websets mount point.** In the OpenAPI document, `/v0/websets` declares `servers: https://api.exa.ai/websets`; the effective path is `/websets/v0/websets…`. Without the prefix every request returns 404.
|
|
75
|
+
|
|
76
|
+
**Entitlement probes.** Batch returns 403 when disabled and Websets returns 401 without a Pro plan. Both probes are free GETs; the plugin runs them at startup and on mode changes and does not register tools that the team cannot use.
|
|
77
|
+
|
|
78
|
+
**Fetch formats.** `exa_fetch` supports `format: text`, `highlights` and `summary`. API key mode requests the corresponding Contents fields; keyless and account modes return body text only and report a clear error for other formats. Multiple URLs are fetched three at a time; a failed URL carries an `error` field and does not affect the remaining pages.
|
|
79
|
+
|
|
80
|
+
**Monitor updates.** `exa_monitor` with `action=update` can rename a monitor, pause or resume it (`status: active | paused`), and change its query or interval. `manual: true` or `period: "none"` creates a monitor without a schedule, which runs only through `trigger`.
|
|
70
81
|
|
|
71
|
-
|
|
82
|
+
**Credential handling.** The credential service is resolved through `ctx.inject`, so a service that activates after the plugin does not cause a fallback to plaintext storage. Every write validates that the record key follows `<scope>/<id>`, which prevents records that the service cannot load at the next start. When a refresh token is revoked (`invalid_grant`), the plugin drops the grant and falls back to an API key or keyless mode instead of returning 401 indefinitely.
|
|
72
83
|
|
|
73
|
-
|
|
74
|
-
- API key and OAuth token: the Harness credential service, under `exa-search/api-key` / `exa-search/oauth`. Only when the vault is unavailable do they fall back to `state.json` (mode 0600).
|
|
75
|
-
- Nothing is written into the install directory, and no state ships with the package.
|
|
84
|
+
**Request policy.** Every outbound request has a hard timeout (search 30s, contents 30s, MCP tools 60s, Agent 240s). 429/5xx responses and network failures are retried twice with exponential backoff, honouring `Retry-After`; a cancellation signal interrupts the wait immediately.
|
|
76
85
|
|
|
77
|
-
|
|
86
|
+
**OAuth token expiry.** On a 401 from MCP, the plugin force-refreshes the token and replays the request once; only a second failure is reported as a sign-in requirement.
|
|
87
|
+
|
|
88
|
+
**Cancelling Agent runs.** `exa_research` supports `action: cancel | stop` with a `runId` (`cancel` stops immediately; `stop` keeps the results collected so far). This is available only in API key mode; OAuth tokens can call MCP only, which exposes no cancellation endpoint.
|
|
89
|
+
|
|
90
|
+
**Spend estimate.** Exa reports `costDollars.total` on billed REST responses. The plugin accumulates it per process and shows it in `exa_status` and on the settings page. Agent runs are recorded once, when they reach a terminal status, so polling does not double-count. OAuth MCP search responses carry no cost field, so that mode estimates Agent runs only; the counter restarts at zero with the process.
|
|
91
|
+
|
|
92
|
+
## Installation
|
|
93
|
+
|
|
94
|
+
Install this directory through the Harness plugin manager, or install `dsh-exa-search` from npm.
|
|
95
|
+
|
|
96
|
+
A local directory install stores a link rather than a copy in the profile; moving or deleting the source directory disables the plugin.
|
|
97
|
+
|
|
98
|
+
Installation pins the `web` row to the `exa` provider (see `cordis.patch.yml`). The pin is required: with two usable search providers, `ctx.web` fails closed with `WEB_PROVIDER_AMBIGUOUS` rather than choosing one.
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
| Key | Default | Description |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| `mode` | `keyless` | Initial access mode; the settings page overrides it. |
|
|
105
|
+
| `apiKeyEnv` | `EXA_API_KEY` | Environment variable read when no key is saved. |
|
|
106
|
+
| `numResults` | `8` | Default number of sources. |
|
|
107
|
+
| `searchType` | `auto` | REST search effort: `auto`, `lite` or `fast`. |
|
|
108
|
+
| `maxCharacters` | `12000` | Per-page fetch limit. |
|
|
109
|
+
| `locale` | `auto` | Message language: `auto` follows the system, or `en` / `zh`. |
|
|
110
|
+
| `mcpTools` | `null` | Overrides the MCP `tools` list (array or comma-separated string). |
|
|
111
|
+
| `toolGroups` | `null` | Initial per-group switches, e.g. `{ "answer": false, "webset": false }`. Changes made in Settings are stored in the state file and override this value. |
|
|
78
112
|
|
|
79
|
-
|
|
113
|
+
Each group on the settings page has a switch. Disabling a group removes its `exa_*` tools from the model's tool list immediately; the `web_search` / `web_fetch` redirection is unaffected. The **Test connection** button at the top performs one minimal search with the active mode and reports latency and a sample result (one search is consumed).
|
|
80
114
|
|
|
81
|
-
|
|
82
|
-
node scripts/repair-credentials.mjs --dry-run
|
|
83
|
-
node scripts/repair-credentials.mjs
|
|
84
|
-
```
|
|
115
|
+
## Data and credentials
|
|
85
116
|
|
|
86
|
-
|
|
117
|
+
- Access mode and OAuth client registration: `<DSH_HOME>/exa-search/state.json`. The Host process does not guarantee `DSH_PROFILE_DIR`, so this state is stored per user rather than per profile.
|
|
118
|
+
- API key and OAuth tokens: the Harness credential service (`ctx.credentials`), under the record keys `exa-search/api-key` and `exa-search/oauth`. They fall back to `state.json` (file mode 0600) only when the credential service is unavailable.
|
|
119
|
+
- No credential is written into the installation directory, and no state file is shipped with the package.
|
|
87
120
|
|
|
88
121
|
## Routes
|
|
89
122
|
|
|
90
123
|
The settings page uses same-origin routes instead of generated remotes:
|
|
91
124
|
|
|
92
|
-
- `GET /exa-plugin/state` — mode,
|
|
93
|
-
- `GET /exa-plugin/diagnostics` — redacted snapshot for
|
|
94
|
-
- `POST /exa-plugin/mode` / `api-key` / `login` / `logout` / `tools` / `test` — mutations
|
|
125
|
+
- `GET /exa-plugin/state` — active mode, credential status, upstream tool list, tool matrix and per-mode counts;
|
|
126
|
+
- `GET /exa-plugin/diagnostics` — a redacted snapshot for issue reports;
|
|
127
|
+
- `POST /exa-plugin/mode` / `api-key` / `login` / `logout` / `tools` / `test` — mutations; cross-origin requests are rejected;
|
|
95
128
|
- `GET /exa-plugin/oauth/callback` — OAuth callback.
|
|
96
129
|
|
|
97
|
-
##
|
|
130
|
+
## Scope
|
|
98
131
|
|
|
99
|
-
Team management, webhooks
|
|
132
|
+
Team management, webhooks and events, imports and Websets enrichments are account or event-delivery plumbing and are not exposed as model tools. The search, contents, answer, research, monitor, dataset and batch workflows are complete.
|
|
100
133
|
|
|
101
134
|
## Uninstall
|
|
102
135
|
|
|
103
|
-
`dsh plugin remove dsh-exa-search`.
|
|
136
|
+
`dsh plugin remove dsh-exa-search`. After removal, the `web` row is decided by the underlying bundles again.
|
package/README.md
CHANGED
|
@@ -1,39 +1,41 @@
|
|
|
1
1
|
# Exa Search for DeepSeek Harness
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> English documentation: [README.en.md](./README.en.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
本插件为 DeepSeek Harness 提供基于 [Exa](https://exa.ai) 的联网搜索与网页抓取能力,支持免登录、API Key 与 Exa 账号三种接入方式,并按接入方式注册工具。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
7
|
+
安装后:
|
|
8
|
+
|
|
9
|
+
- `web_search`、`web_fetch` 的调用方式与参数保持不变,实际请求由 Exa 提供;
|
|
10
|
+
- 插件额外注册 10 个 `exa_*` 工具;仅当当前接入方式可以调用某个工具时,该工具才会暴露给模型;
|
|
11
|
+
- 在「设置 → Exa 搜索」中可切换接入方式,并查看全部工具在三种方式下的可用性。
|
|
10
12
|
|
|
11
13
|
## 工具清单(按功能分组)
|
|
12
14
|
|
|
13
15
|
| 分组 | 工具 | 作用 | 免登录 | API Key | Exa 账号 |
|
|
14
16
|
| --- | --- | --- | --- | --- | --- |
|
|
15
|
-
| 检索 | `web_search` | Harness
|
|
17
|
+
| 检索 | `web_search` | Harness 原生搜索,由 Exa 提供 | ✓ | ✓ | ✓ |
|
|
16
18
|
| 检索 | `exa_search` | 域名 / 分类 / 日期 / 文本 / 子页过滤 | ✓ | ✓ | ✓ |
|
|
17
|
-
| 检索 | `exa_deep_search` | Deep Search
|
|
18
|
-
| 内容 | `web_fetch` | Harness
|
|
19
|
-
| 内容 | `exa_fetch` |
|
|
20
|
-
| 内容 | `exa_find_similar` |
|
|
19
|
+
| 检索 | `exa_deep_search` | Deep Search:多步检索与带引用的综合答案 | — | ✓ | — |
|
|
20
|
+
| 内容 | `web_fetch` | Harness 原生抓取,由 Exa 提供 | ✓ | ✓ | ✓ |
|
|
21
|
+
| 内容 | `exa_fetch` | 最多 5 个网页;可选取 highlights / summary / 子页(后三者需 API Key),3 路并发 | ✓ | ✓ | ✓ |
|
|
22
|
+
| 内容 | `exa_find_similar` | 由一篇已知网址扩展相似页面 | — | ✓ | — |
|
|
21
23
|
| 问答 | `exa_answer` | `/answer`:带引用的直接问答 | — | ✓ | — |
|
|
22
|
-
| 研究 | `exa_research` | Exa Agent
|
|
24
|
+
| 研究 | `exa_research` | Exa Agent:研究、名单构建、结构化输出(续等 / 取消 / 停止) | — | ✓ | 按账号 |
|
|
23
25
|
| 监控 | `exa_monitor` | Monitors:定时或手动搜索,列表 / 新建 / 修改 / 触发 / 删除 | — | ✓ | — |
|
|
24
|
-
| 数据集 | `exa_webset` | Websets
|
|
26
|
+
| 数据集 | `exa_webset` | Websets:构建与读取经过验证的数据集 | — | ✓* | — |
|
|
25
27
|
| 批量 | `exa_batch` | Batch:批量提交 `/search` 或 `/agent/runs` | — | ✓* | — |
|
|
26
28
|
| 状态 | `exa_status` | 报告当前方式、可用工具与上游工具表 | ✓ | ✓ | ✓ |
|
|
27
29
|
|
|
28
|
-
合计 12
|
|
30
|
+
合计 12 个工具,其中 10 个由插件注册,2 个为 Harness 原生工具的 Exa 改道。
|
|
29
31
|
|
|
30
|
-
\* Websets 需要 Pro 计划,Batch 需要团队单独开通。启用 API Key
|
|
32
|
+
\* Websets 需要 Pro 计划,Batch 需要团队单独开通。启用 API Key 时,插件会分别执行一次免费探测;未开通的功能不会注册为工具,设置页会标注「当前团队未开通」,以避免模型调用后收到 401/403。
|
|
31
33
|
|
|
32
|
-
## 旧版 MCP
|
|
34
|
+
## 旧版 MCP 工具映射
|
|
33
35
|
|
|
34
|
-
Exa MCP 仓库的 `toolRegistry.ts`
|
|
36
|
+
Exa MCP 仓库的 `toolRegistry.ts` 中保留了 8 个已弃用条目(默认关闭)。这些能力已由现有工具覆盖,因此插件不再重复注册:
|
|
35
37
|
|
|
36
|
-
| 旧工具 |
|
|
38
|
+
| 旧工具 | 对应工具 |
|
|
37
39
|
| --- | --- |
|
|
38
40
|
| `get_code_context_exa` | `exa_search`,`category=github` |
|
|
39
41
|
| `company_research_exa` | `exa_search`,`category=company` |
|
|
@@ -42,109 +44,95 @@ Exa MCP 仓库的 `toolRegistry.ts` 里还有 8 个已弃用条目(默认关
|
|
|
42
44
|
| `deep_search_exa` | `exa_deep_search` |
|
|
43
45
|
| `crawling_exa` | `exa_fetch` |
|
|
44
46
|
| `deep_researcher_start` | `exa_research` |
|
|
45
|
-
| `deep_researcher_check` | `exa_research
|
|
47
|
+
| `deep_researcher_check` | `exa_research`,传入 `runId` |
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
设置页底部同样列出该对照表,以便核对工具是否缺失。
|
|
48
50
|
|
|
49
|
-
##
|
|
51
|
+
## 接入方式
|
|
50
52
|
|
|
51
|
-
| 方式 | 凭据 | 上游 |
|
|
53
|
+
| 方式 | 凭据 | 上游 | 说明 |
|
|
52
54
|
| --- | --- | --- | --- |
|
|
53
|
-
| 免登录 `keyless` | 不需要 | `https://mcp.exa.ai/mcp?tools=...` |
|
|
54
|
-
| API Key `apiKey` |
|
|
55
|
-
| Exa 账号 `oauth` | 浏览器授权 | `https://mcp.exa.ai/mcp?tools=...` | MCP 工具随账号开放;`agent_run`
|
|
55
|
+
| 免登录 `keyless` | 不需要 | `https://mcp.exa.ai/mcp?tools=...` | 基础搜索、高级搜索与抓取。Exa 说明 `web_search_advanced_exa` 无需认证,因此通过 `tools` 参数启用;`agent_run` 需要认证,不能加入免登录连接(否则整个连接返回 401)。 |
|
|
56
|
+
| API Key `apiKey` | 设置页填入,或环境变量 `EXA_API_KEY` | `https://api.exa.ai` | 全部接口:Search、Contents、Find Similar、Answer、Agent、Monitors、Websets。 |
|
|
57
|
+
| Exa 账号 `oauth` | 浏览器授权 | `https://mcp.exa.ai/mcp?tools=...` | MCP 工具随账号开放;`agent_run` 在登录后探测,探测到才会注册 `exa_research`。 |
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
表格中的「按账号」表示该工具是否注册取决于登录后 `tools/list` 的实际返回,而非固定配置。
|
|
58
60
|
|
|
59
|
-
##
|
|
61
|
+
## 实现说明
|
|
60
62
|
|
|
61
|
-
**MCP 的 tools 参数**。Exa
|
|
63
|
+
**MCP 的 `tools` 参数**。Exa 托管 MCP 默认仅提供 `web_search_exa` 与 `web_fetch_exa`,`agent_run` 仅对有凭据的连接开放。插件按接入方式拼接 URL:
|
|
62
64
|
|
|
63
65
|
- 免登录:`?tools=web_search_exa,web_fetch_exa,web_search_advanced_exa`
|
|
64
|
-
- 账号:`?tools=...,agent_run
|
|
66
|
+
- Exa 账号:`?tools=...,agent_run`;若探测失败,则去掉 `agent_run` 重试。
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
**限流识别**。免登录通道触发限流时,Exa 返回 `_meta.ai.exa/rateLimited` 或一段 "free MCP rate limit" 文本,而非 JSON-RPC 错误。插件会将其转换为明确错误,并提示配置 API Key 或登录,不会将限流文本作为搜索结果返回。
|
|
67
69
|
|
|
68
|
-
**Agent 续等**。`exa_research`
|
|
70
|
+
**Agent 续等**。`exa_research` 单次最多等待 `timeoutSeconds`(默认 120,上限 600);超时返回 `status: "running"` 与 `runId`,再次调用并传入该 `runId` 可继续等待。API Key 模式使用 `POST /agent/runs` 与轮询,Exa 账号模式使用 MCP 的 `agent_run`。
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
**分类映射**。REST 接口接受 `research paper`、`linkedin profile` 等分类名,MCP 高级搜索使用严格枚举。插件将前者映射为 `publication`、`people`;无法映射的分类会被丢弃,不会原样发送。
|
|
71
73
|
|
|
72
|
-
**Deep Search
|
|
74
|
+
**Deep Search 的 `outputSchema`**。Exa 仅在请求包含 `outputSchema` 时生成综合答案;缺失时 `type: deep` 也只返回 `results`。插件默认补充 `{ "type": "text" }` 以获得散文答案;调用方传入对象 schema 时返回结构化 JSON。
|
|
73
75
|
|
|
74
|
-
**Websets
|
|
76
|
+
**Websets 的挂载点**。OpenAPI 文档中 `/v0/websets` 的 `servers` 为 `https://api.exa.ai/websets`,实际地址为 `/websets/v0/websets…`;缺少该前缀会持续返回 404。
|
|
75
77
|
|
|
76
|
-
**功能开通探测**。Batch 未开通返回 403
|
|
78
|
+
**功能开通探测**。Batch 未开通返回 403,Websets 未开通返回 401(Pro 计划)。两者均为免费 GET;插件在启动或切换接入方式时探测,未开通则不注册对应工具。
|
|
77
79
|
|
|
78
|
-
**抓取格式**。`exa_fetch` 的 `format` 支持 `text
|
|
80
|
+
**抓取格式**。`exa_fetch` 的 `format` 支持 `text`、`highlights`、`summary`。API Key 模式下分别请求 Contents API 的对应字段;免登录与账号模式的 MCP 抓取仅返回正文,请求其他格式会返回明确错误。多 URL 抓取采用 3 路并发,单个 URL 失败不影响其余页面,失败信息以 `error` 字段返回。
|
|
79
81
|
|
|
80
|
-
**监控更新**。`exa_monitor
|
|
82
|
+
**监控更新**。`exa_monitor` 的 `update` 操作支持修改名称、暂停 / 恢复(`status: active | paused`)、修改查询与周期;`manual: true` 或 `period: "none"` 可创建不排期的监控,仅能通过 `trigger` 手动执行。
|
|
81
83
|
|
|
82
|
-
**凭据读写**。凭据服务通过 `ctx.inject`
|
|
84
|
+
**凭据读写**。凭据服务通过 `ctx.inject` 解析,即使其激活晚于插件也不会退回明文存储;写入前会校验记录键必须符合 `<scope>/<id>`,避免产生无法在下次启动时加载的记录。刷新令牌被吊销(`invalid_grant`)时,插件会丢弃授权并回退到 API Key 或免登录,而非持续返回 401。
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
**请求策略**。所有出站请求均设置硬超时(搜索 30s、内容 30s、MCP 工具 60s、Agent 240s);429/5xx 与网络失败按指数退避重试 2 次,并遵循 `Retry-After`;取消信号会立即中断等待。
|
|
85
87
|
|
|
86
|
-
**OAuth 令牌失效**。MCP 返回 401
|
|
88
|
+
**OAuth 令牌失效**。MCP 返回 401 时,插件先强制刷新令牌并使用新令牌重放请求;仅当刷新后仍失败时才提示重新登录。
|
|
87
89
|
|
|
88
|
-
**取消 Agent**。`exa_research` 的 `action: cancel | stop` 配合 `runId` 可中止运行(`cancel` 立即取消,`stop`
|
|
90
|
+
**取消 Agent**。`exa_research` 的 `action: cancel | stop` 配合 `runId` 可中止运行(`cancel` 立即取消,`stop` 保留已收集结果),仅在 API Key 模式下可用;OAuth 令牌仅能通过 MCP 调用,没有取消入口。
|
|
89
91
|
|
|
90
|
-
**费用估算**。Exa 在 REST
|
|
92
|
+
**费用估算**。Exa 在 REST 计费响应中返回 `costDollars.total`,插件按进程累计,并在 `exa_status` 与设置页中显示;Agent 运行仅在到达终态时记录一次,轮询不会重复计数。OAuth 模式下搜索响应不包含费用字段,因此该模式的估算仅覆盖 Agent 运行;进程重启后从 0 开始。
|
|
91
93
|
|
|
92
94
|
## 安装
|
|
93
95
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
可通过 Harness 插件管理安装本目录,或从 npm 安装 `dsh-exa-search`。
|
|
97
|
+
使用本地目录安装时,profile 中保存的是链接而非副本;移动或删除源目录会导致插件失效。
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
安装后,`web` 行会被固定到 `exa` provider(见 `cordis.patch.yml`)。这是必要配置:当存在两个可用搜索 provider 时,`ctx.web` 会以 `WEB_PROVIDER_AMBIGUOUS` 拒绝选择,而不会自动猜测。
|
|
98
100
|
|
|
99
101
|
## 配置
|
|
100
102
|
|
|
101
|
-
`cordis.patch.yml`
|
|
103
|
+
`cordis.patch.yml` 中的行配置:
|
|
102
104
|
|
|
103
105
|
| 键 | 默认 | 说明 |
|
|
104
106
|
| --- | --- | --- |
|
|
105
107
|
| `mode` | `keyless` | 首次运行的默认接入方式;设置页会覆盖它。 |
|
|
106
|
-
| `apiKeyEnv` | `EXA_API_KEY` |
|
|
108
|
+
| `apiKeyEnv` | `EXA_API_KEY` | 未保存 Key 时读取的环境变量。 |
|
|
107
109
|
| `numResults` | `8` | 默认返回来源数。 |
|
|
108
110
|
| `searchType` | `auto` | 检索强度:`auto` / `lite` / `fast`。 |
|
|
109
111
|
| `maxCharacters` | `12000` | 单页抓取字符上限。 |
|
|
110
112
|
| `locale` | `auto` | 消息语言:`auto` 跟随系统,或强制 `en` / `zh`。 |
|
|
111
113
|
| `mcpTools` | `null` | 覆盖 MCP 的 `tools` 列表(数组或逗号分隔字符串)。 |
|
|
112
|
-
| `toolGroups` | `null` | 分组开关的初始值,例如 `{ "answer": false, "webset": false }
|
|
113
|
-
|
|
114
|
-
设置页每个分组右侧有一个开关,关掉后该组的 `exa_*` 工具立刻从模型工具表里移除;`web_search` / `web_fetch` 的改道不受影响。设置页顶部还有「测试连接」按钮,会用当前方式跑一次最小搜索并返回耗时与示例结果(消耗一次搜索)。
|
|
115
|
-
|
|
116
|
-
## 数据落在哪
|
|
117
|
-
|
|
118
|
-
- 接入方式、OAuth 客户端注册信息:`<DSH_HOME>/exa-search/state.json`。
|
|
119
|
-
Host 进程不保证有 `DSH_PROFILE_DIR`,所以这份状态按用户存放,而不是按 profile。
|
|
120
|
-
- API Key 与 OAuth 令牌:Harness 凭据服务(`ctx.credentials`),记录键为 `exa-search/api-key`、`exa-search/oauth`;
|
|
121
|
-
只有凭据服务不可用时才退回上面的 `state.json`(文件权限 0600)。
|
|
122
|
-
- 插件不会把任何凭据写进安装目录,也不会随包分发状态文件。
|
|
123
|
-
|
|
124
|
-
> **如果你装过 1.0.0 之前的本地开发版**:旧版把凭据写成 `exa-api-key` / `exa-oauth`,而 Harness 的凭据服务只接受 `<scope>/<id>`,会在下次启动时解析失败并让整个应用起不来。关闭应用后运行下面的修复脚本,或手动改名/删除这两条记录。
|
|
125
|
-
|
|
126
|
-
## 启动崩溃后的修复
|
|
114
|
+
| `toolGroups` | `null` | 分组开关的初始值,例如 `{ "answer": false, "webset": false }`;设置页的改动存入 state 并覆盖它。 |
|
|
127
115
|
|
|
128
|
-
|
|
116
|
+
设置页中每个分组右侧均有开关;关闭后,该分组下的 `exa_*` 工具会立即从模型工具表中移除,`web_search` / `web_fetch` 的 Exa 改道不受影响。设置页顶部的「测试连接」按钮会使用当前接入方式执行一次最小搜索,并返回耗时与示例结果(消耗一次搜索)。
|
|
129
117
|
|
|
130
|
-
|
|
131
|
-
node scripts/repair-credentials.mjs --dry-run # 预演,只打印会改什么
|
|
132
|
-
node scripts/repair-credentials.mjs # 备份并把两个旧键改成 exa-search/*
|
|
133
|
-
```
|
|
118
|
+
## 数据与凭据
|
|
134
119
|
|
|
135
|
-
|
|
120
|
+
- 接入方式与 OAuth 客户端注册信息:`<DSH_HOME>/exa-search/state.json`。Host 进程不保证存在 `DSH_PROFILE_DIR`,因此该状态按用户存储,而非按 profile。
|
|
121
|
+
- API Key 与 OAuth 令牌:Harness 凭据服务(`ctx.credentials`),记录键为 `exa-search/api-key`、`exa-search/oauth`;仅当凭据服务不可用时,才退回上述 `state.json`(文件权限 0600)。
|
|
122
|
+
- 插件不会将任何凭据写入安装目录,也不会随包分发状态文件。
|
|
136
123
|
|
|
137
124
|
## 路由
|
|
138
125
|
|
|
139
126
|
设置页通过同源路由读写 Host,避免为纯 JS 插件生成 Remote 命名空间:
|
|
140
127
|
|
|
141
|
-
- `GET
|
|
142
|
-
- `
|
|
143
|
-
- `
|
|
128
|
+
- `GET /exa-plugin/state` — 当前方式、凭据状态、上游工具表、工具矩阵与各方式计数;
|
|
129
|
+
- `GET /exa-plugin/diagnostics` — 脱敏诊断快照,用于问题反馈;
|
|
130
|
+
- `POST /exa-plugin/mode` / `api-key` / `login` / `logout` / `tools` / `test` — 变更操作,拒绝跨源请求;
|
|
131
|
+
- `GET /exa-plugin/oauth/callback` — OAuth 回调。
|
|
144
132
|
|
|
145
|
-
##
|
|
133
|
+
## 未覆盖范围
|
|
146
134
|
|
|
147
|
-
Team Management、Webhooks/Events、Imports 属于账号与事件投递的底层接口,不注册为模型工具;检索、内容、问答、研究、监控、数据集、批量这条链路已经完整。
|
|
135
|
+
Team Management、Webhooks / Events、Imports 属于账号与事件投递的底层接口,不注册为模型工具;检索、内容、问答、研究、监控、数据集、批量这条链路已经完整。
|
|
148
136
|
|
|
149
137
|
## 卸载
|
|
150
138
|
|
package/index.js
CHANGED
|
@@ -25,9 +25,16 @@ import path from 'node:path'
|
|
|
25
25
|
import os from 'node:os'
|
|
26
26
|
import { createHash, randomBytes } from 'node:crypto'
|
|
27
27
|
|
|
28
|
-
// Read
|
|
29
|
-
//
|
|
30
|
-
|
|
28
|
+
// Read on demand: the Host may keep this module cached across plugin reloads,
|
|
29
|
+
// so a version baked in at load time can go stale after a bump.
|
|
30
|
+
function packageInfo() {
|
|
31
|
+
try {
|
|
32
|
+
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'))
|
|
33
|
+
return { name: typeof pkg.name === 'string' ? pkg.name : 'dsh-exa-search', version: String(pkg.version || 'unknown') }
|
|
34
|
+
} catch {
|
|
35
|
+
return { name: 'dsh-exa-search', version: 'unknown' }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
31
38
|
|
|
32
39
|
export const name = 'exa-search'
|
|
33
40
|
export const inject = ['web', 'tools']
|
|
@@ -1625,7 +1632,7 @@ export function apply(ctx, config = {}) {
|
|
|
1625
1632
|
async function diagnosticsPayload() {
|
|
1626
1633
|
const payload = await statePayload()
|
|
1627
1634
|
return {
|
|
1628
|
-
plugin:
|
|
1635
|
+
plugin: packageInfo(),
|
|
1629
1636
|
generatedAt: new Date().toISOString(),
|
|
1630
1637
|
locale: activeLocale,
|
|
1631
1638
|
mode: payload.mode,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-exa-search",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Exa-powered web search for DeepSeek Harness: keyless free tier, your own API key, or a signed-in Exa account, with a settings page that shows which Exa tools each mode exposes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
"node": ">=18"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
|
-
"test": "node --test"
|
|
19
|
-
"repair-credentials": "node scripts/repair-credentials.mjs"
|
|
18
|
+
"test": "node --test"
|
|
20
19
|
},
|
|
21
20
|
"exports": {
|
|
22
21
|
".": "./index.js",
|
|
@@ -33,8 +32,7 @@
|
|
|
33
32
|
"README.md",
|
|
34
33
|
"README.en.md",
|
|
35
34
|
"LICENSE",
|
|
36
|
-
"CHANGELOG.md"
|
|
37
|
-
"scripts/repair-credentials.mjs"
|
|
35
|
+
"CHANGELOG.md"
|
|
38
36
|
],
|
|
39
37
|
"dsh": {
|
|
40
38
|
"bundle": {
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Repair credential record keys written by pre-release local builds of
|
|
4
|
-
* dsh-exa-search (the old keys were `exa-api-key` / `exa-oauth`).
|
|
5
|
-
*
|
|
6
|
-
* Older builds stored `exa-api-key` / `exa-oauth`. The Harness credential
|
|
7
|
-
* service only accepts `<scope>/<id>`, and a single bad key makes the whole
|
|
8
|
-
* vault fail to parse at startup — the app then refuses to launch.
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* node scripts/repair-credentials.mjs --dry-run
|
|
12
|
-
* node scripts/repair-credentials.mjs
|
|
13
|
-
*
|
|
14
|
-
* The file is backed up next to the original before anything is written.
|
|
15
|
-
* Override the location with DSH_CREDENTIALS, or the home directory with
|
|
16
|
-
* DSH_HOME.
|
|
17
|
-
*/
|
|
18
|
-
import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
19
|
-
import os from 'node:os'
|
|
20
|
-
import path from 'node:path'
|
|
21
|
-
import { pathToFileURL } from 'node:url'
|
|
22
|
-
|
|
23
|
-
const RENAMES = [
|
|
24
|
-
['exa-api-key', 'exa-search/api-key'],
|
|
25
|
-
['exa-oauth', 'exa-search/oauth'],
|
|
26
|
-
]
|
|
27
|
-
|
|
28
|
-
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Compute the repaired document without touching the filesystem.
|
|
32
|
-
* Returns `{ text, changes, conflicts }`; a conflict means both the old and the
|
|
33
|
-
* scoped key exist, which needs a human decision.
|
|
34
|
-
*/
|
|
35
|
-
export function planRepair(text) {
|
|
36
|
-
const source = String(text)
|
|
37
|
-
const lines = source.split(/\r?\n/)
|
|
38
|
-
const present = new Set()
|
|
39
|
-
for (const line of lines) {
|
|
40
|
-
const match = line.match(/^\s{2}([^\s:#][^:]*):\s*$/)
|
|
41
|
-
if (match) present.add(match[1].trim())
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const conflicts = []
|
|
45
|
-
for (const [from, to] of RENAMES) {
|
|
46
|
-
if (present.has(from) && present.has(to)) conflicts.push({ from, to })
|
|
47
|
-
}
|
|
48
|
-
if (conflicts.length) return { text: source, changes: [], conflicts }
|
|
49
|
-
|
|
50
|
-
const changes = []
|
|
51
|
-
for (const [from, to] of RENAMES) {
|
|
52
|
-
if (present.has(from)) changes.push({ from, to })
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
let next = source
|
|
56
|
-
for (const { from, to } of changes) {
|
|
57
|
-
next = next.replace(new RegExp(`^(\\s{2})${escapeRegExp(from)}:\\s*$`, 'm'), `$1${to}:`)
|
|
58
|
-
}
|
|
59
|
-
return { text: next, changes, conflicts }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function main() {
|
|
63
|
-
const dryRun = process.argv.includes('--dry-run')
|
|
64
|
-
const home = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
|
|
65
|
-
const file = process.env.DSH_CREDENTIALS || path.join(home, '.credentials.yaml')
|
|
66
|
-
|
|
67
|
-
if (!existsSync(file)) {
|
|
68
|
-
console.error(`No credentials file at ${file}`)
|
|
69
|
-
process.exit(1)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const original = readFileSync(file, 'utf8')
|
|
73
|
-
const plan = planRepair(original)
|
|
74
|
-
|
|
75
|
-
if (plan.conflicts.length) {
|
|
76
|
-
for (const { from, to } of plan.conflicts) {
|
|
77
|
-
console.error(`Both "${from}" and "${to}" exist; resolve this by hand first.`)
|
|
78
|
-
}
|
|
79
|
-
process.exit(2)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!plan.changes.length) {
|
|
83
|
-
console.log('Nothing to repair: no legacy credential keys found.')
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
for (const { from, to } of plan.changes) {
|
|
88
|
-
console.log(`${dryRun ? 'would rename' : 'rename'} ${from} -> ${to}`)
|
|
89
|
-
}
|
|
90
|
-
if (dryRun) return
|
|
91
|
-
|
|
92
|
-
const backup = `${file}.bak-${Date.now()}`
|
|
93
|
-
copyFileSync(file, backup)
|
|
94
|
-
writeFileSync(file, plan.text, { mode: 0o600 })
|
|
95
|
-
console.log(`Backup written to ${backup}`)
|
|
96
|
-
console.log('Done. Start DeepSeek Harness again.')
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
100
|
-
main()
|
|
101
|
-
}
|