@webcrawl/mcp 1.4.3 → 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/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 +21 -7
- package/dist/core/engine/chrome.js +146 -78
- package/dist/core/extract/index.d.ts +1 -1
- package/dist/core/extract/index.js +1 -1
- package/dist/core/extract/structured.d.ts +19 -0
- package/dist/core/extract/structured.js +192 -20
- package/dist/core/local-engine.d.ts +6 -0
- package/dist/core/local-engine.js +138 -79
- package/dist/core/search/catalog.d.ts +1 -1
- package/dist/core/search/catalog.js +1 -1
- package/dist/core/search/pipeline.js +5 -3
- package/dist/core/search/providers.d.ts +15 -3
- package/dist/core/search/providers.js +177 -39
- package/dist/core/targets/registry.js +1 -1
- package/dist/server.js +3 -0
- package/package.json +3 -3
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Every community improvement to extractors, parsers, or targets in `src/` immediately
|
|
10
10
|
* powers both this local engine (`npx webcrawl`) and the hosted `webcrawl.app` API.
|
|
11
11
|
*/
|
|
12
|
-
import { extractWebsiteData, fastHtmlToMarkdown, fetchWithTlsImpersonation, getCachedPageAsync, isChallengeMarkdown, isLocalChromeAvailable, mapWebsiteUrls,
|
|
12
|
+
import { extractWebsiteData, fastHtmlToMarkdown, fetchWithTlsImpersonation, getCachedPageAsync, isChallengeMarkdown, isLocalChromeAvailable, mapWebsiteUrls, renderViaLocalChromeDetailed, setCachedPage, sha256, } from "./engine.js";
|
|
13
13
|
import { isDomainRenderNeeded, markDomainRenderNeeded, } from "./engine/session.js";
|
|
14
14
|
import { isRenderProviderEnabled, renderViaProvider } from "./engine/render.js";
|
|
15
15
|
import { fetchResource, isAntiBotChallenge, lastFetchTier, needsBrowserEscalation, } from "./engine/fetcher.js";
|
|
@@ -20,7 +20,7 @@ import { parseYouTubeCached, youtubeVideoId } from "./parse/youtube.js";
|
|
|
20
20
|
import { rankDocuments } from "./ranker.js";
|
|
21
21
|
import { getSearchCategoriesCatalog, searchWeb } from "./search.js";
|
|
22
22
|
import { inspectTarget, listTargets, resolveTarget } from "./targets.js";
|
|
23
|
-
import { hasCustomExtractRequest } from "./extract/structured.js";
|
|
23
|
+
import { hasCustomExtractRequest, isEmptyExtractResult, } from "./extract/structured.js";
|
|
24
24
|
/** Lowercased hostname of a URL (memo key for the Tier R render memo). */
|
|
25
25
|
function safeDomainOf(url) {
|
|
26
26
|
try {
|
|
@@ -53,6 +53,34 @@ export function blockedScrapeResult(url, statusCode, tier, latencyMs, reason) {
|
|
|
53
53
|
evidence: { tier, blocked: true, statusCode, latencyMs, limitations: [error] },
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* JS page whose renderer failed. `success: false` so callers never treat the
|
|
58
|
+
* unrendered nav shell as content (v1.4.3 smoke test: 114 chars of nav with
|
|
59
|
+
* success:true after Chrome hung on --dump-dom).
|
|
60
|
+
*/
|
|
61
|
+
export function jsRenderFailedResult(url, statusCode, tier, latencyMs, reason) {
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
mode: "local",
|
|
65
|
+
url,
|
|
66
|
+
blocked: false,
|
|
67
|
+
needsJs: true,
|
|
68
|
+
statusCode,
|
|
69
|
+
error: reason,
|
|
70
|
+
title: url,
|
|
71
|
+
markdown: "",
|
|
72
|
+
links: [],
|
|
73
|
+
data: { url, title: url, markdown: "", links: [] },
|
|
74
|
+
evidence: {
|
|
75
|
+
tier,
|
|
76
|
+
blocked: false,
|
|
77
|
+
needsJs: true,
|
|
78
|
+
statusCode,
|
|
79
|
+
latencyMs,
|
|
80
|
+
limitations: [reason],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
56
84
|
/** Shells shorter than this are unrendered JS / empty pages and must not be cached as content. */
|
|
57
85
|
export const MIN_CACHEABLE_MARKDOWN = 200;
|
|
58
86
|
/** Attach source HTML when the caller asked for the `html` format. */
|
|
@@ -82,33 +110,55 @@ function emptyPageError(statusCode) {
|
|
|
82
110
|
"The site may be blocking automated clients or serving a client-rendered shell. " +
|
|
83
111
|
"Retry with `--mode cloud` (hosted stealth browser), or self-host a render provider via WEBCRAWL_RENDER_URL.");
|
|
84
112
|
}
|
|
113
|
+
function defaultJsRenderError() {
|
|
114
|
+
return ("Page needs JavaScript rendering; local Chrome was not available or produced no content. " +
|
|
115
|
+
"Install Chrome (see `webcrawl doctor`), point WEBCRAWL_CHROME_PATH at chrome-headless-shell " +
|
|
116
|
+
"(`npx playwright install chromium-headless-shell`), run `webcrawl login` and retry with `--mode cloud`, " +
|
|
117
|
+
"or self-host a render provider via WEBCRAWL_RENDER_URL.");
|
|
118
|
+
}
|
|
85
119
|
/**
|
|
86
|
-
* JS / challenge shell → render provider, then local Chrome dump-dom,
|
|
87
|
-
* Returns null when no renderer produced real content.
|
|
120
|
+
* JS / challenge shell → render provider, then local Chrome (CDP, then dump-dom),
|
|
121
|
+
* then stealth. Returns null when no renderer produced real content.
|
|
88
122
|
*/
|
|
89
123
|
async function escalateJsRender(url) {
|
|
124
|
+
const errors = [];
|
|
90
125
|
if (isRenderProviderEnabled()) {
|
|
91
126
|
const rendered = await renderViaProvider(url);
|
|
92
127
|
if (rendered && rendered.markdown.trim().length >= 40) {
|
|
93
128
|
return {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
129
|
+
page: {
|
|
130
|
+
title: rendered.title,
|
|
131
|
+
markdown: rendered.markdown,
|
|
132
|
+
html: "",
|
|
133
|
+
links: [],
|
|
134
|
+
tier: "render",
|
|
135
|
+
},
|
|
99
136
|
};
|
|
100
137
|
}
|
|
138
|
+
errors.push("render provider produced no content");
|
|
101
139
|
}
|
|
102
140
|
if (isLocalChromeAvailable()) {
|
|
103
|
-
const
|
|
104
|
-
if (html && html.length > 100) {
|
|
105
|
-
const parsed = fastHtmlToMarkdown(html, url);
|
|
141
|
+
const chrome = await renderViaLocalChromeDetailed(url);
|
|
142
|
+
if (chrome.html && chrome.html.length > 100) {
|
|
143
|
+
const parsed = fastHtmlToMarkdown(chrome.html, url);
|
|
106
144
|
if (parsed.markdown.trim().length > 40 &&
|
|
107
|
-
!isAntiBotChallenge(html, 200) &&
|
|
145
|
+
!isAntiBotChallenge(chrome.html, 200) &&
|
|
108
146
|
!isChallengeMarkdown(parsed.title, parsed.markdown)) {
|
|
109
|
-
return { ...parsed, html, tier: "browser" };
|
|
147
|
+
return { page: { ...parsed, html: chrome.html, tier: "browser" } };
|
|
110
148
|
}
|
|
149
|
+
errors.push(chrome.method
|
|
150
|
+
? `${chrome.method} rendered HTML but markdown was still a shell`
|
|
151
|
+
: "local Chrome rendered HTML but markdown was still a shell");
|
|
152
|
+
}
|
|
153
|
+
else if (chrome.error) {
|
|
154
|
+
errors.push(chrome.error);
|
|
111
155
|
}
|
|
156
|
+
else {
|
|
157
|
+
errors.push("local Chrome produced no content");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
errors.push("no local Chrome/Chromium binary");
|
|
112
162
|
}
|
|
113
163
|
try {
|
|
114
164
|
const { stealthAvailable } = await import("./engine/stealth.js");
|
|
@@ -119,19 +169,22 @@ async function escalateJsRender(url) {
|
|
|
119
169
|
esc.markdown.trim().length > 40 &&
|
|
120
170
|
!isChallengeResult(esc.html, esc.markdown, esc.statusCode)) {
|
|
121
171
|
return {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
172
|
+
page: {
|
|
173
|
+
title: esc.title,
|
|
174
|
+
markdown: esc.markdown,
|
|
175
|
+
html: esc.html,
|
|
176
|
+
links: esc.links,
|
|
177
|
+
tier: esc.tier,
|
|
178
|
+
},
|
|
127
179
|
};
|
|
128
180
|
}
|
|
181
|
+
errors.push("stealth browser produced no content");
|
|
129
182
|
}
|
|
130
183
|
}
|
|
131
184
|
catch {
|
|
132
185
|
/* stealth optional */
|
|
133
186
|
}
|
|
134
|
-
return null;
|
|
187
|
+
return { page: null, error: errors.filter(Boolean).join("; ") || defaultJsRenderError() };
|
|
135
188
|
}
|
|
136
189
|
/**
|
|
137
190
|
* Apply `map --filter` / `search` after discovery, then honour `-n` / `limit`.
|
|
@@ -329,12 +382,15 @@ export function createLocalEngineClient() {
|
|
|
329
382
|
const needsRender = needsBrowserEscalation(fetched.status, html, markdown);
|
|
330
383
|
const nativeTier = nativeScrapeTier();
|
|
331
384
|
// JS shell / challenge: memoize the domain and escalate to a renderer
|
|
332
|
-
// (hosted provider, local Chrome
|
|
333
|
-
// the unrendered shell.
|
|
385
|
+
// (hosted provider, local Chrome CDP, then stealth). Never cache
|
|
386
|
+
// the unrendered shell, and never report the shell as success:true.
|
|
334
387
|
if (needsRender) {
|
|
335
388
|
await markDomainRenderNeeded(renderDomain);
|
|
336
389
|
// 403/429/503 is already a refusal — don't spend a Chrome launch on the block page.
|
|
337
|
-
const
|
|
390
|
+
const escalated = blocked && fetched.status >= 400
|
|
391
|
+
? { page: null, error: undefined }
|
|
392
|
+
: await escalateJsRender(targetUrl);
|
|
393
|
+
const rendered = escalated.page;
|
|
338
394
|
if (rendered) {
|
|
339
395
|
const renderHash = sha256(rendered.markdown);
|
|
340
396
|
if (rendered.markdown.trim().length >= MIN_CACHEABLE_MARKDOWN) {
|
|
@@ -369,43 +425,10 @@ export function createLocalEngineClient() {
|
|
|
369
425
|
},
|
|
370
426
|
}, rendered.html, p.formats);
|
|
371
427
|
}
|
|
372
|
-
if (blocked
|
|
373
|
-
return blockedScrapeResult(targetUrl, fetched.status, nativeTier, Date.now() - start
|
|
428
|
+
if (blocked) {
|
|
429
|
+
return blockedScrapeResult(targetUrl, fetched.status, nativeTier, Date.now() - start);
|
|
374
430
|
}
|
|
375
|
-
|
|
376
|
-
url: targetUrl,
|
|
377
|
-
html,
|
|
378
|
-
markdown,
|
|
379
|
-
});
|
|
380
|
-
return withOptionalHtml({
|
|
381
|
-
success: true,
|
|
382
|
-
mode: "local",
|
|
383
|
-
url: targetUrl,
|
|
384
|
-
title: title || targetInspection.title || targetUrl,
|
|
385
|
-
markdown: targetInspection.markdown || markdown,
|
|
386
|
-
links,
|
|
387
|
-
data: {
|
|
388
|
-
url: targetUrl,
|
|
389
|
-
title: title || targetInspection.title || targetUrl,
|
|
390
|
-
markdown: targetInspection.markdown || markdown,
|
|
391
|
-
links,
|
|
392
|
-
structured: targetInspection.structured,
|
|
393
|
-
},
|
|
394
|
-
evidence: {
|
|
395
|
-
...targetInspection.evidence,
|
|
396
|
-
tier: nativeTier,
|
|
397
|
-
contentHash: sha256(markdown || html),
|
|
398
|
-
latencyMs: Date.now() - start,
|
|
399
|
-
limitations: [
|
|
400
|
-
...(Array.isArray(targetInspection.evidence?.limitations)
|
|
401
|
-
? targetInspection.evidence.limitations
|
|
402
|
-
: []),
|
|
403
|
-
"Page needs JS rendering; local Chrome was not available or produced no content. " +
|
|
404
|
-
"Install Chrome (see `webcrawl doctor`), run `webcrawl login` and retry with `--mode cloud`, " +
|
|
405
|
-
"or self-host a render provider via WEBCRAWL_RENDER_URL.",
|
|
406
|
-
],
|
|
407
|
-
},
|
|
408
|
-
}, html, p.formats);
|
|
431
|
+
return jsRenderFailedResult(targetUrl, fetched.status, nativeTier, Date.now() - start, escalated.error || (!markdown.trim() ? emptyPageError(fetched.status) : defaultJsRenderError()));
|
|
409
432
|
}
|
|
410
433
|
if (blocked || !markdown.trim()) {
|
|
411
434
|
return blockedScrapeResult(targetUrl, fetched.status, nativeTier, Date.now() - start, blocked ? undefined : emptyPageError(fetched.status));
|
|
@@ -557,22 +580,38 @@ export function createLocalEngineClient() {
|
|
|
557
580
|
formats: ["markdown", "html"],
|
|
558
581
|
noCache: true,
|
|
559
582
|
}));
|
|
560
|
-
|
|
561
|
-
|
|
583
|
+
let markdown = (scraped.data && scraped.data.markdown) || scraped.markdown || "";
|
|
584
|
+
let html = (scraped.data && scraped.data.html) || scraped.html || "";
|
|
562
585
|
if (scraped.blocked || scraped.success === false) {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
586
|
+
// Bot-blocked pages stay failures. A JS shell whose Chrome render failed
|
|
587
|
+
// can still yield structured data from JSON islands once script tags are
|
|
588
|
+
// stripped for card matching — never from `document.write` templates.
|
|
589
|
+
if (scraped.needsJs && scraped.blocked !== true) {
|
|
590
|
+
try {
|
|
591
|
+
const fetched = await fetchWithTlsImpersonation(targetUrl);
|
|
592
|
+
html = fetched.html || html;
|
|
593
|
+
const parsedShell = fastHtmlToMarkdown(html, targetUrl);
|
|
594
|
+
markdown = parsedShell.markdown || markdown;
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
/* keep whatever scrape returned */
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
return {
|
|
602
|
+
success: false,
|
|
603
|
+
blocked: true,
|
|
604
|
+
mode: "local",
|
|
605
|
+
url: targetUrl,
|
|
606
|
+
data: {},
|
|
607
|
+
error: scraped.error || emptyPageError(200),
|
|
608
|
+
evidence: {
|
|
609
|
+
tier: lastFetchTier(),
|
|
610
|
+
sourceUrl: targetUrl,
|
|
611
|
+
limitations: [String(scraped.error || emptyPageError(200))],
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
}
|
|
576
615
|
}
|
|
577
616
|
const targetInspection = await inspectTarget({
|
|
578
617
|
url: targetUrl,
|
|
@@ -587,15 +626,35 @@ export function createLocalEngineClient() {
|
|
|
587
626
|
bypassCache: true,
|
|
588
627
|
});
|
|
589
628
|
const { cached: _c, cacheLayer: _l, ...extracted } = siteData;
|
|
629
|
+
const data = extractOutputData(extracted, {
|
|
630
|
+
prompt: p.prompt,
|
|
631
|
+
schema: p.schema,
|
|
632
|
+
target: targetInspection.structured,
|
|
633
|
+
});
|
|
634
|
+
if (hasCustomExtractRequest(p.prompt, p.schema) && isEmptyExtractResult(data)) {
|
|
635
|
+
const error = "Extraction produced no data matching the prompt/schema. " +
|
|
636
|
+
"If this is a JS-rendered page, install Chrome (see `webcrawl doctor`) and retry; " +
|
|
637
|
+
"empty objects are not reported as success.";
|
|
638
|
+
return {
|
|
639
|
+
success: false,
|
|
640
|
+
mode: "local",
|
|
641
|
+
url: targetUrl,
|
|
642
|
+
data: {},
|
|
643
|
+
error,
|
|
644
|
+
evidence: {
|
|
645
|
+
...targetInspection.evidence,
|
|
646
|
+
limitations: [
|
|
647
|
+
...(targetInspection.evidence?.limitations ?? []),
|
|
648
|
+
error,
|
|
649
|
+
],
|
|
650
|
+
},
|
|
651
|
+
};
|
|
652
|
+
}
|
|
590
653
|
return {
|
|
591
654
|
success: true,
|
|
592
655
|
mode: "local",
|
|
593
656
|
url: targetUrl,
|
|
594
|
-
data
|
|
595
|
-
prompt: p.prompt,
|
|
596
|
-
schema: p.schema,
|
|
597
|
-
target: targetInspection.structured,
|
|
598
|
-
}),
|
|
657
|
+
data,
|
|
599
658
|
evidence: targetInspection.evidence,
|
|
600
659
|
};
|
|
601
660
|
},
|
|
@@ -52,7 +52,7 @@ export interface SearchRequest {
|
|
|
52
52
|
bypassCache?: boolean;
|
|
53
53
|
/**
|
|
54
54
|
* Pick one search provider and skip the default multi-engine pipeline.
|
|
55
|
-
* `auto` (default) uses the free engines and falls back to
|
|
55
|
+
* `auto` (default) uses the free engines and falls back to a keyed provider when results are thin.
|
|
56
56
|
*/
|
|
57
57
|
provider?: SearchProviderChoice;
|
|
58
58
|
rankOptions?: {
|
|
@@ -11,7 +11,7 @@ export const SEARCH_PROVIDER_ENV_KEYS = {
|
|
|
11
11
|
export const SEARCH_CATEGORY_CATALOG = {
|
|
12
12
|
general: {
|
|
13
13
|
description: "Broad web search across general-purpose engines.",
|
|
14
|
-
engines: ["brave-native", "ddg-lite", "wikipedia"],
|
|
14
|
+
engines: ["brave-native", "ddg-lite", "wikipedia", "hn-algolia", "bing-rss"],
|
|
15
15
|
localizeBy: "none",
|
|
16
16
|
preferLocalDomains: false,
|
|
17
17
|
subcategories: {
|
|
@@ -115,7 +115,9 @@ export async function searchWeb(req) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
const perEngineLimit = Math.min(Math.max(limit + 4, 8), 15);
|
|
118
|
-
|
|
118
|
+
// Must outlast DDG/Brave impersonation (2200ms) so a slow SERP is not cut off
|
|
119
|
+
// after Wikipedia's 2 hits — the v1.4.3 keyless-search failure mode.
|
|
120
|
+
const quorumTimeoutMs = req.rankOptions?.quorumTimeoutMs ?? 2800;
|
|
119
121
|
// Stage 1: Hedged Multi-Engine Native Federation
|
|
120
122
|
const collectedOffers = [];
|
|
121
123
|
const offerByUrl = new Map();
|
|
@@ -257,7 +259,7 @@ export async function searchWeb(req) {
|
|
|
257
259
|
filterContext,
|
|
258
260
|
};
|
|
259
261
|
let ranked = await rankDocuments(localizedSearchQuery, rankCandidates, rankOpts);
|
|
260
|
-
// Thin result set after ranking and filtering: ask
|
|
262
|
+
// Thin result set after ranking and filtering: ask the keyed search provider once, then re-rank.
|
|
261
263
|
const minResults = Math.min(3, limit);
|
|
262
264
|
if ((!req.provider || req.provider === "auto") &&
|
|
263
265
|
ranked.results.length < minResults &&
|
|
@@ -649,7 +651,7 @@ export async function searchWeb(req) {
|
|
|
649
651
|
...(matchedCves?.length ? { cves: matchedCves } : {}),
|
|
650
652
|
};
|
|
651
653
|
});
|
|
652
|
-
// Charge the fallback surcharge only when a
|
|
654
|
+
// Charge the fallback surcharge only when a keyed-provider result actually reached the caller.
|
|
653
655
|
// An explicitly chosen provider is not a fallback.
|
|
654
656
|
const usedJinaFallback = (!req.provider || req.provider === "auto") &&
|
|
655
657
|
finalItems.some((item) => item.sources?.includes("jina"));
|
|
@@ -14,18 +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
19
|
/** True when a scraped SERP is a bot-check / captcha rather than results. */
|
|
20
20
|
export declare function isSerpChallengeHtml(html: string, status?: number): boolean;
|
|
21
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>;
|
|
22
31
|
/** Parse Bing's `format=rss` web-search feed into candidate docs. */
|
|
23
32
|
export declare function parseBingWebRss(xml: string, limit: number): RawCandidateDocument[];
|
|
24
33
|
/**
|
|
25
|
-
* Last-resort keyless engines used when DDG/Brave HTML SERPs come back empty
|
|
26
|
-
* Wikipedia OpenSearch, HN Algolia,
|
|
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.
|
|
27
37
|
*/
|
|
28
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[]>;
|
|
29
41
|
export declare function fetchJinaSearch(query: string, limit: number, req?: {
|
|
30
42
|
country?: string;
|
|
31
43
|
lang?: string;
|