webcrawl 1.4.2 → 1.4.4
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 +2 -2
- package/dist/cli.js +13 -2
- package/dist/core/engine/chrome-cdp.d.ts +22 -0
- package/dist/core/engine/chrome-cdp.js +318 -0
- package/dist/core/engine/chrome-launch.d.ts +38 -0
- package/dist/core/engine/chrome-launch.js +214 -0
- package/dist/core/engine/chrome.d.ts +28 -0
- package/dist/core/engine/chrome.js +208 -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/extract/index.d.ts +1 -0
- package/dist/core/extract/index.js +1 -0
- package/dist/core/extract/structured.d.ts +33 -0
- package/dist/core/extract/structured.js +316 -0
- package/dist/core/local-engine.d.ts +17 -1
- package/dist/core/local-engine.js +272 -138
- package/dist/core/ranker/lexical.js +1 -0
- package/dist/core/ranker/types.d.ts +1 -1
- package/dist/core/search/catalog.d.ts +3 -1
- package/dist/core/search/catalog.js +1 -1
- package/dist/core/search/pipeline.js +29 -12
- package/dist/core/search/providers.d.ts +23 -1
- package/dist/core/search/providers.js +253 -20
- package/dist/core/targets/registry.js +1 -1
- package/dist/format.js +5 -1
- package/dist/setup.js +7 -3
- package/package.json +3 -3
|
@@ -14,8 +14,30 @@ export declare function isEntityQuery(q: string): boolean;
|
|
|
14
14
|
* preventing any single slow engine from inflating tail latency.
|
|
15
15
|
*/
|
|
16
16
|
export declare function fetchHedgedNativeFederation(rawQuery: string, localizedQuery: string, perEngineLimit: number, req: SearchRequest, userLocation: UserLocationContext, quorumTimeoutMs: number, targetSites: string[], onOffer: (offer: ProductOffer) => void, onCve?: (cve: CveRecord) => void): Promise<RawCandidateDocument[]>;
|
|
17
|
-
/** Below this many unique URLs from the free engines, the
|
|
17
|
+
/** Below this many unique URLs from the free engines, the keyed search fallback runs. */
|
|
18
18
|
export declare const JINA_FALLBACK_MIN_URLS = 3;
|
|
19
|
+
/** True when a scraped SERP is a bot-check / captcha rather than results. */
|
|
20
|
+
export declare function isSerpChallengeHtml(html: string, status?: number): boolean;
|
|
21
|
+
export declare const EMPTY_SEARCH_ERROR: string;
|
|
22
|
+
/** Canonical URL for search dedupe (host lowercased, trailing slash / tracking params stripped). */
|
|
23
|
+
export declare function canonicalSearchUrl(raw: string | undefined): string;
|
|
24
|
+
/** Round-robin merge so one source (e.g. Wikipedia) cannot fill `-n` by itself. */
|
|
25
|
+
export declare function mergeSearchCandidates(groups: RawCandidateDocument[][], limit: number): RawCandidateDocument[];
|
|
26
|
+
export declare function withRetry<T>(fn: () => Promise<T>, opts?: {
|
|
27
|
+
retries?: number;
|
|
28
|
+
delayMs?: number;
|
|
29
|
+
isEmpty?: (v: T) => boolean;
|
|
30
|
+
}): Promise<T>;
|
|
31
|
+
/** Parse Bing's `format=rss` web-search feed into candidate docs. */
|
|
32
|
+
export declare function parseBingWebRss(xml: string, limit: number): RawCandidateDocument[];
|
|
33
|
+
/**
|
|
34
|
+
* Last-resort keyless engines used when DDG/Brave HTML SERPs come back empty
|
|
35
|
+
* or thinner than `limit`. Wikipedia OpenSearch, HN Algolia, Bing RSS, and
|
|
36
|
+
* DuckDuckGo Instant Answer do not require a JS challenge or API key.
|
|
37
|
+
*/
|
|
38
|
+
export declare function fetchKeylessSearchFallbacks(query: string, limit: number): Promise<RawCandidateDocument[]>;
|
|
39
|
+
/** DuckDuckGo Instant Answer API — keyless JSON, unrelated to the HTML SERP challenge. */
|
|
40
|
+
export declare function fetchDuckDuckGoInstantAnswer(query: string, limit: number): Promise<RawCandidateDocument[]>;
|
|
19
41
|
export declare function fetchJinaSearch(query: string, limit: number, req?: {
|
|
20
42
|
country?: string;
|
|
21
43
|
lang?: string;
|
|
@@ -89,11 +89,15 @@ export async function fetchHedgedNativeFederation(rawQuery, localizedQuery, perE
|
|
|
89
89
|
},
|
|
90
90
|
wikipedia: {
|
|
91
91
|
primary: false,
|
|
92
|
-
run: () => fetchWikipediaOpenSearch(localizedQuery,
|
|
92
|
+
run: () => fetchWikipediaOpenSearch(localizedQuery, Math.min(perEngineLimit, 5)),
|
|
93
93
|
},
|
|
94
94
|
"hn-algolia": {
|
|
95
95
|
primary: false,
|
|
96
|
-
run: () => fetchHackerNewsAlgolia(localizedQuery,
|
|
96
|
+
run: () => fetchHackerNewsAlgolia(localizedQuery, perEngineLimit),
|
|
97
|
+
},
|
|
98
|
+
"bing-rss": {
|
|
99
|
+
primary: false,
|
|
100
|
+
run: () => fetchBingWebRss(localizedQuery, perEngineLimit),
|
|
97
101
|
},
|
|
98
102
|
arxiv: {
|
|
99
103
|
primary: category === "academic",
|
|
@@ -159,14 +163,17 @@ export async function fetchHedgedNativeFederation(rawQuery, localizedQuery, perE
|
|
|
159
163
|
resolve();
|
|
160
164
|
return;
|
|
161
165
|
}
|
|
162
|
-
|
|
166
|
+
const uniqueCount = new Set(collected.map((c) => canonicalSearchUrl(c.url)).filter(Boolean)).size;
|
|
167
|
+
const fillTarget = Math.max(1, req.limit ?? 5);
|
|
168
|
+
// Early-exit quorum: at least 1 primary web engine + 2 total engines + enough
|
|
169
|
+
// *unique* URLs to fill `limit` (not just Wikipedia's 2 hits).
|
|
163
170
|
if (completedShoppingEngine &&
|
|
164
171
|
completedNewsEngine &&
|
|
165
172
|
completedNvdEngine &&
|
|
166
173
|
completedArxivEngine &&
|
|
167
174
|
completedPrimaryEngines >= 1 &&
|
|
168
175
|
totalCompletedEngines >= 2 &&
|
|
169
|
-
|
|
176
|
+
uniqueCount >= fillTarget &&
|
|
170
177
|
!graceTimer) {
|
|
171
178
|
graceTimer = setTimeout(() => {
|
|
172
179
|
if (!settled) {
|
|
@@ -209,7 +216,7 @@ export async function fetchHedgedNativeFederation(rawQuery, localizedQuery, perE
|
|
|
209
216
|
}
|
|
210
217
|
});
|
|
211
218
|
// Fallback: the free engines are scraped and can be blocked or rate limited. When they
|
|
212
|
-
// come back thin, fill in from
|
|
219
|
+
// come back thin, fill in from a keyed search provider so the caller still gets an answer.
|
|
213
220
|
const uniqueUrls = new Set(collected.map((c) => c.url).filter(Boolean));
|
|
214
221
|
if (uniqueUrls.size < JINA_FALLBACK_MIN_URLS && process.env.JINA_API_KEY) {
|
|
215
222
|
const t0 = performance.now();
|
|
@@ -219,10 +226,220 @@ export async function fetchHedgedNativeFederation(rawQuery, localizedQuery, perE
|
|
|
219
226
|
}
|
|
220
227
|
collected.push(...jina);
|
|
221
228
|
}
|
|
229
|
+
// HTML SERPs (DDG, Brave) are frequently challenge/429 pages. When they
|
|
230
|
+
// yield nothing *or come back thinner than `limit`*, fill from keyless APIs
|
|
231
|
+
// (Wikipedia, HN Algolia, Bing RSS, DDG Instant Answer) with retry/backoff.
|
|
232
|
+
const fillTarget = Math.max(1, req.limit ?? 5);
|
|
233
|
+
const uniqueAfter = new Set(collected.map((c) => canonicalSearchUrl(c.url)).filter(Boolean));
|
|
234
|
+
if (uniqueAfter.size < fillTarget && (!req.provider || req.provider === "auto")) {
|
|
235
|
+
const extras = await fetchKeylessSearchFallbacks(localizedQuery, Math.max(fillTarget, perEngineLimit));
|
|
236
|
+
collected.push(...extras);
|
|
237
|
+
}
|
|
222
238
|
return collected;
|
|
223
239
|
}
|
|
224
|
-
/** Below this many unique URLs from the free engines, the
|
|
240
|
+
/** Below this many unique URLs from the free engines, the keyed search fallback runs. */
|
|
225
241
|
export const JINA_FALLBACK_MIN_URLS = 3;
|
|
242
|
+
/** True when a scraped SERP is a bot-check / captcha rather than results. */
|
|
243
|
+
export function isSerpChallengeHtml(html, status = 200) {
|
|
244
|
+
if (status === 403 || status === 429 || status === 503)
|
|
245
|
+
return true;
|
|
246
|
+
const lower = String(html ?? "").slice(0, 8000).toLowerCase();
|
|
247
|
+
return (lower.includes("bots use duckduckgo") ||
|
|
248
|
+
lower.includes("anomaly-modal") ||
|
|
249
|
+
lower.includes("please complete the following challenge") ||
|
|
250
|
+
lower.includes("schedule a captcha") ||
|
|
251
|
+
lower.includes("flagged as being suspicious") ||
|
|
252
|
+
lower.includes("enable javascript to function"));
|
|
253
|
+
}
|
|
254
|
+
export const EMPTY_SEARCH_ERROR = "Keyless search engines returned no results (DuckDuckGo and Brave often serve a bot challenge). " +
|
|
255
|
+
"Tried Wikipedia, Hacker News, Bing RSS, and DuckDuckGo Instant Answer as well. " +
|
|
256
|
+
"Set BRAVE_SEARCH_API_KEY, SERPER_API_KEY, or JINA_API_KEY, or retry later.";
|
|
257
|
+
/** Canonical URL for search dedupe (host lowercased, trailing slash / tracking params stripped). */
|
|
258
|
+
export function canonicalSearchUrl(raw) {
|
|
259
|
+
if (!raw)
|
|
260
|
+
return "";
|
|
261
|
+
try {
|
|
262
|
+
const u = new URL(raw);
|
|
263
|
+
u.hash = "";
|
|
264
|
+
u.hostname = u.hostname.toLowerCase();
|
|
265
|
+
for (const key of [...u.searchParams.keys()]) {
|
|
266
|
+
if (/^(utm_|ref|fbclid|gclid|mc_)/i.test(key))
|
|
267
|
+
u.searchParams.delete(key);
|
|
268
|
+
}
|
|
269
|
+
let path = u.pathname.replace(/\/+$/, "") || "/";
|
|
270
|
+
u.pathname = path;
|
|
271
|
+
return u.toString();
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return raw.replace(/\/+$/, "").toLowerCase();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/** Round-robin merge so one source (e.g. Wikipedia) cannot fill `-n` by itself. */
|
|
278
|
+
export function mergeSearchCandidates(groups, limit) {
|
|
279
|
+
const seen = new Set();
|
|
280
|
+
const out = [];
|
|
281
|
+
const queues = groups.map((g) => [...g]);
|
|
282
|
+
let progressed = true;
|
|
283
|
+
while (out.length < limit && progressed) {
|
|
284
|
+
progressed = false;
|
|
285
|
+
for (const q of queues) {
|
|
286
|
+
while (q.length > 0) {
|
|
287
|
+
const item = q.shift();
|
|
288
|
+
const key = canonicalSearchUrl(item.url);
|
|
289
|
+
if (!key || seen.has(key))
|
|
290
|
+
continue;
|
|
291
|
+
seen.add(key);
|
|
292
|
+
out.push(item);
|
|
293
|
+
progressed = true;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
if (out.length >= limit)
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
export async function withRetry(fn, opts = {}) {
|
|
303
|
+
const retries = opts.retries ?? 1;
|
|
304
|
+
const delayMs = opts.delayMs ?? 250;
|
|
305
|
+
const isEmpty = opts.isEmpty ?? ((v) => (Array.isArray(v) ? v.length === 0 : !v));
|
|
306
|
+
let last = await fn();
|
|
307
|
+
for (let i = 0; i < retries && isEmpty(last); i++) {
|
|
308
|
+
await new Promise((r) => setTimeout(r, delayMs * (i + 1)));
|
|
309
|
+
last = await fn();
|
|
310
|
+
}
|
|
311
|
+
return last;
|
|
312
|
+
}
|
|
313
|
+
/** Parse Bing's `format=rss` web-search feed into candidate docs. */
|
|
314
|
+
export function parseBingWebRss(xml, limit) {
|
|
315
|
+
const items = [];
|
|
316
|
+
const itemRe = /<item>([\s\S]*?)<\/item>/gi;
|
|
317
|
+
let match;
|
|
318
|
+
while ((match = itemRe.exec(xml)) !== null && items.length < limit) {
|
|
319
|
+
const block = match[1];
|
|
320
|
+
const title = decodeHtmlEntities((block.match(/<title>([\s\S]*?)<\/title>/i)?.[1] || "")
|
|
321
|
+
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1")
|
|
322
|
+
.trim());
|
|
323
|
+
const link = (block.match(/<link>([\s\S]*?)<\/link>/i)?.[1] || "")
|
|
324
|
+
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1")
|
|
325
|
+
.trim();
|
|
326
|
+
const description = decodeHtmlEntities((block.match(/<description>([\s\S]*?)<\/description>/i)?.[1] || "")
|
|
327
|
+
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1")
|
|
328
|
+
.replace(/<[^>]+>/g, " ")
|
|
329
|
+
.replace(/\s+/g, " ")
|
|
330
|
+
.trim());
|
|
331
|
+
if (!title || !link.startsWith("http"))
|
|
332
|
+
continue;
|
|
333
|
+
items.push({
|
|
334
|
+
title,
|
|
335
|
+
url: link,
|
|
336
|
+
description: description || title,
|
|
337
|
+
engine: "bing-rss",
|
|
338
|
+
engineRank: items.length + 1,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return items;
|
|
342
|
+
}
|
|
343
|
+
async function fetchBingWebRss(query, limit) {
|
|
344
|
+
try {
|
|
345
|
+
const rssUrl = `https://www.bing.com/search?format=rss&q=${encodeURIComponent(query)}`;
|
|
346
|
+
const res = await fetch(rssUrl, {
|
|
347
|
+
headers: {
|
|
348
|
+
Accept: "application/rss+xml, application/xml, text/xml;q=0.9, */*;q=0.8",
|
|
349
|
+
"User-Agent": "Mozilla/5.0 (compatible; WebCrawl/1.4; +https://webcrawl.app)",
|
|
350
|
+
},
|
|
351
|
+
signal: AbortSignal.timeout(4000),
|
|
352
|
+
});
|
|
353
|
+
if (!res.ok)
|
|
354
|
+
return [];
|
|
355
|
+
const xml = await res.text();
|
|
356
|
+
if (!xml.includes("<item>"))
|
|
357
|
+
return [];
|
|
358
|
+
return parseBingWebRss(xml, limit);
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Last-resort keyless engines used when DDG/Brave HTML SERPs come back empty
|
|
366
|
+
* or thinner than `limit`. Wikipedia OpenSearch, HN Algolia, Bing RSS, and
|
|
367
|
+
* DuckDuckGo Instant Answer do not require a JS challenge or API key.
|
|
368
|
+
*/
|
|
369
|
+
export async function fetchKeylessSearchFallbacks(query, limit) {
|
|
370
|
+
const per = Math.max(limit, 5);
|
|
371
|
+
const empty = (v) => v.length === 0;
|
|
372
|
+
const run = () => Promise.all([
|
|
373
|
+
withRetry(() => fetchHackerNewsAlgolia(query, per), { retries: 1, delayMs: 280, isEmpty: empty }),
|
|
374
|
+
withRetry(() => fetchWikipediaOpenSearch(query, per), { retries: 1, delayMs: 280, isEmpty: empty }),
|
|
375
|
+
withRetry(() => fetchBingWebRss(query, per), { retries: 1, delayMs: 280, isEmpty: empty }),
|
|
376
|
+
withRetry(() => fetchDuckDuckGoInstantAnswer(query, per), { retries: 1, delayMs: 280, isEmpty: empty }),
|
|
377
|
+
]);
|
|
378
|
+
let groups = await run();
|
|
379
|
+
if (groups.every((g) => g.length === 0)) {
|
|
380
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
381
|
+
groups = await run();
|
|
382
|
+
}
|
|
383
|
+
return mergeSearchCandidates(groups, Math.max(limit, 8));
|
|
384
|
+
}
|
|
385
|
+
/** DuckDuckGo Instant Answer API — keyless JSON, unrelated to the HTML SERP challenge. */
|
|
386
|
+
export async function fetchDuckDuckGoInstantAnswer(query, limit) {
|
|
387
|
+
try {
|
|
388
|
+
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_redirect=1&no_html=1&skip_disambig=1`;
|
|
389
|
+
const res = await fetch(url, {
|
|
390
|
+
headers: {
|
|
391
|
+
Accept: "application/json",
|
|
392
|
+
"User-Agent": "WebCrawl/1.4 (https://webcrawl.app; search@webcrawl.app)",
|
|
393
|
+
},
|
|
394
|
+
signal: AbortSignal.timeout(2500),
|
|
395
|
+
});
|
|
396
|
+
if (!res.ok)
|
|
397
|
+
return [];
|
|
398
|
+
const json = (await res.json());
|
|
399
|
+
const items = [];
|
|
400
|
+
const push = (title, href, description) => {
|
|
401
|
+
if (!href.startsWith("http") || items.length >= limit)
|
|
402
|
+
return;
|
|
403
|
+
if (items.some((i) => canonicalSearchUrl(i.url) === canonicalSearchUrl(href)))
|
|
404
|
+
return;
|
|
405
|
+
items.push({
|
|
406
|
+
title: title || href,
|
|
407
|
+
url: href,
|
|
408
|
+
description: description || title,
|
|
409
|
+
engine: "ddg-lite",
|
|
410
|
+
engineRank: items.length + 1,
|
|
411
|
+
});
|
|
412
|
+
};
|
|
413
|
+
if (json.AbstractURL && (json.Heading || json.AbstractText)) {
|
|
414
|
+
push(json.Heading || json.AbstractSource || json.AbstractURL, json.AbstractURL, json.AbstractText || "");
|
|
415
|
+
}
|
|
416
|
+
for (const row of json.Results ?? []) {
|
|
417
|
+
if (row.FirstURL && row.Text)
|
|
418
|
+
push(row.Text, row.FirstURL, row.Text);
|
|
419
|
+
}
|
|
420
|
+
const walk = (node) => {
|
|
421
|
+
if (items.length >= limit)
|
|
422
|
+
return;
|
|
423
|
+
if (Array.isArray(node)) {
|
|
424
|
+
for (const n of node)
|
|
425
|
+
walk(n);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (!node || typeof node !== "object")
|
|
429
|
+
return;
|
|
430
|
+
const rec = node;
|
|
431
|
+
if (rec.FirstURL && rec.Text)
|
|
432
|
+
push(rec.Text, rec.FirstURL, rec.Text);
|
|
433
|
+
if (rec.Topics)
|
|
434
|
+
walk(rec.Topics);
|
|
435
|
+
};
|
|
436
|
+
walk(json.RelatedTopics);
|
|
437
|
+
return items;
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
}
|
|
226
443
|
export async function fetchJinaSearch(query, limit, req) {
|
|
227
444
|
const key = process.env.JINA_API_KEY;
|
|
228
445
|
if (!key) {
|
|
@@ -435,7 +652,7 @@ export async function fetchBraveNativeSerp(query, limit, userLocation) {
|
|
|
435
652
|
: "";
|
|
436
653
|
const url = `https://search.brave.com/search?q=${encodeURIComponent(query)}&source=web${countryParam}`;
|
|
437
654
|
const res = await fetchWithTlsImpersonation(url, 2200);
|
|
438
|
-
if (res.status !== 200 || !res.html) {
|
|
655
|
+
if (res.status !== 200 || !res.html || isSerpChallengeHtml(res.html, res.status)) {
|
|
439
656
|
return [];
|
|
440
657
|
}
|
|
441
658
|
const html = res.html;
|
|
@@ -544,17 +761,33 @@ async function fetchDuckDuckGoLiteOnly(query, limit, userLocation) {
|
|
|
544
761
|
const countryCode = (userLocation?.country || "").toUpperCase();
|
|
545
762
|
const kl = ddgRegion(countryCode, undefined);
|
|
546
763
|
const klParam = kl ? `&kl=${encodeURIComponent(kl)}` : "";
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
764
|
+
const endpoints = [
|
|
765
|
+
`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}${klParam}`,
|
|
766
|
+
`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}${klParam}`,
|
|
767
|
+
];
|
|
768
|
+
for (const endpoint of endpoints) {
|
|
769
|
+
if (items.length >= limit)
|
|
770
|
+
break;
|
|
771
|
+
try {
|
|
772
|
+
const liteRes = await fetchWithTlsImpersonation(endpoint, 2200);
|
|
773
|
+
if (!liteRes?.html || isSerpChallengeHtml(liteRes.html, liteRes.status)) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
const html = liteRes.html;
|
|
777
|
+
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));
|
|
778
|
+
const snippetMatches = Array.from(html.matchAll(/<td\s+[^>]*class=["']result-snippet["'][^>]*>([\s\S]*?)<\/td>/gi));
|
|
779
|
+
for (let i = 0; i < linkMatches.length && items.length < limit; i++) {
|
|
780
|
+
const m = linkMatches[i];
|
|
781
|
+
const rawUrl = m[1] || m[3] || "";
|
|
782
|
+
const rawTitle = m[2] || m[4] || "";
|
|
783
|
+
const rawSnippet = snippetMatches[i]?.[1] || "";
|
|
784
|
+
addResult(rawTitle, rawUrl, rawSnippet);
|
|
785
|
+
}
|
|
786
|
+
if (items.length > 0)
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
// try the next DDG HTML endpoint
|
|
558
791
|
}
|
|
559
792
|
}
|
|
560
793
|
}
|
|
@@ -629,7 +862,7 @@ async function fetchWikipediaOpenSearch(query, limit) {
|
|
|
629
862
|
"User-Agent": "WebCrawl/1.0 (https://webcrawl.app; search@webcrawl.app)",
|
|
630
863
|
Accept: "application/json",
|
|
631
864
|
},
|
|
632
|
-
signal: AbortSignal.timeout(
|
|
865
|
+
signal: AbortSignal.timeout(2200),
|
|
633
866
|
});
|
|
634
867
|
if (!res.ok) {
|
|
635
868
|
return [];
|
|
@@ -666,7 +899,7 @@ async function fetchHackerNewsAlgolia(query, limit) {
|
|
|
666
899
|
const url = `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(cleanQuery)}&tags=story&hitsPerPage=${limit}`;
|
|
667
900
|
const res = await fetch(url, {
|
|
668
901
|
headers: { Accept: "application/json" },
|
|
669
|
-
signal: AbortSignal.timeout(
|
|
902
|
+
signal: AbortSignal.timeout(2200),
|
|
670
903
|
});
|
|
671
904
|
if (!res.ok) {
|
|
672
905
|
return [];
|
|
@@ -62,7 +62,7 @@ export const pdfOfficeTargetPlugin = {
|
|
|
62
62
|
name: "PDFs & Office Documents (DOCX, XLSX, PPTX)",
|
|
63
63
|
category: "documents",
|
|
64
64
|
returns: "Page-numbered Markdown, tables, native text extraction, automatic OCR on scanned pages, and skipped-page diagnostics",
|
|
65
|
-
requirements: "Built-in
|
|
65
|
+
requirements: "Built-in PDF inspector, mammoth, and officeparser; optional OCR provider for scanned pages",
|
|
66
66
|
examples: [
|
|
67
67
|
"npx webcrawl scrape https://arxiv.org/pdf/1706.03762.pdf",
|
|
68
68
|
],
|
package/dist/format.js
CHANGED
|
@@ -49,7 +49,11 @@ 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) {
|
|
55
59
|
if (res.success === false && str(res.error)) {
|
package/dist/setup.js
CHANGED
|
@@ -147,19 +147,23 @@ 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}`
|
|
162
|
-
: "No local Chrome binary detected; static
|
|
165
|
+
? `Found Chrome at ${foundChrome} (used to render JS pages)`
|
|
166
|
+
: "No local Chrome binary detected; static pages still work. For JS pages install Chrome or `npx playwright install chromium-headless-shell`, or run `webcrawl login` for cloud stealth browsers",
|
|
163
167
|
});
|
|
164
168
|
// 4. Search providers
|
|
165
169
|
const serper = Boolean(env.SERPER_API_KEY);
|
|
@@ -180,7 +184,7 @@ export function runDoctor(env = process.env, effectiveMode = "local", apiKeySour
|
|
|
180
184
|
checks.push({
|
|
181
185
|
name: "Document & Media parsers",
|
|
182
186
|
status: "ok",
|
|
183
|
-
detail: "PDF
|
|
187
|
+
detail: "PDF inspector, Office (mammoth/officeparser), YouTube captions, NVD/OSV CVEs ready",
|
|
184
188
|
});
|
|
185
189
|
return { mode: effectiveMode, checks };
|
|
186
190
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcrawl",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
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.4",
|
|
131
|
+
"@webcrawl/curl-impersonate": "^1.4.4",
|
|
132
132
|
"ai": "^7.0.150",
|
|
133
133
|
"impit": "^0.14.7",
|
|
134
134
|
"mammoth": "^1.13.0",
|