dsh-free-search 0.2.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 +15 -1
- package/README.md +15 -1
- package/lib/client.js +3 -2
- package/lib/index.js +322 -12
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -27,6 +27,7 @@ This plugin provides multiple free engines with automatic failover, fully indepe
|
|
|
27
27
|
- **FREE / API KEY badges** — green FREE badge for free engines, orange API KEY badge for paid ones
|
|
28
28
|
- **Clean integration** — implements the official `WebSearchProvider` seam, coexists with official plugins
|
|
29
29
|
- **web_fetch** — agent can fetch page content (official `dsh-web-fetch-http` provider, pure JS, zero extra deps)
|
|
30
|
+
- **platform_search** — search GitHub / V2EX / Bilibili (public APIs, zero deps)
|
|
30
31
|
|
|
31
32
|
## Engines
|
|
32
33
|
|
|
@@ -35,8 +36,9 @@ This plugin provides multiple free engines with automatic failover, fully indepe
|
|
|
35
36
|
| `ddg` | DuckDuckGo HTML | Free | Occasionally rate-limited (anti-bot), recovers automatically |
|
|
36
37
|
| `ddg-lite` | DuckDuckGo Lite | Free | Lightweight variant, same caveat |
|
|
37
38
|
| `bing` | Bing | Free | **Default engine**, most stable, zh-CN optimized |
|
|
39
|
+
| `anysearch` | AnySearch AI | Free | AI search, no key (anonymous quota) |
|
|
38
40
|
| `searxng` | SearXNG meta-search | Free | Multi-instance failover, custom instances supported |
|
|
39
|
-
| `exa` | Exa |
|
|
41
|
+
| `exa` | Exa | Free | **Works keyless** (anonymous MCP), add key for higher limits |
|
|
40
42
|
| `perplexity` | Perplexity | Paid | Requires `PERPLEXITY_API_KEY` |
|
|
41
43
|
| `deepseek-official` | DeepSeek Official | Paid | Requires `DEEPSEEK_API_KEY` |
|
|
42
44
|
|
|
@@ -105,6 +107,18 @@ After search, ask the agent to **read a page** (e.g. "open the first link and su
|
|
|
105
107
|
- Timeout and size limits
|
|
106
108
|
- ⚠️ Note: `web_fetch` has no SSRF protection — the agent could reach internal addresses. Use deliberately.
|
|
107
109
|
|
|
110
|
+
### Platform search (platform_search)
|
|
111
|
+
|
|
112
|
+
Ask the agent to search a specific platform, e.g. "search GitHub for deepseek harness", "find Bilibili videos about this", "V2EX threads about dsh". The `platform_search` tool supports:
|
|
113
|
+
|
|
114
|
+
| Platform | Purpose |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `github` | GitHub repository search (public API, free, no key) |
|
|
117
|
+
| `v2ex` | V2EX hot/relevant topics |
|
|
118
|
+
| `bilibili` | Bilibili video/content search (public endpoint) |
|
|
119
|
+
|
|
120
|
+
All use public APIs with zero external dependencies — works out of the box.
|
|
121
|
+
|
|
108
122
|
## Local engine switcher (tools/)
|
|
109
123
|
|
|
110
124
|
Prefer a local tool over the web UI? The `tools/` directory ships a zero-dependency switcher:
|
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
26
26
|
- **系统提示词注入** —— agent 知道当前用哪个引擎、哪些需要 key
|
|
27
27
|
- **免费标注** —— 设置页中免费引擎带绿色 `FREE` 徽章,付费引擎带橙色 `API KEY` 徽章
|
|
28
28
|
- **网页抓取(web_fetch)** —— 让 agent 抓取网页内容(官方 `dsh-web-fetch-http` provider,纯 JS,零额外依赖)
|
|
29
|
+
- **平台搜索(platform_search)** —— 搜 GitHub / V2EX / B站(公开 API,零依赖)
|
|
29
30
|
- **干净集成** —— 实现官方 `WebSearchProvider` seam 接口,与官方插件共存
|
|
30
31
|
|
|
31
32
|
## 引擎列表
|
|
@@ -35,8 +36,9 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
35
36
|
| `ddg` | DuckDuckGo HTML | 免费 | 偶发限流(反爬),解封自动恢复 |
|
|
36
37
|
| `ddg-lite` | DuckDuckGo Lite | 免费 | 轻量版,同上 |
|
|
37
38
|
| `bing` | Bing | 免费 | **默认引擎**,最稳定,中文优化(zh-CN) |
|
|
39
|
+
| `anysearch` | AnySearch AI | 免费 | AI 搜索,无 key(匿名额度) |
|
|
38
40
|
| `searxng` | SearXNG 元搜索 | 免费 | 多实例自动切换,支持自定义实例 |
|
|
39
|
-
| `exa` | Exa |
|
|
41
|
+
| `exa` | Exa | 免费 | **无 key 也可用**(MCP 匿名),配 key 提升额度 |
|
|
40
42
|
| `perplexity` | Perplexity | 付费 | 需 `PERPLEXITY_API_KEY` |
|
|
41
43
|
| `deepseek-official` | DeepSeek 官方 | 付费 | 需 `DEEPSEEK_API_KEY` |
|
|
42
44
|
|
|
@@ -104,6 +106,18 @@ Search engine test:
|
|
|
104
106
|
- 支持超时和大小限制
|
|
105
107
|
- ⚠️ 注意:`web_fetch` 无 SSRF 防护,agent 理论上可访问内网地址——按需使用
|
|
106
108
|
|
|
109
|
+
### 平台搜索(platform_search)
|
|
110
|
+
|
|
111
|
+
让 agent 搜特定平台,如"在 GitHub 上搜 deepseek harness"、"看看 B站有什么相关视频"、"V2EX 上关于 dsh 的讨论"。`platform_search` 工具支持:
|
|
112
|
+
|
|
113
|
+
| 平台 | 用途 |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `github` | GitHub 仓库搜索(API,免费无 key) |
|
|
116
|
+
| `v2ex` | V2EX 热门/相关主题 |
|
|
117
|
+
| `bilibili` | B站视频/内容搜索(公开接口) |
|
|
118
|
+
|
|
119
|
+
全部走公开 API,零外部依赖,开箱即用。
|
|
120
|
+
|
|
107
121
|
## 本地引擎切换工具(tools/)
|
|
108
122
|
|
|
109
123
|
不想用网页设置页?`tools/` 目录附带了一个本地切换小工具(零依赖):
|
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,231 @@ 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
|
+
|
|
335
|
+
//#region platform search (GitHub / V2EX / Bilibili / Reddit)
|
|
336
|
+
const PLATFORMS = {
|
|
337
|
+
github: { name: "GitHub" },
|
|
338
|
+
v2ex: { name: "V2EX" },
|
|
339
|
+
bilibili: { name: "Bilibili" },
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
async function searchGithub(query, maxResults, signal) {
|
|
343
|
+
const response = await fetch(
|
|
344
|
+
`https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&per_page=${maxResults ?? 5}`,
|
|
345
|
+
{
|
|
346
|
+
headers: { "user-agent": USER_AGENT, accept: "application/vnd.github+json" },
|
|
347
|
+
...(signal !== undefined ? { signal } : {}),
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
if (!response.ok) throw new Error(`GitHub API error (HTTP ${response.status})`);
|
|
351
|
+
const data = await response.json();
|
|
352
|
+
return {
|
|
353
|
+
sources: (data.items ?? []).map((item) => ({
|
|
354
|
+
url: item.html_url,
|
|
355
|
+
title: item.full_name ?? item.name,
|
|
356
|
+
snippet: `${item.description ?? ""}${item.stargazers_count ? ` ⭐${item.stargazers_count}` : ""}`.trim(),
|
|
357
|
+
})),
|
|
358
|
+
truncated: false,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function searchV2ex(query, maxResults, signal) {
|
|
363
|
+
const response = await fetch("https://www.v2ex.com/api/topics/hot.json", {
|
|
364
|
+
headers: { "user-agent": USER_AGENT },
|
|
365
|
+
...(signal !== undefined ? { signal } : {}),
|
|
366
|
+
});
|
|
367
|
+
if (!response.ok) throw new Error(`V2EX API error (HTTP ${response.status})`);
|
|
368
|
+
const topics = await response.json();
|
|
369
|
+
const q = query.toLowerCase();
|
|
370
|
+
const matched = Array.isArray(topics)
|
|
371
|
+
? topics.filter((t) => (t.title ?? "").toLowerCase().includes(q) || (t.content ?? "").toLowerCase().includes(q))
|
|
372
|
+
: [];
|
|
373
|
+
return {
|
|
374
|
+
sources: matched.slice(0, maxResults ?? 5).map((t) => ({
|
|
375
|
+
url: `https://www.v2ex.com/t/${t.id}`,
|
|
376
|
+
title: t.title,
|
|
377
|
+
...(t.content ? { snippet: String(t.content).slice(0, 200) } : {}),
|
|
378
|
+
})),
|
|
379
|
+
truncated: false,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function searchBilibili(query, maxResults, signal) {
|
|
384
|
+
const response = await fetch(
|
|
385
|
+
`https://api.bilibili.com/x/web-interface/search/all/v2?keyword=${encodeURIComponent(query)}`,
|
|
386
|
+
{
|
|
387
|
+
headers: { "user-agent": USER_AGENT, referer: "https://www.bilibili.com" },
|
|
388
|
+
...(signal !== undefined ? { signal } : {}),
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
if (!response.ok) throw new Error(`Bilibili API error (HTTP ${response.status})`);
|
|
392
|
+
const data = await response.json();
|
|
393
|
+
if (data.code !== 0) throw new Error(`Bilibili API error: ${data.message ?? data.code}`);
|
|
394
|
+
const sources = [];
|
|
395
|
+
for (const section of data.data?.result ?? []) {
|
|
396
|
+
for (const item of section.data ?? []) {
|
|
397
|
+
if (!item.arcurl) continue;
|
|
398
|
+
sources.push({
|
|
399
|
+
url: item.arcurl,
|
|
400
|
+
title: item.title ? String(item.title).replace(/<[^>]+>/g, "") : item.bvid,
|
|
401
|
+
...(item.desc ? { snippet: String(item.desc).slice(0, 200) } : {}),
|
|
402
|
+
});
|
|
403
|
+
if (sources.length >= (maxResults ?? 5)) break;
|
|
404
|
+
}
|
|
405
|
+
if (sources.length >= (maxResults ?? 5)) break;
|
|
406
|
+
}
|
|
407
|
+
return { sources, truncated: false };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function searchReddit(query, maxResults, signal) {
|
|
411
|
+
const response = await fetch(
|
|
412
|
+
`https://old.reddit.com/search.json?q=${encodeURIComponent(query)}&limit=${maxResults ?? 5}&sort=relevance`,
|
|
413
|
+
{
|
|
414
|
+
headers: {
|
|
415
|
+
"user-agent": `${USER_AGENT} (dsh-free-search; contact: github.com/DDDMUC)`,
|
|
416
|
+
accept: "application/json",
|
|
417
|
+
},
|
|
418
|
+
...(signal !== undefined ? { signal } : {}),
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
if (!response.ok) throw new Error(`Reddit API error (HTTP ${response.status})`);
|
|
422
|
+
const data = await response.json();
|
|
423
|
+
return {
|
|
424
|
+
sources: (data.data?.children ?? [])
|
|
425
|
+
.map((c) => c.data)
|
|
426
|
+
.filter((p) => p && p.url)
|
|
427
|
+
.map((p) => ({
|
|
428
|
+
url: p.url,
|
|
429
|
+
title: p.title ?? "",
|
|
430
|
+
...(p.selftext ? { snippet: String(p.selftext).slice(0, 200) } : {}),
|
|
431
|
+
})),
|
|
432
|
+
truncated: false,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function searchPlatform(platform, query, maxResults, signal) {
|
|
437
|
+
switch (platform) {
|
|
438
|
+
case "github":
|
|
439
|
+
return searchGithub(query, maxResults, signal);
|
|
440
|
+
case "v2ex":
|
|
441
|
+
return searchV2ex(query, maxResults, signal);
|
|
442
|
+
case "bilibili":
|
|
443
|
+
return searchBilibili(query, maxResults, signal);
|
|
444
|
+
case "reddit":
|
|
445
|
+
return searchReddit(query, maxResults, signal);
|
|
446
|
+
default:
|
|
447
|
+
throw new Error(`unknown platform: ${platform}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
//#endregion
|
|
451
|
+
|
|
227
452
|
//#region paid engines (exa / perplexity / deepseek-official)
|
|
228
453
|
async function searchExa(query, maxResults, apiKey, signal) {
|
|
229
454
|
if (!apiKey) throw new Error("Exa search requires EXA_API_KEY");
|
|
@@ -551,10 +776,11 @@ function apply(ctx, config) {
|
|
|
551
776
|
const cfg = current();
|
|
552
777
|
const preferred = cfg.provider ?? "bing";
|
|
553
778
|
|
|
554
|
-
// 付费引擎:直接调用,缺 key
|
|
779
|
+
// 付费引擎:直接调用,缺 key 报错(exa 无 key 时走 Exa MCP keyless)
|
|
555
780
|
if (preferred === "exa") {
|
|
556
781
|
const key = await resolveApiKey("EXA_API_KEY", "exaApiKey");
|
|
557
|
-
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);
|
|
558
784
|
}
|
|
559
785
|
if (preferred === "perplexity") {
|
|
560
786
|
const key = await resolveApiKey("PERPLEXITY_API_KEY", "perplexityApiKey");
|
|
@@ -565,15 +791,17 @@ function apply(ctx, config) {
|
|
|
565
791
|
return searchDeepSeekOfficial(request.query, request.maxResults, key, signal);
|
|
566
792
|
}
|
|
567
793
|
|
|
568
|
-
// 免费引擎:首选 + 自动回退(searxng
|
|
794
|
+
// 免费引擎:首选 + 自动回退(anysearch / searxng 无 key)
|
|
569
795
|
const chain =
|
|
570
796
|
preferred === "ddg-lite"
|
|
571
|
-
? ["ddg-lite", "ddg", "searxng", "bing"]
|
|
797
|
+
? ["ddg-lite", "ddg", "anysearch", "searxng", "bing"]
|
|
572
798
|
: preferred === "bing"
|
|
573
|
-
? ["bing", "searxng", "ddg", "ddg-lite"]
|
|
799
|
+
? ["bing", "anysearch", "searxng", "ddg", "ddg-lite"]
|
|
574
800
|
: preferred === "searxng"
|
|
575
|
-
? ["searxng", "bing", "ddg", "ddg-lite"]
|
|
576
|
-
:
|
|
801
|
+
? ["searxng", "anysearch", "bing", "ddg", "ddg-lite"]
|
|
802
|
+
: preferred === "anysearch"
|
|
803
|
+
? ["anysearch", "bing", "searxng", "ddg", "ddg-lite"]
|
|
804
|
+
: ["ddg", "anysearch", "searxng", "ddg-lite", "bing"];
|
|
577
805
|
let lastError = null;
|
|
578
806
|
for (const engine of chain) {
|
|
579
807
|
try {
|
|
@@ -591,6 +819,9 @@ function apply(ctx, config) {
|
|
|
591
819
|
case "searxng":
|
|
592
820
|
result = await searchSearxng(request.query, request.maxResults, cfg, signal);
|
|
593
821
|
break;
|
|
822
|
+
case "anysearch":
|
|
823
|
+
result = await searchAnysearch(request.query, request.maxResults, signal);
|
|
824
|
+
break;
|
|
594
825
|
default:
|
|
595
826
|
result = await searchDdgHtml(request.query, request.maxResults, cfg, signal);
|
|
596
827
|
}
|
|
@@ -639,10 +870,12 @@ function apply(ctx, config) {
|
|
|
639
870
|
return await searchBing(q, 2, cfg);
|
|
640
871
|
case "searxng":
|
|
641
872
|
return await searchSearxng(q, 2, cfg);
|
|
873
|
+
case "anysearch":
|
|
874
|
+
return await searchAnysearch(q, 2);
|
|
642
875
|
case "exa": {
|
|
643
876
|
const key = await resolveApiKey("EXA_API_KEY", "exaApiKey");
|
|
644
|
-
if (
|
|
645
|
-
return await
|
|
877
|
+
if (key) return await searchExa(q, 2, key);
|
|
878
|
+
return await searchExaMCP(q, 2);
|
|
646
879
|
}
|
|
647
880
|
case "perplexity": {
|
|
648
881
|
const key = await resolveApiKey("PERPLEXITY_API_KEY", "perplexityApiKey");
|
|
@@ -759,6 +992,70 @@ function apply(ctx, config) {
|
|
|
759
992
|
}, "free-search: test engines tool");
|
|
760
993
|
});
|
|
761
994
|
|
|
995
|
+
// 平台搜索工具:GitHub / V2EX / Bilibili(公开 API,零依赖)
|
|
996
|
+
ctx.inject(["tools"], (sctx) => {
|
|
997
|
+
sctx.effect(() => {
|
|
998
|
+
const dispose = sctx.tools.register(
|
|
999
|
+
defineTool({
|
|
1000
|
+
name: "platform_search",
|
|
1001
|
+
description:
|
|
1002
|
+
"Search a specific platform (GitHub / V2EX / Bilibili) for a query. Returns source URLs with titles and snippets. Use this when the user asks about repos, code, forum threads, or videos.",
|
|
1003
|
+
parameters: {
|
|
1004
|
+
platform: {
|
|
1005
|
+
type: "string",
|
|
1006
|
+
description: "Platform to search: github, v2ex, bilibili",
|
|
1007
|
+
},
|
|
1008
|
+
query: {
|
|
1009
|
+
type: "string",
|
|
1010
|
+
description: "The search query.",
|
|
1011
|
+
},
|
|
1012
|
+
maxResults: {
|
|
1013
|
+
type: "number",
|
|
1014
|
+
description: "Optional result count (default 5, max 10).",
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
output: {
|
|
1018
|
+
schema: {
|
|
1019
|
+
type: "object",
|
|
1020
|
+
additionalProperties: false,
|
|
1021
|
+
properties: {
|
|
1022
|
+
platform: { type: "string" },
|
|
1023
|
+
sources: {
|
|
1024
|
+
type: "array",
|
|
1025
|
+
items: {
|
|
1026
|
+
type: "object",
|
|
1027
|
+
additionalProperties: false,
|
|
1028
|
+
properties: {
|
|
1029
|
+
url: { type: "string" },
|
|
1030
|
+
title: { type: "string" },
|
|
1031
|
+
snippet: { type: "string" },
|
|
1032
|
+
},
|
|
1033
|
+
},
|
|
1034
|
+
},
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
1037
|
+
render(args, value) {
|
|
1038
|
+
const lines = value.sources.map((s, i) => `- [${s.title ?? s.url}](${s.url})${s.snippet ? ` - ${s.snippet.slice(0, 120)}` : ""}`);
|
|
1039
|
+
return `Platform search (${value.platform}):\n${lines.join("\n") || "No results found."}`;
|
|
1040
|
+
},
|
|
1041
|
+
},
|
|
1042
|
+
async execute(args) {
|
|
1043
|
+
const platform = args.platform;
|
|
1044
|
+
if (!PLATFORMS[platform]) {
|
|
1045
|
+
throw new Error(`unknown platform "${platform}" - use one of: ${Object.keys(PLATFORMS).join(", ")}`);
|
|
1046
|
+
}
|
|
1047
|
+
const limit = Math.min(args.maxResults ?? 5, 10);
|
|
1048
|
+
const result = await searchPlatform(platform, args.query, limit);
|
|
1049
|
+
return { platform, sources: result.sources };
|
|
1050
|
+
},
|
|
1051
|
+
})
|
|
1052
|
+
);
|
|
1053
|
+
return () => {
|
|
1054
|
+
dispose();
|
|
1055
|
+
};
|
|
1056
|
+
}, "free-search: platform search tool");
|
|
1057
|
+
});
|
|
1058
|
+
|
|
762
1059
|
// 让 agent 知道可用搜索引擎(动态生成,随 key 配置变化)
|
|
763
1060
|
ctx.inject(["systemPrompt"], (sctx) => {
|
|
764
1061
|
sctx.effect(() => {
|
|
@@ -776,13 +1073,16 @@ function apply(ctx, config) {
|
|
|
776
1073
|
"- ddg-lite (DuckDuckGo Lite) - FREE, no key (may be rate-limited)",
|
|
777
1074
|
"- bing (Bing) - FREE, no key (most stable)",
|
|
778
1075
|
"- searxng (meta-search, multi-instance) - FREE, no key",
|
|
779
|
-
"-
|
|
1076
|
+
"- anysearch (AI search) - FREE, no key",
|
|
1077
|
+
"- exa - FREE keyless (MCP) or EXA_API_KEY for higher limits",
|
|
780
1078
|
"- perplexity - requires PERPLEXITY_API_KEY",
|
|
781
1079
|
"- deepseek-official - requires DEEPSEEK_API_KEY",
|
|
782
1080
|
"",
|
|
783
1081
|
"FREE engines auto-fallback to another FREE engine on failure. Paid engines fail with a clear error when their key is missing - tell the user which key to configure.",
|
|
784
1082
|
"",
|
|
785
1083
|
"Use the free_search_test tool to test which engines actually work right now.",
|
|
1084
|
+
"",
|
|
1085
|
+
"For platform-specific searches (GitHub repos, V2EX threads, Bilibili videos), use the platform_search tool with platform: github|v2ex|bilibili.",
|
|
786
1086
|
].join("\n"),
|
|
787
1087
|
};
|
|
788
1088
|
const dispose = sctx.systemPrompt.section(section);
|
|
@@ -795,21 +1095,31 @@ function apply(ctx, config) {
|
|
|
795
1095
|
|
|
796
1096
|
export {
|
|
797
1097
|
ALL_ENGINES,
|
|
1098
|
+
ANYSEARCH_URL,
|
|
798
1099
|
BING_URL,
|
|
799
1100
|
Config,
|
|
800
1101
|
DDG_HTML_URL,
|
|
801
1102
|
DDG_LITE_URL,
|
|
1103
|
+
EXA_MCP_URL,
|
|
802
1104
|
FREE_ENGINES,
|
|
803
1105
|
FREE_SEARCH_NS,
|
|
1106
|
+
PLATFORMS,
|
|
804
1107
|
SEARXNG_INSTANCES,
|
|
805
1108
|
apply,
|
|
806
1109
|
inject,
|
|
807
1110
|
name,
|
|
1111
|
+
searchAnysearch,
|
|
808
1112
|
searchBing,
|
|
1113
|
+
searchBilibili,
|
|
809
1114
|
searchDeepSeekOfficial,
|
|
810
1115
|
searchDdgHtml,
|
|
811
1116
|
searchDdgLite,
|
|
812
1117
|
searchExa,
|
|
1118
|
+
searchExaMCP,
|
|
1119
|
+
searchGithub,
|
|
813
1120
|
searchPerplexity,
|
|
1121
|
+
searchPlatform,
|
|
1122
|
+
searchReddit,
|
|
814
1123
|
searchSearxng,
|
|
1124
|
+
searchV2ex,
|
|
815
1125
|
};
|
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: DuckDuckGo/
|
|
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": {
|