dsh-free-search 0.4.25 → 0.4.26

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,11 +29,11 @@ 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、Perplexity、DeepSeek 官方
32
+ - **多引擎可选**:DuckDuckGo(html/lite)、Bing、SearXNG(元搜索,支持自定义实例)、AnySearch、Exa、Tavily、Keenable、Firecrawl、Parallel、Perplexity、DeepSeek 官方
33
33
  - **网页设置页** —— 引擎切换 + API key 配置(UI 中 key 脱敏显示"已配置")+ 中英文切换
34
34
  - **弹出式切换命令** —— 聊天框输入 `/free-search-engine`,弹出引擎选择窗口,点选即切换(等效设置页 + 保存)
35
35
  - **引擎测试** —— `free_search_test` 工具让 agent 一键测试所有引擎;设置页也有"测试引擎"按钮(直测当前引擎,不走回退链,付费引擎无 key 会明确报错)
36
- - **统一引擎回退** —— 任何引擎失败(付费/免费,缺 key/401/限流/网络)自动轮流尝试下一个引擎:首选引擎 → 其他引擎(exa/tavily/keenable 无 key 也会尝试,因为它们自带 keyless 免费额度)→ 剩余免费引擎,搜索永不直接失败;结果顶部注明实际生效的引擎(如 `Note: perplexity unavailable or failed, using exa.`)
36
+ - **统一引擎回退** —— 任何引擎失败(付费/免费,缺 key/401/限流/网络)自动轮流尝试下一个引擎:首选引擎 → 其他引擎(exa/tavily/keenable/firecrawl 无 key 也会尝试,因为它们自带 keyless 免费额度)→ 剩余免费引擎,搜索永不直接失败;结果顶部注明实际生效的引擎(如 `Note: perplexity unavailable or failed, using exa.`)
37
37
  - **时间过滤** —— `advanced_search` 工具支持 `timeRange`:固定档、自定义相对值、绝对日期三种形式(详见下方逻辑说明)
38
38
  - **系统提示词注入** —— agent 知道当前用哪个引擎、哪些需要 key
39
39
  - **版本号 + 检查更新** —— 设置卡片显示当前版本(v0.4.17),"检查更新"按钮直连 npm registry 对比最新版,有新版本时提示并可一键跳转
@@ -57,6 +57,8 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
57
57
  | `exa` | Exa | 免费 | **无 key 也可用**(MCP 匿名),配 key 提升额度 |
58
58
  | `tavily` | Tavily | 免费 | **无 key 也可用**(keyless 匿名),配 key 提升额度 |
59
59
  | `keenable` | Keenable | 免费 | **无 key 也可用**(MCP 匿名),配 key 提升额度 |
60
+ | `firecrawl` | Firecrawl | 免费 | **无 key 也可用**(官方免 key 匿名额度),配 key 提升限额 |
61
+ | `parallel` | Parallel | 付费 | 需 `PARALLEL_API_KEY`(platform.parallel.ai 有免费额度) |
60
62
  | `perplexity` | Perplexity | 付费 | 需 `PERPLEXITY_API_KEY` |
61
63
  | `deepseek-official` | DeepSeek 官方 | 付费 | 需 `DEEPSEEK_API_KEY` |
62
64
 
@@ -66,6 +68,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
66
68
  - Exa:<https://dashboard.exa.ai/api-keys>
67
69
  - Tavily:<https://app.tavily.com/home>
68
70
  - Keenable:<https://keenable.ai/login>
71
+ - Parallel:<https://platform.parallel.ai>
69
72
  - Perplexity:<https://www.perplexity.ai/settings/api>
70
73
  - DeepSeek:<https://platform.deepseek.com/api_keys>
71
74
 
@@ -75,6 +78,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
75
78
  - **Exa**:公开 MCP 端点(`mcp.exa.ai/mcp`)支持匿名调用,不配 key 也能用;配置 `EXA_API_KEY` 后可获得更高额度。
76
79
  - **Tavily**:通过 `x-tavily-access-mode: keyless` 头走 keyless 匿名额度,不配 key 即可用;配置 `TAVILY_API_KEY` 后走账号档,额度更高、结果质量更稳定。
77
80
  - **Keenable**:无 key 时走其公开 MCP 端点(`api.keenable.ai/mcp`)匿名调用;配置 `KEENABLE_API_KEY` 后走 REST API(`api.keenable.ai/v1/search`),额度更高、按组织限流。
81
+ - **Firecrawl**:其 `/v2/search` 端点**无需 key** 即可使用(官方文档明确说明,有匿名限流);配置 `FIRECRAWL_API_KEY` 后可提高限额。支持 `tbs` 时间过滤(`qdr:h/d/w/m/y` 与自定义日期区间)。
78
82
 
79
83
  ### 安装
80
84
 
@@ -110,7 +114,7 @@ dsh web
110
114
  安装后,打开 **设置 → 插件 → 可配置** 标签页 → **Free Search** 卡片(官方设置页):
111
115
 
112
116
  - **Search engine**:下拉框切换引擎,保存即生效
113
- - **API keys**:为 Exa / Tavily / Keenable / Perplexity / DeepSeek 填写 key(密码框,保存后只显示"已配置")
117
+ - **API keys**:为 Exa / Tavily / Keenable / Firecrawl / Parallel / Perplexity / DeepSeek 填写 key(密码框,保存后只显示"已配置")
114
118
  - **推荐**:付费引擎 key 建议写入 harness 凭据中心 `~/.dsh/.credentials.yaml`(如 `DEEPSEEK_API_KEY: sk-...`,与官方 LLM provider 一致,一处管理所有 key)。插件读取优先级:凭据中心 > 设置页 > 环境变量,设置页填的 key 仅作为遗留兼容。
