dsh-free-search 0.4.28 → 0.4.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -8
- package/cordis.patch.yml +4 -2
- package/lib/client.js +56 -8
- package/lib/index.js +92 -6
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -29,8 +29,8 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
29
29
|
### 特性
|
|
30
30
|
|
|
31
31
|
- **零成本** —— 多个免费引擎,无需 key、无需注册
|
|
32
|
-
- **多引擎可选**:DuckDuckGo(html/lite)、Bing、SearXNG(元搜索,支持自定义实例)、AnySearch、Exa、Tavily、Keenable、Firecrawl、Parallel、Perplexity、DeepSeek 官方
|
|
33
|
-
- **网页设置页** —— 引擎切换 + API key 配置(UI 中 key 脱敏显示"已配置")+
|
|
32
|
+
- **多引擎可选**:DuckDuckGo(html/lite)、Bing、SearXNG(元搜索,支持自定义实例)、AnySearch、Exa、Tavily、Keenable、Firecrawl、Parallel、Perplexity、SerpBase、DeepSeek 官方
|
|
33
|
+
- **网页设置页** —— 引擎切换 + API key 配置(UI 中 key 脱敏显示"已配置")+ 中英文切换;DSH 0.1.6-alpha.2+ 入口在左侧「插件」页的组件行配置,旧版在「设置 → 插件 → 可配置」
|
|
34
34
|
- **弹出式切换命令** —— 聊天框输入 `/free-search-engine`,弹出引擎选择窗口,点选即切换(等效设置页 + 保存)
|
|
35
35
|
- **引擎测试** —— `free_search_test` 工具让 agent 一键测试所有引擎;设置页也有"测试引擎"按钮(直测当前引擎,不走回退链,付费引擎无 key 会明确报错)
|
|
36
36
|
- **统一引擎回退** —— 任何引擎失败(付费/免费,缺 key/401/限流/网络)自动轮流尝试下一个引擎:首选引擎 → 其他引擎(exa/tavily/keenable/firecrawl 无 key 也会尝试,因为它们自带 keyless 免费额度)→ 剩余免费引擎,搜索永不直接失败;结果顶部注明实际生效的引擎(如 `Note: perplexity unavailable or failed, using exa.`)
|
|
@@ -60,6 +60,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
60
60
|
| `firecrawl` | Firecrawl | 免费 | **无 key 也可用**(官方免 key 匿名额度),配 key 提升限额 |
|
|
61
61
|
| `parallel` | Parallel | 付费 | 需 `PARALLEL_API_KEY`(platform.parallel.ai 有免费额度) |
|
|
62
62
|
| `perplexity` | Perplexity | 付费 | 需 `PERPLEXITY_API_KEY` |
|
|
63
|
+
| `serpbase` | SerpBase | 付费 | 需 `SERPBASE_API_KEY`(serpbase.dev,注册送 100 次免费额度) |
|
|
63
64
|
| `deepseek-official` | DeepSeek 官方 | 付费 | 需 `DEEPSEEK_API_KEY` |
|
|
64
65
|
|
|
65
66
|
- **默认引擎为 `bing`**(免费且最稳定),安装后开箱即用。
|
|
@@ -70,6 +71,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
70
71
|
- Keenable:<https://keenable.ai/login>
|
|
71
72
|
- Parallel:<https://platform.parallel.ai>
|
|
72
73
|
- Perplexity:<https://www.perplexity.ai/settings/api>
|
|
74
|
+
- SerpBase:<https://serpbase.dev>
|
|
73
75
|
- DeepSeek:<https://platform.deepseek.com/api_keys>
|
|
74
76
|
|
|
75
77
|
#### 为什么免费引擎不需要 key?
|
|
@@ -111,7 +113,12 @@ dsh web
|
|
|
111
113
|
|
|
112
114
|
#### 网页设置(推荐)
|
|
113
115
|
|
|
114
|
-
|
|
116
|
+
安装后按 DSH 版本打开配置页:
|
|
117
|
+
|
|
118
|
+
- **DSH 0.1.6-alpha.2 及以上**:左侧 **插件** 页 → **已安装** 分组 → `free-search` → 点击组件行 `web-search-free`(行内"配置"入口)
|
|
119
|
+
- **DSH 0.1.5 及更早**:**设置 → 插件 → 可配置** 标签页 → **Free Search** 卡片
|
|
120
|
+
|
|
121
|
+
配置页提供:
|
|
115
122
|
|
|
116
123
|
- **Search engine**:下拉框切换引擎,保存即生效
|
|
117
124
|
- **API keys**:为 Exa / Tavily / Keenable / Firecrawl / Parallel / Perplexity / DeepSeek 填写 key(密码框,保存后只显示"已配置")
|
|
@@ -152,7 +159,7 @@ dsh web
|
|
|
152
159
|
|
|
153
160
|
```yaml
|
|
154
161
|
free-search:
|
|
155
|
-
provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / deepseek-official
|
|
162
|
+
provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / deepseek-official
|
|
156
163
|
lang: zh # 设置页界面语言(zh / en)
|
|
157
164
|
bingMarket: zh-CN # Bing 市场
|
|
158
165
|
region: cn-zh # DuckDuckGo 区域(可选)
|
|
@@ -164,6 +171,7 @@ free-search:
|
|
|
164
171
|
firecrawlApiKey: ... # 或通过设置页填写
|
|
165
172
|
parallelApiKey: ... # 或通过设置页填写
|
|
166
173
|
perplexityApiKey: ...
|
|
174
|
+
serpbaseApiKey: ... # 或通过设置页填写
|
|
167
175
|
deepseekApiKey: ...
|
|
168
176
|
```
|
|
169
177
|
|
|
@@ -290,8 +298,8 @@ This plugin provides multiple free search engines with automatic fallback, compl
|
|
|
290
298
|
### Features
|
|
291
299
|
|
|
292
300
|
- **Zero Cost** — Multiple free engines with no API key or registration required
|
|
293
|
-
- **Multi-Engine Support** — DuckDuckGo (HTML / Lite), Bing, AnySearch AI, SearXNG (meta-search with custom instances), Exa, Tavily, Keenable, Firecrawl, Parallel, Perplexity, and DeepSeek Official
|
|
294
|
-
- **Web Settings UI** — Engine switching, API key configuration (keys masked as "configured" in the UI), and a Chinese/English toggle
|
|
301
|
+
- **Multi-Engine Support** — DuckDuckGo (HTML / Lite), Bing, AnySearch AI, SearXNG (meta-search with custom instances), Exa, Tavily, Keenable, Firecrawl, Parallel, Perplexity, SerpBase, and DeepSeek Official
|
|
302
|
+
- **Web Settings UI** — Engine switching, API key configuration (keys masked as "configured" in the UI), and a Chinese/English toggle; on DSH 0.1.6-alpha.2+ the entry is the component-row config on the sidebar Plugins page, on older versions Settings → Plugins → Configurable
|
|
295
303
|
- **Popup Switch Command** — Type `/free-search-engine` in the chat: a picker opens with all engines; click one to switch (equivalent to the settings page + save)
|
|
296
304
|
- **Engine Testing** — `free_search_test` for the agent to check all engines in one call; the settings UI also has a "Test engine" button that tests the selected engine directly (no fallback chain; paid engines without a key report an explicit error)
|
|
297
305
|
- **Unified Engine Fallback** — Any engine failure (paid or free, missing key, 401, rate limit, network error) automatically tries the next engine: the configured engine first, then other engines (exa/tavily/keenable/firecrawl are tried even without a key because they have built-in keyless quota), then the remaining free engines (Bing/AnySearch etc.) — with a note attached to the results naming the engine that actually served them (e.g. `Note: perplexity unavailable or failed, using exa.`). Search never fails outright.
|
|
@@ -321,6 +329,7 @@ If this plugin has been helpful, a ⭐ on [GitHub](https://github.com/DDDMUC/dsh
|
|
|
321
329
|
| `firecrawl` | Firecrawl | Free | **Usable without a key** (official keyless anonymous quota); configure a key for higher limits |
|
|
322
330
|
| `parallel` | Parallel | Paid | Requires `PARALLEL_API_KEY` (free tier available at platform.parallel.ai) |
|
|
323
331
|
| `perplexity` | Perplexity | Paid | Requires `PERPLEXITY_API_KEY` |
|
|
332
|
+
| `serpbase` | SerpBase | Paid | Requires `SERPBASE_API_KEY` (serpbase.dev, 100 free queries on signup) |
|
|
324
333
|
| `deepseek-official` | DeepSeek Official | Paid | Requires `DEEPSEEK_API_KEY` |
|
|
325
334
|
|
|
326
335
|
- **Default engine is `bing`** (free and most stable), ready to use out of the box after installation.
|
|
@@ -331,6 +340,7 @@ If this plugin has been helpful, a ⭐ on [GitHub](https://github.com/DDDMUC/dsh
|
|
|
331
340
|
- Keenable: <https://keenable.ai/login>
|
|
332
341
|
- Parallel: <https://platform.parallel.ai>
|
|
333
342
|
- Perplexity: <https://www.perplexity.ai/settings/api>
|
|
343
|
+
- SerpBase: <https://serpbase.dev>
|
|
334
344
|
- DeepSeek: <https://platform.deepseek.com/api_keys>
|
|
335
345
|
|
|
336
346
|
#### Why are some engines free?
|
|
@@ -372,7 +382,12 @@ This plugin intentionally specifies `@deepseek-ai/dsh-settings` and `@deepseek-a
|
|
|
372
382
|
|
|
373
383
|
#### Web Settings (Recommended)
|
|
374
384
|
|
|
375
|
-
After installation,
|
|
385
|
+
After installation, open the config page according to your DSH version:
|
|
386
|
+
|
|
387
|
+
- **DSH 0.1.6-alpha.2 and later**: sidebar **Plugins** page → **Installed** group → `free-search` → click the `web-search-free` component row (the row's "configure" entry)
|
|
388
|
+
- **DSH 0.1.5 and earlier**: **Settings → Plugins → Configurable** tab → **Free Search** card
|
|
389
|
+
|
|
390
|
+
The config page provides:
|
|
376
391
|
|
|
377
392
|
- **Search engine**: Select an engine from the dropdown; changes take effect immediately upon saving.
|
|
378
393
|
- **API keys**: Enter keys for Exa / Tavily / Keenable / Firecrawl / Parallel / Perplexity / DeepSeek (password fields; displayed as "configured" once saved).
|
|
@@ -413,7 +428,7 @@ Configuration is stored in `~/.dsh/settings.yaml`:
|
|
|
413
428
|
|
|
414
429
|
```yaml
|
|
415
430
|
free-search:
|
|
416
|
-
provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / deepseek-official
|
|
431
|
+
provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / deepseek-official
|
|
417
432
|
lang: zh # settings UI language (zh / en)
|
|
418
433
|
bingMarket: zh-CN # Bing market
|
|
419
434
|
region: cn-zh # DuckDuckGo region (optional)
|
|
@@ -425,6 +440,7 @@ free-search:
|
|
|
425
440
|
firecrawlApiKey: ... # Or configure via the web settings UI
|
|
426
441
|
parallelApiKey: ... # Or configure via the web settings UI
|
|
427
442
|
perplexityApiKey: ...
|
|
443
|
+
serpbaseApiKey: ... # Or configure via the web settings UI
|
|
428
444
|
deepseekApiKey: ...
|
|
429
445
|
```
|
|
430
446
|
|
package/cordis.patch.yml
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# dsh-free-search — free web search providers (no API key needed)
|
|
2
2
|
# Registers the free-search plugin into ctx.web + a settings bridge
|
|
3
3
|
# (/api/dsh-free-search-settings) that serves the "free-search" namespace.
|
|
4
|
-
# The engine (ddg / ddg-lite / bing) is chosen from the web Settings page
|
|
5
|
-
#
|
|
4
|
+
# The engine (ddg / ddg-lite / bing) is chosen from the web Settings page:
|
|
5
|
+
# DSH 0.1.6-alpha.2+: sidebar Plugins page -> Installed -> free-search -> row config
|
|
6
|
+
# DSH 0.1.5 and earlier: Settings > Plugins > Configurable > Free Search
|
|
7
|
+
# or from settings.yaml:
|
|
6
8
|
# free-search:
|
|
7
9
|
# provider: bing
|
|
8
10
|
# region: cn-zh # DuckDuckGo region
|
package/lib/client.js
CHANGED
|
@@ -13,6 +13,10 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
".dshfs-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
|
|
14
14
|
".dshfs-header{width:100%;color:inherit;cursor:pointer;text-align:left;font:inherit;background:0 0;border:0;align-items:center;gap:8px;padding:10px 14px;display:flex}",
|
|
15
15
|
".dshfs-header:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}",
|
|
16
|
+
// 插件页 page 模式:页面自带标题,卡片固定展开、表头不可折叠
|
|
17
|
+
".dshfs-pageMode>.dshfs-header{cursor:default}",
|
|
18
|
+
".dshfs-pageMode>.dshfs-header:hover{background:0 0}",
|
|
19
|
+
".dshfs-pageMode .dshfs-chevron{display:none}",
|
|
16
20
|
".dshfs-headText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex;overflow:hidden}",
|
|
17
21
|
".dshfs-name{color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;font-weight:600;overflow:hidden}",
|
|
18
22
|
".dshfs-description{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;font-size:12px;overflow:hidden}",
|
|
@@ -96,11 +100,12 @@ window.__ModuleLoader__.load({
|
|
|
96
100
|
parallelPh: (c) => c ? "Parallel API 密钥(已配置)" : "Parallel API 密钥(必填,platform.parallel.ai)",
|
|
97
101
|
perplexityPh: (c) => c ? "Perplexity API 密钥(已配置)" : "Perplexity API 密钥(pplx-...)",
|
|
98
102
|
deepseekPh: (c) => c ? "DeepSeek API 密钥(已配置)" : "DeepSeek API 密钥(sk-...)",
|
|
103
|
+
serpbasePh: (c) => c ? "SerpBase API 密钥(已配置)" : "SerpBase API 密钥(serpbase.dev 获取)",
|
|
99
104
|
keysHint: "密钥读取优先级:.credentials.yaml 凭据中心 > 这里 > 环境变量。推荐把 key 写进凭据中心(与官方 LLM 一致,一处管理)。",
|
|
100
105
|
keyStorage: "Key 存储位置",
|
|
101
106
|
keyStorageCred: "凭据中心(推荐)",
|
|
102
107
|
keyStorageSettings: "设置页(兼容)",
|
|
103
|
-
keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
|
|
108
|
+
keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
|
|
104
109
|
keyStorageSettingsHint: "保存后写入 settings.yaml(向后兼容路径,优先级低于凭据中心)。",
|
|
105
110
|
platformSearch: "平台搜索(platform_search 工具)",
|
|
106
111
|
platformHint: "为 agent 的 platform_search 工具启用平台。禁用的平台会被跳过。",
|
|
@@ -162,11 +167,12 @@ window.__ModuleLoader__.load({
|
|
|
162
167
|
parallelPh: (c) => c ? "Parallel API key (configured)" : "Parallel API key (required, platform.parallel.ai)",
|
|
163
168
|
perplexityPh: (c) => c ? "Perplexity API key (configured)" : "Perplexity API key (pplx-...)",
|
|
164
169
|
deepseekPh: (c) => c ? "DeepSeek API key (configured)" : "DeepSeek API key (sk-...)",
|
|
170
|
+
serpbasePh: (c) => c ? "SerpBase API key (configured)" : "SerpBase API key (from serpbase.dev)",
|
|
165
171
|
keysHint: "Key resolution: .credentials.yaml credential center > here > environment variables. Recommended: store keys in the credential center (same as official LLM providers, one place for all).",
|
|
166
172
|
keyStorage: "Key storage",
|
|
167
173
|
keyStorageCred: "Credential center (recommended)",
|
|
168
174
|
keyStorageSettings: "Settings page (legacy)",
|
|
169
|
-
keyStorageCredHint: (c) => `Saved to ~/.dsh/.credentials.yaml (highest priority). Currently configured: ${["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "none"}`,
|
|
175
|
+
keyStorageCredHint: (c) => `Saved to ~/.dsh/.credentials.yaml (highest priority). Currently configured: ${["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "none"}`,
|
|
170
176
|
keyStorageSettingsHint: "Saved to settings.yaml (backward-compatible path; lower priority than the credential center).",
|
|
171
177
|
platformSearch: "Platform search (platform_search tool)",
|
|
172
178
|
platformHint: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
|
|
@@ -216,7 +222,7 @@ window.__ModuleLoader__.load({
|
|
|
216
222
|
};
|
|
217
223
|
const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
|
|
218
224
|
// 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
|
|
219
|
-
const PLUGIN_VERSION = "0.4.
|
|
225
|
+
const PLUGIN_VERSION = "0.4.29";
|
|
220
226
|
const ENGINES = [
|
|
221
227
|
{ id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
|
|
222
228
|
{ id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
|
|
@@ -229,6 +235,7 @@ window.__ModuleLoader__.load({
|
|
|
229
235
|
{ id: "firecrawl", label: "Firecrawl", badge: "FREE", link: "https://www.firecrawl.dev" },
|
|
230
236
|
{ id: "parallel", label: "Parallel", badge: "API KEY", link: "https://platform.parallel.ai" },
|
|
231
237
|
{ id: "perplexity", label: "Perplexity", badge: "API KEY", link: "https://www.perplexity.ai/settings/api" },
|
|
238
|
+
{ id: "serpbase", label: "SerpBase · Google", badge: "API KEY", link: "https://serpbase.dev" },
|
|
232
239
|
{ id: "deepseek-official", label: "DeepSeek Official", badge: "API KEY", link: "https://platform.deepseek.com/api_keys" },
|
|
233
240
|
];
|
|
234
241
|
|
|
@@ -295,6 +302,14 @@ window.__ModuleLoader__.load({
|
|
|
295
302
|
return response.json();
|
|
296
303
|
}
|
|
297
304
|
|
|
305
|
+
// 插件页行配置的摘要文案(bundle 页每行显示;不触发 bridge 读取)
|
|
306
|
+
function summaryText() {
|
|
307
|
+
const primary = typeof navigator === "undefined" ? "zh" : String((navigator.languages && navigator.languages[0]) || navigator.language || "zh").toLowerCase();
|
|
308
|
+
return primary.startsWith("en")
|
|
309
|
+
? "13 engines · time filtering · platform search · web_fetch"
|
|
310
|
+
: "13 个搜索引擎 · 时间筛选 · 平台搜索 · web_fetch";
|
|
311
|
+
}
|
|
312
|
+
|
|
298
313
|
function FreeSearchCard(props) {
|
|
299
314
|
// 检测应用主题深浅(读 body 的 --dsw-alias-bg-base 变量亮度),用于锁定原生下拉配色
|
|
300
315
|
const isDarkScheme = react.useMemo(() => {
|
|
@@ -316,7 +331,10 @@ window.__ModuleLoader__.load({
|
|
|
316
331
|
return typeof matchMedia === "function" ? matchMedia("(prefers-color-scheme: dark)").matches : false;
|
|
317
332
|
}, []);
|
|
318
333
|
const selectColorScheme = isDarkScheme ? "dark" : "light";
|
|
319
|
-
|
|
334
|
+
// 插件页(0.1.6-alpha.2+ 的 plugins.row.config)以 page 模式复用本卡片:
|
|
335
|
+
// 页面自带标题/面包屑,卡片固定展开、表头不再可折叠。
|
|
336
|
+
const pageMode = !!(props && props.page);
|
|
337
|
+
const [open, setOpen] = react.useState(pageMode);
|
|
320
338
|
const [state, setState] = react.useState({ status: "loading" });
|
|
321
339
|
const [provider, setProvider] = react.useState("bing");
|
|
322
340
|
const [safeSearch, setSafeSearch] = react.useState("off");
|
|
@@ -328,6 +346,7 @@ window.__ModuleLoader__.load({
|
|
|
328
346
|
const [parallelKey, setParallelKey] = react.useState("");
|
|
329
347
|
const [perplexityKey, setPerplexityKey] = react.useState("");
|
|
330
348
|
const [deepseekKey, setDeepseekKey] = react.useState("");
|
|
349
|
+
const [serpbaseKey, setSerpbaseKey] = react.useState("");
|
|
331
350
|
const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
|
|
332
351
|
const [cacheTtl, setCacheTtl] = react.useState(5);
|
|
333
352
|
const [keysConfigured, setKeysConfigured] = react.useState({});
|
|
@@ -363,6 +382,7 @@ window.__ModuleLoader__.load({
|
|
|
363
382
|
setParallelKey(v.parallelApiKey ?? "");
|
|
364
383
|
setPerplexityKey(v.perplexityApiKey ?? "");
|
|
365
384
|
setDeepseekKey(v.deepseekApiKey ?? "");
|
|
385
|
+
setSerpbaseKey(v.serpbaseApiKey ?? "");
|
|
366
386
|
setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
|
|
367
387
|
setCacheTtl(v.cacheTtl === undefined ? 5 : Math.min(Math.max(Number(v.cacheTtl) ?? 5, 0), 5));
|
|
368
388
|
// secrets 字段标记哪些 key 已配置(值被脱敏,仅显示"已配置")
|
|
@@ -377,6 +397,7 @@ window.__ModuleLoader__.load({
|
|
|
377
397
|
if (path === "parallelApiKey") configured.parallel = true;
|
|
378
398
|
if (path === "perplexityApiKey") configured.perplexity = true;
|
|
379
399
|
if (path === "deepseekApiKey") configured.deepseek = true;
|
|
400
|
+
if (path === "serpbaseApiKey") configured.serpbase = true;
|
|
380
401
|
}
|
|
381
402
|
}
|
|
382
403
|
setKeysConfigured(configured);
|
|
@@ -388,7 +409,7 @@ window.__ModuleLoader__.load({
|
|
|
388
409
|
const cred = await bridgeCredentialsStatus();
|
|
389
410
|
if (cred.ok) {
|
|
390
411
|
const cc = {};
|
|
391
|
-
const map = { exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", firecrawlApiKey: "firecrawl", parallelApiKey: "parallel", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek" };
|
|
412
|
+
const map = { exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", firecrawlApiKey: "firecrawl", parallelApiKey: "parallel", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek", serpbaseApiKey: "serpbase" };
|
|
392
413
|
for (const [k, v] of Object.entries(cred.value.configured ?? {})) {
|
|
393
414
|
if (map[k]) cc[map[k]] = v;
|
|
394
415
|
}
|
|
@@ -429,6 +450,7 @@ window.__ModuleLoader__.load({
|
|
|
429
450
|
["parallelApiKey", parallelKey],
|
|
430
451
|
["perplexityApiKey", perplexityKey],
|
|
431
452
|
["deepseekApiKey", deepseekKey],
|
|
453
|
+
["serpbaseApiKey", serpbaseKey],
|
|
432
454
|
];
|
|
433
455
|
let credFailed = false;
|
|
434
456
|
if (keyStorage === "credentials") {
|
|
@@ -559,13 +581,13 @@ window.__ModuleLoader__.load({
|
|
|
559
581
|
};
|
|
560
582
|
|
|
561
583
|
return react_jsx_runtime.jsx("li", {
|
|
562
|
-
className: open ? "dshfs-card dshfs-cardOpen" : "dshfs-card",
|
|
584
|
+
className: (open ? "dshfs-card dshfs-cardOpen" : "dshfs-card") + (pageMode ? " dshfs-pageMode" : ""),
|
|
563
585
|
children: [
|
|
564
586
|
react_jsx_runtime.jsx("button", {
|
|
565
587
|
type: "button",
|
|
566
588
|
className: "dshfs-header",
|
|
567
|
-
"aria-expanded": open,
|
|
568
|
-
onClick: () => setOpen(!open),
|
|
589
|
+
"aria-expanded": pageMode ? void 0 : open,
|
|
590
|
+
onClick: pageMode ? void 0 : () => setOpen(!open),
|
|
569
591
|
children: [
|
|
570
592
|
react_jsx_runtime.jsx("span", { className: "dshfs-headText", children: [
|
|
571
593
|
react_jsx_runtime.jsx("span", { className: "dshfs-name", children: title }),
|
|
@@ -766,6 +788,18 @@ react_jsx_runtime.jsx("div", {
|
|
|
766
788
|
setFailed(false);
|
|
767
789
|
},
|
|
768
790
|
}),
|
|
791
|
+
react_jsx_runtime.jsx("input", {
|
|
792
|
+
className: "dshfs-input",
|
|
793
|
+
type: "password",
|
|
794
|
+
placeholder: t.serpbasePh(keysConfigured.serpbase),
|
|
795
|
+
value: serpbaseKey,
|
|
796
|
+
disabled: !ready || saving,
|
|
797
|
+
onChange: (e) => {
|
|
798
|
+
setSerpbaseKey(e.target.value);
|
|
799
|
+
setDirty(true);
|
|
800
|
+
setFailed(false);
|
|
801
|
+
},
|
|
802
|
+
}),
|
|
769
803
|
react_jsx_runtime.jsx("input", {
|
|
770
804
|
className: "dshfs-input",
|
|
771
805
|
type: "password",
|
|
@@ -1050,6 +1084,20 @@ react_jsx_runtime.jsx("div", {
|
|
|
1050
1084
|
FreeSearchCard
|
|
1051
1085
|
)
|
|
1052
1086
|
);
|
|
1087
|
+
// 挂 0.1.6-alpha.2+ 的插件页 slot:侧栏 插件 → 已安装 → free-search → 行配置页。
|
|
1088
|
+
// key 规则见 ui-plugin-manager 的 rowConfigKey:<包名>#<patch 里声明的行 id>。
|
|
1089
|
+
ctx.slots.inject("plugins.row.config", () =>
|
|
1090
|
+
ctx.slots.register(
|
|
1091
|
+
{
|
|
1092
|
+
name: "plugins.row.config",
|
|
1093
|
+
key: "dsh-free-search#web-search-free",
|
|
1094
|
+
},
|
|
1095
|
+
(slotProps) =>
|
|
1096
|
+
slotProps && slotProps.view === "summary"
|
|
1097
|
+
? summaryText()
|
|
1098
|
+
: react_jsx_runtime.jsx(FreeSearchCard, { page: true })
|
|
1099
|
+
)
|
|
1100
|
+
);
|
|
1053
1101
|
// /free-search-engine 弹出式命令:输入 "/" 选中后弹出引擎列表,点选即切换。
|
|
1054
1102
|
// 等效于设置页切换引擎+保存;命令只改 provider 配置,搜索仍走回退链。
|
|
1055
1103
|
// description 必须传函数:ui-commands 读回的是 contribution.description(),
|
package/lib/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const FIRECRAWL_URL = "https://api.firecrawl.dev/v2/search";
|
|
|
13
13
|
const PARALLEL_URL = "https://api.parallel.ai/v1/search";
|
|
14
14
|
const KEENABLE_URL = "https://api.keenable.ai/v1/search";
|
|
15
15
|
const KEENABLE_MCP_URL = "https://api.keenable.ai/mcp";
|
|
16
|
+
const SERPBASE_URL = "https://api.serpbase.dev/google/search";
|
|
16
17
|
const USER_AGENT =
|
|
17
18
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
|
|
18
19
|
const ACCEPT_LANG = "zh-CN,zh;q=0.9,en;q=0.8";
|
|
@@ -47,10 +48,10 @@ const MARKET_TO_LANG = {
|
|
|
47
48
|
const FREE_SEARCH_NS = "free-search";
|
|
48
49
|
const BRIDGE_PREFIX = "/api/dsh-free-search-settings";
|
|
49
50
|
const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch"];
|
|
50
|
-
const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "deepseek-official"];
|
|
51
|
+
const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek-official"];
|
|
51
52
|
|
|
52
53
|
// 当前插件版本(发布时与 package.json 同步)
|
|
53
|
-
const PLUGIN_VERSION = "0.4.
|
|
54
|
+
const PLUGIN_VERSION = "0.4.29";
|
|
54
55
|
// 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
|
|
55
56
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
|
|
56
57
|
const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
|
|
@@ -758,7 +759,7 @@ async function searchPlatform(platform, query, maxResults, signal, lang) {
|
|
|
758
759
|
}
|
|
759
760
|
//#endregion
|
|
760
761
|
|
|
761
|
-
//#region paid engines (exa / tavily / perplexity / deepseek-official)
|
|
762
|
+
//#region paid engines (exa / tavily / perplexity / serpbase / deepseek-official)
|
|
762
763
|
async function searchExa(query, maxResults, apiKey, timeRange, signal) {
|
|
763
764
|
if (!apiKey) throw new Error("Exa search requires EXA_API_KEY");
|
|
764
765
|
const body = {
|
|
@@ -1206,6 +1207,72 @@ async function searchDeepSeekOfficial(query, maxResults, apiKey, signal) {
|
|
|
1206
1207
|
}
|
|
1207
1208
|
return { sources: uniqueSources(sources, maxResults ?? 10), truncated: false };
|
|
1208
1209
|
}
|
|
1210
|
+
|
|
1211
|
+
// SerpBase: 必须配置 SERPBASE_API_KEY(无 key 跳过,同 perplexity)
|
|
1212
|
+
// Google SERP API:POST + X-API-Key 头,返回 organic[](title/link/snippet/published_at)。
|
|
1213
|
+
// 注意:SerpBase 始终返回 HTTP 200,业务状态放在 JSON 的 status 字段(0=成功,1001=key 无效/缺失,1000=请求非法)。
|
|
1214
|
+
const SERPBASE_LOCALE = {
|
|
1215
|
+
zh: { hl: "zh-CN", gl: "cn" },
|
|
1216
|
+
en: { hl: "en", gl: "us" },
|
|
1217
|
+
ru: { hl: "ru", gl: "ru" },
|
|
1218
|
+
ja: { hl: "ja", gl: "jp" },
|
|
1219
|
+
de: { hl: "de", gl: "de" },
|
|
1220
|
+
fr: { hl: "fr", gl: "fr" },
|
|
1221
|
+
es: { hl: "es", gl: "es" },
|
|
1222
|
+
ko: { hl: "ko", gl: "kr" },
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
async function searchSerpbase(query, maxResults, apiKey, options, signal) {
|
|
1226
|
+
if (!apiKey) throw new Error("SerpBase search requires SERPBASE_API_KEY");
|
|
1227
|
+
// hl/gl 跟随设置页的 lang(SerpBase 支持 Google 的 hl/gl 本地化),默认 en/us
|
|
1228
|
+
const locale = SERPBASE_LOCALE[options?.lang] ?? SERPBASE_LOCALE.en;
|
|
1229
|
+
const controller = new AbortController();
|
|
1230
|
+
const timer = setTimeout(() => controller.abort(), 15000);
|
|
1231
|
+
const onAbort = () => controller.abort();
|
|
1232
|
+
signal?.addEventListener("abort", onAbort);
|
|
1233
|
+
let response;
|
|
1234
|
+
try {
|
|
1235
|
+
response = await fetch(SERPBASE_URL, {
|
|
1236
|
+
method: "POST",
|
|
1237
|
+
redirect: "error",
|
|
1238
|
+
headers: {
|
|
1239
|
+
"X-API-Key": apiKey,
|
|
1240
|
+
"content-type": "application/json",
|
|
1241
|
+
accept: "application/json",
|
|
1242
|
+
"user-agent": "deepseek-harness/free-search",
|
|
1243
|
+
},
|
|
1244
|
+
body: JSON.stringify({ q: query, hl: locale.hl, gl: locale.gl, page: 1 }),
|
|
1245
|
+
signal: controller.signal,
|
|
1246
|
+
});
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
if (signal?.aborted) throw error;
|
|
1249
|
+
throw new Error(`SerpBase request failed: ${error?.message ?? String(error)}`);
|
|
1250
|
+
} finally {
|
|
1251
|
+
clearTimeout(timer);
|
|
1252
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1253
|
+
}
|
|
1254
|
+
if (!response.ok) {
|
|
1255
|
+
const detail = await response.text().catch(() => "");
|
|
1256
|
+
throw new Error(`SerpBase API error (HTTP ${response.status}): ${detail.slice(0, 200)}`);
|
|
1257
|
+
}
|
|
1258
|
+
// HTTP 200 + status 字段:0 成功 / 1001 key 无效或缺 / 1000 请求非法
|
|
1259
|
+
const data = await response.json();
|
|
1260
|
+
if (data.status !== 0) {
|
|
1261
|
+
if (data.status === 1001) {
|
|
1262
|
+
throw new Error("SerpBase API key is invalid or missing (status 1001) - update it in Settings > Plugins > Free Search");
|
|
1263
|
+
}
|
|
1264
|
+
throw new Error(`SerpBase API error (status ${data.status}): ${String(data.error ?? "").slice(0, 200)}`);
|
|
1265
|
+
}
|
|
1266
|
+
const sources = (data.organic ?? [])
|
|
1267
|
+
.filter((r) => r.link)
|
|
1268
|
+
.map((r) => ({
|
|
1269
|
+
url: r.link,
|
|
1270
|
+
...(r.title ? { title: String(r.title) } : {}),
|
|
1271
|
+
...(r.snippet ? { snippet: String(r.snippet) } : {}),
|
|
1272
|
+
...((r.published_at ?? r.date) ? { publishedAt: String(r.published_at ?? r.date) } : {}),
|
|
1273
|
+
}));
|
|
1274
|
+
return { sources: uniqueSources(sources, maxResults ?? 10), truncated: false };
|
|
1275
|
+
}
|
|
1209
1276
|
//#endregion
|
|
1210
1277
|
|
|
1211
1278
|
//#region bridge
|
|
@@ -1582,6 +1649,7 @@ const KEY_REF_MAP = {
|
|
|
1582
1649
|
firecrawlApiKey: "FIRECRAWL_API_KEY",
|
|
1583
1650
|
parallelApiKey: "PARALLEL_API_KEY",
|
|
1584
1651
|
perplexityApiKey: "PERPLEXITY_API_KEY",
|
|
1652
|
+
serpbaseApiKey: "SERPBASE_API_KEY",
|
|
1585
1653
|
deepseekApiKey: "DEEPSEEK_API_KEY",
|
|
1586
1654
|
};
|
|
1587
1655
|
|
|
@@ -1602,6 +1670,7 @@ const Config = z.object({
|
|
|
1602
1670
|
firecrawlApiKey: z.string().role("secret"),
|
|
1603
1671
|
parallelApiKey: z.string().role("secret"),
|
|
1604
1672
|
perplexityApiKey: z.string().role("secret"),
|
|
1673
|
+
serpbaseApiKey: z.string().role("secret"),
|
|
1605
1674
|
deepseekApiKey: z.string().role("secret"),
|
|
1606
1675
|
});
|
|
1607
1676
|
|
|
@@ -1675,7 +1744,7 @@ function apply(ctx, config) {
|
|
|
1675
1744
|
}
|
|
1676
1745
|
|
|
1677
1746
|
// 统一引擎链:首选优先,然后其他付费引擎(有 key 的优先尝试),最后免费引擎
|
|
1678
|
-
const paidEngines = ["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "deepseek-official"];
|
|
1747
|
+
const paidEngines = ["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek-official"];
|
|
1679
1748
|
const freeEngines = ["bing", "anysearch", "ddg", "ddg-lite", "searxng"];
|
|
1680
1749
|
// 支持 time_range 过滤的引擎:tavily / exa / keenable / firecrawl / parallel / searxng / ddg / ddg-lite
|
|
1681
1750
|
const timeEngines = ["tavily", "exa", "keenable", "firecrawl", "parallel", "searxng", "ddg", "ddg-lite"];
|
|
@@ -1775,6 +1844,16 @@ function apply(ctx, config) {
|
|
|
1775
1844
|
continue; // 无 key 跳过
|
|
1776
1845
|
}
|
|
1777
1846
|
result = await searchDeepSeekOfficial(request.query, request.maxResults, key, effSignal);
|
|
1847
|
+
} else if (engine === "serpbase") {
|
|
1848
|
+
// serpbase:必须配置 SERPBASE_API_KEY(无 key 跳过,同 perplexity)
|
|
1849
|
+
const key = await resolveApiKey("SERPBASE_API_KEY", "serpbaseApiKey");
|
|
1850
|
+
if (!key) {
|
|
1851
|
+
lastError = new Error("SerpBase requires SERPBASE_API_KEY");
|
|
1852
|
+
if (engine === preferred) preferredFailure = "SERPBASE_API_KEY is not configured";
|
|
1853
|
+
logger.warn(`free-search: engine "${engine}" skipped (no key), trying next engine`);
|
|
1854
|
+
continue; // 无 key 跳过
|
|
1855
|
+
}
|
|
1856
|
+
result = await searchSerpbase(request.query, request.maxResults, key, cfg, effSignal);
|
|
1778
1857
|
} else {
|
|
1779
1858
|
continue;
|
|
1780
1859
|
}
|
|
@@ -1933,6 +2012,11 @@ function apply(ctx, config) {
|
|
|
1933
2012
|
if (!key) return { ok: false, error: "DEEPSEEK_API_KEY not configured" };
|
|
1934
2013
|
return await searchDeepSeekOfficial(q, 2, key);
|
|
1935
2014
|
}
|
|
2015
|
+
case "serpbase": {
|
|
2016
|
+
const key = await resolveApiKey("SERPBASE_API_KEY", "serpbaseApiKey");
|
|
2017
|
+
if (!key) return { ok: false, error: "SERPBASE_API_KEY not configured" };
|
|
2018
|
+
return await searchSerpbase(q, 2, key, cfg);
|
|
2019
|
+
}
|
|
1936
2020
|
default:
|
|
1937
2021
|
return { ok: false, error: `unknown engine: ${engine}` };
|
|
1938
2022
|
}
|
|
@@ -1968,7 +2052,7 @@ function apply(ctx, config) {
|
|
|
1968
2052
|
parameters: {
|
|
1969
2053
|
engines: {
|
|
1970
2054
|
type: "array",
|
|
1971
|
-
description: "Which engines to test (default: all). Options: ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, firecrawl, parallel, perplexity, deepseek-official.",
|
|
2055
|
+
description: "Which engines to test (default: all). Options: ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, firecrawl, parallel, perplexity, serpbase, deepseek-official.",
|
|
1972
2056
|
items: { type: "string" },
|
|
1973
2057
|
},
|
|
1974
2058
|
query: {
|
|
@@ -2152,7 +2236,7 @@ function apply(ctx, config) {
|
|
|
2152
2236
|
},
|
|
2153
2237
|
engine: {
|
|
2154
2238
|
type: "string",
|
|
2155
|
-
description: "Optional specific engine to try first: ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, firecrawl, parallel, perplexity, deepseek-official.",
|
|
2239
|
+
description: "Optional specific engine to try first: ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, firecrawl, parallel, perplexity, serpbase, deepseek-official.",
|
|
2156
2240
|
},
|
|
2157
2241
|
},
|
|
2158
2242
|
output: {
|
|
@@ -2253,6 +2337,7 @@ function apply(ctx, config) {
|
|
|
2253
2337
|
"- parallel - requires PARALLEL_API_KEY",
|
|
2254
2338
|
"- perplexity - requires PERPLEXITY_API_KEY",
|
|
2255
2339
|
"- deepseek-official - requires DEEPSEEK_API_KEY",
|
|
2340
|
+
"- serpbase - Google organic results via API, requires SERPBASE_API_KEY (100 free queries on signup)",
|
|
2256
2341
|
"",
|
|
2257
2342
|
"IMPORTANT: If the configured engine fails (missing key, invalid key, 401, rate limit, or network error), web_search automatically tries other engines in this order: (1) the configured engine first, (2) then other engines with API keys configured (exa/tavily/keenable/firecrawl work keyless too, so they are tried even without a key), (3) then the remaining free engines (Bing, AnySearch, DuckDuckGo, SearXNG). This applies to ALL engines - paid or free. The results include a note showing which engine was actually used and why the preferred one was skipped. Understand the two note forms: (a) 'Note: X does not support time filtering (timeRange=...), using Y.' means X cannot filter by time so it was skipped BEFORE any attempt (X did NOT fail); (b) 'Note: X unavailable or failed (reason), using Y.' means X was actually tried but failed (missing key / invalid key / 401 / rate limit / network / 0 results). Never tell the user search is unavailable - it always falls back.",
|
|
2258
2343
|
"",
|
|
@@ -2320,6 +2405,7 @@ export {
|
|
|
2320
2405
|
searchPlatform,
|
|
2321
2406
|
searchReddit,
|
|
2322
2407
|
searchSearxng,
|
|
2408
|
+
searchSerpbase,
|
|
2323
2409
|
searchStackOverflow,
|
|
2324
2410
|
searchTavily,
|
|
2325
2411
|
searchV2ex,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-free-search",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Free web search for DeepSeek Harness:
|
|
3
|
+
"version": "0.4.30",
|
|
4
|
+
"description": "Free web search for DeepSeek Harness: 13 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable/Firecrawl keyless; Parallel/Perplexity/SerpBase/DeepSeek with key) + time filtering + platform search + web_fetch, with web settings UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"firecrawl",
|
|
28
28
|
"parallel",
|
|
29
29
|
"perplexity",
|
|
30
|
+
"serpbase",
|
|
30
31
|
"search",
|
|
31
32
|
"free"
|
|
32
33
|
],
|