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 +4 -4
- package/lib/index.js +90 -5
- package/lib/vertical.js +60 -0
- package/package.json +1 -1
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.
|
|
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.
|
|
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(/&/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
|
|
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.
|
|
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",
|