mcp-scraper 0.86.5 → 0.87.0
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/CHANGELOG.md +9 -1
- package/README.md +15 -2
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-core.js +4 -4
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.js +4 -4
- package/dist/bin/paa-harvest.js +1 -1
- package/dist/{chunk-TV32LC76.js → chunk-6XDNIYGP.js} +3 -3
- package/dist/{chunk-NH4QAH3X.js → chunk-R2HRN2FW.js} +177 -52
- package/dist/{chunk-3JRZZVWL.js → chunk-UDLPENTY.js} +1 -1
- package/dist/{chunk-QJJQ4OAZ.js → chunk-XX6XUZQM.js} +44 -6
- package/dist/{chunk-BA6BPDSP.js → chunk-Y3AIFO7C.js} +1 -1
- package/dist/index.cjs +177 -52
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1 -1
- package/dist/{server-ARMF2IRI.js → server-4QWARJET.js} +96 -7
- package/dist/{worker-OQSFSLNG.js → worker-DAVCZ3FP.js} +1 -1
- package/package.json +1 -1
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
} from "./chunk-P7FWOMU7.js";
|
|
51
51
|
import {
|
|
52
52
|
PACKAGE_VERSION
|
|
53
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-Y3AIFO7C.js";
|
|
54
54
|
import {
|
|
55
55
|
PUBLIC_ERROR_CODES,
|
|
56
56
|
buildPublicErrorEnvelope,
|
|
@@ -1126,6 +1126,28 @@ function errorAttemptsSection(body) {
|
|
|
1126
1126
|
Attempts:
|
|
1127
1127
|
${lines.join("\n")}`;
|
|
1128
1128
|
}
|
|
1129
|
+
function publicHarvestPagination(value) {
|
|
1130
|
+
const row = structuredRecord(value);
|
|
1131
|
+
const statuses = ["not_requested", "not_attempted", "captured", "unavailable", "failed"];
|
|
1132
|
+
const codes = ["missing_next", "invalid_next", "empty_page", "captcha", "timeout", "navigation_error", "unsupported_driver"];
|
|
1133
|
+
if (row.requestedPages !== 1 && row.requestedPages !== 2 || row.capturedPages !== 1 && row.capturedPages !== 2 || !statuses.includes(row.page2Status) || !Number.isInteger(row.page1OrganicCount) || Number(row.page1OrganicCount) < 0 || !Number.isInteger(row.page2OrganicCount) || Number(row.page2OrganicCount) < 0) return null;
|
|
1134
|
+
const captured = row.page2Status === "captured";
|
|
1135
|
+
if (captured !== (row.capturedPages === 2) || captured && (row.requestedPages !== 2 || Number(row.page2OrganicCount) === 0) || !captured && row.page2OrganicCount !== 0 || row.requestedPages === 1 && row.page2Status !== "not_requested" || row.requestedPages === 2 && row.page2Status === "not_requested" || row.page2Status === "not_attempted" && row.failureCode !== void 0) return null;
|
|
1136
|
+
return {
|
|
1137
|
+
requestedPages: row.requestedPages,
|
|
1138
|
+
capturedPages: row.capturedPages,
|
|
1139
|
+
page2Status: row.page2Status,
|
|
1140
|
+
page1OrganicCount: Number(row.page1OrganicCount),
|
|
1141
|
+
page2OrganicCount: Number(row.page2OrganicCount),
|
|
1142
|
+
...codes.includes(row.failureCode) ? { failureCode: row.failureCode } : {}
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
function harvestPaginationText(pagination) {
|
|
1146
|
+
if (!pagination) return "";
|
|
1147
|
+
const outcome = pagination.page2Status === "captured" ? "Page 2 organic results captured; PAA harvested on the original first page." : pagination.requestedPages === 2 ? `Page 2 ${pagination.page2Status.replaceAll("_", " ")}${pagination.failureCode ? ` (${pagination.failureCode})` : ""}; first-page evidence retained.` : "PAA harvested on the first page.";
|
|
1148
|
+
return `
|
|
1149
|
+
**Pagination:** ${pagination.capturedPages} of ${pagination.requestedPages} requested organic pages captured. ${outcome}`;
|
|
1150
|
+
}
|
|
1129
1151
|
function publicPaaLifecycle(value) {
|
|
1130
1152
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1131
1153
|
const row = value;
|
|
@@ -1285,6 +1307,7 @@ function formatHarvestPaa(raw, input) {
|
|
|
1285
1307
|
const aiOvw = d.aiOverview;
|
|
1286
1308
|
const diagnostics = d.diagnostics;
|
|
1287
1309
|
const durationMs = d.stats?.durationMs;
|
|
1310
|
+
const pagination = publicHarvestPagination(diagnostics?.pagination);
|
|
1288
1311
|
const paaRows = flat.map(
|
|
1289
1312
|
(r, i) => `| ${i + 1} | ${cell(r.question)} | ${cell(truncate(r.answer, 120))} | ${cell(r.source_title || r.source_site || "")} |`
|
|
1290
1313
|
).join("\n");
|
|
@@ -1317,7 +1340,7 @@ ${serpRows}` : "";
|
|
|
1317
1340
|
- Dig into a result: use \`extract_url\` on any organic URL`;
|
|
1318
1341
|
const full = `# PAA Report: "${input.query}"${input.location ? ` \xB7 ${input.location}` : ""}
|
|
1319
1342
|
|
|
1320
|
-
${paaTable}${serpTable}${entityIdsSection(entityIds)}${aiSection}${statsLine}${debugSection(diagnostics?.debug)}${tips}`;
|
|
1343
|
+
${paaTable}${harvestPaginationText(pagination)}${serpTable}${entityIdsSection(entityIds)}${aiSection}${statsLine}${debugSection(diagnostics?.debug)}${tips}`;
|
|
1321
1344
|
return {
|
|
1322
1345
|
...oneBlock(full),
|
|
1323
1346
|
structuredContent: {
|
|
@@ -1330,6 +1353,7 @@ ${paaTable}${serpTable}${entityIdsSection(entityIds)}${aiSection}${statsLine}${d
|
|
|
1330
1353
|
degradationReasons: diagnostics?.degradationReasons ?? [],
|
|
1331
1354
|
retryRecommended: diagnostics?.retryRecommended ?? null,
|
|
1332
1355
|
paaLifecycle: publicPaaLifecycle(diagnostics?.paaLifecycle),
|
|
1356
|
+
pagination,
|
|
1333
1357
|
questions: flat.map((r) => publicPaaQuestion(r)),
|
|
1334
1358
|
organicResults: organic.map((r) => publicOrganicResult(r)),
|
|
1335
1359
|
aiOverview: publicAiOverview(aiOvw),
|
|
@@ -1390,6 +1414,7 @@ function durablePaaProgress(result, options) {
|
|
|
1390
1414
|
);
|
|
1391
1415
|
}
|
|
1392
1416
|
return {
|
|
1417
|
+
pagination: publicHarvestPagination(diagnostics.pagination ?? structuredRecord(rawProgress.material).pagination),
|
|
1393
1418
|
requestedQuestions: finiteNonNegative(completeness.requestedQuestions ?? options.maxQuestions),
|
|
1394
1419
|
capturedQuestions: captured,
|
|
1395
1420
|
answeredQuestions: answered,
|
|
@@ -1410,6 +1435,7 @@ function durablePaaResult(result) {
|
|
|
1410
1435
|
resultQuality: nullableBoundedString(diagnostics.resultQuality ?? result.resultQuality),
|
|
1411
1436
|
retryRecommended: typeof diagnostics.retryRecommended === "boolean" ? diagnostics.retryRecommended : typeof result.retryRecommended === "boolean" ? result.retryRecommended : null,
|
|
1412
1437
|
paaLifecycle: publicPaaLifecycle(diagnostics.paaLifecycle ?? structuredRecord(result.progress).lifecycle),
|
|
1438
|
+
pagination: publicHarvestPagination(diagnostics.pagination ?? structuredRecord(structuredRecord(result.progress).material).pagination),
|
|
1413
1439
|
questionCount: rows.length,
|
|
1414
1440
|
questions: rows.map(publicPaaQuestion),
|
|
1415
1441
|
organicResults: Array.isArray(result.organicResults) ? result.organicResults.filter((row) => row && typeof row === "object").map((row) => publicOrganicResult(row)) : [],
|
|
@@ -1521,7 +1547,7 @@ function formatHarvestPaaStatus(raw) {
|
|
|
1521
1547
|
return {
|
|
1522
1548
|
content: [{
|
|
1523
1549
|
type: "text",
|
|
1524
|
-
text: recovering ? `PAA job ${jobId}: recovering automatically after an interrupted worker. ${progress.capturedQuestions ?? 0} questions are preserved; keep polling this same job.` : `PAA job ${jobId}: ${state}. ${progress.capturedQuestions ?? 0} questions are preserved.`
|
|
1550
|
+
text: (recovering ? `PAA job ${jobId}: recovering automatically after an interrupted worker. ${progress.capturedQuestions ?? 0} questions are preserved; keep polling this same job.` : `PAA job ${jobId}: ${state}. ${progress.capturedQuestions ?? 0} questions are preserved.`) + harvestPaginationText(publicHarvestPagination(structuredRecord(result.diagnostics).pagination ?? structuredRecord(structuredRecord(result.progress).material).pagination))
|
|
1525
1551
|
}],
|
|
1526
1552
|
structuredContent
|
|
1527
1553
|
};
|
|
@@ -8725,6 +8751,7 @@ var WebsiteUrlOrDomainSchema = z7.string().trim().min(1).transform((raw, ctx) =>
|
|
|
8725
8751
|
}
|
|
8726
8752
|
});
|
|
8727
8753
|
var HarvestPaaInputSchema = {
|
|
8754
|
+
pages: z7.number().int().min(1).max(2).default(1).describe("Organic result pages to capture. Default 1, maximum 2. Page 2 is captured when available before harvesting PAA on the original first page; it does not add a second PAA graph. Pagination output reports the pages actually captured."),
|
|
8728
8755
|
query: z7.string().min(1).describe('The search topic, exactly as it should be searched, e.g. "best hvac company in Denver". Include the place here when you want it in the search terms \u2014 the server sends your query to Google unchanged and never adds or removes a location.'),
|
|
8729
8756
|
location: z7.string().optional().describe('Where Google should think the searcher is, e.g. "Denver, CO". Sets the Google UULE parameter only \u2014 it never changes your query text and never selects a proxy. To put the place in the search terms too, write it into query.'),
|
|
8730
8757
|
maxQuestions: z7.number().int().min(1).max(200).default(30).describe("PAA questions to extract. Default 30, maximum 200. Use 10 for quick probes, 100-200 for deep research. Billed per extracted question; unused hold refunded."),
|
|
@@ -10019,6 +10046,14 @@ var PaaInteractionOutput = z7.object({
|
|
|
10019
10046
|
sourceCount: z7.number().int().min(0),
|
|
10020
10047
|
errorCode: z7.enum(["click_failed", "click_ack_timeout", "control_missing", "confirmation_timeout"]).nullable()
|
|
10021
10048
|
});
|
|
10049
|
+
var HarvestPaginationOutput = z7.object({
|
|
10050
|
+
requestedPages: z7.union([z7.literal(1), z7.literal(2)]),
|
|
10051
|
+
capturedPages: z7.union([z7.literal(1), z7.literal(2)]),
|
|
10052
|
+
page2Status: z7.enum(["not_requested", "not_attempted", "captured", "unavailable", "failed"]),
|
|
10053
|
+
page1OrganicCount: z7.number().int().min(0),
|
|
10054
|
+
page2OrganicCount: z7.number().int().min(0),
|
|
10055
|
+
failureCode: z7.enum(["missing_next", "invalid_next", "empty_page", "captcha", "timeout", "navigation_error", "unsupported_driver"]).optional()
|
|
10056
|
+
}).nullable();
|
|
10022
10057
|
var HarvestPaaOutputSchema = {
|
|
10023
10058
|
query: z7.string(),
|
|
10024
10059
|
location: NullableString,
|
|
@@ -10029,6 +10064,7 @@ var HarvestPaaOutputSchema = {
|
|
|
10029
10064
|
degradationReasons: z7.array(z7.string()),
|
|
10030
10065
|
retryRecommended: z7.boolean().nullable(),
|
|
10031
10066
|
paaLifecycle: PaaLifecycleOutput,
|
|
10067
|
+
pagination: HarvestPaginationOutput,
|
|
10032
10068
|
questions: z7.array(PaaQuestionOutput),
|
|
10033
10069
|
organicResults: z7.array(OrganicResultOutput),
|
|
10034
10070
|
aiOverview: AiOverviewOutput,
|
|
@@ -10036,6 +10072,7 @@ var HarvestPaaOutputSchema = {
|
|
|
10036
10072
|
durationMs: z7.number().min(0).nullable()
|
|
10037
10073
|
};
|
|
10038
10074
|
var HarvestPaaDurableProgressOutput = z7.object({
|
|
10075
|
+
pagination: HarvestPaginationOutput,
|
|
10039
10076
|
requestedQuestions: z7.number().int().min(0).nullable(),
|
|
10040
10077
|
capturedQuestions: z7.number().int().min(0),
|
|
10041
10078
|
answeredQuestions: z7.number().int().min(0),
|
|
@@ -10077,6 +10114,7 @@ var HarvestPaaDurableBillingOutput = z7.object({
|
|
|
10077
10114
|
refundMc: z7.number().int().min(0).nullable()
|
|
10078
10115
|
});
|
|
10079
10116
|
var HarvestPaaDurableResultOutput = z7.object({
|
|
10117
|
+
pagination: HarvestPaginationOutput,
|
|
10080
10118
|
completionStatus: NullableString,
|
|
10081
10119
|
resultQuality: NullableString,
|
|
10082
10120
|
retryRecommended: z7.boolean().nullable(),
|
|
@@ -17286,7 +17324,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
17286
17324
|
registerPersonalAssistantMcpSurface(server, executor);
|
|
17287
17325
|
server.registerTool("harvest_paa", {
|
|
17288
17326
|
title: "Google PAA + SERP Harvest",
|
|
17289
|
-
description: `Expand one Google People Also Ask SERP into questions, answers, every preserved source, AI Overview evidence, ranking URLs, and entity IDs. maxQuestions is a target count, not traversal depth. Results distinguish target_reached, proven frontier_exhausted, interruption, and recovery_exhausted; a failed click or browser timeout is never reported as exhaustion. This compatibility tool waits; use harvest_paa_start plus harvest_paa_status for long runs. Optional SERP modules require their include flags. Use gl and location for regional context. Costs ${PAA_BASE_CREDITS} Credits per harvest plus ${PAA_QUESTION_CREDITS} Credits per question actually returned; unused hold is refunded. After a timeout or unknown response, reuse the same idempotencyKey. Call credits_info for current pricing and balance.`,
|
|
17327
|
+
description: `Expand one Google People Also Ask SERP into questions, answers, every preserved source, AI Overview evidence, ranking URLs, and entity IDs. Set pages to 2 to add the second organic-results page when available; PAA is still expanded once on the preserved first page, and pagination reports what was captured. maxQuestions is a target count, not traversal depth. Results distinguish target_reached, proven frontier_exhausted, interruption, and recovery_exhausted; a failed click or browser timeout is never reported as exhaustion. This compatibility tool waits; use harvest_paa_start plus harvest_paa_status for long runs. Optional SERP modules require their include flags. Use gl and location for regional context. Costs ${PAA_BASE_CREDITS} Credits per harvest plus ${PAA_QUESTION_CREDITS} Credits per question actually returned; unused hold is refunded. After a timeout or unknown response, reuse the same idempotencyKey. Call credits_info for current pricing and balance.`,
|
|
17290
17328
|
inputSchema: harvestPaaInputSchema,
|
|
17291
17329
|
outputSchema: recordOutputSchema("harvest_paa", HarvestPaaOutputSchema),
|
|
17292
17330
|
annotations: liveWebToolAnnotations("Google PAA + SERP Harvest")
|
|
@@ -17299,7 +17337,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
17299
17337
|
});
|
|
17300
17338
|
server.registerTool("harvest_paa_start", {
|
|
17301
17339
|
title: "Start Durable Google PAA Harvest",
|
|
17302
|
-
description: `Start a durable Google People Also Ask harvest and return its job receipt. maxQuestions is the requested target count. The job automatically resumes an interrupted serverless worker under the same jobId, idempotency key, and billing hold while preserving checkpoints. Keep one idempotencyKey after a timeout, unknown response, or in-progress reply; replaying it recovers the existing job without a duplicate charge. Poll jobId with harvest_paa_status. Costs ${PAA_BASE_CREDITS} Credits plus ${PAA_QUESTION_CREDITS} per retained question; unused hold is refunded.`,
|
|
17340
|
+
description: `Start a durable Google People Also Ask harvest and return its job receipt. Set pages to 2 to add the second organic-results page when available; PAA is still expanded once on the preserved first page, and pagination survives checkpoint recovery. maxQuestions is the requested target count. The job automatically resumes an interrupted serverless worker under the same jobId, idempotency key, and billing hold while preserving checkpoints. Keep one idempotencyKey after a timeout, unknown response, or in-progress reply; replaying it recovers the existing job without a duplicate charge. Poll jobId with harvest_paa_status. Costs ${PAA_BASE_CREDITS} Credits plus ${PAA_QUESTION_CREDITS} per retained question; unused hold is refunded.`,
|
|
17303
17341
|
inputSchema: harvestPaaStartInputSchema,
|
|
17304
17342
|
outputSchema: recordOutputSchema("harvest_paa_start", HarvestPaaStartOutputSchema),
|
|
17305
17343
|
annotations: { ...liveWebToolAnnotations("Start Durable Google PAA Harvest"), readOnlyHint: false, idempotentHint: true }
|
|
@@ -17309,7 +17347,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
17309
17347
|
});
|
|
17310
17348
|
server.registerTool("harvest_paa_status", {
|
|
17311
17349
|
title: "Check Durable Google PAA Harvest",
|
|
17312
|
-
description: "Poll an owner-scoped harvest_paa_start job. Returns the job/operation/task correlation ID, saved progress, automatic-recovery count, target and discovery status, material completeness, recent per-control interaction outcomes with 0.7/1.0/1.4-second confirmation telemetry, provider-session-correlated attempts, terminal rows, and billing. frontier_exhausted means every observed eligible PAA control was processed plus three healthy no-growth confirmations; interruption never means exhaustion. Polling never starts or bills another run.",
|
|
17350
|
+
description: "Poll an owner-scoped harvest_paa_start job. Returns the job/operation/task correlation ID, saved progress, automatic-recovery count, target and discovery status, material completeness, bounded organic-page pagination, recent per-control interaction outcomes with 0.7/1.0/1.4-second confirmation telemetry, provider-session-correlated attempts, terminal rows, and billing. Page-two outcomes describe organic results only; the PAA graph always comes from the preserved first page. frontier_exhausted means every observed eligible PAA control was processed plus three healthy no-growth confirmations; interruption never means exhaustion. Polling never starts or bills another run.",
|
|
17313
17351
|
inputSchema: HarvestPaaStatusInputSchema,
|
|
17314
17352
|
outputSchema: recordOutputSchema("harvest_paa_status", HarvestPaaStatusOutputSchema),
|
|
17315
17353
|
annotations: { ...liveWebToolAnnotations("Check Durable Google PAA Harvest"), idempotentHint: true, openWorldHint: false }
|
package/dist/index.cjs
CHANGED
|
@@ -395,6 +395,58 @@ var LocationMismatchError = class extends Error {
|
|
|
395
395
|
}
|
|
396
396
|
};
|
|
397
397
|
|
|
398
|
+
// src/driver/IBrowserDriver.ts
|
|
399
|
+
async function withManagedTemporaryPage(context, setup, operation, signal) {
|
|
400
|
+
let page;
|
|
401
|
+
let ended = false;
|
|
402
|
+
let timer;
|
|
403
|
+
let rejectStop = () => {
|
|
404
|
+
};
|
|
405
|
+
const aborted = () => signal?.reason instanceof DOMException && signal.reason.name === "TimeoutError" ? signal.reason : new RequestAbortedError();
|
|
406
|
+
const onAbort = () => rejectStop(aborted());
|
|
407
|
+
const stop = new Promise((_, reject) => {
|
|
408
|
+
rejectStop = reject;
|
|
409
|
+
});
|
|
410
|
+
const close = async (target) => {
|
|
411
|
+
let closeTimer;
|
|
412
|
+
try {
|
|
413
|
+
await Promise.race([
|
|
414
|
+
target.close().catch(() => {
|
|
415
|
+
}),
|
|
416
|
+
new Promise((resolve) => {
|
|
417
|
+
closeTimer = setTimeout(resolve, 2e3);
|
|
418
|
+
})
|
|
419
|
+
]);
|
|
420
|
+
} finally {
|
|
421
|
+
if (closeTimer) clearTimeout(closeTimer);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
if (signal?.aborted) throw aborted();
|
|
425
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
426
|
+
timer = setTimeout(() => rejectStop(new DOMException("Optional pagination timed out", "TimeoutError")), 3e4);
|
|
427
|
+
try {
|
|
428
|
+
return await Promise.race([
|
|
429
|
+
(async () => {
|
|
430
|
+
const created = await context.newPage();
|
|
431
|
+
if (ended) {
|
|
432
|
+
await close(created);
|
|
433
|
+
throw new RequestAbortedError();
|
|
434
|
+
}
|
|
435
|
+
page = created;
|
|
436
|
+
await setup(created);
|
|
437
|
+
if (ended) throw new RequestAbortedError();
|
|
438
|
+
return operation(created);
|
|
439
|
+
})(),
|
|
440
|
+
stop
|
|
441
|
+
]);
|
|
442
|
+
} finally {
|
|
443
|
+
ended = true;
|
|
444
|
+
if (timer) clearTimeout(timer);
|
|
445
|
+
signal?.removeEventListener("abort", onAbort);
|
|
446
|
+
if (page) await close(page);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
398
450
|
// src/api/cost-telemetry.ts
|
|
399
451
|
var import_node_crypto2 = require("crypto");
|
|
400
452
|
|
|
@@ -1085,6 +1137,11 @@ var BrowserDriver = class {
|
|
|
1085
1137
|
};
|
|
1086
1138
|
});
|
|
1087
1139
|
}
|
|
1140
|
+
async withTemporaryPage(operation, signal) {
|
|
1141
|
+
if (!this.context) throw new Error("Browser context is not available");
|
|
1142
|
+
return withManagedTemporaryPage(this.context, async () => {
|
|
1143
|
+
}, operation, signal);
|
|
1144
|
+
}
|
|
1088
1145
|
async navigateToSERP(query, uule, gl, hl, options) {
|
|
1089
1146
|
const params = new URLSearchParams({ q: query, gl, hl, pws: "0" });
|
|
1090
1147
|
if (options?.tbs) params.set("tbs", options.tbs);
|
|
@@ -1489,7 +1546,10 @@ var BrightDataSerpDriver = class {
|
|
|
1489
1546
|
const customCdp = this.cdpSession;
|
|
1490
1547
|
const sessionResponse = await customCdp.send("Browser.getSessionId");
|
|
1491
1548
|
this.providerSessionId = validateProviderSessionId(sessionResponse?.sessionId);
|
|
1492
|
-
await this.page
|
|
1549
|
+
await this.setupPage(this.page);
|
|
1550
|
+
}
|
|
1551
|
+
async setupPage(page) {
|
|
1552
|
+
await page.route("**/*", async (route) => {
|
|
1493
1553
|
const request = route.request();
|
|
1494
1554
|
const resourceType = request.resourceType();
|
|
1495
1555
|
let requestUrl;
|
|
@@ -1514,6 +1574,10 @@ var BrightDataSerpDriver = class {
|
|
|
1514
1574
|
await route.continue();
|
|
1515
1575
|
});
|
|
1516
1576
|
}
|
|
1577
|
+
async withTemporaryPage(operation, signal) {
|
|
1578
|
+
if (!this.context || this.closing) throw new Error("Browser context is not available");
|
|
1579
|
+
return withManagedTemporaryPage(this.context, (page) => this.setupPage(page), operation, signal);
|
|
1580
|
+
}
|
|
1517
1581
|
async navigateToSERP(query, uule, gl, hl, options) {
|
|
1518
1582
|
if (!this.page) throw new Error("Browser page is not available");
|
|
1519
1583
|
const params = new URLSearchParams({ q: query, gl, hl, pws: "0" });
|
|
@@ -2489,6 +2553,11 @@ function parsedGoogleGoto(value) {
|
|
|
2489
2553
|
if (!url || url.protocol !== "https:" || !isGoogleHost(url.hostname) || url.pathname !== "/goto") return null;
|
|
2490
2554
|
return url.searchParams.get("url") ? url : null;
|
|
2491
2555
|
}
|
|
2556
|
+
function hasResolvedGotoDestination(value) {
|
|
2557
|
+
if (value.linkType !== "google_goto_redirect" || value.resolutionStatus !== "resolved" || !parsedGoogleGoto(value.rawUrl) || !value.resolvedUrl) return false;
|
|
2558
|
+
const destination = safeHttpUrl(value.resolvedUrl);
|
|
2559
|
+
return Boolean(destination && !isGoogleHost(destination.hostname) && destination.href === value.resolvedUrl && value.url === destination.href);
|
|
2560
|
+
}
|
|
2492
2561
|
function questionIdFor(question) {
|
|
2493
2562
|
const normalized = question.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^\p{L}\p{N}\s]/gu, "").replace(/\s+/g, " ").trim();
|
|
2494
2563
|
return `paa_${(0, import_node_crypto3.createHash)("sha256").update(normalized).digest("hex").slice(0, 16)}`;
|
|
@@ -2705,8 +2774,12 @@ async function resolveHarvestResultGoogleGotoLinks(result, options = {}) {
|
|
|
2705
2774
|
}
|
|
2706
2775
|
async function resolveGoogleOutboundLinks(page, values, options = {}) {
|
|
2707
2776
|
const byRawUrl = /* @__PURE__ */ new Map();
|
|
2708
|
-
for (const value of values)
|
|
2709
|
-
|
|
2777
|
+
for (const value of values) {
|
|
2778
|
+
const existing = byRawUrl.get(value.rawUrl);
|
|
2779
|
+
if (existing && hasResolvedGotoDestination(existing)) continue;
|
|
2780
|
+
byRawUrl.set(value.rawUrl, value.linkType === "google_goto_redirect" && !hasResolvedGotoDestination(value) ? { ...value, url: value.rawUrl, resolvedUrl: null, resolutionStatus: "unresolved" } : value);
|
|
2781
|
+
}
|
|
2782
|
+
const inputs = [...byRawUrl.values()].filter((value) => value.linkType === "google_goto_redirect" && !hasResolvedGotoDestination(value));
|
|
2710
2783
|
if (inputs.length === 0) return byRawUrl;
|
|
2711
2784
|
const deadlineMs = Date.now() + (options.totalBudgetMs ?? DEFAULT_TOTAL_BUDGET_MS);
|
|
2712
2785
|
const direct = await resolveGoogleGotoUrls(inputs.map((value) => value.rawUrl), options);
|
|
@@ -2922,7 +2995,7 @@ var PAAExtractor = class {
|
|
|
2922
2995
|
];
|
|
2923
2996
|
const unique = new Map(candidates.map((link) => [link.rawUrl, link]));
|
|
2924
2997
|
let resolverNonGoogleRequestsObserved = 0;
|
|
2925
|
-
const resolved = await resolveGoogleOutboundLinks(page,
|
|
2998
|
+
const resolved = await resolveGoogleOutboundLinks(page, candidates, {
|
|
2926
2999
|
onNetworkAudit: (audit) => {
|
|
2927
3000
|
resolverNonGoogleRequestsObserved = audit.nonGoogleRequestsObserved;
|
|
2928
3001
|
}
|
|
@@ -3173,6 +3246,7 @@ var PAAExtractor = class {
|
|
|
3173
3246
|
diagnostics: {
|
|
3174
3247
|
completionStatus: "paa_found",
|
|
3175
3248
|
problem: null,
|
|
3249
|
+
...material?.pagination ? { pagination: { ...material.pagination } } : {},
|
|
3176
3250
|
resultQuality: "partial",
|
|
3177
3251
|
degradedResult: false,
|
|
3178
3252
|
retryRecommended: true,
|
|
@@ -4137,6 +4211,55 @@ var PAAExtractor = class {
|
|
|
4137
4211
|
...locationEvidence ? { locationEvidence } : {}
|
|
4138
4212
|
};
|
|
4139
4213
|
}
|
|
4214
|
+
async captureSecondOrganicPage(page, options, signal) {
|
|
4215
|
+
this.throwIfAborted(signal);
|
|
4216
|
+
if (!this.driver.withTemporaryPage) return { organic: [], status: "failed", failureCode: "unsupported_driver" };
|
|
4217
|
+
let guardedFailure;
|
|
4218
|
+
try {
|
|
4219
|
+
const initial = new URL(page.url());
|
|
4220
|
+
const valid = (raw) => {
|
|
4221
|
+
try {
|
|
4222
|
+
const url = new URL(raw, initial);
|
|
4223
|
+
return initial.origin === "https://www.google.com" && url.origin === initial.origin && !url.username && !url.password && url.pathname === "/search" && url.searchParams.getAll("q").length === 1 && url.searchParams.get("q") === options.query && url.searchParams.getAll("start").length === 1 && url.searchParams.get("start") === "10" && (!url.searchParams.has("num") || url.searchParams.get("num") === "10") && ["gl", "hl"].every((key) => !url.searchParams.has(key) || url.searchParams.getAll(key).length === 1 && url.searchParams.get(key) === options[key]) && ["uule", "tbs", "udm", "tbm", "pws"].every((key) => url.searchParams.getAll(key).length <= 1 && url.searchParams.get(key) === initial.searchParams.get(key));
|
|
4224
|
+
} catch {
|
|
4225
|
+
return false;
|
|
4226
|
+
}
|
|
4227
|
+
};
|
|
4228
|
+
const href = await page.evaluate(() => {
|
|
4229
|
+
const next = document.querySelector('a#pnnext, a[rel="next"]');
|
|
4230
|
+
return next?.getAttribute("href") ?? null;
|
|
4231
|
+
});
|
|
4232
|
+
if (!href) return { organic: [], status: "unavailable", failureCode: "missing_next" };
|
|
4233
|
+
if (!valid(href)) return { organic: [], status: "unavailable", failureCode: "invalid_next" };
|
|
4234
|
+
const organic = await this.driver.withTemporaryPage(async (second) => {
|
|
4235
|
+
await second.route("**/*", async (route) => {
|
|
4236
|
+
const request = route.request();
|
|
4237
|
+
if (request.isNavigationRequest() && request.frame() === second.mainFrame() && !valid(request.url())) {
|
|
4238
|
+
guardedFailure = /^https:\/\/www\.google\.com\/sorry(?:\/|\?)/.test(request.url()) ? "captcha" : "invalid_next";
|
|
4239
|
+
await route.abort("blockedbyclient");
|
|
4240
|
+
} else await route.fallback();
|
|
4241
|
+
});
|
|
4242
|
+
await second.goto(new URL(href, initial).href, { waitUntil: "domcontentloaded", timeout: 2e4 });
|
|
4243
|
+
this.throwIfAborted(signal);
|
|
4244
|
+
await this.throwIfCaptcha(second, "Google SERP page 2");
|
|
4245
|
+
if (!valid(second.url())) {
|
|
4246
|
+
guardedFailure = "invalid_next";
|
|
4247
|
+
throw new Error("Invalid pagination landing");
|
|
4248
|
+
}
|
|
4249
|
+
return this.extractOrganicResults(second);
|
|
4250
|
+
}, signal);
|
|
4251
|
+
this.throwIfAborted(signal);
|
|
4252
|
+
return organic.length > 0 ? { organic: organic.map((row) => ({ ...row, position: row.position + 10 })), status: "captured" } : { organic: [], status: "unavailable", failureCode: "empty_page" };
|
|
4253
|
+
} catch (err) {
|
|
4254
|
+
this.throwIfAborted(signal);
|
|
4255
|
+
if (err instanceof RequestAbortedError) throw err;
|
|
4256
|
+
return {
|
|
4257
|
+
organic: [],
|
|
4258
|
+
status: "failed",
|
|
4259
|
+
failureCode: guardedFailure ?? (err instanceof CaptchaError ? "captcha" : err instanceof Error && (err.name === "TimeoutError" || /timed? ?out/i.test(err.message)) ? "timeout" : "navigation_error")
|
|
4260
|
+
};
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4140
4263
|
async extract(options, signal) {
|
|
4141
4264
|
const startMs = Date.now();
|
|
4142
4265
|
this.completeness = { paaWithoutAnswer: 0, paaWithoutSource: 0, paaAnswersRecovered: 0, aioShareCaptured: null };
|
|
@@ -4340,23 +4463,49 @@ var PAAExtractor = class {
|
|
|
4340
4463
|
const initialLocationEvidence = options.debug ? inferSerpLocationEvidence(canonicalLocation, organicResults, localPack) : void 0;
|
|
4341
4464
|
this.reporter.onVideos(videos);
|
|
4342
4465
|
this.reporter.onForums(forums);
|
|
4466
|
+
const aiSurfaces = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
|
|
4467
|
+
let pagination = {
|
|
4468
|
+
requestedPages: (options.pages ?? 1) >= 2 ? 2 : 1,
|
|
4469
|
+
capturedPages: 1,
|
|
4470
|
+
page2Status: (options.pages ?? 1) >= 2 ? "not_attempted" : "not_requested",
|
|
4471
|
+
page1OrganicCount: organicResults.length,
|
|
4472
|
+
page2OrganicCount: 0
|
|
4473
|
+
};
|
|
4474
|
+
let allOrganic = organicResults;
|
|
4475
|
+
this.checkpointMaterial = {
|
|
4476
|
+
surface: aiSurfaces.surface,
|
|
4477
|
+
aiOverview: aiSurfaces.aiOverview,
|
|
4478
|
+
aiMode: aiSurfaces.aiMode,
|
|
4479
|
+
whatPeopleSaying,
|
|
4480
|
+
videos,
|
|
4481
|
+
forums,
|
|
4482
|
+
organicResults: allOrganic,
|
|
4483
|
+
localPack,
|
|
4484
|
+
entityIds,
|
|
4485
|
+
pagination
|
|
4486
|
+
};
|
|
4487
|
+
await this.emitProgress("serp_captured");
|
|
4488
|
+
if (pagination.requestedPages === 2) {
|
|
4489
|
+
const second = await this.captureSecondOrganicPage(page, executionOptions, signal);
|
|
4490
|
+
allOrganic = [...organicResults, ...second.organic];
|
|
4491
|
+
pagination = {
|
|
4492
|
+
...pagination,
|
|
4493
|
+
capturedPages: second.status === "captured" ? 2 : 1,
|
|
4494
|
+
page2Status: second.status,
|
|
4495
|
+
page2OrganicCount: second.organic.length,
|
|
4496
|
+
...second.failureCode ? { failureCode: second.failureCode } : {}
|
|
4497
|
+
};
|
|
4498
|
+
this.checkpointMaterial = { ...this.checkpointMaterial, organicResults: allOrganic, pagination };
|
|
4499
|
+
await this.emitProgress("serp_captured");
|
|
4500
|
+
await this.resolveMaterialLinks(page, [], allOrganic, aiSurfaces.aiOverview, aiSurfaces.aiMode);
|
|
4501
|
+
this.checkpointMaterial = { ...this.checkpointMaterial, organicResults: allOrganic };
|
|
4502
|
+
await this.emitProgress("serp_captured");
|
|
4503
|
+
}
|
|
4504
|
+
const locationEvidence = options.debug ? inferSerpLocationEvidence(canonicalLocation, allOrganic, localPack) : initialLocationEvidence;
|
|
4343
4505
|
if (!hasPaa) {
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
|
|
4348
|
-
if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
|
|
4349
|
-
if (uule) p2params.set("uule", uule);
|
|
4350
|
-
await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
|
|
4351
|
-
await this.throwIfCaptcha(page, "Google SERP page 2");
|
|
4352
|
-
const p2organic = await this.extractOrganicResults(page);
|
|
4353
|
-
noPaaOrganic = [...organicResults, ...p2organic.map((r) => ({ ...r, position: r.position + 10 }))];
|
|
4354
|
-
if (options.debug) {
|
|
4355
|
-
locationEvidence2 = inferSerpLocationEvidence(canonicalLocation, noPaaOrganic, localPack);
|
|
4356
|
-
}
|
|
4357
|
-
}
|
|
4358
|
-
const aiSurfaces2 = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
|
|
4359
|
-
await this.resolveMaterialLinks(page, [], noPaaOrganic, aiSurfaces2.aiOverview, aiSurfaces2.aiMode);
|
|
4506
|
+
await this.resolveMaterialLinks(page, [], allOrganic, aiSurfaces.aiOverview, aiSurfaces.aiMode);
|
|
4507
|
+
this.checkpointMaterial = { ...this.checkpointMaterial, organicResults: allOrganic };
|
|
4508
|
+
await this.emitProgress("serp_captured");
|
|
4360
4509
|
const stats2 = {
|
|
4361
4510
|
seed: executionOptions.query,
|
|
4362
4511
|
totalQuestions: 0,
|
|
@@ -4373,39 +4522,27 @@ var PAAExtractor = class {
|
|
|
4373
4522
|
completionStatus: "no_paa",
|
|
4374
4523
|
noPaaObserved: true,
|
|
4375
4524
|
problem: null,
|
|
4525
|
+
pagination,
|
|
4376
4526
|
paaLifecycle: { ...this.paaLifecycle },
|
|
4377
4527
|
completeness: { ...this.completeness },
|
|
4378
4528
|
links: { ...this.linkDiagnostics },
|
|
4379
|
-
...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule,
|
|
4529
|
+
...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence, locationResolution) } : {}
|
|
4380
4530
|
},
|
|
4381
4531
|
totalQuestions: 0,
|
|
4382
|
-
surface:
|
|
4383
|
-
aiOverview:
|
|
4384
|
-
aiMode:
|
|
4532
|
+
surface: aiSurfaces.surface,
|
|
4533
|
+
aiOverview: aiSurfaces.aiOverview,
|
|
4534
|
+
aiMode: aiSurfaces.aiMode,
|
|
4385
4535
|
whatPeopleSaying,
|
|
4386
4536
|
tree: [],
|
|
4387
4537
|
flat: [],
|
|
4388
4538
|
videos,
|
|
4389
4539
|
forums,
|
|
4390
|
-
organicResults:
|
|
4540
|
+
organicResults: allOrganic,
|
|
4391
4541
|
localPack,
|
|
4392
4542
|
entityIds,
|
|
4393
4543
|
stats: stats2
|
|
4394
4544
|
};
|
|
4395
4545
|
}
|
|
4396
|
-
const aiSurfaces = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
|
|
4397
|
-
this.checkpointMaterial = {
|
|
4398
|
-
surface: aiSurfaces.surface,
|
|
4399
|
-
aiOverview: aiSurfaces.aiOverview,
|
|
4400
|
-
aiMode: aiSurfaces.aiMode,
|
|
4401
|
-
whatPeopleSaying,
|
|
4402
|
-
videos,
|
|
4403
|
-
forums,
|
|
4404
|
-
organicResults,
|
|
4405
|
-
localPack,
|
|
4406
|
-
entityIds
|
|
4407
|
-
};
|
|
4408
|
-
await this.emitProgress("serp_captured");
|
|
4409
4546
|
const flat = await this.runBFS(page, executionOptions, signal);
|
|
4410
4547
|
this.throwIfAborted(signal);
|
|
4411
4548
|
const shortVidsParams = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", udm: ShortVideoSelectors.udm });
|
|
@@ -4426,20 +4563,6 @@ var PAAExtractor = class {
|
|
|
4426
4563
|
}
|
|
4427
4564
|
}
|
|
4428
4565
|
this.reporter.onVideos(shortVideos);
|
|
4429
|
-
let allOrganic = organicResults;
|
|
4430
|
-
let locationEvidence = initialLocationEvidence;
|
|
4431
|
-
if ((options.pages ?? 1) >= 2) {
|
|
4432
|
-
const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
|
|
4433
|
-
if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
|
|
4434
|
-
if (uule) p2params.set("uule", uule);
|
|
4435
|
-
await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
|
|
4436
|
-
await this.throwIfCaptcha(page, "Google SERP page 2");
|
|
4437
|
-
const p2organic = await this.extractOrganicResults(page);
|
|
4438
|
-
allOrganic = [...organicResults, ...p2organic.map((r) => ({ ...r, position: r.position + 10 }))];
|
|
4439
|
-
if (options.debug) {
|
|
4440
|
-
locationEvidence = inferSerpLocationEvidence(canonicalLocation, allOrganic, localPack);
|
|
4441
|
-
}
|
|
4442
|
-
}
|
|
4443
4566
|
await this.resolveMaterialLinks(page, flat, allOrganic, aiSurfaces.aiOverview, aiSurfaces.aiMode);
|
|
4444
4567
|
this.checkpointMaterial = {
|
|
4445
4568
|
...this.checkpointMaterial,
|
|
@@ -4449,6 +4572,7 @@ var PAAExtractor = class {
|
|
|
4449
4572
|
videos: [...videos, ...shortVideos],
|
|
4450
4573
|
organicResults: allOrganic
|
|
4451
4574
|
};
|
|
4575
|
+
await this.emitProgress("expansion_finished");
|
|
4452
4576
|
const allVideos = [...videos, ...shortVideos];
|
|
4453
4577
|
const tree = this.buildTree(flat, executionOptions.query);
|
|
4454
4578
|
const stats = {
|
|
@@ -4466,6 +4590,7 @@ var PAAExtractor = class {
|
|
|
4466
4590
|
diagnostics: {
|
|
4467
4591
|
completionStatus: "paa_found",
|
|
4468
4592
|
problem: null,
|
|
4593
|
+
pagination,
|
|
4469
4594
|
paaLifecycle: { ...this.paaLifecycle },
|
|
4470
4595
|
completeness: { ...this.completeness },
|
|
4471
4596
|
links: { ...this.linkDiagnostics },
|
package/dist/index.d.cts
CHANGED
|
@@ -177,9 +177,19 @@ interface PaaInteractionObservation {
|
|
|
177
177
|
errorCode: 'click_failed' | 'click_ack_timeout' | 'control_missing' | 'confirmation_timeout' | null;
|
|
178
178
|
}
|
|
179
179
|
type PaaProgressPhase = 'serp_captured' | 'discovered' | 'click_observed' | 'answer_captured' | 'fill_incomplete' | 'expansion_finished' | 'attempt_ending';
|
|
180
|
+
/** Observed organic pagination outcome; page two never expands a separate PAA graph. */
|
|
181
|
+
interface HarvestPaginationDiagnostics {
|
|
182
|
+
requestedPages: 1 | 2;
|
|
183
|
+
capturedPages: 1 | 2;
|
|
184
|
+
page2Status: 'not_requested' | 'not_attempted' | 'captured' | 'unavailable' | 'failed';
|
|
185
|
+
page1OrganicCount: number;
|
|
186
|
+
page2OrganicCount: number;
|
|
187
|
+
failureCode?: 'missing_next' | 'invalid_next' | 'empty_page' | 'captcha' | 'timeout' | 'navigation_error' | 'unsupported_driver';
|
|
188
|
+
}
|
|
180
189
|
/** Material already observed before PAA expansion completes. It is checkpointed
|
|
181
190
|
* so a worker interruption cannot discard the SERP that led to the PAA graph. */
|
|
182
191
|
interface PaaProgressMaterial {
|
|
192
|
+
pagination?: HarvestPaginationDiagnostics;
|
|
183
193
|
surface: GoogleSurface;
|
|
184
194
|
aiOverview: AIOverviewResult;
|
|
185
195
|
aiMode: AIModeResult;
|
|
@@ -201,6 +211,7 @@ interface PaaProgressSnapshot {
|
|
|
201
211
|
interactions?: PaaInteractionObservation[];
|
|
202
212
|
}
|
|
203
213
|
interface HarvestDiagnostics {
|
|
214
|
+
pagination?: HarvestPaginationDiagnostics;
|
|
204
215
|
completionStatus: HarvestCompletionStatus;
|
|
205
216
|
/** Set only when the extractor directly observed a readable SERP without a PAA block. */
|
|
206
217
|
noPaaObserved?: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -177,9 +177,19 @@ interface PaaInteractionObservation {
|
|
|
177
177
|
errorCode: 'click_failed' | 'click_ack_timeout' | 'control_missing' | 'confirmation_timeout' | null;
|
|
178
178
|
}
|
|
179
179
|
type PaaProgressPhase = 'serp_captured' | 'discovered' | 'click_observed' | 'answer_captured' | 'fill_incomplete' | 'expansion_finished' | 'attempt_ending';
|
|
180
|
+
/** Observed organic pagination outcome; page two never expands a separate PAA graph. */
|
|
181
|
+
interface HarvestPaginationDiagnostics {
|
|
182
|
+
requestedPages: 1 | 2;
|
|
183
|
+
capturedPages: 1 | 2;
|
|
184
|
+
page2Status: 'not_requested' | 'not_attempted' | 'captured' | 'unavailable' | 'failed';
|
|
185
|
+
page1OrganicCount: number;
|
|
186
|
+
page2OrganicCount: number;
|
|
187
|
+
failureCode?: 'missing_next' | 'invalid_next' | 'empty_page' | 'captcha' | 'timeout' | 'navigation_error' | 'unsupported_driver';
|
|
188
|
+
}
|
|
180
189
|
/** Material already observed before PAA expansion completes. It is checkpointed
|
|
181
190
|
* so a worker interruption cannot discard the SERP that led to the PAA graph. */
|
|
182
191
|
interface PaaProgressMaterial {
|
|
192
|
+
pagination?: HarvestPaginationDiagnostics;
|
|
183
193
|
surface: GoogleSurface;
|
|
184
194
|
aiOverview: AIOverviewResult;
|
|
185
195
|
aiMode: AIModeResult;
|
|
@@ -201,6 +211,7 @@ interface PaaProgressSnapshot {
|
|
|
201
211
|
interactions?: PaaInteractionObservation[];
|
|
202
212
|
}
|
|
203
213
|
interface HarvestDiagnostics {
|
|
214
|
+
pagination?: HarvestPaginationDiagnostics;
|
|
204
215
|
completionStatus: HarvestCompletionStatus;
|
|
205
216
|
/** Set only when the extractor directly observed a readable SERP without a PAA block. */
|
|
206
217
|
noPaaObserved?: boolean;
|