dsh-free-search 0.4.28 → 0.4.29

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 CHANGED
@@ -29,7 +29,7 @@ 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 官方
32
+ - **多引擎可选**:DuckDuckGo(html/lite)、Bing、SearXNG(元搜索,支持自定义实例)、AnySearch、Exa、Tavily、Keenable、Firecrawl、Parallel、Perplexity、SerpBase、DeepSeek 官方
33
33
  - **网页设置页** —— 引擎切换 + API key 配置(UI 中 key 脱敏显示"已配置")+ 中英文切换
34
34
  - **弹出式切换命令** —— 聊天框输入 `/free-search-engine`,弹出引擎选择窗口,点选即切换(等效设置页 + 保存)
35
35
  - **引擎测试** —— `free_search_test` 工具让 agent 一键测试所有引擎;设置页也有"测试引擎"按钮(直测当前引擎,不走回退链,付费引擎无 key 会明确报错)
@@ -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?
@@ -152,7 +154,7 @@ dsh web
152
154
 
153
155
  ```yaml
154
156
  free-search:
155
- provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / deepseek-official
157
+ provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / deepseek-official
156
158
  lang: zh # 设置页界面语言(zh / en)
157
159
  bingMarket: zh-CN # Bing 市场
158
160
  region: cn-zh # DuckDuckGo 区域(可选)
@@ -164,6 +166,7 @@ free-search:
164
166
  firecrawlApiKey: ... # 或通过设置页填写
165
167
  parallelApiKey: ... # 或通过设置页填写
166
168
  perplexityApiKey: ...
169
+ serpbaseApiKey: ... # 或通过设置页填写
167
170
  deepseekApiKey: ...
168
171
  ```
169
172
 
@@ -290,7 +293,7 @@ This plugin provides multiple free search engines with automatic fallback, compl
290
293
  ### Features
291
294
 
292
295
  - **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
296
+ - **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
294
297
  - **Web Settings UI** — Engine switching, API key configuration (keys masked as "configured" in the UI), and a Chinese/English toggle
295
298
  - **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
299
  - **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)
