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 +2 -1
- package/README.md +2 -1
- package/lib/client.js +3 -2
- package/lib/index.js +133 -12
- package/package.json +2 -2
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 |
|
|
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 |
|
|
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: "
|
|
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 (
|
|
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
|
-
:
|
|
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 (
|
|
762
|
-
return await
|
|
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
|
-
"-
|
|
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.
|
|
4
|
-
"description": "Free web search for DeepSeek Harness:
|
|
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": {
|