dsh-free-search 0.4.36 → 0.4.38

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
@@ -30,7 +30,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
30
30
 
31
31
  - **零成本** —— 多个免费引擎,无需 key、无需注册
32
32
  - **多引擎可选**:DuckDuckGo(html/lite)、Bing、SearXNG(元搜索,支持自定义实例)、AnySearch、Exa、Tavily、Keenable、Firecrawl、Parallel、Perplexity、SerpBase、DeepSeek 官方
33
- - **网页设置页** —— 引擎切换 + API key 配置(UI 中 key 脱敏显示"已配置")+ 中英文切换;DSH 0.1.6-alpha.2+ 入口在左侧「插件」页的组件行配置,旧版在「设置 → 插件 → 可配置」
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
36
  - **统一引擎回退** —— 任何引擎失败(付费/免费,缺 key/401/限流/网络)自动轮流尝试下一个引擎:首选引擎 → 其他引擎(exa/tavily/keenable/firecrawl/parallel 无 key 也会尝试,因为它们自带 keyless 免费额度)→ 剩余免费引擎,搜索永不直接失败;结果顶部注明实际生效的引擎(如 `Note: perplexity unavailable or failed, using exa.`)
@@ -114,10 +114,9 @@ dsh web
114
114
 
115
115
  #### 网页设置(推荐)
116
116
 
117
- 安装后按 DSH 版本打开配置页:
117
+ 安装后打开配置页(DSH 0.1.7-rc.1+):
118
118
 
119
- - **DSH 0.1.6-alpha.2 及以上**:左侧 **插件** 页 → **已安装** 分组 → `free-search` → 点击组件行 `web-search-free`(行内"配置"入口)
120
- - **DSH 0.1.5 及更早**:**设置 → 插件 → 可配置** 标签页 → **Free Search** 卡片
119
+ - 左侧 **插件** 页 → **已安装** 分组 → `free-search` → 点击组件行 `web-search-free`(行内"配置"入口)
121
120
 
122
121
  配置页提供:
123
122
 
@@ -156,24 +155,24 @@ dsh web
156
155
 
157
156
  #### 配置文件
158
157
 
159
- 配置存在 `~/.dsh/settings.yaml`:
158
+ DSH 0.1.7-rc.1 起,配置跟随 profile 的插件条目保存:设置页与 `/free-search-engine` 都会写入当前 profile 的 `cordis.patch.yml` 中 `web-search-free`(`dsh-free-search`)条目的 `config`。旧版 `~/.dsh/settings.yaml` 的 `free-search:` 段只会在启动时自动导入一次,随后原文件被重命名为 `settings.yaml.imported`。
160
159
 
161
160
  ```yaml
162
- free-search:
163
- provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / deepseek-official
164
- lang: zh # 设置页界面语言(zh / en)
165
- bingMarket: zh-CN # Bing 市场
166
- region: cn-zh # DuckDuckGo 区域(可选)
167
- searxngInstances: # 自定义 SearXNG 实例(可选)
168
- - https://your-instance.example
169
- exaApiKey: ... # 或通过设置页填写
170
- tavilyApiKey: ... # 或通过设置页填写
171
- keenableApiKey: ... # 或通过设置页填写
172
- firecrawlApiKey: ... # 或通过设置页填写
173
- parallelApiKey: ... # 或通过设置页填写
174
- perplexityApiKey: ...
175
- serpbaseApiKey: ... # 或通过设置页填写
176
- deepseekApiKey: ...
161
+ # profiles/<profile>/cordis.patch.yml 中该条目的 config:
162
+ provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / deepseek-official
163
+ lang: zh # 设置页界面语言(zh / en)
164
+ bingMarket: zh-CN # Bing 市场
165
+ region: cn-zh # DuckDuckGo 区域(可选)
166
+ searxngInstances: # 自定义 SearXNG 实例(可选)
167
+ - https://your-instance.example
168
+ exaApiKey: ... # 或通过设置页填写
169
+ tavilyApiKey: ... # 或通过设置页填写
170
+ keenableApiKey: ... # 或通过设置页填写
171
+ firecrawlApiKey: ... # 或通过设置页填写
172
+ parallelApiKey: ... # 或通过设置页填写
173
+ perplexityApiKey: ...
174
+ serpbaseApiKey: ... # 或通过设置页填写
175
+ deepseekApiKey: ...
177
176
  ```
178
177
 
179
178
  #### 让 agent 测试所有引擎
@@ -254,7 +253,7 @@ Search engine test:
254
253
 
255
254
  切换后重启 `dsh web` 生效。
256
255
 
257
- > 配置卡片挂在官方设置页的 `settings.plugin.item` 插槽(dsh 自带),配置读写走插件自建 bridge,**不依赖 dsh-web-ui**,插件可独立使用。
256
+ > 配置卡片挂在左侧「插件」页的 `plugins.row.config` 行配置插槽(dsh 自带),配置读写走插件自建 bridge,**不依赖 dsh-web-ui**,插件可独立使用。
258
257
 
259
258
  ### 代理说明(国内用户)
260
259
 