@@ -321,6 +324,7 @@ If this plugin has been helpful, a ⭐ on [GitHub](https://github.com/DDDMUC/dsh
321
324
  | `firecrawl` | Firecrawl | Free | **Usable without a key** (official keyless anonymous quota); configure a key for higher limits |
322
325
  | `parallel` | Parallel | Paid | Requires `PARALLEL_API_KEY` (free tier available at platform.parallel.ai) |
323
326
  | `perplexity` | Perplexity | Paid | Requires `PERPLEXITY_API_KEY` |
327
+ | `serpbase` | SerpBase | Paid | Requires `SERPBASE_API_KEY` (serpbase.dev, 100 free queries on signup) |
324
328
  | `deepseek-official` | DeepSeek Official | Paid | Requires `DEEPSEEK_API_KEY` |
325
329
 
326
330
  - **Default engine is `bing`** (free and most stable), ready to use out of the box after installation.
@@ -331,6 +335,7 @@ If this plugin has been helpful, a ⭐ on [GitHub](https://github.com/DDDMUC/dsh
331
335
  - Keenable: <https://keenable.ai/login>
332
336
  - Parallel: <https://platform.parallel.ai>
333
337
  - Perplexity: <https://www.perplexity.ai/settings/api>
338
+ - SerpBase: <https://serpbase.dev>
334
339
  - DeepSeek: <https://platform.deepseek.com/api_keys>
335
340
 
336
341
  #### Why are some engines free?
@@ -413,7 +418,7 @@ Configuration is stored in `~/.dsh/settings.yaml`:
413
418
 
414
419
  ```yaml
415
420
  free-search:
416
- provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / deepseek-official
421
+ provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / deepseek-official
417
422
  lang: zh # settings UI language (zh / en)
418
423
  bingMarket: zh-CN # Bing market
419
424
  region: cn-zh # DuckDuckGo region (optional)
@@ -425,6 +430,7 @@ free-search:
425
430
  firecrawlApiKey: ... # Or configure via the web settings UI
426
431
  parallelApiKey: ... # Or configure via the web settings UI
427
432
  perplexityApiKey: ...
433
+ serpbaseApiKey: ... # Or configure via the web settings UI
428
434
  deepseekApiKey: ...
429
435
  ```
430
436
 
package/lib/client.js CHANGED
@@ -96,11 +96,12 @@ window.__ModuleLoader__.load({
96
96
  parallelPh: (c) => c ? "Parallel API 密钥(已配置)" : "Parallel API 密钥(必填,platform.parallel.ai)",
97
97
  perplexityPh: (c) => c ? "Perplexity API 密钥(已配置)" : "Perplexity API 密钥(pplx-...)",
98
98
  deepseekPh: (c) => c ? "DeepSeek API 密钥(已配置)" : "DeepSeek API 密钥(sk-...)",
99
+ serpbasePh: (c) => c ? "SerpBase API 密钥(已配置)" : "SerpBase API 密钥(serpbase.dev 获取)",
99
100
  keysHint: "密钥读取优先级:.credentials.yaml 凭据中心 > 这里 > 环境变量。推荐把 key 写进凭据中心(与官方 LLM 一致,一处管理)。",
100
101
  keyStorage: "Key 存储位置",
101
102
  keyStorageCred: "凭据中心(推荐)",
102
103
  keyStorageSettings: "设置页(兼容)",
103
- keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
104
+ keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
104
105
  keyStorageSettingsHint: "保存后写入 settings.yaml(向后兼容路径,优先级低于凭据中心)。",
105
106
  platformSearch: "平台搜索(platform_search 工具)",
106
107
  platformHint: "为 agent 的 platform_search 工具启用平台。禁用的平台会被跳过。",
@@ -162,11 +163,12 @@ window.__ModuleLoader__.load({
162
163
  parallelPh: (c) => c ? "Parallel API key (configured)" : "Parallel API key (required, platform.parallel.ai)",
163
164
  perplexityPh: (c) => c ? "Perplexity API key (configured)" : "Perplexity API key (pplx-...)",
164
165
  deepseekPh: (c) => c ? "DeepSeek API key (configured)" : "DeepSeek API key (sk-...)",
166
+ serpbasePh: (c) => c ? "SerpBase API key (configured)" : "SerpBase API key (from serpbase.dev)",
165
167
  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
168
  keyStorage: "Key storage",
167
169
  keyStorageCred: "Credential center (recommended)",
168
170
  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"}`,
171
+ 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
172
  keyStorageSettingsHint: "Saved to settings.yaml (backward-compatible path; lower priority than the credential center).",
171
173
  platformSearch: "Platform search (platform_search tool)",
172
174
  platformHint: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
@@ -216,7 +218,7 @@ window.__ModuleLoader__.load({
216
218
  };
217
219
  const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
218
220
  // 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
219
- const PLUGIN_VERSION = "0.4.28";
221
+ const PLUGIN_VERSION = "0.4.29";
220
222
  const ENGINES = [
221
223
  { id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
222
224
  { id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
@@ -229,6 +231,7 @@ window.__ModuleLoader__.load({
229
231
  { id: "firecrawl", label: "Firecrawl", badge: "FREE", link: "https://www.firecrawl.dev" },
230
232
  { id: "parallel", label: "Parallel", badge: "API KEY", link: "https://platform.parallel.ai" },
231
233
  { id: "perplexity", label: "Perplexity", badge: "API KEY", link: "https://www.perplexity.ai/settings/api" },
234
+ { id: "serpbase", label: "SerpBase · Google", badge: "API KEY", link: "https://serpbase.dev" },
232
235
  { id: "deepseek-official", label: "DeepSeek Official", badge: "API KEY", link: "https://platform.deepseek.com/api_keys" },
233
236
  ];
234
237
 
@@ -328,6 +331,7 @@ window.__ModuleLoader__.load({
328
331
  const [parallelKey, setParallelKey] = react.useState("");
329
332
  const [perplexityKey, setPerplexityKey] = react.useState("");
330
333
  const [deepseekKey, setDeepseekKey] = react.useState("");
334
+ const [serpbaseKey, setSerpbaseKey] = react.useState("");
331
335
  const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
332
336
  const [cacheTtl, setCacheTtl] = react.useState(5);
333
337
  const [keysConfigured, setKeysConfigured] = react.useState({});
@@ -363,6 +367,7 @@ window.__ModuleLoader__.load({
363
367
  setParallelKey(v.parallelApiKey ?? "");
364
368
  setPerplexityKey(v.perplexityApiKey ?? "");
365
369
  setDeepseekKey(v.deepseekApiKey ?? "");
370
+ setSerpbaseKey(v.serpbaseApiKey ?? "");
366
371
  setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
367
372
  setCacheTtl(v.cacheTtl === undefined ? 5 : Math.min(Math.max(Number(v.cacheTtl) ?? 5, 0), 5));
368
373
  // secrets 字段标记哪些 key 已配置(值被脱敏,仅显示"已配置")
@@ -377,6 +382,7 @@ window.__ModuleLoader__.load({
377
382
  if (path === "parallelApiKey") configured.parallel = true;
378
383
  if (path === "perplexityApiKey") configured.perplexity = true;
379
384
  if (path === "deepseekApiKey") configured.deepseek = true;
385
+ if (path === "serpbaseApiKey") configured.serpbase = true;
380
386
  }
381
387
  }
382
388
  setKeysConfigured(configured);
@@ -388,7 +394,7 @@ window.__ModuleLoader__.load({
388
394
  const cred = await bridgeCredentialsStatus();
389
395
  if (cred.ok) {
390
396
  const cc = {};
391
- const map = { exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", firecrawlApiKey: "firecrawl", parallelApiKey: "parallel", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek" };
397
+ const map = { exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", firecrawlApiKey: "firecrawl", parallelApiKey: "parallel", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek", serpbaseApiKey: "serpbase" };
392
398
  for (const [k, v] of Object.entries(cred.value.configured ?? {})) {
393
399
  if (map[k]) cc[map[k]] = v;
394
400
  }
@@ -429,6 +435,7 @@ window.__ModuleLoader__.load({
429
435
  ["parallelApiKey", parallelKey],
430
436
  ["perplexityApiKey", perplexityKey],
431
437
  ["deepseekApiKey", deepseekKey],
438
+ ["serpbaseApiKey", serpbaseKey],
432
439
  ];
433
440
  let credFailed = false;
434
441
  if (keyStorage === "credentials") {
@@ -766,6 +773,18 @@ react_jsx_runtime.jsx("div", {
766
773
  setFailed(false);
767
774
  },
768
775
  }),
776
+ react_jsx_runtime.jsx("input", {
777
+ className: "dshfs-input",
778
+ type: "password",
779
+ placeholder: t.serpbasePh(keysConfigured.serpbase),
780
+ value: serpbaseKey,
781
+ disabled: !ready || saving,
782
+ onChange: (e) => {
783
+ setSerpbaseKey(e.target.value);
784
+ setDirty(true);
785
+ setFailed(false);
786
+ },
787
+ }),
769
788
  react_jsx_runtime.jsx("input", {
770
789
  className: "dshfs-input",
771
790
  type: "password",
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.28";
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.28",
4
- "description": "Free web search for DeepSeek Harness: 12 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable/Firecrawl keyless; Parallel/Perplexity/DeepSeek with key) + time filtering + platform search + web_fetch, with web settings UI.",
3
+ "version": "0.4.29",
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
  ],