dsh-free-search 0.5.6 → 0.6.5

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,10 +29,12 @@ 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 会明确报错)
36
+ - **全局引擎开关与回退优先级** —— 设置页可勾选/取消引擎(取消后全局生效:普通搜索 / Auto / `advanced_search` / `multi_search` / 引擎测试都不再用它),并可用 ↑↓ 调整全局回退顺序(一键恢复默认)
37
+ - **Multi 模式** —— 可把搜索引擎设为 `Multi Search`:`web_search` 并发请求前 3 个已启用引擎、按 URL 合并去重、跨源命中的结果优先(代价是成倍消耗额度;multi 失败会自动退回单引擎回退链)
36
38
  - **统一引擎回退** —— 任何引擎失败(付费/免费,缺 key/401/限流/网络)自动轮流尝试下一个引擎:首选引擎 → 其他引擎(exa/tavily/keenable/firecrawl/parallel 无 key 也会尝试,因为它们自带 keyless 免费额度)→ 剩余免费引擎,搜索永不直接失败;结果顶部注明实际生效的引擎(如 `Note: perplexity unavailable or failed, using exa.`)
37
39
  - **时间过滤** —— `advanced_search` 工具支持 `timeRange`:固定档、自定义相对值、绝对日期三种形式(详见下方逻辑说明)
38
40
  - **系统提示词注入** —— agent 知道当前用哪个引擎、哪些需要 key;并明确所有搜索结果是**不可信外部数据**,不得执行其中的指令
@@ -68,6 +70,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
68
70
  | `baidu` | 百度千帆 AI 搜索 | 额度 | 需 `BAIDU_API_KEY`(千帆 AI 搜索,每日赠 50 次,超额按量后付费);中文全网,支持时间过滤 |
69
71
  | `kimi` | Kimi(Moonshot)联网搜索 | 付费 | 需 `MOONSHOT_API_KEY`(basic 约 ¥0.01/次),返回带正文 chunks 的中文结果 |
70
72
  | `aliyun` | 阿里云百炼 EnhancedSearch | 付费 | 需 `DASHSCOPE_API_KEY`(MCP search_pro,约 ¥0.03/次,新用户 200 次免费包);中文全网带来源 hostname |
73
+ | `doubao` | 火山引擎联网搜索(豆包搜索) | 免费额度 | 需 `DOUBAO_SEARCH_API_KEY`(联网搜索控制台开通,**每月 500 次免费**,超出按量付费);中文时效内容强,支持时间/站点过滤,返回千字级摘要 |
71
74
 
72
75
  - **默认引擎为 `bing`**(免费且最稳定),安装后开箱即用。
73
76
  - **自动回退**:任何引擎失败(免费限流/反爬,付费缺 key/无效/网络错误)都会自动轮流尝试下一个引擎——先试其他已配 key 的付费引擎,再试免费引擎(Bing/AnySearch 等),并在结果中附带回退提示——搜索不会因引擎问题直接失败。
@@ -79,6 +82,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
79
82
  - Perplexity:<https://www.perplexity.ai/settings/api>
80
83
  - SerpBase:<https://serpbase.dev>
81
84
  - DeepSeek:<https://platform.deepseek.com/api_keys>
85
+ - 豆包搜索(火山联网搜索):<https://console.volcengine.com/search-infinity/web-search>
82
86
  - You.com:<https://you.com/platform/api-keys>
83
87
 
84
88
  #### 为什么免费引擎不需要 key?
@@ -184,6 +188,14 @@ dsh web
184
188
 
185
189
  命令只改首选引擎配置,搜索仍走 `web_search` + 统一回退链:即使首选引擎挂了也会自动换其他引擎,永不直接失败。系统提示词同步刷新。
186
190
 
191
+ #### 引擎开关、回退优先级与 Multi 模式
192
+
193
+ 设置页有三个新块(都会保存进条目 config):
194
+
195
+ - **全局启用的搜索引擎**:取消勾选后,该引擎会从普通 `web_search` 回退链、Auto 智能路由、`advanced_search`、`multi_search` 和引擎测试中**全局排除**;至少要保留一个引擎。
196
+ - **全局回退优先级**:用 ↑↓ 调整先后顺序。首选引擎仍先尝试;Auto 保留"语言/时间"路由规则,但自定义后同一分组内及后续回退按此顺序。被禁用的引擎保留在列表里(标记「已禁用」)但不执行;「恢复默认顺序」一键还原。
197
+ - **搜索引擎下拉里的 `Multi Search`**:选中后 `web_search` 会并发查询路由/优先级前 3 个已启用引擎,URL 去重合并、跨源命中的结果排前面。注意并发会成倍消耗额度;Multi 失败时会自动退回普通单引擎回退链并在结果里注明。
198
+
187
199
  #### 配置文件