@@ -270,8 +269,8 @@ Windows 用户:桌面快捷方式已内置此配置(`set NODE_USE_ENV_PROXY=
270
269
 
271
270
  ### 工作原理
272
271
 
273
- - `lib/index.js`:host 端。实现 `WebSearchProvider`(`id` / `available()` / `search()`),统一引擎路由 + 自动回退(付费引擎优先,免费兜底);解析 `timeRange`(固定档/相对值/绝对日期)并透传给各引擎;注册 `free-search` settings namespace;提供 `/api/dsh-free-search-settings` 读写桥 + `raw-search` 调试接口;注册 `free_search_test`、`platform_search`、`advanced_search` 工具;动态注入引擎清单到系统提示词(设置变更时自动刷新)。
274
- - `lib/client.js`:浏览器端。React 配置卡片(引擎选择 + key 输入 + 连通测试 + 中英切换),挂载到官方设置页的 `settings.plugin.item` 插槽;注册 `/free-search-engine` 弹出式切换命令(`commandUi` popupSelect,与 `/model` 同机制)。
272
+ - `lib/index.js`:host 端。实现 `WebSearchProvider`(`id` / `available()` / `search()`),统一引擎路由 + 自动回退(付费引擎优先,免费兜底);解析 `timeRange`(固定档/相对值/绝对日期)并透传给各引擎;在 `web-search-free` 条目上声明可编辑配置(`.volatile()`)并自带设置页(`plugins.row.config`);提供 `/api/dsh-free-search-settings` 读写桥 + `raw-search` 调试接口;注册 `free_search_test`、`platform_search`、`advanced_search` 工具;动态注入引擎清单到系统提示词(设置变更时自动刷新)。
273
+ - `lib/client.js`:浏览器端。React 配置卡片(引擎选择 + key 输入 + 连通测试 + 中英切换),挂载到左侧「插件」页的 `plugins.row.config` 行配置插槽;注册 `/free-search-engine` 弹出式切换命令(`commandUi` popupSelect,与 `/model` 同机制)。
275
274
  - `cordis.patch.yml`:插件 loader 配置。
276
275
 
277
276
  ---
@@ -300,7 +299,7 @@ This plugin provides multiple free search engines with automatic fallback, compl
300
299
 
301
300
  - **Zero Cost** — Multiple free engines with no API key or registration required
302
301
  - **Multi-Engine Support** — DuckDuckGo (HTML / Lite), Bing, AnySearch AI, SearXNG (meta-search with custom instances), Exa, Tavily, Keenable, Firecrawl, Parallel, Perplexity, SerpBase, and DeepSeek Official
303
- - **Web Settings UI** — Engine switching, API key configuration (keys masked as "configured" in the UI), and a Chinese/English toggle; on DSH 0.1.6-alpha.2+ the entry is the component-row config on the sidebar Plugins page, on older versions Settings → Plugins → Configurable
302
+ - **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+)
304
303
  - **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)
305
304
  - **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)
306
305
  - **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.
@@ -384,10 +383,9 @@ This plugin intentionally specifies `@deepseek-ai/dsh-settings` and `@deepseek-a
384
383
 
385
384
  #### Web Settings (Recommended)
386
385
 
387
- After installation, open the config page according to your DSH version:
386
+ After installation, open the config page (DSH 0.1.7-rc.1+):
388
387
 
389
- - **DSH 0.1.6-alpha.2 and later**: sidebar **Plugins** page → **Installed** group → `free-search` → click the `web-search-free` component row (the row's "configure" entry)
390
- - **DSH 0.1.5 and earlier**: **Settings → Plugins → Configurable** tab → **Free Search** card
388
+ - Sidebar **Plugins** page → **Installed** group → `free-search` → click the `web-search-free` component row (the row's "configure" entry)
391
389
 
392
390
  The config page provides:
393
391
 
@@ -426,24 +424,24 @@ The command only changes the preferred engine; search still goes through `web_se
426
424
 
427
425
  #### Configuration File
428
426
 
429
- Configuration is stored in `~/.dsh/settings.yaml`:
427
+ 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`. The old `free-search:` section of `~/.dsh/settings.yaml` is imported once at startup; the file is then renamed to `settings.yaml.imported`.
430
428
 
431
429
  ```yaml
432
- free-search:
433
- provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / deepseek-official
434
- lang: zh # settings UI language (zh / en)
435
- bingMarket: zh-CN # Bing market
436
- region: cn-zh # DuckDuckGo region (optional)
437
- searxngInstances: # Custom SearXNG instances (optional)
438
- - https://your-instance.example
439
- exaApiKey: ... # Or configure via the web settings UI
440
- tavilyApiKey: ... # Or configure via the web settings UI
441
- keenableApiKey: ... # Or configure via the web settings UI
442
- firecrawlApiKey: ... # Or configure via the web settings UI
443
- parallelApiKey: ... # Or configure via the web settings UI
444
- perplexityApiKey: ...
445
- serpbaseApiKey: ... # Or configure via the web settings UI
446
- deepseekApiKey: ...
430
+ # config of that entry in profiles/<profile>/cordis.patch.yml:
431
+ provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / deepseek-official
432
+ lang: zh # settings UI language (zh / en)
433
+ bingMarket: zh-CN # Bing market
434
+ region: cn-zh # DuckDuckGo region (optional)
435
+ searxngInstances: # Custom SearXNG instances (optional)
436
+ - https://your-instance.example
437
+ exaApiKey: ... # Or configure via the web settings UI
438
+ tavilyApiKey: ... # Or configure via the web settings UI
439
+ keenableApiKey: ... # Or configure via the web settings UI
440
+ firecrawlApiKey: ... # Or configure via the web settings UI
441
+ parallelApiKey: ... # Or configure via the web settings UI
442
+ perplexityApiKey: ...
443
+ serpbaseApiKey: ... # Or configure via the web settings UI
444
+ deepseekApiKey: ...
447
445
  ```
448
446
 
449
447
  #### Asking the Agent to Test All Engines
@@ -524,7 +522,7 @@ The `tools/` directory includes a lightweight, zero-dependency switcher:
524
522
 
525
523
  Restart `dsh web` after switching to apply changes.
526
524
 
527
- > The settings card mounts into the official `settings.plugin.item` slot (built into DSH), and configuration reads/writes go through the plugin's own bridge. **No `dsh-web-ui` dependency — the plugin can be used standalone.**
525
+ > The settings card mounts into the official `plugins.row.config` component-row slot on the sidebar Plugins page (built into DSH), and configuration reads/writes go through the plugin's own bridge. **No `dsh-web-ui` dependency — the plugin can be used standalone.**
528
526
 
529
527
  ### Proxy Note (for Users in Mainland China)
530
528
 
@@ -540,8 +538,8 @@ Windows users: The desktop shortcut already includes this configuration (`set NO
540
538
 
541
539
  ### How It Works
542
540
 
543
- - `lib/index.js`: Host side. Implements `WebSearchProvider` (`id` / `available()` / `search()`), unified engine routing + auto-fallback (paid engines first, free as fallback); parses `timeRange` (fixed tiers / relative values / absolute dates) and forwards it to each engine; registers the `free-search` settings namespace; provides the `/api/dsh-free-search-settings` read/write bridge + `raw-search` debug endpoint; registers the `free_search_test`, `platform_search`, and `advanced_search` tools; dynamically injects the engine list into system prompts (auto-refreshes on settings change).
544
- - `lib/client.js`: Browser side. React configuration card (engine select, key inputs, connectivity test, and Chinese/English toggle), mounted into the official `settings.plugin.item` slot; registers the `/free-search-engine` popup switch command (`commandUi` popupSelect, the same mechanism as `/model`).
541
+ - `lib/index.js`: Host side. Implements `WebSearchProvider` (`id` / `available()` / `search()`), unified engine routing + auto-fallback (paid engines first, free as fallback); parses `timeRange` (fixed tiers / relative values / absolute dates) and forwards it to each engine; declares its editable config as volatile fields on the `web-search-free` composition entry and ships its own settings page (`plugins.row.config`); provides the `/api/dsh-free-search-settings` read/write bridge + `raw-search` debug endpoint; registers the `free_search_test`, `platform_search`, and `advanced_search` tools; dynamically injects the engine list into system prompts (auto-refreshes on settings change).
542
+ - `lib/client.js`: Browser side. React configuration card (engine select, key inputs, connectivity test, and Chinese/English toggle), mounted into the official `plugins.row.config` component-row slot on the sidebar Plugins page; registers the `/free-search-engine` popup switch command (`commandUi` popupSelect, the same mechanism as `/model`).
545
543
  - `cordis.patch.yml`: Plugin loader configuration.
546
544
 
547
545
  ### License
package/lib/client.js CHANGED
@@ -83,7 +83,10 @@ window.__ModuleLoader__.load({
83
83
  //#endregion
84
84
 
85
85
  const BRIDGE_PREFIX = "/api/dsh-free-search-settings";
86
- const NS = "free-search";
86
+ // rc.1: the settings namespace is the profile composition entry id (the
87
+ // `web-search-free` row declared by cordis.patch.yml), not the old `free-search`
88
+ // section name. Keep in sync with FREE_SEARCH_NS in lib/index.js.
89
+ const NS = "web-search-free";
87
90
  const I18N = {
88
91
  zh: {
89
92
  description: "免费搜索 —— 无需 API key(Bing / DuckDuckGo / AnySearch / Exa / Tavily / Keenable / Firecrawl / Parallel)",
@@ -106,13 +109,14 @@ window.__ModuleLoader__.load({
106
109
  keyStorageCred: "凭据中心(推荐)",
107
110
  keyStorageSettings: "设置页(兼容)",
108
111
  keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
109
- keyStorageSettingsHint: "保存后写入 settings.yaml(向后兼容路径,优先级低于凭据中心)。",
112
+ keyStorageSettingsHint: "保存后写入当前 profile 的插件条目 config(cordis.patch.yml,优先级低于凭据中心)。",
110
113
  platformSearch: "平台搜索(platform_search 工具)",
111
114
  platformHint: "为 agent 的 platform_search 工具启用平台。禁用的平台会被跳过。",
112
115
  cacheTtl: "结果缓存时长(分钟)",
113
116
  cacheTtlHint: "0 关闭缓存,最长 5 分钟。缩短可加快时效,延长可防限流、省额度。",
114
117
  unavailable: "设置不可用 —— free-search 桥接未暴露。",
115
118
  saveFailed: "保存失败",
119
+ saveFailedDetail: (d) => `保存失败:${d}`,
116
120
  testing: "测试中…",
117
121
  testEngine: "测试引擎",
118
122
  useBing: "恢复 Bing 默认",
@@ -173,13 +177,14 @@ window.__ModuleLoader__.load({
173
177
  keyStorageCred: "Credential center (recommended)",
174
178
  keyStorageSettings: "Settings page (legacy)",
175
179
  keyStorageCredHint: (c) => `Saved to ~/.dsh/.credentials.yaml (highest priority). Currently configured: ${["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "none"}`,
176
- keyStorageSettingsHint: "Saved to settings.yaml (backward-compatible path; lower priority than the credential center).",
180
+ keyStorageSettingsHint: "Saved to the active profile plugin entry config (cordis.patch.yml; lower priority than the credential center).",
177
181
  platformSearch: "Platform search (platform_search tool)",
178
182
  platformHint: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
179
183
  cacheTtl: "Result cache TTL (minutes)",
180
184
  cacheTtlHint: "0 disables caching, max 5 minutes. Lower = fresher results, higher = less rate-limiting / fewer credits used.",
181
185
  unavailable: "Settings unavailable — the free-search bridge is not exposed.",
182
186
  saveFailed: "save failed",
187
+ saveFailedDetail: (d) => `save failed: ${d}`,
183
188
  testing: "Testing…",
184
189
  testEngine: "Test engine",
185
190
  useBing: "Use Bing default",
@@ -222,7 +227,7 @@ window.__ModuleLoader__.load({
222
227
  };
223
228
  const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
224
229
  // 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
225
- const PLUGIN_VERSION = "0.4.36";
230
+ const PLUGIN_VERSION = "0.4.38";
226
231
  const ENGINES = [
227
232
  { id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
228
233
  { id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
@@ -358,6 +363,8 @@ window.__ModuleLoader__.load({
358
363
  const [dirty, setDirty] = react.useState(false);
359
364
  const [saving, setSaving] = react.useState(false);
360
365
  const [failed, setFailed] = react.useState(false);
366
+ // 保存失败明细(credentials-set / mutate 的 code+message),成功或改表单时清空
367
+ const [saveError, setSaveError] = react.useState("");
361
368
  const [testing, setTesting] = react.useState(false);
362
369
  const [testResult, setTestResult] = react.useState(null);
363
370
  const [checkingUpdate, setCheckingUpdate] = react.useState(false);
@@ -435,11 +442,14 @@ window.__ModuleLoader__.load({
435
442
  setProvider(value);
436
443
  setDirty(true);
437
444
  setFailed(false);
445
+ setSaveError("");
438
446
  };
439
447
 
440
448
  const save = async () => {
441
449
  setSaving(true);
442
450
  setFailed(false);
451
+ setSaveError("");
452
+ const errors = [];
443
453
  try {
444
454
  // key 存储分流:凭据中心(默认)走 credentials-set;设置页走 settings mutate(兼容)
445
455
  const keyFields = [
@@ -452,12 +462,15 @@ window.__ModuleLoader__.load({
452
462
  ["deepseekApiKey", deepseekKey],
453
463
  ["serpbaseApiKey", serpbaseKey],
454
464
  ];
455
- let credFailed = false;
456
465
  if (keyStorage === "credentials") {
457
466
  for (const [field, value] of keyFields) {
458
467
  if (!value.trim()) continue;
459
468
  const r = await bridgeCredentialsSet(field, value.trim());
460
- if (!r.ok) credFailed = true;
469
+ if (!r || !r.ok) {
470
+ const code = (r && r.code) || "error";
471
+ const msg = (r && r.message) || "";
472
+ errors.push(`credentials-set ${field}: ${code}${msg ? " — " + msg : ""}`);
473
+ }
461
474
  }
462
475
  }
463
476
  const ops = [{ op: "set", path: ["provider"], value: provider }];
@@ -466,7 +479,7 @@ window.__ModuleLoader__.load({
466
479
  ops.push({ op: "set", path: ["safeSearch"], value: safeSearch });
467
480
  ops.push({ op: "set", path: ["bingMarket"], value: bingMarket });
468
481
  if (keyStorage !== "credentials") {
469
- // settings 模式:key 仍写 settings.yaml(旧行为)
482
+ // settings 模式:key 写入当前 profile 的插件条目 config(cordis.patch.yml)
470
483
  for (const [field, value] of keyFields) {
471
484
  if (value.trim()) ops.push({ op: "set", path: [field], value: value.trim() });
472
485
  }
@@ -474,16 +487,28 @@ window.__ModuleLoader__.load({
474
487
  ops.push({ op: "set", path: ["platforms"], value: platforms });
475
488
  ops.push({ op: "set", path: ["cacheTtl"], value: Math.min(Math.max(Number(cacheTtl) ?? 5, 0), 5) });
476
489
  const result = await bridgeMutate({ ns: NS, ops });
477
- if (result.ok && !credFailed) {
490
+ if (!result || !result.ok) {
491
+ const code = (result && result.code) || "error";
492
+ const msg = (result && result.message) || "";
493
+ errors.push(`mutate: ${code}${msg ? " — " + msg : ""}`);
494
+ }
495
+ if (errors.length === 0) {
478
496
  setDirty(false);
479
497
  setProvider(result.value.value.provider ?? provider);
480
498
  setFailed(false);
499
+ setSaveError("");
481
500
  load();
482
501
  } else {
502
+ const detail = errors.join("; ");
483
503
  setFailed(true);
504
+ setSaveError(detail);
505
+ console.error("[dsh-free-search] save failed:", detail);
484
506
  }
485
- } catch {
507
+ } catch (e) {
508
+ const msg = e && e.message ? e.message : String(e);
486
509
  setFailed(true);
510
+ setSaveError(`exception: ${msg}`);
511
+ console.error("[dsh-free-search] save exception:", e);
487
512
  } finally {
488
513
  setSaving(false);
489
514
  }
@@ -493,12 +518,14 @@ window.__ModuleLoader__.load({
493
518
  load();
494
519
  setDirty(false);
495
520
  setFailed(false);
521
+ setSaveError("");
496
522
  };
497
523
 
498
524
  const runTest = async () => {
499
525
  setTesting(true);
500
526
  setTestResult(null);
501
527
  setFailed(false);
528
+ setSaveError("");
502
529
  try {
503
530
  const result = await bridgeRawSearch({
504
531
  query: "DeepSeek Harness",
@@ -528,6 +555,7 @@ window.__ModuleLoader__.load({
528
555
  setCheckingUpdate(true);
529
556
  setUpdateInfo(null);
530
557
  setFailed(false);
558
+ setSaveError("");
531
559
  try {
532
560
  const result = await bridgeCheckUpdate();
533
561
  if (result.ok) {
@@ -546,6 +574,7 @@ window.__ModuleLoader__.load({
546
574
  setUpgrading(true);
547
575
  setUpdateInfo(null);
548
576
  setFailed(false);
577
+ setSaveError("");
549
578
  try {
550
579
  const response = await fetch(`${BRIDGE_PREFIX}/update`, {
551
580
  method: "POST",
@@ -578,6 +607,7 @@ window.__ModuleLoader__.load({
578
607
  setLang((prev) => (prev === "en" ? "zh" : "en"));
579
608
  setDirty(true);
580
609
  setFailed(false);
610
+ setSaveError("");
581
611
  };
582
612
 
583
613
  return react_jsx_runtime.jsx("li", {
@@ -726,6 +756,7 @@ react_jsx_runtime.jsx("div", {
726
756
  setExaKey(e.target.value);
727
757
  setDirty(true);
728
758
  setFailed(false);
759
+ setSaveError("");
729
760
  },
730
761
  }),
731
762
  react_jsx_runtime.jsx("input", {
@@ -738,6 +769,7 @@ react_jsx_runtime.jsx("div", {
738
769
  setTavilyKey(e.target.value);
739
770
  setDirty(true);
740
771
  setFailed(false);
772
+ setSaveError("");
741
773
  },
742
774
  }),
743
775
  react_jsx_runtime.jsx("input", {
@@ -750,6 +782,7 @@ react_jsx_runtime.jsx("div", {
750
782
  setKeenableKey(e.target.value);
751
783
  setDirty(true);
752
784
  setFailed(false);
785
+ setSaveError("");
753
786
  },
754
787
  }),
755
788
  react_jsx_runtime.jsx("input", {
@@ -762,6 +795,7 @@ react_jsx_runtime.jsx("div", {
762
795
  setFirecrawlKey(e.target.value);
763
796
  setDirty(true);
764
797
  setFailed(false);
798
+ setSaveError("");
765
799
  },
766
800
  }),
767
801
  react_jsx_runtime.jsx("input", {
@@ -774,6 +808,7 @@ react_jsx_runtime.jsx("div", {
774
808
  setParallelKey(e.target.value);
775
809
  setDirty(true);
776
810
  setFailed(false);
811
+ setSaveError("");
777
812
  },
778
813
  }),
779
814
  react_jsx_runtime.jsx("input", {
@@ -786,6 +821,7 @@ react_jsx_runtime.jsx("div", {
786
821
  setPerplexityKey(e.target.value);
787
822
  setDirty(true);
788
823
  setFailed(false);
824
+ setSaveError("");
789
825
  },
790
826
  }),
791
827
  react_jsx_runtime.jsx("input", {
@@ -798,6 +834,7 @@ react_jsx_runtime.jsx("div", {
798
834
  setSerpbaseKey(e.target.value);
799
835
  setDirty(true);
800
836
  setFailed(false);
837
+ setSaveError("");
801
838
  },
802
839
  }),
803
840
  react_jsx_runtime.jsx("input", {
@@ -810,6 +847,7 @@ react_jsx_runtime.jsx("div", {
810
847
  setDeepseekKey(e.target.value);
811
848
  setDirty(true);
812
849
  setFailed(false);
850
+ setSaveError("");
813
851
  },
814
852
  }),
815
853
  react_jsx_runtime.jsx("p", {
@@ -832,6 +870,7 @@ react_jsx_runtime.jsx("div", {
832
870
  setKeyStorage(e.target.value);
833
871
  setDirty(true);
834
872
  setFailed(false);
873
+ setSaveError("");
835
874
  },
836
875
  children: [
837
876
  react_jsx_runtime.jsx("option", { value: "credentials", children: t.keyStorageCred }),
@@ -872,6 +911,7 @@ react_jsx_runtime.jsx("div", {
872
911
  );
873
912
  setDirty(true);
874
913
  setFailed(false);
914
+ setSaveError("");
875
915
  },
876
916
  }),
877
917
  label,
@@ -904,6 +944,7 @@ react_jsx_runtime.jsx("div", {
904
944
  setCacheTtl(Number(e.target.value));
905
945
  setDirty(true);
906
946
  setFailed(false);
947
+ setSaveError("");
907
948
  },
908
949
  }),
909
950
  react_jsx_runtime.jsx("p", {
@@ -915,7 +956,12 @@ react_jsx_runtime.jsx("div", {
915
956
  react_jsx_runtime.jsx("div", {
916
957
  className: "dshfs-resultRow",
917
958
  children: [
918
- failed ? react_jsx_runtime.jsx("span", { className: "dshfs-failed", children: t.saveFailed }) : null,
959
+ failed
960
+ ? react_jsx_runtime.jsx("span", {
961
+ className: "dshfs-failed",
962
+ children: saveError ? t.saveFailedDetail(saveError) : t.saveFailed,
963
+ })
964
+ : null,
919
965
  testResult
920
966
  ? react_jsx_runtime.jsx("span", {
921
967
  className: testResult.ok ? "dshfs-testOk" : "dshfs-failed",
@@ -1038,6 +1084,7 @@ react_jsx_runtime.jsx("div", {
1038
1084
  setProvider("bing");
1039
1085
  setDirty(true);
1040
1086
  setFailed(false);
1087
+ setSaveError("");
1041
1088
  },
1042
1089
  disabled: saving || !ready || provider === "bing",
1043
1090
  children: t.useBing,
@@ -1070,22 +1117,10 @@ react_jsx_runtime.jsx("div", {
1070
1117
  const inject = ["slots", "commandUi"];
1071
1118
 
1072
1119
  function apply(ctx) {
1073
- // 挂官方插槽 settings.plugin.item(设置 → 插件 → 可配置标签页)。
1074
- // 不依赖 dsh-web-ui:配置读写走自建 bridge(/api/dsh-free-search-settings)。
1075
- ctx.slots.inject("settings.plugin.item", () =>
1076
- ctx.slots.register(
1077
- {
1078
- name: "settings.plugin.item",
1079
- key: "free-search",
1080
- id: "dsh-free-search",
1081
- order: 120,
1082
- inject: () => ({}),
1083
- },
1084
- FreeSearchCard
1085
- )
1086
- );
1087
- // 挂 0.1.6-alpha.2+ 的插件页 slot:侧栏 插件 → 已安装 → free-search → 行配置页。
1120
+ // 挂插件页 slot:侧栏 插件 → 已安装 → free-search → 行配置页。
1121
+ // rc.1 废弃了旧版设置页的插件配置 slot,只保留 plugins.row.config;
1088
1122
  // key 规则见 ui-plugin-manager 的 rowConfigKey:<包名>#<patch 里声明的行 id>。
1123
+ // 配置读写走自建 bridge(/api/dsh-free-search-settings),不依赖 dsh-web-ui。
1089
1124
  ctx.slots.inject("plugins.row.config", () =>
1090
1125
  ctx.slots.register(
1091
1126
  {
package/lib/index.js CHANGED
@@ -46,13 +46,16 @@ const MARKET_TO_LANG = {
46
46
  "ko-KR": "ko-KR,ko;q=0.9,en;q=0.8",
47
47
  };
48
48
 
49
- const FREE_SEARCH_NS = "free-search";
49
+ // rc.1: settings moved into the profile-owned plugin Config. The namespace is the
50
+ // composition entry id (the `web-search-free` row the bundle's cordis.patch.yml
51
+ // declares), not the old `$DSH_HOME/settings.yaml` section name.
52
+ const FREE_SEARCH_NS = "web-search-free";
50
53
  const BRIDGE_PREFIX = "/api/dsh-free-search-settings";
51
54
  const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch"];
52
55
  const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek-official"];
53
56
 
54
57
  // 当前插件版本(发布时与 package.json 同步)
55
- const PLUGIN_VERSION = "0.4.36";
58
+ const PLUGIN_VERSION = "0.4.38";
56
59
  // 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
57
60
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
58
61
  const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
@@ -1785,29 +1788,44 @@ const KEY_REF_MAP = {
1785
1788
  deepseekApiKey: "DEEPSEEK_API_KEY",
1786
1789
  };
1787
1790
 
1791
+ // rc.1 declares editable fields `.volatile()`: the Loader hands `apply` live
1792
+ // references and commits profile edits in place without remounting. `resolveConfig`
1793
+ // unwraps those references so every read sees the latest accepted value.
1794
+ // `.volatile()` requires the scoped `@deepseek-ai/schemastery` (>= 3.18.2).
1788
1795
  const Config = z.object({
1789
- provider: z.string().default("bing"),
1790
- cache: z.boolean().default(true), // 单 query 结果缓存开关(防限流/省额度)
1791
- cacheTtl: z.number().default(5), // 缓存时长(分钟),0-5 可配置(使用处再 clamp)
1792
- keyStorage: z.string().default("credentials"), // key 存储位置:credentials(凭据中心)| settings(设置页)
1793
- lang: z.string().default("zh"),
1794
- region: z.string(),
1795
- bingMarket: z.string().default("zh-CN"),
1796
- safeSearch: z.string().default("off"),
1797
- searxngInstances: z.array(z.string()),
1798
- platforms: z.array(z.string()).default(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]),
1799
- exaApiKey: z.string().role("secret"),
1800
- tavilyApiKey: z.string().role("secret"),
1801
- keenableApiKey: z.string().role("secret"),
1802
- firecrawlApiKey: z.string().role("secret"),
1803
- parallelApiKey: z.string().role("secret"),
1804
- perplexityApiKey: z.string().role("secret"),
1805
- serpbaseApiKey: z.string().role("secret"),
1806
- deepseekApiKey: z.string().role("secret"),
1796
+ provider: z.string().default("bing").volatile(),
1797
+ cache: z.boolean().default(true).volatile(), // 单 query 结果缓存开关(防限流/省额度)
1798
+ cacheTtl: z.number().default(5).volatile(), // 缓存时长(分钟),0-5 可配置(使用处再 clamp)
1799
+ keyStorage: z.string().default("credentials").volatile(), // key 存储位置:credentials(凭据中心)| settings(设置页)
1800
+ lang: z.string().default("zh").volatile(),
1801
+ region: z.string().volatile(),
1802
+ bingMarket: z.string().default("zh-CN").volatile(),
1803
+ safeSearch: z.string().default("off").volatile(),
1804
+ searxngInstances: z.array(z.string()).volatile(),
1805
+ platforms: z.array(z.string()).default(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]).volatile(),
1806
+ exaApiKey: z.string().role("secret").volatile(),
1807
+ tavilyApiKey: z.string().role("secret").volatile(),
1808
+ keenableApiKey: z.string().role("secret").volatile(),
1809
+ firecrawlApiKey: z.string().role("secret").volatile(),
1810
+ parallelApiKey: z.string().role("secret").volatile(),
1811
+ perplexityApiKey: z.string().role("secret").volatile(),
1812
+ serpbaseApiKey: z.string().role("secret").volatile(),
1813
+ deepseekApiKey: z.string().role("secret").volatile(),
1807
1814
  });
1808
1815
 
1816
+ // Unwrap the live `Volatile<T>` references the Loader passes for `.volatile()`
1817
+ // fields into a plain object, once per read.
1818
+ function resolveConfig(config) {
1819
+ if (config === null || typeof config !== "object") return {};
1820
+ const out = {};
1821
+ for (const [key, value] of Object.entries(config)) {
1822
+ out[key] = value !== null && typeof value === "object" && typeof value.get === "function" ? value.get() : value;
1823
+ }
1824
+ return out;
1825
+ }
1826
+
1809
1827
  function apply(ctx, config) {
1810
- let current = () => config ?? {};
1828
+ const current = () => resolveConfig(config);
1811
1829
  const logger = ctx.logger;
1812
1830
  // credentials 服务可能晚于本插件挂载(跨 bundle 顺序),运行期动态获取而不是 apply 时缓存
1813
1831
  const getCredentials = () => ctx.get("credentials");
@@ -2035,37 +2053,16 @@ function apply(ctx, config) {
2035
2053
  };
2036
2054
 
2037
2055
  ctx.inject(["settings"], (sctx) => {
2038
- // alpha.2+(SettingsProvider 有 installSection 方法)与 rc.2(模块级 installSettingsSection)双兼容:
2039
- // 特性检测优先用 alpha 方法;缺失时动态 import 旧 API 注册(rc.2 环境,不会触发 alpha 的缺失导出报错)。
2040
- if (typeof sctx.settings.installSection === "function") {
2041
- sctx.settings.installSection(ctx, FREE_SEARCH_NS, Config, config ?? {}, {
2042
- setSource: (source) => {
2043
- current = source;
2044
- },
2045
- onChange: () => {
2046
- // settings 变更时刷新系统提示词(显示最新引擎)
2047
- if (typeof refreshPrompt === "function") refreshPrompt();
2048
- },
2049
- });
2050
- } else {
2051
- void (async () => {
2052
- const legacy = await import("@deepseek-ai/dsh-settings");
2053
- if (typeof legacy.installSettingsSection === "function" && legacy.settingsNamespace) {
2054
- const legacyNs = legacy.settingsNamespace(FREE_SEARCH_NS);
2055
- legacy.installSettingsSection(ctx, legacyNs, Config, config ?? {}, {
2056
- setSource: (source) => {
2057
- current = source;
2058
- },
2059
- onChange: () => {
2060
- if (typeof refreshPrompt === "function") refreshPrompt();
2061
- },
2062
- });
2063
- } else {
2064
- sctx.logger?.warn?.("free-search: dsh-settings 无可用注册 API(installSection/installSettingsSection 均缺失)");
2065
- return;
2066
- }
2067
- })();
2068
- }
2056
+ // rc.1 moved plugin configuration into the profile-owned Config, so the old
2057
+ // namespace-registration API is gone. Editable fields are declared `.volatile()`
2058
+ // on `Config` (above); this only records that the plugin ships its own settings
2059
+ // page (`auto: false`) so the host does not also generate a schema form. Values
2060
+ // persist per profile in `cordis.patch.yml` under this entry (`FREE_SEARCH_NS`).
2061
+ sctx.effect(() => sctx.settings.configure({ auto: false }, ctx.fiber));
2062
+ // Re-render the dynamic system-prompt engine list whenever this entry's config changes.
2063
+ sctx.on("settings/document-updated", (ns) => {
2064
+ if (String(ns) === FREE_SEARCH_NS && typeof refreshPrompt === "function") refreshPrompt();
2065
+ });
2069
2066
  });
2070
2067
 
2071
2068
  ctx.inject(["webServer", "settings"], (sctx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-free-search",
3
- "version": "0.4.36",
3
+ "version": "0.4.38",
4
4
  "description": "Free web search for DeepSeek Harness: 13 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable/Firecrawl/Parallel keyless; Perplexity/SerpBase/DeepSeek with key) + time filtering + platform search + web_fetch, with web settings UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -43,15 +43,47 @@
43
43
  "node": ">=20"
44
44
  },
45
45
  "dependencies": {
46
- "@deepseek-ai/schemastery": "^3.18.1"
46
+ "@deepseek-ai/schemastery": "^3.18.4"
47
47
  },
48
48
  "peerDependencies": {
49
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2 || ^0.1.3-alpha.2 || ^0.1.5-alpha.1",
50
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2 || ^0.1.3-alpha.2 || ^0.1.5-alpha.1"
49
+ "@deepseek-ai/cordis": "^4.0.1",
50
+ "@deepseek-ai/dsh-client-ui-commands": "^0.1.7-rc.1",
51
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.1.7-rc.1",
52
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.1",
53
+ "@deepseek-ai/dsh-settings": "^0.1.7-rc.1",
54
+ "@deepseek-ai/dsh-tools": "^0.1.7-rc.1",
55
+ "@deepseek-ai/schemastery": "^3.18.4",
56
+ "react": "^18.2.0"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "@deepseek-ai/cordis": {
60
+ "optional": true
61
+ },
62
+ "@deepseek-ai/dsh-client-ui-commands": {
63
+ "optional": true
64
+ },
65
+ "@deepseek-ai/dsh-client-ui-plugin-manager": {
66
+ "optional": true
67
+ },
68
+ "@deepseek-ai/dsh-client-ui-renderer": {
69
+ "optional": true
70
+ },
71
+ "@deepseek-ai/dsh-settings": {
72
+ "optional": true
73
+ },
74
+ "@deepseek-ai/dsh-tools": {
75
+ "optional": true
76
+ },
77
+ "@deepseek-ai/schemastery": {
78
+ "optional": true
79
+ },
80
+ "react": {
81
+ "optional": true
82
+ }
51
83
  },
52
84
  "devDependencies": {
53
- "@deepseek-ai/dsh-settings": "0.1.0-rc.7",
54
- "@deepseek-ai/dsh-tools": "0.1.0-rc.7"
85
+ "@deepseek-ai/dsh-settings": "^0.1.7-rc.1",
86
+ "@deepseek-ai/dsh-tools": "^0.1.7-rc.1"
55
87
  },
56
88
  "dsh": {
57
89
  "bundle": {
@@ -59,27 +91,18 @@
59
91
  },
60
92
  "client": {
61
93
  "inject": [
62
- "@deepseek-ai/dsh-client-runtime"
94
+ "@deepseek-ai/dsh-client-ui-commands",
95
+ "@deepseek-ai/dsh-client-ui-plugin-manager",
96
+ "@deepseek-ai/dsh-client-ui-renderer"
63
97
  ],
64
98
  "platform": "web"
65
99
  },
66
100
  "engines": {
67
- "dsh": ">=0.1.1-rc.1"
101
+ "dsh": ">=0.1.7-rc.1"
68
102
  },
69
103
  "compatibility": {
70
104
  "dshReleases": {
71
- "rc.7": "unknown",
72
- "rc.8": "unknown",
73
- "0.1.1-rc.1": "compatible",
74
- "0.1.1-rc.2": "compatible",
75
- "0.1.2-alpha.1": "unknown",
76
- "0.1.2-alpha.2": "compatible",
77
- "0.1.2-alpha.3": "compatible",
78
- "0.1.2-alpha.4": "compatible",
79
- "0.1.2-alpha.5": "unknown",
80
- "0.1.2-rc.1": "compatible",
81
- "0.1.5-rc.1": "compatible",
82
- "0.1.5-rc.2": "compatible"
105
+ "0.1.7-rc.1": "compatible"
83
106
  }
84
107
  }
85
108
  }