@darlingc/dsh-freesearch 0.5.4 → 0.5.6

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
@@ -9,8 +9,8 @@
9
9
  ## 中文
10
10
 
11
11
  <div align="center">
12
- <a href="https://raw.githubusercontent.com/DDDMUC/dsh-free-search/master/assets/settings-free1.png">
13
- <img src="https://raw.githubusercontent.com/DDDMUC/dsh-free-search/master/assets/settings-free1.png" alt="免费引擎设置 (Bing)" width="820" />
12
+ <a href="https://raw.githubusercontent.com/DarlingC/dsh-freesearch/master/assets/settings-free1.png">
13
+ <img src="https://raw.githubusercontent.com/DarlingC/dsh-freesearch/master/assets/settings-free1.png" alt="免费引擎设置 (Bing)" width="820" />
14
14
  </a>
15
15
  <br>
16
16
  <sub>▲ 免费引擎(以Bing为例)</sub>
@@ -36,7 +36,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
36
36
  - **统一引擎回退** —— 任何引擎失败(付费/免费,缺 key/401/限流/网络)自动轮流尝试下一个引擎:首选引擎 → 其他引擎(exa/tavily/keenable 无 key 也会尝试,因为它们自带 keyless 免费额度)→ 剩余免费引擎,搜索永不直接失败;结果顶部注明实际生效的引擎(如 `Note: perplexity unavailable or failed, using exa.`)
37
37
  - **时间过滤** —— `advanced_search` 工具支持 `timeRange`:固定档、自定义相对值、绝对日期三种形式(详见下方逻辑说明)
38
38
  - **系统提示词注入** —— agent 知道当前用哪个引擎、哪些需要 key
39
- - **版本号 + 检查更新** —— 设置卡片显示当前版本(v0.4.12),"检查更新"按钮直连 npm registry 对比最新版,有新版本时提示并可一键跳转
39
+ - **版本号 + 检查更新** —— 设置卡片显示当前版本(v0.5.6),"检查更新"按钮直连 npm registry 对比最新版,有新版本时提示并可一键跳转
40
40
  - **结果缓存** —— 相同查询(含引擎/时间过滤参数)5 分钟内命中缓存(LRU 50 条),防免费引擎限流、省付费额度;时长可在设置页 0-5 分钟自由配置(0 关闭)
41
41
  - **免费标注** —— 设置页中免费引擎带绿色 `FREE` 徽章,付费引擎带橙色 `API KEY` 徽章
42
42
  - **网页抓取(web_fetch)** —— 让 agent 抓取网页内容(官方 `dsh-web-fetch-http` provider,纯 JS,零额外依赖)
@@ -81,8 +81,8 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
81
81
  - **Key 配置**:在 AI Studio(aistudio.google.com/apikey)免费创建 `GEMINI_API_KEY`,放入 `~/.dsh/.credentials.yaml` 的 `refs.GEMINI_API_KEY`,或在 `~/.dsh/settings.yaml` 的 `free-search:` 下设置 `geminiApiKey`。
82
82
  - **模型**:默认 `gemini-2.5-flash`(免费层唯一已实测可联网的模型)。当前 AI Studio 免费层**无需绑卡**即可用 Google Search grounding。
83
83
  - **⚠️ 配额警示(务必注意)**:
84
- - `gemini-2.5-flash` **文本输出 RPD 仅约 20/天** —— 千万别把它当常规文本问答模型用,会秒爆。本引擎只走 grounding(联网搜索),计的是**搜索 RPD**,不占那 20 次文本额度。
85
- - **搜索 RPD 约 1,500/天**(与 Flash-Lite 共享),**RPM 约 5**。每次搜索引擎调用 = 1 次 grounded prompt = 1 搜索 RPD。超限会返回 HTTP 429,本插件会**自动回退到其它免费引擎**(Bing 等),搜索不会因此失败。
84
+ - `gemini-2.5-flash` **文本输出 RPD 仅约 20/天** —— 千万别把它当常规文本问答模型用,会秒爆。⚠️ **每次发起 grounded 搜索请求,模型生成过程会同时扣 1 次文本输出额度 + 1 次搜索接地额度**:因此那 **20 次文本输出 RPD 就是实际瓶颈**,即便搜索 RPD 高达约 1,500/天,实际每天都只能发起约 **20 次** grounded 搜索。
85
+ - **搜索 RPD 约 1,500/天**(与 Flash-Lite 共享),**RPM 约 5**。每次搜索引擎调用 = 1 次 grounded prompt = **同时耗 1 文本 + 1 搜索额度**。超限会返回 HTTP 429,本插件会**自动回退到其它免费引擎**(Bing 等),搜索不会因此失败。
86
86
  - 因此**不建议把 `gemini` 设为默认首选引擎**(`free-search.provider: gemini`),默认 `bing` + 失败自动回退即可;把 `gemini` 当按需/回退引擎最稳。
87
87
  - **与其它引擎差异**:Gemini grounding 返回的是"模型 Ground 到的来源集合",不是像 Bing 那样的排序结果列表——数量不固定、通常偏少、无优先级;`maxResults` 只能做客户端截断。作为默认搜索体验与真实搜索引擎略有不同。
88
88
 
@@ -122,6 +122,29 @@ dsh web
122
122
 
123
123
  > ⚠️ 依赖:CI 的 `npm stage publish` 需要 GitHub 仓库配置 `NPM_TOKEN` secret(npmjs.com → Access Tokens → Publish 类型 token)。且该包需先在 npm registry 上存在(首次发布无法 stage,需先在网页/本地完成一次正式发布)。
124
124
 