115
119
  - **Test engine**:直测当前引擎可用性(不走回退链,付费引擎无 key 会明确报错)
116
120
  - **Use Bing default**:把当前搜索引擎切回稳定的免费 Bing;`Discard` 只撤销尚未保存的编辑
@@ -148,7 +152,7 @@ dsh web
148
152
 
149
153
  ```yaml
150
154
  free-search:
151
- provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / perplexity / deepseek-official
155
+ provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / deepseek-official
152
156
  lang: zh # 设置页界面语言(zh / en)
153
157
  bingMarket: zh-CN # Bing 市场
154
158
  region: cn-zh # DuckDuckGo 区域(可选)
@@ -157,6 +161,8 @@ free-search:
157
161
  exaApiKey: ... # 或通过设置页填写
158
162
  tavilyApiKey: ... # 或通过设置页填写
159
163
  keenableApiKey: ... # 或通过设置页填写
164
+ firecrawlApiKey: ... # 或通过设置页填写
165
+ parallelApiKey: ... # 或通过设置页填写
160
166
  perplexityApiKey: ...
161
167
  deepseekApiKey: ...
162
168
  ```
@@ -191,13 +197,15 @@ Search engine test:
191
197
  | Exa | `startPublishedDate` | ✅ 精确 | 自定义天数转成 ISO 日期(N 天前),绝对日期原样传入 |
192
198
  | Keenable | `published_after` | ✅ 精确 | 相对值原样传(`12h/3d/2mo/1y`),绝对日期原样传 |
193
199
  | Tavily | `time_range` | ⚠️ 近似 | 只认固定档,自定义天数自动映射到最近似档位 |
200
+ | Firecrawl | `tbs` | ⚠️ 近似 | 固定档映射到 `qdr:d/w/m/y`;绝对日期用 `cdr:1,cd_min:M/D/YYYY`(精确) |
201
+ | Parallel | `source_policy.after_date` | ✅ 精确 | 自定义天数转成 ISO 日期(N 天前),绝对日期原样传入 |
194
202
  | SearXNG | `time_range` | ⚠️ 近似 | 同上 |
195
203
  | DuckDuckGo / Lite | `df` | ⚠️ 近似 | 同上 |
196
204
  | Bing / AnySearch | — | ❌ 忽略 | 无对应参数 |
197
205
 
198
206
  **"最近似档位"映射规则**:`≤2 天 → day`,`≤14 天 → week`,`≤90 天 → month`,否则 `year`。例如 `3d` 在 Tavily 上按 `day` 处理,`2mo` 按 `month` 处理。
199
207
 
200
- **引擎链优先级**:当带 timeRange 搜索时,支持时间过滤的引擎(tavily / exa / keenable / searxng / ddg / ddg-lite)会排到引擎链前面,确保过滤真正生效——即使首选引擎是 bing(不支持过滤),也会先尝试支持过滤的引擎。
208
+ **引擎链优先级**:当带 timeRange 搜索时,支持时间过滤的引擎(tavily / exa / keenable / firecrawl / parallel / searxng / ddg / ddg-lite)会排到引擎链前面,确保过滤真正生效——即使首选引擎是 bing(不支持过滤),也会先尝试支持过滤的引擎。
201
209
 
202
210
  示例对话:*"帮我搜最近 3 天关于 DSH 的新闻"* → agent 调用 `advanced_search`,`timeRange: "3d"`。
203
211
 
@@ -282,11 +290,11 @@ This plugin provides multiple free search engines with automatic fallback, compl
282
290
  ### Features
283
291
 
284
292
  - **Zero Cost** — Multiple free engines with no API key or registration required
285
- - **Multi-Engine Support** — DuckDuckGo (HTML / Lite), Bing, AnySearch AI, SearXNG (meta-search with custom instances), Exa, Tavily, Keenable, Perplexity, and DeepSeek Official
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
286
294
  - **Web Settings UI** — Engine switching, API key configuration (keys masked as "configured" in the UI), and a Chinese/English toggle
287
295
  - **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)
288
296
  - **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)
289
- - **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 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.
297
+ - **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.
290
298
  - **Time Filtering** — The `advanced_search` tool supports `timeRange`: fixed tiers, custom relative values, or an absolute date (details below)
291
299
  - **System Prompt Injection** — The agent is aware of the currently active engine and which engines require API keys
292
300
  - **Version + Update Check** — The settings card shows the current version (v0.4.17), and a "Check update" button queries the npm registry to compare against the latest release, prompting a one-click jump when a newer version exists
