webcrawl 1.4.1 → 1.4.3
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/dist/cli.js +11 -2
- package/dist/core/engine/chrome.d.ts +14 -0
- package/dist/core/engine/chrome.js +140 -0
- package/dist/core/engine/crawler.d.ts +6 -5
- package/dist/core/engine/crawler.js +51 -18
- package/dist/core/engine/fetcher.js +24 -5
- package/dist/core/engine/index.d.ts +1 -0
- package/dist/core/engine/index.js +1 -0
- package/dist/core/engine/safe-url.d.ts +6 -0
- package/dist/core/engine/safe-url.js +19 -2
- package/dist/core/extract/index.d.ts +1 -0
- package/dist/core/extract/index.js +1 -0
- package/dist/core/extract/structured.d.ts +14 -0
- package/dist/core/extract/structured.js +144 -0
- package/dist/core/local-engine.d.ts +16 -0
- package/dist/core/local-engine.js +213 -106
- package/dist/core/ranker/lexical.js +1 -0
- package/dist/core/ranker/types.d.ts +1 -1
- package/dist/core/search/catalog.d.ts +2 -0
- package/dist/core/search/pipeline.js +24 -9
- package/dist/core/search/providers.d.ts +10 -0
- package/dist/core/search/providers.js +97 -2
- package/dist/format.js +8 -1
- package/dist/setup.js +5 -1
- package/package.json +3 -3
|
@@ -219,10 +219,105 @@ export async function fetchHedgedNativeFederation(rawQuery, localizedQuery, perE
|
|
|
219
219
|
}
|
|
220
220
|
collected.push(...jina);
|
|
221
221
|
}
|
|
222
|
+
// HTML SERPs (DDG, Brave) are frequently challenge/429 pages. When they
|
|
223
|
+
// yield nothing, fall back to keyless APIs that still work without a key.
|
|
224
|
+
if (collected.length === 0 && (!req.provider || req.provider === "auto")) {
|
|
225
|
+
const extras = await fetchKeylessSearchFallbacks(localizedQuery, perEngineLimit);
|
|
226
|
+
collected.push(...extras);
|
|
227
|
+
}
|
|
222
228
|
return collected;
|
|
223
229
|
}
|
|
224
230
|
/** Below this many unique URLs from the free engines, the Jina fallback runs. */
|
|
225
231
|
export const JINA_FALLBACK_MIN_URLS = 3;
|
|
232
|
+
/** True when a scraped SERP is a bot-check / captcha rather than results. */
|
|
233
|
+
export function isSerpChallengeHtml(html, status = 200) {
|
|
234
|
+
if (status === 403 || status === 429 || status === 503)
|
|
235
|
+
return true;
|
|
236
|
+
const lower = String(html ?? "").slice(0, 8000).toLowerCase();
|
|
237
|
+
return (lower.includes("bots use duckduckgo") ||
|
|
238
|
+
lower.includes("anomaly-modal") ||
|
|
239
|
+
lower.includes("please complete the following challenge") ||
|
|
240
|
+
lower.includes("schedule a captcha") ||
|
|
241
|
+
lower.includes("flagged as being suspicious") ||
|
|
242
|
+
lower.includes("enable javascript to function"));
|
|
243
|
+
}
|
|
244
|
+
export const EMPTY_SEARCH_ERROR = "Keyless search engines returned no results (DuckDuckGo and Brave often serve a bot challenge). " +
|
|
245
|
+
"Tried Wikipedia, Hacker News, and Bing RSS as well. " +
|
|
246
|
+
"Set BRAVE_SEARCH_API_KEY, SERPER_API_KEY, or JINA_API_KEY, or retry later.";
|
|
247
|
+
/** Parse Bing's `format=rss` web-search feed into candidate docs. */
|
|
248
|
+
export function parseBingWebRss(xml, limit) {
|
|
249
|
+
const items = [];
|
|
250
|
+
const itemRe = /<item>([\s\S]*?)<\/item>/gi;
|
|
251
|
+
let match;
|
|
252
|
+
while ((match = itemRe.exec(xml)) !== null && items.length < limit) {
|
|
253
|
+
const block = match[1];
|
|
254
|
+
const title = decodeHtmlEntities((block.match(/<title>([\s\S]*?)<\/title>/i)?.[1] || "")
|
|
255
|
+
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1")
|
|
256
|
+
.trim());
|
|
257
|
+
const link = (block.match(/<link>([\s\S]*?)<\/link>/i)?.[1] || "")
|
|
258
|
+
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1")
|
|
259
|
+
.trim();
|
|
260
|
+
const description = decodeHtmlEntities((block.match(/<description>([\s\S]*?)<\/description>/i)?.[1] || "")
|
|
261
|
+
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1")
|
|
262
|
+
.replace(/<[^>]+>/g, " ")
|
|
263
|
+
.replace(/\s+/g, " ")
|
|
264
|
+
.trim());
|
|
265
|
+
if (!title || !link.startsWith("http"))
|
|
266
|
+
continue;
|
|
267
|
+
items.push({
|
|
268
|
+
title,
|
|
269
|
+
url: link,
|
|
270
|
+
description: description || title,
|
|
271
|
+
engine: "bing-rss",
|
|
272
|
+
engineRank: items.length + 1,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return items;
|
|
276
|
+
}
|
|
277
|
+
async function fetchBingWebRss(query, limit) {
|
|
278
|
+
try {
|
|
279
|
+
const rssUrl = `https://www.bing.com/search?format=rss&q=${encodeURIComponent(query)}`;
|
|
280
|
+
const res = await fetch(rssUrl, {
|
|
281
|
+
headers: {
|
|
282
|
+
Accept: "application/rss+xml, application/xml, text/xml;q=0.9, */*;q=0.8",
|
|
283
|
+
"User-Agent": "Mozilla/5.0 (compatible; WebCrawl/1.4; +https://webcrawl.app)",
|
|
284
|
+
},
|
|
285
|
+
signal: AbortSignal.timeout(4000),
|
|
286
|
+
});
|
|
287
|
+
if (!res.ok)
|
|
288
|
+
return [];
|
|
289
|
+
const xml = await res.text();
|
|
290
|
+
if (!xml.includes("<item>"))
|
|
291
|
+
return [];
|
|
292
|
+
return parseBingWebRss(xml, limit);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Last-resort keyless engines used when DDG/Brave HTML SERPs come back empty.
|
|
300
|
+
* Wikipedia OpenSearch, HN Algolia, and Bing RSS do not require a JS challenge.
|
|
301
|
+
*/
|
|
302
|
+
export async function fetchKeylessSearchFallbacks(query, limit) {
|
|
303
|
+
const per = Math.max(limit, 5);
|
|
304
|
+
const [hn, wiki, bing] = await Promise.all([
|
|
305
|
+
fetchHackerNewsAlgolia(query, per),
|
|
306
|
+
fetchWikipediaOpenSearch(query, per),
|
|
307
|
+
fetchBingWebRss(query, per),
|
|
308
|
+
]);
|
|
309
|
+
const seen = new Set();
|
|
310
|
+
const out = [];
|
|
311
|
+
for (const item of [...hn, ...wiki, ...bing]) {
|
|
312
|
+
if (!item.url || seen.has(item.url))
|
|
313
|
+
continue;
|
|
314
|
+
seen.add(item.url);
|
|
315
|
+
out.push(item);
|
|
316
|
+
if (out.length >= Math.max(limit + 4, 8))
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
226
321
|
export async function fetchJinaSearch(query, limit, req) {
|
|
227
322
|
const key = process.env.JINA_API_KEY;
|
|
228
323
|
if (!key) {
|
|
@@ -435,7 +530,7 @@ export async function fetchBraveNativeSerp(query, limit, userLocation) {
|
|
|
435
530
|
: "";
|
|
436
531
|
const url = `https://search.brave.com/search?q=${encodeURIComponent(query)}&source=web${countryParam}`;
|
|
437
532
|
const res = await fetchWithTlsImpersonation(url, 2200);
|
|
438
|
-
if (res.status !== 200 || !res.html) {
|
|
533
|
+
if (res.status !== 200 || !res.html || isSerpChallengeHtml(res.html, res.status)) {
|
|
439
534
|
return [];
|
|
440
535
|
}
|
|
441
536
|
const html = res.html;
|
|
@@ -545,7 +640,7 @@ async function fetchDuckDuckGoLiteOnly(query, limit, userLocation) {
|
|
|
545
640
|
const kl = ddgRegion(countryCode, undefined);
|
|
546
641
|
const klParam = kl ? `&kl=${encodeURIComponent(kl)}` : "";
|
|
547
642
|
const liteRes = await fetchWithTlsImpersonation(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}${klParam}`, 2200);
|
|
548
|
-
if (liteRes && liteRes.html) {
|
|
643
|
+
if (liteRes && liteRes.html && !isSerpChallengeHtml(liteRes.html, liteRes.status)) {
|
|
549
644
|
const html = liteRes.html;
|
|
550
645
|
const linkMatches = Array.from(html.matchAll(/<a\s+[^>]*href=["']([^"']+)["'][^>]*class=["']result-link["'][^>]*>([\s\S]*?)<\/a>|<a\s+[^>]*class=["']result-link["'][^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi));
|
|
551
646
|
const snippetMatches = Array.from(html.matchAll(/<td\s+[^>]*class=["']result-snippet["'][^>]*>([\s\S]*?)<\/td>/gi));
|
package/dist/format.js
CHANGED
|
@@ -49,9 +49,16 @@ export function formatSearch(res) {
|
|
|
49
49
|
})
|
|
50
50
|
.join("\n")}`);
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
if (parts.length)
|
|
53
|
+
return parts.join("\n\n");
|
|
54
|
+
if (res.success === false && str(res.error))
|
|
55
|
+
return `Error: ${str(res.error)}`;
|
|
56
|
+
return "No results.";
|
|
53
57
|
}
|
|
54
58
|
export function formatScrape(res) {
|
|
59
|
+
if (res.success === false && str(res.error)) {
|
|
60
|
+
return `Error: ${str(res.error)}`;
|
|
61
|
+
}
|
|
55
62
|
const d = isRec(res.data) ? res.data : res;
|
|
56
63
|
return str(d.markdown) || str(d.content) || "(no content returned)";
|
|
57
64
|
}
|
package/dist/setup.js
CHANGED
|
@@ -147,18 +147,22 @@ export function runDoctor(env = process.env, effectiveMode = "local", apiKeySour
|
|
|
147
147
|
});
|
|
148
148
|
// 3. Local Chrome / Stealth Browser availability
|
|
149
149
|
const chromePaths = [
|
|
150
|
+
env.WEBCRAWL_CHROME_PATH,
|
|
151
|
+
env.INTERACT_CHROME_PATH,
|
|
150
152
|
env.CHROME_PATH,
|
|
151
153
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
152
154
|
"/usr/bin/google-chrome",
|
|
155
|
+
"/usr/bin/google-chrome-stable",
|
|
153
156
|
"/usr/bin/chromium",
|
|
154
157
|
"/usr/bin/chromium-browser",
|
|
158
|
+
"/snap/bin/chromium",
|
|
155
159
|
].filter(Boolean);
|
|
156
160
|
const foundChrome = chromePaths.find((p) => existsSync(p));
|
|
157
161
|
checks.push({
|
|
158
162
|
name: "Local stealth browser",
|
|
159
163
|
status: foundChrome ? "ok" : "optional",
|
|
160
164
|
detail: foundChrome
|
|
161
|
-
? `Found Chrome at ${foundChrome}`
|
|
165
|
+
? `Found Chrome at ${foundChrome} (used to render JS pages)`
|
|
162
166
|
: "No local Chrome binary detected; static/TLS fetch works out of the box, or run `webcrawl login` for cloud stealth browsers",
|
|
163
167
|
});
|
|
164
168
|
// 4. Search providers
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcrawl",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "Webcrawl Anything CLI & Core Engine — crawl, search, and extract structured evidence from any website, SPA, PDF, Office file, YouTube video, GitHub repo, ArXiv paper, or CVE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -127,8 +127,8 @@
|
|
|
127
127
|
"@ai-sdk/gateway": "^4.0.123",
|
|
128
128
|
"@firecrawl/pdf-inspector": "^1.25.2",
|
|
129
129
|
"@vercel/sandbox": "^1.0.0",
|
|
130
|
-
"@webcrawl/client": "^1.4.
|
|
131
|
-
"@webcrawl/curl-impersonate": "^1.4.
|
|
130
|
+
"@webcrawl/client": "^1.4.3",
|
|
131
|
+
"@webcrawl/curl-impersonate": "^1.4.3",
|
|
132
132
|
"ai": "^7.0.150",
|
|
133
133
|
"impit": "^0.14.7",
|
|
134
134
|
"mammoth": "^1.13.0",
|