@webcrawl/mcp 1.4.3 → 1.4.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/dist/core/engine/chrome-cdp.d.ts +29 -0
- package/dist/core/engine/chrome-cdp.js +339 -0
- package/dist/core/engine/chrome-launch.d.ts +49 -0
- package/dist/core/engine/chrome-launch.js +244 -0
- package/dist/core/engine/chrome.d.ts +26 -7
- package/dist/core/engine/chrome.js +155 -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
|
@@ -1,14 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
export { DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, VIRTUAL_TIME_MS, discoverChromeBinaries, findPlaywrightHeadlessShell, installedChromePath, isLocalChromeAvailable, parseDevToolsActivePort, chromeCdpArgs, chromeDumpDomArgs, killProcessGroup, resolveChromeConnectTimeoutMs, resolveChromeTimeoutMs, type ChromeHeadlessMode, } from "./chrome-launch.js";
|
|
2
|
+
export { ChromeConnectError } from "./chrome-cdp.js";
|
|
3
|
+
export interface ChromeRenderAttempt {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
method: string;
|
|
6
|
+
ms: number;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ChromeRenderResult {
|
|
10
|
+
html: string | null;
|
|
11
|
+
method?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
attempts: ChromeRenderAttempt[];
|
|
14
|
+
}
|
|
8
15
|
/**
|
|
9
16
|
* Render `url` in local Chrome and return the hydrated HTML, or null when
|
|
10
17
|
* Chrome is missing, times out, or produces nothing.
|
|
11
18
|
*/
|
|
12
19
|
export declare function renderViaLocalChrome(url: string, opts?: {
|
|
13
20
|
timeoutMs?: number;
|
|
21
|
+
connectTimeoutMs?: number;
|
|
22
|
+
binaries?: string[];
|
|
14
23
|
}): Promise<string | null>;
|
|
24
|
+
/**
|
|
25
|
+
* Same as `renderViaLocalChrome` but with the attempt log and an actionable
|
|
26
|
+
* error when every driver fails. Callers that need to report honest JS-render
|
|
27
|
+
* failures should use this.
|
|
28
|
+
*/
|
|
29
|
+
export declare function renderViaLocalChromeDetailed(url: string, opts?: {
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
connectTimeoutMs?: number;
|
|
32
|
+
binaries?: string[];
|
|
33
|
+
}): Promise<ChromeRenderResult>;
|
|
@@ -1,131 +1,208 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Local Chrome
|
|
2
|
+
* Local Chrome renderer (JS pages the native fetch ladder cannot hydrate).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Primary driver is Chrome DevTools Protocol:
|
|
5
|
+
* launch headless Chrome, navigate, wait until the DOM is content-ready
|
|
6
|
+
* (load + 200ms mutation-idle), then read `documentElement.outerHTML`.
|
|
7
|
+
*
|
|
8
|
+
* dump-dom is a last-resort fallback: Chrome 156 hangs on dump-dom with a
|
|
9
|
+
* fresh profile, which used to look like a successful scrape of the
|
|
10
|
+
* unrendered nav shell. Every attempt has a hard timeout and a process-group
|
|
11
|
+
* kill. Playwright's chrome-headless-shell is discovered automatically when
|
|
12
|
+
* present.
|
|
8
13
|
*/
|
|
9
14
|
import { spawn } from "node:child_process";
|
|
10
|
-
import {
|
|
15
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
11
16
|
import { tmpdir } from "node:os";
|
|
12
17
|
import { join } from "node:path";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
process.env.CHROME_PATH?.trim();
|
|
25
|
-
if (fromEnv)
|
|
26
|
-
return fromEnv;
|
|
27
|
-
const candidates = [];
|
|
28
|
-
if (process.platform === "darwin") {
|
|
29
|
-
candidates.push("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", `${process.env.HOME || ""}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`);
|
|
30
|
-
}
|
|
31
|
-
else if (process.platform === "win32") {
|
|
32
|
-
candidates.push(`${process.env["ProgramFiles"] || "C:\\Program Files"}\\Google\\Chrome\\Application\\chrome.exe`, `${process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)"}\\Google\\Chrome\\Application\\chrome.exe`);
|
|
18
|
+
import { ChromeConnectError, renderUrlViaCdp } from "./chrome-cdp.js";
|
|
19
|
+
import { DUMP_MAX_BYTES, chromeDumpDomArgs, discoverChromeBinaries, killProcessGroup, resolveChromeConnectTimeoutMs, resolveChromeTimeoutMs, } from "./chrome-launch.js";
|
|
20
|
+
export { DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, VIRTUAL_TIME_MS, discoverChromeBinaries, findPlaywrightHeadlessShell, installedChromePath, isLocalChromeAvailable, parseDevToolsActivePort, chromeCdpArgs, chromeDumpDomArgs, killProcessGroup, resolveChromeConnectTimeoutMs, resolveChromeTimeoutMs, } from "./chrome-launch.js";
|
|
21
|
+
export { ChromeConnectError } from "./chrome-cdp.js";
|
|
22
|
+
function attemptPlan(binary) {
|
|
23
|
+
const isShell = /headless[-_]?shell/i.test(binary);
|
|
24
|
+
if (isShell) {
|
|
25
|
+
return [
|
|
26
|
+
{ driver: "cdp", headless: "shell" },
|
|
27
|
+
{ driver: "dump-dom", headless: "shell" },
|
|
28
|
+
];
|
|
33
29
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return p;
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
/* not installed here */
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
30
|
+
return [
|
|
31
|
+
{ driver: "cdp", headless: "new" },
|
|
32
|
+
{ driver: "cdp", headless: "old" },
|
|
33
|
+
{ driver: "dump-dom", headless: "old" },
|
|
34
|
+
{ driver: "dump-dom", headless: "new" },
|
|
35
|
+
];
|
|
47
36
|
}
|
|
48
|
-
|
|
49
|
-
|
|
37
|
+
function methodName(binary, kind) {
|
|
38
|
+
const base = binary.split(/[/\\]/).pop() || binary;
|
|
39
|
+
return `${kind.driver}/${kind.headless} (${base})`;
|
|
50
40
|
}
|
|
51
41
|
/**
|
|
52
42
|
* Render `url` in local Chrome and return the hydrated HTML, or null when
|
|
53
43
|
* Chrome is missing, times out, or produces nothing.
|
|
54
44
|
*/
|
|
55
45
|
export async function renderViaLocalChrome(url, opts = {}) {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
46
|
+
const result = await renderViaLocalChromeDetailed(url, opts);
|
|
47
|
+
return result.html;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Same as `renderViaLocalChrome` but with the attempt log and an actionable
|
|
51
|
+
* error when every driver fails. Callers that need to report honest JS-render
|
|
52
|
+
* failures should use this.
|
|
53
|
+
*/
|
|
54
|
+
export async function renderViaLocalChromeDetailed(url, opts = {}) {
|
|
55
|
+
const binaries = opts.binaries?.length ? opts.binaries : discoverChromeBinaries();
|
|
56
|
+
if (binaries.length === 0) {
|
|
57
|
+
return {
|
|
58
|
+
html: null,
|
|
59
|
+
error: "No local Chrome/Chromium binary found. Install Chrome, set WEBCRAWL_CHROME_PATH, " +
|
|
60
|
+
"or run `npx playwright install chromium-headless-shell`. See `webcrawl doctor`.",
|
|
61
|
+
attempts: [],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const budgetMs = resolveChromeTimeoutMs(opts.timeoutMs);
|
|
65
|
+
const connectTimeoutMs = resolveChromeConnectTimeoutMs(opts.connectTimeoutMs);
|
|
66
|
+
const started = Date.now();
|
|
67
|
+
const attempts = [];
|
|
68
|
+
let lastError = "local Chrome produced no content";
|
|
69
|
+
for (const binary of binaries) {
|
|
70
|
+
let hungConnect = false;
|
|
71
|
+
for (const kind of attemptPlan(binary)) {
|
|
72
|
+
const remaining = budgetMs - (Date.now() - started);
|
|
73
|
+
if (remaining < 1_200)
|
|
74
|
+
break;
|
|
75
|
+
// A process that launched but never answered CDP will hang dump-dom too.
|
|
76
|
+
if (kind.driver === "dump-dom" && hungConnect)
|
|
77
|
+
continue;
|
|
78
|
+
const method = methodName(binary, kind);
|
|
79
|
+
const attemptBudget = kind.driver === "cdp"
|
|
80
|
+
? Math.min(remaining, kind.headless === "new" ? 12_000 : 8_000)
|
|
81
|
+
: Math.min(remaining, 6_000);
|
|
82
|
+
const t0 = Date.now();
|
|
83
|
+
try {
|
|
84
|
+
const html = kind.driver === "cdp"
|
|
85
|
+
? (await renderUrlViaCdp(binary, url, {
|
|
86
|
+
timeoutMs: attemptBudget,
|
|
87
|
+
connectTimeoutMs: Math.min(connectTimeoutMs, remaining),
|
|
88
|
+
headless: kind.headless,
|
|
89
|
+
})).html
|
|
90
|
+
: await dumpDom(binary, url, kind.headless, attemptBudget);
|
|
91
|
+
const ms = Date.now() - t0;
|
|
92
|
+
if (html && html.length > 100) {
|
|
93
|
+
attempts.push({ ok: true, method, ms });
|
|
94
|
+
return { html, method, attempts };
|
|
95
|
+
}
|
|
96
|
+
lastError = `${method} returned empty HTML`;
|
|
97
|
+
attempts.push({ ok: false, method, ms, error: lastError });
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
const ms = Date.now() - t0;
|
|
101
|
+
lastError = `${method}: ${err instanceof Error ? err.message : String(err)}`;
|
|
102
|
+
attempts.push({ ok: false, method, ms, error: lastError });
|
|
103
|
+
if (err instanceof ChromeConnectError && err.hung)
|
|
104
|
+
hungConnect = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
html: null,
|
|
110
|
+
error: `JavaScript rendering failed (${lastError}). ` +
|
|
111
|
+
"Chrome hung, timed out, or produced no content. Install Chrome, point WEBCRAWL_CHROME_PATH " +
|
|
112
|
+
"at chrome-headless-shell (`npx playwright install chromium-headless-shell`), " +
|
|
113
|
+
"run `webcrawl login` and retry with `--mode cloud`, or set WEBCRAWL_RENDER_URL.",
|
|
114
|
+
attempts,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async function dumpDom(chrome, url, headless, timeoutMs) {
|
|
60
118
|
let profile = "";
|
|
61
119
|
try {
|
|
62
120
|
profile = mkdtempSync(join(tmpdir(), "wc-chrome-"));
|
|
63
121
|
}
|
|
64
122
|
catch {
|
|
65
|
-
|
|
123
|
+
throw new Error("could not create Chrome profile directory");
|
|
66
124
|
}
|
|
67
|
-
const args =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"--disable-gpu",
|
|
71
|
-
"--disable-dev-shm-usage",
|
|
72
|
-
"--no-first-run",
|
|
73
|
-
"--no-default-browser-check",
|
|
74
|
-
"--disable-crash-reporter",
|
|
75
|
-
"--disable-breakpad",
|
|
76
|
-
`--user-data-dir=${profile}`,
|
|
77
|
-
`--virtual-time-budget=${VIRTUAL_TIME_MS}`,
|
|
78
|
-
`--timeout=${timeoutMs}`,
|
|
79
|
-
"--dump-dom",
|
|
125
|
+
const args = chromeDumpDomArgs({
|
|
126
|
+
profile,
|
|
127
|
+
headless,
|
|
80
128
|
url,
|
|
81
|
-
|
|
129
|
+
timeoutMs,
|
|
130
|
+
binaryPath: chrome,
|
|
131
|
+
});
|
|
82
132
|
try {
|
|
83
|
-
return await new Promise((resolve) => {
|
|
133
|
+
return await new Promise((resolve, reject) => {
|
|
84
134
|
let settled = false;
|
|
85
|
-
const done = (value) => {
|
|
135
|
+
const done = (err, value) => {
|
|
86
136
|
if (settled)
|
|
87
137
|
return;
|
|
88
138
|
settled = true;
|
|
89
|
-
|
|
139
|
+
if (err)
|
|
140
|
+
reject(err);
|
|
141
|
+
else
|
|
142
|
+
resolve(value ?? "");
|
|
90
143
|
};
|
|
91
144
|
let child;
|
|
92
145
|
try {
|
|
93
|
-
child = spawn(chrome, args, {
|
|
146
|
+
child = spawn(chrome, args, {
|
|
147
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
148
|
+
detached: true,
|
|
149
|
+
});
|
|
94
150
|
}
|
|
95
|
-
catch {
|
|
96
|
-
done(
|
|
151
|
+
catch (err) {
|
|
152
|
+
done(err instanceof Error ? err : new Error(String(err)));
|
|
97
153
|
return;
|
|
98
154
|
}
|
|
99
155
|
const chunks = [];
|
|
100
156
|
let bytes = 0;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
157
|
+
let idle = null;
|
|
158
|
+
const finish = (err) => {
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
if (idle)
|
|
161
|
+
clearTimeout(idle);
|
|
162
|
+
killProcessGroup(child.pid, child);
|
|
163
|
+
if (err) {
|
|
164
|
+
done(err);
|
|
165
|
+
return;
|
|
107
166
|
}
|
|
108
|
-
|
|
109
|
-
|
|
167
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
168
|
+
done(null, text.trim());
|
|
169
|
+
};
|
|
170
|
+
const timer = setTimeout(() => {
|
|
171
|
+
if (bytes > 100)
|
|
172
|
+
finish(null);
|
|
173
|
+
else
|
|
174
|
+
finish(new Error(`dump-dom timed out after ${timeoutMs}ms`));
|
|
175
|
+
}, timeoutMs);
|
|
110
176
|
child.stdout?.on("data", (c) => {
|
|
111
177
|
if (bytes >= DUMP_MAX_BYTES)
|
|
112
178
|
return;
|
|
113
179
|
const take = c.subarray(0, DUMP_MAX_BYTES - bytes);
|
|
114
180
|
chunks.push(take);
|
|
115
181
|
bytes += take.length;
|
|
182
|
+
// Chrome 156 often prints the DOM then never exits (`--dump-dom` hang).
|
|
183
|
+
if (idle)
|
|
184
|
+
clearTimeout(idle);
|
|
185
|
+
idle = setTimeout(() => {
|
|
186
|
+
if (bytes > 40)
|
|
187
|
+
finish(null);
|
|
188
|
+
}, 400);
|
|
116
189
|
});
|
|
117
|
-
child.on("error", () => {
|
|
190
|
+
child.on("error", (err) => {
|
|
118
191
|
clearTimeout(timer);
|
|
119
|
-
|
|
192
|
+
if (idle)
|
|
193
|
+
clearTimeout(idle);
|
|
194
|
+
done(err);
|
|
120
195
|
});
|
|
121
196
|
child.on("close", () => {
|
|
122
197
|
clearTimeout(timer);
|
|
198
|
+
if (idle)
|
|
199
|
+
clearTimeout(idle);
|
|
123
200
|
if (bytes === 0) {
|
|
124
|
-
done(
|
|
201
|
+
done(new Error("dump-dom produced no stdout"));
|
|
125
202
|
return;
|
|
126
203
|
}
|
|
127
204
|
const text = Buffer.concat(chunks).toString("utf8");
|
|
128
|
-
done(text.trim().length > 0 ? text :
|
|
205
|
+
done(null, text.trim().length > 0 ? text : "");
|
|
129
206
|
});
|
|
130
207
|
});
|
|
131
208
|
}
|
|
@@ -6,4 +6,4 @@ export { dedupeHotelListings, extractHotelListingFromSerpResult, extractHotelLis
|
|
|
6
6
|
export { dedupeBusinessListings, extractBusinessListingFromSerpResult, extractBusinessListingsFromHtml, type BusinessListing, } from "./business.js";
|
|
7
7
|
export { cvss3BaseScore, cveSeverityLabel, extractCveIdsFromText, extractGhsaIdsFromText, looksLikeVulnId, mergeCveRecords, normalizeCveId, nvdUrl, parseNvdCve, parseNvdResponse, parseOsvVuln, severityFromScore, sortCveRecords, type CveAffected, type CveRecord, type CveSeverity, } from "./cve.js";
|
|
8
8
|
export { extractBusinessListingsWithScaleDown, extractFlightOffersWithScaleDown, extractHotelListingsWithScaleDown, extractPropertyListingsWithScaleDown, } from "./scaledown.js";
|
|
9
|
-
export { extractByPromptAndSchema, extractQuoteAuthorPairs, hasCustomExtractRequest, shapeQuoteAuthors, wantsQuoteAuthorExtract, type QuoteAuthor, } from "./structured.js";
|
|
9
|
+
export { coerceExtractSchema, extractByPromptAndSchema, extractQuoteAuthorPairs, hasCustomExtractRequest, isEmptyExtractResult, isJsTemplateJunk, nestedQuoteAuthorFields, normalizeExtractSchema, shapeQuoteAuthors, stripNonContentHtml, wantsQuoteAuthorExtract, type QuoteAuthor, } from "./structured.js";
|
|
@@ -6,4 +6,4 @@ export { dedupeHotelListings, extractHotelListingFromSerpResult, extractHotelLis
|
|
|
6
6
|
export { dedupeBusinessListings, extractBusinessListingFromSerpResult, extractBusinessListingsFromHtml, } from "./business.js";
|
|
7
7
|
export { cvss3BaseScore, cveSeverityLabel, extractCveIdsFromText, extractGhsaIdsFromText, looksLikeVulnId, mergeCveRecords, normalizeCveId, nvdUrl, parseNvdCve, parseNvdResponse, parseOsvVuln, severityFromScore, sortCveRecords, } from "./cve.js";
|
|
8
8
|
export { extractBusinessListingsWithScaleDown, extractFlightOffersWithScaleDown, extractHotelListingsWithScaleDown, extractPropertyListingsWithScaleDown, } from "./scaledown.js";
|
|
9
|
-
export { extractByPromptAndSchema, extractQuoteAuthorPairs, hasCustomExtractRequest, shapeQuoteAuthors, wantsQuoteAuthorExtract, } from "./structured.js";
|
|
9
|
+
export { coerceExtractSchema, extractByPromptAndSchema, extractQuoteAuthorPairs, hasCustomExtractRequest, isEmptyExtractResult, isJsTemplateJunk, nestedQuoteAuthorFields, normalizeExtractSchema, shapeQuoteAuthors, stripNonContentHtml, wantsQuoteAuthorExtract, } from "./structured.js";
|
|
@@ -2,8 +2,27 @@ export interface QuoteAuthor {
|
|
|
2
2
|
text: string;
|
|
3
3
|
author: string;
|
|
4
4
|
}
|
|
5
|
+
/** A bare property map or shorthand array becomes a JSON Schema. */
|
|
6
|
+
export declare function coerceExtractSchema(node: unknown): Record<string, unknown>;
|
|
7
|
+
/**
|
|
8
|
+
* Root-array wrap: a top-level array becomes
|
|
9
|
+
* `{ type: object, properties: { items: <array> } }`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizeExtractSchema(schema?: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
12
|
+
export declare function schemaProperties(schema?: Record<string, unknown>): Record<string, unknown>;
|
|
5
13
|
export declare function hasCustomExtractRequest(prompt?: string, schema?: Record<string, unknown>): boolean;
|
|
14
|
+
/** Nested object/array field names that look like quote+author records. */
|
|
15
|
+
export declare function nestedQuoteAuthorFields(schema?: Record<string, unknown>): {
|
|
16
|
+
arrayPath: string[];
|
|
17
|
+
textKey: string;
|
|
18
|
+
authorKey: string;
|
|
19
|
+
} | null;
|
|
6
20
|
export declare function wantsQuoteAuthorExtract(prompt?: string, schema?: Record<string, unknown>): boolean;
|
|
21
|
+
/** Strip script/style/template (including unclosed tags) before reading visible DOM. */
|
|
22
|
+
export declare function stripNonContentHtml(html: string): string;
|
|
23
|
+
/** Inline `document.write("... " + d['text'] + ...)` leftovers, not real quotes. */
|
|
24
|
+
export declare function isJsTemplateJunk(text: string): boolean;
|
|
25
|
+
export declare function isEmptyExtractResult(data: Record<string, unknown> | null | undefined): boolean;
|
|
7
26
|
/** Pull quote/author pairs from HTML (cards, microdata, `var data = [...]`) and markdown. */
|
|
8
27
|
export declare function extractQuoteAuthorPairs(html: string, markdown?: string): QuoteAuthor[];
|
|
9
28
|
export declare function shapeQuoteAuthors(pairs: QuoteAuthor[], schema?: Record<string, unknown>): Record<string, unknown>;
|