@@ -310,15 +318,18 @@ If this plugin has been helpful, a ⭐ on [GitHub](https://github.com/DDDMUC/dsh
310
318
  | `exa` | Exa | Free | **Usable without a key** (anonymous MCP); configure a key for higher quota |
311
319
  | `tavily` | Tavily | Free | **Usable without a key** (keyless anonymous); configure a key for higher quota |
312
320
  | `keenable` | Keenable | Free | **Usable without a key** (anonymous MCP); configure a key for higher quota |
321
+ | `firecrawl` | Firecrawl | Free | **Usable without a key** (official keyless anonymous quota); configure a key for higher limits |
322
+ | `parallel` | Parallel | Paid | Requires `PARALLEL_API_KEY` (free tier available at platform.parallel.ai) |
313
323
  | `perplexity` | Perplexity | Paid | Requires `PERPLEXITY_API_KEY` |
314
324
  | `deepseek-official` | DeepSeek Official | Paid | Requires `DEEPSEEK_API_KEY` |
315
325
 
316
326
  - **Default engine is `bing`** (free and most stable), ready to use out of the box after installation.
317
- - **Auto-failover**: any engine failure (rate-limited free engine, or missing/invalid paid key, network error) automatically tries the next engine — the configured engine first, then other engines (exa/tavily/keenable 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 because of engine issues.
327
+ - **Auto-failover**: any engine failure (rate-limited free engine, or missing/invalid paid key, 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 because of engine issues.
318
328
  - **Official Links in Settings**: Free engines display "Visit Website →", while paid engines display "Get API Key →" (opens in a new tab):
319
329
  - Exa: <https://dashboard.exa.ai/api-keys>
320
330
  - Tavily: <https://app.tavily.com/home>
321
331
  - Keenable: <https://keenable.ai/login>
332
+ - Parallel: <https://platform.parallel.ai>
322
333
  - Perplexity: <https://www.perplexity.ai/settings/api>
323
334
  - DeepSeek: <https://platform.deepseek.com/api_keys>
324
335
 
@@ -328,6 +339,7 @@ If this plugin has been helpful, a ⭐ on [GitHub](https://github.com/DDDMUC/dsh
328
339
  - **Exa**: its public MCP endpoint (`mcp.exa.ai/mcp`) supports anonymous requests, so it works without a key; configuring `EXA_API_KEY` grants a higher usage quota.
329
340
  - **Tavily**: offers keyless anonymous quota via the `x-tavily-access-mode: keyless` header — it works without a key; configuring `TAVILY_API_KEY` switches to the account tier for higher quota and more stable results.
330
341
  - **Keenable**: without a key it is called via its public MCP endpoint (`api.keenable.ai/mcp`); configuring `KEENABLE_API_KEY` switches to the REST API (`api.keenable.ai/v1/search`) for higher quota and organization-scoped rate limits.
342
+ - **Firecrawl**: its `/v2/search` endpoint works **without a key** out of the box (the official docs state "No API key needed to get started", with anonymous rate limits); configuring `FIRECRAWL_API_KEY` raises the limits. Supports `tbs` time filtering (`qdr:h/d/w/m/y` and custom date ranges).
331
343
 
332
344
  ### Installation
333
345
 
@@ -363,7 +375,7 @@ This plugin intentionally specifies `@deepseek-ai/dsh-settings` and `@deepseek-a
363
375
  After installation, navigate to **Settings → Plugins → Configurable** tab → **Free Search** card (the official settings page):
364
376
 
365
377
  - **Search engine**: Select an engine from the dropdown; changes take effect immediately upon saving.
366
- - **API keys**: Enter keys for Exa / Tavily / Keenable / Perplexity / DeepSeek (password fields; displayed as "configured" once saved).
378
+ - **API keys**: Enter keys for Exa / Tavily / Keenable / Firecrawl / Parallel / Perplexity / DeepSeek (password fields; displayed as "configured" once saved).
367
379
  - **Recommended**: store paid-engine keys in the harness credential center `~/.dsh/.credentials.yaml` (e.g. `DEEPSEEK_API_KEY: sk-...`, same as the official LLM providers — one place for all keys). Resolution order: credentials center > settings page > environment variable; the settings-page fields remain for backward compatibility.
368
380
  - **Test engine**: Tests the selected engine directly (no fallback chain; paid engines without a key report an explicit error).
369
381
  - **Use Bing default**: stage a switch back to the stable free Bing engine; `Discard` only cancels unsaved edits
@@ -401,7 +413,7 @@ Configuration is stored in `~/.dsh/settings.yaml`:
401
413
 
402
414
  ```yaml
403
415
  free-search:
404
- provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / perplexity / deepseek-official
416
+ provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / deepseek-official
405
417
  lang: zh # settings UI language (zh / en)
406
418
  bingMarket: zh-CN # Bing market
407
419
  region: cn-zh # DuckDuckGo region (optional)
@@ -410,6 +422,8 @@ free-search:
410
422
  exaApiKey: ... # Or configure via the web settings UI
411
423
  tavilyApiKey: ... # Or configure via the web settings UI
412
424
  keenableApiKey: ... # Or configure via the web settings UI
425
+ firecrawlApiKey: ... # Or configure via the web settings UI
426
+ parallelApiKey: ... # Or configure via the web settings UI
413
427
  perplexityApiKey: ...
414
428
  deepseekApiKey: ...
415
429
  ```
@@ -444,13 +458,15 @@ Ask the agent for *"news from the last week"*, *"releases this month"*, *"update
444
458
  | Exa | `startPublishedDate` | ✅ precise | custom days become an ISO date (N days ago); absolute dates pass through |
445
459
  | Keenable | `published_after` | ✅ precise | relative values (`12h/3d/2mo/1y`) and absolute dates pass through |
446
460
  | Tavily | `time_range` | ⚠️ approximate | only fixed tiers; custom days map to the nearest tier |
461
+ | Firecrawl | `tbs` | ⚠️ approximate | fixed tiers map to `qdr:d/w/m/y`; absolute dates use `cdr:1,cd_min:M/D/YYYY` (precise) |
462
+ | Parallel | `source_policy.after_date` | ✅ precise | custom days become an ISO date (N days ago); absolute dates pass through |
447
463
  | SearXNG | `time_range` | ⚠️ approximate | same as above |
448
464
  | DuckDuckGo / Lite | `df` | ⚠️ approximate | same as above |
449
465
  | Bing / AnySearch | — | ❌ ignored | no corresponding parameter |
450
466
 
451
467
  **Nearest-tier mapping rule**: `≤2 days → day`, `≤14 days → week`, `≤90 days → month`, otherwise `year`. For example, `3d` becomes `day` on Tavily, and `2mo` becomes `month`.
452
468
 
453
- **Engine-chain priority**: when a `timeRange` is present, engines that support time filtering (tavily / exa / keenable / searxng / ddg / ddg-lite) are moved to the front of the fallback chain, so the filter actually takes effect — even if the preferred engine is bing (which does not support filtering), a filtering-capable engine is tried first.
469
+ **Engine-chain priority**: when a `timeRange` is present, engines that support time filtering (tavily / exa / keenable / firecrawl / parallel / searxng / ddg / ddg-lite) are moved to the front of the fallback chain, so the filter actually takes effect — even if the preferred engine is bing (which does not support filtering), a filtering-capable engine is tried first.
454
470
 
455
471
  Example: *"Find DSH news from the last 3 days"* → agent calls `advanced_search` with `timeRange: "3d"`.
456
472
 
package/lib/client.js CHANGED
@@ -78,7 +78,7 @@ window.__ModuleLoader__.load({
78
78
  const NS = "free-search";
79
79
  const I18N = {
80
80
  zh: {
81
- description: "免费搜索 —— 无需 API key(Bing / DuckDuckGo / AnySearch / Exa / Tavily / Keenable)",
81
+ description: "免费搜索 —— 无需 API key(Bing / DuckDuckGo / AnySearch / Exa / Tavily / Keenable / Firecrawl)",
82
82
  unsaved: "未保存",
83
83
  searchEngine: "搜索引擎",
84
84
  visit: "访问官网 →",
@@ -88,13 +88,15 @@ window.__ModuleLoader__.load({
88
88
  exaPh: (c) => c ? "Exa API 密钥(已配置)" : "Exa API 密钥(可选,不填也可免费使用)",
89
89
  tavilyPh: (c) => c ? "Tavily API 密钥(已配置)" : "Tavily API 密钥(可选,不填也可免费使用)",
90
90
  keenablePh: (c) => c ? "Keenable API 密钥(已配置)" : "Keenable API 密钥(可选,不填也可免费使用)",
91
+ firecrawlPh: (c) => c ? "Firecrawl API 密钥(已配置)" : "Firecrawl API 密钥(可选,不填也可免费使用)",
92
+ parallelPh: (c) => c ? "Parallel API 密钥(已配置)" : "Parallel API 密钥(必填,platform.parallel.ai)",
91
93
  perplexityPh: (c) => c ? "Perplexity API 密钥(已配置)" : "Perplexity API 密钥(pplx-...)",
92
94
  deepseekPh: (c) => c ? "DeepSeek API 密钥(已配置)" : "DeepSeek API 密钥(sk-...)",
93
95
  keysHint: "密钥读取优先级:.credentials.yaml 凭据中心 > 这里 > 环境变量。推荐把 key 写进凭据中心(与官方 LLM 一致,一处管理)。",
94
96
  keyStorage: "Key 存储位置",
95
97
  keyStorageCred: "凭据中心(推荐)",
96
98
  keyStorageSettings: "设置页(兼容)",
97
- keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["exa", "tavily", "keenable", "perplexity", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
99
+ keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
98
100
  keyStorageSettingsHint: "保存后写入 settings.yaml(向后兼容路径,优先级低于凭据中心)。",
99
101
  platformSearch: "平台搜索(platform_search 工具)",
100
102
  platformHint: "为 agent 的 platform_search 工具启用平台。禁用的平台会被跳过。",
@@ -141,7 +143,7 @@ window.__ModuleLoader__.load({
141
143
  marketKoKR: "ko-KR —— 韩国",
142
144
  },
143
145
  en: {
144
- description: "Free web search — no API key needed (Bing / DuckDuckGo / AnySearch / Exa / Tavily / Keenable)",
146
+ description: "Free web search — no API key needed (Bing / DuckDuckGo / AnySearch / Exa / Tavily / Keenable / Firecrawl)",
145
147
  unsaved: "unsaved",
146
148
  searchEngine: "Search engine",
147
149
  visit: "Visit website →",
@@ -151,13 +153,15 @@ window.__ModuleLoader__.load({
151
153
  exaPh: (c) => c ? "Exa API key (configured)" : "Exa API key (optional, free without)",
152
154
  tavilyPh: (c) => c ? "Tavily API key (configured)" : "Tavily API key (optional, free without)",
153
155
  keenablePh: (c) => c ? "Keenable API key (configured)" : "Keenable API key (optional, free without)",
156
+ firecrawlPh: (c) => c ? "Firecrawl API key (configured)" : "Firecrawl API key (optional, free without)",
157
+ parallelPh: (c) => c ? "Parallel API key (configured)" : "Parallel API key (required, platform.parallel.ai)",
154
158
  perplexityPh: (c) => c ? "Perplexity API key (configured)" : "Perplexity API key (pplx-...)",
155
159
  deepseekPh: (c) => c ? "DeepSeek API key (configured)" : "DeepSeek API key (sk-...)",
156
160
  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).",
157
161
  keyStorage: "Key storage",
158
162
  keyStorageCred: "Credential center (recommended)",
159
163
  keyStorageSettings: "Settings page (legacy)",
160
- keyStorageCredHint: (c) => `Saved to ~/.dsh/.credentials.yaml (highest priority). Currently configured: ${["exa", "tavily", "keenable", "perplexity", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "none"}`,
164
+ 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"}`,
161
165
  keyStorageSettingsHint: "Saved to settings.yaml (backward-compatible path; lower priority than the credential center).",
162
166
  platformSearch: "Platform search (platform_search tool)",
163
167
  platformHint: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
@@ -206,7 +210,7 @@ window.__ModuleLoader__.load({
206
210
  };
207
211
  const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
208
212
  // 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
209
- const PLUGIN_VERSION = "0.4.25";
213
+ const PLUGIN_VERSION = "0.4.26";
210
214
  const ENGINES = [
211
215
  { id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
212
216
  { id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
@@ -216,6 +220,8 @@ window.__ModuleLoader__.load({
216
220
  { id: "exa", label: "Exa", badge: "FREE", link: "https://dashboard.exa.ai/api-keys" },
217
221
  { id: "tavily", label: "Tavily", badge: "FREE", link: "https://app.tavily.com/home" },
218
222
  { id: "keenable", label: "Keenable", badge: "FREE", link: "https://keenable.ai/login" },
223
+ { id: "firecrawl", label: "Firecrawl", badge: "FREE", link: "https://www.firecrawl.dev" },
224
+ { id: "parallel", label: "Parallel", badge: "API KEY", link: "https://platform.parallel.ai" },
219
225
  { id: "perplexity", label: "Perplexity", badge: "API KEY", link: "https://www.perplexity.ai/settings/api" },
220
226
  { id: "deepseek-official", label: "DeepSeek Official", badge: "API KEY", link: "https://platform.deepseek.com/api_keys" },
221
227
  ];
@@ -312,6 +318,8 @@ window.__ModuleLoader__.load({
312
318
  const [exaKey, setExaKey] = react.useState("");
313
319
  const [tavilyKey, setTavilyKey] = react.useState("");
314
320
  const [keenableKey, setKeenableKey] = react.useState("");
321
+ const [firecrawlKey, setFirecrawlKey] = react.useState("");
322
+ const [parallelKey, setParallelKey] = react.useState("");
315
323
  const [perplexityKey, setPerplexityKey] = react.useState("");
316
324
  const [deepseekKey, setDeepseekKey] = react.useState("");
317
325
  const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
@@ -345,6 +353,8 @@ window.__ModuleLoader__.load({
345
353
  setExaKey(v.exaApiKey ?? "");
346
354
  setTavilyKey(v.tavilyApiKey ?? "");
347
355
  setKeenableKey(v.keenableApiKey ?? "");
356
+ setFirecrawlKey(v.firecrawlApiKey ?? "");
357
+ setParallelKey(v.parallelApiKey ?? "");
348
358
  setPerplexityKey(v.perplexityApiKey ?? "");
349
359
  setDeepseekKey(v.deepseekApiKey ?? "");
350
360
  setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
@@ -357,6 +367,8 @@ window.__ModuleLoader__.load({
357
367
  if (path === "exaApiKey") configured.exa = true;
358
368
  if (path === "tavilyApiKey") configured.tavily = true;
359
369
  if (path === "keenableApiKey") configured.keenable = true;
370
+ if (path === "firecrawlApiKey") configured.firecrawl = true;
371
+ if (path === "parallelApiKey") configured.parallel = true;
360
372
  if (path === "perplexityApiKey") configured.perplexity = true;
361
373
  if (path === "deepseekApiKey") configured.deepseek = true;
362
374
  }
@@ -370,7 +382,7 @@ window.__ModuleLoader__.load({
370
382
  const cred = await bridgeCredentialsStatus();
371
383
  if (cred.ok) {
372
384
  const cc = {};
373
- const map = { exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek" };
385
+ const map = { exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", firecrawlApiKey: "firecrawl", parallelApiKey: "parallel", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek" };
374
386
  for (const [k, v] of Object.entries(cred.value.configured ?? {})) {
375
387
  if (map[k]) cc[map[k]] = v;
376
388
  }
@@ -407,6 +419,8 @@ window.__ModuleLoader__.load({
407
419
  ["exaApiKey", exaKey],
408
420
  ["tavilyApiKey", tavilyKey],
409
421
  ["keenableApiKey", keenableKey],
422
+ ["firecrawlApiKey", firecrawlKey],
423
+ ["parallelApiKey", parallelKey],
410
424
  ["perplexityApiKey", perplexityKey],
411
425
  ["deepseekApiKey", deepseekKey],
412
426
  ];
@@ -710,6 +724,30 @@ react_jsx_runtime.jsx("div", {
710
724
  setFailed(false);
711
725
  },
712
726
  }),
727
+ react_jsx_runtime.jsx("input", {
728
+ className: "dshfs-input",
729
+ type: "password",
730
+ placeholder: t.firecrawlPh(keysConfigured.firecrawl),
731
+ value: firecrawlKey,
732
+ disabled: !ready || saving,
733
+ onChange: (e) => {
734
+ setFirecrawlKey(e.target.value);
735
+ setDirty(true);
736
+ setFailed(false);
737
+ },
738
+ }),
739
+ react_jsx_runtime.jsx("input", {
740
+ className: "dshfs-input",
741
+ type: "password",
742
+ placeholder: t.parallelPh(keysConfigured.parallel),
743
+ value: parallelKey,
744
+ disabled: !ready || saving,
745
+ onChange: (e) => {
746
+ setParallelKey(e.target.value);
747
+ setDirty(true);
748
+ setFailed(false);
749
+ },
750
+ }),
713
751
  react_jsx_runtime.jsx("input", {
714
752
  className: "dshfs-input",
715
753
  type: "password",
package/lib/index.js CHANGED
@@ -9,6 +9,8 @@ const DDG_HTML_URL = "https://html.duckduckgo.com/html/";
9
9
  const DDG_LITE_URL = "https://lite.duckduckgo.com/lite/";
10
10
  const BING_URL = "https://www.bing.com/search";
11
11
  const TAVILY_URL = "https://api.tavily.com/search";
12
+ const FIRECRAWL_URL = "https://api.firecrawl.dev/v2/search";
13
+ const PARALLEL_URL = "https://api.parallel.ai/v1/search";
12
14
  const KEENABLE_URL = "https://api.keenable.ai/v1/search";
13
15
  const KEENABLE_MCP_URL = "https://api.keenable.ai/mcp";
14
16
  const USER_AGENT =
@@ -45,10 +47,10 @@ const MARKET_TO_LANG = {
45
47
  const FREE_SEARCH_NS = "free-search";
46
48
  const BRIDGE_PREFIX = "/api/dsh-free-search-settings";
47
49
  const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch"];
48
- const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "perplexity", "deepseek-official"];
50
+ const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "deepseek-official"];
49
51
 
50
52
  // 当前插件版本(发布时与 package.json 同步)
51
- const PLUGIN_VERSION = "0.4.25";
53
+ const PLUGIN_VERSION = "0.4.26";
52
54
  // 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
53
55
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
54
56
  const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
@@ -862,6 +864,132 @@ async function searchTavily(query, maxResults, apiKey, timeRange, signal) {
862
864
  return { sources: uniqueSources(sources, maxResults ?? 10), truncated: false };
863
865
  }
864
866
 
867
+ // Firecrawl: 无 key 走 keyless(官方免 key 匿名额度),有 key 走账号档(Bearer)
868
+ const FIRECRAWL_TBS = { day: "qdr:d", week: "qdr:w", month: "qdr:m", year: "qdr:y" };
869
+
870
+ // Firecrawl 自定义绝对日期 → Google tbs 语法(cd_min 用 M/D/YYYY)
871
+ function formatFirecrawlDate(date) {
872
+ const [y, m, d] = String(date).split("-").map((n) => parseInt(n, 10));
873
+ return `${m}/${d}/${y}`;
874
+ }
875
+
876
+ async function searchFirecrawl(query, maxResults, apiKey, timeRange, signal) {
877
+ const controller = new AbortController();
878
+ const timer = setTimeout(() => controller.abort(), 20000);
879
+ const onAbort = () => controller.abort();
880
+ signal?.addEventListener("abort", onAbort);
881
+ let response;
882
+ try {
883
+ const body = { query, limit: Math.min(Math.max(maxResults ?? 5, 1), 10) };
884
+ // Firecrawl 时间过滤:tbs 支持固定档(qdr:d/w/m/y)与自定义绝对区间(cdr:1,cd_min:...)
885
+ if (timeRange) {
886
+ if (timeRange.after) {
887
+ body.tbs = `cdr:1,cd_min:${formatFirecrawlDate(timeRange.after)}`;
888
+ } else if (timeRange.days !== undefined) {
889
+ const tr = approximateTimeRange(timeRange.days);
890
+ if (FIRECRAWL_TBS[tr]) body.tbs = FIRECRAWL_TBS[tr];
891
+ }
892
+ }
893
+ response = await fetch(FIRECRAWL_URL, {
894
+ method: "POST",
895
+ headers: {
896
+ "content-type": "application/json",
897
+ accept: "application/json",
898
+ ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
899
+ },
900
+ body: JSON.stringify(body),
901
+ signal: controller.signal,
902
+ redirect: "error",
903
+ });
904
+ } catch (error) {
905
+ if (signal?.aborted) throw error;
906
+ throw new Error(`Firecrawl request failed: ${error?.message ?? String(error)}`);
907
+ } finally {
908
+ clearTimeout(timer);
909
+ signal?.removeEventListener("abort", onAbort);
910
+ }
911
+ if (!response.ok) {
912
+ const detail = await response.text().catch(() => "");
913
+ if (response.status === 401) {
914
+ throw new Error("Firecrawl API key is invalid (HTTP 401) - update it in Settings > Plugins > Free Search");
915
+ }
916
+ if (response.status === 429) {
917
+ throw new Error("Firecrawl rate limit exceeded (HTTP 429) - configure FIRECRAWL_API_KEY for higher limits");
918
+ }
919
+ throw new Error(`Firecrawl API error (HTTP ${response.status}): ${detail.slice(0, 200)}`);
920
+ }
921
+ const data = await response.json();
922
+ const sources = (data.data?.web ?? [])
923
+ .filter((r) => r.url)
924
+ .map((r) => ({
925
+ url: r.url,
926
+ ...(r.title ? { title: String(r.title) } : {}),
927
+ ...(r.description ? { snippet: String(r.description).slice(0, 300) } : {}),
928
+ }));
929
+ return { sources: uniqueSources(sources, maxResults ?? 10), truncated: false };
930
+ }
931
+
932
+ // Parallel: 需 PARALLEL_API_KEY(x-api-key),自然语言 objective + search_queries,返回带 excerpts 的结果
933
+ async function searchParallel(query, maxResults, apiKey, timeRange, signal) {
934
+ if (!apiKey) throw new Error("Parallel search requires PARALLEL_API_KEY");
935
+ const controller = new AbortController();
936
+ const timer = setTimeout(() => controller.abort(), 25000);
937
+ const onAbort = () => controller.abort();
938
+ signal?.addEventListener("abort", onAbort);
939
+ let response;
940
+ try {
941
+ const body = {
942
+ objective: query,
943
+ search_queries: [query],
944
+ mode: "fast",
945
+ advanced_settings: { max_results: Math.min(Math.max(maxResults ?? 5, 1), 20) },
946
+ };
947
+ // Parallel 时间过滤:source_policy.after_date(YYYY-MM-DD,精确)
948
+ if (timeRange) {
949
+ const after =
950
+ timeRange.after ??
951
+ (timeRange.days !== undefined ? isoDaysAgo(timeRange.days).slice(0, 10) : undefined);
952
+ if (after) body.advanced_settings.source_policy = { after_date: after };
953
+ }
954
+ response = await fetch(PARALLEL_URL, {
955
+ method: "POST",
956
+ headers: { "x-api-key": apiKey, "content-type": "application/json", accept: "application/json" },
957
+ body: JSON.stringify(body),
958
+ signal: controller.signal,
959
+ redirect: "error",
960
+ });
961
+ } catch (error) {
962
+ if (signal?.aborted) throw error;
963
+ throw new Error(`Parallel request failed: ${error?.message ?? String(error)}`);
964
+ } finally {
965
+ clearTimeout(timer);
966
+ signal?.removeEventListener("abort", onAbort);
967
+ }
968
+ if (!response.ok) {
969
+ const detail = await response.text().catch(() => "");
970
+ if (response.status === 401 || response.status === 403) {
971
+ throw new Error(`Parallel API key is invalid (HTTP ${response.status}) - update it in Settings > Plugins > Free Search`);
972
+ }
973
+ if (response.status === 402) {
974
+ throw new Error(`Parallel quota/billing error (HTTP 402) - check usage/credits at platform.parallel.ai. ${detail.slice(0, 200)}`);
975
+ }
976
+ throw new Error(`Parallel API error (HTTP ${response.status}): ${detail.slice(0, 200)}`);
977
+ }
978
+ const data = await response.json();
979
+ const sources = (data.results ?? [])
980
+ .filter((r) => r.url)
981
+ .map((r) => {
982
+ const excerpt = (r.excerpts ?? []).find((e) => String(e).trim().length > 0);
983
+ return {
984
+ url: r.url,
985
+ ...(r.title ? { title: String(r.title) } : {}),
986
+ ...(excerpt ? { snippet: String(excerpt).slice(0, 300) } : {}),
987
+ ...(r.publish_date ? { publishedAt: String(r.publish_date) } : {}),
988
+ };
989
+ });
990
+ return { sources: uniqueSources(sources, maxResults ?? 10), truncated: false };
991
+ }
992
+
865
993
  // 把任意天数转成 Keenable 的相对时间格式(12h / Nd / Nmo / Ny)
866
994
  function formatKeenableRelative(days) {
867
995
  if (days <= 0.5) return "12h";
@@ -1451,6 +1579,8 @@ const KEY_REF_MAP = {
1451
1579
  exaApiKey: "EXA_API_KEY",
1452
1580
  tavilyApiKey: "TAVILY_API_KEY",
1453
1581
  keenableApiKey: "KEENABLE_API_KEY",
1582
+ firecrawlApiKey: "FIRECRAWL_API_KEY",
1583
+ parallelApiKey: "PARALLEL_API_KEY",
1454
1584
  perplexityApiKey: "PERPLEXITY_API_KEY",
1455
1585
  deepseekApiKey: "DEEPSEEK_API_KEY",
1456
1586
  };
@@ -1469,6 +1599,8 @@ const Config = z.object({
1469
1599
  exaApiKey: z.string().role("secret"),
1470
1600
  tavilyApiKey: z.string().role("secret"),
1471
1601
  keenableApiKey: z.string().role("secret"),
1602
+ firecrawlApiKey: z.string().role("secret"),
1603
+ parallelApiKey: z.string().role("secret"),
1472
1604
  perplexityApiKey: z.string().role("secret"),
1473
1605
  deepseekApiKey: z.string().role("secret"),
1474
1606
  });
@@ -1543,10 +1675,10 @@ function apply(ctx, config) {
1543
1675
  }
1544
1676
 
1545
1677
  // 统一引擎链:首选优先,然后其他付费引擎(有 key 的优先尝试),最后免费引擎
1546
- const paidEngines = ["exa", "tavily", "keenable", "perplexity", "deepseek-official"];
1678
+ const paidEngines = ["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "deepseek-official"];
1547
1679
  const freeEngines = ["bing", "anysearch", "ddg", "ddg-lite", "searxng"];
1548
- // 支持 time_range 过滤的引擎:tavily / exa / keenable / searxng / ddg / ddg-lite
1549
- const timeEngines = ["tavily", "exa", "keenable", "searxng", "ddg", "ddg-lite"];
1680
+ // 支持 time_range 过滤的引擎:tavily / exa / keenable / firecrawl / parallel / searxng / ddg / ddg-lite
1681
+ const timeEngines = ["tavily", "exa", "keenable", "firecrawl", "parallel", "searxng", "ddg", "ddg-lite"];
1550
1682
  let chain;
1551
1683
  // 首选引擎被跳过的原因(用于生成准确的 Note,避免误导 agent/用户):
1552
1684
  // - "time-filter":带 timeRange 且首选引擎不支持时间过滤(根本没尝试)
@@ -1611,6 +1743,20 @@ function apply(ctx, config) {
1611
1743
  // keenable:有 key 走 REST,无 key 走 keyless MCP(免费)
1612
1744
  const key = await resolveApiKey("KEENABLE_API_KEY", "keenableApiKey");
1613
1745
  result = await searchKeenable(request.query, request.maxResults, key, timeRange, effSignal);
1746
+ } else if (engine === "firecrawl") {
1747
+ // firecrawl:无 key 也可用(官方免 key 匿名额度),有 key 走账号档(更高限额)
1748
+ const key = await resolveApiKey("FIRECRAWL_API_KEY", "firecrawlApiKey");
1749
+ result = await searchFirecrawl(request.query, request.maxResults, key, timeRange, effSignal);
1750
+ } else if (engine === "parallel") {
1751
+ // parallel:必须配置 PARALLEL_API_KEY(无 key 跳过,同 perplexity)
1752
+ const key = await resolveApiKey("PARALLEL_API_KEY", "parallelApiKey");
1753
+ if (!key) {
1754
+ lastError = new Error("Parallel requires PARALLEL_API_KEY");
1755
+ if (engine === preferred) preferredFailure = "PARALLEL_API_KEY is not configured";
1756
+ logger.warn(`free-search: engine "${engine}" skipped (no key), trying next engine`);
1757
+ continue; // 无 key 跳过
1758
+ }
1759
+ result = await searchParallel(request.query, request.maxResults, key, timeRange, effSignal);
1614
1760
  } else if (engine === "perplexity") {
1615
1761
  const key = await resolveApiKey("PERPLEXITY_API_KEY", "perplexityApiKey");
1616
1762
  if (!key) {
@@ -1768,6 +1914,15 @@ function apply(ctx, config) {
1768
1914
  const key = await resolveApiKey("KEENABLE_API_KEY", "keenableApiKey");
1769
1915
  return await searchKeenable(q, 2, key, tr);
1770
1916
  }
1917
+ case "firecrawl": {
1918
+ const key = await resolveApiKey("FIRECRAWL_API_KEY", "firecrawlApiKey");
1919
+ return await searchFirecrawl(q, 2, key, tr);
1920
+ }
1921
+ case "parallel": {
1922
+ const key = await resolveApiKey("PARALLEL_API_KEY", "parallelApiKey");
1923
+ if (!key) return { ok: false, error: "PARALLEL_API_KEY not configured" };
1924
+ return await searchParallel(q, 2, key, tr);
1925
+ }
1771
1926
  case "perplexity": {
1772
1927
  const key = await resolveApiKey("PERPLEXITY_API_KEY", "perplexityApiKey");
1773
1928
  if (!key) return { ok: false, error: "PERPLEXITY_API_KEY not configured" };
@@ -1813,7 +1968,7 @@ function apply(ctx, config) {
1813
1968
  parameters: {
1814
1969
  engines: {
1815
1970
  type: "array",
1816
- description: "Which engines to test (default: all). Options: ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, perplexity, deepseek-official.",
1971
+ description: "Which engines to test (default: all). Options: ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, firecrawl, parallel, perplexity, deepseek-official.",
1817
1972
  items: { type: "string" },
1818
1973
  },
1819
1974
  query: {
@@ -1997,7 +2152,7 @@ function apply(ctx, config) {
1997
2152
  },
1998
2153
  engine: {
1999
2154
  type: "string",
2000
- description: "Optional specific engine to try first: ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, perplexity, deepseek-official.",
2155
+ description: "Optional specific engine to try first: ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, firecrawl, parallel, perplexity, deepseek-official.",
2001
2156
  },
2002
2157
  },
2003
2158
  output: {
@@ -2094,10 +2249,12 @@ function apply(ctx, config) {
2094
2249
  "- exa - FREE keyless (MCP) or EXA_API_KEY for higher limits",
2095
2250
  "- tavily - FREE keyless or TAVILY_API_KEY for higher limits",
2096
2251
  "- keenable - FREE keyless (MCP) or KEENABLE_API_KEY for higher limits",
2252
+ "- firecrawl - FREE keyless or FIRECRAWL_API_KEY for higher limits",
2253
+ "- parallel - requires PARALLEL_API_KEY",
2097
2254
  "- perplexity - requires PERPLEXITY_API_KEY",
2098
2255
  "- deepseek-official - requires DEEPSEEK_API_KEY",
2099
2256
  "",
2100
- "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 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.",
2257
+ "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.",
2101
2258
  "",
2102
2259
  "Use the free_search_test tool to test which engines actually work right now.",
2103
2260
  "",
@@ -2127,10 +2284,12 @@ export {
2127
2284
  DDG_HTML_URL,
2128
2285
  DDG_LITE_URL,
2129
2286
  EXA_MCP_URL,
2287
+ FIRECRAWL_URL,
2130
2288
  FREE_ENGINES,
2131
2289
  FREE_SEARCH_NS,
2132
2290
  KEENABLE_MCP_URL,
2133
2291
  KEENABLE_URL,
2292
+ PARALLEL_URL,
2134
2293
  PLATFORMS,
2135
2294
  SEARXNG_INSTANCES,
2136
2295
  TAVILY_URL,
@@ -2149,12 +2308,14 @@ export {
2149
2308
  searchDdgLite,
2150
2309
  searchExa,
2151
2310
  searchExaMCP,
2311
+ searchFirecrawl,
2152
2312
  searchGithub,
2153
2313
  searchHackerNews,
2154
2314
  searchKeenable,
2155
2315
  searchKeenableMCP,
2156
2316
  searchKeenableREST,
2157
2317
  searchNpm,
2318
+ searchParallel,
2158
2319
  searchPerplexity,
2159
2320
  searchPlatform,
2160
2321
  searchReddit,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-free-search",
3
- "version": "0.4.25",
4
- "description": "Free web search for DeepSeek Harness: 10 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable keyless) + time filtering + platform search + web_fetch, with web settings UI.",
3
+ "version": "0.4.26",
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.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -24,6 +24,8 @@
24
24
  "tavily",
25
25
  "exa",
26
26
  "keenable",
27
+ "firecrawl",
28
+ "parallel",
27
29
  "perplexity",
28
30
  "search",
29
31
  "free"