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
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ All notable changes to MCP Scraper are documented here. The format is based on [
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.87.0] - 2026-09-02
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added optional `pages: 2` support to synchronous and durable PAA harvests, capturing a second organic result page before expanding questions on the untouched first page.
|
|
12
|
+
- Added requested/captured page counts and bounded second-page outcomes to live results, durable recovery, the dashboard, and generated client contracts. A failed optional page-two capture preserves first-page evidence.
|
|
13
|
+
|
|
7
14
|
## [0.86.5] - 2026-09-02
|
|
8
15
|
|
|
9
16
|
### Fixed
|
|
@@ -1705,7 +1712,8 @@ All notable changes to MCP Scraper are documented here. The format is based on [
|
|
|
1705
1712
|
- Write actions remain unavailable until the account owner explicitly enables them.
|
|
1706
1713
|
- Provider-specific connection data is normalized into one agent-facing contract.
|
|
1707
1714
|
|
|
1708
|
-
[Unreleased]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.
|
|
1715
|
+
[Unreleased]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.87.0...HEAD
|
|
1716
|
+
[0.87.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.86.5...v0.87.0
|
|
1709
1717
|
[0.86.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.85.0...v0.86.0
|
|
1710
1718
|
[0.84.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.83.5...v0.84.0
|
|
1711
1719
|
[0.83.5]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.83.4...v0.83.5
|
package/README.md
CHANGED
|
@@ -175,7 +175,7 @@ Build the branded one-click bundle:
|
|
|
175
175
|
npm run build:mcpb
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
-
The generated bundle is written to `build/mcpb/mcp-scraper-<version>.mcpb` and copied to `public/downloads/` for the hosted download. The current public bundle is `https://mcpscraper.dev/downloads/mcp-scraper.mcpb` (`0.
|
|
178
|
+
The generated bundle is written to `build/mcpb/mcp-scraper-<version>.mcpb` and copied to `public/downloads/` for the hosted download. The current public bundle is `https://mcpscraper.dev/downloads/mcp-scraper.mcpb` (`0.87.0`, SHA-256 `4805165c18efda420136b9d00862a6d658dd0d4c7bef6d6b77bcb94922ce4d38`). Install it by opening or dragging it into Claude Desktop. Claude displays the `MCP Scraper` install card, icon, API-key configuration field, and manually curated current-release message from the bundle manifest.
|
|
179
179
|
|
|
180
180
|
The MCPB install exposes every tool — web-intelligence plus all `browser_*` tools — through the one `mcp-scraper` server.
|
|
181
181
|
|
|
@@ -235,9 +235,22 @@ env = { MCP_SCRAPER_API_KEY = "sk_live_your_key" }
|
|
|
235
235
|
|
|
236
236
|
## Tools
|
|
237
237
|
|
|
238
|
+
### Two-page PAA harvests
|
|
239
|
+
|
|
240
|
+
Pass this input to `harvest_paa` (or add an `idempotencyKey` for `harvest_paa_start`):
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
{"query":"commercial truck insurance","pages":2,"maxQuestions":10}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The harvester attempts page 2 organic results before expanding PAA on the untouched first page. It does not harvest a second PAA graph. `pages` accepts 1 or 2 and defaults to 1; pricing is unchanged. If page 2 is missing, empty, or cannot load, available first-page evidence is retained.
|
|
247
|
+
|
|
248
|
+
Check `pagination.requestedPages`, `pagination.capturedPages`, and `pagination.page2Status` in the MCP result before assuming both pages were captured. Raw REST results place the same fields under `diagnostics.pagination`. Legacy saved results may have no pagination metadata, which MCP formatters expose as `null`.
|
|
249
|
+
|
|
238
250
|
### Web-intelligence tools
|
|
239
251
|
|
|
240
|
-
- `harvest_paa`
|
|
252
|
+
- `harvest_paa` — expand People Also Ask on the original first result page. Optional `pages: 2` captures a second organic result page first; the default is one page.
|
|
253
|
+
- `harvest_paa_start` — start the same harvest as a durable job, with the same optional `pages: 2`.
|
|
241
254
|
- `search_serp`
|
|
242
255
|
- `extract_url` — extract normal or Wayback-replayed page copy; Wayback results omit playback chrome and can include a timestamp-matched featured image. Set `preserveMedia:true` to union static and rendered/lazy media, collapse responsive variants, attach up to `maxInlineImages` AI-readable images, and receive an owner-scoped ZIP manifest readable with `archive_read`. Branding output ranks the site logo separately from evidence-bounded proof images such as certifications, awards, memberships, partner/customer marks, and press mentions.
|
|
243
256
|
- `map_site_urls`
|
package/dist/bin/api-server.js
CHANGED
|
@@ -17,8 +17,8 @@ loadDotEnv();
|
|
|
17
17
|
async function main() {
|
|
18
18
|
const [{ serve }, { app, personalAssistantProductionStartup }, { startWorker }, { migrate }] = await Promise.all([
|
|
19
19
|
import("@hono/node-server"),
|
|
20
|
-
import("../server-
|
|
21
|
-
import("../worker-
|
|
20
|
+
import("../server-4QWARJET.js"),
|
|
21
|
+
import("../worker-DAVCZ3FP.js"),
|
|
22
22
|
import("../db-Z34LPZNR.js")
|
|
23
23
|
]);
|
|
24
24
|
const PORT = parseInt(process.env.PORT ?? "3001");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runMcpScraperStdio
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-6XDNIYGP.js";
|
|
5
|
+
import "../chunk-XX6XUZQM.js";
|
|
6
6
|
import "../chunk-PGJQDMC2.js";
|
|
7
7
|
import "../chunk-DNM65UCK.js";
|
|
8
8
|
import "../chunk-T3MZISOF.js";
|
|
@@ -10,10 +10,10 @@ import "../chunk-6HAV7LCE.js";
|
|
|
10
10
|
import "../chunk-NKRO2SUO.js";
|
|
11
11
|
import "../chunk-OM7HVEJ3.js";
|
|
12
12
|
import "../chunk-OPQIGAFB.js";
|
|
13
|
-
import "../chunk-
|
|
13
|
+
import "../chunk-UDLPENTY.js";
|
|
14
14
|
import "../chunk-P7FWOMU7.js";
|
|
15
15
|
import "../chunk-GGZEC22A.js";
|
|
16
|
-
import "../chunk-
|
|
16
|
+
import "../chunk-Y3AIFO7C.js";
|
|
17
17
|
import "../chunk-YXNDOQXN.js";
|
|
18
18
|
|
|
19
19
|
// src/mcp/thorbit-restricted-tool-surface.ts
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
renderInstallTerminal
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-UDLPENTY.js";
|
|
5
5
|
import {
|
|
6
6
|
PACKAGE_VERSION
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-Y3AIFO7C.js";
|
|
8
8
|
|
|
9
9
|
// bin/mcp-scraper-install.ts
|
|
10
10
|
var noColor = process.argv.includes("--no-color") || process.env.NO_COLOR !== void 0 || process.env.FORCE_COLOR === "0" || !process.stdout.isTTY;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runMcpScraperStdio
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-6XDNIYGP.js";
|
|
5
|
+
import "../chunk-XX6XUZQM.js";
|
|
6
6
|
import "../chunk-PGJQDMC2.js";
|
|
7
7
|
import "../chunk-DNM65UCK.js";
|
|
8
8
|
import "../chunk-T3MZISOF.js";
|
|
@@ -10,10 +10,10 @@ import "../chunk-6HAV7LCE.js";
|
|
|
10
10
|
import "../chunk-NKRO2SUO.js";
|
|
11
11
|
import "../chunk-OM7HVEJ3.js";
|
|
12
12
|
import "../chunk-OPQIGAFB.js";
|
|
13
|
-
import "../chunk-
|
|
13
|
+
import "../chunk-UDLPENTY.js";
|
|
14
14
|
import "../chunk-P7FWOMU7.js";
|
|
15
15
|
import "../chunk-GGZEC22A.js";
|
|
16
|
-
import "../chunk-
|
|
16
|
+
import "../chunk-Y3AIFO7C.js";
|
|
17
17
|
import "../chunk-YXNDOQXN.js";
|
|
18
18
|
|
|
19
19
|
// bin/mcp-stdio-server.ts
|
package/dist/bin/paa-harvest.js
CHANGED
|
@@ -12,13 +12,13 @@ import {
|
|
|
12
12
|
registerScheduledResultsMcpTools,
|
|
13
13
|
registerSerpIntelligenceCaptureTools,
|
|
14
14
|
resolveDeploymentProfile
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-XX6XUZQM.js";
|
|
16
16
|
import {
|
|
17
17
|
renderInstallTerminal
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-UDLPENTY.js";
|
|
19
19
|
import {
|
|
20
20
|
PACKAGE_VERSION
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-Y3AIFO7C.js";
|
|
22
22
|
|
|
23
23
|
// src/mcp/stdio-runtime.ts
|
|
24
24
|
import { readFileSync } from "fs";
|
|
@@ -134,6 +134,58 @@ var MapsSelectors = {
|
|
|
134
134
|
expandReview: '[data-review-id] button[aria-label*="See more"], [data-review-id] button.w8nwRe'
|
|
135
135
|
};
|
|
136
136
|
|
|
137
|
+
// src/driver/IBrowserDriver.ts
|
|
138
|
+
async function withManagedTemporaryPage(context, setup, operation, signal) {
|
|
139
|
+
let page;
|
|
140
|
+
let ended = false;
|
|
141
|
+
let timer;
|
|
142
|
+
let rejectStop = () => {
|
|
143
|
+
};
|
|
144
|
+
const aborted = () => signal?.reason instanceof DOMException && signal.reason.name === "TimeoutError" ? signal.reason : new RequestAbortedError();
|
|
145
|
+
const onAbort = () => rejectStop(aborted());
|
|
146
|
+
const stop = new Promise((_, reject) => {
|
|
147
|
+
rejectStop = reject;
|
|
148
|
+
});
|
|
149
|
+
const close = async (target) => {
|
|
150
|
+
let closeTimer;
|
|
151
|
+
try {
|
|
152
|
+
await Promise.race([
|
|
153
|
+
target.close().catch(() => {
|
|
154
|
+
}),
|
|
155
|
+
new Promise((resolve) => {
|
|
156
|
+
closeTimer = setTimeout(resolve, 2e3);
|
|
157
|
+
})
|
|
158
|
+
]);
|
|
159
|
+
} finally {
|
|
160
|
+
if (closeTimer) clearTimeout(closeTimer);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
if (signal?.aborted) throw aborted();
|
|
164
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
165
|
+
timer = setTimeout(() => rejectStop(new DOMException("Optional pagination timed out", "TimeoutError")), 3e4);
|
|
166
|
+
try {
|
|
167
|
+
return await Promise.race([
|
|
168
|
+
(async () => {
|
|
169
|
+
const created = await context.newPage();
|
|
170
|
+
if (ended) {
|
|
171
|
+
await close(created);
|
|
172
|
+
throw new RequestAbortedError();
|
|
173
|
+
}
|
|
174
|
+
page = created;
|
|
175
|
+
await setup(created);
|
|
176
|
+
if (ended) throw new RequestAbortedError();
|
|
177
|
+
return operation(created);
|
|
178
|
+
})(),
|
|
179
|
+
stop
|
|
180
|
+
]);
|
|
181
|
+
} finally {
|
|
182
|
+
ended = true;
|
|
183
|
+
if (timer) clearTimeout(timer);
|
|
184
|
+
signal?.removeEventListener("abort", onAbort);
|
|
185
|
+
if (page) await close(page);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
137
189
|
// src/lib/browser-user-agent.ts
|
|
138
190
|
var RUNTIME_BROWSER_UA = /\b(?:HeadlessChrome|Chrome|Chromium|Firefox)\/\d+|\bVersion\/\d+.*\bSafari\//;
|
|
139
191
|
async function detectPageUserAgent(page) {
|
|
@@ -523,6 +575,11 @@ var BrowserDriver = class {
|
|
|
523
575
|
};
|
|
524
576
|
});
|
|
525
577
|
}
|
|
578
|
+
async withTemporaryPage(operation, signal) {
|
|
579
|
+
if (!this.context) throw new Error("Browser context is not available");
|
|
580
|
+
return withManagedTemporaryPage(this.context, async () => {
|
|
581
|
+
}, operation, signal);
|
|
582
|
+
}
|
|
526
583
|
async navigateToSERP(query, uule, gl, hl, options) {
|
|
527
584
|
const params = new URLSearchParams({ q: query, gl, hl, pws: "0" });
|
|
528
585
|
if (options?.tbs) params.set("tbs", options.tbs);
|
|
@@ -927,7 +984,10 @@ var BrightDataSerpDriver = class {
|
|
|
927
984
|
const customCdp = this.cdpSession;
|
|
928
985
|
const sessionResponse = await customCdp.send("Browser.getSessionId");
|
|
929
986
|
this.providerSessionId = validateProviderSessionId(sessionResponse?.sessionId);
|
|
930
|
-
await this.page
|
|
987
|
+
await this.setupPage(this.page);
|
|
988
|
+
}
|
|
989
|
+
async setupPage(page) {
|
|
990
|
+
await page.route("**/*", async (route) => {
|
|
931
991
|
const request = route.request();
|
|
932
992
|
const resourceType = request.resourceType();
|
|
933
993
|
let requestUrl;
|
|
@@ -952,6 +1012,10 @@ var BrightDataSerpDriver = class {
|
|
|
952
1012
|
await route.continue();
|
|
953
1013
|
});
|
|
954
1014
|
}
|
|
1015
|
+
async withTemporaryPage(operation, signal) {
|
|
1016
|
+
if (!this.context || this.closing) throw new Error("Browser context is not available");
|
|
1017
|
+
return withManagedTemporaryPage(this.context, (page) => this.setupPage(page), operation, signal);
|
|
1018
|
+
}
|
|
955
1019
|
async navigateToSERP(query, uule, gl, hl, options) {
|
|
956
1020
|
if (!this.page) throw new Error("Browser page is not available");
|
|
957
1021
|
const params = new URLSearchParams({ q: query, gl, hl, pws: "0" });
|
|
@@ -1927,6 +1991,11 @@ function parsedGoogleGoto(value) {
|
|
|
1927
1991
|
if (!url || url.protocol !== "https:" || !isGoogleHost(url.hostname) || url.pathname !== "/goto") return null;
|
|
1928
1992
|
return url.searchParams.get("url") ? url : null;
|
|
1929
1993
|
}
|
|
1994
|
+
function hasResolvedGotoDestination(value) {
|
|
1995
|
+
if (value.linkType !== "google_goto_redirect" || value.resolutionStatus !== "resolved" || !parsedGoogleGoto(value.rawUrl) || !value.resolvedUrl) return false;
|
|
1996
|
+
const destination = safeHttpUrl(value.resolvedUrl);
|
|
1997
|
+
return Boolean(destination && !isGoogleHost(destination.hostname) && destination.href === value.resolvedUrl && value.url === destination.href);
|
|
1998
|
+
}
|
|
1930
1999
|
function questionIdFor(question) {
|
|
1931
2000
|
const normalized = question.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^\p{L}\p{N}\s]/gu, "").replace(/\s+/g, " ").trim();
|
|
1932
2001
|
return `paa_${createHash("sha256").update(normalized).digest("hex").slice(0, 16)}`;
|
|
@@ -2143,8 +2212,12 @@ async function resolveHarvestResultGoogleGotoLinks(result, options = {}) {
|
|
|
2143
2212
|
}
|
|
2144
2213
|
async function resolveGoogleOutboundLinks(page, values, options = {}) {
|
|
2145
2214
|
const byRawUrl = /* @__PURE__ */ new Map();
|
|
2146
|
-
for (const value of values)
|
|
2147
|
-
|
|
2215
|
+
for (const value of values) {
|
|
2216
|
+
const existing = byRawUrl.get(value.rawUrl);
|
|
2217
|
+
if (existing && hasResolvedGotoDestination(existing)) continue;
|
|
2218
|
+
byRawUrl.set(value.rawUrl, value.linkType === "google_goto_redirect" && !hasResolvedGotoDestination(value) ? { ...value, url: value.rawUrl, resolvedUrl: null, resolutionStatus: "unresolved" } : value);
|
|
2219
|
+
}
|
|
2220
|
+
const inputs = [...byRawUrl.values()].filter((value) => value.linkType === "google_goto_redirect" && !hasResolvedGotoDestination(value));
|
|
2148
2221
|
if (inputs.length === 0) return byRawUrl;
|
|
2149
2222
|
const deadlineMs = Date.now() + (options.totalBudgetMs ?? DEFAULT_TOTAL_BUDGET_MS);
|
|
2150
2223
|
const direct = await resolveGoogleGotoUrls(inputs.map((value) => value.rawUrl), options);
|
|
@@ -2360,7 +2433,7 @@ var PAAExtractor = class {
|
|
|
2360
2433
|
];
|
|
2361
2434
|
const unique = new Map(candidates.map((link) => [link.rawUrl, link]));
|
|
2362
2435
|
let resolverNonGoogleRequestsObserved = 0;
|
|
2363
|
-
const resolved = await resolveGoogleOutboundLinks(page,
|
|
2436
|
+
const resolved = await resolveGoogleOutboundLinks(page, candidates, {
|
|
2364
2437
|
onNetworkAudit: (audit) => {
|
|
2365
2438
|
resolverNonGoogleRequestsObserved = audit.nonGoogleRequestsObserved;
|
|
2366
2439
|
}
|
|
@@ -2611,6 +2684,7 @@ var PAAExtractor = class {
|
|
|
2611
2684
|
diagnostics: {
|
|
2612
2685
|
completionStatus: "paa_found",
|
|
2613
2686
|
problem: null,
|
|
2687
|
+
...material?.pagination ? { pagination: { ...material.pagination } } : {},
|
|
2614
2688
|
resultQuality: "partial",
|
|
2615
2689
|
degradedResult: false,
|
|
2616
2690
|
retryRecommended: true,
|
|
@@ -3575,6 +3649,55 @@ var PAAExtractor = class {
|
|
|
3575
3649
|
...locationEvidence ? { locationEvidence } : {}
|
|
3576
3650
|
};
|
|
3577
3651
|
}
|
|
3652
|
+
async captureSecondOrganicPage(page, options, signal) {
|
|
3653
|
+
this.throwIfAborted(signal);
|
|
3654
|
+
if (!this.driver.withTemporaryPage) return { organic: [], status: "failed", failureCode: "unsupported_driver" };
|
|
3655
|
+
let guardedFailure;
|
|
3656
|
+
try {
|
|
3657
|
+
const initial = new URL(page.url());
|
|
3658
|
+
const valid = (raw) => {
|
|
3659
|
+
try {
|
|
3660
|
+
const url = new URL(raw, initial);
|
|
3661
|
+
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));
|
|
3662
|
+
} catch {
|
|
3663
|
+
return false;
|
|
3664
|
+
}
|
|
3665
|
+
};
|
|
3666
|
+
const href = await page.evaluate(() => {
|
|
3667
|
+
const next = document.querySelector('a#pnnext, a[rel="next"]');
|
|
3668
|
+
return next?.getAttribute("href") ?? null;
|
|
3669
|
+
});
|
|
3670
|
+
if (!href) return { organic: [], status: "unavailable", failureCode: "missing_next" };
|
|
3671
|
+
if (!valid(href)) return { organic: [], status: "unavailable", failureCode: "invalid_next" };
|
|
3672
|
+
const organic = await this.driver.withTemporaryPage(async (second) => {
|
|
3673
|
+
await second.route("**/*", async (route) => {
|
|
3674
|
+
const request = route.request();
|
|
3675
|
+
if (request.isNavigationRequest() && request.frame() === second.mainFrame() && !valid(request.url())) {
|
|
3676
|
+
guardedFailure = /^https:\/\/www\.google\.com\/sorry(?:\/|\?)/.test(request.url()) ? "captcha" : "invalid_next";
|
|
3677
|
+
await route.abort("blockedbyclient");
|
|
3678
|
+
} else await route.fallback();
|
|
3679
|
+
});
|
|
3680
|
+
await second.goto(new URL(href, initial).href, { waitUntil: "domcontentloaded", timeout: 2e4 });
|
|
3681
|
+
this.throwIfAborted(signal);
|
|
3682
|
+
await this.throwIfCaptcha(second, "Google SERP page 2");
|
|
3683
|
+
if (!valid(second.url())) {
|
|
3684
|
+
guardedFailure = "invalid_next";
|
|
3685
|
+
throw new Error("Invalid pagination landing");
|
|
3686
|
+
}
|
|
3687
|
+
return this.extractOrganicResults(second);
|
|
3688
|
+
}, signal);
|
|
3689
|
+
this.throwIfAborted(signal);
|
|
3690
|
+
return organic.length > 0 ? { organic: organic.map((row) => ({ ...row, position: row.position + 10 })), status: "captured" } : { organic: [], status: "unavailable", failureCode: "empty_page" };
|
|
3691
|
+
} catch (err) {
|
|
3692
|
+
this.throwIfAborted(signal);
|
|
3693
|
+
if (err instanceof RequestAbortedError) throw err;
|
|
3694
|
+
return {
|
|
3695
|
+
organic: [],
|
|
3696
|
+
status: "failed",
|
|
3697
|
+
failureCode: guardedFailure ?? (err instanceof CaptchaError ? "captcha" : err instanceof Error && (err.name === "TimeoutError" || /timed? ?out/i.test(err.message)) ? "timeout" : "navigation_error")
|
|
3698
|
+
};
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3578
3701
|
async extract(options, signal) {
|
|
3579
3702
|
const startMs = Date.now();
|
|
3580
3703
|
this.completeness = { paaWithoutAnswer: 0, paaWithoutSource: 0, paaAnswersRecovered: 0, aioShareCaptured: null };
|
|
@@ -3778,23 +3901,49 @@ var PAAExtractor = class {
|
|
|
3778
3901
|
const initialLocationEvidence = options.debug ? inferSerpLocationEvidence(canonicalLocation, organicResults, localPack) : void 0;
|
|
3779
3902
|
this.reporter.onVideos(videos);
|
|
3780
3903
|
this.reporter.onForums(forums);
|
|
3904
|
+
const aiSurfaces = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
|
|
3905
|
+
let pagination = {
|
|
3906
|
+
requestedPages: (options.pages ?? 1) >= 2 ? 2 : 1,
|
|
3907
|
+
capturedPages: 1,
|
|
3908
|
+
page2Status: (options.pages ?? 1) >= 2 ? "not_attempted" : "not_requested",
|
|
3909
|
+
page1OrganicCount: organicResults.length,
|
|
3910
|
+
page2OrganicCount: 0
|
|
3911
|
+
};
|
|
3912
|
+
let allOrganic = organicResults;
|
|
3913
|
+
this.checkpointMaterial = {
|
|
3914
|
+
surface: aiSurfaces.surface,
|
|
3915
|
+
aiOverview: aiSurfaces.aiOverview,
|
|
3916
|
+
aiMode: aiSurfaces.aiMode,
|
|
3917
|
+
whatPeopleSaying,
|
|
3918
|
+
videos,
|
|
3919
|
+
forums,
|
|
3920
|
+
organicResults: allOrganic,
|
|
3921
|
+
localPack,
|
|
3922
|
+
entityIds,
|
|
3923
|
+
pagination
|
|
3924
|
+
};
|
|
3925
|
+
await this.emitProgress("serp_captured");
|
|
3926
|
+
if (pagination.requestedPages === 2) {
|
|
3927
|
+
const second = await this.captureSecondOrganicPage(page, executionOptions, signal);
|
|
3928
|
+
allOrganic = [...organicResults, ...second.organic];
|
|
3929
|
+
pagination = {
|
|
3930
|
+
...pagination,
|
|
3931
|
+
capturedPages: second.status === "captured" ? 2 : 1,
|
|
3932
|
+
page2Status: second.status,
|
|
3933
|
+
page2OrganicCount: second.organic.length,
|
|
3934
|
+
...second.failureCode ? { failureCode: second.failureCode } : {}
|
|
3935
|
+
};
|
|
3936
|
+
this.checkpointMaterial = { ...this.checkpointMaterial, organicResults: allOrganic, pagination };
|
|
3937
|
+
await this.emitProgress("serp_captured");
|
|
3938
|
+
await this.resolveMaterialLinks(page, [], allOrganic, aiSurfaces.aiOverview, aiSurfaces.aiMode);
|
|
3939
|
+
this.checkpointMaterial = { ...this.checkpointMaterial, organicResults: allOrganic };
|
|
3940
|
+
await this.emitProgress("serp_captured");
|
|
3941
|
+
}
|
|
3942
|
+
const locationEvidence = options.debug ? inferSerpLocationEvidence(canonicalLocation, allOrganic, localPack) : initialLocationEvidence;
|
|
3781
3943
|
if (!hasPaa) {
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
|
|
3786
|
-
if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
|
|
3787
|
-
if (uule) p2params.set("uule", uule);
|
|
3788
|
-
await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
|
|
3789
|
-
await this.throwIfCaptcha(page, "Google SERP page 2");
|
|
3790
|
-
const p2organic = await this.extractOrganicResults(page);
|
|
3791
|
-
noPaaOrganic = [...organicResults, ...p2organic.map((r) => ({ ...r, position: r.position + 10 }))];
|
|
3792
|
-
if (options.debug) {
|
|
3793
|
-
locationEvidence2 = inferSerpLocationEvidence(canonicalLocation, noPaaOrganic, localPack);
|
|
3794
|
-
}
|
|
3795
|
-
}
|
|
3796
|
-
const aiSurfaces2 = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
|
|
3797
|
-
await this.resolveMaterialLinks(page, [], noPaaOrganic, aiSurfaces2.aiOverview, aiSurfaces2.aiMode);
|
|
3944
|
+
await this.resolveMaterialLinks(page, [], allOrganic, aiSurfaces.aiOverview, aiSurfaces.aiMode);
|
|
3945
|
+
this.checkpointMaterial = { ...this.checkpointMaterial, organicResults: allOrganic };
|
|
3946
|
+
await this.emitProgress("serp_captured");
|
|
3798
3947
|
const stats2 = {
|
|
3799
3948
|
seed: executionOptions.query,
|
|
3800
3949
|
totalQuestions: 0,
|
|
@@ -3811,39 +3960,27 @@ var PAAExtractor = class {
|
|
|
3811
3960
|
completionStatus: "no_paa",
|
|
3812
3961
|
noPaaObserved: true,
|
|
3813
3962
|
problem: null,
|
|
3963
|
+
pagination,
|
|
3814
3964
|
paaLifecycle: { ...this.paaLifecycle },
|
|
3815
3965
|
completeness: { ...this.completeness },
|
|
3816
3966
|
links: { ...this.linkDiagnostics },
|
|
3817
|
-
...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule,
|
|
3967
|
+
...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence, locationResolution) } : {}
|
|
3818
3968
|
},
|
|
3819
3969
|
totalQuestions: 0,
|
|
3820
|
-
surface:
|
|
3821
|
-
aiOverview:
|
|
3822
|
-
aiMode:
|
|
3970
|
+
surface: aiSurfaces.surface,
|
|
3971
|
+
aiOverview: aiSurfaces.aiOverview,
|
|
3972
|
+
aiMode: aiSurfaces.aiMode,
|
|
3823
3973
|
whatPeopleSaying,
|
|
3824
3974
|
tree: [],
|
|
3825
3975
|
flat: [],
|
|
3826
3976
|
videos,
|
|
3827
3977
|
forums,
|
|
3828
|
-
organicResults:
|
|
3978
|
+
organicResults: allOrganic,
|
|
3829
3979
|
localPack,
|
|
3830
3980
|
entityIds,
|
|
3831
3981
|
stats: stats2
|
|
3832
3982
|
};
|
|
3833
3983
|
}
|
|
3834
|
-
const aiSurfaces = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
|
|
3835
|
-
this.checkpointMaterial = {
|
|
3836
|
-
surface: aiSurfaces.surface,
|
|
3837
|
-
aiOverview: aiSurfaces.aiOverview,
|
|
3838
|
-
aiMode: aiSurfaces.aiMode,
|
|
3839
|
-
whatPeopleSaying,
|
|
3840
|
-
videos,
|
|
3841
|
-
forums,
|
|
3842
|
-
organicResults,
|
|
3843
|
-
localPack,
|
|
3844
|
-
entityIds
|
|
3845
|
-
};
|
|
3846
|
-
await this.emitProgress("serp_captured");
|
|
3847
3984
|
const flat = await this.runBFS(page, executionOptions, signal);
|
|
3848
3985
|
this.throwIfAborted(signal);
|
|
3849
3986
|
const shortVidsParams = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", udm: ShortVideoSelectors.udm });
|
|
@@ -3864,20 +4001,6 @@ var PAAExtractor = class {
|
|
|
3864
4001
|
}
|
|
3865
4002
|
}
|
|
3866
4003
|
this.reporter.onVideos(shortVideos);
|
|
3867
|
-
let allOrganic = organicResults;
|
|
3868
|
-
let locationEvidence = initialLocationEvidence;
|
|
3869
|
-
if ((options.pages ?? 1) >= 2) {
|
|
3870
|
-
const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
|
|
3871
|
-
if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
|
|
3872
|
-
if (uule) p2params.set("uule", uule);
|
|
3873
|
-
await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
|
|
3874
|
-
await this.throwIfCaptcha(page, "Google SERP page 2");
|
|
3875
|
-
const p2organic = await this.extractOrganicResults(page);
|
|
3876
|
-
allOrganic = [...organicResults, ...p2organic.map((r) => ({ ...r, position: r.position + 10 }))];
|
|
3877
|
-
if (options.debug) {
|
|
3878
|
-
locationEvidence = inferSerpLocationEvidence(canonicalLocation, allOrganic, localPack);
|
|
3879
|
-
}
|
|
3880
|
-
}
|
|
3881
4004
|
await this.resolveMaterialLinks(page, flat, allOrganic, aiSurfaces.aiOverview, aiSurfaces.aiMode);
|
|
3882
4005
|
this.checkpointMaterial = {
|
|
3883
4006
|
...this.checkpointMaterial,
|
|
@@ -3887,6 +4010,7 @@ var PAAExtractor = class {
|
|
|
3887
4010
|
videos: [...videos, ...shortVideos],
|
|
3888
4011
|
organicResults: allOrganic
|
|
3889
4012
|
};
|
|
4013
|
+
await this.emitProgress("expansion_finished");
|
|
3890
4014
|
const allVideos = [...videos, ...shortVideos];
|
|
3891
4015
|
const tree = this.buildTree(flat, executionOptions.query);
|
|
3892
4016
|
const stats = {
|
|
@@ -3904,6 +4028,7 @@ var PAAExtractor = class {
|
|
|
3904
4028
|
diagnostics: {
|
|
3905
4029
|
completionStatus: "paa_found",
|
|
3906
4030
|
problem: null,
|
|
4031
|
+
pagination,
|
|
3907
4032
|
paaLifecycle: { ...this.paaLifecycle },
|
|
3908
4033
|
completeness: { ...this.completeness },
|
|
3909
4034
|
links: { ...this.linkDiagnostics },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// release-message.json
|
|
2
2
|
var release_message_default = {
|
|
3
|
-
message: "
|
|
3
|
+
message: "PAA harvests can now request two organic result pages before expanding questions on the original first page. Use pages: 2 in MCP, SDKs, or the dashboard and check the reported capture status; an unavailable second page preserves first-page evidence."
|
|
4
4
|
};
|
|
5
5
|
|
|
6
6
|
// src/install-terminal.ts
|