125
+ #### 发布到 GitHub Packages(本地,可选,不走 CI/CD)
126
+
127
+ 除 npmjs 外,也可把本包发布到 **GitHub Packages**(`npm.pkg.github.com`,归属 `DarlingC` 组织)。仓库内置了专用 npm 配置 `.npmrc-github`,它**不会自动加载**,因此不会影响常规安装或上方 npmjs Staged Publishing 流程。
128
+
129
+ ```sh
130
+ # 1) 生成带 write:packages 权限的 GitHub PAT(Settings → Developer settings → Personal access tokens)
131
+ export GITHUB_TOKEN=ghp_xxx
132
+
133
+ # 2) 用专用配置发布到 GitHub Packages(版本号须 >= 已存在版本)
134
+ npm publish --registry=https://npm.pkg.github.com/ --userconfig=.npmrc-github
135
+ ```
136
+
137
+ 从 GitHub Packages 安装:
138
+
139
+ ```sh
140
+ export GITHUB_TOKEN=ghp_xxx # 需 read:packages 权限
141
+ dsh plugin --profile web add @darlingc/dsh-freesearch --registry=https://npm.pkg.github.com/
142
+ # 或在用户/项目 .npmrc 里加一行让该 scope 走 GitHub Packages:
143
+ # @darlingc:registry=https://npm.pkg.github.com/
144
+ ```
145
+
146
+ > 📌 说明:GitHub Packages 适合「仓库内私有/组织内发布」场景;对外推荐走 npmjs(上方 Staged Publishing)。两套 registry 互不影响,tag push 自动建的 GitHub Release(见 `.github/workflows/publish.yml`)与 GitHub Packages 发布相互独立。
147
+
125
148
  ### 使用
126
149
 
127
150
  #### 网页设置(推荐)
@@ -130,6 +153,7 @@ dsh web
130
153
 
131
154
  - **Search engine**:下拉框切换引擎,保存即生效
132
155
  - **API keys**:为 Exa / Tavily / Keenable / Perplexity / DeepSeek 填写 key(密码框,保存后只显示"已配置")
156
+ - **推荐**:付费引擎 key 建议写入 harness 凭据中心 `~/.dsh/.credentials.yaml`(如 `DEEPSEEK_API_KEY: sk-...`,与官方 LLM provider 一致,一处管理所有 key)。插件读取优先级:凭据中心 > 设置页 > 环境变量,设置页填的 key 仅作为遗留兼容。
133
157
  - **Test engine**:直测当前引擎可用性(不走回退链,付费引擎无 key 会明确报错)
134
158
  - **Use Bing default**:把当前搜索引擎切回稳定的免费 Bing;`Discard` 只撤销尚未保存的编辑
135
159
  - **Platform search**:勾选启用 GitHub / V2EX / Bilibili 平台搜索(`platform_search` 工具按此过滤)
@@ -138,15 +162,15 @@ dsh web
138
162
  <table align="center" style="border: none; border-collapse: collapse;">
139
163
  <tr style="border: none;">
140
164
  <td align="center" width="50%" style="border: none; padding: 6px;">
141
- <a href="https://raw.githubusercontent.com/DDDMUC/dsh-free-search/master/assets/settings-free.png">
142
- <img src="https://raw.githubusercontent.com/DDDMUC/dsh-free-search/master/assets/settings-free.png" alt="免费引擎设置" width="100%" />
165
+ <a href="https://raw.githubusercontent.com/DarlingC/dsh-freesearch/master/assets/settings-free.png">
166
+ <img src="https://raw.githubusercontent.com/DarlingC/dsh-freesearch/master/assets/settings-free.png" alt="免费引擎设置" width="100%" />
143
167
  </a>
144
168
  <br>
145
169
  <sub>▲ <b>免费引擎</b>(显示绿色 FREE 徽章与官网链接)</sub>
146
170
  </td>
147
171
  <td align="center" width="50%" style="border: none; padding: 6px;">
148
- <a href="https://raw.githubusercontent.com/DDDMUC/dsh-free-search/master/assets/settings-apikey.png">
149
- <img src="https://raw.githubusercontent.com/DDDMUC/dsh-free-search/master/assets/settings-apikey.png" alt="付费引擎设置" width="100%" />
172
+ <a href="https://raw.githubusercontent.com/DarlingC/dsh-freesearch/master/assets/settings-apikey.png">
173
+ <img src="https://raw.githubusercontent.com/DarlingC/dsh-freesearch/master/assets/settings-apikey.png" alt="付费引擎设置" width="100%" />
150
174
  </a>
151
175
  <br>
152
176
  <sub>▲ <b>付费/API Key 引擎</b>(显示橙色 API KEY 徽章与获取链接)</sub>
@@ -244,17 +268,6 @@ Search engine test:
244
268
 
245
269
  全部走公开 API,零外部依赖、无需任何 key,开箱即用。
246
270
 
247
- ### 本地引擎切换工具(tools/)
248
-
249
- `tools/` 目录附带了一个本地切换小工具(零依赖):
250
-
251
- - **`启动搜索引擎切换器.cmd`**(Windows)——双击启动本地 Node 服务(`http://127.0.0.1:4789`)并自动打开浏览器选择页面
252
- - **`switch-engine.html`** —— 选择页面:显示当前引擎,点选新引擎,一键写入配置
253
- - **`server.mjs`** —— 本地服务,负责读写 `~/.dsh/profiles/web/cordis.patch.yml`
254
- - **`switch-engine.ps1`** —— 无界面命令行版:`powershell -File tools/switch-engine.ps1 -Engine bing`
255
-
256
- 切换后重启 `dsh web` 生效。
257
-
258
271
  > 配置卡片挂在官方设置页的 `settings.plugin.item` 插槽(dsh 自带),配置读写走插件自建 bridge,**不依赖 dsh-web-ui**,插件可独立使用。
259
272
 
260
273
  ### 代理说明(国内用户)