188
200
 
189
201
  DSH 0.1.7-rc.1 起,配置跟随 profile 的插件条目保存:设置页与 `/free-search-engine` 都会写入当前 profile 的 `cordis.patch.yml` 中 `web-search-free`(`dsh-free-search`)条目的 `config`。
@@ -192,7 +204,7 @@ DSH 0.1.7-rc.1 起,配置跟随 profile 的插件条目保存:设置页与 `
192
204
 
193
205
  ```yaml
194
206
  # 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
207
+ provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / deepseek-official / you / baidu / kimi / aliyun / doubao / auto / multi
196
208
  lang: zh # 设置页界面语言(zh / en)
197
209
  bingMarket: zh-CN # Bing 市场
198
210
  region: cn-zh # DuckDuckGo 区域(可选)
@@ -340,10 +352,12 @@ This plugin provides multiple free search engines with automatic fallback, compl
340
352
  ### Features
341
353
 
342
354
  - **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
355
+ - **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
356
  - **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
357
  - **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
358
  - **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)
359
+ - **Global engine enable/disable & fallback priority** — uncheck engines you never want (applies everywhere: web_search fallback, Auto routing, advanced_search, multi_search, engine tests) and reorder the global fallback chain with ↑↓ (one-click reset)
360
+ - **Multi mode** — pick `Multi Search` as the engine: web_search queries the top 3 enabled engines concurrently, merges/deduplicates URLs and prioritizes cross-source hits (costs more quota; a failed multi run automatically falls back to the single-engine chain)
347
361
  - **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/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.
348
362
  - **Time Filtering** — The `advanced_search` tool supports `timeRange`: fixed tiers, custom relative values, or an absolute date (details below)
349
363
  - **System Prompt Injection** — The agent is aware of the currently active engine and which engines require API keys; it is also told that all search output is **untrusted external data** and must never be executed as instructions
