dsh-free-search 0.8.10 → 0.9.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/lib/client.js CHANGED
@@ -376,7 +376,7 @@ window.__ModuleLoader__.load({
376
376
  };
377
377
  const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
378
378
  // 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致;跟随上游 v0.7.5)
379
- const PLUGIN_VERSION = "0.8.10";
379
+ const PLUGIN_VERSION = "0.9.0";
380
380
  const ENGINES = [
381
381
  { id: "auto", label: "Auto · 智能路由", badge: "AUTO", link: null },
382
382
  { id: "multi", label: "Multi Search · 多引擎", badge: "MULTI", link: null },
@@ -604,7 +604,7 @@ window.__ModuleLoader__.load({
604
604
  const [zhihuKey, setZhihuKey] = react.useState("");
605
605
  const [tinyfishKey, setTinyfishKey] = react.useState("");
606
606
  const [jinaKey, setJinaKey] = react.useState("");
607
- const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm", "youtube", "vimeo"]);
607
+ const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm", "youtube", "vimeo", "juejin", "douban", "qiita"]);
608
608
  const [cacheTtl, setCacheTtl] = react.useState(5);
609
609
  const [multiMaxEngines, setMultiMaxEngines] = react.useState(0);
610
610
  const [multiMinSources, setMultiMinSources] = react.useState(1);
@@ -713,7 +713,7 @@ window.__ModuleLoader__.load({
713
713
  setZhihuKey(v.zhihuApiKey ?? "");
714
714
  setTinyfishKey(v.tinyfishApiKey ?? "");
715
715
  setJinaKey(v.jinaApiKey ?? "");
716
- setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm", "youtube", "vimeo"]);
716
+ setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm", "youtube", "vimeo", "juejin", "douban", "qiita"]);
717
717
  setCacheTtl(v.cacheTtl === undefined ? 5 : Math.min(Math.max(Number(v.cacheTtl) ?? 5, 0), 5));
718
718
  // #69:multi 上限 0 = 不限制(旧行为);目标成功源数下限 1
719
719
  setMultiMaxEngines(v.multiMaxEngines === undefined ? 0 : Math.max(Number(v.multiMaxEngines) ?? 0, 0));
@@ -1554,7 +1554,7 @@ react_jsx_runtime.jsx("div", {
1554
1554
  children: [
1555
1555
  ["github", "GitHub"], ["v2ex", "V2EX"], ["bilibili", "Bilibili"], ["reddit", "Reddit"],
1556
1556
  ["hn", "Hacker News"], ["stackoverflow", "Stack Overflow"], ["wikipedia", "Wikipedia"], ["npm", "npm"],
1557
- ["youtube", "YouTube"], ["vimeo", "Vimeo"],
1557
+ ["youtube", "YouTube"], ["vimeo", "Vimeo"], ["juejin", "掘金"], ["douban", "豆瓣"], ["qiita", "Qiita"],
1558
1558
  ].map(([id, label]) =>
1559
1559
  react_jsx_runtime.jsx("label", {
1560
1560
  className: "dshfs-platform",
package/lib/index.js CHANGED
@@ -244,7 +244,7 @@ function routeEngines(query, { timeRange, disabledEngines = [], fallbackOrder =
244
244
  }
245
245
 
246
246
  // 当前插件版本(发布时与 package.json 同步;跟随上游 v0.7.5,不在 PR 里 bump)
247
- const PLUGIN_VERSION = "0.8.10";
247
+ const PLUGIN_VERSION = "0.9.0";
248
248
  // 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
249
249
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
250
250
  const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
@@ -847,6 +847,9 @@ const PLATFORMS = {
847
847
  npm: { name: "npm" },
848
848
  youtube: { name: "YouTube" },
849
849
  vimeo: { name: "Vimeo" },
850
+ juejin: { name: "掘金" },
851
+ douban: { name: "豆瓣" },
852
+ qiita: { name: "Qiita" },
850
853
  };
851
854
 
852
855
  async function searchGithub(query, maxResults, signal) {
@@ -1038,6 +1041,82 @@ async function searchNpm(query, maxResults, signal) {
1038
1041
  };
1039
1042
  }
1040
1043
 
1044
+ async function searchJuejin(query, maxResults, signal) {
1045
+ const response = await fetch(
1046
+ `https://api.juejin.cn/search_api/v1/search?query=${encodeURIComponent(query)}&id_type=0&cursor=0&limit=${maxResults ?? 5}`,
1047
+ {
1048
+ headers: { "user-agent": USER_AGENT, accept: "application/json" },
1049
+ ...(signal !== undefined ? { signal } : {}),
1050
+ }
1051
+ );
1052
+ if (!response.ok) throw new Error(`Juejin API error (HTTP ${response.status})`);
1053
+ const data = await response.json();
1054
+ if (data.err_no !== 0) throw new Error(`Juejin API error: ${data.err_msg ?? data.err_no}`);
1055
+ return {
1056
+ sources: (data.data ?? [])
1057
+ .map((x) => x.result_model?.article_info)
1058
+ .filter((a) => a && a.article_id)
1059
+ .map((a) => ({
1060
+ url: `https://juejin.cn/post/${a.article_id}`,
1061
+ title: a.title ?? "",
1062
+ ...(a.brief_content ? { snippet: String(a.brief_content).slice(0, 200) } : {}),
1063
+ })),
1064
+ truncated: false,
1065
+ };
1066
+ }
1067
+
1068
+ // 豆瓣搜索是 HTML(无公开 JSON API);结果块 `<div class="result">`,链接是 link2 跳转,需解码 url 参数。
1069
+ // 页面改版即失效(返回空数组),与 so360/sogou 同款取舍。
1070
+ async function searchDouban(query, maxResults, signal) {
1071
+ const response = await fetch(`https://www.douban.com/search?q=${encodeURIComponent(query)}&cat=1001`, {
1072
+ headers: {
1073
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
1074
+ "accept-language": "zh-CN,zh;q=0.9",
1075
+ accept: "text/html,*/*",
1076
+ },
1077
+ ...(signal !== undefined ? { signal } : {}),
1078
+ });
1079
+ if (!response.ok) throw new Error(`Douban error (HTTP ${response.status})`);
1080
+ const html = await response.text();
1081
+ const sources = [];
1082
+ for (const block of html.split('<div class="result">').slice(1)) {
1083
+ const h3 = block.match(/<h3>([\s\S]*?)<\/h3>/);
1084
+ if (!h3) continue;
1085
+ const a = h3[1].match(/<a[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/);
1086
+ if (!a) continue;
1087
+ let url = a[1].replace(/&amp;/g, "&");
1088
+ const wrapped = url.match(/[?&]url=([^&]+)/);
1089
+ if (wrapped) { try { url = decodeURIComponent(wrapped[1]); } catch { /* keep the raw link */ } }
1090
+ const title = a[2].replace(/<[^>]+>/g, "").trim();
1091
+ const info = block.match(/<div class="info">([\s\S]*?)<\/div>/);
1092
+ const snippet = info ? info[1].replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 200) : "";
1093
+ if (!url.startsWith("http") || !title) continue;
1094
+ sources.push({ url, title, ...(snippet ? { snippet } : {}) });
1095
+ if (sources.length >= (maxResults ?? 5)) break;
1096
+ }
1097
+ return { sources, truncated: false };
1098
+ }
1099
+
1100
+ async function searchQiita(query, maxResults, signal) {
1101
+ const response = await fetch(
1102
+ `https://qiita.com/api/v2/items?query=${encodeURIComponent(query)}&per_page=${maxResults ?? 5}`,
1103
+ {
1104
+ headers: { "user-agent": USER_AGENT, accept: "application/json" },
1105
+ ...(signal !== undefined ? { signal } : {}),
1106
+ }
1107
+ );
1108
+ if (!response.ok) throw new Error(`Qiita API error (HTTP ${response.status})`);
1109
+ const data = await response.json();
1110
+ return {
1111
+ sources: (Array.isArray(data) ? data : []).map((it) => ({
1112
+ url: it.url,
1113
+ title: it.title ?? "",
1114
+ ...(it.body ? { snippet: String(it.body).replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 200) } : {}),
1115
+ })),
1116
+ truncated: false,
1117
+ };
1118
+ }
1119
+
1041
1120
  async function searchPlatform(platform, query, maxResults, signal, lang) {
1042
1121
  switch (platform) {
1043
1122
  case "github":
@@ -1060,6 +1139,12 @@ async function searchPlatform(platform, query, maxResults, signal, lang) {
1060
1139
  return searchYoutube(query, maxResults, signal);
1061
1140
  case "vimeo":
1062
1141
  return searchVimeo(query, maxResults, signal);
1142
+ case "juejin":
1143
+ return searchJuejin(query, maxResults, signal);
1144
+ case "douban":
1145
+ return searchDouban(query, maxResults, signal);
1146
+ case "qiita":
1147
+ return searchQiita(query, maxResults, signal);
1063
1148
  default:
1064
1149
  throw new Error(`unknown platform: ${platform}`);
1065
1150
  }
@@ -3090,7 +3175,7 @@ const Config = z.object({
3090
3175
  bingMarket: z.string().default("zh-CN").volatile(),
3091
3176
  safeSearch: z.string().default("off").volatile(),
3092
3177
  searxngInstances: z.array(z.string()).volatile(),
3093
- platforms: z.array(z.string()).default(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm", "youtube", "vimeo"]).volatile(),
3178
+ platforms: z.array(z.string()).default(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm", "youtube", "vimeo", "juejin", "douban", "qiita"]).volatile(),
3094
3179
  anysearchApiKey: z.string().role("secret").volatile(),
3095
3180
  exaApiKey: z.string().role("secret").volatile(),
3096
3181
  tavilyApiKey: z.string().role("secret").volatile(),
@@ -4336,11 +4421,11 @@ function apply(ctx, config) {
4336
4421
  defineTool({
4337
4422
  name: "platform_search",
4338
4423
  description:
4339
- "Search a specific platform (GitHub / V2EX / Bilibili / Reddit / Hacker News / Stack Overflow / Wikipedia / npm / YouTube / Vimeo) 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.",
4424
+ "Search a specific platform (GitHub / V2EX / Bilibili / Reddit / Hacker News / Stack Overflow / Wikipedia / npm / YouTube / Vimeo / 掘金 / 豆瓣 / Qiita) 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, packages, or Chinese/Japanese tech blogs.",
4340
4425
  parameters: {
4341
4426
  platform: {
4342
4427
  type: "string",
4343
- description: "Platform to search: github, v2ex, bilibili, reddit, hn, stackoverflow, wikipedia, npm, youtube, vimeo",
4428
+ description: "Platform to search: github, v2ex, bilibili, reddit, hn, stackoverflow, wikipedia, npm, youtube, vimeo, juejin, douban, qiita",
4344
4429
  },
4345
4430
  query: {
4346
4431
  type: "string",
@@ -4382,7 +4467,7 @@ function apply(ctx, config) {
4382
4467
  throw new Error(`unknown platform "${platform}" - use one of: ${Object.keys(PLATFORMS).join(", ")}`);
4383
4468
  }
4384
4469
  // 平台开关:settings 里禁用某平台时,工具明确告知
4385
- const enabled = current().platforms ?? ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm", "youtube", "vimeo"];
4470
+ const enabled = current().platforms ?? ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm", "youtube", "vimeo", "juejin", "douban", "qiita"];
4386
4471
  if (!enabled.includes(platform)) {
4387
4472
  throw new Error(
4388
4473
  `platform "${platform}" is disabled in Free Search settings - enable it in Settings > Plugins > Free Search to use it`
package/lib/vertical.js CHANGED
@@ -578,6 +578,64 @@ const sogou = {
578
578
  },
579
579
  };
580
580
 
581
+ // ---------------- P3-8 法律 ----------------
582
+ // 四个免 key 法律源(2026-10 实测):Federal Register / CourtListener / openlegaldata / legislation.gov.uk。
583
+ // 中国与香港的官方库(flk.npc.gov.cn、elegislation.gov.hk)是 SPA + 反爬,没有可直连的 JSON API,故不接。
584
+
585
+ const federalregister = {
586
+ desc: "US Federal Register: US federal rules, proposed rules and notices by keyword (keyless JSON)",
587
+ async run(q) {
588
+ const d = await getJson(
589
+ `https://www.federalregister.gov/api/v1/documents.json?per_page=3&order=relevance&conditions[term]=${encodeURIComponent(q)}` +
590
+ `&fields[]=title&fields[]=html_url&fields[]=publication_date&fields[]=type`,
591
+ );
592
+ return (d.results || []).map((x) => src(x.html_url, x.title, x.type || "", day(x.publication_date)));
593
+ },
594
+ };
595
+
596
+ const courtlistener = {
597
+ desc: "US CourtListener: US case-law opinions by keyword (keyless search API)",
598
+ async run(q) {
599
+ const d = await getJson(`https://www.courtlistener.com/api/rest/v4/search/?q=${encodeURIComponent(q)}&type=o&page_size=3`);
600
+ return (d.results || [])
601
+ .map((r) => src(
602
+ r.absolute_url ? `https://www.courtlistener.com${r.absolute_url}` : "",
603
+ r.caseName || "",
604
+ [r.court, r.docketNumber ? `No. ${r.docketNumber}` : "", r.citation?.[0] || ""].filter(Boolean).join(" · "),
605
+ day(r.dateFiled),
606
+ ))
607
+ .filter((s) => s.url);
608
+ },
609
+ };
610
+
611
+ const openlegaldata = {
612
+ desc: "openlegaldata.io: German court decisions by keyword (keyless JSON; query goes in ?text=)",
613
+ async run(q) {
614
+ const d = await getJson(`https://de.openlegaldata.io/api/cases/search/?text=${encodeURIComponent(q)}&format=json&limit=3`);
615
+ return (d.results || []).map((r) => src(
616
+ `https://de.openlegaldata.io/case/${r.slug || r.id}/`,
617
+ `${r.court || "?"} · ${r.decision_type || r.slug || ""}`.trim(),
618
+ htmlToText(String(r.snippets?.[0]?.text || "")).slice(0, 220),
619
+ day(r.date),
620
+ ));
621
+ },
622
+ };
623
+
624
+ const uklegislation = {
625
+ desc: "UK legislation.gov.uk: UK Acts and statutory instruments by title keyword (keyless Atom feed)",
626
+ async run(q) {
627
+ const r = await httpsJson("GET", `https://www.legislation.gov.uk/all/data.feed?title=${encodeURIComponent(q)}`, { headers: { ...UA, Accept: "application/atom+xml, application/xml, text/xml, */*" }, timeoutMs: 25000 });
628
+ if (r.status !== 200) throw new Error(`HTTP ${r.status}`);
629
+ return [...r.text.matchAll(/<entry>([\s\S]*?)<\/entry>/g)].slice(0, 3).map(([e]) => {
630
+ const pick = (t) => { const x = e.match(new RegExp(`<${t}[^>]*>([\\s\\S]*?)</${t}>`)); return x ? htmlToText(x[1]) : ""; };
631
+ const links = [...e.matchAll(/<link\b[^>]*href="([^"]+)"/g)].map((m) => m[1]);
632
+ const link = (links.find((u) => !u.includes("/id/")) || links[0] || "").replace(/^http:/, "https:");
633
+ const updated = pick("updated");
634
+ return src(link, pick("title"), "", updated ? updated.slice(0, 10) : "");
635
+ }).filter((s) => s.url && s.title);
636
+ },
637
+ };
638
+
581
639
  // ---------------- 工具注册 ----------------
582
640
 
583
641
  export const ENGINES = {
@@ -593,6 +651,8 @@ export const ENGINES = {
593
651
  worldbank, wikidata, holidays, worldclock, openmeteo, rdap, gleif,
594
652
  // 新闻
595
653
  googlenews, gdelt, aihot,
654
+ // 法律
655
+ federalregister, courtlistener, openlegaldata, uk_legislation: uklegislation,
596
656
  // 中文站
597
657
  so360, sogou,
598
658
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-free-search",
3
- "version": "0.8.10",
3
+ "version": "0.9.0",
4
4
  "description": "Free web search for DeepSeek Harness: 24 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable/Firecrawl/Parallel keyless; Zhihu global/site via MCP and Perplexity/SerpBase/DeepSeek/You.com/Baidu/Kimi/Aliyun/Doubao with key; OpenAI/Gemini/Claude model built-in search, explicit-only) + auto smart routing + global engine enable/disable & fallback priority + editable zh/en language pools + multi mode (parallel full language-pool merge) + multi_search cross-source tool + failure-aware fallback (quota/auth cooldown, transient retry, fallbackOn) + time filtering + platform search + domain blocklist (blockedDomains) + web_fetch, with web settings UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",