@darlingc/dsh-freesearch 0.5.3 → 0.5.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 +53 -4
- package/cordis.patch.yml +3 -0
- package/lib/client.js +140 -11
- package/lib/index.js +115 -16
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -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.
|
|
39
|
+
- **版本号 + 检查更新** —— 设置卡片显示当前版本(v0.5.5),"检查更新"按钮直连 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,零额外依赖)
|
|
@@ -57,8 +57,9 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
57
57
|
| `keenable` | Keenable | 免费 | **无 key 也可用**(MCP 匿名),配 key 提升额度 |
|
|
58
58
|
| `perplexity` | Perplexity | 付费 | 需 `PERPLEXITY_API_KEY` |
|
|
59
59
|
| `deepseek-official` | DeepSeek 官方 | 付费 | 需 `DEEPSEEK_API_KEY` |
|
|
60
|
+
| `gemini` | Gemini (Google Search Grounding) | 免费层 | 需 `GEMINI_API_KEY`(AI Studio 免费层;走 Google 联网 grounding) |
|
|
60
61
|
|
|
61
|
-
- **默认引擎为 `bing
|
|
62
|
+
- **默认引擎为 `bing`**(免费且最稳定),安装后开箱即用;**`gemini` 建议作为回退/按需引擎**,不要默认首选(见下方配额警示)。
|
|
62
63
|
- **自动回退**:任何引擎失败(免费限流/反爬,付费缺 key/无效/网络错误)都会自动轮流尝试下一个引擎——先试其他已配 key 的付费引擎,再试免费引擎(Bing/AnySearch 等),并在结果中附带回退提示——搜索不会因引擎问题直接失败。
|
|
63
64
|
- **设置页有官网链接**:免费引擎显示"访问官网 →",付费引擎显示"获取 API Key →"(新标签页打开):
|
|
64
65
|
- Exa:<https://dashboard.exa.ai/api-keys>
|
|
@@ -74,6 +75,17 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
74
75
|
- **Tavily**:通过 `x-tavily-access-mode: keyless` 头走 keyless 匿名额度,不配 key 即可用;配置 `TAVILY_API_KEY` 后走账号档,额度更高、结果质量更稳定。
|
|
75
76
|
- **Keenable**:无 key 时走其公开 MCP 端点(`api.keenable.ai/mcp`)匿名调用;配置 `KEENABLE_API_KEY` 后走 REST API(`api.keenable.ai/v1/search`),额度更高、按组织限流。
|
|
76
77
|
|
|
78
|
+
#### Gemini 联网搜索引擎(Google Search Grounding)
|
|
79
|
+
|
|
80
|
+
- **原理**:调用 Gemini API 的 `generateContent` + `tools:[{google_search:{}}]`(Google Search Grounding),模型在生成时联网并返回 `groundingChunks`(来源 `{uri,title}`)+ `groundingSupports`(回答段落与来源的映射,用于拼 snippet)。结果自动转成统一 `{url,title,snippet}` 形式接入本插件。
|
|
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
|
+
- **模型**:默认 `gemini-2.5-flash`(免费层唯一已实测可联网的模型)。当前 AI Studio 免费层**无需绑卡**即可用 Google Search grounding。
|
|
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 等),搜索不会因此失败。
|
|
86
|
+
- 因此**不建议把 `gemini` 设为默认首选引擎**(`free-search.provider: gemini`),默认 `bing` + 失败自动回退即可;把 `gemini` 当按需/回退引擎最稳。
|
|
87
|
+
- **与其它引擎差异**:Gemini grounding 返回的是"模型 Ground 到的来源集合",不是像 Bing 那样的排序结果列表——数量不固定、通常偏少、无优先级;`maxResults` 只能做客户端截断。作为默认搜索体验与真实搜索引擎略有不同。
|
|
88
|
+
|
|
77
89
|
### 安装
|
|
78
90
|
|
|
79
91
|
推荐从 npm registry 或 git 安装(装成真实拷贝,避免本地 link 软链导致的依赖解析问题):
|
|
@@ -110,6 +122,29 @@ dsh web
|
|
|
110
122
|
|
|
111
123
|
> ⚠️ 依赖:CI 的 `npm stage publish` 需要 GitHub 仓库配置 `NPM_TOKEN` secret(npmjs.com → Access Tokens → Publish 类型 token)。且该包需先在 npm registry 上存在(首次发布无法 stage,需先在网页/本地完成一次正式发布)。
|
|
112
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
|
+
|
|
113
148
|
### 使用
|
|
114
149
|
|
|
115
150
|
#### 网页设置(推荐)
|
|
@@ -118,6 +153,7 @@ dsh web
|
|
|
118
153
|
|
|
119
154
|
- **Search engine**:下拉框切换引擎,保存即生效
|
|
120
155
|
- **API keys**:为 Exa / Tavily / Keenable / Perplexity / DeepSeek 填写 key(密码框,保存后只显示"已配置")
|
|
156
|
+
- **推荐**:付费引擎 key 建议写入 harness 凭据中心 `~/.dsh/.credentials.yaml`(如 `DEEPSEEK_API_KEY: sk-...`,与官方 LLM provider 一致,一处管理所有 key)。插件读取优先级:凭据中心 > 设置页 > 环境变量,设置页填的 key 仅作为遗留兼容。
|
|
121
157
|
- **Test engine**:直测当前引擎可用性(不走回退链,付费引擎无 key 会明确报错)
|
|
122
158
|
- **Use Bing default**:把当前搜索引擎切回稳定的免费 Bing;`Discard` 只撤销尚未保存的编辑
|
|
123
159
|
- **Platform search**:勾选启用 GitHub / V2EX / Bilibili 平台搜索(`platform_search` 工具按此过滤)
|
|
@@ -295,7 +331,7 @@ This plugin provides multiple free search engines with automatic fallback, compl
|
|
|
295
331
|
- **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.
|
|
296
332
|
- **Time Filtering** — The `advanced_search` tool supports `timeRange`: fixed tiers, custom relative values, or an absolute date (details below)
|
|
297
333
|
- **System Prompt Injection** — The agent is aware of the currently active engine and which engines require API keys
|
|
298
|
-
- **Version + Update Check** — The settings card shows the current version (v0.
|
|
334
|
+
- **Version + Update Check** — The settings card shows the current version (v0.5.5), 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
|
|
299
335
|
- **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)
|
|
300
336
|
- **Visual Badges** — Free engines feature a green `FREE` badge, while paid engines show an orange `API KEY` badge in the settings UI
|
|
301
337
|
- **Webpage Fetching (`web_fetch`)** — Allows the agent to read full webpage contents (official `dsh-web-fetch-http` provider, pure JS, zero extra dependencies)
|
|
@@ -316,8 +352,9 @@ This plugin provides multiple free search engines with automatic fallback, compl
|
|
|
316
352
|
| `keenable` | Keenable | Free | **Usable without a key** (anonymous MCP); configure a key for higher quota |
|
|
317
353
|
| `perplexity` | Perplexity | Paid | Requires `PERPLEXITY_API_KEY` |
|
|
318
354
|
| `deepseek-official` | DeepSeek Official | Paid | Requires `DEEPSEEK_API_KEY` |
|
|
355
|
+
| `gemini` | Gemini (Google Search Grounding) | Free tier | Requires `GEMINI_API_KEY` (AI Studio free tier; uses Google Search Grounding) |
|
|
319
356
|
|
|
320
|
-
- **Default engine is `bing`** (free and most stable), ready to use out of the box
|
|
357
|
+
- **Default engine is `bing`** (free and most stable), ready to use out of the box. **Prefer keeping `gemini` as a fallback / on-demand engine** rather than the default (see quota warning below).
|
|
321
358
|
- **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 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.
|
|
322
359
|
- **Official Links in Settings**: Free engines display "Visit Website →", while paid engines display "Get API Key →" (opens in a new tab):
|
|
323
360
|
- Exa: <https://dashboard.exa.ai/api-keys>
|
|
@@ -333,6 +370,17 @@ This plugin provides multiple free search engines with automatic fallback, compl
|
|
|
333
370
|
- **Tavily**: offers keyless anonymous quota via the `x-tavily-access-mode: keyless` header — it works without a key; configuring `TAVILY_API_KEY` switches to the account tier for higher quota and more stable results.
|
|
334
371
|
- **Keenable**: without a key it is called via its public MCP endpoint (`api.keenable.ai/mcp`); configuring `KEENABLE_API_KEY` switches to the REST API (`api.keenable.ai/v1/search`) for higher quota and organization-scoped rate limits.
|
|
335
372
|
|
|
373
|
+
#### Gemini live web search (Google Search Grounding)
|
|
374
|
+
|
|
375
|
+
- **How it works**: calls `generateContent` with `tools:[{google_search:{}}]` (Google Search Grounding); the model searches live during generation and returns `groundingChunks` (sources `{uri,title}`) + `groundingSupports` (answer-segment → chunk mapping used to build the snippet). Results are normalized into the plugin's unified `{url,title,snippet}` shape.
|
|
376
|
+
- **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`.
|
|
377
|
+
- **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**.
|
|
378
|
+
- **⚠️ Quota warning (important)**:
|
|
379
|
+
- `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.
|
|
380
|
+
- **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.
|
|
381
|
+
- So keep `provider: bing` as default and let `gemini` act as a fallback / on-demand engine rather than the primary load.
|
|
382
|
+
- **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.
|
|
383
|
+
|
|
336
384
|
### Installation
|
|
337
385
|
|
|
338
386
|
Install from the npm registry or Git (installed as a real copy, which avoids the dependency-resolution problem caused by a local `link:` symlink):
|
|
@@ -367,6 +415,7 @@ After installation, navigate to **Settings → Plugins → Configurable** tab
|
|
|
367
415
|
|
|
368
416
|
- **Search engine**: Select an engine from the dropdown; changes take effect immediately upon saving.
|
|
369
417
|
- **API keys**: Enter keys for Exa / Tavily / Keenable / Perplexity / DeepSeek (password fields; displayed as "configured" once saved).
|
|
418
|
+
- **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.
|
|
370
419
|
- **Test engine**: Tests the selected engine directly (no fallback chain; paid engines without a key report an explicit error).
|
|
371
420
|
- **Use Bing default**: stage a switch back to the stable free Bing engine; `Discard` only cancels unsaved edits
|
|
372
421
|
- **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).
|
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}",
|
|
@@ -84,7 +91,12 @@ window.__ModuleLoader__.load({
|
|
|
84
91
|
perplexityPh: (c) => c ? "Perplexity API 密钥(已配置)" : "Perplexity API 密钥(pplx-...)",
|
|
85
92
|
deepseekPh: (c) => c ? "DeepSeek API 密钥(已配置)" : "DeepSeek API 密钥(sk-...)",
|
|
86
93
|
geminiPh: (c) => c ? "Gemini API 密钥(已配置)" : "Gemini API 密钥(免费层,联网搜索)",
|
|
87
|
-
keysHint: "
|
|
94
|
+
keysHint: "密钥读取优先级:.credentials.yaml 凭据中心 > 这里 > 环境变量。推荐把 key 写进凭据中心(与官方 LLM 一致,一处管理)。",
|
|
95
|
+
keyStorage: "Key 存储位置",
|
|
96
|
+
keyStorageCred: "凭据中心(推荐)",
|
|
97
|
+
keyStorageSettings: "设置页(兼容)",
|
|
98
|
+
keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["exa", "tavily", "keenable", "perplexity", "deepseek", "gemini"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
|
|
99
|
+
keyStorageSettingsHint: "保存后写入 settings.yaml(向后兼容路径,优先级低于凭据中心)。",
|
|
88
100
|
platformSearch: "平台搜索(platform_search 工具)",
|
|
89
101
|
platformHint: "为 agent 的 platform_search 工具启用平台。禁用的平台会被跳过。",
|
|
90
102
|
cacheTtl: "结果缓存时长(分钟)",
|
|
@@ -126,7 +138,12 @@ window.__ModuleLoader__.load({
|
|
|
126
138
|
perplexityPh: (c) => c ? "Perplexity API key (configured)" : "Perplexity API key (pplx-...)",
|
|
127
139
|
deepseekPh: (c) => c ? "DeepSeek API key (configured)" : "DeepSeek API key (sk-...)",
|
|
128
140
|
geminiPh: (c) => c ? "Gemini API key (configured)" : "Gemini API key (free tier, web search)",
|
|
129
|
-
keysHint: "
|
|
141
|
+
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).",
|
|
142
|
+
keyStorage: "Key storage",
|
|
143
|
+
keyStorageCred: "Credential center (recommended)",
|
|
144
|
+
keyStorageSettings: "Settings page (legacy)",
|
|
145
|
+
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"}`,
|
|
146
|
+
keyStorageSettingsHint: "Saved to settings.yaml (backward-compatible path; lower priority than the credential center).",
|
|
130
147
|
platformSearch: "Platform search (platform_search tool)",
|
|
131
148
|
platformHint: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
|
|
132
149
|
cacheTtl: "Result cache TTL (minutes)",
|
|
@@ -156,8 +173,8 @@ window.__ModuleLoader__.load({
|
|
|
156
173
|
},
|
|
157
174
|
};
|
|
158
175
|
const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
|
|
159
|
-
// 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
|
|
160
|
-
const PLUGIN_VERSION = "0.5.
|
|
176
|
+
// 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION、package.json 保持一致)
|
|
177
|
+
const PLUGIN_VERSION = "0.5.5";
|
|
161
178
|
const ENGINES = [
|
|
162
179
|
{ id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
|
|
163
180
|
{ id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
|
|
@@ -208,7 +225,54 @@ window.__ModuleLoader__.load({
|
|
|
208
225
|
return response.json();
|
|
209
226
|
}
|
|
210
227
|
|
|
228
|
+
async function bridgeCredentialsStatus() {
|
|
229
|
+
const response = await fetch(`${BRIDGE_PREFIX}/credentials-status`, {
|
|
230
|
+
method: "POST",
|
|
231
|
+
headers: { "content-type": "application/json" },
|
|
232
|
+
body: "{}",
|
|
233
|
+
});
|
|
234
|
+
return response.json();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function bridgeCredentialsSet(key, value) {
|
|
238
|
+
const response = await fetch(`${BRIDGE_PREFIX}/credentials-set`, {
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers: { "content-type": "application/json" },
|
|
241
|
+
body: JSON.stringify({ key, value }),
|
|
242
|
+
});
|
|
243
|
+
return response.json();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function bridgeCredentialsUnset(key) {
|
|
247
|
+
const response = await fetch(`${BRIDGE_PREFIX}/credentials-unset`, {
|
|
248
|
+
method: "POST",
|
|
249
|
+
headers: { "content-type": "application/json" },
|
|
250
|
+
body: JSON.stringify({ key }),
|
|
251
|
+
});
|
|
252
|
+
return response.json();
|
|
253
|
+
}
|
|
254
|
+
|
|
211
255
|
function FreeSearchCard(props) {
|
|
256
|
+
// 检测应用主题深浅(读 body 的 --dsw-alias-bg-base 变量亮度),用于锁定原生下拉配色
|
|
257
|
+
const isDarkScheme = react.useMemo(() => {
|
|
258
|
+
try {
|
|
259
|
+
const root = document.body || document.documentElement;
|
|
260
|
+
const bg = getComputedStyle(root).getPropertyValue("--dsw-alias-bg-base").trim();
|
|
261
|
+
const m = bg.match(/(\d+)\s*[, ]\s*(\d+)\s*[, ]\s*(\d+)/);
|
|
262
|
+
if (m) {
|
|
263
|
+
const l = 0.299 * Number(m[1]) + 0.587 * Number(m[2]) + 0.114 * Number(m[3]);
|
|
264
|
+
return l < 128;
|
|
265
|
+
}
|
|
266
|
+
if (/^#([0-9a-f]{3,8})/i.test(bg)) {
|
|
267
|
+
const hex = bg.slice(1);
|
|
268
|
+
const h = hex.length <= 4 ? hex.replace(/./g, (c) => c + c) : hex;
|
|
269
|
+
const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
|
|
270
|
+
return 0.299 * r + 0.587 * g + 0.114 * b < 128;
|
|
271
|
+
}
|
|
272
|
+
} catch {}
|
|
273
|
+
return typeof matchMedia === "function" ? matchMedia("(prefers-color-scheme: dark)").matches : false;
|
|
274
|
+
}, []);
|
|
275
|
+
const selectColorScheme = isDarkScheme ? "dark" : "light";
|
|
212
276
|
const [open, setOpen] = react.useState(false);
|
|
213
277
|
const [state, setState] = react.useState({ status: "loading" });
|
|
214
278
|
const [provider, setProvider] = react.useState("bing");
|
|
@@ -221,6 +285,10 @@ window.__ModuleLoader__.load({
|
|
|
221
285
|
const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
|
|
222
286
|
const [cacheTtl, setCacheTtl] = react.useState(5);
|
|
223
287
|
const [keysConfigured, setKeysConfigured] = react.useState({});
|
|
288
|
+
// key 存储位置:credentials(凭据中心,默认)| settings(设置页,兼容旧行为)
|
|
289
|
+
const [keyStorage, setKeyStorage] = react.useState("credentials");
|
|
290
|
+
// 凭据中心里已配置的 key(describe 不返回,需单独查)
|
|
291
|
+
const [credConfigured, setCredConfigured] = react.useState({});
|
|
224
292
|
const [lang, setLang] = react.useState("zh");
|
|
225
293
|
const [dirty, setDirty] = react.useState(false);
|
|
226
294
|
const [saving, setSaving] = react.useState(false);
|
|
@@ -262,7 +330,21 @@ window.__ModuleLoader__.load({
|
|
|
262
330
|
}
|
|
263
331
|
}
|
|
264
332
|
setKeysConfigured(configured);
|
|
333
|
+
// key 存储位置(默认凭据中心)
|
|
334
|
+
setKeyStorage(v.keyStorage === "settings" ? "settings" : "credentials");
|
|
265
335
|
setState({ status: "ready", writable: result.value.writable });
|
|
336
|
+
// 查询凭据中心里各 key 的配置状态
|
|
337
|
+
try {
|
|
338
|
+
const cred = await bridgeCredentialsStatus();
|
|
339
|
+
if (cred.ok) {
|
|
340
|
+
const cc = {};
|
|
341
|
+
const map = { exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek", geminiApiKey: "gemini" };
|
|
342
|
+
for (const [k, v] of Object.entries(cred.value.configured ?? {})) {
|
|
343
|
+
if (map[k]) cc[map[k]] = v;
|
|
344
|
+
}
|
|
345
|
+
setCredConfigured(cc);
|
|
346
|
+
}
|
|
347
|
+
} catch {}
|
|
266
348
|
} else {
|
|
267
349
|
setState({ status: "unavailable" });
|
|
268
350
|
}
|
|
@@ -288,18 +370,36 @@ window.__ModuleLoader__.load({
|
|
|
288
370
|
setSaving(true);
|
|
289
371
|
setFailed(false);
|
|
290
372
|
try {
|
|
373
|
+
// key 存储分流:凭据中心(默认)走 credentials-set;设置页走 settings mutate(兼容)
|
|
374
|
+
const keyFields = [
|
|
375
|
+
["exaApiKey", exaKey],
|
|
376
|
+
["tavilyApiKey", tavilyKey],
|
|
377
|
+
["keenableApiKey", keenableKey],
|
|
378
|
+
["perplexityApiKey", perplexityKey],
|
|
379
|
+
["deepseekApiKey", deepseekKey],
|
|
380
|
+
["geminiApiKey", geminiKey],
|
|
381
|
+
];
|
|
382
|
+
let credFailed = false;
|
|
383
|
+
if (keyStorage === "credentials") {
|
|
384
|
+
for (const [field, value] of keyFields) {
|
|
385
|
+
if (!value.trim()) continue;
|
|
386
|
+
const r = await bridgeCredentialsSet(field, value.trim());
|
|
387
|
+
if (!r.ok) credFailed = true;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
291
390
|
const ops = [{ op: "set", path: ["provider"], value: provider }];
|
|
292
391
|
ops.push({ op: "set", path: ["lang"], value: lang });
|
|
293
|
-
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
392
|
+
ops.push({ op: "set", path: ["keyStorage"], value: keyStorage });
|
|
393
|
+
if (keyStorage !== "credentials") {
|
|
394
|
+
// settings 模式:key 仍写 settings.yaml(旧行为)
|
|
395
|
+
for (const [field, value] of keyFields) {
|
|
396
|
+
if (value.trim()) ops.push({ op: "set", path: [field], value: value.trim() });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
299
399
|
ops.push({ op: "set", path: ["platforms"], value: platforms });
|
|
300
400
|
ops.push({ op: "set", path: ["cacheTtl"], value: Math.min(Math.max(Number(cacheTtl) ?? 5, 0), 5) });
|
|
301
401
|
const result = await bridgeMutate({ ns: NS, ops });
|
|
302
|
-
if (result.ok) {
|
|
402
|
+
if (result.ok && !credFailed) {
|
|
303
403
|
setDirty(false);
|
|
304
404
|
setProvider(result.value.value.provider ?? provider);
|
|
305
405
|
setFailed(false);
|
|
@@ -452,6 +552,7 @@ react_jsx_runtime.jsx("div", {
|
|
|
452
552
|
react_jsx_runtime.jsx("select", {
|
|
453
553
|
className: "dshfs-select",
|
|
454
554
|
value: provider,
|
|
555
|
+
style: { colorScheme: selectColorScheme },
|
|
455
556
|
disabled: !ready || saving,
|
|
456
557
|
onChange: (e) => select(e.target.value),
|
|
457
558
|
children: ENGINES.map((engine) =>
|
|
@@ -559,6 +660,34 @@ react_jsx_runtime.jsx("div", {
|
|
|
559
660
|
className: "dshfs-hint",
|
|
560
661
|
children: t.keysHint,
|
|
561
662
|
}),
|
|
663
|
+
react_jsx_runtime.jsx("div", {
|
|
664
|
+
className: "dshfs-fieldRow",
|
|
665
|
+
children: [
|
|
666
|
+
react_jsx_runtime.jsx("label", {
|
|
667
|
+
className: "dshfs-label",
|
|
668
|
+
children: t.keyStorage,
|
|
669
|
+
}),
|
|
670
|
+
react_jsx_runtime.jsx("select", {
|
|
671
|
+
className: "dshfs-select dshfs-keyStorage",
|
|
672
|
+
value: keyStorage,
|
|
673
|
+
style: { colorScheme: selectColorScheme },
|
|
674
|
+
disabled: !ready || saving,
|
|
675
|
+
onChange: (e) => {
|
|
676
|
+
setKeyStorage(e.target.value);
|
|
677
|
+
setDirty(true);
|
|
678
|
+
setFailed(false);
|
|
679
|
+
},
|
|
680
|
+
children: [
|
|
681
|
+
react_jsx_runtime.jsx("option", { value: "credentials", children: t.keyStorageCred }),
|
|
682
|
+
react_jsx_runtime.jsx("option", { value: "settings", children: t.keyStorageSettings }),
|
|
683
|
+
],
|
|
684
|
+
}),
|
|
685
|
+
],
|
|
686
|
+
}),
|
|
687
|
+
react_jsx_runtime.jsx("p", {
|
|
688
|
+
className: "dshfs-hint",
|
|
689
|
+
children: keyStorage === "credentials" ? t.keyStorageCredHint(credConfigured) : t.keyStorageSettingsHint,
|
|
690
|
+
}),
|
|
562
691
|
],
|
|
563
692
|
}),
|
|
564
693
|
react_jsx_runtime.jsx("div", {
|
package/lib/index.js
CHANGED
|
@@ -28,7 +28,7 @@ const GEMINI_MODEL = "gemini-2.5-flash";
|
|
|
28
28
|
const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`;
|
|
29
29
|
|
|
30
30
|
// 当前插件版本(发布时与 package.json 同步)
|
|
31
|
-
const PLUGIN_VERSION = "0.5.
|
|
31
|
+
const PLUGIN_VERSION = "0.5.5";
|
|
32
32
|
// 检查更新的 npm registry 元数据地址(@darlingc/dsh-freesearch 是 npmjs 上的公开包)
|
|
33
33
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/@darlingc%2Fdsh-freesearch/latest";
|
|
34
34
|
const PLUGIN_NPM_URL = "https://www.npmjs.com/package/@darlingc/dsh-freesearch";
|
|
@@ -67,21 +67,26 @@ function compareVersions(a, b) {
|
|
|
67
67
|
return 0;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// 检测插件的安装模式:遍历 profiles/*/node_modules
|
|
70
|
+
// 检测插件的安装模式:遍历 profiles/*/node_modules/<pkg>,
|
|
71
71
|
// symlink(link: 本地开发)→ isLink=true;npm 真安装 → isLink=false;找不到 → null
|
|
72
|
+
// 主包名已 scoped 为 @darlingc/dsh-freesearch(路径 node_modules/@darlingc/dsh-freesearch);
|
|
73
|
+
// 兼容历史遗留的非 scoped 旧名 dsh-free-search,避免旧安装检测不到。
|
|
74
|
+
const PACKAGE_BASENAMES = ["@darlingc/dsh-freesearch", "dsh-free-search"];
|
|
72
75
|
function detectInstallMode() {
|
|
73
76
|
const profilesDir = path.join(process.cwd(), "profiles");
|
|
74
77
|
let found = null;
|
|
75
78
|
try {
|
|
76
79
|
for (const name of fs.readdirSync(profilesDir)) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
for (const dirName of PACKAGE_BASENAMES) {
|
|
81
|
+
const pkgPath = path.join(profilesDir, name, "node_modules", ...dirName.split("/"));
|
|
82
|
+
if (!fs.existsSync(pkgPath)) continue;
|
|
83
|
+
let isLink = false;
|
|
84
|
+
try {
|
|
85
|
+
isLink = fs.lstatSync(pkgPath).isSymbolicLink();
|
|
86
|
+
} catch {}
|
|
87
|
+
found = { profileDir: path.join(profilesDir, name), isLink };
|
|
88
|
+
return found;
|
|
89
|
+
}
|
|
85
90
|
}
|
|
86
91
|
} catch {}
|
|
87
92
|
return found;
|
|
@@ -1180,7 +1185,7 @@ function toView(descriptor) {
|
|
|
1180
1185
|
};
|
|
1181
1186
|
}
|
|
1182
1187
|
|
|
1183
|
-
function makeBridgeRoutes(settings, search, testEngine) {
|
|
1188
|
+
function makeBridgeRoutes(settings, search, testEngine, getCredentials) {
|
|
1184
1189
|
const allowlisted = () =>
|
|
1185
1190
|
settings
|
|
1186
1191
|
.describe({ redactSecrets: true })
|
|
@@ -1333,6 +1338,52 @@ function makeBridgeRoutes(settings, search, testEngine) {
|
|
|
1333
1338
|
}
|
|
1334
1339
|
return { ok: true, value: toView(descriptor) };
|
|
1335
1340
|
},
|
|
1341
|
+
// 凭据中心:查询各引擎 key 的配置状态(value 不返回,只返回是否已配置)
|
|
1342
|
+
async credentialsStatus() {
|
|
1343
|
+
const credentials = getCredentials();
|
|
1344
|
+
if (!credentials) return { ok: false, code: "credentials-unavailable", message: "credentials service is not available" };
|
|
1345
|
+
const configured = {};
|
|
1346
|
+
for (const [settingsKey, ref] of Object.entries(KEY_REF_MAP)) {
|
|
1347
|
+
try {
|
|
1348
|
+
const info = await credentials.describe(ref);
|
|
1349
|
+
configured[settingsKey] = info !== undefined && info.configured === true;
|
|
1350
|
+
} catch {
|
|
1351
|
+
configured[settingsKey] = false;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return { ok: true, value: { configured, available: true } };
|
|
1355
|
+
},
|
|
1356
|
+
// 凭据中心:写入一个引擎 key(ref 白名单限定)
|
|
1357
|
+
async credentialsSet(request) {
|
|
1358
|
+
const credentials = getCredentials();
|
|
1359
|
+
if (!credentials) return { ok: false, code: "credentials-unavailable", message: "credentials service is not available" };
|
|
1360
|
+
const { key, value } = request ?? {};
|
|
1361
|
+
const ref = KEY_REF_MAP[key];
|
|
1362
|
+
if (!ref) return { ok: false, code: "credentials-rejected", message: `unknown credential key "${key}"` };
|
|
1363
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1364
|
+
return { ok: false, code: "credentials-rejected", message: "value is required" };
|
|
1365
|
+
}
|
|
1366
|
+
try {
|
|
1367
|
+
await credentials.set(ref, value.trim());
|
|
1368
|
+
return { ok: true, value: { ref, set: true } };
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
return { ok: false, code: "credentials-write-failed", message: error instanceof Error ? error.message : String(error) };
|
|
1371
|
+
}
|
|
1372
|
+
},
|
|
1373
|
+
// 凭据中心:删除一个引擎 key
|
|
1374
|
+
async credentialsUnset(request) {
|
|
1375
|
+
const credentials = getCredentials();
|
|
1376
|
+
if (!credentials) return { ok: false, code: "credentials-unavailable", message: "credentials service is not available" };
|
|
1377
|
+
const { key } = request ?? {};
|
|
1378
|
+
const ref = KEY_REF_MAP[key];
|
|
1379
|
+
if (!ref) return { ok: false, code: "credentials-rejected", message: `unknown credential key "${key}"` };
|
|
1380
|
+
try {
|
|
1381
|
+
await credentials.unset(ref);
|
|
1382
|
+
return { ok: true, value: { ref, set: false } };
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
return { ok: false, code: "credentials-write-failed", message: error instanceof Error ? error.message : String(error) };
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1336
1387
|
};
|
|
1337
1388
|
|
|
1338
1389
|
const guard = (req, res) => {
|
|
@@ -1369,6 +1420,40 @@ function makeBridgeRoutes(settings, search, testEngine) {
|
|
|
1369
1420
|
writeJson(res, 200, await handlers.mutate(body));
|
|
1370
1421
|
},
|
|
1371
1422
|
},
|
|
1423
|
+
{
|
|
1424
|
+
kind: "exact",
|
|
1425
|
+
path: `${BRIDGE_PREFIX}/credentials-status`,
|
|
1426
|
+
handler: async (req, res) => {
|
|
1427
|
+
if (!guard(req, res)) return;
|
|
1428
|
+
writeJson(res, 200, await handlers.credentialsStatus());
|
|
1429
|
+
},
|
|
1430
|
+
},
|
|
1431
|
+
{
|
|
1432
|
+
kind: "exact",
|
|
1433
|
+
path: `${BRIDGE_PREFIX}/credentials-set`,
|
|
1434
|
+
handler: async (req, res) => {
|
|
1435
|
+
if (!guard(req, res)) return;
|
|
1436
|
+
const body = await readJsonBody(req);
|
|
1437
|
+
if (body === undefined) {
|
|
1438
|
+
writeJson(res, 400, { ok: false, code: "credentials-rejected", message: "malformed JSON body" });
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
writeJson(res, 200, await handlers.credentialsSet(body));
|
|
1442
|
+
},
|
|
1443
|
+
},
|
|
1444
|
+
{
|
|
1445
|
+
kind: "exact",
|
|
1446
|
+
path: `${BRIDGE_PREFIX}/credentials-unset`,
|
|
1447
|
+
handler: async (req, res) => {
|
|
1448
|
+
if (!guard(req, res)) return;
|
|
1449
|
+
const body = await readJsonBody(req);
|
|
1450
|
+
if (body === undefined) {
|
|
1451
|
+
writeJson(res, 400, { ok: false, code: "credentials-rejected", message: "malformed JSON body" });
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
writeJson(res, 200, await handlers.credentialsUnset(body));
|
|
1455
|
+
},
|
|
1456
|
+
},
|
|
1372
1457
|
{
|
|
1373
1458
|
kind: "exact",
|
|
1374
1459
|
path: `${BRIDGE_PREFIX}/check-update`,
|
|
@@ -1405,10 +1490,21 @@ function makeBridgeRoutes(settings, search, testEngine) {
|
|
|
1405
1490
|
const name = "web-search-free";
|
|
1406
1491
|
const inject = ["web"];
|
|
1407
1492
|
|
|
1493
|
+
// 引擎 key 与凭据中心 ref 的映射(白名单:只允许这些 ref 被 UI 读写凭据中心)
|
|
1494
|
+
const KEY_REF_MAP = {
|
|
1495
|
+
exaApiKey: "EXA_API_KEY",
|
|
1496
|
+
tavilyApiKey: "TAVILY_API_KEY",
|
|
1497
|
+
keenableApiKey: "KEENABLE_API_KEY",
|
|
1498
|
+
perplexityApiKey: "PERPLEXITY_API_KEY",
|
|
1499
|
+
deepseekApiKey: "DEEPSEEK_API_KEY",
|
|
1500
|
+
geminiApiKey: "GEMINI_API_KEY",
|
|
1501
|
+
};
|
|
1502
|
+
|
|
1408
1503
|
const Config = z.object({
|
|
1409
1504
|
provider: z.string().default("bing"),
|
|
1410
1505
|
cache: z.boolean().default(true), // 单 query 结果缓存开关(防限流/省额度)
|
|
1411
1506
|
cacheTtl: z.number().default(5), // 缓存时长(分钟),0-5 可配置(使用处再 clamp)
|
|
1507
|
+
keyStorage: z.string().default("credentials"), // key 存储位置:credentials(凭据中心)| settings(设置页)
|
|
1412
1508
|
lang: z.string().default("zh"),
|
|
1413
1509
|
region: z.string(),
|
|
1414
1510
|
bingMarket: z.string().default("zh-CN"),
|
|
@@ -1425,7 +1521,8 @@ const Config = z.object({
|
|
|
1425
1521
|
function apply(ctx, config) {
|
|
1426
1522
|
let current = () => config ?? {};
|
|
1427
1523
|
const logger = ctx.logger;
|
|
1428
|
-
|
|
1524
|
+
// credentials 服务可能晚于本插件挂载(跨 bundle 顺序),运行期动态获取而不是 apply 时缓存
|
|
1525
|
+
const getCredentials = () => ctx.get("credentials");
|
|
1429
1526
|
|
|
1430
1527
|
// 系统提示词动态刷新:设置变更时重新生成,避免显示旧引擎
|
|
1431
1528
|
let refreshPrompt = null;
|
|
@@ -1433,16 +1530,17 @@ function apply(ctx, config) {
|
|
|
1433
1530
|
// 单 query 结果缓存(provider.search 内闭包持有):LRU 50 条 / TTL 可配置
|
|
1434
1531
|
const searchCache = new Map(); // key -> { value, expiresAt }
|
|
1435
1532
|
|
|
1436
|
-
// key 优先级:settings 的 free-search.<x>ApiKey
|
|
1533
|
+
// key 优先级:credentials(.credentials.yaml 凭据中心,官方推荐)> settings 的 free-search.<x>ApiKey(遗留兼容)> 环境变量
|
|
1437
1534
|
const resolveApiKey = async (envName, settingsKey) => {
|
|
1438
|
-
const
|
|
1439
|
-
if (settingsKey && cfg[settingsKey]) return cfg[settingsKey];
|
|
1535
|
+
const credentials = getCredentials();
|
|
1440
1536
|
if (credentials) {
|
|
1441
1537
|
try {
|
|
1442
1538
|
const resolved = await credentials.resolve(envName);
|
|
1443
1539
|
if (resolved?.value) return resolved.value;
|
|
1444
1540
|
} catch {}
|
|
1445
1541
|
}
|
|
1542
|
+
const cfg = current();
|
|
1543
|
+
if (settingsKey && cfg[settingsKey]) return cfg[settingsKey];
|
|
1446
1544
|
return process.env[envName] ?? "";
|
|
1447
1545
|
};
|
|
1448
1546
|
|
|
@@ -1650,7 +1748,8 @@ function apply(ctx, config) {
|
|
|
1650
1748
|
const disposers = makeBridgeRoutes(
|
|
1651
1749
|
sctx.settings,
|
|
1652
1750
|
(request) => provider.search(request, undefined),
|
|
1653
|
-
(engine, query, timeRange) => runEngineTest(engine, query, timeRange)
|
|
1751
|
+
(engine, query, timeRange) => runEngineTest(engine, query, timeRange),
|
|
1752
|
+
getCredentials
|
|
1654
1753
|
).map((route) => sctx.webServer.register(route));
|
|
1655
1754
|
return () => {
|
|
1656
1755
|
for (const dispose of disposers) dispose();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@darlingc/dsh-freesearch",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
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",
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
"lib/client.js",
|
|
15
15
|
"cordis.patch.yml"
|
|
16
16
|
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node scripts/check-version.mjs",
|
|
19
|
+
"check:version": "node scripts/check-version.mjs",
|
|
20
|
+
"postversion": "node scripts/check-version.mjs"
|
|
21
|
+
},
|
|
17
22
|
"keywords": [
|
|
18
23
|
"deepseek-harness",
|
|
19
24
|
"dsh",
|
|
@@ -34,7 +39,7 @@
|
|
|
34
39
|
},
|
|
35
40
|
"repository": {
|
|
36
41
|
"type": "git",
|
|
37
|
-
"url": "https://github.com/DarlingC/dsh-freesearch.git"
|
|
42
|
+
"url": "git+https://github.com/DarlingC/dsh-freesearch.git"
|
|
38
43
|
},
|
|
39
44
|
"engines": {
|
|
40
45
|
"node": ">=20"
|
|
@@ -67,6 +72,9 @@
|
|
|
67
72
|
"@deepseek-ai/dsh-client-runtime"
|
|
68
73
|
],
|
|
69
74
|
"platform": "web"
|
|
75
|
+
},
|
|
76
|
+
"engines": {
|
|
77
|
+
"dsh": ">=0.1.1-rc.1"
|
|
70
78
|
}
|
|
71
79
|
}
|
|
72
80
|
}
|