webcrawl 1.2.4 → 1.2.7
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 +3 -2
- package/dist/core/engine/awswaf.d.ts +37 -0
- package/dist/core/engine/awswaf.js +189 -0
- package/dist/core/engine/cache.d.ts +70 -0
- package/dist/core/engine/cache.js +297 -0
- package/dist/core/engine/crawl-policy.d.ts +36 -0
- package/dist/core/engine/crawl-policy.js +134 -0
- package/dist/core/engine/crawler.d.ts +151 -0
- package/dist/core/engine/crawler.js +885 -0
- package/dist/core/engine/fetcher.d.ts +66 -0
- package/dist/core/engine/fetcher.js +185 -0
- package/dist/core/engine/index.d.ts +4 -0
- package/dist/core/engine/index.js +4 -0
- package/dist/core/engine/markdown.d.ts +33 -0
- package/dist/core/engine/markdown.js +334 -0
- package/dist/core/engine/path-scope.d.ts +21 -0
- package/dist/core/engine/path-scope.js +88 -0
- package/dist/core/engine/safe-url.d.ts +23 -0
- package/dist/core/engine/safe-url.js +92 -0
- package/dist/core/engine/stealth.d.ts +79 -0
- package/dist/core/engine/stealth.js +155 -0
- package/dist/core/engine.d.ts +1 -0
- package/dist/core/engine.js +1 -0
- package/dist/core/extract/business.d.ts +28 -0
- package/dist/core/extract/business.js +344 -0
- package/dist/core/extract/currency.d.ts +15 -0
- package/dist/core/extract/currency.js +259 -0
- package/dist/core/extract/cve-cache.d.ts +62 -0
- package/dist/core/extract/cve-cache.js +277 -0
- package/dist/core/extract/cve.d.ts +57 -0
- package/dist/core/extract/cve.js +397 -0
- package/dist/core/extract/dedupe-key.d.ts +12 -0
- package/dist/core/extract/dedupe-key.js +36 -0
- package/dist/core/extract/flights.d.ts +37 -0
- package/dist/core/extract/flights.js +373 -0
- package/dist/core/extract/hotels.d.ts +35 -0
- package/dist/core/extract/hotels.js +473 -0
- package/dist/core/extract/index.d.ts +8 -0
- package/dist/core/extract/index.js +8 -0
- package/dist/core/extract/real-estate.d.ts +53 -0
- package/dist/core/extract/real-estate.js +835 -0
- package/dist/core/extract/scaledown.d.ts +14 -0
- package/dist/core/extract/scaledown.js +484 -0
- package/dist/core/extract/shopping.d.ts +44 -0
- package/dist/core/extract/shopping.js +393 -0
- package/dist/core/image-url.d.ts +5 -0
- package/dist/core/image-url.js +13 -0
- package/dist/core/interact/driver.d.ts +49 -0
- package/dist/core/interact/driver.js +264 -0
- package/dist/core/interact/remote.d.ts +41 -0
- package/dist/core/interact/remote.js +133 -0
- package/dist/core/interact/spans.d.ts +46 -0
- package/dist/core/interact/spans.js +32 -0
- package/dist/core/interact/validate.d.ts +34 -0
- package/dist/core/interact/validate.js +135 -0
- package/dist/core/interact/vm.d.ts +42 -0
- package/dist/core/interact/vm.js +296 -0
- package/dist/core/local-engine.d.ts +35 -0
- package/dist/core/local-engine.js +367 -0
- package/dist/core/parse/detect.d.ts +29 -0
- package/dist/core/parse/detect.js +152 -0
- package/dist/core/parse/fetch.d.ts +20 -0
- package/dist/core/parse/fetch.js +39 -0
- package/dist/core/parse/image.d.ts +13 -0
- package/dist/core/parse/image.js +100 -0
- package/dist/core/parse/index.d.ts +53 -0
- package/dist/core/parse/index.js +153 -0
- package/dist/core/parse/limits.d.ts +12 -0
- package/dist/core/parse/limits.js +29 -0
- package/dist/core/parse/media.d.ts +42 -0
- package/dist/core/parse/media.js +238 -0
- package/dist/core/parse/merge.d.ts +19 -0
- package/dist/core/parse/merge.js +48 -0
- package/dist/core/parse/ocr.d.ts +25 -0
- package/dist/core/parse/ocr.js +121 -0
- package/dist/core/parse/office.d.ts +28 -0
- package/dist/core/parse/office.js +83 -0
- package/dist/core/parse/pdf.d.ts +27 -0
- package/dist/core/parse/pdf.js +156 -0
- package/dist/core/parse/types.d.ts +38 -0
- package/dist/core/parse/types.js +1 -0
- package/dist/core/parse/youtube.d.ts +30 -0
- package/dist/core/parse/youtube.js +264 -0
- package/dist/core/ranker/decision.d.ts +28 -0
- package/dist/core/ranker/decision.js +151 -0
- package/dist/core/ranker/index.d.ts +5 -0
- package/dist/core/ranker/index.js +361 -0
- package/dist/core/ranker/lexical.d.ts +82 -0
- package/dist/core/ranker/lexical.js +642 -0
- package/dist/core/ranker/types.d.ts +90 -0
- package/dist/core/ranker/types.js +1 -0
- package/dist/core/ranker.d.ts +1 -0
- package/dist/core/ranker.js +1 -0
- package/dist/core/sandbox.d.ts +29 -0
- package/dist/core/sandbox.js +182 -0
- package/dist/core/search/catalog.d.ts +174 -0
- package/dist/core/search/catalog.js +488 -0
- package/dist/core/search/index.d.ts +3 -0
- package/dist/core/search/index.js +3 -0
- package/dist/core/search/pipeline.d.ts +7 -0
- package/dist/core/search/pipeline.js +704 -0
- package/dist/core/search/providers.d.ts +62 -0
- package/dist/core/search/providers.js +1175 -0
- package/dist/core/search.d.ts +1 -0
- package/dist/core/search.js +1 -0
- package/dist/core/targets/developer.d.ts +44 -0
- package/dist/core/targets/developer.js +338 -0
- package/dist/core/targets/index.d.ts +3 -0
- package/dist/core/targets/index.js +3 -0
- package/dist/core/targets/registry.d.ts +32 -0
- package/dist/core/targets/registry.js +411 -0
- package/dist/core/targets/types.d.ts +72 -0
- package/dist/core/targets/types.js +8 -0
- package/dist/core/targets.d.ts +1 -0
- package/dist/core/targets.js +1 -0
- package/package.json +82 -4
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crawl politeness helpers: robots.txt rules, adaptive concurrency (AIMD) and retries.
|
|
3
|
+
* Pure and dependency-free so they are easy to test.
|
|
4
|
+
*/
|
|
5
|
+
const ROBOT_TOKEN = "webcrawl";
|
|
6
|
+
function patternToRegExp(path) {
|
|
7
|
+
const anchored = path.endsWith("$");
|
|
8
|
+
const body = (anchored ? path.slice(0, -1) : path)
|
|
9
|
+
.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
|
|
10
|
+
.replace(/\*/g, ".*");
|
|
11
|
+
return new RegExp(`^${body}${anchored ? "$" : ""}`);
|
|
12
|
+
}
|
|
13
|
+
/** Parses robots.txt. Our own group wins over `*`; longest matching rule wins; Allow beats Disallow on ties. */
|
|
14
|
+
export function parseRobots(text) {
|
|
15
|
+
const groups = [];
|
|
16
|
+
let current = null;
|
|
17
|
+
let lastWasAgent = false;
|
|
18
|
+
for (const rawLine of (text ?? "").split(/\r?\n/)) {
|
|
19
|
+
const line = rawLine.replace(/#.*$/, "").trim();
|
|
20
|
+
if (!line)
|
|
21
|
+
continue;
|
|
22
|
+
const idx = line.indexOf(":");
|
|
23
|
+
if (idx < 0)
|
|
24
|
+
continue;
|
|
25
|
+
const key = line.slice(0, idx).trim().toLowerCase();
|
|
26
|
+
const value = line.slice(idx + 1).trim();
|
|
27
|
+
if (key === "user-agent") {
|
|
28
|
+
if (!current || !lastWasAgent) {
|
|
29
|
+
current = { agents: [], rules: [], delay: 0 };
|
|
30
|
+
groups.push(current);
|
|
31
|
+
}
|
|
32
|
+
current.agents.push(value.toLowerCase());
|
|
33
|
+
lastWasAgent = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
lastWasAgent = false;
|
|
37
|
+
if (!current)
|
|
38
|
+
continue;
|
|
39
|
+
if (key === "disallow" || key === "allow") {
|
|
40
|
+
if (!value && key === "disallow")
|
|
41
|
+
continue; // empty Disallow = allow all
|
|
42
|
+
current.rules.push({
|
|
43
|
+
allow: key === "allow",
|
|
44
|
+
pattern: patternToRegExp(value),
|
|
45
|
+
length: value.length,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
else if (key === "crawl-delay") {
|
|
49
|
+
const n = Number(value);
|
|
50
|
+
if (Number.isFinite(n) && n > 0)
|
|
51
|
+
current.delay = Math.min(n, 10) * 1000;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const mine = groups.filter((g) => g.agents.some((a) => a !== "*" && ROBOT_TOKEN.includes(a)));
|
|
55
|
+
const chosen = mine.length ? mine : groups.filter((g) => g.agents.includes("*"));
|
|
56
|
+
const rules = chosen.flatMap((g) => g.rules);
|
|
57
|
+
const crawlDelayMs = Math.max(0, ...chosen.map((g) => g.delay));
|
|
58
|
+
return {
|
|
59
|
+
crawlDelayMs,
|
|
60
|
+
allows(url) {
|
|
61
|
+
let path;
|
|
62
|
+
try {
|
|
63
|
+
const u = new URL(url);
|
|
64
|
+
path = u.pathname + u.search;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
let best = null;
|
|
70
|
+
for (const r of rules) {
|
|
71
|
+
if (!r.pattern.test(path))
|
|
72
|
+
continue;
|
|
73
|
+
if (!best || r.length > best.length || (r.length === best.length && r.allow))
|
|
74
|
+
best = r;
|
|
75
|
+
}
|
|
76
|
+
return best ? best.allow : true;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Additive-increase / multiplicative-decrease concurrency controller. */
|
|
81
|
+
export class AdaptiveConcurrency {
|
|
82
|
+
min;
|
|
83
|
+
max;
|
|
84
|
+
target;
|
|
85
|
+
constructor(min = 1, max = 16, initial = 4) {
|
|
86
|
+
this.min = min;
|
|
87
|
+
this.max = max;
|
|
88
|
+
this.target = Math.min(Math.max(initial, min), max);
|
|
89
|
+
}
|
|
90
|
+
get current() {
|
|
91
|
+
return this.target;
|
|
92
|
+
}
|
|
93
|
+
/** Feed the outcome of a finished wave. */
|
|
94
|
+
record(outcome) {
|
|
95
|
+
const total = outcome.ok + outcome.failed;
|
|
96
|
+
if (outcome.throttled || (total > 0 && outcome.failed / total > 0.3)) {
|
|
97
|
+
this.target = Math.max(this.min, Math.floor(this.target / 2));
|
|
98
|
+
}
|
|
99
|
+
else if (outcome.ok > 0) {
|
|
100
|
+
this.target = Math.min(this.max, this.target + 1);
|
|
101
|
+
}
|
|
102
|
+
return this.target;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
106
|
+
export function backoffDelayMs(attempt, baseMs = 250, capMs = 4000, rand = Math.random) {
|
|
107
|
+
const exp = Math.min(capMs, baseMs * 2 ** attempt);
|
|
108
|
+
return Math.round(exp / 2 + rand() * (exp / 2));
|
|
109
|
+
}
|
|
110
|
+
export function isRetryableStatus(status) {
|
|
111
|
+
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
112
|
+
}
|
|
113
|
+
/** Retries transient failures (network errors, 408/425/429/5xx) with jittered exponential backoff. */
|
|
114
|
+
export async function withRetries(run, opts = {}) {
|
|
115
|
+
const retries = opts.retries ?? 2;
|
|
116
|
+
const wait = opts.sleepFn ?? sleep;
|
|
117
|
+
let lastErr;
|
|
118
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
119
|
+
try {
|
|
120
|
+
const res = await run();
|
|
121
|
+
if (!isRetryableStatus(res.status) || attempt === retries)
|
|
122
|
+
return res;
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
if (err?.name === "UnsafeUrlError")
|
|
126
|
+
throw err;
|
|
127
|
+
lastErr = err;
|
|
128
|
+
if (attempt === retries)
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
await wait(backoffDelayMs(attempt, opts.baseMs));
|
|
132
|
+
}
|
|
133
|
+
throw lastErr;
|
|
134
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
export interface MapWebsiteOptions {
|
|
2
|
+
/** Only crawl the exact root/target domain (default: true when includeSubdomains is false and selectedDomains is empty) */
|
|
3
|
+
onlyRootDomain?: boolean;
|
|
4
|
+
/** Also crawl subdomains of the allowed domain(s), e.g. docs.example.com (default: false) */
|
|
5
|
+
includeSubdomains?: boolean;
|
|
6
|
+
/** Explicit allowlist of domains/subdomains (supports exact hosts like "docs.example.com" or wildcards like "*.example.com") */
|
|
7
|
+
selectedDomains?: string[];
|
|
8
|
+
/** Maximum number of discovered URLs to return (default: 5000) */
|
|
9
|
+
limit?: number;
|
|
10
|
+
/** Maximum number of HTML pages to fetch during multi-hop BFS crawl when sitemap is missing (default: 250) */
|
|
11
|
+
maxCrawlPages?: number;
|
|
12
|
+
/** Alias for maxCrawlPages */
|
|
13
|
+
maxCrawl?: number;
|
|
14
|
+
/** Maximum BFS hop depth from the entry URL (default: 8) */
|
|
15
|
+
maxDepth?: number;
|
|
16
|
+
/** Only keep URLs whose path matches one of these (prefix, `*` wildcards, trailing `$`). */
|
|
17
|
+
includePaths?: string[];
|
|
18
|
+
/** Drop URLs whose path matches any of these. Wins over includePaths. */
|
|
19
|
+
excludePaths?: string[];
|
|
20
|
+
/** Stay under the start URL's path when no includePaths are given (default: true). */
|
|
21
|
+
restrictToSubpath?: boolean;
|
|
22
|
+
/** Sitemaps to read first (e.g. a site whose sitemap is not at /sitemap.xml). */
|
|
23
|
+
sitemapUrls?: string[];
|
|
24
|
+
/** Overall map timeout in milliseconds (default: 25000) */
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
/** Bypass 24-hour map cache (default: false) */
|
|
27
|
+
bypassCache?: boolean;
|
|
28
|
+
/** When a bot check blocks plain requests, find pages through a real browser (default: true) */
|
|
29
|
+
stealth?: boolean;
|
|
30
|
+
/** Time budget for the stealth browser session (default: 75000) */
|
|
31
|
+
stealthBudgetMs?: number;
|
|
32
|
+
/** Allow the paid remote stealth provider (default true). Off for unbilled runs. */
|
|
33
|
+
remoteStealth?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function mapWebsiteUrls(targetUrl: string, options?: MapWebsiteOptions): Promise<{
|
|
36
|
+
success: boolean;
|
|
37
|
+
links: string[];
|
|
38
|
+
latencyMs: number;
|
|
39
|
+
source: string;
|
|
40
|
+
cached?: boolean;
|
|
41
|
+
cacheLayer?: "memory" | "disk" | "postgres" | "miss";
|
|
42
|
+
pagesCrawled?: number;
|
|
43
|
+
/** The site answered plain requests with a bot check. */
|
|
44
|
+
blocked?: boolean;
|
|
45
|
+
/** Pages were found through the stealth browser. */
|
|
46
|
+
stealth?: boolean;
|
|
47
|
+
/** What a remote stealth provider charged for discovery, in USD. */
|
|
48
|
+
stealthCostUsd?: number;
|
|
49
|
+
scope?: {
|
|
50
|
+
onlyRootDomain: boolean;
|
|
51
|
+
includeSubdomains: boolean;
|
|
52
|
+
selectedDomains: string[];
|
|
53
|
+
maxCrawlPages: number;
|
|
54
|
+
maxDepth: number;
|
|
55
|
+
includePaths: string[];
|
|
56
|
+
excludePaths: string[];
|
|
57
|
+
};
|
|
58
|
+
}>;
|
|
59
|
+
/**
|
|
60
|
+
* Ultra-fast HTTP status batch check for `websiteDeletedPages.ts`.
|
|
61
|
+
* Checks if URLs return 404/410 without downloading/rendering full markdown!
|
|
62
|
+
*/
|
|
63
|
+
export declare function checkUrlsStatusBatch(urls: string[], timeoutMs?: number): Promise<Array<{
|
|
64
|
+
url: string;
|
|
65
|
+
statusCode: number;
|
|
66
|
+
ok: boolean;
|
|
67
|
+
}>>;
|
|
68
|
+
/**
|
|
69
|
+
* Extracts structured `{ app_name, product_features }` (or custom JSON schema) from a URL.
|
|
70
|
+
* Replaces `firecrawl.asyncExtract` in `website-extract.ts`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function extractWebsiteData(url: string, options?: {
|
|
73
|
+
prompt?: string;
|
|
74
|
+
schema?: Record<string, unknown>;
|
|
75
|
+
openaiApiKey?: string;
|
|
76
|
+
bypassCache?: boolean;
|
|
77
|
+
/** Already-extracted content (e.g. a parsed PDF); skips the HTML fetch and cache. */
|
|
78
|
+
markdown?: string;
|
|
79
|
+
}): Promise<{
|
|
80
|
+
app_name: string;
|
|
81
|
+
product_features: string[];
|
|
82
|
+
cached?: boolean;
|
|
83
|
+
cacheLayer?: "memory" | "disk" | "postgres" | "miss";
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* Full-website multi-page crawler (`POST /v1/crawl`) backed by the shared 3-tier
|
|
87
|
+
* L1 Memory -> L2 Disk -> L3 Postgres URL store (`crawled_pages`).
|
|
88
|
+
* Pages already stored in Postgres for this website are served directly without re-crawling!
|
|
89
|
+
*/
|
|
90
|
+
export declare function crawlWebsitePages(targetUrl: string, options?: {
|
|
91
|
+
maxPages?: number;
|
|
92
|
+
includeSubdomains?: boolean;
|
|
93
|
+
onlyRootDomain?: boolean;
|
|
94
|
+
bypassCache?: boolean;
|
|
95
|
+
extractStructured?: boolean;
|
|
96
|
+
timeoutMs?: number;
|
|
97
|
+
}): Promise<{
|
|
98
|
+
success: boolean;
|
|
99
|
+
rootUrl: string;
|
|
100
|
+
pagesCount: number;
|
|
101
|
+
cachedPagesCount: number;
|
|
102
|
+
postgresHits: number;
|
|
103
|
+
latencyMs: number;
|
|
104
|
+
pages: CrawledPage[];
|
|
105
|
+
}>;
|
|
106
|
+
export interface CrawledPage {
|
|
107
|
+
url: string;
|
|
108
|
+
title: string;
|
|
109
|
+
markdown: string;
|
|
110
|
+
linksCount: number;
|
|
111
|
+
cached: boolean;
|
|
112
|
+
cacheLayer: "memory" | "disk" | "postgres" | "miss";
|
|
113
|
+
structuredData?: Record<string, unknown> | null;
|
|
114
|
+
statusCode?: number;
|
|
115
|
+
error?: string;
|
|
116
|
+
/** The site served a bot check instead of this page. */
|
|
117
|
+
blocked?: boolean;
|
|
118
|
+
/** Read through the stealth browser. */
|
|
119
|
+
stealth?: boolean;
|
|
120
|
+
/** Remote provider that hosted the stealth browser (billed with a surcharge). */
|
|
121
|
+
stealthProvider?: string;
|
|
122
|
+
}
|
|
123
|
+
/** True for markdown converted from a bot-check page (old cache entries). */
|
|
124
|
+
export declare function isChallengeMarkdown(title: string, markdown: string): boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Re-reads blocked pages through one stealth browser session. Pages that come
|
|
127
|
+
* back clean are cached and returned as content; pages still blocked keep
|
|
128
|
+
* `blocked: true` and an honest error.
|
|
129
|
+
*/
|
|
130
|
+
export declare function crawlPagesStealth(entryUrl: string, urls: string[], options?: {
|
|
131
|
+
extractStructured?: boolean;
|
|
132
|
+
budgetMs?: number;
|
|
133
|
+
remote?: boolean;
|
|
134
|
+
}): Promise<{
|
|
135
|
+
pages: CrawledPage[];
|
|
136
|
+
solved: boolean;
|
|
137
|
+
durationMs: number;
|
|
138
|
+
error?: string;
|
|
139
|
+
provider?: string;
|
|
140
|
+
providerCostUsd?: number;
|
|
141
|
+
}>;
|
|
142
|
+
/**
|
|
143
|
+
* Fetches one page for a crawl: cache first (unless bypassed), then a
|
|
144
|
+
* TLS-impersonated fetch. Never throws; failures come back with `error`.
|
|
145
|
+
* Used by `crawlWebsitePages` and by the durable crawl workflow (one call per page).
|
|
146
|
+
*/
|
|
147
|
+
export declare function crawlPage(pageUrl: string, options?: {
|
|
148
|
+
bypassCache?: boolean;
|
|
149
|
+
extractStructured?: boolean;
|
|
150
|
+
timeoutMs?: number;
|
|
151
|
+
}): Promise<CrawledPage>;
|