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 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 | Paid | Requires `EXA_API_KEY` |
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 | 付费 | 需 `EXA_API_KEY` |
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: "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,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
- : ["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"];
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 (!key) return { ok: false, error: "EXA_API_KEY not configured" };
645
- return await searchExa(q, 2, key);
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
- "- exa - requires EXA_API_KEY",
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.2.0",
4
- "description": "Free web search for DeepSeek Harness: DuckDuckGo/Bing (FREE) + Exa/Perplexity/DeepSeek (API key) engines, 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": {