dsh-free-search 0.3.0 → 0.4.0

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
@@ -36,8 +36,9 @@ This plugin provides multiple free engines with automatic failover, fully indepe
36
36
  | `ddg` | DuckDuckGo HTML | Free | Occasionally rate-limited (anti-bot), recovers automatically |
37
37
  | `ddg-lite` | DuckDuckGo Lite | Free | Lightweight variant, same caveat |
38
38
  | `bing` | Bing | Free | **Default engine**, most stable, zh-CN optimized |
39
+ | `anysearch` | AnySearch AI | Free | AI search, no key (anonymous quota) |
39
40
  | `searxng` | SearXNG meta-search | Free | Multi-instance failover, custom instances supported |
40
- | `exa` | Exa | Paid | Requires `EXA_API_KEY` |
41
+ | `exa` | Exa | Free | **Works keyless** (anonymous MCP), add key for higher limits |
41
42
  | `perplexity` | Perplexity | Paid | Requires `PERPLEXITY_API_KEY` |
42
43
  | `deepseek-official` | DeepSeek Official | Paid | Requires `DEEPSEEK_API_KEY` |
43
44
 
package/README.md CHANGED
@@ -36,8 +36,9 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
36
36
  | `ddg` | DuckDuckGo HTML | 免费 | 偶发限流(反爬),解封自动恢复 |
37
37
  | `ddg-lite` | DuckDuckGo Lite | 免费 | 轻量版,同上 |
38
38
  | `bing` | Bing | 免费 | **默认引擎**,最稳定,中文优化(zh-CN) |
39
+ | `anysearch` | AnySearch AI | 免费 | AI 搜索,无 key(匿名额度) |
39
40
  | `searxng` | SearXNG 元搜索 | 免费 | 多实例自动切换,支持自定义实例 |
40
- | `exa` | Exa | 付费 | 需 `EXA_API_KEY` |
41
+ | `exa` | Exa | 免费 | **无 key 也可用**(MCP 匿名),配 key 提升额度 |
41
42
  | `perplexity` | Perplexity | 付费 | 需 `PERPLEXITY_API_KEY` |
42
43
  | `deepseek-official` | DeepSeek 官方 | 付费 | 需 `DEEPSEEK_API_KEY` |
43
44
 
