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 +27 -11
- package/lib/client.js +44 -6
- package/lib/index.js +169 -8
- package/package.json +4 -2
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.
|
|
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.
|
|
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.
|
|
4
|
-
"description": "Free web search for DeepSeek Harness:
|
|
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"
|