dsh-web-search-plugin 0.1.2 → 0.2.0
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 +13 -0
- package/README.md +25 -15
- package/cordis.patch.yml +8 -2
- package/lib/client.js +137 -69
- package/lib/deepseek.js +148 -0
- package/lib/index.js +39 -14
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
本项目的重要变更都记录在此。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [0.2.0] - 2026-08-19
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
|
|
9
|
+
- **DeepSeek(官方)后端**(`lib/deepseek.js`)。设置项 `provider: deepseek-official` 走 DeepSeek 的 Anthropic 兼容 Messages API(原生 `web_search_20250305` 工具,凭据名 `DEEPSEEK_API_KEY`)。bundle 层顺带禁用内置 `web-search-deepseek` Host 插件,从而移除旧的「插件 → 网页搜索」卡片、避免重复注册 provider。
|
|
10
|
+
- **顶层「设置 → 网页搜索」分区**。配置入口从「设置 → 插件 → 插件配置」里的 keyed 卡片改为 `settings.section`(id `web-search`)——与「插件」分区同级,一张卡内切换 DeepSeek / Tavily / Brave,并各自展开对应配置项(模型、API 版本、max tokens / max uses 等)。
|
|
11
|
+
|
|
12
|
+
### 变更
|
|
13
|
+
|
|
14
|
+
- `provider` 取值扩展为 `deepseek-official` / `tavily`(默认)/ `brave`;接缝 id 仍为 `dsh-web-search`。
|
|
15
|
+
- 新增 `lib/deepseek.js`;`package.json` 版本号 bump 到 `0.2.0`,`check` 脚本纳入新文件。
|
|
16
|
+
|
|
5
17
|
## [0.1.2] - 2026-08-18
|
|
6
18
|
|
|
7
19
|
### 新增
|
|
@@ -44,6 +56,7 @@
|
|
|
44
56
|
- Keyless 模式(`X-Tavily-Access-Mode: keyless`)与 keyed 模式(通过 `TAVILY_API_KEY` 凭据引用发送 Bearer token)。
|
|
45
57
|
- DSH 插件配置页上的浏览器设置卡片。
|
|
46
58
|
|
|
59
|
+
[0.2.0]: https://github.com/X-C1811/dsh-web-search-plugin/compare/v0.1.2...v0.2.0
|
|
47
60
|
[0.1.2]: https://github.com/X-C1811/dsh-web-search-plugin/compare/v0.1.1...v0.1.2
|
|
48
61
|
[0.1.1]: https://github.com/X-C1811/dsh-web-search-plugin/compare/v0.1.0...v0.1.1
|
|
49
62
|
[0.1.0]: https://github.com/X-C1811/dsh-web-search-plugin/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# dsh-web-search-plugin
|
|
2
2
|
|
|
3
|
-
面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) web 能力接缝(`ctx.web
|
|
3
|
+
面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) web 能力接缝(`ctx.web`)的统一网页搜索插件,内置 **DeepSeek(官方)/ Tavily / [Brave Search](https://brave.com/search/api/) 三个后端**。本包只向接缝注册 **一个** `WebSearchProvider`,稳定 id 为 `dsh-web-search`。在设置卡里切换引擎即可,不必再改 `web.searchProvider`。
|
|
4
4
|
|
|
5
|
+
- **DeepSeek(官方)** — 走 DeepSeek 的 Anthropic 兼容 Messages API(原生 `web_search_20250305` 工具,凭据名 `DEEPSEEK_API_KEY`),一次搜索消耗一个模型轮次。
|
|
5
6
|
- **Tavily(默认)** — `keyless`(免费、限流、无需账号)或 `keyed`(`TAVILY_API_KEY`,Bearer token)。
|
|
6
7
|
- **Brave Search** — `GET https://api.search.brave.com/res/v1/web/search`,请求头 `X-Subscription-Token`(凭据名 `BRAVE_API_KEY`)。一次搜索就是一次 HTTP 请求,不走模型轮次。
|
|
7
|
-
- **不写自定义 session 事件** —
|
|
8
|
+
- **不写自定义 session 事件** — 工具结果已经走接缝自己的事件,无需多余信封。
|
|
8
9
|
|
|
9
10
|
## 特性
|
|
10
11
|
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
|
|
22
23
|
## 安装
|
|
23
24
|
|
|
24
|
-
本包是 **bundle**:`dsh.bundle.patch` + `cordis.patch.yml` 会插入 Host
|
|
25
|
+
本包是 **bundle**:`dsh.bundle.patch` + `cordis.patch.yml` 会插入 Host 行、把 `web.searchProvider` 设为 `dsh-web-search`,并**禁用内置 `web-search-deepseek` Host 插件**(本插件已自行托管 DeepSeek 后端,禁用它能移除旧的「插件 → 网页搜索」卡片、避免重复注册 provider)。没有这一声明时,`dsh plugin add` 只写入依赖,插件不会挂载。
|
|
25
26
|
|
|
26
27
|
### 从 npm 安装
|
|
27
28
|
|
|
@@ -52,7 +53,7 @@ dsh plugin --profile web add "path/to/dsh-web-search-plugin"
|
|
|
52
53
|
dsh plugin --profile web remove @dsh-ltctfer/dsh-web-search-brave
|
|
53
54
|
```
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
然后在 **设置 → 网页搜索** 里把引擎切到想要的提供方,即可继续使用对应的 API key。
|
|
56
57
|
|
|
57
58
|
> **关于 `file:`(本地)安装** — pnpm 会把 `file:path` 依赖做成快照。改完本仓库后,需要再执行一次 `dsh plugin --profile web add "path/to/dsh-web-search-plugin"` 并重启。
|
|
58
59
|
|
|
@@ -62,15 +63,22 @@ dsh plugin --profile web remove @dsh-ltctfer/dsh-web-search-brave
|
|
|
62
63
|
|
|
63
64
|
| 键 | 默认值 | 含义 |
|
|
64
65
|
|---|---|---|
|
|
65
|
-
| `provider` | `tavily` | `tavily` 或 `
|
|
66
|
+
| `provider` | `tavily` | `tavily`、`brave` 或 `deepseek-official` |
|
|
66
67
|
| `mode` | `keyless` | 仅 Tavily:`keyless` 或 `keyed` |
|
|
67
68
|
| `apiKey` | — | Tavily API key 字面量(走凭据域,不会回显) |
|
|
68
69
|
| `apiKeyEnv` | `TAVILY_API_KEY` | keyed Tavily 使用的凭据/环境变量名 |
|
|
69
70
|
| `baseURL` | `https://api.tavily.com` | Tavily REST 基址;会再拼 `/search` |
|
|
70
|
-
| `maxResults` | `8` | 每次搜索返回的源数量(1–20
|
|
71
|
+
| `maxResults` | `8` | 每次搜索返回的源数量(1–20),三个后端共用 |
|
|
71
72
|
| `searchDepth` | `basic` | 仅 Tavily:`basic` 或 `advanced` |
|
|
72
73
|
| `includeAnswer` | `true` | 仅 Tavily:请求生成摘要,写入结果 `content` |
|
|
73
74
|
| `topic` | `general` | 仅 Tavily:`general` 或 `news` |
|
|
75
|
+
| `deepseekApiKey` | — | DeepSeek API key 字面量(走凭据域) |
|
|
76
|
+
| `deepseekApiKeyEnv` | `DEEPSEEK_API_KEY` | DeepSeek 使用的凭据/环境变量名 |
|
|
77
|
+
| `deepseekBaseURL` | `https://api.deepseek.com/anthropic/v1` | DeepSeek Anthropic 兼容 Messages 基址;再拼 `/messages` |
|
|
78
|
+
| `model` | `deepseek-v4-flash` | Anthropic 格式模型名 |
|
|
79
|
+
| `apiVersion` | `2023-06-01` | `anthropic-version` 请求头 |
|
|
80
|
+
| `maxTokens` | `4096` | Messages 请求生成 token 上限 |
|
|
81
|
+
| `maxUses` | `5` | 每次请求 `web_search` 工具的最大调用次数 |
|
|
74
82
|
| `braveApiKey` | — | Brave 订阅 token 字面量(走凭据域) |
|
|
75
83
|
| `braveApiKeyEnv` | `BRAVE_API_KEY` | Brave 使用的凭据/环境变量名 |
|
|
76
84
|
| `braveBaseURL` | `https://api.search.brave.com/res/v1/web/search` | Brave 网页搜索接口 |
|
|
@@ -81,12 +89,13 @@ dsh plugin --profile web remove @dsh-ltctfer/dsh-web-search-brave
|
|
|
81
89
|
|
|
82
90
|
环境变量:启动时 `DSH_WEB_SEARCH_PROVIDER=dsh-web-search` 会选中本接缝 id。未设置 `baseURL` 时,Tavily 基址回退 `$TAVILY_BASE_URL`。
|
|
83
91
|
|
|
84
|
-
##
|
|
92
|
+
## 切回内置 DeepSeek
|
|
85
93
|
|
|
86
|
-
|
|
94
|
+
DeepSeek 已并入本插件(`provider: deepseek-official`),无需切回。若确要恢复 DSH 内置的 DeepSeek host 插件,请在 profile patch 里去掉对 `web-search-deepseek` 的 `disabled` 并把 `searchProvider` 设回 `deepseek-official`;本插件可以继续挂着,只是不会被选中。
|
|
87
95
|
|
|
88
96
|
## 工作方式
|
|
89
97
|
|
|
98
|
+
- **DeepSeek** — `POST {deepseekBaseURL}/messages`,请求头 `x-api-key` / `authorization: Bearer`,工具 `web_search_20250305`。
|
|
90
99
|
- **Tavily** — `POST {baseURL}/search`。keyless 发送 `x-tavily-access-mode: keyless`;keyed 发送 `authorization: Bearer <key>`。
|
|
91
100
|
- **Brave** — `GET {braveBaseURL}?q=&count=`,请求头 `x-subscription-token`。不向 session 追加自定义事件。
|
|
92
101
|
- 非 2xx 映射为 `WEB_PROVIDER_ERROR`;调用方取消映射为 `WEB_ABORTED`。
|
|
@@ -95,11 +104,12 @@ dsh plugin --profile web remove @dsh-ltctfer/dsh-web-search-brave
|
|
|
95
104
|
|
|
96
105
|
```
|
|
97
106
|
lib/index.js Host 插件:Config、分发提供方、设置段
|
|
107
|
+
lib/deepseek.js DeepSeek 后端(Anthropic Messages + web_search_20250305)
|
|
98
108
|
lib/tavily.js Tavily 后端
|
|
99
109
|
lib/brave.js Brave 后端(不写自定义 session 事件)
|
|
100
110
|
lib/shared.js 中止 / 凭据解析辅助
|
|
101
|
-
lib/client.js 浏览器 bundle
|
|
102
|
-
cordis.patch.yml Bundle patch:插入 Host
|
|
111
|
+
lib/client.js 浏览器 bundle:顶层「网页搜索」分区 + 引擎切换表单
|
|
112
|
+
cordis.patch.yml Bundle patch:插入 Host 行、设 searchProvider、禁用内置 deepseek
|
|
103
113
|
```
|
|
104
114
|
|
|
105
115
|
## 开发
|
|
@@ -108,19 +118,19 @@ cordis.patch.yml Bundle patch:插入 Host 行,并把 searchProvider 设为
|
|
|
108
118
|
npm run check
|
|
109
119
|
```
|
|
110
120
|
|
|
111
|
-
客户端 bundle 必须保持 `window.__ModuleLoader__.load({ id, factory })` 线格式 — 由 `dsh-client-modules` 加载,不是打包器。必须同时导出 `apply` 和 `inject`(`slots`、`locale`、`connection`、`remote`、`settingsScope
|
|
121
|
+
客户端 bundle 必须保持 `window.__ModuleLoader__.load({ id, factory })` 线格式 — 由 `dsh-client-modules` 加载,不是打包器。必须同时导出 `apply` 和 `inject`(`slots`、`locale`、`connection`、`remote`、`settingsScope`),并把分区注册进 `settings.section`(`id: "web-search"`)。
|
|
112
122
|
|
|
113
123
|
## 发布
|
|
114
124
|
|
|
115
|
-
发布由 GitHub Actions(`.github/workflows/publish.yml`)自动完成:**推送 `v*` 标签**(例如 `v0.
|
|
125
|
+
发布由 GitHub Actions(`.github/workflows/publish.yml`)自动完成:**推送 `v*` 标签**(例如 `v0.2.0`)会带 provenance 发到 npm。向 `main` 的普通推送不会发布。
|
|
116
126
|
|
|
117
|
-
1. 确认 `package.json` 的 `version` 与即将推送的标签一致(例如 `0.
|
|
127
|
+
1. 确认 `package.json` 的 `version` 与即将推送的标签一致(例如 `0.2.0` → `v0.2.0`)。
|
|
118
128
|
2. 在 GitHub **Settings → Secrets and variables → Actions** 里配置有 publish 权限的 npm automation token(仓库密钥 `NPM_TOKEN`),并允许 Actions 运行。
|
|
119
129
|
3. 打标签并推送:
|
|
120
130
|
|
|
121
131
|
```powershell
|
|
122
|
-
git tag v0.
|
|
123
|
-
git push origin v0.
|
|
132
|
+
git tag v0.2.0
|
|
133
|
+
git push origin v0.2.0
|
|
124
134
|
```
|
|
125
135
|
|
|
126
136
|
工作流会先核对标签与 `package.json` 的 `version`,再执行 `npm publish --provenance --access public`。
|
package/cordis.patch.yml
CHANGED
|
@@ -5,13 +5,19 @@
|
|
|
5
5
|
#
|
|
6
6
|
# `searchProvider` is restated in full because a Cordis patch replaces the
|
|
7
7
|
# targeted row's whole `config`. The profile's own cordis.patch.yml still
|
|
8
|
-
# wins if it later overrides `web`. The seam id is `dsh-web-search`;
|
|
9
|
-
#
|
|
8
|
+
# wins if it later overrides `web`. The seam id is `dsh-web-search`; the
|
|
9
|
+
# backend (DeepSeek / Tavily / Brave) is the plugin's own `provider` setting.
|
|
10
10
|
- id: web
|
|
11
11
|
name: '@deepseek-ai/dsh-web'
|
|
12
12
|
config:
|
|
13
13
|
searchProvider: dsh-web-search
|
|
14
14
|
|
|
15
|
+
# This plugin re-hosts the DeepSeek backend, so the in-box DeepSeek host
|
|
16
|
+
# plugin is redundant: disabling it removes its `deepseek-official` provider
|
|
17
|
+
# (no duplicate) and hides the legacy "Plugins → web search" card.
|
|
18
|
+
- id: web-search-deepseek
|
|
19
|
+
disabled: true
|
|
20
|
+
|
|
15
21
|
- insert:
|
|
16
22
|
- id: dsh-web-search-plugin
|
|
17
23
|
name: dsh-web-search-plugin
|
package/lib/client.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser half of `dsh-web-search-plugin`: a
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Browser half of `dsh-web-search-plugin`: a top-level "Web search" settings
|
|
3
|
+
* section (settings → Web search) that edits the `dsh-web-search-plugin`
|
|
4
|
+
* settings namespace. The card switches the DeepSeek / Tavily / Brave backend
|
|
5
|
+
* and writes each engine's credential through the credentials domain
|
|
6
|
+
* (`DEEPSEEK_API_KEY` / `TAVILY_API_KEY` / `BRAVE_API_KEY` by default).
|
|
7
7
|
*
|
|
8
8
|
* This bundle is a self-contained hand-written module in the exact wire format
|
|
9
9
|
* the client module system expects: one `window.__ModuleLoader__.load` call
|
|
@@ -29,11 +29,15 @@ window.__ModuleLoader__.load({
|
|
|
29
29
|
const DEFAULT_TAVILY_KEY_REF = "TAVILY_API_KEY";
|
|
30
30
|
/** Credential reference Brave resolves when the section names none. */
|
|
31
31
|
const DEFAULT_BRAVE_KEY_REF = "BRAVE_API_KEY";
|
|
32
|
+
/** Credential reference DeepSeek resolves when the section names none. */
|
|
33
|
+
const DEFAULT_DEEPSEEK_KEY_REF = "DEEPSEEK_API_KEY";
|
|
32
34
|
/** Form field the Tavily write-only credential control stages under. */
|
|
33
35
|
const TAVILY_API_KEY_FIELD = "apiKey";
|
|
34
36
|
/** Form field the Brave write-only credential control stages under. */
|
|
35
37
|
const BRAVE_API_KEY_FIELD = "braveApiKey";
|
|
36
|
-
|
|
38
|
+
/** Form field the DeepSeek write-only credential control stages under. */
|
|
39
|
+
const DEEPSEEK_API_KEY_FIELD = "deepseekApiKey";
|
|
40
|
+
const SECRET_FIELDS = new Set([TAVILY_API_KEY_FIELD, BRAVE_API_KEY_FIELD, DEEPSEEK_API_KEY_FIELD]);
|
|
37
41
|
|
|
38
42
|
/** Locale bundles for the card. */
|
|
39
43
|
const en = {
|
|
@@ -42,6 +46,7 @@ window.__ModuleLoader__.load({
|
|
|
42
46
|
provider: "Search engine",
|
|
43
47
|
providerTavily: "Tavily",
|
|
44
48
|
providerBrave: "Brave Search",
|
|
49
|
+
providerDeepseek: "DeepSeek (official)",
|
|
45
50
|
mode: "Auth mode",
|
|
46
51
|
modeKeyless: "Keyless (free, rate-limited)",
|
|
47
52
|
modeKeyed: "API key",
|
|
@@ -79,6 +84,18 @@ window.__ModuleLoader__.load({
|
|
|
79
84
|
freshnessYear: "Past year",
|
|
80
85
|
proxy: "Proxy",
|
|
81
86
|
proxyHint: "HTTP(S) proxy URL. Leave blank to use HTTPS_PROXY / HTTP_PROXY.",
|
|
87
|
+
deepseekApiKeyEnv: "Credential reference",
|
|
88
|
+
deepseekApiKeyEnvHint: "Name of the credential/env var resolved for DeepSeek search.",
|
|
89
|
+
deepseekBaseURL: "Endpoint",
|
|
90
|
+
deepseekBaseURLHint: "Leave blank to use the DeepSeek Anthropic-compatible API default.",
|
|
91
|
+
model: "Model",
|
|
92
|
+
modelHint: "Anthropic-format model name for the search request.",
|
|
93
|
+
apiVersion: "API version",
|
|
94
|
+
apiVersionHint: "anthropic-version header value.",
|
|
95
|
+
maxTokens: "Max tokens",
|
|
96
|
+
maxTokensHint: "Upper bound on generated tokens for the search request.",
|
|
97
|
+
maxUses: "Max uses",
|
|
98
|
+
maxUsesHint: "Maximum web_search server-tool uses per request.",
|
|
82
99
|
expand: "Show settings",
|
|
83
100
|
collapse: "Hide settings",
|
|
84
101
|
save: "Save",
|
|
@@ -96,6 +113,7 @@ window.__ModuleLoader__.load({
|
|
|
96
113
|
provider: "搜索引擎",
|
|
97
114
|
providerTavily: "Tavily",
|
|
98
115
|
providerBrave: "Brave Search",
|
|
116
|
+
providerDeepseek: "DeepSeek(官方)",
|
|
99
117
|
mode: "认证模式",
|
|
100
118
|
modeKeyless: "Keyless(免费、限流)",
|
|
101
119
|
modeKeyed: "API key",
|
|
@@ -133,6 +151,18 @@ window.__ModuleLoader__.load({
|
|
|
133
151
|
freshnessYear: "过去一年",
|
|
134
152
|
proxy: "代理",
|
|
135
153
|
proxyHint: "HTTP(S) 代理 URL。留空则使用 HTTPS_PROXY / HTTP_PROXY。",
|
|
154
|
+
deepseekApiKeyEnv: "凭据引用名",
|
|
155
|
+
deepseekApiKeyEnvHint: "DeepSeek 搜索解析的凭据/环境变量名。",
|
|
156
|
+
deepseekBaseURL: "接口地址",
|
|
157
|
+
deepseekBaseURLHint: "留空则使用 DeepSeek Anthropic 兼容 API 默认地址。",
|
|
158
|
+
model: "模型",
|
|
159
|
+
modelHint: "搜索请求使用的 Anthropic 格式模型名。",
|
|
160
|
+
apiVersion: "API 版本",
|
|
161
|
+
apiVersionHint: "anthropic-version 请求头值。",
|
|
162
|
+
maxTokens: "最大 token",
|
|
163
|
+
maxTokensHint: "搜索请求生成 token 的上限。",
|
|
164
|
+
maxUses: "最大调用次数",
|
|
165
|
+
maxUsesHint: "每次请求 web_search 服务端工具的最大调用次数。",
|
|
136
166
|
expand: "展开设置",
|
|
137
167
|
collapse: "收起设置",
|
|
138
168
|
save: "保存",
|
|
@@ -208,7 +238,13 @@ window.__ModuleLoader__.load({
|
|
|
208
238
|
textField("country"),
|
|
209
239
|
textField("searchLang"),
|
|
210
240
|
choiceField("freshness"),
|
|
211
|
-
textField("proxy")
|
|
241
|
+
textField("proxy"),
|
|
242
|
+
textField("deepseekApiKeyEnv"),
|
|
243
|
+
textField("deepseekBaseURL"),
|
|
244
|
+
textField("model"),
|
|
245
|
+
textField("apiVersion"),
|
|
246
|
+
numberField("maxTokens"),
|
|
247
|
+
numberField("maxUses")
|
|
212
248
|
].map((spec) => [spec.field, spec]));
|
|
213
249
|
this.staged = new Map();
|
|
214
250
|
this.saving = false;
|
|
@@ -351,7 +387,7 @@ window.__ModuleLoader__.load({
|
|
|
351
387
|
const staged = this.staged.get("provider");
|
|
352
388
|
if (staged !== void 0 && !staged.clear && staged.text.trim() !== "") return staged.text.trim();
|
|
353
389
|
const declared = this.sectionValue("provider");
|
|
354
|
-
return declared === "brave"
|
|
390
|
+
return declared === "brave" || declared === "deepseek-official" ? declared : "tavily";
|
|
355
391
|
}
|
|
356
392
|
tavilyRef() {
|
|
357
393
|
const declared = this.sectionValue("apiKeyEnv");
|
|
@@ -361,9 +397,14 @@ window.__ModuleLoader__.load({
|
|
|
361
397
|
const declared = this.sectionValue("braveApiKeyEnv");
|
|
362
398
|
return declared !== void 0 && declared.length > 0 ? declared : DEFAULT_BRAVE_KEY_REF;
|
|
363
399
|
}
|
|
400
|
+
deepseekRef() {
|
|
401
|
+
const declared = this.sectionValue("deepseekApiKeyEnv");
|
|
402
|
+
return declared !== void 0 && declared.length > 0 ? declared : DEFAULT_DEEPSEEK_KEY_REF;
|
|
403
|
+
}
|
|
364
404
|
/** The credential reference the currently visible engine uses. */
|
|
365
405
|
refOf() {
|
|
366
|
-
|
|
406
|
+
const provider = this.providerOf();
|
|
407
|
+
return provider === "brave" ? this.braveRef() : provider === "deepseek-official" ? this.deepseekRef() : this.tavilyRef();
|
|
367
408
|
}
|
|
368
409
|
async readCredential() {
|
|
369
410
|
const ref = this.refOf();
|
|
@@ -393,7 +434,7 @@ window.__ModuleLoader__.load({
|
|
|
393
434
|
this.readCredential();
|
|
394
435
|
}
|
|
395
436
|
async writeKey(field, value) {
|
|
396
|
-
const ref = field === BRAVE_API_KEY_FIELD ? this.braveRef() : this.tavilyRef();
|
|
437
|
+
const ref = field === BRAVE_API_KEY_FIELD ? this.braveRef() : field === DEEPSEEK_API_KEY_FIELD ? this.deepseekRef() : this.tavilyRef();
|
|
397
438
|
try {
|
|
398
439
|
await this.api.credentials.set({ ref, value });
|
|
399
440
|
} catch (_credentialWriteFailure) {}
|
|
@@ -432,6 +473,13 @@ window.__ModuleLoader__.load({
|
|
|
432
473
|
searchLang: this.form.field("searchLang"),
|
|
433
474
|
freshness: this.form.field("freshness"),
|
|
434
475
|
proxy: this.form.field("proxy"),
|
|
476
|
+
deepseekApiKey: this.form.field(DEEPSEEK_API_KEY_FIELD),
|
|
477
|
+
deepseekApiKeyEnv: this.form.field("deepseekApiKeyEnv"),
|
|
478
|
+
deepseekBaseURL: this.form.field("deepseekBaseURL"),
|
|
479
|
+
model: this.form.field("model"),
|
|
480
|
+
apiVersion: this.form.field("apiVersion"),
|
|
481
|
+
maxTokens: this.form.field("maxTokens"),
|
|
482
|
+
maxUses: this.form.field("maxUses"),
|
|
435
483
|
apiKeyConfigured: this.form.credential.configured,
|
|
436
484
|
apiKeyWritable: this.form.credential.writable
|
|
437
485
|
};
|
|
@@ -454,63 +502,84 @@ window.__ModuleLoader__.load({
|
|
|
454
502
|
const selectInput = (props) => react.createElement("select", { style: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", height: "34px", font: "inherit", color: "var(--dsw-alias-label-primary)", borderRadius: "8px", padding: "0 12px", fontSize: "13px", lineHeight: "1.5" }, ...props });
|
|
455
503
|
const checkboxInput = (props) => react.createElement("input", { type: "checkbox", style: { width: "16px", height: "16px", accentColor: "var(--dsw-alias-brand-primary)" }, ...props });
|
|
456
504
|
|
|
457
|
-
/** Render
|
|
458
|
-
function
|
|
459
|
-
const { t } = props;
|
|
460
|
-
const state = props.usePluginSearchCard((snapshot) => snapshot);
|
|
461
|
-
const [open, setOpen] = react.useState(false);
|
|
505
|
+
/** Render the provider selector plus the backend-specific staged form. */
|
|
506
|
+
function WebSearchCard(props) {
|
|
507
|
+
const { t, state, actions } = props;
|
|
462
508
|
if (!state.available) return null;
|
|
463
509
|
const disabled = !state.writable;
|
|
464
510
|
const blocked = !state.dirty || state.invalid || state.saving;
|
|
465
|
-
const
|
|
511
|
+
const provider = state.provider.text === "brave" ? "brave" : state.provider.text === "deepseek-official" ? "deepseek-official" : "tavily";
|
|
512
|
+
const brave = provider === "brave";
|
|
513
|
+
const deepseek = provider === "deepseek-official";
|
|
466
514
|
const keyed = state.mode.text === "keyed";
|
|
467
|
-
return react.createElement("
|
|
468
|
-
react.createElement("
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
515
|
+
return react.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "4px" } },
|
|
516
|
+
!state.writable ? react.createElement("p", { style: { color: "var(--dsw-alias-label-tertiary)", margin: "0", fontSize: "12px", lineHeight: "1.5" } }, t("readOnly")) : null,
|
|
517
|
+
fieldRow(t("provider"), null, selectInput({ id: "ws-provider", value: provider, disabled, onChange: (e) => actions.edit("provider", e.target.value), children: [
|
|
518
|
+
react.createElement("option", { value: "tavily", key: "tavily" }, t("providerTavily")),
|
|
519
|
+
react.createElement("option", { value: "brave", key: "brave" }, t("providerBrave")),
|
|
520
|
+
react.createElement("option", { value: "deepseek-official", key: "deepseek-official" }, t("providerDeepseek"))
|
|
521
|
+
] })),
|
|
522
|
+
deepseek ? null : brave ? null : fieldRow(t("mode"), null, selectInput({ id: "ws-mode", value: state.mode.text, disabled, onChange: (e) => actions.edit("mode", e.target.value), children: [
|
|
523
|
+
react.createElement("option", { value: "keyless", key: "keyless" }, t("modeKeyless")),
|
|
524
|
+
react.createElement("option", { value: "keyed", key: "keyed" }, t("modeKeyed"))
|
|
525
|
+
] })),
|
|
526
|
+
// API key + credential reference for the visible backend.
|
|
527
|
+
deepseek ? fieldRow(t("apiKey"), t("apiKeyHint"), secretInput({ id: "ws-deepseek-key", value: state.deepseekApiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(DEEPSEEK_API_KEY_FIELD, e.target.value) })) : null,
|
|
528
|
+
deepseek ? fieldRow(t("deepseekApiKeyEnv"), t("deepseekApiKeyEnvHint"), textInput({ id: "ws-deepseek-keyref", value: state.deepseekApiKeyEnv.text, disabled, onChange: (e) => actions.edit("deepseekApiKeyEnv", e.target.value), onReset: () => actions.resetField("deepseekApiKeyEnv") })) : null,
|
|
529
|
+
brave ? fieldRow(t("apiKey"), t("apiKeyHint"), secretInput({ id: "ws-brave-key", value: state.braveApiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(BRAVE_API_KEY_FIELD, e.target.value) })) : null,
|
|
530
|
+
brave ? fieldRow(t("braveApiKeyEnv"), t("braveApiKeyEnvHint"), textInput({ id: "ws-brave-keyref", value: state.braveApiKeyEnv.text, disabled, onChange: (e) => actions.edit("braveApiKeyEnv", e.target.value), onReset: () => actions.resetField("braveApiKeyEnv") })) : null,
|
|
531
|
+
(!brave && !deepseek && keyed) ? fieldRow(t("apiKey"), t("apiKeyHint"), secretInput({ id: "ws-tavily-key", value: state.apiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(TAVILY_API_KEY_FIELD, e.target.value) })) : null,
|
|
532
|
+
(!brave && !deepseek && keyed) ? fieldRow(t("apiKeyEnv"), t("apiKeyEnvHint"), textInput({ id: "ws-tavily-keyref", value: state.apiKeyEnv.text, disabled, onChange: (e) => actions.edit("apiKeyEnv", e.target.value), onReset: () => actions.resetField("apiKeyEnv") })) : null,
|
|
533
|
+
// Endpoint.
|
|
534
|
+
deepseek ? fieldRow(t("deepseekBaseURL"), t("deepseekBaseURLHint"), textInput({ id: "ws-deepseek-base", value: state.deepseekBaseURL.text, disabled, onChange: (e) => actions.edit("deepseekBaseURL", e.target.value), onReset: () => actions.resetField("deepseekBaseURL") })) : null,
|
|
535
|
+
brave ? fieldRow(t("braveBaseURL"), t("braveBaseURLHint"), textInput({ id: "ws-brave-base", value: state.braveBaseURL.text, disabled, onChange: (e) => actions.edit("braveBaseURL", e.target.value), onReset: () => actions.resetField("braveBaseURL") })) : null,
|
|
536
|
+
(!brave && !deepseek) ? fieldRow(t("baseURL"), t("baseURLHint"), textInput({ id: "ws-tavily-base", value: state.baseURL.text, disabled, onChange: (e) => actions.edit("baseURL", e.target.value), onReset: () => actions.resetField("baseURL") })) : null,
|
|
537
|
+
// DeepSeek options.
|
|
538
|
+
deepseek ? fieldRow(t("model"), t("modelHint"), textInput({ id: "ws-model", value: state.model.text, disabled, onChange: (e) => actions.edit("model", e.target.value), onReset: () => actions.resetField("model") })) : null,
|
|
539
|
+
deepseek ? fieldRow(t("apiVersion"), t("apiVersionHint"), textInput({ id: "ws-apiversion", value: state.apiVersion.text, disabled, onChange: (e) => actions.edit("apiVersion", e.target.value), onReset: () => actions.resetField("apiVersion") })) : null,
|
|
540
|
+
deepseek ? fieldRow(t("maxTokens"), t("maxTokensHint"), textInput({ id: "ws-maxtokens", numeric: true, value: state.maxTokens.text, disabled, onChange: (e) => actions.edit("maxTokens", e.target.value), onReset: () => actions.resetField("maxTokens") })) : null,
|
|
541
|
+
deepseek ? fieldRow(t("maxUses"), t("maxUsesHint"), textInput({ id: "ws-maxuses", numeric: true, value: state.maxUses.text, disabled, onChange: (e) => actions.edit("maxUses", e.target.value), onReset: () => actions.resetField("maxUses") })) : null,
|
|
542
|
+
// Tavily options.
|
|
543
|
+
(!brave && !deepseek) ? fieldRow(t("searchDepth"), null, selectInput({ id: "ws-depth", value: state.searchDepth.text, disabled, onChange: (e) => actions.edit("searchDepth", e.target.value), children: [
|
|
544
|
+
react.createElement("option", { value: "basic", key: "basic" }, t("searchDepthBasic")),
|
|
545
|
+
react.createElement("option", { value: "advanced", key: "advanced" }, t("searchDepthAdvanced"))
|
|
546
|
+
] })) : null,
|
|
547
|
+
(!brave && !deepseek) ? fieldRow(t("topic"), null, selectInput({ id: "ws-topic", value: state.topic.text, disabled, onChange: (e) => actions.edit("topic", e.target.value), children: [
|
|
548
|
+
react.createElement("option", { value: "general", key: "general" }, t("topicGeneral")),
|
|
549
|
+
react.createElement("option", { value: "news", key: "news" }, t("topicNews"))
|
|
550
|
+
] })) : null,
|
|
551
|
+
(!brave && !deepseek) ? fieldRow(t("includeAnswer"), t("includeAnswerHint"), checkboxInput({ id: "ws-answer", checked: state.includeAnswer.text === "true", disabled, onChange: (e) => actions.edit("includeAnswer", e.target.checked ? "true" : "") })) : null,
|
|
552
|
+
// Brave options.
|
|
553
|
+
brave ? fieldRow(t("country"), t("countryHint"), textInput({ id: "ws-country", value: state.country.text, disabled, onChange: (e) => actions.edit("country", e.target.value), onReset: () => actions.resetField("country") })) : null,
|
|
554
|
+
brave ? fieldRow(t("searchLang"), t("searchLangHint"), textInput({ id: "ws-lang", value: state.searchLang.text, disabled, onChange: (e) => actions.edit("searchLang", e.target.value), onReset: () => actions.resetField("searchLang") })) : null,
|
|
555
|
+
brave ? fieldRow(t("freshness"), null, selectInput({ id: "ws-freshness", value: state.freshness.text, disabled, onChange: (e) => actions.edit("freshness", e.target.value), children: [
|
|
556
|
+
react.createElement("option", { value: "", key: "any" }, t("freshnessAny")),
|
|
557
|
+
react.createElement("option", { value: "pd", key: "pd" }, t("freshnessDay")),
|
|
558
|
+
react.createElement("option", { value: "pw", key: "pw" }, t("freshnessWeek")),
|
|
559
|
+
react.createElement("option", { value: "pm", key: "pm" }, t("freshnessMonth")),
|
|
560
|
+
react.createElement("option", { value: "py", key: "py" }, t("freshnessYear"))
|
|
561
|
+
] })) : null,
|
|
562
|
+
brave ? fieldRow(t("proxy"), t("proxyHint"), textInput({ id: "ws-proxy", value: state.proxy.text, disabled, onChange: (e) => actions.edit("proxy", e.target.value), onReset: () => actions.resetField("proxy") })) : null,
|
|
563
|
+
fieldRow(t("maxResults"), t("maxResultsHint"), textInput({ id: "ws-max", numeric: true, value: state.maxResults.text, disabled, onChange: (e) => actions.edit("maxResults", e.target.value), onReset: () => actions.resetField("maxResults") })),
|
|
564
|
+
react.createElement("div", { style: { borderTop: "1px solid var(--dsw-alias-border-l2)", justifyContent: "flex-end", alignItems: "center", gap: "8px", padding: "12px 0 4px", display: "flex" } },
|
|
565
|
+
state.failed ? react.createElement("p", { role: "status", style: { minWidth: 0, color: "var(--dsw-alias-label-error)", flex: 1, margin: 0, fontSize: "12px", lineHeight: "1.5" } }, t("saveFailed")) : null,
|
|
566
|
+
react.createElement("button", { type: "button", disabled: !state.dirty || state.saving, onClick: actions.discard, style: { appearance: "none", font: "inherit", cursor: "pointer", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: "8px", padding: "5px 14px", fontSize: "13px", lineHeight: "1.5", color: "var(--dsw-alias-label-secondary)", background: "0 0" } }, t("discard")),
|
|
567
|
+
react.createElement("button", { type: "button", disabled: blocked, onClick: actions.save, style: { appearance: "none", font: "inherit", cursor: "pointer", border: "1px solid transparent", borderRadius: "8px", padding: "5px 14px", fontSize: "13px", lineHeight: "1.5", color: "var(--dsw-alias-label-on-brand)", background: "var(--dsw-alias-bg-brand-solid)" } }, t(state.saving ? "saving" : "save"))));
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** Section component: the "Web search" page body, driven by useSyncExternalStore. */
|
|
571
|
+
function makeSection(ctx, controller) {
|
|
572
|
+
return function WebSearchSection() {
|
|
573
|
+
const state = react.useSyncExternalStore(controller.store.subscribe, controller.store.getSnapshot);
|
|
574
|
+
const t = ctx.locale.bind(NS);
|
|
575
|
+
if (!state.available) return react.createElement("p", { style: { padding: "16px" } }, t("readOnly"));
|
|
576
|
+
return react.createElement("div", { style: { padding: "16px" } },
|
|
577
|
+
react.createElement("div", { style: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", borderRadius: "12px", padding: "4px 16px" } },
|
|
578
|
+
react.createElement(WebSearchCard, { t: t, state: state, actions: controller.form.actions() })));
|
|
579
|
+
};
|
|
511
580
|
}
|
|
512
581
|
|
|
513
|
-
/** Mount the
|
|
582
|
+
/** Mount the top-level "Web search" settings section. */
|
|
514
583
|
function apply(ctx) {
|
|
515
584
|
const { api } = ctx.get("connection");
|
|
516
585
|
const t = ctx.locale.bind(NS);
|
|
@@ -519,14 +588,13 @@ window.__ModuleLoader__.load({
|
|
|
519
588
|
ctx.effect(() => ctx.remote.$on("credentials/updated", (ref) => {
|
|
520
589
|
controller.form.refreshCredential(ref);
|
|
521
590
|
}), "dsh-web-search-plugin: credential invalidations");
|
|
522
|
-
ctx.slots.inject("settings.
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
});
|
|
591
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
592
|
+
name: "settings.section",
|
|
593
|
+
id: "web-search",
|
|
594
|
+
order: 10,
|
|
595
|
+
label: () => t("nav"),
|
|
596
|
+
locale: NS
|
|
597
|
+
}, makeSection(ctx, controller)));
|
|
530
598
|
}
|
|
531
599
|
|
|
532
600
|
const inject = ["slots", "locale", "connection", "remote", "settingsScope"];
|
package/lib/deepseek.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek search backend: the Anthropic-compatible Messages API with the
|
|
3
|
+
* native `web_search_20250305` server tool. Each search costs a model turn
|
|
4
|
+
* and returns structured `web_search_tool_result` blocks; absence of those
|
|
5
|
+
* blocks is an error rather than a prose-scraping fallback.
|
|
6
|
+
*
|
|
7
|
+
* This plugin re-hosts the `deepseek-official` provider so the DeepSeek
|
|
8
|
+
* configuration can sit beside Tavily and Brave. The bundle patch disables
|
|
9
|
+
* the in-box `web-search-deepseek` host plugin (its namespace feed the legacy
|
|
10
|
+
* "plugins → web search" card, and re-registering its provider would collide).
|
|
11
|
+
*
|
|
12
|
+
* @module dsh-web-search-plugin/deepseek
|
|
13
|
+
*/
|
|
14
|
+
import { WebError } from "@deepseek-ai/dsh-web";
|
|
15
|
+
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
16
|
+
import { MAX_RESULTS_CAP, USER_AGENT, isAbortError, resolveApiKey, resolveSecret, searchAborted, throwIfSearchAborted } from "./shared.js";
|
|
17
|
+
|
|
18
|
+
/** Default endpoint: DeepSeek's Anthropic-compatible API (`/messages` appended). */
|
|
19
|
+
export const DEEPSEEK_DEFAULT_BASE_URL = "https://api.deepseek.com/anthropic/v1";
|
|
20
|
+
/** Default credential reference for this backend. */
|
|
21
|
+
export const DEEPSEEK_DEFAULT_API_KEY_ENV = "DEEPSEEK_API_KEY";
|
|
22
|
+
/** Environment variable naming this backend's endpoint override. */
|
|
23
|
+
const DEEPSEEK_SEARCH_BASE_URL_ENV = "DEEPSEEK_SEARCH_BASE_URL";
|
|
24
|
+
const DEFAULT_MODEL = "deepseek-v4-flash";
|
|
25
|
+
const DEFAULT_API_VERSION = "2023-06-01";
|
|
26
|
+
const DEFAULT_MAX_TOKENS = 4096;
|
|
27
|
+
const DEFAULT_MAX_USES = 5;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build a `url → cited_text` map from `text` blocks' `citations[]` (the snippet
|
|
31
|
+
* source for Anthropic `web_search_result` items, which carry url/title but no
|
|
32
|
+
* inline excerpt).
|
|
33
|
+
*/
|
|
34
|
+
function citationSnippets(blocks) {
|
|
35
|
+
const map = /* @__PURE__ */ new Map();
|
|
36
|
+
for (const block of blocks) {
|
|
37
|
+
if (block.type !== "text") continue;
|
|
38
|
+
for (const cite of block.citations ?? []) if (cite.url != null && cite.url.length > 0 && cite.cited_text != null && cite.cited_text.length > 0 && !map.has(cite.url)) map.set(cite.url, cite.cited_text);
|
|
39
|
+
}
|
|
40
|
+
return map;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Map an Anthropic Messages response to a normalized search result. */
|
|
44
|
+
function mapAnthropicResponse(response) {
|
|
45
|
+
const blocks = response.content ?? [];
|
|
46
|
+
const resultBlocks = blocks.filter((block) => block.type === "web_search_tool_result");
|
|
47
|
+
if (resultBlocks.length === 0) throw new WebError("DeepSeek returned no web_search_tool_result blocks; the request may not have triggered native web search", "WEB_PROVIDER_ERROR");
|
|
48
|
+
const snippets = citationSnippets(blocks);
|
|
49
|
+
const seen = /* @__PURE__ */ new Set();
|
|
50
|
+
const sources = [];
|
|
51
|
+
for (const block of resultBlocks) for (const item of block.content ?? []) {
|
|
52
|
+
if (item.type !== "web_search_result" || typeof item.url !== "string" || item.url.length === 0 || seen.has(item.url)) continue;
|
|
53
|
+
seen.add(item.url);
|
|
54
|
+
const snippet = snippets.get(item.url);
|
|
55
|
+
sources.push({
|
|
56
|
+
url: item.url,
|
|
57
|
+
...typeof item.title === "string" && item.title.length > 0 ? { title: item.title } : {},
|
|
58
|
+
...snippet !== void 0 ? { snippet } : {},
|
|
59
|
+
...typeof item.page_age === "string" && item.page_age.length > 0 ? { publishedAt: item.page_age } : {}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return { sources, truncated: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The DeepSeek-backed search backend; HTTP redirects fail as `WEB_PROVIDER_ERROR`. */
|
|
66
|
+
export class DeepSeekSearchProvider {
|
|
67
|
+
constructor(resolveOptions) {
|
|
68
|
+
this.resolveOptions = resolveOptions;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
available() {
|
|
72
|
+
const options = this.resolveOptions();
|
|
73
|
+
return ((options.apiKey?.length ?? 0) > 0 || options.resolveApiKey !== void 0)
|
|
74
|
+
&& URL.canParse(options.baseURL)
|
|
75
|
+
&& Number.isInteger(options.maxTokens) && options.maxTokens > 0
|
|
76
|
+
&& Number.isInteger(options.maxUses) && options.maxUses > 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async search(request, signal) {
|
|
80
|
+
const options = this.resolveOptions();
|
|
81
|
+
const apiKey = await resolveApiKey(options, signal, `DeepSeek search has no API key for "${options.apiKeyEnv ?? DEEPSEEK_DEFAULT_API_KEY_ENV}"; store it through the credentials service, export it in the launching environment, or set a literal "deepseekApiKey" in the dsh-web-search-plugin config`);
|
|
82
|
+
throwIfSearchAborted(signal);
|
|
83
|
+
const endpoint = `${options.baseURL.replace(/\/+$/u, "")}/messages`;
|
|
84
|
+
const body = {
|
|
85
|
+
model: options.model,
|
|
86
|
+
max_tokens: options.maxTokens,
|
|
87
|
+
messages: [{
|
|
88
|
+
role: "user",
|
|
89
|
+
content: [{ type: "text", text: `Perform a web search for the query: ${request.query}` }]
|
|
90
|
+
}],
|
|
91
|
+
tools: [{ type: "web_search_20250305", name: "web_search", max_uses: options.maxUses }]
|
|
92
|
+
};
|
|
93
|
+
throwIfSearchAborted(signal);
|
|
94
|
+
let response;
|
|
95
|
+
try {
|
|
96
|
+
response = await fetch(endpoint, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
redirect: "error",
|
|
99
|
+
headers: {
|
|
100
|
+
"x-api-key": apiKey,
|
|
101
|
+
"authorization": `Bearer ${apiKey}`,
|
|
102
|
+
"anthropic-version": options.apiVersion,
|
|
103
|
+
"content-type": "application/json",
|
|
104
|
+
"accept": "application/json",
|
|
105
|
+
"user-agent": USER_AGENT
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify(body),
|
|
108
|
+
...signal !== void 0 ? { signal } : {}
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
112
|
+
throw new WebError(`DeepSeek search request failed: ${String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
|
|
113
|
+
}
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
let message = `DeepSeek API error (HTTP ${response.status})`;
|
|
116
|
+
try {
|
|
117
|
+
const parsed = await response.json();
|
|
118
|
+
const detail = typeof parsed.error === "string" ? parsed.error : parsed.error?.message ?? parsed.message;
|
|
119
|
+
if (typeof detail === "string" && detail.length > 0) message = detail;
|
|
120
|
+
} catch {
|
|
121
|
+
/* non-JSON error body — keep the status-line message */
|
|
122
|
+
}
|
|
123
|
+
throw new WebError(message, "WEB_PROVIDER_ERROR");
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
return mapAnthropicResponse(await response.json());
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
129
|
+
if (error instanceof WebError) throw error;
|
|
130
|
+
throw new WebError(`DeepSeek returned an unprocessable response body: ${String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Project the plugin section into DeepSeek backend options. */
|
|
136
|
+
export function resolveDeepseekOptions(ctx, config) {
|
|
137
|
+
return {
|
|
138
|
+
...resolveSecret(ctx, {
|
|
139
|
+
literal: config.deepseekApiKey,
|
|
140
|
+
envName: config.deepseekApiKeyEnv ?? DEEPSEEK_DEFAULT_API_KEY_ENV
|
|
141
|
+
}),
|
|
142
|
+
baseURL: config.deepseekBaseURL ?? launchEnvironmentOf(ctx).get(DEEPSEEK_SEARCH_BASE_URL_ENV)?.value ?? DEEPSEEK_DEFAULT_BASE_URL,
|
|
143
|
+
model: config.model ?? DEFAULT_MODEL,
|
|
144
|
+
apiVersion: config.apiVersion ?? DEFAULT_API_VERSION,
|
|
145
|
+
maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
146
|
+
maxUses: config.maxUses ?? DEFAULT_MAX_USES
|
|
147
|
+
};
|
|
148
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Web-search provider plugin for the DeepSeek Harness web capability seam
|
|
3
3
|
* (`ctx.web`). Registers a single `WebSearchProvider` under the stable id
|
|
4
|
-
* `dsh-web-search` and dispatches each search to
|
|
5
|
-
* to the `provider` setting — so switching backends does
|
|
6
|
-
* `web.searchProvider` patch change.
|
|
4
|
+
* `dsh-web-search` and dispatches each search to DeepSeek (official), Tavily,
|
|
5
|
+
* or Brave according to the `provider` setting — so switching backends does
|
|
6
|
+
* not require a `web.searchProvider` patch change.
|
|
7
7
|
*
|
|
8
|
-
* Tavily supports `keyless` (free, rate-limited) and `keyed` modes.
|
|
9
|
-
*
|
|
8
|
+
* Tavily supports `keyless` (free, rate-limited) and `keyed` modes. Brave
|
|
9
|
+
* always needs a subscription token (`BRAVE_API_KEY` by default). DeepSeek
|
|
10
|
+
* uses the Anthropic-compatible Messages API native `web_search_20250305`
|
|
11
|
+
* tool and reuses `DEEPSEEK_API_KEY` (the in-box `web-search-deepseek` host
|
|
12
|
+
* plugin is disabled by the bundle patch).
|
|
10
13
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* event vocabulary and cannot be marked `ignorable`, so a cold load refused
|
|
14
|
-
* the conversation. Tool results already go through the seam's own events.
|
|
14
|
+
* No backend writes custom session events; tool results already go through
|
|
15
|
+
* the seam's own events.
|
|
15
16
|
*
|
|
16
17
|
* @module dsh-web-search-plugin
|
|
17
18
|
*/
|
|
@@ -20,6 +21,7 @@ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-sett
|
|
|
20
21
|
import { MAX_RESULTS_CAP } from "./shared.js";
|
|
21
22
|
import { TAVILY_DEFAULT_API_KEY_ENV, TAVILY_DEFAULT_BASE_URL, TavilySearchProvider, resolveTavilyOptions } from "./tavily.js";
|
|
22
23
|
import { BRAVE_DEFAULT_API_KEY_ENV, BRAVE_DEFAULT_BASE_URL, BraveSearchProvider, resolveBraveOptions } from "./brave.js";
|
|
24
|
+
import { DEEPSEEK_DEFAULT_API_KEY_ENV, DEEPSEEK_DEFAULT_BASE_URL, DeepSeekSearchProvider, resolveDeepseekOptions } from "./deepseek.js";
|
|
23
25
|
|
|
24
26
|
/** Stable id this plugin registers under on `ctx.web`. */
|
|
25
27
|
const SEARCH_PROVIDER_ID = "dsh-web-search";
|
|
@@ -31,7 +33,7 @@ const inject = ["web"];
|
|
|
31
33
|
|
|
32
34
|
/** Plugin config (all optional — `apply` fills defaults). */
|
|
33
35
|
const Config = z.object({
|
|
34
|
-
/** Active backend: `tavily` (default) or `brave`. */
|
|
36
|
+
/** Active backend: `deepseek-official`, `tavily` (default), or `brave`. */
|
|
35
37
|
provider: z.string().default("tavily"),
|
|
36
38
|
/** Tavily auth mode: `keyless` (default) or `keyed`. */
|
|
37
39
|
mode: z.string().default("keyless"),
|
|
@@ -62,7 +64,21 @@ const Config = z.object({
|
|
|
62
64
|
/** Optional Brave `freshness` (`pd` / `pw` / `pm` / `py`). */
|
|
63
65
|
freshness: z.string().default(""),
|
|
64
66
|
/** Optional HTTP(S) proxy URL for Brave (falls back to HTTPS_PROXY / HTTP_PROXY). */
|
|
65
|
-
proxy: z.string().default("")
|
|
67
|
+
proxy: z.string().default(""),
|
|
68
|
+
/** Literal DeepSeek API key; prefer {@link deepseekApiKeyEnv}. */
|
|
69
|
+
deepseekApiKey: z.string().role("secret").default(""),
|
|
70
|
+
/** Credential reference for DeepSeek search. */
|
|
71
|
+
deepseekApiKeyEnv: z.string().role("credential-ref").default(DEEPSEEK_DEFAULT_API_KEY_ENV),
|
|
72
|
+
/** DeepSeek Anthropic-compatible Messages base URL. `/messages` is appended. */
|
|
73
|
+
deepseekBaseURL: z.string().default(DEEPSEEK_DEFAULT_BASE_URL),
|
|
74
|
+
/** Anthropic-format model name for the Messages request. */
|
|
75
|
+
model: z.string().default("deepseek-v4-flash"),
|
|
76
|
+
/** `anthropic-version` header value. */
|
|
77
|
+
apiVersion: z.string().default("2023-06-01"),
|
|
78
|
+
/** Upper bound on generated tokens for the Messages request. */
|
|
79
|
+
maxTokens: z.number().step(1).min(1).default(4096),
|
|
80
|
+
/** Maximum `web_search` server-tool uses per request. */
|
|
81
|
+
maxUses: z.number().step(1).min(1).default(5)
|
|
66
82
|
});
|
|
67
83
|
|
|
68
84
|
/** Settings namespace carrying this plugin's backend switch and options. */
|
|
@@ -80,6 +96,7 @@ class PluginSearchProvider {
|
|
|
80
96
|
|
|
81
97
|
constructor(resolveOptions) {
|
|
82
98
|
this.resolveOptions = resolveOptions;
|
|
99
|
+
this.deepseek = new DeepSeekSearchProvider(() => this.resolveOptions().deepseek);
|
|
83
100
|
this.tavily = new TavilySearchProvider(() => this.resolveOptions().tavily);
|
|
84
101
|
this.brave = new BraveSearchProvider(() => this.resolveOptions().brave);
|
|
85
102
|
}
|
|
@@ -93,14 +110,19 @@ class PluginSearchProvider {
|
|
|
93
110
|
}
|
|
94
111
|
|
|
95
112
|
backend() {
|
|
96
|
-
|
|
113
|
+
const provider = this.resolveOptions().provider;
|
|
114
|
+
if (provider === "brave") return this.brave;
|
|
115
|
+
if (provider === "deepseek-official") return this.deepseek;
|
|
116
|
+
return this.tavily;
|
|
97
117
|
}
|
|
98
118
|
}
|
|
99
119
|
|
|
100
|
-
/** Project one resolved section into options for
|
|
120
|
+
/** Project one resolved section into options for each backend. */
|
|
101
121
|
function resolveOptions(ctx, config) {
|
|
122
|
+
const provider = config.provider === "brave" || config.provider === "deepseek-official" ? config.provider : "tavily";
|
|
102
123
|
return {
|
|
103
|
-
provider
|
|
124
|
+
provider,
|
|
125
|
+
deepseek: resolveDeepseekOptions(ctx, config),
|
|
104
126
|
tavily: resolveTavilyOptions(ctx, config),
|
|
105
127
|
brave: resolveBraveOptions(ctx, config)
|
|
106
128
|
};
|
|
@@ -123,6 +145,9 @@ export {
|
|
|
123
145
|
BRAVE_DEFAULT_BASE_URL,
|
|
124
146
|
BraveSearchProvider,
|
|
125
147
|
Config,
|
|
148
|
+
DEEPSEEK_DEFAULT_API_KEY_ENV,
|
|
149
|
+
DEEPSEEK_DEFAULT_BASE_URL,
|
|
150
|
+
DeepSeekSearchProvider,
|
|
126
151
|
SEARCH_PROVIDER_ID,
|
|
127
152
|
TAVILY_DEFAULT_API_KEY_ENV,
|
|
128
153
|
TAVILY_DEFAULT_BASE_URL,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-web-search-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Unified web search for the DeepSeek Harness seam (ctx.web): DeepSeek (official), Tavily, and Brave backends with an in-browser selector",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
|
7
7
|
"deepseek-harness",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
|
-
"check": "node --check lib/index.js && node --check lib/client.js && node --check lib/shared.js && node --check lib/tavily.js && node --check lib/brave.js"
|
|
56
|
+
"check": "node --check lib/index.js && node --check lib/client.js && node --check lib/shared.js && node --check lib/tavily.js && node --check lib/brave.js && node --check lib/deepseek.js"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"undici": "^6.21.0"
|