package/lib/client.js CHANGED
@@ -57,8 +57,9 @@ window.__ModuleLoader__.load({
57
57
  { id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
58
58
  { id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
59
59
  { id: "bing", label: "Bing", badge: "FREE", link: "https://www.bing.com" },
60
+ { id: "anysearch", label: "AnySearch · AI", badge: "FREE", link: "https://anysearch.com" },
60
61
  { id: "searxng", label: "SearXNG · 元搜索", badge: "FREE", link: "https://github.com/searxng/searxng" },
61
- { id: "exa", label: "Exa", badge: "API KEY", link: "https://dashboard.exa.ai/api-keys" },
62
+ { id: "exa", label: "Exa", badge: "FREE", link: "https://dashboard.exa.ai/api-keys" },
62
63
  { id: "perplexity", label: "Perplexity", badge: "API KEY", link: "https://www.perplexity.ai/settings/api" },
63
64
  { id: "deepseek-official", label: "DeepSeek Official", badge: "API KEY", link: "https://platform.deepseek.com/api_keys" },
64
65
  ];
@@ -251,7 +252,7 @@ window.__ModuleLoader__.load({
251
252
  react_jsx_runtime.jsx("input", {
252
253
  className: "dshfs-input",
253
254
  type: "password",
254
- placeholder: keysConfigured.exa ? "Exa API key (configured)" : "Exa API key (sk-...)",
255
+ placeholder: keysConfigured.exa ? "Exa API key (configured)" : "Exa API key (optional, free without)",
255
256
  value: exaKey,
256
257
  disabled: !ready || saving,
257
258
  onChange: (e) => {
package/lib/index.js CHANGED
@@ -11,8 +11,8 @@ const ACCEPT_LANG = "zh-CN,zh;q=0.9,en;q=0.8";
11
11
 
12
12
  const FREE_SEARCH_NS = settingsNamespace("free-search");
13
13
  const BRIDGE_PREFIX = "/api/dsh-free-search-settings";
14
- const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng"];
15
- const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "exa", "perplexity", "deepseek-official"];
14
+ const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch"];
15
+ const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "perplexity", "deepseek-official"];
16
16
 
17
17
  function decodeEntities(text) {
18
18
  return String(text)
@@ -224,6 +224,114 @@ async function searchSearxng(query, maxResults, options, signal) {
224
224
  }
225
225
  //#endregion
226
226
 
227
+ //#region keyless engines (AnySearch / Exa MCP - free, no API key)
228
+ const ANYSEARCH_URL = "https://api.anysearch.com/v1/search";
229
+ const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
230
+
231
+ // AnySearch: 免费匿名额度(无 key),结构化 JSON 结果
232
+ async function searchAnysearch(query, maxResults, signal) {
233
+ const controller = new AbortController();
234
+ const timer = setTimeout(() => controller.abort(), 12000);
235
+ const onAbort = () => controller.abort();
236
+ signal?.addEventListener("abort", onAbort);
237
+ let response;
238
+ try {
239
+ response = await fetch(ANYSEARCH_URL, {
240
+ method: "POST",
241
+ headers: { "content-type": "application/json" },
242
+ body: JSON.stringify({ query, max_results: maxResults ?? 5 }),
243
+ signal: controller.signal,
244
+ });
245
+ } catch (error) {
246
+ if (signal?.aborted) throw error;
247
+ throw new Error(`AnySearch request failed: ${error?.message ?? String(error)}`);
248
+ } finally {
249
+ clearTimeout(timer);
250
+ signal?.removeEventListener("abort", onAbort);
251
+ }
252
+ if (!response.ok) throw new Error(`AnySearch API error (HTTP ${response.status})`);
253
+ const data = await response.json();
254
+ if (data.code !== 0) throw new Error(`AnySearch API error: ${data.message ?? data.code}`);
255
+ const results = data.data?.results ?? [];
256
+ return {
257
+ sources: results
258
+ .filter((r) => r.url)
259
+ .map((r) => ({
260
+ url: r.url,
261
+ ...(r.title ? { title: String(r.title) } : {}),
262
+ ...(r.snippet ? { snippet: String(r.snippet).slice(0, 300) } : {}),
263
+ })),
264
+ truncated: false,
265
+ };
266
+ }
267
+
268
+ // Exa MCP: 匿名公开 MCP(无 key),web_search_exa 工具
269
+ async function searchExaMCP(query, maxResults, signal) {
270
+ const controller = new AbortController();
271
+ const timer = setTimeout(() => controller.abort(), 20000);
272
+ const onAbort = () => controller.abort();
273
+ signal?.addEventListener("abort", onAbort);
274
+ let response;
275
+ try {
276
+ response = await fetch(EXA_MCP_URL, {
277
+ method: "POST",
278
+ headers: { "content-type": "application/json", accept: "application/json, text/event-stream" },
279
+ body: JSON.stringify({
280
+ jsonrpc: "2.0",
281
+ id: Date.now(),
282
+ method: "tools/call",
283
+ params: { name: "web_search_exa", arguments: { query, numResults: maxResults ?? 5 } },
284
+ }),
285
+ signal: controller.signal,
286
+ });
287
+ } catch (error) {
288
+ if (signal?.aborted) throw error;
289
+ throw new Error(`Exa MCP request failed: ${error?.message ?? String(error)}`);
290
+ } finally {
291
+ clearTimeout(timer);
292
+ signal?.removeEventListener("abort", onAbort);
293
+ }
294
+ if (!response.ok) throw new Error(`Exa MCP error (HTTP ${response.status})`);
295
+ const text = await response.text();
296
+ // 解析 SSE 格式:event: message\ndata: {...}
297
+ const lines = text.split("\n");
298
+ let json = null;
299
+ for (const line of lines) {
300
+ if (line.startsWith("data: ")) {
301
+ try {
302
+ json = JSON.parse(line.slice(6));
303
+ break;
304
+ } catch {}
305
+ }
306
+ }
307
+ if (!json || json.error) {
308
+ throw new Error(`Exa MCP error: ${json?.error?.message ?? "no data"}`);
309
+ }
310
+ const content = json.result?.content ?? [];
311
+ const sources = [];
312
+ const textBlocks = content
313
+ .filter((b) => b.type === "text")
314
+ .map((b) => b.text)
315
+ .join("\n");
316
+ // 解析 "Title: X\nURL: Y\nPublished: Z\nHighlights:\n..."
317
+ const blocks = textBlocks.split(/\n(?=Title:)/);
318
+ for (const block of blocks) {
319
+ const title = block.match(/^Title: (.+)$/m)?.[1];
320
+ const url = block.match(/^URL: (\S+)$/m)?.[1];
321
+ const published = block.match(/^Published: (.+)$/m)?.[1];
322
+ const highlights = block.split(/^Highlights:$/m)[1]?.split("\n").filter((l) => l.trim() && !l.trim().startsWith("...")).slice(0, 3).join(" ");
323
+ if (!url) continue;
324
+ sources.push({
325
+ url,
326
+ ...(title ? { title } : {}),
327
+ ...(highlights ? { snippet: highlights.slice(0, 300) } : {}),
328
+ ...(published ? { publishedAt: published } : {}),
329
+ });
330
+ }
331
+ return { sources, truncated: false };
332
+ }
333
+ //#endregion
334
+
227
335
  //#region platform search (GitHub / V2EX / Bilibili / Reddit)
228
336
  const PLATFORMS = {
229
337
  github: { name: "GitHub" },
@@ -668,10 +776,11 @@ function apply(ctx, config) {
668
776
  const cfg = current();
669
777
  const preferred = cfg.provider ?? "bing";
670
778
 
671
- // 付费引擎:直接调用,缺 key 报错
779
+ // 付费引擎:直接调用,缺 key 报错(exa 无 key 时走 Exa MCP keyless)
672
780
  if (preferred === "exa") {
673
781
  const key = await resolveApiKey("EXA_API_KEY", "exaApiKey");
674
- return searchExa(request.query, request.maxResults, key, signal);
782
+ if (key) return searchExa(request.query, request.maxResults, key, signal);
783
+ return searchExaMCP(request.query, request.maxResults, signal);
675
784
  }
676
785
  if (preferred === "perplexity") {
677
786
  const key = await resolveApiKey("PERPLEXITY_API_KEY", "perplexityApiKey");
@@ -682,15 +791,17 @@ function apply(ctx, config) {
682
791
  return searchDeepSeekOfficial(request.query, request.maxResults, key, signal);
683
792
  }
684
793
 
685
- // 免费引擎:首选 + 自动回退(searxng 元搜索多实例自动切换)
794
+ // 免费引擎:首选 + 自动回退(anysearch / searxng 无 key)
686
795
  const chain =
687
796
  preferred === "ddg-lite"
688
- ? ["ddg-lite", "ddg", "searxng", "bing"]
797
+ ? ["ddg-lite", "ddg", "anysearch", "searxng", "bing"]
689
798
  : preferred === "bing"
690
- ? ["bing", "searxng", "ddg", "ddg-lite"]
799
+ ? ["bing", "anysearch", "searxng", "ddg", "ddg-lite"]
691
800
  : preferred === "searxng"
692
- ? ["searxng", "bing", "ddg", "ddg-lite"]
693
- : ["ddg", "searxng", "ddg-lite", "bing"];
801
+ ? ["searxng", "anysearch", "bing", "ddg", "ddg-lite"]
802
+ : preferred === "anysearch"
803
+ ? ["anysearch", "bing", "searxng", "ddg", "ddg-lite"]
804
+ : ["ddg", "anysearch", "searxng", "ddg-lite", "bing"];
694
805
  let lastError = null;
695
806
  for (const engine of chain) {
696
807
  try {
@@ -708,6 +819,9 @@ function apply(ctx, config) {
708
819
  case "searxng":
709
820
  result = await searchSearxng(request.query, request.maxResults, cfg, signal);
710
821
  break;
822
+ case "anysearch":
823
+ result = await searchAnysearch(request.query, request.maxResults, signal);
824
+ break;
711
825
  default:
712
826
  result = await searchDdgHtml(request.query, request.maxResults, cfg, signal);
713
827
  }
@@ -756,10 +870,12 @@ function apply(ctx, config) {
756
870
  return await searchBing(q, 2, cfg);
757
871
  case "searxng":
758
872
  return await searchSearxng(q, 2, cfg);
873
+ case "anysearch":
874
+ return await searchAnysearch(q, 2);
759
875
  case "exa": {
760
876
  const key = await resolveApiKey("EXA_API_KEY", "exaApiKey");
761
- if (!key) return { ok: false, error: "EXA_API_KEY not configured" };
762
- return await searchExa(q, 2, key);
877
+ if (key) return await searchExa(q, 2, key);
878
+ return await searchExaMCP(q, 2);
763
879
  }
764
880
  case "perplexity": {
765
881
  const key = await resolveApiKey("PERPLEXITY_API_KEY", "perplexityApiKey");
@@ -957,7 +1073,8 @@ function apply(ctx, config) {
957
1073
  "- ddg-lite (DuckDuckGo Lite) - FREE, no key (may be rate-limited)",
958
1074
  "- bing (Bing) - FREE, no key (most stable)",
959
1075
  "- searxng (meta-search, multi-instance) - FREE, no key",
960
- "- exa - requires EXA_API_KEY",
1076
+ "- anysearch (AI search) - FREE, no key",
1077
+ "- exa - FREE keyless (MCP) or EXA_API_KEY for higher limits",
961
1078
  "- perplexity - requires PERPLEXITY_API_KEY",
962
1079
  "- deepseek-official - requires DEEPSEEK_API_KEY",
963
1080
  "",
@@ -978,10 +1095,12 @@ function apply(ctx, config) {
978
1095
 
979
1096
  export {
980
1097
  ALL_ENGINES,
1098
+ ANYSEARCH_URL,
981
1099
  BING_URL,
982
1100
  Config,
983
1101
  DDG_HTML_URL,
984
1102
  DDG_LITE_URL,
1103
+ EXA_MCP_URL,
985
1104
  FREE_ENGINES,
986
1105
  FREE_SEARCH_NS,
987
1106
  PLATFORMS,
@@ -989,12 +1108,14 @@ export {
989
1108
  apply,
990
1109
  inject,
991
1110
  name,
1111
+ searchAnysearch,
992
1112
  searchBing,
993
1113
  searchBilibili,
994
1114
  searchDeepSeekOfficial,
995
1115
  searchDdgHtml,
996
1116
  searchDdgLite,
997
1117
  searchExa,
1118
+ searchExaMCP,
998
1119
  searchGithub,
999
1120
  searchPerplexity,
1000
1121
  searchPlatform,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-free-search",
3
- "version": "0.3.0",
4
- "description": "Free web search for DeepSeek Harness: 7 engines + platform search (GitHub/V2EX/Bilibili) + web_fetch, with web settings UI and engine test tool.",
3
+ "version": "0.4.0",
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",
7
7
  "exports": {