@darlingc/dsh-freesearch 0.5.4 → 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 +27 -2
- package/cordis.patch.yml +3 -0
- package/lib/client.js +139 -10
- package/lib/index.js +101 -7
- package/package.json +5 -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,零额外依赖)
|
|
@@ -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` 工具按此过滤)
|
|
@@ -307,7 +331,7 @@ This plugin provides multiple free search engines with automatic fallback, compl
|
|
|
307
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.
|
|
308
332
|
- **Time Filtering** — The `advanced_search` tool supports `timeRange`: fixed tiers, custom relative values, or an absolute date (details below)
|
|
309
333
|
- **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.
|
|
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
|
|
311
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)
|
|
312
336
|
- **Visual Badges** — Free engines feature a green `FREE` badge, while paid engines show an orange `API KEY` badge in the settings UI
|
|
313
337
|
- **Webpage Fetching (`web_fetch`)** — Allows the agent to read full webpage contents (official `dsh-web-fetch-http` provider, pure JS, zero extra dependencies)
|
|
@@ -391,6 +415,7 @@ After installation, navigate to **Settings → Plugins → Configurable** tab
|
|
|
391
415
|
|
|
392
416
|
- **Search engine**: Select an engine from the dropdown; changes take effect immediately upon saving.
|
|
393
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.
|
|
394
419
|
- **Test engine**: Tests the selected engine directly (no fallback chain; paid engines without a key report an explicit error).
|
|
395
420
|
- **Use Bing default**: stage a switch back to the stable free Bing engine; `Discard` only cancels unsaved edits
|
|
396
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)",
|
|
@@ -157,7 +174,7 @@ window.__ModuleLoader__.load({
|
|
|
157
174
|
};
|
|
158
175
|
const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
|
|
159
176
|
// 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION、package.json 保持一致)
|
|
160
|
-
const PLUGIN_VERSION = "0.5.
|
|
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";
|
|
@@ -1185,7 +1185,7 @@ function toView(descriptor) {
|
|
|
1185
1185
|
};
|
|
1186
1186
|
}
|
|
1187
1187
|
|
|
1188
|
-
function makeBridgeRoutes(settings, search, testEngine) {
|
|
1188
|
+
function makeBridgeRoutes(settings, search, testEngine, getCredentials) {
|
|
1189
1189
|
const allowlisted = () =>
|
|
1190
1190
|
settings
|
|
1191
1191
|
.describe({ redactSecrets: true })
|
|
@@ -1338,6 +1338,52 @@ function makeBridgeRoutes(settings, search, testEngine) {
|
|
|
1338
1338
|
}
|
|
1339
1339
|
return { ok: true, value: toView(descriptor) };
|
|
1340
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
|
+
},
|
|
1341
1387
|
};
|
|
1342
1388
|
|
|
1343
1389
|
const guard = (req, res) => {
|
|
@@ -1374,6 +1420,40 @@ function makeBridgeRoutes(settings, search, testEngine) {
|
|
|
1374
1420
|
writeJson(res, 200, await handlers.mutate(body));
|
|
1375
1421
|
},
|
|
1376
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
|
+
},
|
|
1377
1457
|
{
|
|
1378
1458
|
kind: "exact",
|
|
1379
1459
|
path: `${BRIDGE_PREFIX}/check-update`,
|
|
@@ -1410,10 +1490,21 @@ function makeBridgeRoutes(settings, search, testEngine) {
|
|
|
1410
1490
|
const name = "web-search-free";
|
|
1411
1491
|
const inject = ["web"];
|
|
1412
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
|
+
|
|
1413
1503
|
const Config = z.object({
|
|
1414
1504
|
provider: z.string().default("bing"),
|
|
1415
1505
|
cache: z.boolean().default(true), // 单 query 结果缓存开关(防限流/省额度)
|
|
1416
1506
|
cacheTtl: z.number().default(5), // 缓存时长(分钟),0-5 可配置(使用处再 clamp)
|
|
1507
|
+
keyStorage: z.string().default("credentials"), // key 存储位置:credentials(凭据中心)| settings(设置页)
|
|
1417
1508
|
lang: z.string().default("zh"),
|
|
1418
1509
|
region: z.string(),
|
|
1419
1510
|
bingMarket: z.string().default("zh-CN"),
|
|
@@ -1430,7 +1521,8 @@ const Config = z.object({
|
|
|
1430
1521
|
function apply(ctx, config) {
|
|
1431
1522
|
let current = () => config ?? {};
|
|
1432
1523
|
const logger = ctx.logger;
|
|
1433
|
-
|
|
1524
|
+
// credentials 服务可能晚于本插件挂载(跨 bundle 顺序),运行期动态获取而不是 apply 时缓存
|
|
1525
|
+
const getCredentials = () => ctx.get("credentials");
|
|
1434
1526
|
|
|
1435
1527
|
// 系统提示词动态刷新:设置变更时重新生成,避免显示旧引擎
|
|
1436
1528
|
let refreshPrompt = null;
|
|
@@ -1438,16 +1530,17 @@ function apply(ctx, config) {
|
|
|
1438
1530
|
// 单 query 结果缓存(provider.search 内闭包持有):LRU 50 条 / TTL 可配置
|
|
1439
1531
|
const searchCache = new Map(); // key -> { value, expiresAt }
|
|
1440
1532
|
|
|
1441
|
-
// key 优先级:settings 的 free-search.<x>ApiKey
|
|
1533
|
+
// key 优先级:credentials(.credentials.yaml 凭据中心,官方推荐)> settings 的 free-search.<x>ApiKey(遗留兼容)> 环境变量
|
|
1442
1534
|
const resolveApiKey = async (envName, settingsKey) => {
|
|
1443
|
-
const
|
|
1444
|
-
if (settingsKey && cfg[settingsKey]) return cfg[settingsKey];
|
|
1535
|
+
const credentials = getCredentials();
|
|
1445
1536
|
if (credentials) {
|
|
1446
1537
|
try {
|
|
1447
1538
|
const resolved = await credentials.resolve(envName);
|
|
1448
1539
|
if (resolved?.value) return resolved.value;
|
|
1449
1540
|
} catch {}
|
|
1450
1541
|
}
|
|
1542
|
+
const cfg = current();
|
|
1543
|
+
if (settingsKey && cfg[settingsKey]) return cfg[settingsKey];
|
|
1451
1544
|
return process.env[envName] ?? "";
|
|
1452
1545
|
};
|
|
1453
1546
|
|
|
@@ -1655,7 +1748,8 @@ function apply(ctx, config) {
|
|
|
1655
1748
|
const disposers = makeBridgeRoutes(
|
|
1656
1749
|
sctx.settings,
|
|
1657
1750
|
(request) => provider.search(request, undefined),
|
|
1658
|
-
(engine, query, timeRange) => runEngineTest(engine, query, timeRange)
|
|
1751
|
+
(engine, query, timeRange) => runEngineTest(engine, query, timeRange),
|
|
1752
|
+
getCredentials
|
|
1659
1753
|
).map((route) => sctx.webServer.register(route));
|
|
1660
1754
|
return () => {
|
|
1661
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",
|
|
@@ -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
|
}
|