dsh-free-search 0.2.0 → 0.3.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
 
@@ -105,6 +106,18 @@ After search, ask the agent to **read a page** (e.g. "open the first link and su
105
106
  - Timeout and size limits
106
107
  - ⚠️ Note: `web_fetch` has no SSRF protection — the agent could reach internal addresses. Use deliberately.
107
108
 
109
+ ### Platform search (platform_search)
110
+
111
+ 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:
112
+
113
+ | Platform | Purpose |
114
+ |---|---|
115
+ | `github` | GitHub repository search (public API, free, no key) |
116
+ | `v2ex` | V2EX hot/relevant topics |
117
+ | `bilibili` | Bilibili video/content search (public endpoint) |
118
+
119
+ All use public APIs with zero external dependencies — works out of the box.
120
+
108
121
  ## Local engine switcher (tools/)
109
122
 
110
123
  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
  ## 引擎列表
@@ -104,6 +105,18 @@ Search engine test:
104
105
  - 支持超时和大小限制
105
106
  - ⚠️ 注意:`web_fetch` 无 SSRF 防护,agent 理论上可访问内网地址——按需使用
106
107
 
108
+ ### 平台搜索(platform_search)
109
+
110
+ 让 agent 搜特定平台,如"在 GitHub 上搜 deepseek harness"、"看看 B站有什么相关视频"、"V2EX 上关于 dsh 的讨论"。`platform_search` 工具支持:
111
+
112
+ | 平台 | 用途 |
113
+ |---|---|
114
+ | `github` | GitHub 仓库搜索(API,免费无 key) |
115
+ | `v2ex` | V2EX 热门/相关主题 |
116
+ | `bilibili` | B站视频/内容搜索(公开接口) |
117
+
118
+ 全部走公开 API,零外部依赖,开箱即用。
119
+
107
120
  ## 本地引擎切换工具(tools/)
108
121
 
109
122
  不想用网页设置页?`tools/` 目录附带了一个本地切换小工具(零依赖):
package/lib/index.js CHANGED
@@ -224,6 +224,123 @@ async function searchSearxng(query, maxResults, options, signal) {
224
224
  }
225
225
  //#endregion
226
226
 
227
+ //#region platform search (GitHub / V2EX / Bilibili / Reddit)
228
+ const PLATFORMS = {
229
+ github: { name: "GitHub" },
230
+ v2ex: { name: "V2EX" },
231
+ bilibili: { name: "Bilibili" },
232
+ };
233
+
234
+ async function searchGithub(query, maxResults, signal) {
235
+ const response = await fetch(
236
+ `https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&per_page=${maxResults ?? 5}`,
237
+ {
238
+ headers: { "user-agent": USER_AGENT, accept: "application/vnd.github+json" },
239
+ ...(signal !== undefined ? { signal } : {}),
240
+ }
241
+ );
242
+ if (!response.ok) throw new Error(`GitHub API error (HTTP ${response.status})`);
243
+ const data = await response.json();
244
+ return {
245
+ sources: (data.items ?? []).map((item) => ({
246
+ url: item.html_url,
247
+ title: item.full_name ?? item.name,
248
+ snippet: `${item.description ?? ""}${item.stargazers_count ? ` ⭐${item.stargazers_count}` : ""}`.trim(),
249
+ })),
250
+ truncated: false,
251
+ };
252
+ }
253
+
254
+ async function searchV2ex(query, maxResults, signal) {
255
+ const response = await fetch("https://www.v2ex.com/api/topics/hot.json", {
256
+ headers: { "user-agent": USER_AGENT },
257
+ ...(signal !== undefined ? { signal } : {}),
258
+ });
259
+ if (!response.ok) throw new Error(`V2EX API error (HTTP ${response.status})`);
260
+ const topics = await response.json();
261
+ const q = query.toLowerCase();
262
+ const matched = Array.isArray(topics)
263
+ ? topics.filter((t) => (t.title ?? "").toLowerCase().includes(q) || (t.content ?? "").toLowerCase().includes(q))
264
+ : [];
265
+ return {
266
+ sources: matched.slice(0, maxResults ?? 5).map((t) => ({
267
+ url: `https://www.v2ex.com/t/${t.id}`,
268
+ title: t.title,
269
+ ...(t.content ? { snippet: String(t.content).slice(0, 200) } : {}),
270
+ })),
271
+ truncated: false,
272
+ };
273
+ }
274
+
275
+ async function searchBilibili(query, maxResults, signal) {
276
+ const response = await fetch(
277
+ `https://api.bilibili.com/x/web-interface/search/all/v2?keyword=${encodeURIComponent(query)}`,
278
+ {
279
+ headers: { "user-agent": USER_AGENT, referer: "https://www.bilibili.com" },
280
+ ...(signal !== undefined ? { signal } : {}),
281
+ }
282
+ );
283
+ if (!response.ok) throw new Error(`Bilibili API error (HTTP ${response.status})`);
284
+ const data = await response.json();
285
+ if (data.code !== 0) throw new Error(`Bilibili API error: ${data.message ?? data.code}`);
286
+ const sources = [];
287
+ for (const section of data.data?.result ?? []) {
288
+ for (const item of section.data ?? []) {
289
+ if (!item.arcurl) continue;
290
+ sources.push({
291
+ url: item.arcurl,
292
+ title: item.title ? String(item.title).replace(/<[^>]+>/g, "") : item.bvid,
293
+ ...(item.desc ? { snippet: String(item.desc).slice(0, 200) } : {}),
294
+ });
295
+ if (sources.length >= (maxResults ?? 5)) break;
296
+ }
297
+ if (sources.length >= (maxResults ?? 5)) break;
298
+ }
299
+ return { sources, truncated: false };
300
+ }
301
+
302
+ async function searchReddit(query, maxResults, signal) {
303
+ const response = await fetch(
304
+ `https://old.reddit.com/search.json?q=${encodeURIComponent(query)}&limit=${maxResults ?? 5}&sort=relevance`,
305
+ {
306
+ headers: {
307
+ "user-agent": `${USER_AGENT} (dsh-free-search; contact: github.com/DDDMUC)`,
308
+ accept: "application/json",
309
+ },
310
+ ...(signal !== undefined ? { signal } : {}),
311
+ }
312
+ );
313
+ if (!response.ok) throw new Error(`Reddit API error (HTTP ${response.status})`);
314
+ const data = await response.json();
315
+ return {
316
+ sources: (data.data?.children ?? [])
317
+ .map((c) => c.data)
318
+ .filter((p) => p && p.url)
319
+ .map((p) => ({
320
+ url: p.url,
321
+ title: p.title ?? "",
322
+ ...(p.selftext ? { snippet: String(p.selftext).slice(0, 200) } : {}),
323
+ })),
324
+ truncated: false,
325
+ };
326
+ }
327
+
328
+ async function searchPlatform(platform, query, maxResults, signal) {
329
+ switch (platform) {
330
+ case "github":
331
+ return searchGithub(query, maxResults, signal);
332
+ case "v2ex":
333
+ return searchV2ex(query, maxResults, signal);
334
+ case "bilibili":
335
+ return searchBilibili(query, maxResults, signal);
336
+ case "reddit":
337
+ return searchReddit(query, maxResults, signal);
338
+ default:
339
+ throw new Error(`unknown platform: ${platform}`);
340
+ }
341
+ }
342
+ //#endregion
343
+
227
344
  //#region paid engines (exa / perplexity / deepseek-official)
228
345
  async function searchExa(query, maxResults, apiKey, signal) {
229
346
  if (!apiKey) throw new Error("Exa search requires EXA_API_KEY");
@@ -759,6 +876,70 @@ function apply(ctx, config) {
759
876
  }, "free-search: test engines tool");
760
877
  });
