dsh-free-search 0.4.13 → 0.4.14
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 +2 -2
- package/lib/client.js +136 -7
- package/lib/index.js +97 -4
- package/package.json +1 -1
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.4.
|
|
39
|
+
- **版本号 + 检查更新** —— 设置卡片显示当前版本(v0.4.14),"检查更新"按钮直连 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,零额外依赖)
|
|
@@ -277,7 +277,7 @@ This plugin provides multiple free search engines with automatic fallback, compl
|
|
|
277
277
|
- **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.
|
|
278
278
|
- **Time Filtering** — The `advanced_search` tool supports `timeRange`: fixed tiers, custom relative values, or an absolute date (details below)
|
|
279
279
|
- **System Prompt Injection** — The agent is aware of the currently active engine and which engines require API keys
|
|
280
|
-
- **Version + Update Check** — The settings card shows the current version (v0.4.
|
|
280
|
+
- **Version + Update Check** — The settings card shows the current version (v0.4.14), 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
|
|
281
281
|
- **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)
|
|
282
282
|
- **Visual Badges** — Free engines feature a green `FREE` badge, while paid engines show an orange `API KEY` badge in the settings UI
|
|
283
283
|
- **Webpage Fetching (`web_fetch`)** — Allows the agent to read full webpage contents (official `dsh-web-fetch-http` provider, pure JS, zero extra dependencies)
|
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,6 +91,11 @@ 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
|
keysHint: "密钥读取优先级:.credentials.yaml 凭据中心 > 这里 > 环境变量。推荐把 key 写进凭据中心(与官方 LLM 一致,一处管理)。",
|
|
94
|
+
keyStorage: "Key 存储位置",
|
|
95
|
+
keyStorageCred: "凭据中心(推荐)",
|
|
96
|
+
keyStorageSettings: "设置页(兼容)",
|
|
97
|
+
keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["exa", "tavily", "keenable", "perplexity", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
|
|
98
|
+
keyStorageSettingsHint: "保存后写入 settings.yaml(向后兼容路径,优先级低于凭据中心)。",
|
|
87
99
|
platformSearch: "平台搜索(platform_search 工具)",
|
|
88
100
|
platformHint: "为 agent 的 platform_search 工具启用平台。禁用的平台会被跳过。",
|
|
89
101
|
cacheTtl: "结果缓存时长(分钟)",
|
|
@@ -125,6 +137,11 @@ window.__ModuleLoader__.load({
|
|
|
125
137
|
perplexityPh: (c) => c ? "Perplexity API key (configured)" : "Perplexity API key (pplx-...)",
|
|
126
138
|
deepseekPh: (c) => c ? "DeepSeek API key (configured)" : "DeepSeek API key (sk-...)",
|
|
127
139
|
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).",
|
|
140
|
+
keyStorage: "Key storage",
|
|
141
|
+
keyStorageCred: "Credential center (recommended)",
|
|
142
|
+
keyStorageSettings: "Settings page (legacy)",
|
|
143
|
+
keyStorageCredHint: (c) => `Saved to ~/.dsh/.credentials.yaml (highest priority). Currently configured: ${["exa", "tavily", "keenable", "perplexity", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "none"}`,
|
|
144
|
+
keyStorageSettingsHint: "Saved to settings.yaml (backward-compatible path; lower priority than the credential center).",
|
|
128
145
|
platformSearch: "Platform search (platform_search tool)",
|
|
129
146
|
platformHint: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
|
|
130
147
|
cacheTtl: "Result cache TTL (minutes)",
|
|
@@ -155,7 +172,7 @@ window.__ModuleLoader__.load({
|
|
|
155
172
|
};
|
|
156
173
|
const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
|
|
157
174
|
// 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
|
|
158
|
-
const PLUGIN_VERSION = "0.4.
|
|
175
|
+
const PLUGIN_VERSION = "0.4.14";
|
|
159
176
|
const ENGINES = [
|
|
160
177
|
{ id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
|
|
161
178
|
{ id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
|
|
@@ -205,7 +222,54 @@ window.__ModuleLoader__.load({
|
|
|
205
222
|
return response.json();
|
|
206
223
|
}
|
|
207
224
|
|
|
225
|
+
async function bridgeCredentialsStatus() {
|
|
226
|
+
const response = await fetch(`${BRIDGE_PREFIX}/credentials-status`, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: { "content-type": "application/json" },
|
|
229
|
+
body: "{}",
|
|
230
|
+
});
|
|
231
|
+
return response.json();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function bridgeCredentialsSet(key, value) {
|
|
235
|
+
const response = await fetch(`${BRIDGE_PREFIX}/credentials-set`, {
|
|
236
|
+
method: "POST",
|
|
237
|
+
headers: { "content-type": "application/json" },
|
|
238
|
+
body: JSON.stringify({ key, value }),
|
|
239
|
+
});
|
|
240
|
+
return response.json();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function bridgeCredentialsUnset(key) {
|
|
244
|
+
const response = await fetch(`${BRIDGE_PREFIX}/credentials-unset`, {
|
|
245
|
+
method: "POST",
|
|
246
|
+
headers: { "content-type": "application/json" },
|
|
247
|
+
body: JSON.stringify({ key }),
|
|
248
|
+
});
|
|
249
|
+
return response.json();
|
|
250
|
+
}
|
|
251
|
+
|
|
208
252
|
function FreeSearchCard(props) {
|
|
253
|
+
// 检测应用主题深浅(读 body 的 --dsw-alias-bg-base 变量亮度),用于锁定原生下拉配色
|
|
254
|
+
const isDarkScheme = react.useMemo(() => {
|
|
255
|
+
try {
|
|
256
|
+
const root = document.body || document.documentElement;
|
|
257
|
+
const bg = getComputedStyle(root).getPropertyValue("--dsw-alias-bg-base").trim();
|
|
258
|
+
const m = bg.match(/(\d+)\s*[, ]\s*(\d+)\s*[, ]\s*(\d+)/);
|
|
259
|
+
if (m) {
|
|
260
|
+
const l = 0.299 * Number(m[1]) + 0.587 * Number(m[2]) + 0.114 * Number(m[3]);
|
|
261
|
+
return l < 128;
|
|
262
|
+
}
|
|
263
|
+
if (/^#([0-9a-f]{3,8})/i.test(bg)) {
|
|
264
|
+
const hex = bg.slice(1);
|
|
265
|
+
const h = hex.length <= 4 ? hex.replace(/./g, (c) => c + c) : hex;
|
|
266
|
+
const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
|
|
267
|
+
return 0.299 * r + 0.587 * g + 0.114 * b < 128;
|
|
268
|
+
}
|
|
269
|
+
} catch {}
|
|
270
|
+
return typeof matchMedia === "function" ? matchMedia("(prefers-color-scheme: dark)").matches : false;
|
|
271
|
+
}, []);
|
|
272
|
+
const selectColorScheme = isDarkScheme ? "dark" : "light";
|
|
209
273
|
const [open, setOpen] = react.useState(false);
|
|
210
274
|
const [state, setState] = react.useState({ status: "loading" });
|
|
211
275
|
const [provider, setProvider] = react.useState("bing");
|
|
@@ -217,6 +281,10 @@ window.__ModuleLoader__.load({
|
|
|
217
281
|
const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
|
|
218
282
|
const [cacheTtl, setCacheTtl] = react.useState(5);
|
|
219
283
|
const [keysConfigured, setKeysConfigured] = react.useState({});
|
|
284
|
+
// key 存储位置:credentials(凭据中心,默认)| settings(设置页,兼容旧行为)
|
|
285
|
+
const [keyStorage, setKeyStorage] = react.useState("credentials");
|
|
286
|
+
// 凭据中心里已配置的 key(describe 不返回,需单独查)
|
|
287
|
+
const [credConfigured, setCredConfigured] = react.useState({});
|
|
220
288
|
const [lang, setLang] = react.useState("zh");
|
|
221
289
|
const [dirty, setDirty] = react.useState(false);
|
|
222
290
|
const [saving, setSaving] = react.useState(false);
|
|
@@ -256,7 +324,21 @@ window.__ModuleLoader__.load({
|
|
|
256
324
|
}
|
|
257
325
|
}
|
|
258
326
|
setKeysConfigured(configured);
|
|
327
|
+
// key 存储位置(默认凭据中心)
|
|
328
|
+
setKeyStorage(v.keyStorage === "settings" ? "settings" : "credentials");
|
|
259
329
|
setState({ status: "ready", writable: result.value.writable });
|
|
330
|
+
// 查询凭据中心里各 key 的配置状态
|
|
331
|
+
try {
|
|
332
|
+
const cred = await bridgeCredentialsStatus();
|
|
333
|
+
if (cred.ok) {
|
|
334
|
+
const cc = {};
|
|
335
|
+
const map = { exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek" };
|
|
336
|
+
for (const [k, v] of Object.entries(cred.value.configured ?? {})) {
|
|
337
|
+
if (map[k]) cc[map[k]] = v;
|
|
338
|
+
}
|
|
339
|
+
setCredConfigured(cc);
|
|
340
|
+
}
|
|
341
|
+
} catch {}
|
|
260
342
|
} else {
|
|
261
343
|
setState({ status: "unavailable" });
|
|
262
344
|
}
|
|
@@ -282,17 +364,35 @@ window.__ModuleLoader__.load({
|
|
|
282
364
|
setSaving(true);
|
|
283
365
|
setFailed(false);
|
|
284
366
|
try {
|
|
367
|
+
// key 存储分流:凭据中心(默认)走 credentials-set;设置页走 settings mutate(兼容)
|
|
368
|
+
const keyFields = [
|
|
369
|
+
["exaApiKey", exaKey],
|
|
370
|
+
["tavilyApiKey", tavilyKey],
|
|
371
|
+
["keenableApiKey", keenableKey],
|
|
372
|
+
["perplexityApiKey", perplexityKey],
|
|
373
|
+
["deepseekApiKey", deepseekKey],
|
|
374
|
+
];
|
|
375
|
+
let credFailed = false;
|
|
376
|
+
if (keyStorage === "credentials") {
|
|
377
|
+
for (const [field, value] of keyFields) {
|
|
378
|
+
if (!value.trim()) continue;
|
|
379
|
+
const r = await bridgeCredentialsSet(field, value.trim());
|
|
380
|
+
if (!r.ok) credFailed = true;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
285
383
|
const ops = [{ op: "set", path: ["provider"], value: provider }];
|
|
286
384
|
ops.push({ op: "set", path: ["lang"], value: lang });
|
|
287
|
-
|
|
288
|
-
if (
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
385
|
+
ops.push({ op: "set", path: ["keyStorage"], value: keyStorage });
|
|
386
|
+
if (keyStorage !== "credentials") {
|
|
387
|
+
// settings 模式:key 仍写 settings.yaml(旧行为)
|
|
388
|
+
for (const [field, value] of keyFields) {
|
|
389
|
+
if (value.trim()) ops.push({ op: "set", path: [field], value: value.trim() });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
292
392
|
ops.push({ op: "set", path: ["platforms"], value: platforms });
|
|
293
393
|
ops.push({ op: "set", path: ["cacheTtl"], value: Math.min(Math.max(Number(cacheTtl) ?? 5, 0), 5) });
|
|
294
394
|
const result = await bridgeMutate({ ns: NS, ops });
|
|
295
|
-
if (result.ok) {
|
|
395
|
+
if (result.ok && !credFailed) {
|
|
296
396
|
setDirty(false);
|
|
297
397
|
setProvider(result.value.value.provider ?? provider);
|
|
298
398
|
setFailed(false);
|
|
@@ -445,6 +545,7 @@ react_jsx_runtime.jsx("div", {
|
|
|
445
545
|
react_jsx_runtime.jsx("select", {
|
|
446
546
|
className: "dshfs-select",
|
|
447
547
|
value: provider,
|
|
548
|
+
style: { colorScheme: selectColorScheme },
|
|
448
549
|
disabled: !ready || saving,
|
|
449
550
|
onChange: (e) => select(e.target.value),
|
|
450
551
|
children: ENGINES.map((engine) =>
|
|
@@ -540,6 +641,34 @@ react_jsx_runtime.jsx("div", {
|
|
|
540
641
|
className: "dshfs-hint",
|
|
541
642
|
children: t.keysHint,
|
|
542
643
|
}),
|
|
644
|
+
react_jsx_runtime.jsx("div", {
|
|
645
|
+
className: "dshfs-fieldRow",
|
|
646
|
+
children: [
|
|
647
|
+
react_jsx_runtime.jsx("label", {
|
|
648
|
+
className: "dshfs-label",
|
|
649
|
+
children: t.keyStorage,
|
|
650
|
+
}),
|
|
651
|
+
react_jsx_runtime.jsx("select", {
|
|
652
|
+
className: "dshfs-select dshfs-keyStorage",
|
|
653
|
+
value: keyStorage,
|
|
654
|
+
style: { colorScheme: selectColorScheme },
|
|
655
|
+
disabled: !ready || saving,
|
|
656
|
+
onChange: (e) => {
|
|
657
|
+
setKeyStorage(e.target.value);
|
|
658
|
+
setDirty(true);
|
|
659
|
+
setFailed(false);
|
|
660
|
+
},
|
|
661
|
+
children: [
|
|
662
|
+
react_jsx_runtime.jsx("option", { value: "credentials", children: t.keyStorageCred }),
|
|
663
|
+
react_jsx_runtime.jsx("option", { value: "settings", children: t.keyStorageSettings }),
|
|
664
|
+
],
|
|
665
|
+
}),
|
|
666
|
+
],
|
|
667
|
+
}),
|
|
668
|
+
react_jsx_runtime.jsx("p", {
|
|
669
|
+
className: "dshfs-hint",
|
|
670
|
+
children: keyStorage === "credentials" ? t.keyStorageCredHint(credConfigured) : t.keyStorageSettingsHint,
|
|
671
|
+
}),
|
|
543
672
|
],
|
|
544
673
|
}),
|
|
545
674
|
react_jsx_runtime.jsx("div", {
|
package/lib/index.js
CHANGED
|
@@ -21,7 +21,7 @@ const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch"];
|
|
|
21
21
|
const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "perplexity", "deepseek-official"];
|
|
22
22
|
|
|
23
23
|
// 当前插件版本(发布时与 package.json 同步)
|
|
24
|
-
const PLUGIN_VERSION = "0.4.
|
|
24
|
+
const PLUGIN_VERSION = "0.4.14";
|
|
25
25
|
// 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
|
|
26
26
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
|
|
27
27
|
const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
|
|
@@ -1095,7 +1095,7 @@ function toView(descriptor) {
|
|
|
1095
1095
|
};
|
|
1096
1096
|
}
|
|
1097
1097
|
|
|
1098
|
-
function makeBridgeRoutes(settings, search, testEngine) {
|
|
1098
|
+
function makeBridgeRoutes(settings, search, testEngine, getCredentials) {
|
|
1099
1099
|
const allowlisted = () =>
|
|
1100
1100
|
settings
|
|
1101
1101
|
.describe({ redactSecrets: true })
|
|
@@ -1248,6 +1248,52 @@ function makeBridgeRoutes(settings, search, testEngine) {
|
|
|
1248
1248
|
}
|
|
1249
1249
|
return { ok: true, value: toView(descriptor) };
|
|
1250
1250
|
},
|
|
1251
|
+
// 凭据中心:查询各引擎 key 的配置状态(value 不返回,只返回是否已配置)
|
|
1252
|
+
async credentialsStatus() {
|
|
1253
|
+
const credentials = getCredentials();
|
|
1254
|
+
if (!credentials) return { ok: false, code: "credentials-unavailable", message: "credentials service is not available" };
|
|
1255
|
+
const configured = {};
|
|
1256
|
+
for (const [settingsKey, ref] of Object.entries(KEY_REF_MAP)) {
|
|
1257
|
+
try {
|
|
1258
|
+
const info = await credentials.describe(ref);
|
|
1259
|
+
configured[settingsKey] = info !== undefined && info.configured === true;
|
|
1260
|
+
} catch {
|
|
1261
|
+
configured[settingsKey] = false;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return { ok: true, value: { configured, available: true } };
|
|
1265
|
+
},
|
|
1266
|
+
// 凭据中心:写入一个引擎 key(ref 白名单限定)
|
|
1267
|
+
async credentialsSet(request) {
|
|
1268
|
+
const credentials = getCredentials();
|
|
1269
|
+
if (!credentials) return { ok: false, code: "credentials-unavailable", message: "credentials service is not available" };
|
|
1270
|
+
const { key, value } = request ?? {};
|
|
1271
|
+
const ref = KEY_REF_MAP[key];
|
|
1272
|
+
if (!ref) return { ok: false, code: "credentials-rejected", message: `unknown credential key "${key}"` };
|
|
1273
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1274
|
+
return { ok: false, code: "credentials-rejected", message: "value is required" };
|
|
1275
|
+
}
|
|
1276
|
+
try {
|
|
1277
|
+
await credentials.set(ref, value.trim());
|
|
1278
|
+
return { ok: true, value: { ref, set: true } };
|
|
1279
|
+
} catch (error) {
|
|
1280
|
+
return { ok: false, code: "credentials-write-failed", message: error instanceof Error ? error.message : String(error) };
|
|
1281
|
+
}
|
|
1282
|
+
},
|
|
1283
|
+
// 凭据中心:删除一个引擎 key
|
|
1284
|
+
async credentialsUnset(request) {
|
|
1285
|
+
const credentials = getCredentials();
|
|
1286
|
+
if (!credentials) return { ok: false, code: "credentials-unavailable", message: "credentials service is not available" };
|
|
1287
|
+
const { key } = request ?? {};
|
|
1288
|
+
const ref = KEY_REF_MAP[key];
|
|
1289
|
+
if (!ref) return { ok: false, code: "credentials-rejected", message: `unknown credential key "${key}"` };
|
|
1290
|
+
try {
|
|
1291
|
+
await credentials.unset(ref);
|
|
1292
|
+
return { ok: true, value: { ref, set: false } };
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
return { ok: false, code: "credentials-write-failed", message: error instanceof Error ? error.message : String(error) };
|
|
1295
|
+
}
|
|
1296
|
+
},
|
|
1251
1297
|
};
|
|
1252
1298
|
|
|
1253
1299
|
const guard = (req, res) => {
|
|
@@ -1284,6 +1330,40 @@ function makeBridgeRoutes(settings, search, testEngine) {
|
|
|
1284
1330
|
writeJson(res, 200, await handlers.mutate(body));
|
|
1285
1331
|
},
|
|
1286
1332
|
},
|
|
1333
|
+
{
|
|
1334
|
+
kind: "exact",
|
|
1335
|
+
path: `${BRIDGE_PREFIX}/credentials-status`,
|
|
1336
|
+
handler: async (req, res) => {
|
|
1337
|
+
if (!guard(req, res)) return;
|
|
1338
|
+
writeJson(res, 200, await handlers.credentialsStatus());
|
|
1339
|
+
},
|
|
1340
|
+
},
|
|
1341
|
+
{
|
|
1342
|
+
kind: "exact",
|
|
1343
|
+
path: `${BRIDGE_PREFIX}/credentials-set`,
|
|
1344
|
+
handler: async (req, res) => {
|
|
1345
|
+
if (!guard(req, res)) return;
|
|
1346
|
+
const body = await readJsonBody(req);
|
|
1347
|
+
if (body === undefined) {
|
|
1348
|
+
writeJson(res, 400, { ok: false, code: "credentials-rejected", message: "malformed JSON body" });
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
writeJson(res, 200, await handlers.credentialsSet(body));
|
|
1352
|
+
},
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
kind: "exact",
|
|
1356
|
+
path: `${BRIDGE_PREFIX}/credentials-unset`,
|
|
1357
|
+
handler: async (req, res) => {
|
|
1358
|
+
if (!guard(req, res)) return;
|
|
1359
|
+
const body = await readJsonBody(req);
|
|
1360
|
+
if (body === undefined) {
|
|
1361
|
+
writeJson(res, 400, { ok: false, code: "credentials-rejected", message: "malformed JSON body" });
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
writeJson(res, 200, await handlers.credentialsUnset(body));
|
|
1365
|
+
},
|
|
1366
|
+
},
|
|
1287
1367
|
{
|
|
1288
1368
|
kind: "exact",
|
|
1289
1369
|
path: `${BRIDGE_PREFIX}/check-update`,
|
|
@@ -1320,10 +1400,20 @@ function makeBridgeRoutes(settings, search, testEngine) {
|
|
|
1320
1400
|
const name = "web-search-free";
|
|
1321
1401
|
const inject = ["web"];
|
|
1322
1402
|
|
|
1403
|
+
// 引擎 key 与凭据中心 ref 的映射(白名单:只允许这些 ref 被 UI 读写凭据中心)
|
|
1404
|
+
const KEY_REF_MAP = {
|
|
1405
|
+
exaApiKey: "EXA_API_KEY",
|
|
1406
|
+
tavilyApiKey: "TAVILY_API_KEY",
|
|
1407
|
+
keenableApiKey: "KEENABLE_API_KEY",
|
|
1408
|
+
perplexityApiKey: "PERPLEXITY_API_KEY",
|
|
1409
|
+
deepseekApiKey: "DEEPSEEK_API_KEY",
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1323
1412
|
const Config = z.object({
|
|
1324
1413
|
provider: z.string().default("bing"),
|
|
1325
1414
|
cache: z.boolean().default(true), // 单 query 结果缓存开关(防限流/省额度)
|
|
1326
1415
|
cacheTtl: z.number().default(5), // 缓存时长(分钟),0-5 可配置(使用处再 clamp)
|
|
1416
|
+
keyStorage: z.string().default("credentials"), // key 存储位置:credentials(凭据中心)| settings(设置页)
|
|
1327
1417
|
lang: z.string().default("zh"),
|
|
1328
1418
|
region: z.string(),
|
|
1329
1419
|
bingMarket: z.string().default("zh-CN"),
|
|
@@ -1339,7 +1429,8 @@ const Config = z.object({
|
|
|
1339
1429
|
function apply(ctx, config) {
|
|
1340
1430
|
let current = () => config ?? {};
|
|
1341
1431
|
const logger = ctx.logger;
|
|
1342
|
-
|
|
1432
|
+
// credentials 服务可能晚于本插件挂载(跨 bundle 顺序),运行期动态获取而不是 apply 时缓存
|
|
1433
|
+
const getCredentials = () => ctx.get("credentials");
|
|
1343
1434
|
|
|
1344
1435
|
// 系统提示词动态刷新:设置变更时重新生成,避免显示旧引擎
|
|
1345
1436
|
let refreshPrompt = null;
|
|
@@ -1349,6 +1440,7 @@ function apply(ctx, config) {
|
|
|
1349
1440
|
|
|
1350
1441
|
// key 优先级:credentials(.credentials.yaml 凭据中心,官方推荐)> settings 的 free-search.<x>ApiKey(遗留兼容)> 环境变量
|
|
1351
1442
|
const resolveApiKey = async (envName, settingsKey) => {
|
|
1443
|
+
const credentials = getCredentials();
|
|
1352
1444
|
if (credentials) {
|
|
1353
1445
|
try {
|
|
1354
1446
|
const resolved = await credentials.resolve(envName);
|
|
@@ -1555,7 +1647,8 @@ function apply(ctx, config) {
|
|
|
1555
1647
|
const disposers = makeBridgeRoutes(
|
|
1556
1648
|
sctx.settings,
|
|
1557
1649
|
(request) => provider.search(request, undefined),
|
|
1558
|
-
(engine, query, timeRange) => runEngineTest(engine, query, timeRange)
|
|
1650
|
+
(engine, query, timeRange) => runEngineTest(engine, query, timeRange),
|
|
1651
|
+
getCredentials
|
|
1559
1652
|
).map((route) => sctx.webServer.register(route));
|
|
1560
1653
|
return () => {
|
|
1561
1654
|
for (const dispose of disposers) dispose();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-free-search",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.14",
|
|
4
4
|
"description": "Free web search for DeepSeek Harness: 10 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable keyless) + time filtering + platform search + web_fetch, with web settings UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|