dsh-free-search 0.4.11 → 0.4.13
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.md +19 -7
- package/lib/client.js +6 -5
- package/lib/index.js +132 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,11 +36,11 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
36
36
|
- **统一引擎回退** —— 任何引擎失败(付费/免费,缺 key/401/限流/网络)自动轮流尝试下一个引擎:首选引擎 → 其他引擎(exa/tavily/keenable 无 key 也会尝试,因为它们自带 keyless 免费额度)→ 剩余免费引擎,搜索永不直接失败;结果顶部注明实际生效的引擎(如 `Note: perplexity unavailable or failed, using exa.`)
|
|
37
37
|
- **时间过滤** —— `advanced_search` 工具支持 `timeRange`:固定档、自定义相对值、绝对日期三种形式(详见下方逻辑说明)
|
|
38
38
|
- **系统提示词注入** —— agent 知道当前用哪个引擎、哪些需要 key
|
|
39
|
-
- **版本号 + 检查更新** —— 设置卡片显示当前版本(v0.4.
|
|
39
|
+
- **版本号 + 检查更新** —— 设置卡片显示当前版本(v0.4.13),"检查更新"按钮直连 npm registry 对比最新版,有新版本时提示并可一键跳转
|
|
40
40
|
- **结果缓存** —— 相同查询(含引擎/时间过滤参数)5 分钟内命中缓存(LRU 50 条),防免费引擎限流、省付费额度;时长可在设置页 0-5 分钟自由配置(0 关闭)
|
|
41
41
|
- **免费标注** —— 设置页中免费引擎带绿色 `FREE` 徽章,付费引擎带橙色 `API KEY` 徽章
|
|
42
42
|
- **网页抓取(web_fetch)** —— 让 agent 抓取网页内容(官方 `dsh-web-fetch-http` provider,纯 JS,零额外依赖)
|
|
43
|
-
- **平台搜索(platform_search)** —— 搜 GitHub / V2EX / B
|
|
43
|
+
- **平台搜索(platform_search)** —— 搜 GitHub / V2EX / B站 / Reddit / Hacker News / Stack Overflow / 维基百科 / npm(公开 API,零依赖)
|
|
44
44
|
- **干净集成** —— 实现官方 `WebSearchProvider` seam 接口,与官方插件共存
|
|
45
45
|
|
|
46
46
|
### 引擎列表
|
|
@@ -99,6 +99,7 @@ dsh web
|
|
|
99
99
|
|
|
100
100
|
- **Search engine**:下拉框切换引擎,保存即生效
|
|
101
101
|
- **API keys**:为 Exa / Tavily / Keenable / Perplexity / DeepSeek 填写 key(密码框,保存后只显示"已配置")
|
|
102
|
+
- **推荐**:付费引擎 key 建议写入 harness 凭据中心 `~/.dsh/.credentials.yaml`(如 `DEEPSEEK_API_KEY: sk-...`,与官方 LLM provider 一致,一处管理所有 key)。插件读取优先级:凭据中心 > 设置页 > 环境变量,设置页填的 key 仅作为遗留兼容。
|
|
102
103
|
- **Test engine**:直测当前引擎可用性(不走回退链,付费引擎无 key 会明确报错)
|
|
103
104
|
- **Use Bing default**:把当前搜索引擎切回稳定的免费 Bing;`Discard` 只撤销尚未保存的编辑
|
|
104
105
|
- **Platform search**:勾选启用 GitHub / V2EX / Bilibili 平台搜索(`platform_search` 工具按此过滤)
|
|
@@ -205,8 +206,13 @@ Search engine test:
|
|
|
205
206
|
| `github` | GitHub 仓库搜索(API,免费无 key) |
|
|
206
207
|
| `v2ex` | V2EX 热门/相关主题 |
|
|
207
208
|
| `bilibili` | B站视频/内容搜索(公开接口) |
|
|
209
|
+
| `reddit` | Reddit 帖子/讨论搜索(公开 JSON API;部分网络环境可能被 Reddit 反爬拦截) |
|
|
210
|
+
| `hn` | Hacker News 技术社区讨论(Algolia 官方 API) |
|
|
211
|
+
| `stackoverflow` | Stack Overflow 技术问答(Stack Exchange 官方公开 API) |
|
|
212
|
+
| `wikipedia` | 维基百科词条(中文环境用 zh.wikipedia.org,`lang: en` 时切换 en.wikipedia.org) |
|
|
213
|
+
| `npm` | npm 包搜索(registry 官方 API) |
|
|
208
214
|
|
|
209
|
-
全部走公开 API
|
|
215
|
+
全部走公开 API,零外部依赖、无需任何 key,开箱即用。
|
|
210
216
|
|
|
211
217
|
### 本地引擎切换工具(tools/)
|
|
212
218
|
|
|
@@ -271,11 +277,11 @@ This plugin provides multiple free search engines with automatic fallback, compl
|
|
|
271
277
|
- **Unified Engine Fallback** — Any engine failure (paid or free, missing key, 401, rate limit, network error) automatically tries the next engine: the configured engine first, then other engines (exa/tavily/keenable are tried even without a key because they have built-in keyless quota), then the remaining free engines (Bing/AnySearch etc.) — with a note attached to the results naming the engine that actually served them (e.g. `Note: perplexity unavailable or failed, using exa.`). Search never fails outright.
|
|
272
278
|
- **Time Filtering** — The `advanced_search` tool supports `timeRange`: fixed tiers, custom relative values, or an absolute date (details below)
|
|
273
279
|
- **System Prompt Injection** — The agent is aware of the currently active engine and which engines require API keys
|
|
274
|
-
- **Version + Update Check** — The settings card shows the current version (v0.4.
|
|
280
|
+
- **Version + Update Check** — The settings card shows the current version (v0.4.13), and a "Check update" button queries the npm registry to compare against the latest release, prompting a one-click jump when a newer version exists
|
|
275
281
|
- **Result Caching** — Identical queries (same engine / time-filter args) hit an LRU cache (50 entries) for up to 5 minutes, protecting free engines from rate-limiting and saving paid quota; the TTL is configurable from 0-5 minutes in the settings UI (0 disables caching)
|
|
276
282
|
- **Visual Badges** — Free engines feature a green `FREE` badge, while paid engines show an orange `API KEY` badge in the settings UI
|
|
277
283
|
- **Webpage Fetching (`web_fetch`)** — Allows the agent to read full webpage contents (official `dsh-web-fetch-http` provider, pure JS, zero extra dependencies)
|
|
278
|
-
- **Platform Search (`platform_search`)** — Search GitHub / V2EX / Bilibili (public APIs, zero extra dependencies)
|
|
284
|
+
- **Platform Search (`platform_search`)** — Search GitHub / V2EX / Bilibili / Reddit / Hacker News / Stack Overflow / Wikipedia / npm (public APIs, zero extra dependencies)
|
|
279
285
|
- **Clean Integration** — Implements the official `WebSearchProvider` seam interface, coexisting seamlessly with official plugins
|
|
280
286
|
|
|
281
287
|
### Supported Engines
|
|
@@ -334,9 +340,10 @@ After installation, navigate to **Settings → Plugins → Configurable** tab
|
|
|
334
340
|
|
|
335
341
|
- **Search engine**: Select an engine from the dropdown; changes take effect immediately upon saving.
|
|
336
342
|
- **API keys**: Enter keys for Exa / Tavily / Keenable / Perplexity / DeepSeek (password fields; displayed as "configured" once saved).
|
|
343
|
+
- **Recommended**: store paid-engine keys in the harness credential center `~/.dsh/.credentials.yaml` (e.g. `DEEPSEEK_API_KEY: sk-...`, same as the official LLM providers — one place for all keys). Resolution order: credentials center > settings page > environment variable; the settings-page fields remain for backward compatibility.
|
|
337
344
|
- **Test engine**: Tests the selected engine directly (no fallback chain; paid engines without a key report an explicit error).
|
|
338
345
|
- **Use Bing default**: stage a switch back to the stable free Bing engine; `Discard` only cancels unsaved edits
|
|
339
|
-
- **Platform search**: check GitHub / V2EX / Bilibili to enable them for the `platform_search` tool (disabled platforms are skipped).
|
|
346
|
+
- **Platform search**: check platforms (GitHub / V2EX / Bilibili / Reddit / HN / Stack Overflow / Wikipedia / npm) to enable them for the `platform_search` tool (disabled platforms are skipped).
|
|
340
347
|
- **EN / 中文**: toggle the interface language (default Chinese).
|
|
341
348
|
|
|
342
349
|
<table align="center" style="border: none; border-collapse: collapse;">
|
|
@@ -440,8 +447,13 @@ Ask the agent to search specific platforms (e.g., *"Search GitHub for deepseek h
|
|
|
440
447
|
| `github` | GitHub repository search (public API, free, no key required) |
|
|
441
448
|
| `v2ex` | V2EX hot / relevant topics |
|
|
442
449
|
| `bilibili` | Bilibili video / content search (public API) |
|
|
450
|
+
| `reddit` | Reddit posts / discussions (public JSON API; may be blocked by Reddit anti-bot in some network environments) |
|
|
451
|
+
| `hn` | Hacker News tech community discussions (official Algolia API) |
|
|
452
|
+
| `stackoverflow` | Stack Overflow Q&A (official public Stack Exchange API) |
|
|
453
|
+
| `wikipedia` | Wikipedia articles (zh.wikipedia.org for Chinese; switches to en.wikipedia.org when `lang: en`) |
|
|
454
|
+
| `npm` | npm package search (registry official API) |
|
|
443
455
|
|
|
444
|
-
All platform searches rely on public endpoints with zero external dependencies and work out of the box.
|
|
456
|
+
All platform searches rely on public endpoints with zero external dependencies and no API keys — they work out of the box.
|
|
445
457
|
|
|
446
458
|
### Local Engine Switcher (`tools/`)
|
|
447
459
|
|
package/lib/client.js
CHANGED
|
@@ -83,7 +83,7 @@ window.__ModuleLoader__.load({
|
|
|
83
83
|
keenablePh: (c) => c ? "Keenable API 密钥(已配置)" : "Keenable API 密钥(可选,不填也可免费使用)",
|
|
84
84
|
perplexityPh: (c) => c ? "Perplexity API 密钥(已配置)" : "Perplexity API 密钥(pplx-...)",
|
|
85
85
|
deepseekPh: (c) => c ? "DeepSeek API 密钥(已配置)" : "DeepSeek API 密钥(sk-...)",
|
|
86
|
-
keysHint: "
|
|
86
|
+
keysHint: "密钥读取优先级:.credentials.yaml 凭据中心 > 这里 > 环境变量。推荐把 key 写进凭据中心(与官方 LLM 一致,一处管理)。",
|
|
87
87
|
platformSearch: "平台搜索(platform_search 工具)",
|
|
88
88
|
platformHint: "为 agent 的 platform_search 工具启用平台。禁用的平台会被跳过。",
|
|
89
89
|
cacheTtl: "结果缓存时长(分钟)",
|
|
@@ -124,7 +124,7 @@ window.__ModuleLoader__.load({
|
|
|
124
124
|
keenablePh: (c) => c ? "Keenable API key (configured)" : "Keenable API key (optional, free without)",
|
|
125
125
|
perplexityPh: (c) => c ? "Perplexity API key (configured)" : "Perplexity API key (pplx-...)",
|
|
126
126
|
deepseekPh: (c) => c ? "DeepSeek API key (configured)" : "DeepSeek API key (sk-...)",
|
|
127
|
-
keysHint: "
|
|
127
|
+
keysHint: "Key resolution: .credentials.yaml credential center > here > environment variables. Recommended: store keys in the credential center (same as official LLM providers, one place for all).",
|
|
128
128
|
platformSearch: "Platform search (platform_search tool)",
|
|
129
129
|
platformHint: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
|
|
130
130
|
cacheTtl: "Result cache TTL (minutes)",
|
|
@@ -155,7 +155,7 @@ window.__ModuleLoader__.load({
|
|
|
155
155
|
};
|
|
156
156
|
const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
|
|
157
157
|
// 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
|
|
158
|
-
const PLUGIN_VERSION = "0.4.
|
|
158
|
+
const PLUGIN_VERSION = "0.4.13";
|
|
159
159
|
const ENGINES = [
|
|
160
160
|
{ id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
|
|
161
161
|
{ id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
|
|
@@ -214,7 +214,7 @@ window.__ModuleLoader__.load({
|
|
|
214
214
|
const [keenableKey, setKeenableKey] = react.useState("");
|
|
215
215
|
const [perplexityKey, setPerplexityKey] = react.useState("");
|
|
216
216
|
const [deepseekKey, setDeepseekKey] = react.useState("");
|
|
217
|
-
const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit"]);
|
|
217
|
+
const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
|
|
218
218
|
const [cacheTtl, setCacheTtl] = react.useState(5);
|
|
219
219
|
const [keysConfigured, setKeysConfigured] = react.useState({});
|
|
220
220
|
const [lang, setLang] = react.useState("zh");
|
|
@@ -241,7 +241,7 @@ window.__ModuleLoader__.load({
|
|
|
241
241
|
setKeenableKey(v.keenableApiKey ?? "");
|
|
242
242
|
setPerplexityKey(v.perplexityApiKey ?? "");
|
|
243
243
|
setDeepseekKey(v.deepseekApiKey ?? "");
|
|
244
|
-
setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit"]);
|
|
244
|
+
setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
|
|
245
245
|
setCacheTtl(v.cacheTtl === undefined ? 5 : Math.min(Math.max(Number(v.cacheTtl) ?? 5, 0), 5));
|
|
246
246
|
// secrets 字段标记哪些 key 已配置(值被脱敏,仅显示"已配置")
|
|
247
247
|
const configured = {};
|
|
@@ -553,6 +553,7 @@ react_jsx_runtime.jsx("div", {
|
|
|
553
553
|
className: "dshfs-platforms",
|
|
554
554
|
children: [
|
|
555
555
|
["github", "GitHub"], ["v2ex", "V2EX"], ["bilibili", "Bilibili"], ["reddit", "Reddit"],
|
|
556
|
+
["hn", "Hacker News"], ["stackoverflow", "Stack Overflow"], ["wikipedia", "Wikipedia"], ["npm", "npm"],
|
|
556
557
|
].map(([id, label]) =>
|
|
557
558
|
react_jsx_runtime.jsx("label", {
|
|
558
559
|
className: "dshfs-platform",
|
package/lib/index.js
CHANGED
|
@@ -21,7 +21,7 @@ const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch"];
|
|
|
21
21
|
const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "perplexity", "deepseek-official"];
|
|
22
22
|
|
|
23
23
|
// 当前插件版本(发布时与 package.json 同步)
|
|
24
|
-
const PLUGIN_VERSION = "0.4.
|
|
24
|
+
const PLUGIN_VERSION = "0.4.13";
|
|
25
25
|
// 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
|
|
26
26
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
|
|
27
27
|
const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
|
|
@@ -488,12 +488,16 @@ async function searchExaMCP(query, maxResults, signal) {
|
|
|
488
488
|
}
|
|
489
489
|
//#endregion
|
|
490
490
|
|
|
491
|
-
//#region platform search (GitHub / V2EX / Bilibili / Reddit)
|
|
491
|
+
//#region platform search (GitHub / V2EX / Bilibili / Reddit / HN / StackOverflow / Wikipedia / npm)
|
|
492
492
|
const PLATFORMS = {
|
|
493
493
|
github: { name: "GitHub" },
|
|
494
494
|
v2ex: { name: "V2EX" },
|
|
495
495
|
bilibili: { name: "Bilibili" },
|
|
496
496
|
reddit: { name: "Reddit" },
|
|
497
|
+
hn: { name: "Hacker News" },
|
|
498
|
+
stackoverflow: { name: "Stack Overflow" },
|
|
499
|
+
wikipedia: { name: "Wikipedia" },
|
|
500
|
+
npm: { name: "npm" },
|
|
497
501
|
};
|
|
498
502
|
|
|
499
503
|
async function searchGithub(query, maxResults, signal) {
|
|
@@ -590,7 +594,102 @@ async function searchReddit(query, maxResults, signal) {
|
|
|
590
594
|
};
|
|
591
595
|
}
|
|
592
596
|
|
|
593
|
-
async function
|
|
597
|
+
async function searchHackerNews(query, maxResults, signal) {
|
|
598
|
+
const response = await fetch(
|
|
599
|
+
`https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(query)}&hitsPerPage=${maxResults ?? 5}`,
|
|
600
|
+
{
|
|
601
|
+
headers: { "user-agent": USER_AGENT, accept: "application/json" },
|
|
602
|
+
...(signal !== undefined ? { signal } : {}),
|
|
603
|
+
}
|
|
604
|
+
);
|
|
605
|
+
if (!response.ok) throw new Error(`Hacker News API error (HTTP ${response.status})`);
|
|
606
|
+
const data = await response.json();
|
|
607
|
+
return {
|
|
608
|
+
sources: (data.hits ?? [])
|
|
609
|
+
.filter((h) => h.title || h.story_title)
|
|
610
|
+
.map((h) => ({
|
|
611
|
+
// 有外链用外链,纯讨论帖用 HN 讨论页
|
|
612
|
+
url: h.url ?? `https://news.ycombinator.com/item?id=${h.objectID}`,
|
|
613
|
+
title: h.title ?? h.story_title,
|
|
614
|
+
...((h.points !== undefined && h.points !== null) || (h.num_comments !== undefined && h.num_comments !== null)
|
|
615
|
+
? { snippet: `HN discussion · ${h.points ?? 0} points · ${h.num_comments ?? 0} comments` }
|
|
616
|
+
: {}),
|
|
617
|
+
})),
|
|
618
|
+
truncated: false,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async function searchStackOverflow(query, maxResults, signal) {
|
|
623
|
+
const response = await fetch(
|
|
624
|
+
`https://api.stackexchange.com/2.3/search/advanced?order=desc&sort=relevance&q=${encodeURIComponent(query)}&site=stackoverflow&pagesize=${maxResults ?? 5}&filter=!nNPvSNVZJS`,
|
|
625
|
+
{
|
|
626
|
+
headers: { "user-agent": USER_AGENT, accept: "application/json" },
|
|
627
|
+
...(signal !== undefined ? { signal } : {}),
|
|
628
|
+
}
|
|
629
|
+
);
|
|
630
|
+
if (!response.ok) throw new Error(`Stack Exchange API error (HTTP ${response.status})`);
|
|
631
|
+
const data = await response.json();
|
|
632
|
+
if (data.error_message) throw new Error(`Stack Exchange API error: ${data.error_message}`);
|
|
633
|
+
return {
|
|
634
|
+
sources: (data.items ?? []).map((it) => ({
|
|
635
|
+
url: it.link,
|
|
636
|
+
title: it.title,
|
|
637
|
+
...(it.score !== undefined || it.answer_count !== undefined
|
|
638
|
+
? { snippet: `${it.is_answered ? "✓ answered" : "unanswered"} · score ${it.score ?? 0} · ${it.answer_count ?? 0} answers` }
|
|
639
|
+
: {}),
|
|
640
|
+
})),
|
|
641
|
+
truncated: false,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async function searchWikipedia(query, maxResults, signal, lang) {
|
|
646
|
+
const host = lang === "en" ? "en.wikipedia.org" : "zh.wikipedia.org";
|
|
647
|
+
const response = await fetch(
|
|
648
|
+
`https://${host}/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&format=json&srlimit=${maxResults ?? 5}`,
|
|
649
|
+
{
|
|
650
|
+
headers: { "user-agent": USER_AGENT, accept: "application/json" },
|
|
651
|
+
...(signal !== undefined ? { signal } : {}),
|
|
652
|
+
}
|
|
653
|
+
);
|
|
654
|
+
if (!response.ok) throw new Error(`Wikipedia API error (HTTP ${response.status})`);
|
|
655
|
+
const data = await response.json();
|
|
656
|
+
return {
|
|
657
|
+
sources: (data.query?.search ?? []).map((s) => ({
|
|
658
|
+
url: `https://${host}/wiki/${encodeURIComponent(String(s.title).replace(/ /g, "_"))}`,
|
|
659
|
+
title: s.title,
|
|
660
|
+
// snippet 含 <span class="searchmatch"> 高亮标签,剥掉
|
|
661
|
+
...(s.snippet ? { snippet: stripTags(s.snippet).slice(0, 200) } : {}),
|
|
662
|
+
})),
|
|
663
|
+
truncated: false,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
async function searchNpm(query, maxResults, signal) {
|
|
668
|
+
const response = await fetch(
|
|
669
|
+
`https://registry.npmjs.com/-/v1/search?text=${encodeURIComponent(query)}&size=${maxResults ?? 5}`,
|
|
670
|
+
{
|
|
671
|
+
headers: { "user-agent": USER_AGENT, accept: "application/json" },
|
|
672
|
+
...(signal !== undefined ? { signal } : {}),
|
|
673
|
+
}
|
|
674
|
+
);
|
|
675
|
+
if (!response.ok) throw new Error(`npm registry API error (HTTP ${response.status})`);
|
|
676
|
+
const data = await response.json();
|
|
677
|
+
return {
|
|
678
|
+
sources: (data.objects ?? [])
|
|
679
|
+
.map((o) => o.package)
|
|
680
|
+
.filter((p) => p && p.name)
|
|
681
|
+
.map((p) => ({
|
|
682
|
+
url: p.links?.npm ?? `https://www.npmjs.com/package/${p.name}`,
|
|
683
|
+
title: p.name,
|
|
684
|
+
...((p.description || p.version)
|
|
685
|
+
? { snippet: `v${p.version ?? "?"}${p.description ? ` — ${String(p.description).slice(0, 160)}` : ""}` }
|
|
686
|
+
: {}),
|
|
687
|
+
})),
|
|
688
|
+
truncated: false,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async function searchPlatform(platform, query, maxResults, signal, lang) {
|
|
594
693
|
switch (platform) {
|
|
595
694
|
case "github":
|
|
596
695
|
return searchGithub(query, maxResults, signal);
|
|
@@ -600,6 +699,14 @@ async function searchPlatform(platform, query, maxResults, signal) {
|
|
|
600
699
|
return searchBilibili(query, maxResults, signal);
|
|
601
700
|
case "reddit":
|
|
602
701
|
return searchReddit(query, maxResults, signal);
|
|
702
|
+
case "hn":
|
|
703
|
+
return searchHackerNews(query, maxResults, signal);
|
|
704
|
+
case "stackoverflow":
|
|
705
|
+
return searchStackOverflow(query, maxResults, signal);
|
|
706
|
+
case "wikipedia":
|
|
707
|
+
return searchWikipedia(query, maxResults, signal, lang);
|
|
708
|
+
case "npm":
|
|
709
|
+
return searchNpm(query, maxResults, signal);
|
|
603
710
|
default:
|
|
604
711
|
throw new Error(`unknown platform: ${platform}`);
|
|
605
712
|
}
|
|
@@ -1221,7 +1328,7 @@ const Config = z.object({
|
|
|
1221
1328
|
region: z.string(),
|
|
1222
1329
|
bingMarket: z.string().default("zh-CN"),
|
|
1223
1330
|
searxngInstances: z.array(z.string()),
|
|
1224
|
-
platforms: z.array(z.string()).default(["github", "v2ex", "bilibili", "reddit"]),
|
|
1331
|
+
platforms: z.array(z.string()).default(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]),
|
|
1225
1332
|
exaApiKey: z.string().role("secret"),
|
|
1226
1333
|
tavilyApiKey: z.string().role("secret"),
|
|
1227
1334
|
keenableApiKey: z.string().role("secret"),
|
|
@@ -1240,16 +1347,16 @@ function apply(ctx, config) {
|
|
|
1240
1347
|
// 单 query 结果缓存(provider.search 内闭包持有):LRU 50 条 / TTL 可配置
|
|
1241
1348
|
const searchCache = new Map(); // key -> { value, expiresAt }
|
|
1242
1349
|
|
|
1243
|
-
// key 优先级:settings 的 free-search.<x>ApiKey
|
|
1350
|
+
// key 优先级:credentials(.credentials.yaml 凭据中心,官方推荐)> settings 的 free-search.<x>ApiKey(遗留兼容)> 环境变量
|
|
1244
1351
|
const resolveApiKey = async (envName, settingsKey) => {
|
|
1245
|
-
const cfg = current();
|
|
1246
|
-
if (settingsKey && cfg[settingsKey]) return cfg[settingsKey];
|
|
1247
1352
|
if (credentials) {
|
|
1248
1353
|
try {
|
|
1249
1354
|
const resolved = await credentials.resolve(envName);
|
|
1250
1355
|
if (resolved?.value) return resolved.value;
|
|
1251
1356
|
} catch {}
|
|
1252
1357
|
}
|
|
1358
|
+
const cfg = current();
|
|
1359
|
+
if (settingsKey && cfg[settingsKey]) return cfg[settingsKey];
|
|
1253
1360
|
return process.env[envName] ?? "";
|
|
1254
1361
|
};
|
|
1255
1362
|
|
|
@@ -1458,6 +1565,15 @@ function apply(ctx, config) {
|
|
|
1458
1565
|
|
|
1459
1566
|
ctx.web.registerSearchProvider(provider);
|
|
1460
1567
|
|
|
1568
|
+
// 运行时兜底:DSH 0.1.1+ 中 profile patch 的 config 会整体覆盖 bundle patch 的 config,
|
|
1569
|
+
// 用户的 `- id: web` patch(如只设 fetchProvider)会静默抹掉 searchProvider,导致回退 DeepSeek 官方搜索。
|
|
1570
|
+
// 这里在 provider 注册后检查:未指向任何 provider(undefined)时自动接管为本插件;
|
|
1571
|
+
// 用户显式配置了其他 provider 则不动。
|
|
1572
|
+
if (!ctx.web.searchProviderId) {
|
|
1573
|
+
ctx.web.searchProviderId = provider.id;
|
|
1574
|
+
logger.info(`free-search: web.searchProvider was unset (patch override or missing config), taking over as "${provider.id}"`);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1461
1577
|
// 测试工具:让 agent 逐个测试所有搜索引擎,报告可用性
|
|
1462
1578
|
const runEngineTest = async (engine, query, timeRange) => {
|
|
1463
1579
|
const cfg = current();
|
|
@@ -1616,11 +1732,11 @@ function apply(ctx, config) {
|
|
|
1616
1732
|
defineTool({
|
|
1617
1733
|
name: "platform_search",
|
|
1618
1734
|
description:
|
|
1619
|
-
"Search a specific platform (GitHub / V2EX / Bilibili / Reddit) for a query. Returns source URLs with titles and snippets. Use this when the user asks about repos, code, forum threads, videos, or
|
|
1735
|
+
"Search a specific platform (GitHub / V2EX / Bilibili / Reddit / Hacker News / Stack Overflow / Wikipedia / npm) for a query. Returns source URLs with titles and snippets. Use this when the user asks about repos, code, forum threads, videos, discussions, Q&A, encyclopedia entries, or packages.",
|
|
1620
1736
|
parameters: {
|
|
1621
1737
|
platform: {
|
|
1622
1738
|
type: "string",
|
|
1623
|
-
description: "Platform to search: github, v2ex, bilibili, reddit",
|
|
1739
|
+
description: "Platform to search: github, v2ex, bilibili, reddit, hn, stackoverflow, wikipedia, npm",
|
|
1624
1740
|
},
|
|
1625
1741
|
query: {
|
|
1626
1742
|
type: "string",
|
|
@@ -1662,14 +1778,14 @@ function apply(ctx, config) {
|
|
|
1662
1778
|
throw new Error(`unknown platform "${platform}" - use one of: ${Object.keys(PLATFORMS).join(", ")}`);
|
|
1663
1779
|
}
|
|
1664
1780
|
// 平台开关:settings 里禁用某平台时,工具明确告知
|
|
1665
|
-
const enabled = current().platforms ?? ["github", "v2ex", "bilibili", "reddit"];
|
|
1781
|
+
const enabled = current().platforms ?? ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"];
|
|
1666
1782
|
if (!enabled.includes(platform)) {
|
|
1667
1783
|
throw new Error(
|
|
1668
1784
|
`platform "${platform}" is disabled in Free Search settings - enable it in Settings > Plugins > Free Search to use it`
|
|
1669
1785
|
);
|
|
1670
1786
|
}
|
|
1671
1787
|
const limit = Math.min(args.maxResults ?? 5, 10);
|
|
1672
|
-
const result = await searchPlatform(platform, args.query, limit);
|
|
1788
|
+
const result = await searchPlatform(platform, args.query, limit, undefined, current().lang);
|
|
1673
1789
|
// lossless JSON 不允许 undefined 字段:剔除缺失字段
|
|
1674
1790
|
const sources = (result.sources ?? []).map((s) => {
|
|
1675
1791
|
const source = {};
|
|
@@ -1821,7 +1937,7 @@ function apply(ctx, config) {
|
|
|
1821
1937
|
"",
|
|
1822
1938
|
"When the user wants results from a specific time window (e.g. 'last week', 'this month', 'last 3 days'), use the advanced_search tool with timeRange. Fixed tiers: day|week|month|year. Custom: 12h, 3d, 2mo, 1y, or an absolute date like 2026-07-01.",
|
|
1823
1939
|
"",
|
|
1824
|
-
"For platform-specific searches (GitHub repos, V2EX threads, Bilibili videos), use the platform_search tool with platform: github|v2ex|bilibili.",
|
|
1940
|
+
"For platform-specific searches (GitHub repos, V2EX threads, Bilibili videos, Reddit posts, Hacker News discussions, Stack Overflow questions, Wikipedia articles, npm packages), use the platform_search tool with platform: github|v2ex|bilibili|reddit|hn|stackoverflow|wikipedia|npm.",
|
|
1825
1941
|
"",
|
|
1826
1942
|
"The user can switch the search engine themselves by typing /free-search-engine in the chat — it opens a picker to choose an engine, just like the settings page. This changes the preferred engine; search still falls back to other engines automatically if it fails. You should not switch engines on your own; let the user decide.",
|
|
1827
1943
|
].join("\n"),
|
|
@@ -1868,13 +1984,17 @@ export {
|
|
|
1868
1984
|
searchExa,
|
|
1869
1985
|
searchExaMCP,
|
|
1870
1986
|
searchGithub,
|
|
1987
|
+
searchHackerNews,
|
|
1871
1988
|
searchKeenable,
|
|
1872
1989
|
searchKeenableMCP,
|
|
1873
1990
|
searchKeenableREST,
|
|
1991
|
+
searchNpm,
|
|
1874
1992
|
searchPerplexity,
|
|
1875
1993
|
searchPlatform,
|
|
1876
1994
|
searchReddit,
|
|
1877
1995
|
searchSearxng,
|
|
1996
|
+
searchStackOverflow,
|
|
1878
1997
|
searchTavily,
|
|
1879
1998
|
searchV2ex,
|
|
1999
|
+
searchWikipedia,
|
|
1880
2000
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-free-search",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.13",
|
|
4
4
|
"description": "Free web search for DeepSeek Harness: 10 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable keyless) + time filtering + platform search + web_fetch, with web settings UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|