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 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
- - Free engines automatically fall back to other free engines upon failure. Paid engines report clear error messages when keys are missing or invalid, avoiding silent fallbacks.
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
- - 免费引擎失败会自动回退到其他免费引擎;付费引擎缺 key 或 key 无效时报清晰错误,不会静默切换。
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
- // 免费引擎失败自动回退其他免费引擎;付费引擎缺 key 时报清晰错误(不静默切换)。
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
- // 付费引擎:直接调用,缺 key 报错(exa key 时走 Exa MCP keyless)
780
- if (preferred === "exa") {
781
- const key = await resolveApiKey("EXA_API_KEY", "exaApiKey");
782
- if (key) return searchExa(request.query, request.maxResults, key, signal);
783
- return searchExaMCP(request.query, request.maxResults, signal);
784
- }
785
- if (preferred === "perplexity") {
786
- const key = await resolveApiKey("PERPLEXITY_API_KEY", "perplexityApiKey");
787
- return searchPerplexity(request.query, request.maxResults, key, signal);
788
- }
789
- if (preferred === "deepseek-official") {
790
- const key = await resolveApiKey("DEEPSEEK_API_KEY", "deepseekApiKey");
791
- return searchDeepSeekOfficial(request.query, request.maxResults, key, signal);
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
- // 免费引擎:首选 + 自动回退(anysearch / searxng 无 key)
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
- : ["ddg", "anysearch", "searxng", "ddg-lite", "bing"];
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) return result;
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.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",