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 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.11),"检查更新"按钮直连 npm registry 对比最新版,有新版本时提示并可一键跳转
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站(公开 API,零依赖)
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.11), 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
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: "密钥保存在 settings.yaml(界面脱敏显示)。为某引擎填写密钥后即可使用。",
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: "Keys are stored in settings.yaml (redacted in the UI). Enter a key for an engine to use it.",
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.11";
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.11";
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 searchPlatform(platform, query, maxResults, signal) {
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 > 环境变量/credentials
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 discussions.",
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.11",
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",