dsh-free-search 0.8.4 → 0.8.5
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 +1 -1
- package/lib/fetch_provider.js +117 -5
- package/lib/index.js +1 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -355,7 +355,7 @@ window.__ModuleLoader__.load({
|
|
|
355
355
|
};
|
|
356
356
|
const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
|
|
357
357
|
// 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致;跟随上游 v0.7.5)
|
|
358
|
-
const PLUGIN_VERSION = "0.8.
|
|
358
|
+
const PLUGIN_VERSION = "0.8.5";
|
|
359
359
|
const ENGINES = [
|
|
360
360
|
{ id: "auto", label: "Auto · 智能路由", badge: "AUTO", link: null },
|
|
361
361
|
{ id: "multi", label: "Multi Search · 多引擎", badge: "MULTI", link: null },
|
package/lib/fetch_provider.js
CHANGED
|
@@ -6,12 +6,78 @@
|
|
|
6
6
|
// ponytail: 无服务级令牌桶——web_fetch 为单发用户/agent 驱动调用,非扇出;若未来批量调用再引入共享调度。
|
|
7
7
|
|
|
8
8
|
import https from "node:https";
|
|
9
|
+
import zlib from "node:zlib";
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
9
12
|
import { callExa, callFirecrawl, callCrawl4ai } from "./fetch_upstream.js";
|
|
10
13
|
|
|
11
14
|
const T1_MIN_CHARS = 300;
|
|
12
15
|
const TEXTUAL = /text\/|json|xml|javascript|application\/json/i;
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
// ---------- T1.5:系统浏览器无头渲染(执行 JS 过挑战;无浏览器/失败/超时则静默跳过) ----------
|
|
18
|
+
const BROWSER_CANDIDATES = [
|
|
19
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
20
|
+
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
21
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
22
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
23
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
24
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
25
|
+
];
|
|
26
|
+
let cachedBrowserPath; // undefined = 未检测;null = 无浏览器
|
|
27
|
+
function findSystemBrowser() {
|
|
28
|
+
if (cachedBrowserPath !== undefined) return cachedBrowserPath;
|
|
29
|
+
cachedBrowserPath = null;
|
|
30
|
+
for (const p of BROWSER_CANDIDATES) {
|
|
31
|
+
try {
|
|
32
|
+
if (existsSync(p)) {
|
|
33
|
+
cachedBrowserPath = p;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// 忽略检测错误
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return cachedBrowserPath;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function browserDumpDom(browserPath, url, { timeoutMs = 20000, signal } = {}) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
let child;
|
|
46
|
+
try {
|
|
47
|
+
child = spawn(browserPath, ["--headless=new", "--disable-gpu", "--no-first-run", "--dump-dom", "--virtual-time-budget=10000", url], {
|
|
48
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
49
|
+
windowsHide: true,
|
|
50
|
+
});
|
|
51
|
+
} catch {
|
|
52
|
+
resolve(null);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let out = "";
|
|
56
|
+
let done = false;
|
|
57
|
+
const finish = (ok) => {
|
|
58
|
+
if (done) return;
|
|
59
|
+
done = true;
|
|
60
|
+
try {
|
|
61
|
+
child.kill();
|
|
62
|
+
} catch {
|
|
63
|
+
// 忽略
|
|
64
|
+
}
|
|
65
|
+
resolve(ok && out.length > 0 ? out : null);
|
|
66
|
+
};
|
|
67
|
+
child.stdout.on("data", (c) => {
|
|
68
|
+
if (out.length < 5 * 1024 * 1024) out += c.toString("utf8");
|
|
69
|
+
});
|
|
70
|
+
child.on("error", () => finish(false));
|
|
71
|
+
child.on("exit", () => finish(true));
|
|
72
|
+
setTimeout(() => finish(false), timeoutMs);
|
|
73
|
+
if (signal) {
|
|
74
|
+
if (signal.aborted) finish(false);
|
|
75
|
+
else signal.addEventListener("abort", () => finish(false), { once: true });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** GET 跟随重定向(≤5 跳),返回最终 URL / 状态 / 正文 / 内容类型;支持 gzip/br/deflate 解压。 */
|
|
15
81
|
function httpsGetFollow(url, { headers = {}, timeoutMs = 20000, signal } = {}) {
|
|
16
82
|
return new Promise((resolve, reject) => {
|
|
17
83
|
const hop = (current, left) => {
|
|
@@ -24,9 +90,19 @@ function httpsGetFollow(url, { headers = {}, timeoutMs = 20000, signal } = {}) {
|
|
|
24
90
|
const next = new URL(res.headers.location, current).toString();
|
|
25
91
|
return hop(next, left - 1);
|
|
26
92
|
}
|
|
93
|
+
const enc = String(res.headers["content-encoding"] || "").toLowerCase();
|
|
94
|
+
let stream = res;
|
|
95
|
+
try {
|
|
96
|
+
if (enc.includes("br")) stream = res.pipe(zlib.createBrotliDecompress());
|
|
97
|
+
else if (enc.includes("gzip")) stream = res.pipe(zlib.createGunzip());
|
|
98
|
+
else if (enc.includes("deflate")) stream = res.pipe(zlib.createInflate());
|
|
99
|
+
} catch {
|
|
100
|
+
stream = res;
|
|
101
|
+
}
|
|
27
102
|
const chunks = [];
|
|
28
|
-
|
|
29
|
-
|
|
103
|
+
stream.on("data", (c) => chunks.push(c));
|
|
104
|
+
stream.on("end", () => resolve({ url: current, status: res.statusCode, text: Buffer.concat(chunks).toString("utf8"), contentType: res.headers["content-type"] || "" }));
|
|
105
|
+
stream.on("error", reject);
|
|
30
106
|
}
|
|
31
107
|
);
|
|
32
108
|
req.on("timeout", () => req.destroy(new Error(`timeout after ${timeoutMs}ms`)));
|
|
@@ -37,7 +113,22 @@ function httpsGetFollow(url, { headers = {}, timeoutMs = 20000, signal } = {}) {
|
|
|
37
113
|
});
|
|
38
114
|
}
|
|
39
115
|
|
|
40
|
-
|
|
116
|
+
// 完整浏览器请求头(Chrome 128 形态):部分反爬站(如百度系)对精简头直接 403,完整头可通过。
|
|
117
|
+
// 注意:声明了 Accept-Encoding 就必须解压(httpsGetFollow 已支持 gzip/br/deflate)。
|
|
118
|
+
const BROWSERISH = {
|
|
119
|
+
"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",
|
|
120
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
|
121
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
122
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
123
|
+
"sec-ch-ua": '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"',
|
|
124
|
+
"sec-ch-ua-mobile": "?0",
|
|
125
|
+
"sec-ch-ua-platform": '"Windows"',
|
|
126
|
+
"Sec-Fetch-Dest": "document",
|
|
127
|
+
"Sec-Fetch-Mode": "navigate",
|
|
128
|
+
"Sec-Fetch-Site": "none",
|
|
129
|
+
"Sec-Fetch-User": "?1",
|
|
130
|
+
"Upgrade-Insecure-Requests": "1",
|
|
131
|
+
};
|
|
41
132
|
|
|
42
133
|
export function createFetchProvider({ resolveApiKey, logger }) {
|
|
43
134
|
return {
|
|
@@ -48,10 +139,17 @@ export function createFetchProvider({ resolveApiKey, logger }) {
|
|
|
48
139
|
const url = String(request?.url || "");
|
|
49
140
|
if (!/^https:\/\//i.test(url)) throw new Error(`fs-quality-fetch: only https URLs are supported (got "${url.slice(0, 60)}")`);
|
|
50
141
|
const errors = [];
|
|
142
|
+
// Referer:伪装为"站内点击"(部分反爬站对无 Referer 的匿名请求更严格)
|
|
143
|
+
const headers = { ...BROWSERISH };
|
|
144
|
+
try {
|
|
145
|
+
headers.Referer = new URL(url).origin + "/";
|
|
146
|
+
} catch {
|
|
147
|
+
// URL 已在上面校验过,理论到不了这里
|
|
148
|
+
}
|
|
51
149
|
|
|
52
150
|
// T1:直连(文本类内容;二进制交给后层——exa 取正文,PDF 等仍由 firecrawl 处理)
|
|
53
151
|
try {
|
|
54
|
-
const r = await httpsGetFollow(url, { headers
|
|
152
|
+
const r = await httpsGetFollow(url, { headers, signal });
|
|
55
153
|
if (r.status < 400 && TEXTUAL.test(r.contentType) && r.text.length >= T1_MIN_CHARS) {
|
|
56
154
|
return { url: r.url, statusCode: r.status, body: { kind: "html", content: r.text }, truncated: false };
|
|
57
155
|
}
|
|
@@ -59,6 +157,20 @@ export function createFetchProvider({ resolveApiKey, logger }) {
|
|
|
59
157
|
else errors.push(`direct: ${r.contentType || "unknown type"} ${r.text.length}B`);
|
|
60
158
|
} catch (e) { errors.push(`direct: ${e.message}`); }
|
|
61
159
|
|
|
160
|
+
// T1.5:系统浏览器无头渲染(执行 JS 过挑战,抓直连拿不到的页面;无浏览器自动跳过)
|
|
161
|
+
const browserPath = findSystemBrowser();
|
|
162
|
+
if (browserPath) {
|
|
163
|
+
try {
|
|
164
|
+
const html = await browserDumpDom(browserPath, url, { signal });
|
|
165
|
+
if (html && html.length >= T1_MIN_CHARS && !/403 Forbidden|\bcaptcha\b/i.test(html.slice(0, 3000))) {
|
|
166
|
+
return { url, statusCode: 200, body: { kind: "html", content: html }, truncated: false };
|
|
167
|
+
}
|
|
168
|
+
errors.push(html ? `browser: short or challenge page (${html.length}B)` : "browser: no output (timeout or launch failure)");
|
|
169
|
+
} catch (e) {
|
|
170
|
+
errors.push(`browser: ${e.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
62
174
|
// T2:exa contents($1/1000 页)——直连失败(反爬/JS/过短)时承接;firecrawl 额度保护:仅在本层失败后才走 T3
|
|
63
175
|
const exaKey = await resolveApiKey("EXA_API_KEY", "exaApiKey").catch(() => undefined);
|
|
64
176
|
if (exaKey) {
|
package/lib/index.js
CHANGED
|
@@ -240,7 +240,7 @@ function routeEngines(query, { timeRange, disabledEngines = [], fallbackOrder =
|
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
// 当前插件版本(发布时与 package.json 同步;跟随上游 v0.7.5,不在 PR 里 bump)
|
|
243
|
-
const PLUGIN_VERSION = "0.8.
|
|
243
|
+
const PLUGIN_VERSION = "0.8.5";
|
|
244
244
|
// 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
|
|
245
245
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
|
|
246
246
|
const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-free-search",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
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 + web_fetch, with web settings UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|