@@ -280,8 +293,8 @@ Windows 用户:桌面快捷方式已内置此配置(`set NODE_USE_ENV_PROXY=
280
293
  ## English
281
294
 
282
295
  <div align="center">
283
- <a href="https://raw.githubusercontent.com/DDDMUC/dsh-free-search/master/assets/settings-free1.png">
284
- <img src="https://raw.githubusercontent.com/DDDMUC/dsh-free-search/master/assets/settings-free1.png" alt="Free Engine Settings (Bing)" width="820" />
296
+ <a href="https://raw.githubusercontent.com/DarlingC/dsh-freesearch/master/assets/settings-free1.png">
297
+ <img src="https://raw.githubusercontent.com/DarlingC/dsh-freesearch/master/assets/settings-free1.png" alt="Free Engine Settings (Bing)" width="820" />
285
298
  </a>
286
299
  <br>
287
300
  <sub>▲ Free engine (using Bing as an example)</sub>
@@ -307,7 +320,7 @@ This plugin provides multiple free search engines with automatic fallback, compl
307
320
  - **Unified Engine Fallback** — Any engine failure (paid or free, missing key, 401, rate limit, network error) automatically tries the next engine: the configured engine first, then other engines (exa/tavily/keenable are tried even without a key because they have built-in keyless quota), then the remaining free engines (Bing/AnySearch etc.) — with a note attached to the results naming the engine that actually served them (e.g. `Note: perplexity unavailable or failed, using exa.`). Search never fails outright.
308
321
  - **Time Filtering** — The `advanced_search` tool supports `timeRange`: fixed tiers, custom relative values, or an absolute date (details below)
309
322
  - **System Prompt Injection** — The agent is aware of the currently active engine and which engines require API keys
310
- - **Version + Update Check** — The settings card shows the current version (v0.4.12), and a "Check update" button queries the npm registry to compare against the latest release, prompting a one-click jump when a newer version exists
323
+ - **Version + Update Check** — The settings card shows the current version (v0.5.6), and a "Check update" button queries the npm registry to compare against the latest release, prompting a one-click jump when a newer version exists
311
324
  - **Result Caching** — Identical queries (same engine / time-filter args) hit an LRU cache (50 entries) for up to 5 minutes, protecting free engines from rate-limiting and saving paid quota; the TTL is configurable from 0-5 minutes in the settings UI (0 disables caching)
312
325
  - **Visual Badges** — Free engines feature a green `FREE` badge, while paid engines show an orange `API KEY` badge in the settings UI
313
326
  - **Webpage Fetching (`web_fetch`)** — Allows the agent to read full webpage contents (official `dsh-web-fetch-http` provider, pure JS, zero extra dependencies)
@@ -352,8 +365,8 @@ This plugin provides multiple free search engines with automatic fallback, compl
352
365
  - **Key**: create a free `GEMINI_API_KEY` at AI Studio (aistudio.google.com/apikey) and store it in `refs.GEMINI_API_KEY` under `~/.dsh/.credentials.yaml`, or set `geminiApiKey` under `free-search:` in `~/.dsh/settings.yaml`.
353
366
  - **Model**: defaults to `gemini-2.5-flash` (the only free-tier model verified to work online here). Google Search Grounding is available on the AI Studio **free tier with no credit card**.
354
367
  - **⚠️ Quota warning (important)**:
355
- - `gemini-2.5-flash` has a **text-output RPD of only ~20/day** — do **not** use it as a general text model or you will exhaust it instantly. This engine only uses grounding (web search), which counts against the **search RPD**, not the text quota.
356
- - **Search RPD ≈ 1,500/day** (shared with Flash-Lite), **RPM ≈ 5**. Each engine call = 1 grounded prompt = 1 search RPD. Exceeding it returns HTTP 429, and this plugin **auto-falls-back to other free engines** (Bing, etc.), so search never hard-fails.
368
+ - `gemini-2.5-flash` has a **text-output RPD of only ~20/day** — do **not** use it as a general text model or you will exhaust it instantly. ⚠️ **Each grounded search request consumes 1 text-output quota + 1 search-grounding quota simultaneously during generation**, so the **~20/day text-output RPD is the real bottleneck**: even though the search RPD is ~1,500/day, you can effectively make only about **20 grounded searches per day**.
369
+ - **Search RPD ≈ 1,500/day** (shared with Flash-Lite), **RPM ≈ 5**. Each engine call = 1 grounded prompt = **consumes 1 text + 1 search quota at once**. Exceeding it returns HTTP 429, and this plugin **auto-falls-back to other free engines** (Bing, etc.), so search never hard-fails.
357
370
  - So keep `provider: bing` as default and let `gemini` act as a fallback / on-demand engine rather than the primary load.
358
371
  - **Difference vs. other engines**: Gemini grounding returns the set of sources the model grounded on — not a ranked SERP list like Bing/DDG. Count is not fixed and is usually small, with no ordering; `maxResults` can only truncate client-side.
359
372
 
@@ -391,6 +404,7 @@ After installation, navigate to **Settings → Plugins → Configurable** tab
391
404
 
392
405
  - **Search engine**: Select an engine from the dropdown; changes take effect immediately upon saving.
393
406
  - **API keys**: Enter keys for Exa / Tavily / Keenable / Perplexity / DeepSeek (password fields; displayed as "configured" once saved).
407
+ - **Recommended**: store paid-engine keys in the harness credential center `~/.dsh/.credentials.yaml` (e.g. `DEEPSEEK_API_KEY: sk-...`, same as the official LLM providers — one place for all keys). Resolution order: credentials center > settings page > environment variable; the settings-page fields remain for backward compatibility.
394
408
  - **Test engine**: Tests the selected engine directly (no fallback chain; paid engines without a key report an explicit error).
395
409
  - **Use Bing default**: stage a switch back to the stable free Bing engine; `Discard` only cancels unsaved edits
396
410
  - **Platform search**: check platforms (GitHub / V2EX / Bilibili / Reddit / HN / Stack Overflow / Wikipedia / npm) to enable them for the `platform_search` tool (disabled platforms are skipped).
@@ -399,15 +413,15 @@ After installation, navigate to **Settings → Plugins → Configurable** tab
399
413
  <table align="center" style="border: none; border-collapse: collapse;">
400
414
  <tr style="border: none;">
401
415
  <td align="center" width="50%" style="border: none; padding: 6px;">
402
- <a href="https://raw.githubusercontent.com/DDDMUC/dsh-free-search/master/assets/settings-free.png">
403
- <img src="https://raw.githubusercontent.com/DDDMUC/dsh-free-search/master/assets/settings-free.png" alt="Free Engine Settings" width="100%" />
416
+ <a href="https://raw.githubusercontent.com/DarlingC/dsh-freesearch/master/assets/settings-free.png">
417
+ <img src="https://raw.githubusercontent.com/DarlingC/dsh-freesearch/master/assets/settings-free.png" alt="Free Engine Settings" width="100%" />
404
418
  </a>
405
419
  <br>
406
420
  <sub>▲ <b>Free Engine</b> (shows green FREE badge and official website link)</sub>
407
421
  </td>
408
422
  <td align="center" width="50%" style="border: none; padding: 6px;">
409
- <a href="https://raw.githubusercontent.com/DDDMUC/dsh-free-search/master/assets/settings-apikey.png">
410
- <img src="https://raw.githubusercontent.com/DDDMUC/dsh-free-search/master/assets/settings-apikey.png" alt="Paid/API Key Engine Settings" width="100%" />
423
+ <a href="https://raw.githubusercontent.com/DarlingC/dsh-freesearch/master/assets/settings-apikey.png">
424
+ <img src="https://raw.githubusercontent.com/DarlingC/dsh-freesearch/master/assets/settings-apikey.png" alt="Paid/API Key Engine Settings" width="100%" />
411
425
  </a>
412
426
  <br>
413
427
  <sub>▲ <b>Paid / API Key Engine</b> (shows orange API KEY badge and link to get an API key)</sub>
@@ -505,17 +519,6 @@ Ask the agent to search specific platforms (e.g., *"Search GitHub for deepseek h
505
519
 
506
520
  All platform searches rely on public endpoints with zero external dependencies and no API keys — they work out of the box.
507
521
 
508
- ### Local Engine Switcher (`tools/`)
509
-
510
- The `tools/` directory includes a lightweight, zero-dependency switcher:
511
-
512
- - **`启动搜索引擎切换器.cmd`** (Windows) — Double-click to launch a local Node server (`http://127.0.0.1:4789`) and automatically open the engine selector page in your browser.
513
- - **`switch-engine.html`** — The selector UI: displays current engine status and allows one-click switching.
514
- - **`server.mjs`** — The local backend service responsible for reading/writing `~/.dsh/profiles/web/cordis.patch.yml`.
515
- - **`switch-engine.ps1`** — Headless PowerShell script: `powershell -File tools/switch-engine.ps1 -Engine bing`.
516
-
517
- Restart `dsh web` after switching to apply changes.
518
-
519
522
  > 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.**
520
523
 
521
524
  ### Proxy Note (for Users in Mainland China)
package/cordis.patch.yml CHANGED
@@ -16,6 +16,9 @@
16
16
  provider: bing
17
17
  bingMarket: zh-CN
18
18
  # 自动接管默认搜索:让 harness 的 web_search 直接用本插件的 provider(内部含全部引擎)
19
+ # 注意 patch 语义是"整行替换 config"(DSH 0.1.2+):必须保留 fetchProvider,否则
20
+ # web-fetch-http 会被另一层重新注册,与 base bundle 的注册造成 duplicate loader entry。
19
21
  - id: web
20
22
  config:
21
23
  searchProvider: ddg
24
+ fetchProvider: http
package/lib/client.js CHANGED
@@ -30,8 +30,15 @@ window.__ModuleLoader__.load({
30
30
  ".dshfs-label{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}",
31
31
  ".dshfs-select{border:1px solid var(--dsw-alias-border-l2);font:inherit;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border-radius:6px;padding:6px 8px;font-size:13px;transition:border-color .13s,box-shadow .13s;width:100%}",
32
32
  ".dshfs-select:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}",
33
+ // 下拉选项列表配色锁死:不随皮肤变量变化(皮肤只影响 select 框体本身)
34
+ // color-scheme 让浏览器原生下拉按主题渲染;option 显式固定底色/文字色兜底
35
+ ".dshfs-select{color-scheme:light dark}",
36
+ ".dshfs-select option,.dshfs-select optgroup{background-color:#ffffff;color:#1f2328}",
37
+ "@media (prefers-color-scheme:dark){.dshfs-select{color-scheme:dark}.dshfs-select option,.dshfs-select optgroup{background-color:#1e1f24;color:#e8e8ea}}",
33
38
  ".dshfs-input{border:1px solid var(--dsw-alias-border-l2);font:inherit;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border-radius:6px;padding:6px 8px;font-size:13px;transition:border-color .13s,box-shadow .13s;width:100%}",
34
39
  ".dshfs-ttl{width:88px}",
40
+ ".dshfs-fieldRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
41
+ ".dshfs-keyStorage{width:auto;min-width:180px}",
35
42
  ".dshfs-input:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}",
36
43
  ".dshfs-input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
37
44
  ".dshfs-input:disabled{opacity:.6;cursor:default}",
@@ -67,6 +74,8 @@ window.__ModuleLoader__.load({
67
74
  }
68
75
  //#endregion
69
76
 
77
+ // bridge 路径副本,必须与 lib/index.js 的 BRIDGE_PREFIX(单一权威)保持一致。
78
+ // 客户端是独立浏览器 bundle,无法 import 服务端文件,只能各放一份;改动任一方需同步另一方。
70
79
  const BRIDGE_PREFIX = "/api/dsh-free-search-settings";
71
80
  const NS = "free-search";
72
81
  const I18N = {
@@ -84,7 +93,12 @@ window.__ModuleLoader__.load({
84
93
  perplexityPh: (c) => c ? "Perplexity API 密钥(已配置)" : "Perplexity API 密钥(pplx-...)",
85
94
  deepseekPh: (c) => c ? "DeepSeek API 密钥(已配置)" : "DeepSeek API 密钥(sk-...)",
86
95
  geminiPh: (c) => c ? "Gemini API 密钥(已配置)" : "Gemini API 密钥(免费层,联网搜索)",
87
- keysHint: "密钥保存在 settings.yaml(界面脱敏显示)。为某引擎填写密钥后即可使用。",
96
+ keysHint: "密钥读取优先级:.credentials.yaml 凭据中心 > 这里 > 环境变量。推荐把 key 写进凭据中心(与官方 LLM 一致,一处管理)。",
97
+ keyStorage: "Key 存储位置",
98
+ keyStorageCred: "凭据中心(推荐)",
99
+ keyStorageSettings: "设置页(兼容)",
100
+ keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["exa", "tavily", "keenable", "perplexity", "deepseek", "gemini"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
101
+ keyStorageSettingsHint: "保存后写入 settings.yaml(向后兼容路径,优先级低于凭据中心)。",
88
102
  platformSearch: "平台搜索(platform_search 工具)",
89
103
  platformHint: "为 agent 的 platform_search 工具启用平台。禁用的平台会被跳过。",
90
104
  cacheTtl: "结果缓存时长(分钟)",
@@ -126,7 +140,12 @@ window.__ModuleLoader__.load({
126
140
  perplexityPh: (c) => c ? "Perplexity API key (configured)" : "Perplexity API key (pplx-...)",
127
141
  deepseekPh: (c) => c ? "DeepSeek API key (configured)" : "DeepSeek API key (sk-...)",
128
142
  geminiPh: (c) => c ? "Gemini API key (configured)" : "Gemini API key (free tier, web search)",
129
- keysHint: "Keys are stored in settings.yaml (redacted in the UI). Enter a key for an engine to use it.",
143
+ 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).",
144
+ keyStorage: "Key storage",
145
+ keyStorageCred: "Credential center (recommended)",
146
+ keyStorageSettings: "Settings page (legacy)",
147
+ keyStorageCredHint: (c) => `Saved to ~/.dsh/.credentials.yaml (highest priority). Currently configured: ${["exa", "tavily", "keenable", "perplexity", "deepseek", "gemini"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "none"}`,
148
+ keyStorageSettingsHint: "Saved to settings.yaml (backward-compatible path; lower priority than the credential center).",
130
149
  platformSearch: "Platform search (platform_search tool)",
131
150
  platformHint: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
132
151
  cacheTtl: "Result cache TTL (minutes)",
@@ -157,7 +176,7 @@ window.__ModuleLoader__.load({
157
176
  };
158
177
  const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
159
178
  // 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION、package.json 保持一致)
160
- const PLUGIN_VERSION = "0.5.4";
179
+ const PLUGIN_VERSION = "0.5.6";
161
180
  const ENGINES = [
162
181
  { id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
163
182
  { id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
@@ -208,7 +227,54 @@ window.__ModuleLoader__.load({
208
227
  return response.json();
209
228
  }
210
229
 
230
+ async function bridgeCredentialsStatus() {
231
+ const response = await fetch(`${BRIDGE_PREFIX}/credentials-status`, {
232
+ method: "POST",
233
+ headers: { "content-type": "application/json" },
234
+ body: "{}",
235
+ });
236
+ return response.json();
237
+ }
238
+
239
+ async function bridgeCredentialsSet(key, value) {
240
+ const response = await fetch(`${BRIDGE_PREFIX}/credentials-set`, {
241
+ method: "POST",
242
+ headers: { "content-type": "application/json" },
243
+ body: JSON.stringify({ key, value }),
244
+ });
245
+ return response.json();
246
+ }
247
+
248
+ async function bridgeCredentialsUnset(key) {
249
+ const response = await fetch(`${BRIDGE_PREFIX}/credentials-unset`, {
250
+ method: "POST",
251
+ headers: { "content-type": "application/json" },
252
+ body: JSON.stringify({ key }),
253
+ });
254
+ return response.json();
255
+ }
256
+
211
257
  function FreeSearchCard(props) {
258
+ // 检测应用主题深浅(读 body 的 --dsw-alias-bg-base 变量亮度),用于锁定原生下拉配色
259
+ const isDarkScheme = react.useMemo(() => {
260
+ try {
261
+ const root = document.body || document.documentElement;
262
+ const bg = getComputedStyle(root).getPropertyValue("--dsw-alias-bg-base").trim();
263
+ const m = bg.match(/(\d+)\s*[, ]\s*(\d+)\s*[, ]\s*(\d+)/);
264
+ if (m) {
265
+ const l = 0.299 * Number(m[1]) + 0.587 * Number(m[2]) + 0.114 * Number(m[3]);
266
+ return l < 128;
267
+ }
268
+ if (/^#([0-9a-f]{3,8})/i.test(bg)) {
269
+ const hex = bg.slice(1);
270
+ const h = hex.length <= 4 ? hex.replace(/./g, (c) => c + c) : hex;
271
+ const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
272
+ return 0.299 * r + 0.587 * g + 0.114 * b < 128;
273
+ }
274
+ } catch {}
275
+ return typeof matchMedia === "function" ? matchMedia("(prefers-color-scheme: dark)").matches : false;
276
+ }, []);
277
+ const selectColorScheme = isDarkScheme ? "dark" : "light";
212
278
  const [open, setOpen] = react.useState(false);
213
279
  const [state, setState] = react.useState({ status: "loading" });
214
280
  const [provider, setProvider] = react.useState("bing");
@@ -221,6 +287,10 @@ window.__ModuleLoader__.load({
221
287
  const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
222
288
  const [cacheTtl, setCacheTtl] = react.useState(5);
223
289
  const [keysConfigured, setKeysConfigured] = react.useState({});
290
+ // key 存储位置:credentials(凭据中心,默认)| settings(设置页,兼容旧行为)
291
+ const [keyStorage, setKeyStorage] = react.useState("credentials");
292
+ // 凭据中心里已配置的 key(describe 不返回,需单独查)
293
+ const [credConfigured, setCredConfigured] = react.useState({});
224
294
  const [lang, setLang] = react.useState("zh");
225
295
  const [dirty, setDirty] = react.useState(false);
226
296
  const [saving, setSaving] = react.useState(false);
@@ -262,7 +332,21 @@ window.__ModuleLoader__.load({
262
332
  }
263
333
  }
264
334
  setKeysConfigured(configured);
335
+ // key 存储位置(默认凭据中心)
336
+ setKeyStorage(v.keyStorage === "settings" ? "settings" : "credentials");
265
337
  setState({ status: "ready", writable: result.value.writable });
338
+ // 查询凭据中心里各 key 的配置状态
339
+ try {
340
+ const cred = await bridgeCredentialsStatus();
341
+ if (cred.ok) {
342
+ const cc = {};
343
+ const map = { exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek", geminiApiKey: "gemini" };
344
+ for (const [k, v] of Object.entries(cred.value.configured ?? {})) {
345
+ if (map[k]) cc[map[k]] = v;
346
+ }
347
+ setCredConfigured(cc);
348
+ }
349
+ } catch {}
266
350
  } else {
267
351
  setState({ status: "unavailable" });
268
352
  }
@@ -288,18 +372,36 @@ window.__ModuleLoader__.load({
288
372
  setSaving(true);
289
373
  setFailed(false);
290
374
  try {
375
+ // key 存储分流:凭据中心(默认)走 credentials-set;设置页走 settings mutate(兼容)
376
+ const keyFields = [
377
+ ["exaApiKey", exaKey],
378
+ ["tavilyApiKey", tavilyKey],
379
+ ["keenableApiKey", keenableKey],
380
+ ["perplexityApiKey", perplexityKey],
381
+ ["deepseekApiKey", deepseekKey],
382
+ ["geminiApiKey", geminiKey],
383
+ ];
384
+ let credFailed = false;
385
+ if (keyStorage === "credentials") {
386
+ for (const [field, value] of keyFields) {
387
+ if (!value.trim()) continue;
388
+ const r = await bridgeCredentialsSet(field, value.trim());
389
+ if (!r.ok) credFailed = true;
390
+ }
391
+ }
291
392
  const ops = [{ op: "set", path: ["provider"], value: provider }];
292
393
  ops.push({ op: "set", path: ["lang"], value: lang });
293
- if (exaKey.trim()) ops.push({ op: "set", path: ["exaApiKey"], value: exaKey.trim() });
294
- if (tavilyKey.trim()) ops.push({ op: "set", path: ["tavilyApiKey"], value: tavilyKey.trim() });
295
- if (keenableKey.trim()) ops.push({ op: "set", path: ["keenableApiKey"], value: keenableKey.trim() });
296
- if (perplexityKey.trim()) ops.push({ op: "set", path: ["perplexityApiKey"], value: perplexityKey.trim() });
297
- if (deepseekKey.trim()) ops.push({ op: "set", path: ["deepseekApiKey"], value: deepseekKey.trim() });
298
- if (geminiKey.trim()) ops.push({ op: "set", path: ["geminiApiKey"], value: geminiKey.trim() });
394
+ ops.push({ op: "set", path: ["keyStorage"], value: keyStorage });
395
+ if (keyStorage !== "credentials") {
396
+ // settings 模式:key 仍写 settings.yaml(旧行为)
397
+ for (const [field, value] of keyFields) {
398
+ if (value.trim()) ops.push({ op: "set", path: [field], value: value.trim() });
399
+ }
400
+ }
299
401
  ops.push({ op: "set", path: ["platforms"], value: platforms });
300
402
  ops.push({ op: "set", path: ["cacheTtl"], value: Math.min(Math.max(Number(cacheTtl) ?? 5, 0), 5) });
301
403
  const result = await bridgeMutate({ ns: NS, ops });
302
- if (result.ok) {
404
+ if (result.ok && !credFailed) {
303
405
  setDirty(false);
304
406
  setProvider(result.value.value.provider ?? provider);
305
407
  setFailed(false);
@@ -452,6 +554,7 @@ react_jsx_runtime.jsx("div", {
452
554
  react_jsx_runtime.jsx("select", {
453
555
  className: "dshfs-select",
454
556
  value: provider,
557
+ style: { colorScheme: selectColorScheme },
455
558
  disabled: !ready || saving,
456
559
  onChange: (e) => select(e.target.value),
457
560
  children: ENGINES.map((engine) =>
@@ -559,6 +662,34 @@ react_jsx_runtime.jsx("div", {
559
662
  className: "dshfs-hint",
560
663
  children: t.keysHint,
561
664
  }),
665
+ react_jsx_runtime.jsx("div", {
666
+ className: "dshfs-fieldRow",
667
+ children: [
668
+ react_jsx_runtime.jsx("label", {
669
+ className: "dshfs-label",
670
+ children: t.keyStorage,
671
+ }),
672
+ react_jsx_runtime.jsx("select", {
673
+ className: "dshfs-select dshfs-keyStorage",
674
+ value: keyStorage,
675
+ style: { colorScheme: selectColorScheme },
676
+ disabled: !ready || saving,
677
+ onChange: (e) => {
678
+ setKeyStorage(e.target.value);
679
+ setDirty(true);
680
+ setFailed(false);
681
+ },
682
+ children: [
683
+ react_jsx_runtime.jsx("option", { value: "credentials", children: t.keyStorageCred }),
684
+ react_jsx_runtime.jsx("option", { value: "settings", children: t.keyStorageSettings }),
685
+ ],
686
+ }),
687
+ ],
688
+ }),
689
+ react_jsx_runtime.jsx("p", {
690
+ className: "dshfs-hint",
691
+ children: keyStorage === "credentials" ? t.keyStorageCredHint(credConfigured) : t.keyStorageSettingsHint,
692
+ }),
562
693
  ],
563
694
  }),
564
695
  react_jsx_runtime.jsx("div", {
package/lib/index.js CHANGED
@@ -16,8 +16,14 @@ const USER_AGENT =
16
16
  const ACCEPT_LANG = "zh-CN,zh;q=0.9,en;q=0.8";
17
17
 
18
18
  const FREE_SEARCH_NS = settingsNamespace("free-search");
19
+ // bridge 路径的【单一权威】定义(M1)。
20
+ // client.js 为独立浏览器 bundle,无法 import 本文件,故在其中保留一份同步副本(同名常量);
21
+ // 改动本路径时,必须同步改 lib/client.js 的 BRIDGE_PREFIX,否则两端桥接会断。
19
22
  const BRIDGE_PREFIX = "/api/dsh-free-search-settings";
20
- const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch"];
23
+ // 免费引擎清单(导出为公共 API)。顺序必须与 search() 内部回退链 freeEngines 一致
24
+ // (见下方 search() 中 const freeEngines = [...]):bing 排最前(最稳的默认回退),
25
+ // 其余按回退优先级排列;改动此处需同步改 freeEngines,否则导出顺序会误导维护者。
26
+ const FREE_ENGINES = ["bing", "anysearch", "ddg", "ddg-lite", "searxng"];
21
27
  const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "perplexity", "deepseek-official", "gemini"];
22
28
 
23
29
  // Gemini (Google search grounding) 引擎。
@@ -28,7 +34,7 @@ const GEMINI_MODEL = "gemini-2.5-flash";
28
34
  const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`;
29
35
 
30
36
  // 当前插件版本(发布时与 package.json 同步)
31
- const PLUGIN_VERSION = "0.5.4";
37
+ const PLUGIN_VERSION = "0.5.6";
32
38
  // 检查更新的 npm registry 元数据地址(@darlingc/dsh-freesearch 是 npmjs 上的公开包)
33
39
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/@darlingc%2Fdsh-freesearch/latest";
34
40
  const PLUGIN_NPM_URL = "https://www.npmjs.com/package/@darlingc/dsh-freesearch";
@@ -1185,7 +1191,7 @@ function toView(descriptor) {
1185
1191
  };
1186
1192
  }
1187
1193
 
1188
- function makeBridgeRoutes(settings, search, testEngine) {
1194
+ function makeBridgeRoutes(settings, search, testEngine, getCredentials) {
1189
1195
  const allowlisted = () =>
1190
1196
  settings
1191
1197
  .describe({ redactSecrets: true })
@@ -1338,6 +1344,52 @@ function makeBridgeRoutes(settings, search, testEngine) {
1338
1344
  }
1339
1345
  return { ok: true, value: toView(descriptor) };
1340
1346
  },
1347
+ // 凭据中心:查询各引擎 key 的配置状态(value 不返回,只返回是否已配置)
1348
+ async credentialsStatus() {
1349
+ const credentials = getCredentials();
1350
+ if (!credentials) return { ok: false, code: "credentials-unavailable", message: "credentials service is not available" };
1351
+ const configured = {};
1352
+ for (const [settingsKey, ref] of Object.entries(KEY_REF_MAP)) {
1353
+ try {
1354
+ const info = await credentials.describe(ref);
1355
+ configured[settingsKey] = info !== undefined && info.configured === true;
1356
+ } catch {
1357
+ configured[settingsKey] = false;
1358
+ }
1359
+ }
1360
+ return { ok: true, value: { configured, available: true } };
1361
+ },
1362
+ // 凭据中心:写入一个引擎 key(ref 白名单限定)
1363
+ async credentialsSet(request) {
1364
+ const credentials = getCredentials();
1365
+ if (!credentials) return { ok: false, code: "credentials-unavailable", message: "credentials service is not available" };
1366
+ const { key, value } = request ?? {};
1367
+ const ref = KEY_REF_MAP[key];
1368
+ if (!ref) return { ok: false, code: "credentials-rejected", message: `unknown credential key "${key}"` };
1369
+ if (typeof value !== "string" || value.trim().length === 0) {
1370
+ return { ok: false, code: "credentials-rejected", message: "value is required" };
1371
+ }
1372
+ try {
1373
+ await credentials.set(ref, value.trim());
1374
+ return { ok: true, value: { ref, set: true } };
1375
+ } catch (error) {
1376
+ return { ok: false, code: "credentials-write-failed", message: error instanceof Error ? error.message : String(error) };
1377
+ }
1378
+ },
1379
+ // 凭据中心:删除一个引擎 key
1380
+ async credentialsUnset(request) {
1381
+ const credentials = getCredentials();
1382
+ if (!credentials) return { ok: false, code: "credentials-unavailable", message: "credentials service is not available" };
1383
+ const { key } = request ?? {};
1384
+ const ref = KEY_REF_MAP[key];
1385
+ if (!ref) return { ok: false, code: "credentials-rejected", message: `unknown credential key "${key}"` };
1386
+ try {
1387
+ await credentials.unset(ref);
1388
+ return { ok: true, value: { ref, set: false } };
1389
+ } catch (error) {
1390
+ return { ok: false, code: "credentials-write-failed", message: error instanceof Error ? error.message : String(error) };
1391
+ }
1392
+ },
1341
1393
  };
1342
1394
 
1343
1395
  const guard = (req, res) => {
@@ -1374,6 +1426,40 @@ function makeBridgeRoutes(settings, search, testEngine) {
1374
1426
  writeJson(res, 200, await handlers.mutate(body));
1375
1427
  },
1376
1428
  },
1429
+ {
1430
+ kind: "exact",
1431
+ path: `${BRIDGE_PREFIX}/credentials-status`,
1432
+ handler: async (req, res) => {
1433
+ if (!guard(req, res)) return;
1434
+ writeJson(res, 200, await handlers.credentialsStatus());
1435
+ },
1436
+ },
1437
+ {
1438
+ kind: "exact",
1439
+ path: `${BRIDGE_PREFIX}/credentials-set`,
1440
+ handler: async (req, res) => {
1441
+ if (!guard(req, res)) return;
1442
+ const body = await readJsonBody(req);
1443
+ if (body === undefined) {
1444
+ writeJson(res, 400, { ok: false, code: "credentials-rejected", message: "malformed JSON body" });
1445
+ return;
1446
+ }
1447
+ writeJson(res, 200, await handlers.credentialsSet(body));
1448
+ },
1449
+ },
1450
+ {
1451
+ kind: "exact",
1452
+ path: `${BRIDGE_PREFIX}/credentials-unset`,
1453
+ handler: async (req, res) => {
1454
+ if (!guard(req, res)) return;
1455
+ const body = await readJsonBody(req);
1456
+ if (body === undefined) {
1457
+ writeJson(res, 400, { ok: false, code: "credentials-rejected", message: "malformed JSON body" });
1458
+ return;
1459
+ }
1460
+ writeJson(res, 200, await handlers.credentialsUnset(body));
1461
+ },
1462
+ },
1377
1463
  {
1378
1464
  kind: "exact",
1379
1465
  path: `${BRIDGE_PREFIX}/check-update`,
@@ -1410,10 +1496,21 @@ function makeBridgeRoutes(settings, search, testEngine) {
1410
1496
  const name = "web-search-free";
1411
1497
  const inject = ["web"];
1412
1498
 
1499
+ // 引擎 key 与凭据中心 ref 的映射(白名单:只允许这些 ref 被 UI 读写凭据中心)
1500
+ const KEY_REF_MAP = {
1501
+ exaApiKey: "EXA_API_KEY",
1502
+ tavilyApiKey: "TAVILY_API_KEY",
1503
+ keenableApiKey: "KEENABLE_API_KEY",
1504
+ perplexityApiKey: "PERPLEXITY_API_KEY",
1505
+ deepseekApiKey: "DEEPSEEK_API_KEY",
1506
+ geminiApiKey: "GEMINI_API_KEY",
1507
+ };
1508
+
1413
1509
  const Config = z.object({
1414
1510
  provider: z.string().default("bing"),
1415
1511
  cache: z.boolean().default(true), // 单 query 结果缓存开关(防限流/省额度)
1416
1512
  cacheTtl: z.number().default(5), // 缓存时长(分钟),0-5 可配置(使用处再 clamp)
1513
+ keyStorage: z.string().default("credentials"), // key 存储位置:credentials(凭据中心)| settings(设置页)
1417
1514
  lang: z.string().default("zh"),
1418
1515
  region: z.string(),
1419
1516
  bingMarket: z.string().default("zh-CN"),
@@ -1430,7 +1527,8 @@ const Config = z.object({
1430
1527
  function apply(ctx, config) {
1431
1528
  let current = () => config ?? {};
1432
1529
  const logger = ctx.logger;
1433
- const credentials = ctx.get("credentials");
1530
+ // credentials 服务可能晚于本插件挂载(跨 bundle 顺序),运行期动态获取而不是 apply 时缓存
1531
+ const getCredentials = () => ctx.get("credentials");
1434
1532
 
1435
1533
  // 系统提示词动态刷新:设置变更时重新生成,避免显示旧引擎
1436
1534
  let refreshPrompt = null;
@@ -1438,16 +1536,17 @@ function apply(ctx, config) {
1438
1536
  // 单 query 结果缓存(provider.search 内闭包持有):LRU 50 条 / TTL 可配置
1439
1537
  const searchCache = new Map(); // key -> { value, expiresAt }
1440
1538
 
1441
- // key 优先级:settings 的 free-search.<x>ApiKey > 环境变量/credentials
1539
+ // key 优先级:credentials(.credentials.yaml 凭据中心,官方推荐)> settings 的 free-search.<x>ApiKey(遗留兼容)> 环境变量
1442
1540
  const resolveApiKey = async (envName, settingsKey) => {
1443
- const cfg = current();
1444
- if (settingsKey && cfg[settingsKey]) return cfg[settingsKey];
1541
+ const credentials = getCredentials();
1445
1542
  if (credentials) {
1446
1543
  try {
1447
1544
  const resolved = await credentials.resolve(envName);
1448
1545
  if (resolved?.value) return resolved.value;
1449
1546
  } catch {}
1450
1547
  }
1548
+ const cfg = current();
1549
+ if (settingsKey && cfg[settingsKey]) return cfg[settingsKey];
1451
1550
  return process.env[envName] ?? "";
1452
1551
  };
1453
1552
 
@@ -1655,7 +1754,8 @@ function apply(ctx, config) {
1655
1754
  const disposers = makeBridgeRoutes(
1656
1755
  sctx.settings,
1657
1756
  (request) => provider.search(request, undefined),
1658
- (engine, query, timeRange) => runEngineTest(engine, query, timeRange)
1757
+ (engine, query, timeRange) => runEngineTest(engine, query, timeRange),
1758
+ getCredentials
1659
1759
  ).map((route) => sctx.webServer.register(route));
1660
1760
  return () => {
1661
1761
  for (const dispose of disposers) dispose();
@@ -1938,7 +2038,7 @@ function apply(ctx, config) {
1938
2038
  },
1939
2039
  engine: {
1940
2040
  type: "string",
1941
- description: "Optional specific engine to try first: ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, perplexity, deepseek-official.",
2041
+ description: "Optional specific engine to try first: ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, perplexity, deepseek-official, gemini.",
1942
2042
  },
1943
2043
  },
1944
2044
  output: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darlingc/dsh-freesearch",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Free web search for DeepSeek Harness: 11 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable keyless + Gemini grounding) + time filtering + platform search + web_fetch, with web settings UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "repository": {
41
41
  "type": "git",
42
- "url": "https://github.com/DarlingC/dsh-freesearch.git"
42
+ "url": "git+https://github.com/DarlingC/dsh-freesearch.git"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=20"
@@ -72,6 +72,9 @@
72
72
  "@deepseek-ai/dsh-client-runtime"
73
73
  ],
74
74
  "platform": "web"
75
+ },
76
+ "engines": {
77
+ "dsh": ">=0.1.1-rc.1"
75
78
  }
76
79
  }
77
80
  }