dsh-free-search 0.5.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -6
- package/cordis.patch.yml +14 -11
- package/lib/client.js +24 -4
- package/lib/index.js +264 -14
- package/package.json +6 -3
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、SerpBase、DeepSeek 官方、You.com
|
|
32
|
+
- **多引擎可选**:DuckDuckGo(html/lite)、Bing、SearXNG(元搜索,支持自定义实例)、AnySearch、Exa、Tavily、Keenable、Firecrawl、Parallel、Perplexity、SerpBase、DeepSeek 官方、You.com、百度千帆、Kimi、阿里云百炼、火山联网搜索(豆包搜索)
|
|
33
33
|
- **网页设置页** —— 引擎切换 + API key 配置(UI 中 key 脱敏显示"已配置")+ 中英文切换;入口在左侧「插件」页的组件行配置(`plugins.row.config`,DSH 0.1.7-rc.1+)
|
|
34
34
|
- **弹出式切换命令** —— 聊天框输入 `/free-search-engine`,弹出引擎选择窗口,点选即切换(等效设置页 + 保存)
|
|
35
35
|
- **引擎测试** —— `free_search_test` 工具让 agent 一键测试所有引擎;设置页也有"测试引擎"按钮(直测当前引擎,不走回退链,付费引擎无 key 会明确报错)
|
|
@@ -68,6 +68,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
68
68
|
| `baidu` | 百度千帆 AI 搜索 | 额度 | 需 `BAIDU_API_KEY`(千帆 AI 搜索,每日赠 50 次,超额按量后付费);中文全网,支持时间过滤 |
|
|
69
69
|
| `kimi` | Kimi(Moonshot)联网搜索 | 付费 | 需 `MOONSHOT_API_KEY`(basic 约 ¥0.01/次),返回带正文 chunks 的中文结果 |
|
|
70
70
|
| `aliyun` | 阿里云百炼 EnhancedSearch | 付费 | 需 `DASHSCOPE_API_KEY`(MCP search_pro,约 ¥0.03/次,新用户 200 次免费包);中文全网带来源 hostname |
|
|
71
|
+
| `doubao` | 火山引擎联网搜索(豆包搜索) | 免费额度 | 需 `DOUBAO_SEARCH_API_KEY`(联网搜索控制台开通,**每月 500 次免费**,超出按量付费);中文时效内容强,支持时间/站点过滤,返回千字级摘要 |
|
|
71
72
|
|
|
72
73
|
- **默认引擎为 `bing`**(免费且最稳定),安装后开箱即用。
|
|
73
74
|
- **自动回退**:任何引擎失败(免费限流/反爬,付费缺 key/无效/网络错误)都会自动轮流尝试下一个引擎——先试其他已配 key 的付费引擎,再试免费引擎(Bing/AnySearch 等),并在结果中附带回退提示——搜索不会因引擎问题直接失败。
|
|
@@ -79,6 +80,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
79
80
|
- Perplexity:<https://www.perplexity.ai/settings/api>
|
|
80
81
|
- SerpBase:<https://serpbase.dev>
|
|
81
82
|
- DeepSeek:<https://platform.deepseek.com/api_keys>
|
|
83
|
+
- 豆包搜索(火山联网搜索):<https://console.volcengine.com/search-infinity/web-search>
|
|
82
84
|
- You.com:<https://you.com/platform/api-keys>
|
|
83
85
|
|
|
84
86
|
#### 为什么免费引擎不需要 key?
|
|
@@ -186,11 +188,13 @@ dsh web
|
|
|
186
188
|
|
|
187
189
|
#### 配置文件
|
|
188
190
|
|
|
189
|
-
DSH 0.1.7-rc.1 起,配置跟随 profile 的插件条目保存:设置页与 `/free-search-engine` 都会写入当前 profile 的 `cordis.patch.yml` 中 `web-search-free`(`dsh-free-search`)条目的 `config
|
|
191
|
+
DSH 0.1.7-rc.1 起,配置跟随 profile 的插件条目保存:设置页与 `/free-search-engine` 都会写入当前 profile 的 `cordis.patch.yml` 中 `web-search-free`(`dsh-free-search`)条目的 `config`。
|
|
192
|
+
|
|
193
|
+
旧版 `~/.dsh/settings.yaml` 的 `free-search:` 段**不会被 DSH 核心自动导入**(核心的 `importLegacyDocument` 只为 `ui-developer-tools` / `ui-onboarding` / `shell` 三个段提供了映射),原文件在导入其它段后会被改名为 `settings.yaml.imported`,该段的值只留在那里。插件会在启动时检测 `settings.yaml.imported`(或仍存在的 `settings.yaml`)中的 `free-search:` 段,把可识别的字段**一次性补种**进当前 profile 的条目 `config`(写一次后不再重复,可在启动日志看到 `free-search: migrated N field(s)…`)。如果你想手动处理,也可以照着 `settings.yaml.imported` 里的值在插件页行配置中填一遍。
|
|
190
194
|
|
|
191
195
|
```yaml
|
|
192
196
|
# profiles/<profile>/cordis.patch.yml 中该条目的 config:
|
|
193
|
-
provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / deepseek-official / you
|
|
197
|
+
provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / deepseek-official / you / baidu / kimi / aliyun / doubao
|
|
194
198
|
lang: zh # 设置页界面语言(zh / en)
|
|
195
199
|
bingMarket: zh-CN # Bing 市场
|
|
196
200
|
region: cn-zh # DuckDuckGo 区域(可选)
|
|
@@ -338,7 +342,7 @@ This plugin provides multiple free search engines with automatic fallback, compl
|
|
|
338
342
|
### Features
|
|
339
343
|
|
|
340
344
|
- **Zero Cost** — Multiple free engines with no API key or registration required
|
|
341
|
-
- **Multi-Engine Support** — DuckDuckGo (HTML / Lite), Bing, AnySearch AI, SearXNG (meta-search with custom instances), Exa, Tavily, Keenable, Firecrawl, Parallel, Perplexity, SerpBase, DeepSeek Official,
|
|
345
|
+
- **Multi-Engine Support** — DuckDuckGo (HTML / Lite), Bing, AnySearch AI, SearXNG (meta-search with custom instances), Exa, Tavily, Keenable, Firecrawl, Parallel, Perplexity, SerpBase, DeepSeek Official, You.com, Baidu Qianfan, Kimi, Aliyun Bailian, and Volcano Doubao
|
|
342
346
|
- **Web Settings UI** — Engine switching, API key configuration (keys masked as "configured" in the UI), and a Chinese/English toggle; the entry is the component-row config (`plugins.row.config`) on the sidebar Plugins page (DSH 0.1.7-rc.1+)
|
|
343
347
|
- **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)
|
|
344
348
|
- **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)
|
|
@@ -377,6 +381,7 @@ If this plugin has been helpful, a ⭐ on [GitHub](https://github.com/DDDMUC/dsh
|
|
|
377
381
|
| `baidu` | Baidu Qianfan AI Search | Quota | Requires `BAIDU_API_KEY` (Qianfan AI Search, 50 free calls/day then pay-as-you-go); Chinese web-wide, supports time filtering |
|
|
378
382
|
| `kimi` | Kimi (Moonshot) Web Search | Paid | Requires `MOONSHOT_API_KEY` (basic ~¥0.01/call); returns Chinese results with body chunks |
|
|
379
383
|
| `aliyun` | Aliyun Bailian EnhancedSearch | Paid | Requires `DASHSCOPE_API_KEY` (MCP search_pro, ~¥0.03/call, 200 free calls for new users); Chinese web-wide with source hostnames |
|
|
384
|
+
| `doubao` | Volcano Web Search (Doubao) | Free quota | Requires `DOUBAO_SEARCH_API_KEY` (open it in the Web Search console; **500 free queries/month**, pay-as-you-go beyond); strong for fresh Chinese content, supports time/site filters, returns long summaries |
|
|
380
385
|
|
|
381
386
|
- **Default engine is `bing`** (free and most stable), ready to use out of the box after installation.
|
|
382
387
|
- **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/parallel 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.
|
|
@@ -388,6 +393,7 @@ If this plugin has been helpful, a ⭐ on [GitHub](https://github.com/DDDMUC/dsh
|
|
|
388
393
|
- Perplexity: <https://www.perplexity.ai/settings/api>
|
|
389
394
|
- SerpBase: <https://serpbase.dev>
|
|
390
395
|
- DeepSeek: <https://platform.deepseek.com/api_keys>
|
|
396
|
+
- Doubao / Volcano Web Search: <https://console.volcengine.com/search-infinity/web-search>
|
|
391
397
|
- You.com: <https://you.com/platform/api-keys>
|
|
392
398
|
|
|
393
399
|
#### Why are some engines free?
|
|
@@ -495,11 +501,13 @@ The command only changes the preferred engine; search still goes through `web_se
|
|
|
495
501
|
|
|
496
502
|
#### Configuration File
|
|
497
503
|
|
|
498
|
-
Since DSH 0.1.7-rc.1 the configuration is stored with the profile's plugin entry: the settings page and `/free-search-engine` both write the `config` of the `web-search-free` (`dsh-free-search`) entry in the active profile's `cordis.patch.yml`.
|
|
504
|
+
Since DSH 0.1.7-rc.1 the configuration is stored with the profile's plugin entry: the settings page and `/free-search-engine` both write the `config` of the `web-search-free` (`dsh-free-search`) entry in the active profile's `cordis.patch.yml`.
|
|
505
|
+
|
|
506
|
+
The old `free-search:` section of `~/.dsh/settings.yaml` is **not** imported by the DSH core (`importLegacyDocument` only maps `ui-developer-tools` / `ui-onboarding` / `shell`); the file is renamed to `settings.yaml.imported` after the other sections are imported, and the section's values stay there. The plugin detects that section in `settings.yaml.imported` (or in a still-present `settings.yaml`) at startup and seeds the recognized fields into this entry's `config` **once** (watch for `free-search: migrated N field(s)…` in the startup log). You can also migrate manually by copying the values from `settings.yaml.imported` into the plugin row config.
|
|
499
507
|
|
|
500
508
|
```yaml
|
|
501
509
|
# config of that entry in profiles/<profile>/cordis.patch.yml:
|
|
502
|
-
provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / deepseek-official / you
|
|
510
|
+
provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / deepseek-official / you / baidu / kimi / aliyun / doubao
|
|
503
511
|
lang: zh # settings UI language (zh / en)
|
|
504
512
|
bingMarket: zh-CN # Bing market
|
|
505
513
|
region: cn-zh # DuckDuckGo region (optional)
|
package/cordis.patch.yml
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
# dsh-free-search — free web search providers (no API key needed)
|
|
2
|
-
# Registers the
|
|
3
|
-
#
|
|
4
|
-
# The engine
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
2
|
+
# Registers the plugin into ctx.web + a settings bridge served at
|
|
3
|
+
# /api/dsh-free-search-settings.
|
|
4
|
+
# The engine is chosen from the sidebar Plugins page -> Free Search row config
|
|
5
|
+
# (plugins.row.config, DSH 0.1.7-rc.1+), from the /free-search-engine command,
|
|
6
|
+
# or directly via this entry's config, e.g.:
|
|
7
|
+
# provider: bing # engine id: ddg / bing / baidu / kimi / aliyun / doubao / auto / ...
|
|
8
|
+
# region: cn-zh # DuckDuckGo region
|
|
9
|
+
# bingMarket: zh-CN # Bing market
|
|
10
|
+
# A legacy ~/.dsh/settings.yaml "free-search:" section is migrated into this
|
|
11
|
+
# entry's config once at startup (see README).
|
|
12
12
|
# Failed/empty engines automatically fall back to the next free engine.
|
|
13
|
-
# web.searchProvider must point at this plugin's provider id (ddg)
|
|
13
|
+
# web.searchProvider must point at this plugin's provider id (ddg); the runtime
|
|
14
|
+
# also takes over when it is unset or still the shipped default
|
|
15
|
+
# (deepseek-official), and only warns when another provider was chosen
|
|
16
|
+
# explicitly.
|
|
14
17
|
- insert:
|
|
15
18
|
- id: web-search-free
|
|
16
19
|
name: dsh-free-search
|
package/lib/client.js
CHANGED
|
@@ -137,11 +137,12 @@ window.__ModuleLoader__.load({
|
|
|
137
137
|
baiduPh: (c) => c ? "Baidu API 密钥(已配置)" : "百度千帆 API 密钥(BAIDU_API_KEY)",
|
|
138
138
|
kimiPh: (c) => c ? "Kimi API 密钥(已配置)" : "Kimi API 密钥(MOONSHOT_API_KEY)",
|
|
139
139
|
aliyunPh: (c) => c ? "Aliyun API 密钥(已配置)" : "阿里云 DashScope 密钥(DASHSCOPE_API_KEY)",
|
|
140
|
+
doubaoPh: (c) => c ? "豆包搜索密钥(已配置)" : "豆包搜索 / 火山联网搜索密钥(DOUBAO_SEARCH_API_KEY,每月 500 次免费)",
|
|
140
141
|
keysHint: "密钥读取优先级:.credentials.yaml 凭据中心 > 这里 > 环境变量。推荐把 key 写进凭据中心(与官方 LLM 一致,一处管理)。",
|
|
141
142
|
keyStorage: "Key 存储位置",
|
|
142
143
|
keyStorageCred: "凭据中心(推荐)",
|
|
143
144
|
keyStorageSettings: "设置页(兼容)",
|
|
144
|
-
keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek", "youcom", "baidu", "kimi", "aliyun"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
|
|
145
|
+
keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek", "youcom", "baidu", "kimi", "aliyun", "doubao"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
|
|
145
146
|
keyStorageSettingsHint: "保存后写入当前 profile 的插件条目 config(cordis.patch.yml,优先级低于凭据中心)。",
|
|
146
147
|
platformSearch: "平台搜索(platform_search 工具)",
|
|
147
148
|
platformHint: "为 agent 的 platform_search 工具启用平台。禁用的平台会被跳过。",
|
|
@@ -210,11 +211,12 @@ window.__ModuleLoader__.load({
|
|
|
210
211
|
baiduPh: (c) => c ? "Baidu API key (configured)" : "Baidu Qianfan API key (BAIDU_API_KEY)",
|
|
211
212
|
kimiPh: (c) => c ? "Kimi API key (configured)" : "Kimi API key (MOONSHOT_API_KEY)",
|
|
212
213
|
aliyunPh: (c) => c ? "Aliyun API key (configured)" : "Aliyun DashScope API key (DASHSCOPE_API_KEY)",
|
|
214
|
+
doubaoPh: (c) => c ? "Doubao search key (configured)" : "Doubao / Volcano Web Search key (DOUBAO_SEARCH_API_KEY, 500 free/month)",
|
|
213
215
|
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).",
|
|
214
216
|
keyStorage: "Key storage",
|
|
215
217
|
keyStorageCred: "Credential center (recommended)",
|
|
216
218
|
keyStorageSettings: "Settings page (legacy)",
|
|
217
|
-
keyStorageCredHint: (c) => `Saved to ~/.dsh/.credentials.yaml (highest priority). Currently configured: ${["anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek", "youcom", "baidu", "kimi", "aliyun"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "none"}`,
|
|
219
|
+
keyStorageCredHint: (c) => `Saved to ~/.dsh/.credentials.yaml (highest priority). Currently configured: ${["anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek", "youcom", "baidu", "kimi", "aliyun", "doubao"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "none"}`,
|
|
218
220
|
keyStorageSettingsHint: "Saved to the active profile plugin entry config (cordis.patch.yml; lower priority than the credential center).",
|
|
219
221
|
platformSearch: "Platform search (platform_search tool)",
|
|
220
222
|
platformHint: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
|
|
@@ -265,7 +267,7 @@ window.__ModuleLoader__.load({
|
|
|
265
267
|
};
|
|
266
268
|
const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
|
|
267
269
|
// 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
|
|
268
|
-
const PLUGIN_VERSION = "0.
|
|
270
|
+
const PLUGIN_VERSION = "0.6.0";
|
|
269
271
|
const ENGINES = [
|
|
270
272
|
{ id: "auto", label: "Auto · 智能路由", badge: "AUTO", link: null },
|
|
271
273
|
{ id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
|
|
@@ -285,6 +287,7 @@ window.__ModuleLoader__.load({
|
|
|
285
287
|
{ id: "baidu", label: "Baidu · 百度千帆", badge: "API KEY", link: "https://console.bce.baidu.com/qianfan" },
|
|
286
288
|
{ id: "kimi", label: "Kimi · Moonshot", badge: "API KEY", link: "https://platform.moonshot.cn" },
|
|
287
289
|
{ id: "aliyun", label: "Aliyun · 百炼", badge: "API KEY", link: "https://bailian.console.aliyun.com" },
|
|
290
|
+
{ id: "doubao", label: "Doubao · 豆包搜索", badge: "API KEY", link: "https://console.volcengine.com/search-infinity/web-search" },
|
|
288
291
|
];
|
|
289
292
|
|
|
290
293
|
async function bridgeDescribe() {
|
|
@@ -400,6 +403,7 @@ window.__ModuleLoader__.load({
|
|
|
400
403
|
const [baiduKey, setBaiduKey] = react.useState("");
|
|
401
404
|
const [kimiKey, setKimiKey] = react.useState("");
|
|
402
405
|
const [aliyunKey, setAliyunKey] = react.useState("");
|
|
406
|
+
const [doubaoKey, setDoubaoKey] = react.useState("");
|
|
403
407
|
const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
|
|
404
408
|
const [cacheTtl, setCacheTtl] = react.useState(5);
|
|
405
409
|
const [keysConfigured, setKeysConfigured] = react.useState({});
|
|
@@ -443,6 +447,7 @@ window.__ModuleLoader__.load({
|
|
|
443
447
|
setBaiduKey(v.baiduApiKey ?? "");
|
|
444
448
|
setKimiKey(v.kimiApiKey ?? "");
|
|
445
449
|
setAliyunKey(v.aliyunApiKey ?? "");
|
|
450
|
+
setDoubaoKey(v.doubaoApiKey ?? "");
|
|
446
451
|
setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
|
|
447
452
|
setCacheTtl(v.cacheTtl === undefined ? 5 : Math.min(Math.max(Number(v.cacheTtl) ?? 5, 0), 5));
|
|
448
453
|
// secrets 字段标记哪些 key 已配置(值被脱敏,仅显示"已配置")
|
|
@@ -463,6 +468,7 @@ window.__ModuleLoader__.load({
|
|
|
463
468
|
if (path === "baiduApiKey") configured.baidu = true;
|
|
464
469
|
if (path === "kimiApiKey") configured.kimi = true;
|
|
465
470
|
if (path === "aliyunApiKey") configured.aliyun = true;
|
|
471
|
+
if (path === "doubaoApiKey") configured.doubao = true;
|
|
466
472
|
}
|
|
467
473
|
}
|
|
468
474
|
setKeysConfigured(configured);
|
|
@@ -474,7 +480,7 @@ window.__ModuleLoader__.load({
|
|
|
474
480
|
const cred = await bridgeCredentialsStatus();
|
|
475
481
|
if (cred.ok) {
|
|
476
482
|
const cc = {};
|
|
477
|
-
const map = { anysearchApiKey: "anysearch", exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", firecrawlApiKey: "firecrawl", parallelApiKey: "parallel", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek", serpbaseApiKey: "serpbase", youcomApiKey: "youcom", baiduApiKey: "baidu", kimiApiKey: "kimi", aliyunApiKey: "aliyun" };
|
|
483
|
+
const map = { anysearchApiKey: "anysearch", exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", firecrawlApiKey: "firecrawl", parallelApiKey: "parallel", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek", serpbaseApiKey: "serpbase", youcomApiKey: "youcom", baiduApiKey: "baidu", kimiApiKey: "kimi", aliyunApiKey: "aliyun", doubaoApiKey: "doubao" };
|
|
478
484
|
for (const [k, v] of Object.entries(cred.value.configured ?? {})) {
|
|
479
485
|
if (map[k]) cc[map[k]] = v;
|
|
480
486
|
}
|
|
@@ -524,6 +530,7 @@ window.__ModuleLoader__.load({
|
|
|
524
530
|
["baiduApiKey", baiduKey],
|
|
525
531
|
["kimiApiKey", kimiKey],
|
|
526
532
|
["aliyunApiKey", aliyunKey],
|
|
533
|
+
["doubaoApiKey", doubaoKey],
|
|
527
534
|
];
|
|
528
535
|
if (keyStorage === "credentials") {
|
|
529
536
|
for (const [field, value] of keyFields) {
|
|
@@ -982,6 +989,19 @@ react_jsx_runtime.jsx("div", {
|
|
|
982
989
|
setSaveError("");
|
|
983
990
|
},
|
|
984
991
|
}),
|
|
992
|
+
react_jsx_runtime.jsx("input", {
|
|
993
|
+
className: "dshfs-input",
|
|
994
|
+
type: "password",
|
|
995
|
+
placeholder: t.doubaoPh(keysConfigured.doubao),
|
|
996
|
+
value: doubaoKey,
|
|
997
|
+
disabled: !ready || saving,
|
|
998
|
+
onChange: (e) => {
|
|
999
|
+
setDoubaoKey(e.target.value);
|
|
1000
|
+
setDirty(true);
|
|
1001
|
+
setFailed(false);
|
|
1002
|
+
setSaveError("");
|
|
1003
|
+
},
|
|
1004
|
+
}),
|
|
985
1005
|
react_jsx_runtime.jsx("p", {
|
|
986
1006
|
className: "dshfs-hint",
|
|
987
1007
|
children: t.keysHint,
|
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { SettingsConflictError } from "@deepseek-ai/dsh-settings";
|
|
|
2
2
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
4
|
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { exec } from "node:child_process";
|
|
7
8
|
|
|
@@ -16,6 +17,7 @@ const KEENABLE_URL = "https://api.keenable.ai/v1/search";
|
|
|
16
17
|
const KEENABLE_MCP_URL = "https://api.keenable.ai/mcp";
|
|
17
18
|
const SERPBASE_URL = "https://api.serpbase.dev/google/search";
|
|
18
19
|
const YOUCOM_URL = "https://ydc-index.io/v1/search";
|
|
20
|
+
const DOUBAO_SEARCH_URL = "https://open.feedcoopapi.com/search_api/web_search";
|
|
19
21
|
const USER_AGENT =
|
|
20
22
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
|
|
21
23
|
const ACCEPT_LANG = "zh-CN,zh;q=0.9,en;q=0.8";
|
|
@@ -53,11 +55,11 @@ const MARKET_TO_LANG = {
|
|
|
53
55
|
const FREE_SEARCH_NS = "web-search-free";
|
|
54
56
|
const BRIDGE_PREFIX = "/api/dsh-free-search-settings";
|
|
55
57
|
const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch"];
|
|
56
|
-
const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek-official", "you", "baidu", "kimi", "aliyun"];
|
|
58
|
+
const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek-official", "you", "baidu", "kimi", "aliyun", "doubao"];
|
|
57
59
|
|
|
58
60
|
|
|
59
61
|
// 支持 time_range 过滤的引擎
|
|
60
|
-
const TIME_ENGINES = ["tavily", "exa", "keenable", "firecrawl", "parallel", "searxng", "ddg", "ddg-lite", "baidu"];
|
|
62
|
+
const TIME_ENGINES = ["tavily", "exa", "keenable", "firecrawl", "parallel", "searxng", "ddg", "ddg-lite", "baidu", "doubao"];
|
|
61
63
|
|
|
62
64
|
// 智能路由(provider=auto 时启用):纯确定性规则重排引擎顺序,末尾接上其余全部引擎保证回退链完整
|
|
63
65
|
function routeEngines(query, { timeRange } = {}) {
|
|
@@ -81,7 +83,7 @@ function routeEngines(query, { timeRange } = {}) {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
// 当前插件版本(发布时与 package.json 同步)
|
|
84
|
-
const PLUGIN_VERSION = "0.
|
|
86
|
+
const PLUGIN_VERSION = "0.6.0";
|
|
85
87
|
// 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
|
|
86
88
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
|
|
87
89
|
const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
|
|
@@ -211,14 +213,21 @@ function buildCacheKey(query, maxResults, timeRangeLabel, preferred) {
|
|
|
211
213
|
const SNIPPET_NOISE =
|
|
212
214
|
/\b(sign up|sign in|log in|login|subscribe( to| for)?|member[- ]?only|become a member|create (a )?free account|read more|continue reading|story continues|get started|install (the )?app|view on|medium membership|join \w+ for free|get updates from this writer|stories in your inbox|remember me for|unlock this|free to read|become a patron)\b/gi;
|
|
213
215
|
|
|
214
|
-
function cleanSnippet(text) {
|
|
216
|
+
function cleanSnippet(text, max = 300) {
|
|
215
217
|
if (!text) return text;
|
|
216
218
|
return String(text)
|
|
217
219
|
.replace(SNIPPET_NOISE, " ")
|
|
218
220
|
.replace(/^\s*(#{1,6}\s*|\[\s*x?\s*\]\s*|-\s*\[\s*x?\s*\]\s*|>\s*)/gm, " ")
|
|
219
221
|
.replace(/\s+/g, " ")
|
|
220
222
|
.trim()
|
|
221
|
-
.slice(0,
|
|
223
|
+
.slice(0, max);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 各引擎的摘要展示上限:豆包搜索返回的是千字级 Summary(实测 1300-1800 字),放宽到 2000;
|
|
227
|
+
// 其余引擎维持 300。该上限同时用于 provider 成功后的清洗与 runEngineTest(喂 multi_search)。
|
|
228
|
+
const SNIPPET_MAX_BY_ENGINE = { doubao: 2000 };
|
|
229
|
+
function snippetCap(engine) {
|
|
230
|
+
return SNIPPET_MAX_BY_ENGINE[engine] ?? 300;
|
|
222
231
|
}
|
|
223
232
|
|
|
224
233
|
// 提示注入防护:插件自有工具(advanced_search / platform_search / free_search_test)的
|
|
@@ -1762,6 +1771,98 @@ async function searchAliyun(query, maxResults, apiKey, timeRange, signal) {
|
|
|
1762
1771
|
|
|
1763
1772
|
return { sources: uniqueSources(sources, maxResults ?? 10), truncated: false };
|
|
1764
1773
|
}
|
|
1774
|
+
|
|
1775
|
+
// 火山引擎「联网搜索」(豆包搜索,AskEcho):
|
|
1776
|
+
// key = DOUBAO_SEARCH_API_KEY(联网搜索控制台 / Ark Coding Plan;每月 500 次免费额度)
|
|
1777
|
+
// 注意:错误也返回 HTTP 200,必须解析 ResponseMetadata.Error。
|
|
1778
|
+
// 时间过滤:OneDay | OneWeek | OneMonth | OneYear | "YYYY-MM-DD..YYYY-MM-DD"
|
|
1779
|
+
function doubaoTimeRange(timeRange) {
|
|
1780
|
+
if (!timeRange) return undefined;
|
|
1781
|
+
if (typeof timeRange.after === "string") {
|
|
1782
|
+
const end = new Date().toISOString().slice(0, 10);
|
|
1783
|
+
return `${timeRange.after}..${end}`;
|
|
1784
|
+
}
|
|
1785
|
+
const days = Number(timeRange.days);
|
|
1786
|
+
if (!Number.isFinite(days) || days <= 0) return undefined;
|
|
1787
|
+
if (days <= 1) return "OneDay";
|
|
1788
|
+
if (days <= 7) return "OneWeek";
|
|
1789
|
+
if (days <= 31) return "OneMonth";
|
|
1790
|
+
return "OneYear";
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
const DOUBAO_ERROR_HINTS = {
|
|
1794
|
+
10400: "invalid request parameters (check Query/Count/TimeRange)",
|
|
1795
|
+
10402: "invalid SearchType (only web|image)",
|
|
1796
|
+
10403: "account or permission problem - make sure the key is from the Web Search console",
|
|
1797
|
+
10406: "free quota exhausted (500 requests/month on the free tier) - check your plan",
|
|
1798
|
+
10407: "no available free-tier policy - check the account status",
|
|
1799
|
+
10500: "Web Search service internal error - retry later",
|
|
1800
|
+
700429: "free-tier rate limit hit - slow down and retry",
|
|
1801
|
+
100013: "sub-account is not granted TorchlightApiFullAccess",
|
|
1802
|
+
700901: "invalid api key",
|
|
1803
|
+
};
|
|
1804
|
+
|
|
1805
|
+
async function searchDoubao(query, maxResults, apiKey, timeRange, signal) {
|
|
1806
|
+
if (!apiKey) throw new Error("Doubao search requires DOUBAO_SEARCH_API_KEY");
|
|
1807
|
+
const count = Math.min(Math.max(maxResults ?? 10, 1), 50);
|
|
1808
|
+
const body = {
|
|
1809
|
+
Query: String(query).slice(0, 100),
|
|
1810
|
+
SearchType: "web",
|
|
1811
|
+
Count: count,
|
|
1812
|
+
NeedSummary: true,
|
|
1813
|
+
Filter: { NeedUrl: true },
|
|
1814
|
+
};
|
|
1815
|
+
const range = doubaoTimeRange(timeRange);
|
|
1816
|
+
if (range !== undefined) body.TimeRange = range;
|
|
1817
|
+
|
|
1818
|
+
const response = await fetch(DOUBAO_SEARCH_URL, {
|
|
1819
|
+
method: "POST",
|
|
1820
|
+
redirect: "error",
|
|
1821
|
+
headers: {
|
|
1822
|
+
"content-type": "application/json",
|
|
1823
|
+
authorization: `Bearer ${apiKey}`,
|
|
1824
|
+
"X-Traffic-Tag": "dsh-free-search",
|
|
1825
|
+
"user-agent": "deepseek-harness/free-search",
|
|
1826
|
+
},
|
|
1827
|
+
body: JSON.stringify(body),
|
|
1828
|
+
signal: AbortSignal.any([...(signal !== undefined ? [signal] : []), AbortSignal.timeout(20000)]),
|
|
1829
|
+
});
|
|
1830
|
+
if (!response.ok) {
|
|
1831
|
+
const detail = await response.text().catch(() => "");
|
|
1832
|
+
if (response.status === 401 || response.status === 403) {
|
|
1833
|
+
throw new Error(
|
|
1834
|
+
`Doubao search auth failed (HTTP ${response.status}) - check DOUBAO_SEARCH_API_KEY (Web Search console)`
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
throw new Error(`Doubao search API error (HTTP ${response.status}): ${detail.slice(0, 200)}`);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
const data = await response.json();
|
|
1841
|
+
// HTTP 200 也可能携带错误信封
|
|
1842
|
+
const err = data?.ResponseMetadata?.Error;
|
|
1843
|
+
if (err && (err.Code || err.CodeN || err.Message)) {
|
|
1844
|
+
const code = err.CodeN ?? err.Code;
|
|
1845
|
+
const hint = DOUBAO_ERROR_HINTS[Number(code)] ?? DOUBAO_ERROR_HINTS[String(code)];
|
|
1846
|
+
throw new Error(
|
|
1847
|
+
`Doubao search API error (${code ?? "unknown"}): ${err.Message ?? "request failed"}${hint ? ` - ${hint}` : ""}`
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
const hits = data?.Result?.WebResults ?? [];
|
|
1852
|
+
const sources = hits
|
|
1853
|
+
.filter((item) => item && item.Url)
|
|
1854
|
+
.map((item) => {
|
|
1855
|
+
const raw = item.Summary || item.Content || item.Snippet || "";
|
|
1856
|
+
const snippet = stripTags(String(raw)).replace(/\s+/g, " ").trim();
|
|
1857
|
+
return {
|
|
1858
|
+
url: String(item.Url),
|
|
1859
|
+
...(item.Title ? { title: stripTags(String(item.Title)).trim() } : {}),
|
|
1860
|
+
...(snippet ? { snippet: snippet.slice(0, snippetCap("doubao")) } : {}),
|
|
1861
|
+
...(item.PublishTime ? { publishedAt: String(item.PublishTime) } : {}),
|
|
1862
|
+
};
|
|
1863
|
+
});
|
|
1864
|
+
return { sources: uniqueSources(sources, maxResults ?? 10), truncated: false };
|
|
1865
|
+
}
|
|
1765
1866
|
//#endregion
|
|
1766
1867
|
|
|
1767
1868
|
//#region bridge
|
|
@@ -2151,6 +2252,7 @@ const KEY_REF_MAP = {
|
|
|
2151
2252
|
baiduApiKey: "BAIDU_API_KEY",
|
|
2152
2253
|
kimiApiKey: "MOONSHOT_API_KEY",
|
|
2153
2254
|
aliyunApiKey: "DASHSCOPE_API_KEY",
|
|
2255
|
+
doubaoApiKey: "DOUBAO_SEARCH_API_KEY",
|
|
2154
2256
|
};
|
|
2155
2257
|
|
|
2156
2258
|
// rc.1 declares editable fields `.volatile()`: the Loader hands `apply` live
|
|
@@ -2181,8 +2283,106 @@ const Config = z.object({
|
|
|
2181
2283
|
baiduApiKey: z.string().role("secret").volatile(),
|
|
2182
2284
|
kimiApiKey: z.string().role("secret").volatile(),
|
|
2183
2285
|
aliyunApiKey: z.string().role("secret").volatile(),
|
|
2286
|
+
doubaoApiKey: z.string().role("secret").volatile(),
|
|
2287
|
+
// 旧版 settings.yaml `free-search:` 段的一次性迁移标记(见 readLegacyFreeSearchSection)
|
|
2288
|
+
legacyYamlMigrated: z.boolean().default(false).volatile(),
|
|
2184
2289
|
});
|
|
2185
2290
|
|
|
2291
|
+
// ── 旧版 settings.yaml `free-search:` 段的迁移 ─────────────────────────────────
|
|
2292
|
+
// DSH 核心的 importLegacyDocument 只为 ui-developer-tools / ui-onboarding / shell
|
|
2293
|
+
// 三个段提供了映射;`free-search:` 段无法自动导入,值只会留在启动时被改名的
|
|
2294
|
+
// settings.yaml.imported 里。这里解析该段,并在首次启动时补种进本条目 config,
|
|
2295
|
+
// 用 Config.legacyYamlMigrated 去重(只写一次)。
|
|
2296
|
+
const LEGACY_MIGRATABLE_KEYS = [
|
|
2297
|
+
"provider",
|
|
2298
|
+
"cache",
|
|
2299
|
+
"cacheTtl",
|
|
2300
|
+
"keyStorage",
|
|
2301
|
+
"lang",
|
|
2302
|
+
"region",
|
|
2303
|
+
"bingMarket",
|
|
2304
|
+
"safeSearch",
|
|
2305
|
+
"searxngInstances",
|
|
2306
|
+
"platforms",
|
|
2307
|
+
"anysearchApiKey",
|
|
2308
|
+
"exaApiKey",
|
|
2309
|
+
"tavilyApiKey",
|
|
2310
|
+
"keenableApiKey",
|
|
2311
|
+
"firecrawlApiKey",
|
|
2312
|
+
"parallelApiKey",
|
|
2313
|
+
"perplexityApiKey",
|
|
2314
|
+
"serpbaseApiKey",
|
|
2315
|
+
"deepseekApiKey",
|
|
2316
|
+
"youcomApiKey",
|
|
2317
|
+
"baiduApiKey",
|
|
2318
|
+
"kimiApiKey",
|
|
2319
|
+
"aliyunApiKey",
|
|
2320
|
+
"doubaoApiKey",
|
|
2321
|
+
];
|
|
2322
|
+
|
|
2323
|
+
function dshHomeDir() {
|
|
2324
|
+
return process.env.DSH_HOME ?? path.join(os.homedir(), ".dsh");
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
/** 解析 YAML 标量:引号字符串、布尔、null、数字、内联数组;其余按原样字符串。 */
|
|
2328
|
+
function parseLegacyScalar(raw) {
|
|
2329
|
+
if (raw === "true") return true;
|
|
2330
|
+
if (raw === "false") return false;
|
|
2331
|
+
if (raw === "null" || raw === "~") return null;
|
|
2332
|
+
const quoted = raw.match(/^(['"])([\s\S]*)\1$/);
|
|
2333
|
+
if (quoted) return quoted[2];
|
|
2334
|
+
if (/^-?\d+$/.test(raw)) return Number(raw);
|
|
2335
|
+
if (/^-?\d+\.\d+$/.test(raw)) return Number(raw);
|
|
2336
|
+
if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
2337
|
+
return raw
|
|
2338
|
+
.slice(1, -1)
|
|
2339
|
+
.split(",")
|
|
2340
|
+
.map((part) => part.trim())
|
|
2341
|
+
.filter(Boolean)
|
|
2342
|
+
.map((part) => parseLegacyScalar(part))
|
|
2343
|
+
.filter((value) => value !== null);
|
|
2344
|
+
}
|
|
2345
|
+
return raw;
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
/** 从 settings.yaml 文本里抽出顶层 `free-search:` 段的扁平键值(忽略嵌套块/块状列表)。 */
|
|
2349
|
+
function parseLegacyFreeSearchSection(text) {
|
|
2350
|
+
const lines = String(text).split(/\r?\n/);
|
|
2351
|
+
let start = -1;
|
|
2352
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2353
|
+
if (/^free-search:\s*(#.*)?$/.test(lines[i])) {
|
|
2354
|
+
start = i;
|
|
2355
|
+
break;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
if (start === -1) return null;
|
|
2359
|
+
const section = {};
|
|
2360
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
2361
|
+
const line = lines[i];
|
|
2362
|
+
if (line.trim() === "" || line.trimStart().startsWith("#")) continue;
|
|
2363
|
+
if (/^\S/.test(line)) break; // 下一个顶层键
|
|
2364
|
+
const kv = line.match(/^\s+([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
2365
|
+
if (!kv) continue;
|
|
2366
|
+
const value = kv[2].replace(/\s+#.*$/, "").trim();
|
|
2367
|
+
if (value === "") continue; // 嵌套块(map / 块状列表):跳过,保留人工处理
|
|
2368
|
+
section[kv[1]] = parseLegacyScalar(value);
|
|
2369
|
+
}
|
|
2370
|
+
return section;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
/** 依次尝试 settings.yaml.imported(导入后的遗留段)与 settings.yaml。 */
|
|
2374
|
+
function readLegacyFreeSearchSection() {
|
|
2375
|
+
const home = dshHomeDir();
|
|
2376
|
+
for (const name of ["settings.yaml.imported", "settings.yaml"]) {
|
|
2377
|
+
try {
|
|
2378
|
+
const text = fs.readFileSync(path.join(home, name), "utf8");
|
|
2379
|
+
const section = parseLegacyFreeSearchSection(text);
|
|
2380
|
+
if (section !== null && Object.keys(section).length > 0) return { file: name, section };
|
|
2381
|
+
} catch {}
|
|
2382
|
+
}
|
|
2383
|
+
return null;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2186
2386
|
// Unwrap the live `Volatile<T>` references the Loader passes for `.volatile()`
|
|
2187
2387
|
// fields into a plain object, once per read.
|
|
2188
2388
|
function resolveConfig(config) {
|
|
@@ -2206,6 +2406,27 @@ function apply(ctx, config) {
|
|
|
2206
2406
|
// 单 query 结果缓存(provider.search 内闭包持有):LRU 50 条 / TTL 可配置
|
|
2207
2407
|
const searchCache = new Map(); // key -> { value, expiresAt }
|
|
2208
2408
|
|
|
2409
|
+
// 旧版 settings.yaml `free-search:` 段的一次性补种:核心不会导入它(只映射 3 个特例),
|
|
2410
|
+
// 这里在首次启动时把可识别的字段写进本条目 config,之后靠 legacyYamlMigrated 去重。
|
|
2411
|
+
const migrateLegacySettingsFile = async (settings) => {
|
|
2412
|
+
if (current().legacyYamlMigrated) return;
|
|
2413
|
+
const legacy = readLegacyFreeSearchSection();
|
|
2414
|
+
if (legacy === null) return;
|
|
2415
|
+
const cfg = current();
|
|
2416
|
+
const ops = [];
|
|
2417
|
+
for (const [key, value] of Object.entries(legacy.section)) {
|
|
2418
|
+
if (!LEGACY_MIGRATABLE_KEYS.includes(key)) continue;
|
|
2419
|
+
if (JSON.stringify(cfg[key]) === JSON.stringify(value)) continue;
|
|
2420
|
+
ops.push({ op: "set", path: [key], value });
|
|
2421
|
+
}
|
|
2422
|
+
if (ops.length === 0) return;
|
|
2423
|
+
ops.push({ op: "set", path: ["legacyYamlMigrated"], value: true });
|
|
2424
|
+
await settings.mutate(FREE_SEARCH_NS, ops);
|
|
2425
|
+
logger.info(
|
|
2426
|
+
`free-search: migrated ${ops.length - 1} field(s) from legacy ${legacy.file} "free-search:" section into this entry's config`
|
|
2427
|
+
);
|
|
2428
|
+
};
|
|
2429
|
+
|
|
2209
2430
|
// key 优先级:credentials(.credentials.yaml 凭据中心,官方推荐)> settings 的 free-search.<x>ApiKey(遗留兼容)> 环境变量
|
|
2210
2431
|
const resolveApiKey = async (envName, settingsKey) => {
|
|
2211
2432
|
const credentials = getCredentials();
|
|
@@ -2219,6 +2440,12 @@ function apply(ctx, config) {
|
|
|
2219
2440
|
if (settingsKey && cfg[settingsKey]) return cfg[settingsKey];
|
|
2220
2441
|
return process.env[envName] ?? "";
|
|
2221
2442
|
};
|
|
2443
|
+
// 豆包搜索:主 key 名 DOUBAO_SEARCH_API_KEY;官方示例也用 WEB_SEARCH_API_KEY,作为回退
|
|
2444
|
+
const resolveDoubaoKey = async () => {
|
|
2445
|
+
const primary = await resolveApiKey("DOUBAO_SEARCH_API_KEY", "doubaoApiKey");
|
|
2446
|
+
if (primary) return primary;
|
|
2447
|
+
return resolveApiKey("WEB_SEARCH_API_KEY", undefined);
|
|
2448
|
+
};
|
|
2222
2449
|
|
|
2223
2450
|
// 总控 provider:按 settings 的 provider 字段路由到任意引擎。
|
|
2224
2451
|
// 任何引擎失败(缺 key / 401 / 限流 / 网络)都会自动轮流尝试下一个引擎,
|
|
@@ -2273,7 +2500,7 @@ function apply(ctx, config) {
|
|
|
2273
2500
|
chain = routeEngines(request.query, { timeRange: Boolean(timeRange) });
|
|
2274
2501
|
} else {
|
|
2275
2502
|
// 统一引擎链:首选优先,然后其他付费引擎(有 key 的优先尝试),最后免费引擎
|
|
2276
|
-
const paidEngines = ["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek-official", "you", "baidu", "kimi", "aliyun"];
|
|
2503
|
+
const paidEngines = ["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek-official", "you", "baidu", "kimi", "aliyun", "doubao"];
|
|
2277
2504
|
const freeEngines = ["bing", "anysearch", "ddg", "ddg-lite", "searxng"];
|
|
2278
2505
|
const timeEngines = TIME_ENGINES;
|
|
2279
2506
|
if (timeRange) {
|
|
@@ -2413,6 +2640,15 @@ function apply(ctx, config) {
|
|
|
2413
2640
|
continue;
|
|
2414
2641
|
}
|
|
2415
2642
|
result = await searchAliyun(request.query, request.maxResults, key, timeRange, effSignal);
|
|
2643
|
+
} else if (engine === "doubao") {
|
|
2644
|
+
const key = await resolveDoubaoKey();
|
|
2645
|
+
if (!key) {
|
|
2646
|
+
lastError = new Error("Doubao requires DOUBAO_SEARCH_API_KEY");
|
|
2647
|
+
if (engine === preferred) preferredFailure = "DOUBAO_SEARCH_API_KEY is not configured";
|
|
2648
|
+
logger.warn(`free-search: engine "${engine}" skipped (no key), trying next engine`);
|
|
2649
|
+
continue;
|
|
2650
|
+
}
|
|
2651
|
+
result = await searchDoubao(request.query, request.maxResults, key, timeRange, effSignal);
|
|
2416
2652
|
} else {
|
|
2417
2653
|
continue;
|
|
2418
2654
|
}
|
|
@@ -2421,7 +2657,7 @@ function apply(ctx, config) {
|
|
|
2421
2657
|
usedEngine = engine;
|
|
2422
2658
|
// 统一清洗 snippet:去登录/付费墙/订阅噪音,折叠空白(有值的才处理,保持 lossless JSON)
|
|
2423
2659
|
result.sources = result.sources.map((s) =>
|
|
2424
|
-
s.snippet ? { ...s, snippet: cleanSnippet(s.snippet) } : s
|
|
2660
|
+
s.snippet ? { ...s, snippet: cleanSnippet(s.snippet, snippetCap(engine)) } : s
|
|
2425
2661
|
);
|
|
2426
2662
|
// 用了非首选引擎时,在结果里附上准确提示(区分"不支持时间过滤被跳过"与"真实失败")。
|
|
2427
2663
|
// provider=auto 时 preferred 不是引擎而是路由模式:命中的引擎由 provider 字段体现,
|
|
@@ -2472,6 +2708,13 @@ function apply(ctx, config) {
|
|
|
2472
2708
|
// page (`auto: false`) so the host does not also generate a schema form. Values
|
|
2473
2709
|
// persist per profile in `cordis.patch.yml` under this entry (`FREE_SEARCH_NS`).
|
|
2474
2710
|
sctx.effect(() => sctx.settings.configure({ auto: false }, ctx.fiber));
|
|
2711
|
+
// 旧版 settings.yaml 的 `free-search:` 段不会被核心自动导入(只映射 3 个特例),
|
|
2712
|
+
// 这里一次性补种进本条目 config;失败只警告,不影响插件启动。
|
|
2713
|
+
void migrateLegacySettingsFile(sctx.settings).catch((error) => {
|
|
2714
|
+
logger.warn(
|
|
2715
|
+
`free-search: legacy settings.yaml migration skipped (${error instanceof Error ? error.message : String(error)})`
|
|
2716
|
+
);
|
|
2717
|
+
});
|
|
2475
2718
|
// Re-render the dynamic system-prompt engine list whenever this entry's config changes.
|
|
2476
2719
|
sctx.on("settings/document-updated", (ns) => {
|
|
2477
2720
|
if (String(ns) === FREE_SEARCH_NS && typeof refreshPrompt === "function") refreshPrompt();
|
|
@@ -2508,7 +2751,7 @@ function apply(ctx, config) {
|
|
|
2508
2751
|
const previous = ctx.web.searchProviderId;
|
|
2509
2752
|
ctx.web.searchProviderId = provider.id;
|
|
2510
2753
|
logger.info(
|
|
2511
|
-
previous === undefined
|
|
2754
|
+
previous === undefined || previous === null
|
|
2512
2755
|
? `free-search: web.searchProvider was unset, taking over as "${provider.id}"`
|
|
2513
2756
|
: `free-search: taking over from the default provider "${previous}" as "${provider.id}"`
|
|
2514
2757
|
);
|
|
@@ -2592,6 +2835,11 @@ function apply(ctx, config) {
|
|
|
2592
2835
|
if (!key) return { ok: false, error: "DASHSCOPE_API_KEY not configured" };
|
|
2593
2836
|
return await searchAliyun(q, n, key, tr);
|
|
2594
2837
|
}
|
|
2838
|
+
case "doubao": {
|
|
2839
|
+
const key = await resolveDoubaoKey();
|
|
2840
|
+
if (!key) return { ok: false, error: "DOUBAO_SEARCH_API_KEY not configured" };
|
|
2841
|
+
return await searchDoubao(q, n, key, tr);
|
|
2842
|
+
}
|
|
2595
2843
|
case "you": {
|
|
2596
2844
|
const key = await resolveApiKey("YOUCOM_API_KEY", "youcomApiKey");
|
|
2597
2845
|
if (!key) return { ok: false, error: "YOUCOM_API_KEY not configured" };
|
|
@@ -2613,7 +2861,7 @@ function apply(ctx, config) {
|
|
|
2613
2861
|
return {
|
|
2614
2862
|
ok: true,
|
|
2615
2863
|
sources: (result.sources ?? []).map((s) =>
|
|
2616
|
-
s.snippet ? { ...s, snippet: cleanSnippet(s.snippet) } : s
|
|
2864
|
+
s.snippet ? { ...s, snippet: cleanSnippet(s.snippet, snippetCap(engine)) } : s
|
|
2617
2865
|
),
|
|
2618
2866
|
truncated: result.truncated ?? false,
|
|
2619
2867
|
...(typeof result.content === "string" && result.content ? { content: result.content } : {}),
|
|
@@ -2633,7 +2881,7 @@ function apply(ctx, config) {
|
|
|
2633
2881
|
parameters: {
|
|
2634
2882
|
engines: {
|
|
2635
2883
|
type: "array",
|
|
2636
|
-
description: "Which engines to test (default: all). Options: ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, firecrawl, parallel, perplexity, serpbase, deepseek-official, you, baidu, kimi, aliyun.",
|
|
2884
|
+
description: "Which engines to test (default: all). Options: ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, firecrawl, parallel, perplexity, serpbase, deepseek-official, you, baidu, kimi, aliyun, doubao.",
|
|
2637
2885
|
items: { type: "string" },
|
|
2638
2886
|
},
|
|
2639
2887
|
query: {
|
|
@@ -2754,7 +3002,7 @@ function apply(ctx, config) {
|
|
|
2754
3002
|
},
|
|
2755
3003
|
},
|
|
2756
3004
|
render(args, value) {
|
|
2757
|
-
const lines = value.sources.map((s, i) => `- [${s.title ?? s.url}](${s.url})${s.snippet ? ` - ${s.snippet
|
|
3005
|
+
const lines = value.sources.map((s, i) => `- [${s.title ?? s.url}](${s.url})${s.snippet ? ` - ${s.snippet}` : ""}`);
|
|
2758
3006
|
return [{ type: "text", text: `Platform search (${value.platform}):\n${wrapUntrustedBlock(lines.join("\n") || "No results found.")}` }];
|
|
2759
3007
|
},
|
|
2760
3008
|
},
|
|
@@ -2819,7 +3067,7 @@ function apply(ctx, config) {
|
|
|
2819
3067
|
},
|
|
2820
3068
|
engine: {
|
|
2821
3069
|
type: "string",
|
|
2822
|
-
description: "Optional specific engine to try first: ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, firecrawl, parallel, perplexity, serpbase, deepseek-official, you, baidu, kimi, aliyun.",
|
|
3070
|
+
description: "Optional specific engine to try first: ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, firecrawl, parallel, perplexity, serpbase, deepseek-official, you, baidu, kimi, aliyun, doubao.",
|
|
2823
3071
|
},
|
|
2824
3072
|
},
|
|
2825
3073
|
output: {
|
|
@@ -2845,7 +3093,7 @@ function apply(ctx, config) {
|
|
|
2845
3093
|
},
|
|
2846
3094
|
},
|
|
2847
3095
|
render(args, value) {
|
|
2848
|
-
const lines = value.sources.map((s, i) => `- [${s.title ?? s.url}](${s.url})${s.snippet ? ` - ${s.snippet
|
|
3096
|
+
const lines = value.sources.map((s, i) => `- [${s.title ?? s.url}](${s.url})${s.snippet ? ` - ${s.snippet}` : ""}${s.publishedAt ? ` (${s.publishedAt})` : ""}`);
|
|
2849
3097
|
const body = `${lines.join("\n") || "No results found."}${value.content ? `\n\n${value.content}` : ""}`;
|
|
2850
3098
|
return [{ type: "text", text: `Search (${value.provider}${args.timeRange ? `, timeRange=${args.timeRange}` : ""}):\n${wrapUntrustedBlock(body)}` }];
|
|
2851
3099
|
},
|
|
@@ -2941,7 +3189,7 @@ function apply(ctx, config) {
|
|
|
2941
3189
|
render(args, value) {
|
|
2942
3190
|
const lines = value.sources.map((s) => {
|
|
2943
3191
|
const seenStr = Array.isArray(s.seenIn) && s.seenIn.length > 0 ? ` [seen in: ${s.seenIn.join(", ")}]` : "";
|
|
2944
|
-
return `- [${s.title ?? s.url}](${s.url})${seenStr}${s.snippet ? ` - ${s.snippet
|
|
3192
|
+
return `- [${s.title ?? s.url}](${s.url})${seenStr}${s.snippet ? ` - ${s.snippet}` : ""}${s.publishedAt ? ` (${s.publishedAt})` : ""}`;
|
|
2945
3193
|
});
|
|
2946
3194
|
const body = `${lines.join("\n") || "No results found."}${value.content ? `\n\n${value.content}` : ""}`;
|
|
2947
3195
|
return [{ type: "text", text: `Multi-Search (${value.provider}):\n${wrapUntrustedBlock(body)}` }];
|
|
@@ -3104,6 +3352,7 @@ function apply(ctx, config) {
|
|
|
3104
3352
|
"- baidu - paid, requires BAIDU_API_KEY",
|
|
3105
3353
|
"- kimi - paid, requires MOONSHOT_API_KEY",
|
|
3106
3354
|
"- aliyun - paid, requires DASHSCOPE_API_KEY",
|
|
3355
|
+
"- doubao - requires DOUBAO_SEARCH_API_KEY (Volcano Web Search / 豆包搜索; 500 free queries/month)",
|
|
3107
3356
|
"",
|
|
3108
3357
|
"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 (or smart route sequence if provider is auto), (2) then other engines with API keys configured (exa/tavily/keenable/firecrawl/parallel 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.",
|
|
3109
3358
|
"",
|
|
@@ -3159,6 +3408,7 @@ export {
|
|
|
3159
3408
|
routeEngines,
|
|
3160
3409
|
searchAnysearch,
|
|
3161
3410
|
searchAliyun,
|
|
3411
|
+
searchDoubao,
|
|
3162
3412
|
searchBaidu,
|
|
3163
3413
|
searchKimi,
|
|
3164
3414
|
searchBing,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-free-search",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Free web search for DeepSeek Harness:
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Free web search for DeepSeek Harness: 18 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable/Firecrawl/Parallel keyless; Perplexity/SerpBase/DeepSeek/You.com/Baidu/Kimi/Aliyun/Doubao with key) + auto smart routing + multi_search cross-source tool + time filtering + platform search + web_fetch, with web settings UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -36,7 +36,10 @@
|
|
|
36
36
|
"aliyun",
|
|
37
37
|
"youcom",
|
|
38
38
|
"multi-search",
|
|
39
|
-
"auto-routing"
|
|
39
|
+
"auto-routing",
|
|
40
|
+
"doubao",
|
|
41
|
+
"volcengine",
|
|
42
|
+
"volcano"
|
|
40
43
|
],
|
|
41
44
|
"license": "MIT",
|
|
42
45
|
"publishConfig": {
|