761
878
 
879
+ // 平台搜索工具:GitHub / V2EX / Bilibili(公开 API,零依赖)
880
+ ctx.inject(["tools"], (sctx) => {
881
+ sctx.effect(() => {
882
+ const dispose = sctx.tools.register(
883
+ defineTool({
884
+ name: "platform_search",
885
+ description:
886
+ "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.",
887
+ parameters: {
888
+ platform: {
889
+ type: "string",
890
+ description: "Platform to search: github, v2ex, bilibili",
891
+ },
892
+ query: {
893
+ type: "string",
894
+ description: "The search query.",
895
+ },
896
+ maxResults: {
897
+ type: "number",
898
+ description: "Optional result count (default 5, max 10).",
899
+ },
900
+ },
901
+ output: {
902
+ schema: {
903
+ type: "object",
904
+ additionalProperties: false,
905
+ properties: {
906
+ platform: { type: "string" },
907
+ sources: {
908
+ type: "array",
909
+ items: {
910
+ type: "object",
911
+ additionalProperties: false,
912
+ properties: {
913
+ url: { type: "string" },
914
+ title: { type: "string" },
915
+ snippet: { type: "string" },
916
+ },
917
+ },
918
+ },
919
+ },
920
+ },
921
+ render(args, value) {
922
+ const lines = value.sources.map((s, i) => `- [${s.title ?? s.url}](${s.url})${s.snippet ? ` - ${s.snippet.slice(0, 120)}` : ""}`);
923
+ return `Platform search (${value.platform}):\n${lines.join("\n") || "No results found."}`;
924
+ },
925
+ },
926
+ async execute(args) {
927
+ const platform = args.platform;
928
+ if (!PLATFORMS[platform]) {
929
+ throw new Error(`unknown platform "${platform}" - use one of: ${Object.keys(PLATFORMS).join(", ")}`);
930
+ }
931
+ const limit = Math.min(args.maxResults ?? 5, 10);
932
+ const result = await searchPlatform(platform, args.query, limit);
933
+ return { platform, sources: result.sources };
934
+ },
935
+ })
936
+ );
937
+ return () => {
938
+ dispose();
939
+ };
940
+ }, "free-search: platform search tool");
941
+ });
942
+
762
943
  // 让 agent 知道可用搜索引擎(动态生成,随 key 配置变化)
763
944
  ctx.inject(["systemPrompt"], (sctx) => {
764
945
  sctx.effect(() => {
@@ -783,6 +964,8 @@ function apply(ctx, config) {
783
964
  "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
965
  "",
785
966
  "Use the free_search_test tool to test which engines actually work right now.",
967
+ "",
968
+ "For platform-specific searches (GitHub repos, V2EX threads, Bilibili videos), use the platform_search tool with platform: github|v2ex|bilibili.",
786
969
  ].join("\n"),
787
970
  };
788
971
  const dispose = sctx.systemPrompt.section(section);
@@ -801,15 +984,21 @@ export {
801
984
  DDG_LITE_URL,
802
985
  FREE_ENGINES,
803
986
  FREE_SEARCH_NS,
987
+ PLATFORMS,
804
988
  SEARXNG_INSTANCES,
805
989
  apply,
806
990
  inject,
807
991
  name,
808
992
  searchBing,
993
+ searchBilibili,
809
994
  searchDeepSeekOfficial,
810
995
  searchDdgHtml,
811
996
  searchDdgLite,
812
997
  searchExa,
998
+ searchGithub,
813
999
  searchPerplexity,
1000
+ searchPlatform,
1001
+ searchReddit,
814
1002
  searchSearxng,
1003
+ searchV2ex,
815
1004
  };
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.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.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {