dsh-free-search 0.5.6 → 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 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?
@@ -192,7 +194,7 @@ DSH 0.1.7-rc.1 起,配置跟随 profile 的插件条目保存:设置页与 `
192
194
 
193
195
  ```yaml
194
196
  # profiles/<profile>/cordis.patch.yml 中该条目的 config:
195
- 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
196
198
  lang: zh # 设置页界面语言(zh / en)
197
199
  bingMarket: zh-CN # Bing 市场
198
200
  region: cn-zh # DuckDuckGo 区域(可选)
@@ -340,7 +342,7 @@ This plugin provides multiple free search engines with automatic fallback, compl
340
342
  ### Features
341
343
 
342
344
  - **Zero Cost** — Multiple free engines with no API key or registration required
343
- - **Multi-Engine Support** — DuckDuckGo (HTML / Lite), Bing, AnySearch AI, SearXNG (meta-search with custom instances), Exa, Tavily, Keenable, Firecrawl, Parallel, Perplexity, SerpBase, DeepSeek Official, and You.com
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
344
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+)
345
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)
346
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)
@@ -379,6 +381,7 @@ If this plugin has been helpful, a ⭐ on [GitHub](https://github.com/DDDMUC/dsh
379
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 |
380
382
  | `kimi` | Kimi (Moonshot) Web Search | Paid | Requires `MOONSHOT_API_KEY` (basic ~¥0.01/call); returns Chinese results with body chunks |
381
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 |
382
385
 
383
386
  - **Default engine is `bing`** (free and most stable), ready to use out of the box after installation.
384
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.
@@ -390,6 +393,7 @@ If this plugin has been helpful, a ⭐ on [GitHub](https://github.com/DDDMUC/dsh
390
393
  - Perplexity: <https://www.perplexity.ai/settings/api>
391
394
  - SerpBase: <https://serpbase.dev>
392
395
  - DeepSeek: <https://platform.deepseek.com/api_keys>
396
+ - Doubao / Volcano Web Search: <https://console.volcengine.com/search-infinity/web-search>
393
397
  - You.com: <https://you.com/platform/api-keys>
394
398
 
395
399
  #### Why are some engines free?
@@ -503,7 +507,7 @@ The old `free-search:` section of `~/.dsh/settings.yaml` is **not** imported by
503
507
 
504
508
  ```yaml
505
509
  # config of that entry in profiles/<profile>/cordis.patch.yml:
506
- 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
507
511
  lang: zh # settings UI language (zh / en)
508
512
  bingMarket: zh-CN # Bing market
509
513
  region: cn-zh # DuckDuckGo region (optional)
package/cordis.patch.yml CHANGED
@@ -4,7 +4,7 @@
4
4
  # The engine is chosen from the sidebar Plugins page -> Free Search row config
5
5
  # (plugins.row.config, DSH 0.1.7-rc.1+), from the /free-search-engine command,
6
6
  # or directly via this entry's config, e.g.:
7
- # provider: bing # engine id: ddg / bing / baidu / kimi / aliyun / auto / ...
7
+ # provider: bing # engine id: ddg / bing / baidu / kimi / aliyun / doubao / auto / ...
8
8
  # region: cn-zh # DuckDuckGo region
9
9
  # bingMarket: zh-CN # Bing market
10
10
  # A legacy ~/.dsh/settings.yaml "free-search:" section is migrated into this
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.5.6";
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
@@ -17,6 +17,7 @@ const KEENABLE_URL = "https://api.keenable.ai/v1/search";
17
17
  const KEENABLE_MCP_URL = "https://api.keenable.ai/mcp";
18
18
  const SERPBASE_URL = "https://api.serpbase.dev/google/search";
19
19
  const YOUCOM_URL = "https://ydc-index.io/v1/search";
20
+ const DOUBAO_SEARCH_URL = "https://open.feedcoopapi.com/search_api/web_search";
20
21
  const USER_AGENT =
21
22
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
22
23
  const ACCEPT_LANG = "zh-CN,zh;q=0.9,en;q=0.8";
@@ -54,11 +55,11 @@ const MARKET_TO_LANG = {
54
55
  const FREE_SEARCH_NS = "web-search-free";
55
56
  const BRIDGE_PREFIX = "/api/dsh-free-search-settings";
56
57
  const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch"];
57
- 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"];
58
59
 
59
60
 
60
61
  // 支持 time_range 过滤的引擎
61
- 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"];
62
63
 
63
64
  // 智能路由(provider=auto 时启用):纯确定性规则重排引擎顺序,末尾接上其余全部引擎保证回退链完整
64
65
  function routeEngines(query, { timeRange } = {}) {
@@ -82,7 +83,7 @@ function routeEngines(query, { timeRange } = {}) {
82
83
  }
83
84
 
84
85
  // 当前插件版本(发布时与 package.json 同步)
85
- const PLUGIN_VERSION = "0.5.6";
86
+ const PLUGIN_VERSION = "0.6.0";
86
87
  // 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
87
88
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
88
89
  const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
@@ -212,14 +213,21 @@ function buildCacheKey(query, maxResults, timeRangeLabel, preferred) {
212
213
  const SNIPPET_NOISE =
213
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;
214
215
 
215
- function cleanSnippet(text) {
216
+ function cleanSnippet(text, max = 300) {
216
217
  if (!text) return text;
217
218
  return String(text)
218
219
  .replace(SNIPPET_NOISE, " ")
219
220
  .replace(/^\s*(#{1,6}\s*|\[\s*x?\s*\]\s*|-\s*\[\s*x?\s*\]\s*|>\s*)/gm, " ")
220
221
  .replace(/\s+/g, " ")
221
222
  .trim()
222
- .slice(0, 300);
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;
223
231
  }
224
232
 
225
233
  // 提示注入防护:插件自有工具(advanced_search / platform_search / free_search_test)的
@@ -1763,6 +1771,98 @@ async function searchAliyun(query, maxResults, apiKey, timeRange, signal) {
1763
1771
 
1764
1772
  return { sources: uniqueSources(sources, maxResults ?? 10), truncated: false };
1765
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
+ }
1766
1866
  //#endregion
1767
1867
 
1768
1868
  //#region bridge
@@ -2152,6 +2252,7 @@ const KEY_REF_MAP = {
2152
2252
  baiduApiKey: "BAIDU_API_KEY",
2153
2253
  kimiApiKey: "MOONSHOT_API_KEY",
2154
2254
  aliyunApiKey: "DASHSCOPE_API_KEY",
2255
+ doubaoApiKey: "DOUBAO_SEARCH_API_KEY",
2155
2256
  };
2156
2257
 
2157
2258
  // rc.1 declares editable fields `.volatile()`: the Loader hands `apply` live
@@ -2182,6 +2283,7 @@ const Config = z.object({
2182
2283
  baiduApiKey: z.string().role("secret").volatile(),
2183
2284
  kimiApiKey: z.string().role("secret").volatile(),
2184
2285
  aliyunApiKey: z.string().role("secret").volatile(),
2286
+ doubaoApiKey: z.string().role("secret").volatile(),
2185
2287
  // 旧版 settings.yaml `free-search:` 段的一次性迁移标记(见 readLegacyFreeSearchSection)
2186
2288
  legacyYamlMigrated: z.boolean().default(false).volatile(),
2187
2289
  });
@@ -2215,6 +2317,7 @@ const LEGACY_MIGRATABLE_KEYS = [
2215
2317
  "baiduApiKey",
2216
2318
  "kimiApiKey",
2217
2319
  "aliyunApiKey",
2320
+ "doubaoApiKey",
2218
2321
  ];
2219
2322
 
2220
2323
  function dshHomeDir() {
@@ -2337,6 +2440,12 @@ function apply(ctx, config) {
2337
2440
  if (settingsKey && cfg[settingsKey]) return cfg[settingsKey];
2338
2441
  return process.env[envName] ?? "";
2339
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
+ };
2340
2449
 
2341
2450
  // 总控 provider:按 settings 的 provider 字段路由到任意引擎。
2342
2451
  // 任何引擎失败(缺 key / 401 / 限流 / 网络)都会自动轮流尝试下一个引擎,
@@ -2391,7 +2500,7 @@ function apply(ctx, config) {
2391
2500
  chain = routeEngines(request.query, { timeRange: Boolean(timeRange) });
2392
2501
  } else {
2393
2502
  // 统一引擎链:首选优先,然后其他付费引擎(有 key 的优先尝试),最后免费引擎
2394
- 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"];
2395
2504
  const freeEngines = ["bing", "anysearch", "ddg", "ddg-lite", "searxng"];
2396
2505
  const timeEngines = TIME_ENGINES;
2397
2506
  if (timeRange) {
@@ -2531,6 +2640,15 @@ function apply(ctx, config) {
2531
2640
  continue;
2532
2641
  }
2533
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);
2534
2652
  } else {
2535
2653
  continue;
2536
2654
  }
@@ -2539,7 +2657,7 @@ function apply(ctx, config) {
2539
2657
  usedEngine = engine;
2540
2658
  // 统一清洗 snippet:去登录/付费墙/订阅噪音,折叠空白(有值的才处理,保持 lossless JSON)
2541
2659
  result.sources = result.sources.map((s) =>
2542
- s.snippet ? { ...s, snippet: cleanSnippet(s.snippet) } : s
2660
+ s.snippet ? { ...s, snippet: cleanSnippet(s.snippet, snippetCap(engine)) } : s
2543
2661
  );
2544
2662
  // 用了非首选引擎时,在结果里附上准确提示(区分"不支持时间过滤被跳过"与"真实失败")。
2545
2663
  // provider=auto 时 preferred 不是引擎而是路由模式:命中的引擎由 provider 字段体现,
@@ -2717,6 +2835,11 @@ function apply(ctx, config) {
2717
2835
  if (!key) return { ok: false, error: "DASHSCOPE_API_KEY not configured" };
2718
2836
  return await searchAliyun(q, n, key, tr);
2719
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
+ }
2720
2843
  case "you": {
2721
2844
  const key = await resolveApiKey("YOUCOM_API_KEY", "youcomApiKey");
2722
2845
  if (!key) return { ok: false, error: "YOUCOM_API_KEY not configured" };
@@ -2738,7 +2861,7 @@ function apply(ctx, config) {
2738
2861
  return {
2739
2862
  ok: true,
2740
2863
  sources: (result.sources ?? []).map((s) =>
2741
- s.snippet ? { ...s, snippet: cleanSnippet(s.snippet) } : s
2864
+ s.snippet ? { ...s, snippet: cleanSnippet(s.snippet, snippetCap(engine)) } : s
2742
2865
  ),
2743
2866
  truncated: result.truncated ?? false,
2744
2867
  ...(typeof result.content === "string" && result.content ? { content: result.content } : {}),
@@ -2758,7 +2881,7 @@ function apply(ctx, config) {
2758
2881
  parameters: {
2759
2882
  engines: {
2760
2883
  type: "array",
2761
- 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.",
2762
2885
  items: { type: "string" },
2763
2886
  },
2764
2887
  query: {
@@ -2879,7 +3002,7 @@ function apply(ctx, config) {
2879
3002
  },
2880
3003
  },
2881
3004
  render(args, value) {
2882
- const lines = value.sources.map((s, i) => `- [${s.title ?? s.url}](${s.url})${s.snippet ? ` - ${s.snippet.slice(0, 120)}` : ""}`);
3005
+ const lines = value.sources.map((s, i) => `- [${s.title ?? s.url}](${s.url})${s.snippet ? ` - ${s.snippet}` : ""}`);
2883
3006
  return [{ type: "text", text: `Platform search (${value.platform}):\n${wrapUntrustedBlock(lines.join("\n") || "No results found.")}` }];
2884
3007
  },
2885
3008
  },
@@ -2944,7 +3067,7 @@ function apply(ctx, config) {
2944
3067
  },
2945
3068
  engine: {
2946
3069
  type: "string",
2947
- 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.",
2948
3071
  },
2949
3072
  },
2950
3073
  output: {
@@ -2970,7 +3093,7 @@ function apply(ctx, config) {
2970
3093
  },
2971
3094
  },
2972
3095
  render(args, value) {
2973
- const lines = value.sources.map((s, i) => `- [${s.title ?? s.url}](${s.url})${s.snippet ? ` - ${s.snippet.slice(0, 120)}` : ""}${s.publishedAt ? ` (${s.publishedAt})` : ""}`);
3096
+ const lines = value.sources.map((s, i) => `- [${s.title ?? s.url}](${s.url})${s.snippet ? ` - ${s.snippet}` : ""}${s.publishedAt ? ` (${s.publishedAt})` : ""}`);
2974
3097
  const body = `${lines.join("\n") || "No results found."}${value.content ? `\n\n${value.content}` : ""}`;
2975
3098
  return [{ type: "text", text: `Search (${value.provider}${args.timeRange ? `, timeRange=${args.timeRange}` : ""}):\n${wrapUntrustedBlock(body)}` }];
2976
3099
  },
@@ -3066,7 +3189,7 @@ function apply(ctx, config) {
3066
3189
  render(args, value) {
3067
3190
  const lines = value.sources.map((s) => {
3068
3191
  const seenStr = Array.isArray(s.seenIn) && s.seenIn.length > 0 ? ` [seen in: ${s.seenIn.join(", ")}]` : "";
3069
- return `- [${s.title ?? s.url}](${s.url})${seenStr}${s.snippet ? ` - ${s.snippet.slice(0, 120)}` : ""}${s.publishedAt ? ` (${s.publishedAt})` : ""}`;
3192
+ return `- [${s.title ?? s.url}](${s.url})${seenStr}${s.snippet ? ` - ${s.snippet}` : ""}${s.publishedAt ? ` (${s.publishedAt})` : ""}`;
3070
3193
  });
3071
3194
  const body = `${lines.join("\n") || "No results found."}${value.content ? `\n\n${value.content}` : ""}`;
3072
3195
  return [{ type: "text", text: `Multi-Search (${value.provider}):\n${wrapUntrustedBlock(body)}` }];
@@ -3229,6 +3352,7 @@ function apply(ctx, config) {
3229
3352
  "- baidu - paid, requires BAIDU_API_KEY",
3230
3353
  "- kimi - paid, requires MOONSHOT_API_KEY",
3231
3354
  "- aliyun - paid, requires DASHSCOPE_API_KEY",
3355
+ "- doubao - requires DOUBAO_SEARCH_API_KEY (Volcano Web Search / 豆包搜索; 500 free queries/month)",
3232
3356
  "",
3233
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.",
3234
3358
  "",
@@ -3284,6 +3408,7 @@ export {
3284
3408
  routeEngines,
3285
3409
  searchAnysearch,
3286
3410
  searchAliyun,
3411
+ searchDoubao,
3287
3412
  searchBaidu,
3288
3413
  searchKimi,
3289
3414
  searchBing,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-free-search",
3
- "version": "0.5.6",
4
- "description": "Free web search for DeepSeek Harness: 17 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable/Firecrawl/Parallel keyless; Perplexity/SerpBase/DeepSeek/You.com/Baidu/Kimi/Aliyun with key) + auto smart routing + multi_search cross-source tool + time filtering + platform search + web_fetch, with web settings UI.",
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": {