dsh-free-search 0.4.4 → 0.4.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.en.md +3 -2
- package/README.md +4 -3
- package/lib/client.js +44 -0
- package/lib/index.js +56 -17
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -49,7 +49,7 @@ This plugin provides multiple free search engines with automatic fallback, compl
|
|
|
49
49
|
| `deepseek-official` | DeepSeek Official | Paid | Requires `DEEPSEEK_API_KEY` |
|
|
50
50
|
|
|
51
51
|
- **Default engine is `bing`** (free and most stable), ready to use out of the box after installation.
|
|
52
|
-
-
|
|
52
|
+
- **Auto-failover**: any engine failure (rate-limited free engine, or missing/invalid paid key, network error) automatically falls back to a working free engine (Bing/AnySearch etc.) with a note attached to the results — a search never fails outright because of engine issues.
|
|
53
53
|
- **Official Links in Settings**: Free engines display "Visit Website →", while paid engines display "Get API Key →" (opens in a new tab):
|
|
54
54
|
- Exa: <https://dashboard.exa.ai/api-keys>
|
|
55
55
|
- Perplexity: <https://www.perplexity.ai/settings/api>
|
|
@@ -86,7 +86,8 @@ After installation, navigate to **Settings → Plugins → Configurable** tab
|
|
|
86
86
|
|
|
87
87
|
- **Search engine**: Select an engine from the dropdown; changes take effect immediately upon saving.
|
|
88
88
|
- **API keys**: Enter keys for Exa / Perplexity / DeepSeek (password fields; displayed as "configured" once saved).
|
|
89
|
-
- **Use Bing default**: stage a switch back to the stable free Bing engine; `Discard` only cancels unsaved edits
|
|
89
|
+
- **Use Bing default**: stage a switch back to the stable free Bing engine; `Discard` only cancels unsaved edits
|
|
90
|
+
- **Platform search**: check GitHub / V2EX / Bilibili to enable them for the `platform_search` tool (disabled platforms are skipped).
|
|
90
91
|
|
|
91
92
|
<table align="center" style="border: none; border-collapse: collapse;">
|
|
92
93
|
<tr style="border: none;">
|
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
49
49
|
| `deepseek-official` | DeepSeek 官方 | 付费 | 需 `DEEPSEEK_API_KEY` |
|
|
50
50
|
|
|
51
51
|
- **默认引擎为 `bing`**(免费且最稳定),安装后开箱即用。
|
|
52
|
-
-
|
|
52
|
+
- **自动回退**:任何引擎失败(免费引擎限流/反爬,付费引擎缺 key/无效/网络错误)都会自动回退到可用的免费引擎(Bing/AnySearch 等),并在结果中附带回退提示——搜索不会因引擎问题直接失败。
|
|
53
53
|
- **设置页有官网链接**:免费引擎显示"访问官网 →",付费引擎显示"获取 API Key →"(新标签页打开):
|
|
54
54
|
- Exa:<https://dashboard.exa.ai/api-keys>
|
|
55
55
|
- Perplexity:<https://www.perplexity.ai/settings/api>
|
|
@@ -85,8 +85,9 @@ dsh web
|
|
|
85
85
|
安装后,打开 **设置 → 插件 → 可配置** 标签页 → **Free Search** 卡片(官方设置页):
|
|
86
86
|
|
|
87
87
|
- **Search engine**:下拉框切换引擎,保存即生效
|
|
88
|
-
- **API keys**:为 Exa / Perplexity / DeepSeek 填写 key(密码框,保存后只显示"已配置")
|
|
89
|
-
- **Use Bing default**:把当前搜索引擎切回稳定的免费 Bing;`Discard` 只撤销尚未保存的编辑
|
|
88
|
+
- **API keys**:为 Exa / Perplexity / DeepSeek 填写 key(密码框,保存后只显示"已配置")
|
|
89
|
+
- **Use Bing default**:把当前搜索引擎切回稳定的免费 Bing;`Discard` 只撤销尚未保存的编辑
|
|
90
|
+
- **Platform search**:勾选启用 GitHub / V2EX / Bilibili 平台搜索(`platform_search` 工具按此过滤)
|
|
90
91
|
|
|
91
92
|
<table align="center" style="border: none; border-collapse: collapse;">
|
|
92
93
|
<tr style="border: none;">
|
package/lib/client.js
CHANGED
|
@@ -31,6 +31,9 @@ window.__ModuleLoader__.load({
|
|
|
31
31
|
".dshfs-input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
32
32
|
".dshfs-input:disabled{opacity:.6;cursor:default}",
|
|
33
33
|
".dshfs-hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}",
|
|
34
|
+
".dshfs-platforms{display:flex;gap:10px;flex-wrap:wrap}",
|
|
35
|
+
".dshfs-platform{display:flex;align-items:center;gap:5px;color:var(--dsw-alias-label-primary);font-size:13px;cursor:pointer}",
|
|
36
|
+
".dshfs-platform input{accent-color:var(--dsw-alias-state-business-primary)}",
|
|
34
37
|
".dshfs-link{color:var(--dsw-alias-state-business-primary);font-size:12px;text-decoration:none;align-self:flex-start;padding:2px 0}",
|
|
35
38
|
".dshfs-link:hover{text-decoration:underline}",
|
|
36
39
|
".dshfs-btn{font:inherit;cursor:pointer;border-radius:6px;padding:5px 12px;font-size:13px;transition:background-color .13s,border-color .13s,color .13s}",
|
|
@@ -89,6 +92,7 @@ window.__ModuleLoader__.load({
|
|
|
89
92
|
const [exaKey, setExaKey] = react.useState("");
|
|
90
93
|
const [perplexityKey, setPerplexityKey] = react.useState("");
|
|
91
94
|
const [deepseekKey, setDeepseekKey] = react.useState("");
|
|
95
|
+
const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili"]);
|
|
92
96
|
const [keysConfigured, setKeysConfigured] = react.useState({});
|
|
93
97
|
const [dirty, setDirty] = react.useState(false);
|
|
94
98
|
const [saving, setSaving] = react.useState(false);
|
|
@@ -105,6 +109,7 @@ window.__ModuleLoader__.load({
|
|
|
105
109
|
setExaKey(v.exaApiKey ?? "");
|
|
106
110
|
setPerplexityKey(v.perplexityApiKey ?? "");
|
|
107
111
|
setDeepseekKey(v.deepseekApiKey ?? "");
|
|
112
|
+
setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili"]);
|
|
108
113
|
// secrets 字段标记哪些 key 已配置(值被脱敏,仅显示"已配置")
|
|
109
114
|
const configured = {};
|
|
110
115
|
for (const secret of view.secrets ?? []) {
|
|
@@ -146,6 +151,7 @@ window.__ModuleLoader__.load({
|
|
|
146
151
|
if (exaKey.trim()) ops.push({ op: "set", path: ["exaApiKey"], value: exaKey.trim() });
|
|
147
152
|
if (perplexityKey.trim()) ops.push({ op: "set", path: ["perplexityApiKey"], value: perplexityKey.trim() });
|
|
148
153
|
if (deepseekKey.trim()) ops.push({ op: "set", path: ["deepseekApiKey"], value: deepseekKey.trim() });
|
|
154
|
+
ops.push({ op: "set", path: ["platforms"], value: platforms });
|
|
149
155
|
const result = await bridgeMutate({ ns: NS, ops });
|
|
150
156
|
if (result.ok) {
|
|
151
157
|
setDirty(false);
|
|
@@ -291,6 +297,44 @@ window.__ModuleLoader__.load({
|
|
|
291
297
|
}),
|
|
292
298
|
],
|
|
293
299
|
}),
|
|
300
|
+
react_jsx_runtime.jsx("div", {
|
|
301
|
+
className: "dshfs-field",
|
|
302
|
+
children: [
|
|
303
|
+
react_jsx_runtime.jsx("div", {
|
|
304
|
+
className: "dshfs-label",
|
|
305
|
+
children: "Platform search (platform_search tool)",
|
|
306
|
+
}),
|
|
307
|
+
react_jsx_runtime.jsx("div", {
|
|
308
|
+
className: "dshfs-platforms",
|
|
309
|
+
children: [
|
|
310
|
+
["github", "GitHub"], ["v2ex", "V2EX"], ["bilibili", "Bilibili"],
|
|
311
|
+
].map(([id, label]) =>
|
|
312
|
+
react_jsx_runtime.jsx("label", {
|
|
313
|
+
className: "dshfs-platform",
|
|
314
|
+
children: [
|
|
315
|
+
react_jsx_runtime.jsx("input", {
|
|
316
|
+
type: "checkbox",
|
|
317
|
+
checked: platforms.includes(id),
|
|
318
|
+
disabled: !ready || saving,
|
|
319
|
+
onChange: (e) => {
|
|
320
|
+
setPlatforms((prev) =>
|
|
321
|
+
e.target.checked ? [...prev, id] : prev.filter((p) => p !== id)
|
|
322
|
+
);
|
|
323
|
+
setDirty(true);
|
|
324
|
+
setFailed(false);
|
|
325
|
+
},
|
|
326
|
+
}),
|
|
327
|
+
label,
|
|
328
|
+
],
|
|
329
|
+
}, id)
|
|
330
|
+
),
|
|
331
|
+
}),
|
|
332
|
+
react_jsx_runtime.jsx("p", {
|
|
333
|
+
className: "dshfs-hint",
|
|
334
|
+
children: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
|
|
335
|
+
}),
|
|
336
|
+
],
|
|
337
|
+
}),
|
|
294
338
|
!ready
|
|
295
339
|
? react_jsx_runtime.jsx("p", {
|
|
296
340
|
className: "dshfs-hint",
|
package/lib/index.js
CHANGED
|
@@ -742,6 +742,7 @@ const Config = z.object({
|
|
|
742
742
|
region: z.string(),
|
|
743
743
|
bingMarket: z.string().default("zh-CN"),
|
|
744
744
|
searxngInstances: z.array(z.string()),
|
|
745
|
+
platforms: z.array(z.string()).default(["github", "v2ex", "bilibili"]),
|
|
745
746
|
exaApiKey: z.string().role("secret"),
|
|
746
747
|
perplexityApiKey: z.string().role("secret"),
|
|
747
748
|
deepseekApiKey: z.string().role("secret"),
|
|
@@ -766,7 +767,8 @@ function apply(ctx, config) {
|
|
|
766
767
|
};
|
|
767
768
|
|
|
768
769
|
// 总控 provider:按 settings 的 provider 字段路由到任意引擎。
|
|
769
|
-
//
|
|
770
|
+
// 任何引擎失败(缺 key / 401 / 限流 / 网络)都会自动回退到可用的免费引擎,
|
|
771
|
+
// 并在结果里附带回退提示,避免 agent 搜索直接失败。
|
|
770
772
|
const provider = {
|
|
771
773
|
id: "ddg",
|
|
772
774
|
available() {
|
|
@@ -776,22 +778,46 @@ function apply(ctx, config) {
|
|
|
776
778
|
const cfg = current();
|
|
777
779
|
const preferred = cfg.provider ?? "bing";
|
|
778
780
|
|
|
779
|
-
//
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
781
|
+
// 付费引擎:先尝试;失败(缺 key / 401 / 网络)则回退免费引擎链
|
|
782
|
+
let fallbackReason = null;
|
|
783
|
+
if (preferred === "exa" || preferred === "perplexity" || preferred === "deepseek-official") {
|
|
784
|
+
try {
|
|
785
|
+
if (preferred === "exa") {
|
|
786
|
+
const key = await resolveApiKey("EXA_API_KEY", "exaApiKey");
|
|
787
|
+
if (key) {
|
|
788
|
+
const result = await searchExa(request.query, request.maxResults, key, signal);
|
|
789
|
+
if (result.sources.length > 0) return result;
|
|
790
|
+
fallbackReason = "Exa returned no results";
|
|
791
|
+
} else {
|
|
792
|
+
// exa 无 key 时走 keyless MCP(免费)
|
|
793
|
+
try {
|
|
794
|
+
const result = await searchExaMCP(request.query, request.maxResults, signal);
|
|
795
|
+
if (result.sources.length > 0) return result;
|
|
796
|
+
fallbackReason = "Exa keyless MCP returned no results";
|
|
797
|
+
} catch (mcpError) {
|
|
798
|
+
fallbackReason = mcpError instanceof Error ? mcpError.message : String(mcpError);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
} else if (preferred === "perplexity") {
|
|
802
|
+
const key = await resolveApiKey("PERPLEXITY_API_KEY", "perplexityApiKey");
|
|
803
|
+
if (!key) throw new Error("Perplexity search requires PERPLEXITY_API_KEY");
|
|
804
|
+
const result = await searchPerplexity(request.query, request.maxResults, key, signal);
|
|
805
|
+
if (result.sources.length > 0) return result;
|
|
806
|
+
fallbackReason = "Perplexity returned no results";
|
|
807
|
+
} else {
|
|
808
|
+
const key = await resolveApiKey("DEEPSEEK_API_KEY", "deepseekApiKey");
|
|
809
|
+
if (!key) throw new Error("DeepSeek search requires DEEPSEEK_API_KEY");
|
|
810
|
+
const result = await searchDeepSeekOfficial(request.query, request.maxResults, key, signal);
|
|
811
|
+
if (result.sources.length > 0) return result;
|
|
812
|
+
fallbackReason = "DeepSeek search returned no results";
|
|
813
|
+
}
|
|
814
|
+
} catch (error) {
|
|
815
|
+
fallbackReason = error instanceof Error ? error.message : String(error);
|
|
816
|
+
logger.warn(`free-search: paid engine "${preferred}" failed (${fallbackReason}), falling back to free engines`);
|
|
817
|
+
}
|
|
792
818
|
}
|
|
793
819
|
|
|
794
|
-
//
|
|
820
|
+
// 免费引擎链(付费引擎失败时也走这里)
|
|
795
821
|
const chain =
|
|
796
822
|
preferred === "ddg-lite"
|
|
797
823
|
? ["ddg-lite", "ddg", "anysearch", "searxng", "bing"]
|
|
@@ -801,7 +827,7 @@ function apply(ctx, config) {
|
|
|
801
827
|
? ["searxng", "anysearch", "bing", "ddg", "ddg-lite"]
|
|
802
828
|
: preferred === "anysearch"
|
|
803
829
|
? ["anysearch", "bing", "searxng", "ddg", "ddg-lite"]
|
|
804
|
-
: ["
|
|
830
|
+
: ["bing", "anysearch", "searxng", "ddg", "ddg-lite"];
|
|
805
831
|
let lastError = null;
|
|
806
832
|
for (const engine of chain) {
|
|
807
833
|
try {
|
|
@@ -825,7 +851,13 @@ function apply(ctx, config) {
|
|
|
825
851
|
default:
|
|
826
852
|
result = await searchDdgHtml(request.query, request.maxResults, cfg, signal);
|
|
827
853
|
}
|
|
828
|
-
if (result.sources.length > 0)
|
|
854
|
+
if (result.sources.length > 0) {
|
|
855
|
+
// 付费引擎失败回退时,在结果里附上提示(agent 会看到)
|
|
856
|
+
if (fallbackReason) {
|
|
857
|
+
result.content = `Note: ${preferred} search unavailable (${fallbackReason}). Results from ${engine}.`;
|
|
858
|
+
}
|
|
859
|
+
return result;
|
|
860
|
+
}
|
|
829
861
|
lastError = new Error(`engine "${engine}" returned 0 results`);
|
|
830
862
|
logger.warn(`free-search: ${engine} returned 0 results, trying next engine`);
|
|
831
863
|
} catch (error) {
|
|
@@ -1044,6 +1076,13 @@ function apply(ctx, config) {
|
|
|
1044
1076
|
if (!PLATFORMS[platform]) {
|
|
1045
1077
|
throw new Error(`unknown platform "${platform}" - use one of: ${Object.keys(PLATFORMS).join(", ")}`);
|
|
1046
1078
|
}
|
|
1079
|
+
// 平台开关:settings 里禁用某平台时,工具明确告知
|
|
1080
|
+
const enabled = current().platforms ?? ["github", "v2ex", "bilibili"];
|
|
1081
|
+
if (!enabled.includes(platform)) {
|
|
1082
|
+
throw new Error(
|
|
1083
|
+
`platform "${platform}" is disabled in Free Search settings - enable it in Settings > Plugins > Free Search to use it`
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1047
1086
|
const limit = Math.min(args.maxResults ?? 5, 10);
|
|
1048
1087
|
const result = await searchPlatform(platform, args.query, limit);
|
|
1049
1088
|
return { platform, sources: result.sources };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-free-search",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Free web search for DeepSeek Harness: 8 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa keyless) + platform search + web_fetch, with web settings UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|