dsh-web-search-plugin 0.2.1 → 0.3.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 +21 -0
- package/README.md +104 -67
- package/cordis.patch.yml +1 -1
- package/lib/client.js +113 -26
- package/lib/index.js +96 -8
- package/lib/providers.js +212 -0
- package/lib/rest.js +325 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
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.3.0] - 2026-08-25
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
|
|
9
|
+
- **内置 REST provider 元数据化**(`lib/providers.js` 静态表 + `lib/rest.js` 通用后端):内置 REST 供应商由单一元数据表驱动,新增即"表里加一行 + 官方跳转链接",零定制执行代码。
|
|
10
|
+
- 新增内置 provider:**Serper / SerpApi / Exa / SearXNG / Scavio / Firecrawl**(`provider` 值 `serper` / `serpapi` / `exa` / `searxng` / `scavio` / `firecrawl`)。
|
|
11
|
+
- Firecrawl 响应 `data.web[]` 走专用 `firecrawl` 解析分支;Scavio 复用 `organic_results` 解析。
|
|
12
|
+
- DuckDuckGo 无官方 API:下拉以禁用项提示经 SearXNG 使用,不提供独立后端。
|
|
13
|
+
- 元数据字段:`method`(GET/POST)/ `queryIn`(query-string / body)/ `queryParam` / `countParam` / `params[]`(`when: always` / `nonEmpty` / `keyed` / `keyless`,受限谓词清单)/ `auth.type`(bearer / header / none / **query**)/ `response` / `hooks` / `officialUrl`。
|
|
14
|
+
- 通用后端 `RestSearchProvider` 接管 brave / tavily / serper / serpapi / exa / searxng / scavio / firecrawl;`deepseek-official`(模型工具型)仍走专用 `lib/deepseek.js`。
|
|
15
|
+
- Tavily 额度回传、Brave 限流头/代理作为可选 hook 注入,通用体不写死业务逻辑。
|
|
16
|
+
- 设置卡对需要 key 的内置 provider 渲染「获取 API key ↗」跳官方控制台;免 key 的(SearXNG / Tavily keyless)不渲染。
|
|
17
|
+
|
|
18
|
+
### 变更
|
|
19
|
+
|
|
20
|
+
- **默认引擎从 `tavily` 改为 `deepseek-official`**:schema 默认、服务端/客户端回退、bundle patch 初始值一致改为官方默认(已保存的显式 `provider` 不被覆盖)。
|
|
21
|
+
- `provider` 取值扩展为 `tavily` / `brave` / `deepseek-official` / `serper` / `serpapi` / `exa` / `searxng` / `scavio` / `firecrawl`。
|
|
22
|
+
- **修复 Tavily `include_answer` 类型错误**:`params[]` 布尔值不再强转字符串,请求体发真布尔(`true`/`false`),兼容 Tavily API 对 `include_answer` / `include_images` / `include_usage` 的类型要求。
|
|
23
|
+
- **修复 SearXNG 卡片误显「获取 API key ↗」**:仅需要 key 的 provider 才显示获取 key 跳转,免 key 的(SearXNG / Tavily keyless)不显示。
|
|
24
|
+
- **收紧未配置 key 的提示文案**:placeholder 缩短为「未配置密钥」,补充说明移至输入框下方提示,避免溢出。以下未纳入内置:TinyFish、Google CSE、SERPJET(官网不可访问)、DuckDuckGo(无官方 API,经 SearXNG 使用)。
|
|
25
|
+
|
|
5
26
|
## [0.2.1] - 2026-08-18
|
|
6
27
|
|
|
7
28
|
相对 0.2.0 的补丁:设置卡补上额度进度条、两列布局和输入校验,并收紧空态文案。
|
package/README.md
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
# dsh-web-search-plugin
|
|
2
2
|
|
|
3
|
-
面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) web 能力接缝(`ctx.web`)的统一网页搜索插件,内置 **DeepSeek
|
|
3
|
+
面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) web 能力接缝(`ctx.web`)的统一网页搜索插件,内置 **DeepSeek(官方,默认)/ Tavily / [Brave Search](https://brave.com/search/api/) / Serper / SerpApi / Exa / SearXNG / Scavio / Firecrawl 九个后端**。本包只向接缝注册 **一个** `WebSearchProvider`,稳定 id 为 `dsh-web-search`。在 **设置 → 网页搜索** 里切换引擎即可,不必再改 `web.searchProvider`。
|
|
4
4
|
|
|
5
|
-
- **DeepSeek
|
|
6
|
-
- **Tavily
|
|
5
|
+
- **DeepSeek(官方,默认)** — 走 DeepSeek 的 Anthropic 兼容 Messages API(原生 `web_search_20250305` 工具,凭据名 `DEEPSEEK_API_KEY`),一次搜索消耗一个模型轮次。
|
|
6
|
+
- **Tavily** — `keyless`(免费、限流、无需账号)或 `keyed`(`TAVILY_API_KEY`,Bearer token)。keyed 会在设置卡显示额度进度条。
|
|
7
7
|
- **Brave Search** — `GET https://api.search.brave.com/res/v1/web/search`,请求头 `X-Subscription-Token`(凭据名 `BRAVE_API_KEY`)。一次搜索就是一次 HTTP 请求,不走模型轮次;设置卡按响应头展示 Capacity / 月配额。
|
|
8
|
+
- **Serper** — Google 结果(`POST /search`,头 `X-API-KEY`,凭据名 `SERPER_API_KEY`)。
|
|
9
|
+
- **SerpApi** — `GET /search.json`,key 走 query(`api_key=`),凭据名 `SERPAPI_API_KEY`。
|
|
10
|
+
- **Exa** — 语义/神经搜索(`POST /search`,Bearer,凭据名 `EXA_API_KEY`)。
|
|
11
|
+
- **SearXNG** — 自建/协议免 key 的元搜索(`GET /search?format=json`),可填自托管实例地址。
|
|
12
|
+
- **Scavio** — Google SERP(`POST /api/v2/google`,Bearer,凭据名 `SCAVIO_API_KEY`),响应与 SerpApi 同构。
|
|
13
|
+
- **Firecrawl** — 搜索+抓取(`POST /v2/search`,Bearer,凭据名 `FIRECRAWL_API_KEY`),`sources: ["web"]`。
|
|
14
|
+
- **DuckDuckGo** — 无官方搜索 API,不提供独立后端;下拉中以禁用项提示:选 SearXNG 并指向聚合了 DuckDuckGo 的实例即可。
|
|
15
|
+
- **官方 key 跳转** — 需要 key 的内置 provider 在设置卡带「获取 API key ↗」一键跳官方控制台;免 key 的(SearXNG / Tavily keyless)不渲染。
|
|
8
16
|
- **不写自定义 session 事件** — 工具结果已经走接缝自己的事件,无需多余信封。
|
|
9
17
|
|
|
10
18
|
## 特性
|
|
11
19
|
|
|
12
20
|
- 与官方 `@deepseek-ai/dsh-web-search-deepseek` 相同的提供方约定:`inject: ['web']` + `installSettingsSection` + `ctx.web.registerSearchProvider`。
|
|
13
21
|
- 顶层 **设置 → 网页搜索** 分区:两列布局、未保存草稿、保存 toast;结果数量为 1–20 下拉。
|
|
22
|
+
- **默认引擎为 DeepSeek(官方)**:新装 / 未显式改动时,搜索直接走 `deepseek-official`,不再依赖 keyless 的 Tavily。
|
|
23
|
+
- 纯 REST 后端由**静态元数据表 + 通用后端**驱动,新增 provider 即"表里加一行 + 官方跳转链接",无定制执行代码。
|
|
14
24
|
- Tavily keyless 无需密钥即可用;Brave 需要订阅 token(若本机已有 `BRAVE_API_KEY` 凭据,可直接复用)。
|
|
15
|
-
- Tavily keyed / Brave 在设置卡展示额度进度条(DeepSeek 官方与 Tavily keyless
|
|
25
|
+
- Tavily keyed / Brave 在设置卡展示额度进度条(DeepSeek 官方与 Tavily keyless / 其余 REST 后端不展示)。
|
|
16
26
|
- 各引擎结果都规范化为接缝的 `WebSearchResult`(可选 `content` + `sources[]`),按 URL 去重。
|
|
17
27
|
- 设置段 `dsh-web-search-plugin` 通过 rc.7 的 `settings.describe()` 动态暴露,不需要宿主白名单补丁,也不需要自建回环 settings 桥。
|
|
18
28
|
- 错误映射为 `WEB_PROVIDER_ERROR` / `WEB_ABORTED` / `WEB_PROVIDER_CREDENTIAL_MISSING`。
|
|
@@ -32,56 +42,7 @@
|
|
|
32
42
|
dsh plugin --profile web add dsh-web-search-plugin
|
|
33
43
|
```
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
同盘可以用相对路径:
|
|
38
|
-
|
|
39
|
-
```powershell
|
|
40
|
-
dsh plugin --profile web add ".\dsh-web-search-plugin"
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
**Windows 跨盘不要 `dsh plugin add` 绝对路径。** Profile 在 `C:`、仓库在 `D:` 时,它会写成 `link:d:/...`;pnpm 把盘符当成相对路径,junction 会指到 `profiles\web\D:\...` 并链坏。也不要写 `file:D:/...`:pnpm 10 同样会把跨盘绝对路径拼进 profile 目录。
|
|
44
|
-
|
|
45
|
-
正确做法是快照到与 profile **同盘**,再用 `file:`:
|
|
46
|
-
|
|
47
|
-
```powershell
|
|
48
|
-
$dst = "$env:USERPROFILE\.dsh\profiles\web\.local\dsh-web-search-plugin"
|
|
49
|
-
New-Item -ItemType Directory -Force -Path $dst | Out-Null
|
|
50
|
-
robocopy "D:\gddi\dsh-web-search-plugin" $dst /E /XD node_modules .git .github /NFL /NDL /NJH /NJS
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
在 profile 的 `package.json` 里:
|
|
54
|
-
|
|
55
|
-
```json
|
|
56
|
-
"dsh-web-search-plugin": "file:.local/dsh-web-search-plugin"
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
并保证 `dsh.profile.bundles` 含本包,然后:
|
|
60
|
-
|
|
61
|
-
```powershell
|
|
62
|
-
dsh plugin --profile web install
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
`file:` 是快照:改完仓库后要再 robocopy + `install` 并重启 DSH。
|
|
66
|
-
|
|
67
|
-
用 `dsh --profile web --dump-config` 确认:组成树里应有 `dsh-web-search-plugin` 行,且 `web.searchProvider` 为 `dsh-web-search`。
|
|
68
|
-
|
|
69
|
-
若 profile 已经覆盖了 `web`(例如旧包 `@dsh-ltctfer/dsh-web-search-brave` 留下的 `brave-official`),后打的 patch 仍会生效。请改指向本插件,并卸掉旧包:
|
|
70
|
-
|
|
71
|
-
```yaml
|
|
72
|
-
- id: web
|
|
73
|
-
name: '@deepseek-ai/dsh-web'
|
|
74
|
-
config:
|
|
75
|
-
searchProvider: dsh-web-search
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
```powershell
|
|
79
|
-
dsh plugin --profile web remove @dsh-ltctfer/dsh-web-search-brave
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
然后在 **设置 → 网页搜索** 里把引擎切到想要的提供方,即可继续使用对应的 API key。
|
|
83
|
-
|
|
84
|
-
> **关于 `file:`(本地)安装** — pnpm 会把 `file:` 依赖做成快照。改完本仓库后,需要再同步快照并 `dsh plugin --profile web install`,然后重启。Windows 上 profile 与仓库不在同一盘时,用同盘 `file:.local/...`,不要 `link:` / `file:` 指向另一块盘。
|
|
45
|
+
> 从源码仓库本地安装(含 Windows 跨盘注意事项)见 [开发 → 本地安装与验证](#本地安装与验证)。
|
|
85
46
|
|
|
86
47
|
## 配置
|
|
87
48
|
|
|
@@ -89,12 +50,12 @@ dsh plugin --profile web remove @dsh-ltctfer/dsh-web-search-brave
|
|
|
89
50
|
|
|
90
51
|
| 键 | 默认值 | 含义 |
|
|
91
52
|
|---|---|---|
|
|
92
|
-
| `provider` | `
|
|
53
|
+
| `provider` | `deepseek-official` | `tavily`、`brave`、`deepseek-official`、`serper`、`serpapi`、`exa`、`searxng`、`scavio` 或 `firecrawl` |
|
|
93
54
|
| `mode` | `keyless` | 仅 Tavily:`keyless` 或 `keyed` |
|
|
94
55
|
| `apiKey` | — | Tavily API key 字面量(走凭据域,不会回显) |
|
|
95
56
|
| `apiKeyEnv` | `TAVILY_API_KEY` | keyed Tavily 使用的凭据/环境变量名 |
|
|
96
57
|
| `baseURL` | `https://api.tavily.com` | Tavily REST 基址;会再拼 `/search` |
|
|
97
|
-
| `maxResults` | `8` |
|
|
58
|
+
| `maxResults` | `8` | 每次搜索返回的源数量,各后端共用。设置卡为 1–20 下拉,超出上限会夹到 20 |
|
|
98
59
|
| `searchDepth` | `basic` | 仅 Tavily:`basic` 或 `advanced` |
|
|
99
60
|
| `includeAnswer` | `true` | 仅 Tavily:请求生成摘要,写入结果 `content` |
|
|
100
61
|
| `topic` | `general` | 仅 Tavily:`general` 或 `news` |
|
|
@@ -112,6 +73,23 @@ dsh plugin --profile web remove @dsh-ltctfer/dsh-web-search-brave
|
|
|
112
73
|
| `searchLang` | — | Brave 的 `search_lang`(如 `zh-hans`);留空使用 Brave 默认 |
|
|
113
74
|
| `freshness` | — | Brave 的 `freshness`:`pd` / `pw` / `pm` / `py` |
|
|
114
75
|
| `proxy` | — | Brave 使用的 HTTP(S) 代理;留空回退 `HTTPS_PROXY` / `HTTP_PROXY` |
|
|
76
|
+
| `serperApiKey` / `serperApiKeyEnv` / `serperBaseURL` | — / `SERPER_API_KEY` / — | Serper 的 key 字面量 / 凭据引用名 / 端点覆盖 |
|
|
77
|
+
| `serpapiApiKey` / `serpapiApiKeyEnv` / `serpapiBaseURL` | — / `SERPAPI_API_KEY` / — | SerpApi 的 key 字面量 / 凭据引用名 / 端点覆盖 |
|
|
78
|
+
| `exaApiKey` / `exaApiKeyEnv` / `exaBaseURL` | — / `EXA_API_KEY` / — | Exa 的 key 字面量 / 凭据引用名 / 端点覆盖 |
|
|
79
|
+
| `searxngBaseURL` | — | SearXNG 自托管实例基址;留空用 `https://searx.be` |
|
|
80
|
+
| `scavioApiKey` / `scavioApiKeyEnv` / `scavioBaseURL` | — / `SCAVIO_API_KEY` / — | Scavio 的 key 字面量 / 凭据引用名 / 端点覆盖 |
|
|
81
|
+
| `firecrawlApiKey` / `firecrawlApiKeyEnv` / `firecrawlBaseURL` | — / `FIRECRAWL_API_KEY` / — | Firecrawl 的 key 字面量 / 凭据引用名 / 端点覆盖 |
|
|
82
|
+
|
|
83
|
+
## 未纳入的服务
|
|
84
|
+
|
|
85
|
+
以下服务**不内置**(在引擎下拉中不会出现,也不提供后端):
|
|
86
|
+
|
|
87
|
+
| 服务 | 不做的原因 |
|
|
88
|
+
|---|---|
|
|
89
|
+
| **TinyFish** | 免费额度极小(每分钟 5 次);其 Search + Fetch 形态超出"纯 REST 搜索"边界,仅搜索部分价值低 |
|
|
90
|
+
| **Google CSE** | 官方已对新用户关闭注册,并将于 2027-01-01 停用 |
|
|
91
|
+
| **SERPJET** | 官网当前不可访问,暂不接入 |
|
|
92
|
+
| **DuckDuckGo** | 无官方搜索 API(HTML/社区库抓取不符合元数据表"纯 REST"边界)。下拉中有禁用提示,指引经 SearXNG 使用 |
|
|
115
93
|
|
|
116
94
|
环境变量:启动时 `DSH_WEB_SEARCH_PROVIDER=dsh-web-search` 会选中本接缝 id。未设置 `baseURL` 时,Tavily 基址回退 `$TAVILY_BASE_URL`。
|
|
117
95
|
|
|
@@ -129,25 +107,84 @@ DeepSeek 已并入本插件(`provider: deepseek-official`),无需切回。
|
|
|
129
107
|
## 工作方式
|
|
130
108
|
|
|
131
109
|
- **DeepSeek** — `POST {deepseekBaseURL}/messages`,请求头 `x-api-key` / `authorization: Bearer`,工具 `web_search_20250305`。
|
|
132
|
-
-
|
|
133
|
-
- **
|
|
110
|
+
- **纯 REST 类(Tavily / Brave / Serper / SerpApi / Exa / SearXNG)** — 由静态元数据表(`lib/providers.js`)+ 通用后端(`lib/rest.js`)驱动,无独立定制代码。请求方法 / 路径 / 查询字段名 / 鉴权(bearer / header / none / query)/ 固定参数 / 响应形态全部由表里的行决定。
|
|
111
|
+
- **Tavily** — `POST {baseURL}/search`。keyless 发送 `x-tavily-access-mode: keyless`;keyed 发送 `authorization: Bearer <key>`,并带 `include_usage` 回传 credits。
|
|
112
|
+
- **Brave** — `GET {braveBaseURL}?q=&count=`,请求头 `x-subscription-token`。不向 session 追加自定义事件。
|
|
113
|
+
- **Serper / SerpApi / Exa / SearXNG** — 按各自表的 `method` / `auth` / `params` 约定请求。
|
|
134
114
|
- 非 2xx 映射为 `WEB_PROVIDER_ERROR`;调用方取消映射为 `WEB_ABORTED`。
|
|
135
115
|
|
|
136
116
|
## 仓库布局
|
|
137
117
|
|
|
138
118
|
```
|
|
139
|
-
lib/index.js Host 插件:Config
|
|
140
|
-
lib/
|
|
141
|
-
lib/
|
|
142
|
-
lib/
|
|
119
|
+
lib/index.js Host 插件:Config、按元数据分发提供方、设置段、额度路由
|
|
120
|
+
lib/providers.js 静态元数据表:内置 REST provider 的请求/鉴权/响应/官方跳转(纯数据,无定制代码)
|
|
121
|
+
lib/rest.js 通用 REST 后端:按元数据驱动请求构造与响应规范化(可选 hooks)
|
|
122
|
+
lib/deepseek.js DeepSeek 后端(Anthropic Messages + web_search_20250305,模型工具型专用)
|
|
123
|
+
lib/tavily.js Tavily 选项解析 / 响应映射(额度由 Tavily hook 回传)
|
|
124
|
+
lib/brave.js Brave 选项解析 / 响应映射(解析 X-RateLimit-*)
|
|
143
125
|
lib/usage.js Tavily/Brave 用量本地缓存与 /usage 对账
|
|
144
126
|
lib/shared.js 中止 / 凭据解析辅助
|
|
145
|
-
lib/client.js 浏览器 bundle:顶层「网页搜索」分区 + 引擎切换 + 额度条
|
|
127
|
+
lib/client.js 浏览器 bundle:顶层「网页搜索」分区 + 引擎切换 + 官方 key 跳转 + 额度条
|
|
146
128
|
cordis.patch.yml Bundle patch:插入 Host 行、设 searchProvider、禁用内置 deepseek
|
|
147
129
|
```
|
|
148
130
|
|
|
149
131
|
## 开发
|
|
150
132
|
|
|
133
|
+
### 本地安装与验证
|
|
134
|
+
|
|
135
|
+
改源码后本机验证,把仓库装进某个 profile(以 `web` 为例)。**同盘**可以直接用相对路径:
|
|
136
|
+
|
|
137
|
+
```powershell
|
|
138
|
+
dsh plugin --profile web add ".\dsh-web-search-plugin"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Windows 跨盘不要 `dsh plugin add` 绝对路径。** Profile 在 `C:`、仓库在 `D:` 时,它会写成 `link:d:/...`;pnpm 把盘符当成相对路径,junction 会指到 `profiles\web\D:\...` 并链坏。也不要写 `file:D:/...`:pnpm 10 同样会把跨盘绝对路径拼进 profile 目录。
|
|
142
|
+
|
|
143
|
+
正确做法是快照到与 profile **同盘**,再用 `file:`:
|
|
144
|
+
|
|
145
|
+
```powershell
|
|
146
|
+
$dst = "$env:USERPROFILE\.dsh\profiles\web\.local\dsh-web-search-plugin"
|
|
147
|
+
New-Item -ItemType Directory -Force -Path $dst | Out-Null
|
|
148
|
+
robocopy "D:\sample\dsh-web-search-plugin" $dst /E /XD node_modules .git .github /NFL /NDL /NJH /NJS
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
在 profile 的 `package.json` 里:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
"dsh-web-search-plugin": "file:.local/dsh-web-search-plugin"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
并保证 `dsh.profile.bundles` 含本包,然后:
|
|
158
|
+
|
|
159
|
+
```powershell
|
|
160
|
+
dsh plugin --profile web install
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`file:` 是快照:改完仓库后要再 robocopy + `install` 并重启 DSH。
|
|
164
|
+
|
|
165
|
+
用 `dsh --profile web --dump-config` 确认:组成树里应有 `dsh-web-search-plugin` 行,且 `web.searchProvider` 为 `dsh-web-search`。
|
|
166
|
+
|
|
167
|
+
> **关于 `file:`(本地)安装** — pnpm 会把 `file:` 依赖做成快照。改完本仓库后,需要再同步快照并 `dsh plugin --profile web install`,然后重启。Windows 上 profile 与仓库不在同一盘时,用同盘 `file:.local/...`,不要 `link:` / `file:` 指向另一块盘。
|
|
168
|
+
|
|
169
|
+
### 迁移旧后端包
|
|
170
|
+
|
|
171
|
+
若 profile 已经覆盖了 `web`(例如旧包 `@dsh-ltctfer/dsh-web-search-brave` 留下的 `brave-official`),后打的 patch 仍会生效。请改指向本插件,并卸掉旧包:
|
|
172
|
+
|
|
173
|
+
```yaml
|
|
174
|
+
- id: web
|
|
175
|
+
name: '@deepseek-ai/dsh-web'
|
|
176
|
+
config:
|
|
177
|
+
searchProvider: dsh-web-search
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
```powershell
|
|
181
|
+
dsh plugin --profile web remove @dsh-ltctfer/dsh-web-search-brave
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
然后在 **设置 → 网页搜索** 里把引擎切到想要的提供方,即可继续使用对应的 API key。
|
|
185
|
+
|
|
186
|
+
### 语法检查
|
|
187
|
+
|
|
151
188
|
```powershell
|
|
152
189
|
npm run check
|
|
153
190
|
```
|
|
@@ -156,15 +193,15 @@ npm run check
|
|
|
156
193
|
|
|
157
194
|
## 发布
|
|
158
195
|
|
|
159
|
-
发布由 GitHub Actions(`.github/workflows/publish.yml`)自动完成:**推送 `v*` 标签**(例如 `v0.
|
|
196
|
+
发布由 GitHub Actions(`.github/workflows/publish.yml`)自动完成:**推送 `v*` 标签**(例如 `v0.3.0`)会带 provenance 发到 npm。向 `main` 的普通推送不会发布。
|
|
160
197
|
|
|
161
|
-
1. 确认 `package.json` 的 `version` 与即将推送的标签一致(例如 `0.
|
|
198
|
+
1. 确认 `package.json` 的 `version` 与即将推送的标签一致(例如 `0.3.0` → `v0.3.0`)。
|
|
162
199
|
2. 在 GitHub **Settings → Secrets and variables → Actions** 里配置有 publish 权限的 npm automation token(仓库密钥 `NPM_TOKEN`),并允许 Actions 运行。
|
|
163
200
|
3. 打标签并推送:
|
|
164
201
|
|
|
165
202
|
```powershell
|
|
166
|
-
git tag v0.
|
|
167
|
-
git push origin v0.
|
|
203
|
+
git tag v0.3.0
|
|
204
|
+
git push origin v0.3.0
|
|
168
205
|
```
|
|
169
206
|
|
|
170
207
|
工作流会先核对标签与 `package.json` 的 `version`,再执行 `npm publish --provenance --access public`。
|
package/cordis.patch.yml
CHANGED
package/lib/client.js
CHANGED
|
@@ -25,6 +25,25 @@ window.__ModuleLoader__.load({
|
|
|
25
25
|
|
|
26
26
|
/** Settings namespace of this plugin. */
|
|
27
27
|
const NS = "dsh-web-search-plugin";
|
|
28
|
+
/** Built-in REST providers (mirrors `lib/providers.js`; browser bundle is self-contained). */
|
|
29
|
+
const BUILTIN_REST_PROVIDERS = [
|
|
30
|
+
{ id: "serper", name: "Serper", officialUrl: "https://serper.dev/", keyRequired: true, keyField: "serperApiKey", keyEnvField: "serperApiKeyEnv", baseURLField: "serperBaseURL" },
|
|
31
|
+
{ id: "serpapi", name: "SerpApi", officialUrl: "https://serpapi.com/", keyRequired: true, keyField: "serpapiApiKey", keyEnvField: "serpapiApiKeyEnv", baseURLField: "serpapiBaseURL" },
|
|
32
|
+
{ id: "exa", name: "Exa", officialUrl: "https://dashboard.exa.ai/", keyRequired: true, keyField: "exaApiKey", keyEnvField: "exaApiKeyEnv", baseURLField: "exaBaseURL" },
|
|
33
|
+
{ id: "searxng", name: "SearXNG", officialUrl: "https://docs.searxng.org/", keyRequired: false, keyField: "", keyEnvField: "", baseURLField: "searxngBaseURL" },
|
|
34
|
+
{ id: "scavio", name: "Scavio", officialUrl: "https://scavio.dev/", keyRequired: true, keyField: "scavioApiKey", keyEnvField: "scavioApiKeyEnv", baseURLField: "scavioBaseURL" },
|
|
35
|
+
{ id: "firecrawl", name: "Firecrawl", officialUrl: "https://firecrawl.dev/", keyRequired: true, keyField: "firecrawlApiKey", keyEnvField: "firecrawlApiKeyEnv", baseURLField: "firecrawlBaseURL" }
|
|
36
|
+
];
|
|
37
|
+
/** Find a built-in REST provider by id. */
|
|
38
|
+
function builtinRest(id) {
|
|
39
|
+
for (const entry of BUILTIN_REST_PROVIDERS) if (entry.id === id) return entry;
|
|
40
|
+
return void 0;
|
|
41
|
+
}
|
|
42
|
+
/** Find a built-in REST provider by its literal key field name. */
|
|
43
|
+
function builtinRestByKeyField(field) {
|
|
44
|
+
for (const entry of BUILTIN_REST_PROVIDERS) if (entry.keyField === field) return entry;
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
28
47
|
/** Credential reference Tavily keyed mode resolves when the section names none. */
|
|
29
48
|
const DEFAULT_TAVILY_KEY_REF = "TAVILY_API_KEY";
|
|
30
49
|
/** Credential reference Brave resolves when the section names none. */
|
|
@@ -37,7 +56,9 @@ window.__ModuleLoader__.load({
|
|
|
37
56
|
const BRAVE_API_KEY_FIELD = "braveApiKey";
|
|
38
57
|
/** Form field the DeepSeek write-only credential control stages under. */
|
|
39
58
|
const DEEPSEEK_API_KEY_FIELD = "deepseekApiKey";
|
|
40
|
-
|
|
59
|
+
/** Form fields carrying literal API keys (write-only, staged under the credentials domain). */
|
|
60
|
+
const REST_KEY_FIELDS = BUILTIN_REST_PROVIDERS.map((entry) => entry.keyField).filter((field) => field !== "");
|
|
61
|
+
const SECRET_FIELDS = new Set([TAVILY_API_KEY_FIELD, BRAVE_API_KEY_FIELD, DEEPSEEK_API_KEY_FIELD, ...REST_KEY_FIELDS]);
|
|
41
62
|
const DRAFT_KEY = "dsh-web-search-plugin:settings-draft";
|
|
42
63
|
|
|
43
64
|
function readDraft() {
|
|
@@ -81,7 +102,8 @@ window.__ModuleLoader__.load({
|
|
|
81
102
|
apiKey: "API key",
|
|
82
103
|
apiKeyHint: "Stored outside the settings file. Leave blank to keep the current key.",
|
|
83
104
|
apiKeySet: "A key is configured.",
|
|
84
|
-
apiKeyUnset: "No key
|
|
105
|
+
apiKeyUnset: "No key set",
|
|
106
|
+
apiKeyUnavailableHint: "Search is unavailable until a key is configured.",
|
|
85
107
|
apiKeyEnv: "Credential reference",
|
|
86
108
|
apiKeyEnvHint: "Name of the credential/env var resolved for keyed Tavily search.",
|
|
87
109
|
braveApiKeyEnv: "Credential reference",
|
|
@@ -125,6 +147,9 @@ window.__ModuleLoader__.load({
|
|
|
125
147
|
maxUsesHint: "Maximum web_search server-tool uses per request.",
|
|
126
148
|
expand: "Show settings",
|
|
127
149
|
collapse: "Hide settings",
|
|
150
|
+
getKey: "Get API key",
|
|
151
|
+
ddgDisabled: "DuckDuckGo — use via SearXNG",
|
|
152
|
+
ddgHint: "DuckDuckGo has no official search API. Pick SearXNG and point it at an instance that includes DuckDuckGo (most public ones do), or self-host one.",
|
|
128
153
|
save: "Save",
|
|
129
154
|
saving: "Saving…",
|
|
130
155
|
discard: "Discard",
|
|
@@ -173,7 +198,8 @@ window.__ModuleLoader__.load({
|
|
|
173
198
|
apiKey: "API Key",
|
|
174
199
|
apiKeyHint: "不写入设置文件。留空表示保持当前密钥。",
|
|
175
200
|
apiKeySet: "已配置密钥。",
|
|
176
|
-
apiKeyUnset: "
|
|
201
|
+
apiKeyUnset: "未配置密钥",
|
|
202
|
+
apiKeyUnavailableHint: "未配置密钥前搜索不可用。",
|
|
177
203
|
apiKeyEnv: "凭据引用名",
|
|
178
204
|
apiKeyEnvHint: "keyed 模式解析的凭据/环境变量名。",
|
|
179
205
|
braveApiKeyEnv: "凭据引用名",
|
|
@@ -217,6 +243,9 @@ window.__ModuleLoader__.load({
|
|
|
217
243
|
maxUsesHint: "每次请求 web_search 服务端工具的最大调用次数。",
|
|
218
244
|
expand: "展开设置",
|
|
219
245
|
collapse: "收起设置",
|
|
246
|
+
getKey: "获取 API key",
|
|
247
|
+
ddgDisabled: "DuckDuckGo — 请经 SearXNG 使用",
|
|
248
|
+
ddgHint: "DuckDuckGo 没有官方搜索 API。请选择 SearXNG,并填入一个聚合了 DuckDuckGo 的实例地址(多数公共实例都有),或自建一个。",
|
|
220
249
|
save: "保存",
|
|
221
250
|
saving: "保存中…",
|
|
222
251
|
discard: "放弃修改",
|
|
@@ -328,7 +357,14 @@ window.__ModuleLoader__.load({
|
|
|
328
357
|
textField("model"),
|
|
329
358
|
textField("apiVersion"),
|
|
330
359
|
numberField("maxTokens", { min: 1 }),
|
|
331
|
-
numberField("maxUses", { min: 1 })
|
|
360
|
+
numberField("maxUses", { min: 1 }),
|
|
361
|
+
textField("serperApiKeyEnv"),
|
|
362
|
+
textField("serperBaseURL"),
|
|
363
|
+
textField("serpapiApiKeyEnv"),
|
|
364
|
+
textField("serpapiBaseURL"),
|
|
365
|
+
textField("exaApiKeyEnv"),
|
|
366
|
+
textField("exaBaseURL"),
|
|
367
|
+
textField("searxngBaseURL")
|
|
332
368
|
].map((spec) => [spec.field, spec]));
|
|
333
369
|
this.staged = new Map();
|
|
334
370
|
this.saving = false;
|
|
@@ -486,7 +522,7 @@ window.__ModuleLoader__.load({
|
|
|
486
522
|
this.staged.set(field, edit);
|
|
487
523
|
this.failed = false;
|
|
488
524
|
this.publish();
|
|
489
|
-
if (field === "provider" || field === "apiKeyEnv" || field === "braveApiKeyEnv" || field === "deepseekApiKeyEnv") this.readCredential();
|
|
525
|
+
if (field === "provider" || field === "apiKeyEnv" || field === "braveApiKeyEnv" || field === "deepseekApiKeyEnv" || BUILTIN_REST_PROVIDERS.some((entry) => entry.keyEnvField === field)) this.readCredential();
|
|
490
526
|
}
|
|
491
527
|
snapshotOf() {
|
|
492
528
|
return this.scope.getSnapshot();
|
|
@@ -513,7 +549,8 @@ window.__ModuleLoader__.load({
|
|
|
513
549
|
const staged = this.staged.get("provider");
|
|
514
550
|
if (staged !== void 0 && !staged.clear && staged.text.trim() !== "") return staged.text.trim();
|
|
515
551
|
const declared = this.sectionValue("provider");
|
|
516
|
-
|
|
552
|
+
if (declared === "tavily" || declared === "brave" || declared === "deepseek-official" || builtinRest(declared) !== void 0) return declared;
|
|
553
|
+
return "deepseek-official";
|
|
517
554
|
}
|
|
518
555
|
tavilyRef() {
|
|
519
556
|
const declared = this.sectionValue("apiKeyEnv");
|
|
@@ -530,7 +567,17 @@ window.__ModuleLoader__.load({
|
|
|
530
567
|
/** The credential reference the currently visible engine uses. */
|
|
531
568
|
refOf() {
|
|
532
569
|
const provider = this.providerOf();
|
|
533
|
-
|
|
570
|
+
if (provider === "brave") return this.braveRef();
|
|
571
|
+
if (provider === "deepseek-official") return this.deepseekRef();
|
|
572
|
+
const rest = builtinRest(provider);
|
|
573
|
+
if (rest !== void 0 && rest.keyRequired) return this.restKeyRef(rest);
|
|
574
|
+
return this.tavilyRef();
|
|
575
|
+
}
|
|
576
|
+
/** Credential reference for a built-in REST provider's key env field. */
|
|
577
|
+
restKeyRef(rest) {
|
|
578
|
+
const declared = this.sectionValue(rest.keyEnvField);
|
|
579
|
+
if (declared !== void 0 && declared.length > 0) return declared;
|
|
580
|
+
return rest.id === "serper" ? "SERPER_API_KEY" : rest.id === "serpapi" ? "SERPAPI_API_KEY" : "EXA_API_KEY";
|
|
534
581
|
}
|
|
535
582
|
async readCredential() {
|
|
536
583
|
const ref = this.refOf();
|
|
@@ -560,7 +607,13 @@ window.__ModuleLoader__.load({
|
|
|
560
607
|
this.readCredential();
|
|
561
608
|
}
|
|
562
609
|
async writeKey(field, value) {
|
|
563
|
-
|
|
610
|
+
let ref;
|
|
611
|
+
if (field === BRAVE_API_KEY_FIELD) ref = this.braveRef();
|
|
612
|
+
else if (field === DEEPSEEK_API_KEY_FIELD) ref = this.deepseekRef();
|
|
613
|
+
else {
|
|
614
|
+
const rest = builtinRestByKeyField(field);
|
|
615
|
+
ref = rest !== void 0 ? this.restKeyRef(rest) : this.tavilyRef();
|
|
616
|
+
}
|
|
564
617
|
try {
|
|
565
618
|
await this.api.credentials.set({ ref, value });
|
|
566
619
|
} catch (_credentialWriteFailure) {}
|
|
@@ -606,6 +659,16 @@ window.__ModuleLoader__.load({
|
|
|
606
659
|
apiVersion: this.form.field("apiVersion"),
|
|
607
660
|
maxTokens: this.form.field("maxTokens"),
|
|
608
661
|
maxUses: this.form.field("maxUses"),
|
|
662
|
+
serperApiKey: this.form.field("serperApiKey"),
|
|
663
|
+
serperApiKeyEnv: this.form.field("serperApiKeyEnv"),
|
|
664
|
+
serperBaseURL: this.form.field("serperBaseURL"),
|
|
665
|
+
serpapiApiKey: this.form.field("serpapiApiKey"),
|
|
666
|
+
serpapiApiKeyEnv: this.form.field("serpapiApiKeyEnv"),
|
|
667
|
+
serpapiBaseURL: this.form.field("serpapiBaseURL"),
|
|
668
|
+
exaApiKey: this.form.field("exaApiKey"),
|
|
669
|
+
exaApiKeyEnv: this.form.field("exaApiKeyEnv"),
|
|
670
|
+
exaBaseURL: this.form.field("exaBaseURL"),
|
|
671
|
+
searxngBaseURL: this.form.field("searxngBaseURL"),
|
|
609
672
|
apiKeyConfigured: this.form.credential.configured,
|
|
610
673
|
apiKeyWritable: this.form.credential.writable,
|
|
611
674
|
toastKey: this.form.toastKey
|
|
@@ -674,11 +737,14 @@ window.__ModuleLoader__.load({
|
|
|
674
737
|
".dshws_quota_alert{font-size:11.5px;margin:0;line-height:1.45}",
|
|
675
738
|
".dshws_quota_alert_warning{color:var(--dsw-alias-state-warn-primary,#f59e0b)}",
|
|
676
739
|
".dshws_quota_alert_danger{color:var(--dsw-alias-state-error-primary,#ef4444)}",
|
|
677
|
-
".dshws_btn{appearance:none;font:inherit;cursor:pointer;border-radius:
|
|
678
|
-
".dshws_btn:disabled{cursor:not-allowed;opacity:.
|
|
679
|
-
".dshws_btn_primary{
|
|
680
|
-
".dshws_btn_primary:hover:not(:disabled){filter:brightness(1.
|
|
681
|
-
".
|
|
740
|
+
".dshws_btn{appearance:none;font:inherit;cursor:pointer;border-radius:6px;padding:8px 16px;font-size:12.5px;font-weight:500;line-height:1.5;display:inline-flex;align-items:center;justify-content:center;border:1px solid transparent;transition:filter .15s ease,opacity .15s ease}",
|
|
741
|
+
".dshws_btn:disabled{cursor:not-allowed;opacity:.6}",
|
|
742
|
+
".dshws_btn_primary{background:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary,#3b82f6));color:var(--dsw-alias-label-primary-foreground,#fff);font-weight:600}",
|
|
743
|
+
".dshws_btn_primary:hover:not(:disabled){filter:brightness(1.12)}",
|
|
744
|
+
".dshws_btn_secondary{background:var(--dsw-alias-button-tool-bar-fill,var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.1)));color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2,var(--dsw-alias-border-secondary,rgba(128,128,128,.25)))}",
|
|
745
|
+
".dshws_btn_secondary:hover:not(:disabled){background:var(--dsw-alias-button-tool-bar-hover,var(--dsw-alias-interactive-bg-active,rgba(128,128,128,.16)))}",
|
|
746
|
+
".dshws_btn_outline{background:transparent;border-color:var(--dsw-alias-border-l2,var(--dsw-alias-border-secondary,rgba(128,128,128,.25)));color:var(--dsw-alias-label-secondary)}",
|
|
747
|
+
".dshws_btn_outline:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l1,rgba(128,128,128,.45));background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.1))}",
|
|
682
748
|
".dshws_toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--dsw-alias-state-success-primary,#10b981);color:#fff;padding:8px 18px;border-radius:999px;box-shadow:var(--dsw-shadow-lv3,0 8px 24px rgba(0,0,0,.3));font-size:12.5px;font-weight:500;z-index:100000;animation:dshws-toast-in .2s ease-out}",
|
|
683
749
|
".dshws_toast_error{background:var(--dsw-alias-state-error-primary,#ef4444)}",
|
|
684
750
|
".dshws_toast_info{background:var(--dsw-alias-label-secondary,#6b7280)}",
|
|
@@ -894,10 +960,13 @@ window.__ModuleLoader__.load({
|
|
|
894
960
|
const { t, state, actions } = props;
|
|
895
961
|
if (!state.available) return null;
|
|
896
962
|
const disabled = !state.writable;
|
|
897
|
-
const provider = state.provider.text === "
|
|
963
|
+
const provider = state.provider.text === "tavily" || state.provider.text === "brave" || state.provider.text === "deepseek-official" || builtinRest(state.provider.text) !== void 0 ? state.provider.text : "deepseek-official";
|
|
898
964
|
const brave = provider === "brave";
|
|
899
965
|
const deepseek = provider === "deepseek-official";
|
|
966
|
+
const tavily = provider === "tavily";
|
|
967
|
+
const restMeta = builtinRest(provider);
|
|
900
968
|
const keyed = state.mode.text === "keyed";
|
|
969
|
+
const keyHint = state.apiKeyConfigured ? t("apiKeyHint") : t("apiKeyHint") + " " + t("apiKeyUnavailableHint");
|
|
901
970
|
const toastText = toastCopy(t, state.toastKey);
|
|
902
971
|
const toastTone = state.toastKey === "failed" || state.toastKey === "invalid" ? "error" : state.toastKey === "nothing" ? "info" : "success";
|
|
903
972
|
const row = (field, label, hint, children) => fieldRow(label, hint, children, {
|
|
@@ -930,29 +999,32 @@ window.__ModuleLoader__.load({
|
|
|
930
999
|
row("provider", t("provider"), null, selectInput({ id: "ws-provider", value: provider, disabled, onChange: (e) => actions.edit("provider", e.target.value), children: [
|
|
931
1000
|
react.createElement("option", { value: "tavily", key: "tavily" }, t("providerTavily")),
|
|
932
1001
|
react.createElement("option", { value: "brave", key: "brave" }, t("providerBrave")),
|
|
933
|
-
react.createElement("option", { value: "deepseek-official", key: "deepseek-official" }, t("providerDeepseek"))
|
|
934
|
-
|
|
935
|
-
|
|
1002
|
+
react.createElement("option", { value: "deepseek-official", key: "deepseek-official" }, t("providerDeepseek")),
|
|
1003
|
+
...BUILTIN_REST_PROVIDERS.map((entry) => react.createElement("option", { value: entry.id, key: entry.id }, entry.name)),
|
|
1004
|
+
react.createElement("option", { value: "__ddg__", key: "ddg", disabled: true }, t("ddgDisabled"))
|
|
1005
|
+
] }),
|
|
1006
|
+
react.createElement("p", { className: "dshws_field_hint", style: { padding: "4px 0 0" } }, t("ddgHint"))),
|
|
1007
|
+
tavily ? row("mode", t("mode"), null, selectInput({ id: "ws-mode", value: state.mode.text, disabled, onChange: (e) => actions.edit("mode", e.target.value), children: [
|
|
936
1008
|
react.createElement("option", { value: "keyless", key: "keyless" }, t("modeKeyless")),
|
|
937
1009
|
react.createElement("option", { value: "keyed", key: "keyed" }, t("modeKeyed"))
|
|
938
|
-
] }))
|
|
1010
|
+
] })) : null
|
|
939
1011
|
),
|
|
940
1012
|
react.createElement(QuotaCard, { t, provider, keyed }),
|
|
941
1013
|
deepseek ? pair(
|
|
942
|
-
fieldRow(t("apiKey"),
|
|
1014
|
+
fieldRow(t("apiKey"), keyHint, 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) })),
|
|
943
1015
|
row("deepseekApiKeyEnv", t("deepseekApiKeyEnv"), t("deepseekApiKeyEnvHint"), textInput({ id: "ws-deepseek-keyref", value: state.deepseekApiKeyEnv.text, disabled, onChange: (e) => actions.edit("deepseekApiKeyEnv", e.target.value) }))
|
|
944
1016
|
) : null,
|
|
945
1017
|
brave ? pair(
|
|
946
|
-
fieldRow(t("apiKey"),
|
|
1018
|
+
fieldRow(t("apiKey"), keyHint, 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) })),
|
|
947
1019
|
row("braveApiKeyEnv", t("braveApiKeyEnv"), t("braveApiKeyEnvHint"), textInput({ id: "ws-brave-keyref", value: state.braveApiKeyEnv.text, disabled, onChange: (e) => actions.edit("braveApiKeyEnv", e.target.value) }))
|
|
948
1020
|
) : null,
|
|
949
|
-
(
|
|
950
|
-
fieldRow(t("apiKey"),
|
|
1021
|
+
(tavily && keyed) ? pair(
|
|
1022
|
+
fieldRow(t("apiKey"), keyHint, 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) })),
|
|
951
1023
|
row("apiKeyEnv", t("apiKeyEnv"), t("apiKeyEnvHint"), textInput({ id: "ws-tavily-keyref", value: state.apiKeyEnv.text, disabled, onChange: (e) => actions.edit("apiKeyEnv", e.target.value) }))
|
|
952
1024
|
) : null,
|
|
953
1025
|
deepseek ? row("deepseekBaseURL", t("deepseekBaseURL"), t("deepseekBaseURLHint"), textInput({ id: "ws-deepseek-base", value: state.deepseekBaseURL.text, disabled, onChange: (e) => actions.edit("deepseekBaseURL", e.target.value) })) : null,
|
|
954
1026
|
brave ? row("braveBaseURL", t("braveBaseURL"), t("braveBaseURLHint"), textInput({ id: "ws-brave-base", value: state.braveBaseURL.text, disabled, onChange: (e) => actions.edit("braveBaseURL", e.target.value) })) : null,
|
|
955
|
-
|
|
1027
|
+
tavily ? row("baseURL", t("baseURL"), t("baseURLHint"), textInput({ id: "ws-tavily-base", value: state.baseURL.text, disabled, onChange: (e) => actions.edit("baseURL", e.target.value) })) : null,
|
|
956
1028
|
deepseek ? pair(
|
|
957
1029
|
row("model", t("model"), t("modelHint"), textInput({ id: "ws-model", value: state.model.text, disabled, onChange: (e) => actions.edit("model", e.target.value) })),
|
|
958
1030
|
row("apiVersion", t("apiVersion"), t("apiVersionHint"), textInput({ id: "ws-apiversion", value: state.apiVersion.text, disabled, onChange: (e) => actions.edit("apiVersion", e.target.value) }))
|
|
@@ -961,7 +1033,7 @@ window.__ModuleLoader__.load({
|
|
|
961
1033
|
row("maxTokens", t("maxTokens"), t("maxTokensHint"), numberInput({ id: "ws-maxtokens", value: state.maxTokens.text, disabled, onChange: (e) => actions.edit("maxTokens", e.target.value), className: state.maxTokens.invalid ? "dshws_input_invalid" : void 0 })),
|
|
962
1034
|
row("maxUses", t("maxUses"), t("maxUsesHint"), numberInput({ id: "ws-maxuses", value: state.maxUses.text, disabled, onChange: (e) => actions.edit("maxUses", e.target.value), className: state.maxUses.invalid ? "dshws_input_invalid" : void 0 }))
|
|
963
1035
|
) : null,
|
|
964
|
-
|
|
1036
|
+
tavily ? pair(
|
|
965
1037
|
row("searchDepth", t("searchDepth"), null, selectInput({ id: "ws-depth", value: state.searchDepth.text, disabled, onChange: (e) => actions.edit("searchDepth", e.target.value), children: [
|
|
966
1038
|
react.createElement("option", { value: "basic", key: "basic" }, t("searchDepthBasic")),
|
|
967
1039
|
react.createElement("option", { value: "advanced", key: "advanced" }, t("searchDepthAdvanced"))
|
|
@@ -971,7 +1043,7 @@ window.__ModuleLoader__.load({
|
|
|
971
1043
|
react.createElement("option", { value: "news", key: "news" }, t("topicNews"))
|
|
972
1044
|
] }))
|
|
973
1045
|
) : null,
|
|
974
|
-
|
|
1046
|
+
tavily ? pair(
|
|
975
1047
|
row("includeAnswer", t("includeAnswer"), t("includeAnswerHint"), checkboxInput({ id: "ws-answer", checked: state.includeAnswer.text === "true", disabled, onChange: (e) => actions.edit("includeAnswer", e.target.checked ? "true" : "") })),
|
|
976
1048
|
maxResultsRow
|
|
977
1049
|
) : null,
|
|
@@ -991,10 +1063,25 @@ window.__ModuleLoader__.load({
|
|
|
991
1063
|
) : null,
|
|
992
1064
|
brave ? row("proxy", t("proxy"), null, textInput({ id: "ws-proxy", value: state.proxy.text, disabled, placeholder: t("proxyPlaceholder"), onChange: (e) => actions.edit("proxy", e.target.value) })) : null,
|
|
993
1065
|
deepseek ? maxResultsRow : null,
|
|
1066
|
+
restMeta ? react.createElement("div", { className: "dshws_form" },
|
|
1067
|
+
restMeta.keyRequired && restMeta.officialUrl ? react.createElement("a", {
|
|
1068
|
+
href: restMeta.officialUrl,
|
|
1069
|
+
target: "_blank",
|
|
1070
|
+
rel: "noopener noreferrer",
|
|
1071
|
+
className: "dshws_field_hint",
|
|
1072
|
+
style: { color: "var(--dsw-alias-brand-primary,#3b82f6)", textDecoration: "none", padding: "8px 0", display: "inline-block" }
|
|
1073
|
+
}, t("getKey") + " ↗") : null,
|
|
1074
|
+
restMeta.keyRequired ? pair(
|
|
1075
|
+
fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-" + restMeta.id + "-key", value: state[restMeta.keyField]?.text ?? "", disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(restMeta.keyField, e.target.value) })),
|
|
1076
|
+
row(restMeta.keyEnvField, t("apiKeyEnv"), t("apiKeyEnvHint"), textInput({ id: "ws-" + restMeta.id + "-keyref", value: state[restMeta.keyEnvField]?.text ?? "", disabled, onChange: (e) => actions.edit(restMeta.keyEnvField, e.target.value) }))
|
|
1077
|
+
) : null,
|
|
1078
|
+
row(restMeta.baseURLField, t("baseURL"), t("baseURLHint"), textInput({ id: "ws-" + restMeta.id + "-base", value: state[restMeta.baseURLField]?.text ?? "", disabled, onChange: (e) => actions.edit(restMeta.baseURLField, e.target.value) })),
|
|
1079
|
+
maxResultsRow
|
|
1080
|
+
) : null,
|
|
994
1081
|
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" } },
|
|
995
1082
|
react.createElement("button", {
|
|
996
1083
|
type: "button",
|
|
997
|
-
className: "dshws_btn
|
|
1084
|
+
className: "dshws_btn dshws_btn_secondary",
|
|
998
1085
|
disabled: (!state.dirty && !state.failed) || state.saving,
|
|
999
1086
|
onClick: (event) => {
|
|
1000
1087
|
event.preventDefault();
|
package/lib/index.js
CHANGED
|
@@ -23,6 +23,8 @@ import { TAVILY_DEFAULT_API_KEY_ENV, TAVILY_DEFAULT_BASE_URL, TavilySearchProvid
|
|
|
23
23
|
import { BRAVE_DEFAULT_API_KEY_ENV, BRAVE_DEFAULT_BASE_URL, BraveSearchProvider, resolveBraveOptions } from "./brave.js";
|
|
24
24
|
import { DEEPSEEK_DEFAULT_API_KEY_ENV, DEEPSEEK_DEFAULT_BASE_URL, DeepSeekSearchProvider, resolveDeepseekOptions } from "./deepseek.js";
|
|
25
25
|
import { TAVILY_USAGE_POLL_MS, UsageTracker } from "./usage.js";
|
|
26
|
+
import { RestSearchProvider, resolveRestProvider } from "./rest.js";
|
|
27
|
+
import { BUILTIN_REST_IDS, REST_PROVIDERS, findProvider } from "./providers.js";
|
|
26
28
|
|
|
27
29
|
/** Stable id this plugin registers under on `ctx.web`. */
|
|
28
30
|
const SEARCH_PROVIDER_ID = "dsh-web-search";
|
|
@@ -34,8 +36,8 @@ const inject = ["web"];
|
|
|
34
36
|
|
|
35
37
|
/** Plugin config (all optional — `apply` fills defaults). */
|
|
36
38
|
const Config = z.object({
|
|
37
|
-
/** Active backend: `deepseek-official
|
|
38
|
-
provider: z.string().default("
|
|
39
|
+
/** Active backend: `deepseek-official` (default), `tavily`, `brave`, `serper`, `serpapi`, `exa`, or `searxng`. */
|
|
40
|
+
provider: z.string().default("deepseek-official"),
|
|
39
41
|
/** Tavily auth mode: `keyless` (default) or `keyed`. */
|
|
40
42
|
mode: z.string().default("keyless"),
|
|
41
43
|
/** Literal Tavily API key; prefer {@link apiKeyEnv}. */
|
|
@@ -66,6 +68,38 @@ const Config = z.object({
|
|
|
66
68
|
freshness: z.string().default(""),
|
|
67
69
|
/** Optional HTTP(S) proxy URL for Brave (falls back to HTTPS_PROXY / HTTP_PROXY). */
|
|
68
70
|
proxy: z.string().default(""),
|
|
71
|
+
/** Literal Serper API key; prefer {@link serperApiKeyEnv}. */
|
|
72
|
+
serperApiKey: z.string().role("secret").default(""),
|
|
73
|
+
/** Credential reference for Serper search. */
|
|
74
|
+
serperApiKeyEnv: z.string().role("credential-ref").default("SERPER_API_KEY"),
|
|
75
|
+
/** Serper endpoint base. `/search` is appended. */
|
|
76
|
+
serperBaseURL: z.string().default(""),
|
|
77
|
+
/** Literal SerpApi API key; prefer {@link serpapiApiKeyEnv}. */
|
|
78
|
+
serpapiApiKey: z.string().role("secret").default(""),
|
|
79
|
+
/** Credential reference for SerpApi search. */
|
|
80
|
+
serpapiApiKeyEnv: z.string().role("credential-ref").default("SERPAPI_API_KEY"),
|
|
81
|
+
/** SerpApi endpoint base. `/search.json` is appended. */
|
|
82
|
+
serpapiBaseURL: z.string().default(""),
|
|
83
|
+
/** Literal Exa API key; prefer {@link exaApiKeyEnv}. */
|
|
84
|
+
exaApiKey: z.string().role("secret").default(""),
|
|
85
|
+
/** Credential reference for Exa search. */
|
|
86
|
+
exaApiKeyEnv: z.string().role("credential-ref").default("EXA_API_KEY"),
|
|
87
|
+
/** Exa endpoint base. `/search` is appended. */
|
|
88
|
+
exaBaseURL: z.string().default(""),
|
|
89
|
+
/** Optional SearXNG instance base URL (self-hosted). `/search` is appended. */
|
|
90
|
+
searxngBaseURL: z.string().default(""),
|
|
91
|
+
/** Literal Scavio API key; prefer {@link scavioApiKeyEnv}. */
|
|
92
|
+
scavioApiKey: z.string().role("secret").default(""),
|
|
93
|
+
/** Credential reference for Scavio search. */
|
|
94
|
+
scavioApiKeyEnv: z.string().role("credential-ref").default("SCAVIO_API_KEY"),
|
|
95
|
+
/** Scavio endpoint base. `/api/v2/google` is appended. */
|
|
96
|
+
scavioBaseURL: z.string().default(""),
|
|
97
|
+
/** Literal Firecrawl API key; prefer {@link firecrawlApiKeyEnv}. */
|
|
98
|
+
firecrawlApiKey: z.string().role("secret").default(""),
|
|
99
|
+
/** Credential reference for Firecrawl search. */
|
|
100
|
+
firecrawlApiKeyEnv: z.string().role("credential-ref").default("FIRECRAWL_API_KEY"),
|
|
101
|
+
/** Firecrawl endpoint base. `/v2/search` is appended. */
|
|
102
|
+
firecrawlBaseURL: z.string().default(""),
|
|
69
103
|
/** Literal DeepSeek API key; prefer {@link deepseekApiKeyEnv}. */
|
|
70
104
|
deepseekApiKey: z.string().role("secret").default(""),
|
|
71
105
|
/** Credential reference for DeepSeek search. */
|
|
@@ -106,6 +140,13 @@ class PluginSearchProvider {
|
|
|
106
140
|
...this.resolveOptions().brave,
|
|
107
141
|
onRateLimit: (headers) => usage?.recordBraveHeaders(headers)
|
|
108
142
|
}));
|
|
143
|
+
this.rest = new Map();
|
|
144
|
+
for (const providerId of BUILTIN_REST_IDS) {
|
|
145
|
+
this.rest.set(providerId, new RestSearchProvider(providerId, () => this.resolveOptions().rest[providerId], {
|
|
146
|
+
onUsage: providerId === "tavily" ? (credits) => usage?.recordTavilyCredits(credits) : void 0,
|
|
147
|
+
onRateLimit: providerId === "brave" ? (headers) => usage?.recordBraveHeaders(headers) : void 0
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
109
150
|
}
|
|
110
151
|
|
|
111
152
|
available() {
|
|
@@ -117,21 +158,63 @@ class PluginSearchProvider {
|
|
|
117
158
|
}
|
|
118
159
|
|
|
119
160
|
backend() {
|
|
120
|
-
const
|
|
121
|
-
|
|
161
|
+
const options = this.resolveOptions();
|
|
162
|
+
const provider = options.provider;
|
|
163
|
+
const restInstance = this.rest.get(provider);
|
|
164
|
+
if (restInstance !== void 0) return restInstance;
|
|
122
165
|
if (provider === "deepseek-official") return this.deepseek;
|
|
123
|
-
|
|
166
|
+
// Fallback: provider points at something unknown → official default.
|
|
167
|
+
return this.deepseek;
|
|
124
168
|
}
|
|
125
169
|
}
|
|
126
170
|
|
|
127
171
|
/** Project one resolved section into options for each backend. */
|
|
128
172
|
function resolveOptions(ctx, config) {
|
|
129
|
-
const
|
|
173
|
+
const isBuiltIn = config.provider === "tavily" || config.provider === "brave" || config.provider === "deepseek-official" || BUILTIN_REST_IDS.includes(config.provider);
|
|
174
|
+
const provider = isBuiltIn ? config.provider : "deepseek-official";
|
|
175
|
+
// Per-provider literal key / optional endpoint override, used when the
|
|
176
|
+
// metadata row's default is not enough (self-hosted SearXNG, proxied
|
|
177
|
+
// providers, custom key refs).
|
|
178
|
+
const keySourceByProvider = {
|
|
179
|
+
brave: { literal: config.braveApiKey, envName: config.braveApiKeyEnv ?? BRAVE_DEFAULT_API_KEY_ENV, baseURL: config.braveBaseURL ?? "", proxy: config.proxy },
|
|
180
|
+
tavily: { literal: config.apiKey, envName: config.apiKeyEnv ?? TAVILY_DEFAULT_API_KEY_ENV, baseURL: config.baseURL ?? "" },
|
|
181
|
+
serper: { literal: config.serperApiKey, envName: config.serperApiKeyEnv ?? "SERPER_API_KEY", baseURL: config.serperBaseURL ?? "" },
|
|
182
|
+
serpapi: { literal: config.serpapiApiKey, envName: config.serpapiApiKeyEnv ?? "SERPAPI_API_KEY", baseURL: config.serpapiBaseURL ?? "" },
|
|
183
|
+
exa: { literal: config.exaApiKey, envName: config.exaApiKeyEnv ?? "EXA_API_KEY", baseURL: config.exaBaseURL ?? "" },
|
|
184
|
+
searxng: { baseURL: config.searxngBaseURL ?? "" },
|
|
185
|
+
scavio: { literal: config.scavioApiKey, envName: config.scavioApiKeyEnv ?? "SCAVIO_API_KEY", baseURL: config.scavioBaseURL ?? "" },
|
|
186
|
+
firecrawl: { literal: config.firecrawlApiKey, envName: config.firecrawlApiKeyEnv ?? "FIRECRAWL_API_KEY", baseURL: config.firecrawlBaseURL ?? "" }
|
|
187
|
+
};
|
|
188
|
+
const settings = {
|
|
189
|
+
country: config.country,
|
|
190
|
+
searchLang: config.searchLang,
|
|
191
|
+
freshness: config.freshness,
|
|
192
|
+
searchDepth: config.searchDepth,
|
|
193
|
+
topic: config.topic,
|
|
194
|
+
includeAnswer: config.includeAnswer
|
|
195
|
+
};
|
|
196
|
+
const rest = {};
|
|
197
|
+
for (const providerId of BUILTIN_REST_IDS) {
|
|
198
|
+
const row = findProvider(providerId);
|
|
199
|
+
if (row === void 0) continue;
|
|
200
|
+
const source = keySourceByProvider[providerId] ?? {};
|
|
201
|
+
const resolved = resolveRestProvider(ctx, row, {
|
|
202
|
+
config,
|
|
203
|
+
settings,
|
|
204
|
+
keyed: providerId === "tavily" ? config.mode === "keyed" : false,
|
|
205
|
+
keySource: source
|
|
206
|
+
});
|
|
207
|
+
// Endpoint override: an explicit config baseURL wins over the row default.
|
|
208
|
+
if (source.baseURL != null && source.baseURL.length > 0) resolved.baseURL = source.baseURL;
|
|
209
|
+
if (providerId === "brave" && (source.proxy ?? "").length > 0) resolved.proxy = source.proxy;
|
|
210
|
+
rest[providerId] = resolved;
|
|
211
|
+
}
|
|
130
212
|
return {
|
|
131
213
|
provider,
|
|
132
214
|
deepseek: resolveDeepseekOptions(ctx, config),
|
|
133
215
|
tavily: resolveTavilyOptions(ctx, config),
|
|
134
|
-
brave: resolveBraveOptions(ctx, config)
|
|
216
|
+
brave: resolveBraveOptions(ctx, config),
|
|
217
|
+
rest
|
|
135
218
|
};
|
|
136
219
|
}
|
|
137
220
|
|
|
@@ -236,11 +319,14 @@ function apply(ctx, config) {
|
|
|
236
319
|
export {
|
|
237
320
|
BRAVE_DEFAULT_API_KEY_ENV,
|
|
238
321
|
BRAVE_DEFAULT_BASE_URL,
|
|
322
|
+
BUILTIN_REST_IDS,
|
|
239
323
|
BraveSearchProvider,
|
|
240
324
|
Config,
|
|
241
325
|
DEEPSEEK_DEFAULT_API_KEY_ENV,
|
|
242
326
|
DEEPSEEK_DEFAULT_BASE_URL,
|
|
243
327
|
DeepSeekSearchProvider,
|
|
328
|
+
REST_PROVIDERS,
|
|
329
|
+
RestSearchProvider,
|
|
244
330
|
SEARCH_PROVIDER_ID,
|
|
245
331
|
TAVILY_DEFAULT_API_KEY_ENV,
|
|
246
332
|
TAVILY_DEFAULT_BASE_URL,
|
|
@@ -250,6 +336,8 @@ export {
|
|
|
250
336
|
WEB_SEARCH_SETTINGS_NAMESPACE,
|
|
251
337
|
WEB_SEARCH_TAVILY_SETTINGS_NAMESPACE,
|
|
252
338
|
apply,
|
|
339
|
+
findProvider,
|
|
253
340
|
inject,
|
|
254
|
-
name
|
|
341
|
+
name,
|
|
342
|
+
resolveRestProvider
|
|
255
343
|
};
|
package/lib/providers.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static metadata table for every built-in REST web-search provider.
|
|
3
|
+
*
|
|
4
|
+
* This module is intentionally **data, not code**: adding a new REST provider
|
|
5
|
+
* means adding one row here (plus an optional official "get a key" link in the
|
|
6
|
+
* settings card), with zero per-provider execution code. The generic backend in
|
|
7
|
+
* `lib/rest.js` reads a row and performs `method` / `path` / `queryParam` /
|
|
8
|
+
* `countParam` / `params` / `auth` / `response` / `hooks`.
|
|
9
|
+
*
|
|
10
|
+
* Only **pure REST providers** ("one query → one HTTP request → one result
|
|
11
|
+
* array") belong here. Model-native search tool (DeepSeek's Anthropic Messages
|
|
12
|
+
* `web_search_20250305`, Perplexity sonar tools, OpenAI Responses) is **not**
|
|
13
|
+
* REST and stays on its dedicated backend (`lib/deepseek.js`).
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-web-search-plugin/providers
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Built-in REST providers.
|
|
20
|
+
* `auth.type` ∈ { `bearer`, `header`, `none`, `query` }:
|
|
21
|
+
* - `bearer` — `Authorization: Bearer <key>`
|
|
22
|
+
* - `header` — key in a named header (`auth.header`)
|
|
23
|
+
* - `none` — no key (SearXNG, keyless)
|
|
24
|
+
* - `query` — key as a query-string parameter (`auth.param`, e.g. SerpApi)
|
|
25
|
+
* `params[].when` ∈ { `always`, `nonEmpty`, `keyed`, `keyless` }.
|
|
26
|
+
*/
|
|
27
|
+
export const REST_PROVIDERS = [
|
|
28
|
+
{
|
|
29
|
+
id: "brave",
|
|
30
|
+
name: "Brave Search",
|
|
31
|
+
kind: "rest",
|
|
32
|
+
method: "GET",
|
|
33
|
+
baseURL: "https://api.search.brave.com/res/v1/web/search",
|
|
34
|
+
path: "",
|
|
35
|
+
queryIn: "query",
|
|
36
|
+
queryParam: "q",
|
|
37
|
+
countParam: "count",
|
|
38
|
+
auth: { type: "header", header: "X-Subscription-Token" },
|
|
39
|
+
keyRequired: true,
|
|
40
|
+
apiKeyRef: "BRAVE_API_KEY",
|
|
41
|
+
response: "brave",
|
|
42
|
+
params: [
|
|
43
|
+
{ key: "country", setting: "country", when: "nonEmpty" },
|
|
44
|
+
{ key: "search_lang", setting: "searchLang", when: "nonEmpty" },
|
|
45
|
+
{ key: "freshness", setting: "freshness", when: "nonEmpty" }
|
|
46
|
+
],
|
|
47
|
+
officialUrl: "https://api-dashboard.search.brave.com/",
|
|
48
|
+
hooks: ["rate-limit", "proxy"]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "tavily",
|
|
52
|
+
name: "Tavily",
|
|
53
|
+
kind: "rest",
|
|
54
|
+
method: "POST",
|
|
55
|
+
baseURL: "https://api.tavily.com",
|
|
56
|
+
path: "/search",
|
|
57
|
+
queryIn: "body",
|
|
58
|
+
queryParam: "query",
|
|
59
|
+
countParam: "max_results",
|
|
60
|
+
auth: { type: "bearer" },
|
|
61
|
+
keyRequired: false, // keyless by default; keyed only when a key is configured
|
|
62
|
+
apiKeyRef: "TAVILY_API_KEY",
|
|
63
|
+
response: "tavily",
|
|
64
|
+
params: [
|
|
65
|
+
{ key: "search_depth", setting: "searchDepth", when: "nonEmpty" },
|
|
66
|
+
{ key: "topic", setting: "topic", when: "nonEmpty" },
|
|
67
|
+
{ key: "include_answer", setting: "includeAnswer", when: "nonEmpty" },
|
|
68
|
+
{ key: "include_images", value: false, when: "always" },
|
|
69
|
+
{ key: "include_usage", value: true, when: "keyed" }
|
|
70
|
+
],
|
|
71
|
+
keylessHeaders: { "x-tavily-access-mode": "keyless" },
|
|
72
|
+
keylessHeaderWhen: "keyless",
|
|
73
|
+
officialUrl: "https://app.tavily.com/",
|
|
74
|
+
hooks: ["usage"]
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "serper",
|
|
78
|
+
name: "Serper",
|
|
79
|
+
kind: "rest",
|
|
80
|
+
method: "POST",
|
|
81
|
+
baseURL: "https://google.serper.dev",
|
|
82
|
+
path: "/search",
|
|
83
|
+
queryIn: "body",
|
|
84
|
+
queryParam: "q",
|
|
85
|
+
countParam: "num",
|
|
86
|
+
auth: { type: "header", header: "X-API-KEY" },
|
|
87
|
+
keyRequired: true,
|
|
88
|
+
apiKeyRef: "SERPER_API_KEY",
|
|
89
|
+
response: "serper",
|
|
90
|
+
params: [],
|
|
91
|
+
officialUrl: "https://serper.dev/",
|
|
92
|
+
hooks: []
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "serpapi",
|
|
96
|
+
name: "SerpApi",
|
|
97
|
+
kind: "rest",
|
|
98
|
+
method: "GET",
|
|
99
|
+
baseURL: "https://serpapi.com",
|
|
100
|
+
path: "/search.json",
|
|
101
|
+
queryIn: "query",
|
|
102
|
+
queryParam: "q",
|
|
103
|
+
countParam: "num",
|
|
104
|
+
auth: { type: "query", param: "api_key" },
|
|
105
|
+
keyRequired: true,
|
|
106
|
+
apiKeyRef: "SERPAPI_API_KEY",
|
|
107
|
+
response: "serper",
|
|
108
|
+
params: [],
|
|
109
|
+
officialUrl: "https://serpapi.com/",
|
|
110
|
+
hooks: []
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "exa",
|
|
114
|
+
name: "Exa",
|
|
115
|
+
kind: "rest",
|
|
116
|
+
method: "POST",
|
|
117
|
+
baseURL: "https://api.exa.ai",
|
|
118
|
+
path: "/search",
|
|
119
|
+
queryIn: "body",
|
|
120
|
+
queryParam: "query",
|
|
121
|
+
countParam: "num_results",
|
|
122
|
+
auth: { type: "bearer" },
|
|
123
|
+
keyRequired: true,
|
|
124
|
+
apiKeyRef: "EXA_API_KEY",
|
|
125
|
+
response: "exa",
|
|
126
|
+
params: [
|
|
127
|
+
{ key: "contents", value: "text", when: "always" }
|
|
128
|
+
],
|
|
129
|
+
officialUrl: "https://dashboard.exa.ai/",
|
|
130
|
+
hooks: []
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: "searxng",
|
|
134
|
+
name: "SearXNG",
|
|
135
|
+
kind: "rest",
|
|
136
|
+
method: "GET",
|
|
137
|
+
baseURL: "https://searx.be",
|
|
138
|
+
path: "/search",
|
|
139
|
+
queryIn: "query",
|
|
140
|
+
queryParam: "q",
|
|
141
|
+
countParam: "count",
|
|
142
|
+
auth: { type: "none" },
|
|
143
|
+
keyRequired: false,
|
|
144
|
+
apiKeyRef: "",
|
|
145
|
+
response: "searxng",
|
|
146
|
+
params: [
|
|
147
|
+
{ key: "format", value: "json", when: "always" }
|
|
148
|
+
],
|
|
149
|
+
officialUrl: "https://docs.searxng.org/",
|
|
150
|
+
hooks: []
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: "scavio",
|
|
154
|
+
name: "Scavio",
|
|
155
|
+
kind: "rest",
|
|
156
|
+
method: "POST",
|
|
157
|
+
baseURL: "https://api.scavio.dev",
|
|
158
|
+
path: "/api/v2/google",
|
|
159
|
+
queryIn: "body",
|
|
160
|
+
queryParam: "query",
|
|
161
|
+
countParam: "",
|
|
162
|
+
auth: { type: "bearer" },
|
|
163
|
+
keyRequired: true,
|
|
164
|
+
apiKeyRef: "SCAVIO_API_KEY",
|
|
165
|
+
response: "serper",
|
|
166
|
+
params: [
|
|
167
|
+
{ key: "hl", setting: "searchLang", when: "nonEmpty" },
|
|
168
|
+
{ key: "gl", setting: "country", when: "nonEmpty" }
|
|
169
|
+
],
|
|
170
|
+
officialUrl: "https://scavio.dev/",
|
|
171
|
+
hooks: []
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: "firecrawl",
|
|
175
|
+
name: "Firecrawl",
|
|
176
|
+
kind: "rest",
|
|
177
|
+
method: "POST",
|
|
178
|
+
baseURL: "https://api.firecrawl.dev",
|
|
179
|
+
path: "/v2/search",
|
|
180
|
+
queryIn: "body",
|
|
181
|
+
queryParam: "query",
|
|
182
|
+
countParam: "limit",
|
|
183
|
+
auth: { type: "bearer" },
|
|
184
|
+
keyRequired: true,
|
|
185
|
+
apiKeyRef: "FIRECRAWL_API_KEY",
|
|
186
|
+
response: "firecrawl",
|
|
187
|
+
params: [
|
|
188
|
+
{ key: "sources", value: "web", when: "always" }
|
|
189
|
+
],
|
|
190
|
+
officialUrl: "https://firecrawl.dev/",
|
|
191
|
+
hooks: []
|
|
192
|
+
}
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
/** Location of one provider by id (the id doubles as the `provider` value). */
|
|
196
|
+
export function findProvider(id) {
|
|
197
|
+
for (const entry of REST_PROVIDERS) if (entry.id === id) return entry;
|
|
198
|
+
return void 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Set of ids that live in the metadata table. */
|
|
202
|
+
export function restProviderIds() {
|
|
203
|
+
const ids = /* @__PURE__ */ new Set();
|
|
204
|
+
for (const entry of REST_PROVIDERS) ids.add(entry.id);
|
|
205
|
+
return ids;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The built-in REST provider ids (seam-facing `provider` values). */
|
|
209
|
+
export const BUILTIN_REST_IDS = REST_PROVIDERS.map((entry) => entry.id);
|
|
210
|
+
|
|
211
|
+
/** The model-native (non-REST) provider id, kept on its own backend. */
|
|
212
|
+
export const TOOL_PROVIDER_IDS = ["deepseek-official"];
|
package/lib/rest.js
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic REST web-search backend driven by the static metadata table in
|
|
3
|
+
* `lib/providers.js`. One instance handles any built-in REST provider and any
|
|
4
|
+
* user-defined `customProviders[]` entry, so adding a provider is data (a table
|
|
5
|
+
* row), not a new backend class.
|
|
6
|
+
*
|
|
7
|
+
* Request shape comes from the provider row: `method` (GET/POST), `path`
|
|
8
|
+
* (POST suffix), `queryIn` (query-string vs JSON body), `queryParam` /
|
|
9
|
+
* `countParam` (field names), `params[]` (fixed/conditional extra params), and
|
|
10
|
+
* `auth` (bearer/header/none/query). Response shape is normalized by the
|
|
11
|
+
* lenient parser in this module to the seam's `WebSearchResult`.
|
|
12
|
+
*
|
|
13
|
+
* Side effects (Tavily credits, Brave rate-limit headers, proxy dispatch) are
|
|
14
|
+
* optional hooks the caller injects; the generic body never hard-codes them.
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-web-search-plugin/rest
|
|
17
|
+
*/
|
|
18
|
+
import { WebError } from "@deepseek-ai/dsh-web";
|
|
19
|
+
import { ProxyAgent } from "undici";
|
|
20
|
+
import { MAX_RESULTS_CAP, USER_AGENT, isAbortError, resolveApiKey, resolveSecret, searchAborted, throwIfSearchAborted } from "./shared.js";
|
|
21
|
+
|
|
22
|
+
/** Acceptable auth templates (superset includes `query` for SerpApi). */
|
|
23
|
+
const AUTH_TYPES = new Set(["bearer", "header", "none", "query"]);
|
|
24
|
+
|
|
25
|
+
/** Read a non-empty string field. */
|
|
26
|
+
function str(value) {
|
|
27
|
+
return typeof value === "string" ? value : "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Resolve a `params[]` entry to `{ key, value }` or `null` (skip). */
|
|
31
|
+
/** Keep the param's native type: booleans stay booleans (JSON true/false). */
|
|
32
|
+
function paramValue(value) {
|
|
33
|
+
if (typeof value === "boolean") return value;
|
|
34
|
+
return value === void 0 ? "" : String(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveParam(param, ctx) {
|
|
38
|
+
if (param === null || typeof param !== "object") return null;
|
|
39
|
+
const when = str(param.when) || "always";
|
|
40
|
+
const key = str(param.key);
|
|
41
|
+
if (key === "") return null;
|
|
42
|
+
if (when === "always") {
|
|
43
|
+
return { key, value: paramValue(param.value) };
|
|
44
|
+
}
|
|
45
|
+
if (when === "nonEmpty") {
|
|
46
|
+
const raw = ctx[param.setting];
|
|
47
|
+
// Booleans: only `true` is "non-empty"; `false`/missing means omit the field.
|
|
48
|
+
if (typeof raw === "boolean") return raw === true ? { key, value: true } : null;
|
|
49
|
+
const value = str(raw);
|
|
50
|
+
if (value === "") return null;
|
|
51
|
+
return { key, value };
|
|
52
|
+
}
|
|
53
|
+
if (when === "keyed") {
|
|
54
|
+
return ctx.keyed === true ? { key, value: paramValue(param.value) } : null;
|
|
55
|
+
}
|
|
56
|
+
if (when === "keyless") {
|
|
57
|
+
return ctx.keyed !== true ? { key, value: paramValue(param.value) } : null;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Push one source, deduped by url, only when it has a usable url. */
|
|
63
|
+
function pushSource(sources, seen, item, title, snippet, publishedAt) {
|
|
64
|
+
const url = str(item.url ?? item.link ?? item.id);
|
|
65
|
+
if (url === "" || seen.has(url)) return;
|
|
66
|
+
seen.add(url);
|
|
67
|
+
sources.push({
|
|
68
|
+
url,
|
|
69
|
+
...str(title).length > 0 ? { title } : {},
|
|
70
|
+
...str(snippet).length > 0 ? { snippet } : {},
|
|
71
|
+
...str(publishedAt).length > 0 ? { publishedAt } : {}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Normalize a provider response into a `WebSearchResult` using a lenient
|
|
77
|
+
* (宽容) parser: the results array is located in the first non-empty of
|
|
78
|
+
* `organic_results` / `organic` → `web.results` → `results`, and per-item
|
|
79
|
+
* fields accept the common aliases (`url`/`link`, `content`/`snippet`/
|
|
80
|
+
* `description`/`text`, `published_date`/`date`/`page_age`/`publishedDate`).
|
|
81
|
+
* The `response` hint only influences which answer-style block becomes
|
|
82
|
+
* `content`.
|
|
83
|
+
* @param json - the parsed response body.
|
|
84
|
+
* @param response - the response-shape hint (`tavily`/`brave`/`exa`/`serper`).
|
|
85
|
+
*/
|
|
86
|
+
function mapCustomResponse(json, response) {
|
|
87
|
+
if (json === null || typeof json !== "object") throw new WebError("The provider returned a non-object response body", "WEB_PROVIDER_ERROR");
|
|
88
|
+
const seen = /* @__PURE__ */ new Set();
|
|
89
|
+
const sources = [];
|
|
90
|
+
const hint = str(response) || "tavily";
|
|
91
|
+
let results = null;
|
|
92
|
+
// Position of the results array, ordered by the common providers:
|
|
93
|
+
// Serper/Scavio/SerpApi → `organic_results` / `organic`;
|
|
94
|
+
// Firecrawl → `data.web` (v2) or bare `data` array (v1);
|
|
95
|
+
// Brave → `web.results`; Tavily/Exa/SearXNG → `results`.
|
|
96
|
+
if (hint === "firecrawl") {
|
|
97
|
+
results = Array.isArray(json?.data?.web) ? json.data.web : Array.isArray(json.data) ? json.data : null;
|
|
98
|
+
} else if (hint === "serper" || hint === "serpapi" || hint === "scavio") {
|
|
99
|
+
results = Array.isArray(json.organic_results) ? json.organic_results : Array.isArray(json.organic) ? json.organic : null;
|
|
100
|
+
} else {
|
|
101
|
+
results = Array.isArray(json.organic) ? json.organic : null;
|
|
102
|
+
}
|
|
103
|
+
if (results === null || results.length === 0) {
|
|
104
|
+
results = Array.isArray(json?.web?.results) ? json.web.results : null;
|
|
105
|
+
}
|
|
106
|
+
if (results === null || results.length === 0) {
|
|
107
|
+
results = Array.isArray(json.results) ? json.results : [];
|
|
108
|
+
}
|
|
109
|
+
for (const item of results) {
|
|
110
|
+
if (item === null || typeof item !== "object") continue;
|
|
111
|
+
const snippet = str(item.content).length > 0
|
|
112
|
+
? item.content
|
|
113
|
+
: str(item.snippet).length > 0
|
|
114
|
+
? item.snippet
|
|
115
|
+
: str(item.description).length > 0
|
|
116
|
+
? item.description
|
|
117
|
+
: str(item.text).length > 0
|
|
118
|
+
? item.text
|
|
119
|
+
: Array.isArray(item.highlights) && str(item.highlights[0]).length > 0
|
|
120
|
+
? item.highlights[0]
|
|
121
|
+
: Array.isArray(item.extra_snippets) && str(item.extra_snippets[0]).length > 0
|
|
122
|
+
? item.extra_snippets[0]
|
|
123
|
+
: "";
|
|
124
|
+
pushSource(sources, seen, item, item.title, snippet, item.published_date ?? item.date ?? item.page_age ?? item.publishedDate);
|
|
125
|
+
}
|
|
126
|
+
const answer = str(json.answer).length > 0
|
|
127
|
+
? json.answer
|
|
128
|
+
: str(json.answerBox?.answer).length > 0
|
|
129
|
+
? json.answerBox.answer
|
|
130
|
+
: str(json.knowledgeGraph?.description).length > 0
|
|
131
|
+
? json.knowledgeGraph.description
|
|
132
|
+
: str(json.knowledge_graph?.description).length > 0
|
|
133
|
+
? json.knowledge_graph.description
|
|
134
|
+
: "";
|
|
135
|
+
return {
|
|
136
|
+
...answer.length > 0 ? { content: answer } : {},
|
|
137
|
+
sources,
|
|
138
|
+
truncated: false
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generic REST backend. `resolveOptions` returns a projection with `baseURL`,
|
|
144
|
+
* `method`, `path`, `queryIn`, `queryParam`, `countParam`, `auth`, `response`,
|
|
145
|
+
* `params`, `keyed`, and optional `apiKey` / `resolveApiKey`.
|
|
146
|
+
*/
|
|
147
|
+
export class RestSearchProvider {
|
|
148
|
+
constructor(id, resolveOptions, hooks) {
|
|
149
|
+
this.id = id;
|
|
150
|
+
this.resolveOptions = resolveOptions;
|
|
151
|
+
this.hooks = hooks ?? {};
|
|
152
|
+
this.cachedProxy = void 0;
|
|
153
|
+
this.cachedDispatcher = void 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
available() {
|
|
157
|
+
const options = this.resolveOptions();
|
|
158
|
+
if (options?.baseURL == null || !URL.canParse(options.baseURL)) return false;
|
|
159
|
+
const auth = options.auth ?? { type: "none" };
|
|
160
|
+
if (auth.type === "none") return true;
|
|
161
|
+
return (options.apiKey?.length ?? 0) > 0 || options.resolveApiKey !== void 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async search(request, signal) {
|
|
165
|
+
const options = this.resolveOptions();
|
|
166
|
+
throwIfSearchAborted(signal);
|
|
167
|
+
if (options?.baseURL == null || !URL.canParse(options.baseURL)) {
|
|
168
|
+
throw new WebError(`Provider "${this.id}" has no usable endpoint`, "WEB_PROVIDER_ERROR");
|
|
169
|
+
}
|
|
170
|
+
const auth = AUTH_TYPES.has(options.auth?.type) ? options.auth : { type: "none" };
|
|
171
|
+
const method = options.method === "GET" ? "GET" : "POST";
|
|
172
|
+
const queryIn = options.queryIn === "query" ? "query" : "body";
|
|
173
|
+
const queryParam = str(options.queryParam) || "query";
|
|
174
|
+
const countParam = str(options.countParam) || "max_results";
|
|
175
|
+
const responseHint = str(options.response) || "tavily";
|
|
176
|
+
const count = Math.min(request.maxResults ?? options.maxResults ?? 8, MAX_RESULTS_CAP);
|
|
177
|
+
|
|
178
|
+
let apiKey;
|
|
179
|
+
if (auth.type !== "none") {
|
|
180
|
+
apiKey = await resolveApiKey(options, signal, `Provider "${this.id}" has no API key for "${options.apiKeyEnv ?? options.apiKeyRef ?? "key"}"; store it through the credentials service, export it in the launching environment, or fill it in the settings card`);
|
|
181
|
+
throwIfSearchAborted(signal);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Build query-string params (GET) or JSON body (POST).
|
|
185
|
+
const baseURL = options.baseURL.replace(/\/+$/u, "");
|
|
186
|
+
const path = str(options.path) || "";
|
|
187
|
+
let endpoint = baseURL + (path === "" ? "" : "/" + path.replace(/^\/+/u, ""));
|
|
188
|
+
const query = {
|
|
189
|
+
[queryParam]: request.query
|
|
190
|
+
};
|
|
191
|
+
if (countParam !== "") query[countParam] = String(count);
|
|
192
|
+
const keyed = options.keyed === true;
|
|
193
|
+
for (const param of options.params ?? []) {
|
|
194
|
+
const resolved = resolveParam(param, { keyed, ...options.settings ?? {} });
|
|
195
|
+
if (resolved !== null) query[resolved.key] = resolved.value;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const headers = {
|
|
199
|
+
accept: "application/json",
|
|
200
|
+
"user-agent": USER_AGENT
|
|
201
|
+
};
|
|
202
|
+
let body = void 0;
|
|
203
|
+
if (method === "GET") {
|
|
204
|
+
const params = new URLSearchParams();
|
|
205
|
+
for (const [key, value] of Object.entries(query)) params.set(key, value);
|
|
206
|
+
// SerpApi: the key rides the query string too.
|
|
207
|
+
if (auth.type === "query") params.set(str(auth.param) || "api_key", apiKey);
|
|
208
|
+
endpoint = `${endpoint}${endpoint.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
209
|
+
} else {
|
|
210
|
+
headers["content-type"] = "application/json";
|
|
211
|
+
body = JSON.stringify(query);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Auth headers.
|
|
215
|
+
if (auth.type === "bearer") headers["authorization"] = `Bearer ${apiKey}`;
|
|
216
|
+
else if (auth.type === "header") headers[str(auth.header) || "x-api-key"] = apiKey;
|
|
217
|
+
else if (auth.type === "query") {
|
|
218
|
+
/* handled above for GET; POST with query auth is unsupported by design */
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Keyless-mode headers (Tavily x-tavily-access-mode).
|
|
222
|
+
const keylessHeaders = options.keylessHeaders ?? {};
|
|
223
|
+
const keylessWhen = options.keylessHeaderWhen ?? "";
|
|
224
|
+
if (keylessWhen === "keyless" && keyed === false) Object.assign(headers, keylessHeaders);
|
|
225
|
+
else if (keylessWhen === "keyed" && keyed === true) Object.assign(headers, keylessHeaders);
|
|
226
|
+
else if (keylessWhen === "" || keylessWhen === "always") Object.assign(headers, keylessHeaders);
|
|
227
|
+
|
|
228
|
+
const dispatcher = this.dispatcher(options);
|
|
229
|
+
let httpResponse;
|
|
230
|
+
try {
|
|
231
|
+
httpResponse = await fetch(endpoint, {
|
|
232
|
+
method,
|
|
233
|
+
redirect: "error",
|
|
234
|
+
headers,
|
|
235
|
+
...(body !== void 0 ? { body } : {}),
|
|
236
|
+
...signal !== void 0 ? { signal } : {},
|
|
237
|
+
...dispatcher !== void 0 ? { dispatcher } : {}
|
|
238
|
+
});
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
241
|
+
throw new WebError(`Provider "${this.id}" request failed: ${String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
this.hooks.onRateLimit?.(httpResponse.headers);
|
|
245
|
+
if (!httpResponse.ok) {
|
|
246
|
+
let message = `Provider "${this.id}" API error (HTTP ${httpResponse.status})`;
|
|
247
|
+
try {
|
|
248
|
+
const parsed = await httpResponse.json();
|
|
249
|
+
const detail = parsed?.detail?.error ?? parsed?.error ?? parsed?.message ?? parsed?.search_metadata?.error;
|
|
250
|
+
if (typeof detail === "string" && detail.length > 0) message = detail;
|
|
251
|
+
else if (typeof detail === "object" && detail != null && typeof detail.message === "string") message = detail.message;
|
|
252
|
+
} catch {
|
|
253
|
+
/* non-JSON error body — keep the status-line message */
|
|
254
|
+
}
|
|
255
|
+
throw new WebError(message, "WEB_PROVIDER_ERROR");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const json = await httpResponse.json();
|
|
260
|
+
if (responseHint === "tavily" && typeof this.hooks.onUsage === "function") {
|
|
261
|
+
const credits = Number(json?.usage?.credits);
|
|
262
|
+
if (Number.isFinite(credits) && credits >= 0) this.hooks.onUsage(credits);
|
|
263
|
+
}
|
|
264
|
+
return mapCustomResponse(json, responseHint);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
267
|
+
if (error instanceof WebError) throw error;
|
|
268
|
+
throw new WebError(`Provider "${this.id}" returned an unprocessable response body: ${String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Cached undici ProxyAgent keyed by the current proxy URL. */
|
|
273
|
+
dispatcher(options) {
|
|
274
|
+
const proxy = options?.proxy;
|
|
275
|
+
if (proxy === void 0 || proxy.length === 0) return void 0;
|
|
276
|
+
if (this.cachedProxy === proxy) return this.cachedDispatcher;
|
|
277
|
+
if (this.cachedDispatcher !== void 0) {
|
|
278
|
+
this.cachedDispatcher.close?.().catch(() => {});
|
|
279
|
+
}
|
|
280
|
+
this.cachedProxy = proxy;
|
|
281
|
+
this.cachedDispatcher = new ProxyAgent(proxy);
|
|
282
|
+
return this.cachedDispatcher;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Project a provider table row + plugin section into backend options consumed
|
|
288
|
+
* by `RestSearchProvider`. `resolveSecret(ctx, ...)` supplies `apiKey` /
|
|
289
|
+
* `resolveApiKey` when the provider needs a key.
|
|
290
|
+
*
|
|
291
|
+
* @param {string} ctx - plugin context (for credential/env resolution).
|
|
292
|
+
* @param {object} row - the provider metadata row (`lib/providers.js`).
|
|
293
|
+
* @param {object} opts - `{ config, settings, keyed, keySource }`.
|
|
294
|
+
* @returns backend options for one search.
|
|
295
|
+
*/
|
|
296
|
+
export function resolveRestProvider(ctx, row, opts) {
|
|
297
|
+
const { config, settings } = opts;
|
|
298
|
+
const auth = row.auth ?? { type: "none" };
|
|
299
|
+
const keyed = opts.keyed === true;
|
|
300
|
+
const base = {
|
|
301
|
+
baseURL: row.baseURL,
|
|
302
|
+
method: row.method ?? "POST",
|
|
303
|
+
path: row.path ?? "",
|
|
304
|
+
queryIn: row.queryIn ?? "body",
|
|
305
|
+
queryParam: row.queryParam ?? "query",
|
|
306
|
+
countParam: row.countParam ?? "max_results",
|
|
307
|
+
auth,
|
|
308
|
+
response: row.response ?? "tavily",
|
|
309
|
+
params: row.params ?? [],
|
|
310
|
+
keyed,
|
|
311
|
+
keylessHeaders: row.keylessHeaders ?? {},
|
|
312
|
+
keylessHeaderWhen: row.keylessHeaderWhen ?? "",
|
|
313
|
+
settings,
|
|
314
|
+
maxResults: config.maxResults ?? 8
|
|
315
|
+
};
|
|
316
|
+
if (auth.type === "none") return base;
|
|
317
|
+
const keySource = opts.keySource ?? {};
|
|
318
|
+
return {
|
|
319
|
+
...base,
|
|
320
|
+
...resolveSecret(ctx, {
|
|
321
|
+
literal: keySource.literal,
|
|
322
|
+
envName: keySource.envName ?? row.apiKeyRef ?? ""
|
|
323
|
+
})
|
|
324
|
+
};
|
|
325
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-web-search-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Unified web search for the DeepSeek Harness seam (ctx.web): DeepSeek (official), Tavily,
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Unified web search for the DeepSeek Harness seam (ctx.web): DeepSeek (official), Tavily, Brave, Serper, SerpApi, Exa, SearXNG, Scavio, and Firecrawl backends",
|
|
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 && node --check lib/deepseek.js && node --check lib/usage.js"
|
|
56
|
+
"check": "node --check lib/index.js && node --check lib/client.js && node --check lib/shared.js && node --check lib/providers.js && node --check lib/rest.js && node --check lib/tavily.js && node --check lib/brave.js && node --check lib/deepseek.js && node --check lib/usage.js"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"undici": "^6.21.0"
|