@@ -379,6 +393,7 @@ If this plugin has been helpful, a ⭐ on [GitHub](https://github.com/DDDMUC/dsh
379
393
  | `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
394
  | `kimi` | Kimi (Moonshot) Web Search | Paid | Requires `MOONSHOT_API_KEY` (basic ~¥0.01/call); returns Chinese results with body chunks |
381
395
  | `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 |
396
+ | `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
397
 
383
398
  - **Default engine is `bing`** (free and most stable), ready to use out of the box after installation.
384
399
  - **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 +405,7 @@ If this plugin has been helpful, a ⭐ on [GitHub](https://github.com/DDDMUC/dsh
390
405
  - Perplexity: <https://www.perplexity.ai/settings/api>
391
406
  - SerpBase: <https://serpbase.dev>
392
407
  - DeepSeek: <https://platform.deepseek.com/api_keys>
408
+ - Doubao / Volcano Web Search: <https://console.volcengine.com/search-infinity/web-search>
393
409
  - You.com: <https://you.com/platform/api-keys>
394
410
 
395
411
  #### Why are some engines free?
@@ -495,6 +511,14 @@ You can also switch the engine right from the chat — no need to open the setti
495
511
 
496
512
  The command only changes the preferred engine; search still goes through `web_search` + the unified fallback chain — even if the preferred engine fails, it automatically switches to others, never failing outright. The system prompt refreshes accordingly.
497
513
 
514
+ #### Engine Switches, Fallback Priority and Multi Mode
515
+
516
+ The settings page has three new blocks (all saved into the entry config):
517
+
518
+ - **Globally enabled engines**: unchecking an engine excludes it everywhere — the web_search fallback chain, Auto routing, `advanced_search`, `multi_search` and engine tests. At least one engine must remain enabled.
519
+ - **Global fallback priority**: use ↑↓ to reorder. The preferred engine is still tried first; Auto keeps its language/time routing, while a customized order governs engines inside a route group and the remaining fallback chain. Disabled engines stay in the list (marked "disabled") but are skipped; "Reset default order" restores the defaults.
520
+ - **`Multi Search` in the engine dropdown**: web_search then queries the top 3 enabled routed/prioritized engines concurrently, merges/deduplicates URLs and prioritizes cross-source hits. This multiplies quota usage; if a multi run fails it automatically falls back to the single-engine chain and says so in the result note.
521
+
498
522
  #### Configuration File
499
523
 
500
524
  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`.
@@ -503,7 +527,7 @@ The old `free-search:` section of `~/.dsh/settings.yaml` is **not** imported by
503
527
 
504
528
  ```yaml
505
529
  # 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
530
+ provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / deepseek-official / you / baidu / kimi / aliyun / doubao / auto / multi
507
531
  lang: zh # settings UI language (zh / en)
508
532
  bingMarket: zh-CN # Bing market
509
533
  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 / multi / ...
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
@@ -19,6 +19,7 @@
19
19
  name: dsh-free-search
20
20
  config:
21
21
  provider: bing
22
+ disabledEngines: []
22
23
  bingMarket: zh-CN
23
24
  # 自动接管默认搜索:让 harness 的 web_search 直接用本插件的 provider(内部含全部引擎)
24
25
  # 注意 patch 语义是"整行替换 config"(DSH 0.1.2+):必须保留 fetchProvider,否则
package/lib/client.js CHANGED
@@ -59,6 +59,15 @@ window.__ModuleLoader__.load({
59
59
  ".dshfs-platforms{display:flex;gap:12px 16px;flex-wrap:wrap}",
60
60
  ".dshfs-platform{display:flex;align-items:center;gap:6px;color:inherit;font-size:14px;cursor:pointer}",
61
61
  ".dshfs-platform input{accent-color:#0071e3;width:16px;height:16px}",
62
+ ".dshfs-priorityList{display:flex;flex-direction:column;gap:6px}",
63
+ ".dshfs-priorityRow{display:flex;align-items:center;gap:8px;padding:6px 8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:rgba(0,0,0,.02)}",
64
+ "@media (prefers-color-scheme: dark){.dshfs-priorityRow{border-color:rgba(255,255,255,.09);background:rgba(255,255,255,.035)}}",
65
+ ".dshfs-priorityRowDisabled{opacity:.58}",
66
+ ".dshfs-priorityIndex{width:26px;flex:none;color:#6e6e73;font-size:12px;font-variant-numeric:tabular-nums;text-align:right}",
67
+ "@media (prefers-color-scheme: dark){.dshfs-priorityIndex{color:#a1a1a6}}",
68
+ ".dshfs-priorityName{flex:1;min-width:0;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
69
+ ".dshfs-priorityActions{display:flex;gap:4px;flex:none}",
70
+ ".dshfs-priorityBtn{width:30px;height:28px;padding:0;font-size:14px}",
62
71
  "@media (prefers-color-scheme: dark){.dshfs-platform input{accent-color:#0a84ff}}",
63
72
  ".dshfs-link{color:#0071e3;font-size:13px;text-decoration:none;align-self:flex-start;padding:2px 0;font-weight:500}",
64
73
  ".dshfs-link:hover{text-decoration:underline}",
@@ -80,6 +89,8 @@ window.__ModuleLoader__.load({
80
89
  "@media (prefers-color-scheme: dark){.dshfs-badge{background:rgba(255,255,255,.1);color:#f5f5f7}}",
81
90
  ".dshfs-badgeAuto{background:rgba(175,82,222,.15);color:#af52de;border:1px solid rgba(175,82,222,.2)}",
82
91
  "@media (prefers-color-scheme: dark){.dshfs-badgeAuto{background:rgba(191,90,242,.15);color:#bf5af2;border-color:rgba(191,90,242,.2)}}",
92
+ ".dshfs-badgeMulti{background:rgba(0,122,255,.13);color:#007aff;border:1px solid rgba(0,122,255,.2)}",
93
+ "@media (prefers-color-scheme: dark){.dshfs-badgeMulti{background:rgba(10,132,255,.15);color:#0a84ff;border-color:rgba(10,132,255,.25)}}",
83
94
  ".dshfs-badgeFree{background:rgba(52,199,89,.15);color:#248a3d;border:1px solid rgba(52,199,89,.2)}",
84
95
  "@media (prefers-color-scheme: dark){.dshfs-badgeFree{background:rgba(48,209,88,.15);color:#30d158;border-color:rgba(48,209,88,.2)}}",
85
96
  ".dshfs-badgeKey{background:rgba(255,149,0,.15);color:#cc7a00;border:1px solid rgba(255,149,0,.2)}",
@@ -122,7 +133,7 @@ window.__ModuleLoader__.load({
122
133
  searchEngine: "搜索引擎",
123
134
  visit: "访问官网 →",
124
135
  getKey: "获取 API Key →",
125
- engineHint: "Auto 会根据查询语言及时间过滤自动优选引擎。Bing 是最稳定的免费引擎。DuckDuckGo 在共享 IP 上可能限流。API KEY 引擎需在下方填写凭据。",
136
+ engineHint: "Multi Search 会并发搜索路由/优先级前 3 个已启用引擎并合并去重;Auto 根据查询语言及时间过滤自动优选单个引擎。Bing 是稳定的免费引擎。API KEY 引擎需在下方填写凭据。",
126
137
  apiKeys: "API 密钥(可选)",
127
138
  anysearchPh: (c) => c ? "AnySearch API 密钥(已配置)" : "AnySearch API 密钥(可选,不填免费匿名,填了提额)",
128
139
  exaPh: (c) => c ? "Exa API 密钥(已配置)" : "Exa API 密钥(可选,不填也可免费使用)",
@@ -137,12 +148,19 @@ window.__ModuleLoader__.load({
137
148
  baiduPh: (c) => c ? "Baidu API 密钥(已配置)" : "百度千帆 API 密钥(BAIDU_API_KEY)",
138
149
  kimiPh: (c) => c ? "Kimi API 密钥(已配置)" : "Kimi API 密钥(MOONSHOT_API_KEY)",
139
150
  aliyunPh: (c) => c ? "Aliyun API 密钥(已配置)" : "阿里云 DashScope 密钥(DASHSCOPE_API_KEY)",
151
+ doubaoPh: (c) => c ? "豆包搜索密钥(已配置)" : "豆包搜索 / 火山联网搜索密钥(DOUBAO_SEARCH_API_KEY,每月 500 次免费)",
140
152
  keysHint: "密钥读取优先级:.credentials.yaml 凭据中心 > 这里 > 环境变量。推荐把 key 写进凭据中心(与官方 LLM 一致,一处管理)。",
141
153
  keyStorage: "Key 存储位置",
142
154
  keyStorageCred: "凭据中心(推荐)",
143
155
  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(", ") || "无"}`,
156
+ 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
157
  keyStorageSettingsHint: "保存后写入当前 profile 的插件条目 config(cordis.patch.yml,优先级低于凭据中心)。",
158
+ engineEnable: "全局启用的搜索引擎",
159
+ engineEnableHint: "取消勾选后,该引擎会从普通 web_search 回退链、Auto 智能路由、advanced_search、multi_search 和引擎测试中全局排除。至少保留一个引擎。",
160
+ fallbackPriority: "全局回退优先级",
161
+ fallbackPriorityHint: "从上到下为回退顺序。首选引擎仍先尝试;Auto 保留语言/时间路由,但自定义后同一分组内及后续回退按此顺序。禁用引擎不会执行,但会保留当前位置。",
162
+ fallbackPriorityReset: "恢复默认顺序",
163
+ priorityDisabled: "已禁用",
146
164
  platformSearch: "平台搜索(platform_search 工具)",
147
165
  platformHint: "为 agent 的 platform_search 工具启用平台。禁用的平台会被跳过。",
148
166
  cacheTtl: "结果缓存时长(分钟)",
@@ -195,7 +213,7 @@ window.__ModuleLoader__.load({
195
213
  searchEngine: "Search engine",
196
214
  visit: "Visit website →",
197
215
  getKey: "Get API Key →",
198
- engineHint: "Auto smartly routes engines based on query language and time filter. Bing is the most stable FREE engine. DuckDuckGo may rate-limit on shared IPs. API KEY engines need credentials below.",
216
+ engineHint: "Multi Search queries the top 3 enabled routed/prioritized engines concurrently and merges/deduplicates results. Auto smartly routes a single engine by query language and time filter. API KEY engines need credentials below.",
199
217
  apiKeys: "API keys (optional)",
200
218
  anysearchPh: (c) => c ? "AnySearch API key (configured)" : "AnySearch API key (optional, free anonymous without; key raises quota)",
201
219
  exaPh: (c) => c ? "Exa API key (configured)" : "Exa API key (optional, free without)",
@@ -210,12 +228,19 @@ window.__ModuleLoader__.load({
210
228
  baiduPh: (c) => c ? "Baidu API key (configured)" : "Baidu Qianfan API key (BAIDU_API_KEY)",
211
229
  kimiPh: (c) => c ? "Kimi API key (configured)" : "Kimi API key (MOONSHOT_API_KEY)",
212
230
  aliyunPh: (c) => c ? "Aliyun API key (configured)" : "Aliyun DashScope API key (DASHSCOPE_API_KEY)",
231
+ doubaoPh: (c) => c ? "Doubao search key (configured)" : "Doubao / Volcano Web Search key (DOUBAO_SEARCH_API_KEY, 500 free/month)",
213
232
  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
233
  keyStorage: "Key storage",
215
234
  keyStorageCred: "Credential center (recommended)",
216
235
  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"}`,
236
+ 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
237
  keyStorageSettingsHint: "Saved to the active profile plugin entry config (cordis.patch.yml; lower priority than the credential center).",
238
+ engineEnable: "Globally enabled search engines",
239
+ engineEnableHint: "Unchecked engines are excluded globally from web_search fallback, Auto routing, advanced_search, multi_search, and engine tests. At least one engine must remain enabled.",
240
+ fallbackPriority: "Global fallback priority",
241
+ fallbackPriorityHint: "Top to bottom is the fallback order. The preferred engine is still tried first. Auto keeps language/time routing, while customized priority orders engines within a route group and the remaining fallback chain. Disabled engines keep their position but are skipped.",
242
+ fallbackPriorityReset: "Reset default order",
243
+ priorityDisabled: "disabled",
219
244
  platformSearch: "Platform search (platform_search tool)",
220
245
  platformHint: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
221
246
  cacheTtl: "Result cache TTL (minutes)",
@@ -265,9 +290,10 @@ window.__ModuleLoader__.load({
265
290
  };
266
291
  const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
267
292
  // 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
268
- const PLUGIN_VERSION = "0.5.6";
293
+ const PLUGIN_VERSION = "0.6.5";
269
294
  const ENGINES = [
270
295
  { id: "auto", label: "Auto · 智能路由", badge: "AUTO", link: null },
296
+ { id: "multi", label: "Multi Search · 多引擎", badge: "MULTI", link: null },
271
297
  { id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
272
298
  { id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
273
299
  { id: "bing", label: "Bing", badge: "FREE", link: "https://www.bing.com" },
@@ -285,7 +311,14 @@ window.__ModuleLoader__.load({
285
311
  { id: "baidu", label: "Baidu · 百度千帆", badge: "API KEY", link: "https://console.bce.baidu.com/qianfan" },
286
312
  { id: "kimi", label: "Kimi · Moonshot", badge: "API KEY", link: "https://platform.moonshot.cn" },
287
313
  { id: "aliyun", label: "Aliyun · 百炼", badge: "API KEY", link: "https://bailian.console.aliyun.com" },
314
+ { id: "doubao", label: "Doubao · 豆包搜索", badge: "API KEY", link: "https://console.volcengine.com/search-infinity/web-search" },
288
315
  ];
316
+ const ENGINE_IDS = ENGINES.filter((engine) => engine.id !== "auto" && engine.id !== "multi").map((engine) => engine.id);
317
+ const DEFAULT_FALLBACK_ORDER = ["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek-official", "you", "baidu", "kimi", "aliyun", "doubao", "bing", "anysearch", "ddg", "ddg-lite", "searxng"];
318
+ const normalizeFallbackOrder = (value) => {
319
+ const configured = Array.isArray(value) ? value.filter((id) => ENGINE_IDS.includes(id)) : [];
320
+ return [...new Set([...configured, ...DEFAULT_FALLBACK_ORDER, ...ENGINE_IDS])];
321
+ };
289
322
 
290
323
  async function bridgeDescribe() {
291
324
  const response = await fetch(`${BRIDGE_PREFIX}/describe`, {
@@ -385,6 +418,8 @@ window.__ModuleLoader__.load({
385
418
  const [open, setOpen] = react.useState(pageMode);
386
419
  const [state, setState] = react.useState({ status: "loading" });
387
420
  const [provider, setProvider] = react.useState("bing");
421
+ const [disabledEngines, setDisabledEngines] = react.useState([]);
422
+ const [fallbackOrder, setFallbackOrder] = react.useState(DEFAULT_FALLBACK_ORDER);
388
423
  const [safeSearch, setSafeSearch] = react.useState("off");
389
424
  const [bingMarket, setBingMarket] = react.useState("zh-CN");
390
425
  const [anysearchKey, setAnysearchKey] = react.useState("");
@@ -400,6 +435,7 @@ window.__ModuleLoader__.load({
400
435
  const [baiduKey, setBaiduKey] = react.useState("");
401
436
  const [kimiKey, setKimiKey] = react.useState("");
402
437
  const [aliyunKey, setAliyunKey] = react.useState("");
438
+ const [doubaoKey, setDoubaoKey] = react.useState("");
403
439
  const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
404
440
  const [cacheTtl, setCacheTtl] = react.useState(5);
405
441
  const [keysConfigured, setKeysConfigured] = react.useState({});
@@ -427,6 +463,12 @@ window.__ModuleLoader__.load({
427
463
  if (view) {
428
464
  const v = view.value ?? {};
429
465
  setProvider(v.provider ?? "ddg");
466
+ setDisabledEngines(
467
+ Array.isArray(v.disabledEngines)
468
+ ? [...new Set(v.disabledEngines.filter((id) => ENGINES.some((engine) => engine.id === id && id !== "auto" && id !== "multi")))]
469
+ : []
470
+ );
471
+ setFallbackOrder(normalizeFallbackOrder(v.fallbackOrder));
430
472
  setSafeSearch(v.safeSearch === "strict" || v.safeSearch === "moderate" ? v.safeSearch : "off");
431
473
  setBingMarket(v.bingMarket === undefined ? "zh-CN" : v.bingMarket);
432
474
  setLang(v.lang === "en" ? "en" : "zh");
@@ -443,6 +485,7 @@ window.__ModuleLoader__.load({
443
485
  setBaiduKey(v.baiduApiKey ?? "");
444
486
  setKimiKey(v.kimiApiKey ?? "");
445
487
  setAliyunKey(v.aliyunApiKey ?? "");
488
+ setDoubaoKey(v.doubaoApiKey ?? "");
446
489
  setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
447
490
  setCacheTtl(v.cacheTtl === undefined ? 5 : Math.min(Math.max(Number(v.cacheTtl) ?? 5, 0), 5));
448
491
  // secrets 字段标记哪些 key 已配置(值被脱敏,仅显示"已配置")
@@ -463,6 +506,7 @@ window.__ModuleLoader__.load({
463
506
  if (path === "baiduApiKey") configured.baidu = true;
464
507
  if (path === "kimiApiKey") configured.kimi = true;
465
508
  if (path === "aliyunApiKey") configured.aliyun = true;
509
+ if (path === "doubaoApiKey") configured.doubao = true;
466
510
  }
467
511
  }
468
512
  setKeysConfigured(configured);
@@ -474,7 +518,7 @@ window.__ModuleLoader__.load({
474
518
  const cred = await bridgeCredentialsStatus();
475
519
  if (cred.ok) {
476
520
  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" };
521
+ 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
522
  for (const [k, v] of Object.entries(cred.value.configured ?? {})) {
479
523
  if (map[k]) cc[map[k]] = v;
480
524
  }
@@ -524,6 +568,7 @@ window.__ModuleLoader__.load({
524
568
  ["baiduApiKey", baiduKey],
525
569
  ["kimiApiKey", kimiKey],
526
570
  ["aliyunApiKey", aliyunKey],
571
+ ["doubaoApiKey", doubaoKey],
527
572
  ];
528
573
  if (keyStorage === "credentials") {
529
574
  for (const [field, value] of keyFields) {
@@ -537,6 +582,8 @@ window.__ModuleLoader__.load({
537
582
  }
538
583
  }
539
584
  const ops = [{ op: "set", path: ["provider"], value: provider }];
585
+ ops.push({ op: "set", path: ["disabledEngines"], value: disabledEngines });
586
+ ops.push({ op: "set", path: ["fallbackOrder"], value: normalizeFallbackOrder(fallbackOrder) });
540
587
  ops.push({ op: "set", path: ["lang"], value: lang });
541
588
  ops.push({ op: "set", path: ["keyStorage"], value: keyStorage });
542
589
  ops.push({ op: "set", path: ["safeSearch"], value: safeSearch });
@@ -662,13 +709,29 @@ window.__ModuleLoader__.load({
662
709
  const t = tt(lang);
663
710
  const title = "Free Search";
664
711
  const description = t.description;
712
+ const enabledEngineCount = ENGINES.filter((e) => e.id !== "auto" && e.id !== "multi" && !disabledEngines.includes(e.id)).length;
665
713
  const currentEngine = ENGINES.find((e) => e.id === provider) ?? ENGINES.find((e) => e.id === "bing") ?? ENGINES[0];
714
+ const moveFallbackEngine = (engineId, delta) => {
715
+ setFallbackOrder((prev) => {
716
+ const next = normalizeFallbackOrder(prev);
717
+ const from = next.indexOf(engineId);
718
+ const to = from + delta;
719
+ if (from < 0 || to < 0 || to >= next.length) return next;
720
+ [next[from], next[to]] = [next[to], next[from]];
721
+ return next;
722
+ });
723
+ setDirty(true);
724
+ setFailed(false);
725
+ setSaveError("");
726
+ };
666
727
  const badgeClass =
667
728
  currentEngine.badge === "AUTO"
668
729
  ? "dshfs-badge dshfs-badgeAuto"
669
- : currentEngine.badge === "FREE"
670
- ? "dshfs-badge dshfs-badgeFree"
671
- : "dshfs-badge dshfs-badgeKey";
730
+ : currentEngine.badge === "MULTI"
731
+ ? "dshfs-badge dshfs-badgeMulti"
732
+ : currentEngine.badge === "FREE"
733
+ ? "dshfs-badge dshfs-badgeFree"
734
+ : "dshfs-badge dshfs-badgeKey";
672
735
 
673
736
  const toggleLang = () => {
674
737
  setLang((prev) => (prev === "en" ? "zh" : "en"));
@@ -728,7 +791,11 @@ react_jsx_runtime.jsx("div", {
728
791
  disabled: !ready || saving,
729
792
  onChange: (e) => select(e.target.value),
730
793
  children: ENGINES.map((engine) =>
731
- react_jsx_runtime.jsx("option", { value: engine.id, children: `${engine.label} (${engine.badge})` }, engine.id)
794
+ react_jsx_runtime.jsx("option", {
795
+ value: engine.id,
796
+ disabled: engine.id !== "auto" && engine.id !== "multi" && disabledEngines.includes(engine.id),
797
+ children: `${engine.label} (${engine.badge})${engine.id !== "auto" && engine.id !== "multi" && disabledEngines.includes(engine.id) ? " · DISABLED" : ""}`,
798
+ }, engine.id)
732
799
  ),
733
800
  }),
734
801
  currentEngine.link
@@ -749,6 +816,118 @@ react_jsx_runtime.jsx("div", {
749
816
  }),
750
817
  ],
751
818
  }),
819
+ react_jsx_runtime.jsx("div", {
820
+ className: "dshfs-field",
821
+ children: [
822
+ react_jsx_runtime.jsx("div", {
823
+ className: "dshfs-label",
824
+ children: t.engineEnable,
825
+ }),
826
+ react_jsx_runtime.jsx("div", {
827
+ className: "dshfs-platforms",
828
+ children: ENGINES.filter((engine) => engine.id !== "auto" && engine.id !== "multi").map((engine) => {
829
+ const enabled = !disabledEngines.includes(engine.id);
830
+ const lastEnabled = enabled && enabledEngineCount <= 1;
831
+ return react_jsx_runtime.jsx("label", {
832
+ className: "dshfs-platform",
833
+ children: [
834
+ react_jsx_runtime.jsx("input", {
835
+ type: "checkbox",
836
+ checked: enabled,
837
+ disabled: !ready || saving || lastEnabled,
838
+ onChange: (e) => {
839
+ const nextEnabled = e.target.checked;
840
+ setDisabledEngines((prev) => {
841
+ const next = nextEnabled
842
+ ? prev.filter((id) => id !== engine.id)
843
+ : [...new Set([...prev, engine.id])];
844
+ return next;
845
+ });
846
+ if (!nextEnabled && provider === engine.id) setProvider("auto");
847
+ setDirty(true);
848
+ setFailed(false);
849
+ setSaveError("");
850
+ },
851
+ }),
852
+ `${engine.label} (${engine.badge})`,
853
+ ],
854
+ }, engine.id);
855
+ }),
856
+ }),
857
+ react_jsx_runtime.jsx("p", {
858
+ className: "dshfs-hint",
859
+ children: t.engineEnableHint,
860
+ }),
861
+ ],
862
+ }),
863
+ react_jsx_runtime.jsx("div", {
864
+ className: "dshfs-field",
865
+ children: [
866
+ react_jsx_runtime.jsx("div", {
867
+ className: "dshfs-label",
868
+ children: t.fallbackPriority,
869
+ }),
870
+ react_jsx_runtime.jsx("div", {
871
+ className: "dshfs-priorityList",
872
+ children: fallbackOrder.map((engineId, index) => {
873
+ const engine = ENGINES.find((item) => item.id === engineId);
874
+ if (!engine) return null;
875
+ const isDisabled = disabledEngines.includes(engineId);
876
+ return react_jsx_runtime.jsx("div", {
877
+ className: "dshfs-priorityRow" + (isDisabled ? " dshfs-priorityRowDisabled" : ""),
878
+ children: [
879
+ react_jsx_runtime.jsx("span", { className: "dshfs-priorityIndex", children: String(index + 1) }),
880
+ react_jsx_runtime.jsx("span", {
881
+ className: "dshfs-priorityName",
882
+ children: `${engine.label} (${engine.badge})${isDisabled ? " · " + t.priorityDisabled : ""}`,
883
+ }),
884
+ react_jsx_runtime.jsx("span", {
885
+ className: "dshfs-priorityActions",
886
+ children: [
887
+ react_jsx_runtime.jsx("button", {
888
+ type: "button",
889
+ className: "dshfs-btn dshfs-priorityBtn",
890
+ disabled: !ready || saving || index === 0,
891
+ onClick: () => moveFallbackEngine(engineId, -1),
892
+ "aria-label": `${engine.label} ↑`,
893
+ title: "↑",
894
+ children: "↑",
895
+ }, "up"),
896
+ react_jsx_runtime.jsx("button", {
897
+ type: "button",
898
+ className: "dshfs-btn dshfs-priorityBtn",
899
+ disabled: !ready || saving || index === fallbackOrder.length - 1,
900
+ onClick: () => moveFallbackEngine(engineId, 1),
901
+ "aria-label": `${engine.label} ↓`,
902
+ title: "↓",
903
+ children: "↓",
904
+ }, "down"),
905
+ ],
906
+ }),
907
+ ],
908
+ }, engineId);
909
+ }),
910
+ }),
911
+ react_jsx_runtime.jsx("div", {
912
+ children: react_jsx_runtime.jsx("button", {
913
+ type: "button",
914
+ className: "dshfs-btn",
915
+ disabled: !ready || saving || fallbackOrder.join(",") === DEFAULT_FALLBACK_ORDER.join(","),
916
+ onClick: () => {
917
+ setFallbackOrder(DEFAULT_FALLBACK_ORDER.slice());
918
+ setDirty(true);
919
+ setFailed(false);
920
+ setSaveError("");
921
+ },
922
+ children: t.fallbackPriorityReset,
923
+ }),
924
+ }),
925
+ react_jsx_runtime.jsx("p", {
926
+ className: "dshfs-hint",
927
+ children: t.fallbackPriorityHint,
928
+ }),
929
+ ],
930
+ }),
752
931
  react_jsx_runtime.jsx("div", {
753
932
  className: "dshfs-field",
754
933
  children: [
@@ -982,6 +1161,19 @@ react_jsx_runtime.jsx("div", {
982
1161
  setSaveError("");
983
1162
  },
984
1163
  }),
1164
+ react_jsx_runtime.jsx("input", {
1165
+ className: "dshfs-input",
1166
+ type: "password",
1167
+ placeholder: t.doubaoPh(keysConfigured.doubao),
1168
+ value: doubaoKey,
1169
+ disabled: !ready || saving,
1170
+ onChange: (e) => {
1171
+ setDoubaoKey(e.target.value);
1172
+ setDirty(true);
1173
+ setFailed(false);
1174
+ setSaveError("");
1175
+ },
1176
+ }),
985
1177
  react_jsx_runtime.jsx("p", {
986
1178
  className: "dshfs-hint",
987
1179
  children: t.keysHint,
@@ -1218,7 +1410,7 @@ react_jsx_runtime.jsx("div", {
1218
1410
  setFailed(false);
1219
1411
  setSaveError("");
1220
1412
  },
1221
- disabled: saving || !ready || provider === "bing",
1413
+ disabled: saving || !ready || provider === "bing" || disabledEngines.includes("bing"),
1222
1414
  children: t.useBing,
1223
1415
  }),
1224
1416
  react_jsx_runtime.jsx("button", {
@@ -1283,12 +1475,15 @@ react_jsx_runtime.jsx("div", {
1283
1475
  const result = await bridgeDescribe();
1284
1476
  const view = result.ok ? result.value.namespaces.find((n) => n.ns === NS) : undefined;
1285
1477
  const current = view?.value?.provider ?? "bing";
1286
- return ENGINES.map((e) => ({
1287
- id: e.id,
1288
- label: `${e.label}${e.badge === "AUTO" ? " · 智能路由" : e.badge === "FREE" ? " · 免费" : " · API Key"}`,
1289
- detail: e.id === current ? (view?.value?.lang === "en" ? "current" : "当前") : undefined,
1290
- active: e.id === current,
1291
- }));
1478
+ const disabled = new Set(Array.isArray(view?.value?.disabledEngines) ? view.value.disabledEngines : []);
1479
+ return ENGINES
1480
+ .filter((e) => e.id === "auto" || e.id === "multi" || !disabled.has(e.id))
1481
+ .map((e) => ({
1482
+ id: e.id,
1483
+ label: `${e.label}${e.badge === "AUTO" ? " · 智能路由" : e.badge === "MULTI" ? " · 并发合并" : e.badge === "FREE" ? " · 免费" : " · API Key"}`,
1484
+ detail: e.id === current ? (view?.value?.lang === "en" ? "current" : "当前") : undefined,
1485
+ active: e.id === current,
1486
+ }));
1292
1487
  },
1293
1488
  onSelect: async (option) => {
1294
1489
  await bridgeMutate({ ns: NS, ops: [{ op: "set", path: ["provider"], value: option.id }] });