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 +13 -0
- package/README.md +13 -0
- package/lib/index.js +189 -0
- 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
|
|
|
@@ -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.
|
|
4
|
-
"description": "Free web search for DeepSeek Harness:
|
|
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": {
|