mcp-scraper 0.76.0 → 0.78.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 +23 -0
- package/README.md +1 -1
- package/dist/{analytics-repository-AUTJK4HK.js → analytics-repository-D5LEGQG3.js} +3 -3
- package/dist/bin/api-server.js +4 -4
- package/dist/bin/mcp-scraper-cli.js +3 -3
- package/dist/bin/mcp-scraper-core.js +7 -7
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.js +7 -7
- package/dist/bin/paa-harvest.js +4 -4
- package/dist/{chunk-BOGRXBH2.js → chunk-2P2ZVRPK.js} +189 -57
- package/dist/{chunk-E6Y3VCOJ.js → chunk-3PUMNEOC.js} +3 -3
- package/dist/{chunk-KIPIBPUB.js → chunk-47ZCUBM5.js} +1 -1
- package/dist/{chunk-IBQNOZ7E.js → chunk-74CCAY6T.js} +1 -1
- package/dist/{chunk-7RQULQF3.js → chunk-B6L73AO4.js} +1 -1
- package/dist/{chunk-WFQ2E4WE.js → chunk-BTQ23KCT.js} +1 -1
- package/dist/{chunk-SDX6TRJ6.js → chunk-FODYEDZK.js} +3 -3
- package/dist/{chunk-PJEEKOUM.js → chunk-FUMG43EN.js} +1 -1
- package/dist/{chunk-XSYEDC25.js → chunk-IYYRSVRQ.js} +1 -1
- package/dist/{chunk-A66DGFOU.js → chunk-KAGHJQFO.js} +1 -1
- package/dist/{chunk-CXY5WV45.js → chunk-NGLZ6UJR.js} +1 -1
- package/dist/{chunk-WEFPBAAG.js → chunk-OU4FV2RP.js} +140 -4
- package/dist/{chunk-F5TK4KMT.js → chunk-OUBH6XMY.js} +1 -1
- package/dist/{chunk-77LQV73B.js → chunk-OWF2JJKN.js} +13 -1
- package/dist/{chunk-F6MUGMRN.js → chunk-UFJJGXVO.js} +702 -165
- package/dist/{chunk-KBTUPRTF.js → chunk-VEQPA65D.js} +1 -1
- package/dist/{chunk-D2O7V2CT.js → chunk-VMWSXKUQ.js} +1 -1
- package/dist/{chunk-JNFOBQVH.js → chunk-ZUCU2A6V.js} +1 -1
- package/dist/{db-566MOHHD.js → db-TA567JLA.js} +17 -1
- package/dist/{extract-bundle-OO74L67T.js → extract-bundle-P2DTUVY3.js} +3 -3
- package/dist/{gmail-service-TIU4V5JA.js → gmail-service-HN57R2GS.js} +4 -4
- package/dist/index.cjs +714 -165
- package/dist/index.d.cts +83 -1
- package/dist/index.d.ts +83 -1
- package/dist/index.js +4 -4
- package/dist/{lead-list-enrichment-repository-WWEMJ7EN.js → lead-list-enrichment-repository-5LEWW4X7.js} +2 -2
- package/dist/{location-data-repository-H4B2BWGY.js → location-data-repository-AHKDXTEQ.js} +2 -2
- package/dist/{server-IAGYCPN6.js → server-6B2OG3NN.js} +1434 -983
- package/dist/{site-extract-repository-B47JB62U.js → site-extract-repository-XKSVXSSY.js} +2 -2
- package/dist/{worker-DR6R7J46.js → worker-2BYACLNF.js} +6 -6
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -128,11 +128,23 @@ var MapsSearchOptionsSchema = import_zod.z.object({
|
|
|
128
128
|
headless: import_zod.z.boolean().default(true)
|
|
129
129
|
});
|
|
130
130
|
var RawPAAItemSchema = import_zod.z.object({
|
|
131
|
+
questionId: import_zod.z.string().min(1),
|
|
132
|
+
googleLinkId: import_zod.z.string().nullable(),
|
|
133
|
+
googleEvidenceId: import_zod.z.string().nullable(),
|
|
131
134
|
question: import_zod.z.string().min(1),
|
|
132
135
|
answer: import_zod.z.string().optional(),
|
|
133
136
|
sourceTitle: import_zod.z.string().optional(),
|
|
134
137
|
sourceSite: import_zod.z.string().optional(),
|
|
135
|
-
sourceCite: import_zod.z.string().optional()
|
|
138
|
+
sourceCite: import_zod.z.string().optional(),
|
|
139
|
+
sources: import_zod.z.array(import_zod.z.object({
|
|
140
|
+
title: import_zod.z.string().nullable(),
|
|
141
|
+
site: import_zod.z.string().nullable(),
|
|
142
|
+
url: import_zod.z.string().min(1),
|
|
143
|
+
rawUrl: import_zod.z.string().min(1),
|
|
144
|
+
resolvedUrl: import_zod.z.string().nullable(),
|
|
145
|
+
linkType: import_zod.z.enum(["plain", "google_url_redirect", "google_goto_redirect"]),
|
|
146
|
+
resolutionStatus: import_zod.z.enum(["not_needed", "resolved", "unresolved", "rejected"])
|
|
147
|
+
})).default([])
|
|
136
148
|
});
|
|
137
149
|
var RawMapsOverviewSchema = import_zod.z.object({
|
|
138
150
|
name: import_zod.z.string().nullable(),
|
|
@@ -373,6 +385,9 @@ var RequestAbortedError = class extends Error {
|
|
|
373
385
|
super(message);
|
|
374
386
|
}
|
|
375
387
|
};
|
|
388
|
+
var PaaInteractionInterruptedError = class extends Error {
|
|
389
|
+
name = "PaaInteractionInterruptedError";
|
|
390
|
+
};
|
|
376
391
|
var LocationMismatchError = class extends Error {
|
|
377
392
|
name = "LocationMismatchError";
|
|
378
393
|
constructor(message = "Google returned results for a different location than requested") {
|
|
@@ -2254,16 +2269,15 @@ async function extractAISurfacesFromDocument(config) {
|
|
|
2254
2269
|
if (textOf(heading) === "AI Overview") return true;
|
|
2255
2270
|
const header = el.querySelector(selectors.aio.header);
|
|
2256
2271
|
if (textOf(header).split(/\n|\s{2,}/).some((part) => part.trim() === "AI Overview")) return true;
|
|
2257
|
-
return
|
|
2272
|
+
return false;
|
|
2258
2273
|
}
|
|
2259
2274
|
function findAIORoot() {
|
|
2260
2275
|
const primaryRoots = Array.from(document.querySelectorAll(selectors.aio.root));
|
|
2261
2276
|
const labeledPrimary = primaryRoots.find(hasAIOverviewLabel);
|
|
2262
2277
|
if (labeledPrimary) return labeledPrimary;
|
|
2263
|
-
if (primaryRoots.length > 0) return primaryRoots[0];
|
|
2264
2278
|
if (selectors.aio.legacyRoot) {
|
|
2265
2279
|
const legacy = document.querySelector(selectors.aio.legacyRoot);
|
|
2266
|
-
if (legacy) return legacy;
|
|
2280
|
+
if (legacy && hasAIOverviewLabel(legacy)) return legacy;
|
|
2267
2281
|
}
|
|
2268
2282
|
const headings = document.querySelectorAll(`${selectors.aio.heading}, h1, h2, h3, [role="heading"]`);
|
|
2269
2283
|
for (const h of headings) {
|
|
@@ -2342,26 +2356,12 @@ async function extractAISurfacesFromDocument(config) {
|
|
|
2342
2356
|
if (!raw || /not available|try again|can't generate/i.test(raw)) return null;
|
|
2343
2357
|
return raw;
|
|
2344
2358
|
}
|
|
2345
|
-
function
|
|
2359
|
+
function absoluteHref(rawHref) {
|
|
2346
2360
|
if (!rawHref || rawHref.startsWith("javascript:")) return null;
|
|
2347
|
-
let href = rawHref;
|
|
2348
2361
|
try {
|
|
2349
2362
|
const absolute = new URL(rawHref, window.location.href);
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
href = q;
|
|
2353
|
-
} else {
|
|
2354
|
-
href = absolute.href;
|
|
2355
|
-
}
|
|
2356
|
-
} catch {
|
|
2357
|
-
return null;
|
|
2358
|
-
}
|
|
2359
|
-
if (!/^https?:\/\//i.test(href)) return null;
|
|
2360
|
-
try {
|
|
2361
|
-
const url = new URL(href);
|
|
2362
|
-
const isGoogleInternal = /(\.|^)google\./i.test(url.hostname);
|
|
2363
|
-
if (isGoogleInternal) return null;
|
|
2364
|
-
return url.href;
|
|
2363
|
+
if (!["http:", "https:"].includes(absolute.protocol) || absolute.username || absolute.password) return null;
|
|
2364
|
+
return { href: absolute.href, rawHref: absolute.href };
|
|
2365
2365
|
} catch {
|
|
2366
2366
|
return null;
|
|
2367
2367
|
}
|
|
@@ -2371,17 +2371,18 @@ async function extractAISurfacesFromDocument(config) {
|
|
|
2371
2371
|
const seen = /* @__PURE__ */ new Set();
|
|
2372
2372
|
const citations = [];
|
|
2373
2373
|
for (const a of Array.from(root.querySelectorAll("a[href]"))) {
|
|
2374
|
-
const
|
|
2375
|
-
if (!
|
|
2376
|
-
seen.add(
|
|
2374
|
+
const link = absoluteHref(a.getAttribute("href") ?? "");
|
|
2375
|
+
if (!link || seen.has(link.rawHref)) continue;
|
|
2376
|
+
seen.add(link.rawHref);
|
|
2377
2377
|
let fallbackHost = "";
|
|
2378
2378
|
try {
|
|
2379
|
-
fallbackHost = new URL(href).hostname.replace(/^www\./, "");
|
|
2379
|
+
fallbackHost = new URL(link.href).hostname.replace(/^www\./, "");
|
|
2380
2380
|
} catch {
|
|
2381
2381
|
}
|
|
2382
2382
|
citations.push({
|
|
2383
|
-
text: textOf(a) || fallbackHost || href,
|
|
2384
|
-
href
|
|
2383
|
+
text: textOf(a) || fallbackHost || link.href,
|
|
2384
|
+
href: link.href,
|
|
2385
|
+
rawHref: link.rawHref
|
|
2385
2386
|
});
|
|
2386
2387
|
}
|
|
2387
2388
|
return citations;
|
|
@@ -2436,6 +2437,286 @@ async function extractAISurfacesFromDocument(config) {
|
|
|
2436
2437
|
};
|
|
2437
2438
|
}
|
|
2438
2439
|
|
|
2440
|
+
// src/extractor/google-outbound-links.ts
|
|
2441
|
+
var import_node_crypto3 = require("crypto");
|
|
2442
|
+
var MAX_LINK_LENGTH = 8e3;
|
|
2443
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
2444
|
+
var DEFAULT_TOTAL_BUDGET_MS = 1e4;
|
|
2445
|
+
var DEFAULT_HTTP_CONCURRENCY = 8;
|
|
2446
|
+
function isGoogleHost(hostname) {
|
|
2447
|
+
return /(^|\.)google\.(?:com|[a-z]{2,3}(?:\.[a-z]{2})?)$/i.test(hostname);
|
|
2448
|
+
}
|
|
2449
|
+
function safeHttpUrl(value) {
|
|
2450
|
+
if (!value || value.length > MAX_LINK_LENGTH) return null;
|
|
2451
|
+
try {
|
|
2452
|
+
const url = new URL(value);
|
|
2453
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
2454
|
+
if (url.username || url.password) return null;
|
|
2455
|
+
return url;
|
|
2456
|
+
} catch {
|
|
2457
|
+
return null;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
function absoluteHttpUrl(value, baseUrl) {
|
|
2461
|
+
if (!value || value.length > MAX_LINK_LENGTH || /^javascript:/i.test(value)) return null;
|
|
2462
|
+
try {
|
|
2463
|
+
const absolute = new URL(value, baseUrl);
|
|
2464
|
+
return safeHttpUrl(absolute.href);
|
|
2465
|
+
} catch {
|
|
2466
|
+
return null;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
function parsedGoogleGoto(value) {
|
|
2470
|
+
const url = safeHttpUrl(value);
|
|
2471
|
+
if (!url || url.protocol !== "https:" || !isGoogleHost(url.hostname) || url.pathname !== "/goto") return null;
|
|
2472
|
+
return url.searchParams.get("url") ? url : null;
|
|
2473
|
+
}
|
|
2474
|
+
function questionIdFor(question) {
|
|
2475
|
+
const normalized = question.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^\p{L}\p{N}\s]/gu, "").replace(/\s+/g, " ").trim();
|
|
2476
|
+
return `paa_${(0, import_node_crypto3.createHash)("sha256").update(normalized).digest("hex").slice(0, 16)}`;
|
|
2477
|
+
}
|
|
2478
|
+
function classifyGoogleOutboundLink(rawHref, baseUrl) {
|
|
2479
|
+
const absolute = absoluteHttpUrl(rawHref, baseUrl);
|
|
2480
|
+
if (!absolute) return null;
|
|
2481
|
+
const rawUrl = absolute.href;
|
|
2482
|
+
if (!isGoogleHost(absolute.hostname)) {
|
|
2483
|
+
return {
|
|
2484
|
+
url: rawUrl,
|
|
2485
|
+
rawUrl,
|
|
2486
|
+
resolvedUrl: rawUrl,
|
|
2487
|
+
linkType: "plain",
|
|
2488
|
+
resolutionStatus: "not_needed"
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
if (absolute.pathname === "/url") {
|
|
2492
|
+
const target = safeHttpUrl(absolute.searchParams.get("q") ?? absolute.searchParams.get("url") ?? "");
|
|
2493
|
+
if (!target || isGoogleHost(target.hostname)) return null;
|
|
2494
|
+
return {
|
|
2495
|
+
url: target.href,
|
|
2496
|
+
rawUrl,
|
|
2497
|
+
resolvedUrl: target.href,
|
|
2498
|
+
linkType: "google_url_redirect",
|
|
2499
|
+
resolutionStatus: "resolved"
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
if (absolute.pathname === "/goto" && absolute.searchParams.get("url")) {
|
|
2503
|
+
return {
|
|
2504
|
+
url: rawUrl,
|
|
2505
|
+
rawUrl,
|
|
2506
|
+
resolvedUrl: null,
|
|
2507
|
+
linkType: "google_goto_redirect",
|
|
2508
|
+
resolutionStatus: "unresolved"
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
return null;
|
|
2512
|
+
}
|
|
2513
|
+
function tokenFor(value) {
|
|
2514
|
+
return parsedGoogleGoto(value)?.searchParams.get("url") ?? null;
|
|
2515
|
+
}
|
|
2516
|
+
function externalRedirectTarget(source, location) {
|
|
2517
|
+
try {
|
|
2518
|
+
const sourceUrl = new URL(source);
|
|
2519
|
+
const target = safeHttpUrl(new URL(location, sourceUrl).href);
|
|
2520
|
+
if (!target || isGoogleHost(target.hostname)) return null;
|
|
2521
|
+
if (!target.hash && sourceUrl.hash) target.hash = sourceUrl.hash;
|
|
2522
|
+
return target.href;
|
|
2523
|
+
} catch {
|
|
2524
|
+
return null;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
async function resolveGoogleGotoUrls(rawUrls, options = {}) {
|
|
2528
|
+
const inputs = [...new Set([...rawUrls].flatMap((value) => parsedGoogleGoto(value) ? [value] : []))];
|
|
2529
|
+
const records = new Map(inputs.map((rawUrl) => [rawUrl, {
|
|
2530
|
+
rawUrl,
|
|
2531
|
+
resolvedUrl: null,
|
|
2532
|
+
resolutionStatus: "unresolved"
|
|
2533
|
+
}]));
|
|
2534
|
+
const fallbackFetch = typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : null;
|
|
2535
|
+
const fetchImpl = options.fetchImpl ?? fallbackFetch;
|
|
2536
|
+
if (inputs.length === 0 || !fetchImpl) {
|
|
2537
|
+
return { resolutions: [...records.values()], nonGoogleRequestsObserved: 0 };
|
|
2538
|
+
}
|
|
2539
|
+
const timeoutMs = Math.max(1, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
2540
|
+
const deadlineMs = Date.now() + Math.max(1, options.totalBudgetMs ?? DEFAULT_TOTAL_BUDGET_MS);
|
|
2541
|
+
const concurrency = Math.max(1, Math.min(16, Math.floor(options.concurrency ?? DEFAULT_HTTP_CONCURRENCY)));
|
|
2542
|
+
let cursor = 0;
|
|
2543
|
+
const worker = async () => {
|
|
2544
|
+
while (cursor < inputs.length) {
|
|
2545
|
+
const rawUrl = inputs[cursor++];
|
|
2546
|
+
const source = parsedGoogleGoto(rawUrl);
|
|
2547
|
+
const expectedToken = source?.searchParams.get("url");
|
|
2548
|
+
if (!source || !expectedToken) {
|
|
2549
|
+
records.set(rawUrl, { rawUrl, resolvedUrl: null, resolutionStatus: "rejected" });
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
source.hash = "";
|
|
2553
|
+
let currentUrl = source.href;
|
|
2554
|
+
for (let hop = 0; hop < 2; hop++) {
|
|
2555
|
+
const remainingMs = deadlineMs - Date.now();
|
|
2556
|
+
if (remainingMs <= 0) return;
|
|
2557
|
+
const controller = new AbortController();
|
|
2558
|
+
const timer = setTimeout(() => controller.abort(), Math.min(timeoutMs, remainingMs));
|
|
2559
|
+
let response;
|
|
2560
|
+
try {
|
|
2561
|
+
response = await fetchImpl(currentUrl, {
|
|
2562
|
+
method: "GET",
|
|
2563
|
+
redirect: "manual",
|
|
2564
|
+
headers: {
|
|
2565
|
+
accept: "*/*",
|
|
2566
|
+
"user-agent": "Mozilla/5.0"
|
|
2567
|
+
},
|
|
2568
|
+
signal: controller.signal
|
|
2569
|
+
});
|
|
2570
|
+
} catch {
|
|
2571
|
+
break;
|
|
2572
|
+
} finally {
|
|
2573
|
+
clearTimeout(timer);
|
|
2574
|
+
}
|
|
2575
|
+
if (response.status < 300 || response.status >= 400) break;
|
|
2576
|
+
const location = response.headers.get("location");
|
|
2577
|
+
if (!location) break;
|
|
2578
|
+
const destination = externalRedirectTarget(rawUrl, location);
|
|
2579
|
+
if (destination) {
|
|
2580
|
+
records.set(rawUrl, { rawUrl, resolvedUrl: destination, resolutionStatus: "resolved" });
|
|
2581
|
+
break;
|
|
2582
|
+
}
|
|
2583
|
+
let next;
|
|
2584
|
+
try {
|
|
2585
|
+
next = new URL(location, currentUrl);
|
|
2586
|
+
} catch {
|
|
2587
|
+
records.set(rawUrl, { rawUrl, resolvedUrl: null, resolutionStatus: "rejected" });
|
|
2588
|
+
break;
|
|
2589
|
+
}
|
|
2590
|
+
if (!isGoogleHost(next.hostname) || tokenFor(next.href) !== expectedToken) {
|
|
2591
|
+
records.set(rawUrl, { rawUrl, resolvedUrl: null, resolutionStatus: "rejected" });
|
|
2592
|
+
break;
|
|
2593
|
+
}
|
|
2594
|
+
currentUrl = next.href;
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, inputs.length) }, () => worker()));
|
|
2599
|
+
return { resolutions: [...records.values()], nonGoogleRequestsObserved: 0 };
|
|
2600
|
+
}
|
|
2601
|
+
async function resolveGoogleOutboundLinks(page, values, options = {}) {
|
|
2602
|
+
const byRawUrl = /* @__PURE__ */ new Map();
|
|
2603
|
+
for (const value of values) byRawUrl.set(value.rawUrl, value);
|
|
2604
|
+
const inputs = [...byRawUrl.values()].filter((value) => value.linkType === "google_goto_redirect");
|
|
2605
|
+
if (inputs.length === 0) return byRawUrl;
|
|
2606
|
+
const deadlineMs = Date.now() + (options.totalBudgetMs ?? DEFAULT_TOTAL_BUDGET_MS);
|
|
2607
|
+
const direct = await resolveGoogleGotoUrls(inputs.map((value) => value.rawUrl), options);
|
|
2608
|
+
for (const record of direct.resolutions) {
|
|
2609
|
+
const input = byRawUrl.get(record.rawUrl);
|
|
2610
|
+
if (!input) continue;
|
|
2611
|
+
byRawUrl.set(record.rawUrl, record.resolutionStatus === "resolved" && record.resolvedUrl ? { ...input, url: record.resolvedUrl, resolvedUrl: record.resolvedUrl, resolutionStatus: "resolved" } : { ...input, resolutionStatus: record.resolutionStatus });
|
|
2612
|
+
}
|
|
2613
|
+
const unresolvedInputs = inputs.filter((input) => byRawUrl.get(input.rawUrl)?.resolutionStatus === "unresolved");
|
|
2614
|
+
if (unresolvedInputs.length === 0 || options.browserFallback === false || Date.now() >= deadlineMs) {
|
|
2615
|
+
options.onNetworkAudit?.({ nonGoogleRequestsObserved: 0 });
|
|
2616
|
+
return byRawUrl;
|
|
2617
|
+
}
|
|
2618
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2619
|
+
const resolverPage = await page.context().newPage().catch(() => null);
|
|
2620
|
+
if (!resolverPage) {
|
|
2621
|
+
options.onNetworkAudit?.({ nonGoogleRequestsObserved: 0 });
|
|
2622
|
+
return byRawUrl;
|
|
2623
|
+
}
|
|
2624
|
+
let active = null;
|
|
2625
|
+
let nonGoogleRequestsObserved = 0;
|
|
2626
|
+
try {
|
|
2627
|
+
await resolverPage.route("**/*", async (route) => {
|
|
2628
|
+
const requestUrl = route.request().url();
|
|
2629
|
+
try {
|
|
2630
|
+
if (!isGoogleHost(new URL(requestUrl).hostname)) nonGoogleRequestsObserved += 1;
|
|
2631
|
+
} catch {
|
|
2632
|
+
}
|
|
2633
|
+
const requestToken = tokenFor(requestUrl);
|
|
2634
|
+
if (!active || requestToken !== active.token) {
|
|
2635
|
+
await route.abort("blockedbyclient").catch(() => {
|
|
2636
|
+
});
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
const target = active;
|
|
2640
|
+
try {
|
|
2641
|
+
const response = await route.fetch({ maxRedirects: 0, timeout: target.timeoutMs });
|
|
2642
|
+
target.location = response.headers().location ?? null;
|
|
2643
|
+
} finally {
|
|
2644
|
+
await route.abort("blockedbyclient").catch(() => {
|
|
2645
|
+
});
|
|
2646
|
+
target.finish();
|
|
2647
|
+
}
|
|
2648
|
+
});
|
|
2649
|
+
for (const input of unresolvedInputs) {
|
|
2650
|
+
const remainingMs = deadlineMs - Date.now();
|
|
2651
|
+
if (remainingMs <= 0) break;
|
|
2652
|
+
const source = parsedGoogleGoto(input.rawUrl);
|
|
2653
|
+
const expectedToken = source?.searchParams.get("url");
|
|
2654
|
+
if (!source || !expectedToken) {
|
|
2655
|
+
byRawUrl.set(input.rawUrl, { ...input, resolutionStatus: "rejected" });
|
|
2656
|
+
continue;
|
|
2657
|
+
}
|
|
2658
|
+
const attemptTimeoutMs = Math.min(timeoutMs, remainingMs);
|
|
2659
|
+
source.hash = "";
|
|
2660
|
+
let currentUrl = source.href;
|
|
2661
|
+
let rejected = false;
|
|
2662
|
+
for (let hop = 0; hop < 2; hop++) {
|
|
2663
|
+
let finish;
|
|
2664
|
+
const intercepted = new Promise((resolve) => {
|
|
2665
|
+
finish = resolve;
|
|
2666
|
+
});
|
|
2667
|
+
const frameId = `mcp-goto-${Math.random().toString(36).slice(2)}`;
|
|
2668
|
+
active = { token: expectedToken, timeoutMs: attemptTimeoutMs, location: null, finish };
|
|
2669
|
+
await resolverPage.evaluate(({ url, id }) => {
|
|
2670
|
+
const frame = document.createElement("iframe");
|
|
2671
|
+
frame.id = id;
|
|
2672
|
+
frame.hidden = true;
|
|
2673
|
+
frame.src = url;
|
|
2674
|
+
document.body.append(frame);
|
|
2675
|
+
}, { url: currentUrl, id: frameId }).catch(() => null);
|
|
2676
|
+
await Promise.race([
|
|
2677
|
+
intercepted,
|
|
2678
|
+
new Promise((resolve) => setTimeout(resolve, attemptTimeoutMs))
|
|
2679
|
+
]);
|
|
2680
|
+
const location = active.location;
|
|
2681
|
+
active = null;
|
|
2682
|
+
await resolverPage.evaluate((id) => document.getElementById(id)?.remove(), frameId).catch(() => null);
|
|
2683
|
+
if (!location) break;
|
|
2684
|
+
const destination = externalRedirectTarget(input.rawUrl, location);
|
|
2685
|
+
if (destination) {
|
|
2686
|
+
byRawUrl.set(input.rawUrl, {
|
|
2687
|
+
...input,
|
|
2688
|
+
url: destination,
|
|
2689
|
+
resolvedUrl: destination,
|
|
2690
|
+
resolutionStatus: "resolved"
|
|
2691
|
+
});
|
|
2692
|
+
break;
|
|
2693
|
+
}
|
|
2694
|
+
let next;
|
|
2695
|
+
try {
|
|
2696
|
+
next = new URL(location, currentUrl);
|
|
2697
|
+
} catch {
|
|
2698
|
+
rejected = true;
|
|
2699
|
+
break;
|
|
2700
|
+
}
|
|
2701
|
+
if (!isGoogleHost(next.hostname) || tokenFor(next.href) !== expectedToken) {
|
|
2702
|
+
rejected = true;
|
|
2703
|
+
break;
|
|
2704
|
+
}
|
|
2705
|
+
currentUrl = next.href;
|
|
2706
|
+
}
|
|
2707
|
+
if (rejected && byRawUrl.get(input.rawUrl)?.resolutionStatus !== "resolved") {
|
|
2708
|
+
byRawUrl.set(input.rawUrl, { ...input, resolutionStatus: "rejected" });
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
} finally {
|
|
2712
|
+
active = null;
|
|
2713
|
+
await resolverPage.close().catch(() => {
|
|
2714
|
+
});
|
|
2715
|
+
options.onNetworkAudit?.({ nonGoogleRequestsObserved });
|
|
2716
|
+
}
|
|
2717
|
+
return byRawUrl;
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2439
2720
|
// src/extractor/PAAExtractor.ts
|
|
2440
2721
|
var PAAExtractor = class {
|
|
2441
2722
|
constructor(driver, reporter, onPaaProgress) {
|
|
@@ -2453,10 +2734,118 @@ var PAAExtractor = class {
|
|
|
2453
2734
|
aioShareCaptured: null
|
|
2454
2735
|
};
|
|
2455
2736
|
aioShareUrlEarly = null;
|
|
2737
|
+
linkDiagnostics = this.emptyLinkDiagnostics();
|
|
2456
2738
|
collectedRows = [];
|
|
2457
2739
|
partialContext = null;
|
|
2458
2740
|
progressSequence = 0;
|
|
2459
2741
|
clickedQuestions = /* @__PURE__ */ new Set();
|
|
2742
|
+
paaLifecycle = this.emptyPaaLifecycle(0);
|
|
2743
|
+
checkpointMaterial = null;
|
|
2744
|
+
emptyPaaLifecycle(requestedQuestions) {
|
|
2745
|
+
return {
|
|
2746
|
+
requestedQuestions,
|
|
2747
|
+
questionsFound: 0,
|
|
2748
|
+
targetStatus: "not_met",
|
|
2749
|
+
discoveryStatus: "in_progress",
|
|
2750
|
+
materialStatus: "partial",
|
|
2751
|
+
completionReason: "PAA expansion is still running.",
|
|
2752
|
+
automaticRetries: 0,
|
|
2753
|
+
knownQuestions: 0,
|
|
2754
|
+
processedQuestions: 0,
|
|
2755
|
+
failedInteractions: 0,
|
|
2756
|
+
noGrowthConfirmations: 0
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
emptyLinkDiagnostics() {
|
|
2760
|
+
return {
|
|
2761
|
+
linkCandidatesObserved: 0,
|
|
2762
|
+
plainLinksObserved: 0,
|
|
2763
|
+
googleUrlRedirectsObserved: 0,
|
|
2764
|
+
googleGotoRedirectsObserved: 0,
|
|
2765
|
+
redirectsResolved: 0,
|
|
2766
|
+
redirectsUnresolved: 0,
|
|
2767
|
+
redirectsRejected: 0,
|
|
2768
|
+
resolverNonGoogleRequestsObserved: 0,
|
|
2769
|
+
paaSourcesPreserved: 0,
|
|
2770
|
+
aiOverviewLabelValidated: false
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
mergePaaSources(existing = [], incoming = []) {
|
|
2774
|
+
const byRawUrl = /* @__PURE__ */ new Map();
|
|
2775
|
+
for (const source of [...existing, ...incoming]) {
|
|
2776
|
+
const current = byRawUrl.get(source.rawUrl);
|
|
2777
|
+
if (!current) {
|
|
2778
|
+
byRawUrl.set(source.rawUrl, source);
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2781
|
+
const selected = source.resolutionStatus === "resolved" && current.resolutionStatus !== "resolved" ? source : current;
|
|
2782
|
+
byRawUrl.set(source.rawUrl, { ...selected, title: selected.title ?? current.title ?? source.title, site: selected.site ?? current.site ?? source.site });
|
|
2783
|
+
}
|
|
2784
|
+
return [...byRawUrl.values()];
|
|
2785
|
+
}
|
|
2786
|
+
normalizeAICitations(pageUrl, citations) {
|
|
2787
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2788
|
+
return citations.flatMap((citation) => {
|
|
2789
|
+
const link = classifyGoogleOutboundLink(citation.rawHref ?? citation.href, pageUrl);
|
|
2790
|
+
if (!link || seen.has(link.rawUrl)) return [];
|
|
2791
|
+
seen.add(link.rawUrl);
|
|
2792
|
+
const { url, ...metadata } = link;
|
|
2793
|
+
return [{ ...metadata, text: citation.text, href: url }];
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
async resolveMaterialLinks(page, flat, organicResults, aiOverview, aiMode) {
|
|
2797
|
+
const candidates = [
|
|
2798
|
+
...flat.flatMap((row) => row.sources ?? []),
|
|
2799
|
+
...organicResults,
|
|
2800
|
+
...aiOverview.citations.map(({ text: _text, href, ...link }) => ({ ...link, url: href })),
|
|
2801
|
+
...aiMode.citations.map(({ text: _text, href, ...link }) => ({ ...link, url: href }))
|
|
2802
|
+
];
|
|
2803
|
+
const unique = new Map(candidates.map((link) => [link.rawUrl, link]));
|
|
2804
|
+
let resolverNonGoogleRequestsObserved = 0;
|
|
2805
|
+
const resolved = await resolveGoogleOutboundLinks(page, unique.values(), {
|
|
2806
|
+
onNetworkAudit: (audit) => {
|
|
2807
|
+
resolverNonGoogleRequestsObserved = audit.nonGoogleRequestsObserved;
|
|
2808
|
+
}
|
|
2809
|
+
});
|
|
2810
|
+
const replace = (link) => ({ ...link, ...resolved.get(link.rawUrl) ?? link });
|
|
2811
|
+
for (const row of flat) {
|
|
2812
|
+
row.sources = (row.sources ?? []).map(replace);
|
|
2813
|
+
const primary = row.sources[0];
|
|
2814
|
+
row.source_title = primary?.title ?? row.source_title;
|
|
2815
|
+
row.source_site = primary?.site ?? row.source_site;
|
|
2816
|
+
row.source_cite = primary?.url ?? row.source_cite;
|
|
2817
|
+
}
|
|
2818
|
+
for (let index = 0; index < organicResults.length; index++) {
|
|
2819
|
+
const next = replace(organicResults[index]);
|
|
2820
|
+
if (next.resolvedUrl) {
|
|
2821
|
+
try {
|
|
2822
|
+
next.domain = new URL(next.resolvedUrl).hostname.replace(/^www\./, "");
|
|
2823
|
+
} catch {
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
organicResults[index] = next;
|
|
2827
|
+
}
|
|
2828
|
+
const replaceCitation = (citation) => {
|
|
2829
|
+
const link = replace({ url: citation.href, rawUrl: citation.rawUrl, resolvedUrl: citation.resolvedUrl, linkType: citation.linkType, resolutionStatus: citation.resolutionStatus });
|
|
2830
|
+
const { url, ...metadata } = link;
|
|
2831
|
+
return { ...metadata, text: citation.text, href: url };
|
|
2832
|
+
};
|
|
2833
|
+
aiOverview.citations = aiOverview.citations.map(replaceCitation);
|
|
2834
|
+
aiMode.citations = aiMode.citations.map(replaceCitation);
|
|
2835
|
+
const values = [...resolved.values()];
|
|
2836
|
+
this.linkDiagnostics = {
|
|
2837
|
+
linkCandidatesObserved: unique.size,
|
|
2838
|
+
plainLinksObserved: values.filter((link) => link.linkType === "plain").length,
|
|
2839
|
+
googleUrlRedirectsObserved: values.filter((link) => link.linkType === "google_url_redirect").length,
|
|
2840
|
+
googleGotoRedirectsObserved: values.filter((link) => link.linkType === "google_goto_redirect").length,
|
|
2841
|
+
redirectsResolved: values.filter((link) => link.linkType !== "plain" && link.resolutionStatus === "resolved").length,
|
|
2842
|
+
redirectsUnresolved: values.filter((link) => link.resolutionStatus === "unresolved").length,
|
|
2843
|
+
redirectsRejected: values.filter((link) => link.resolutionStatus === "rejected").length,
|
|
2844
|
+
resolverNonGoogleRequestsObserved,
|
|
2845
|
+
paaSourcesPreserved: flat.reduce((total, row) => total + (row.sources ?? []).length, 0),
|
|
2846
|
+
aiOverviewLabelValidated: aiOverview.detected
|
|
2847
|
+
};
|
|
2848
|
+
}
|
|
2460
2849
|
normalizeQuestion(q) {
|
|
2461
2850
|
return q.toLowerCase().replace(/[^\w\s]/g, "").replace(/\s+/g, " ").trim();
|
|
2462
2851
|
}
|
|
@@ -2493,34 +2882,6 @@ var PAAExtractor = class {
|
|
|
2493
2882
|
function cleanSourceLabel(value) {
|
|
2494
2883
|
return (value ?? "").replace(/\s*\(\+\d+\)\s*-\s*View related links.*$/i, "").replace(/\s*-\s*Opens in new tab.*$/i, "").trim();
|
|
2495
2884
|
}
|
|
2496
|
-
function isGoogleHost(host) {
|
|
2497
|
-
const normalized = host.toLowerCase().replace(/\.$/, "");
|
|
2498
|
-
return normalized === "google.com" || normalized.endsWith(".google.com") || /^google\.[a-z.]+$/.test(normalized) || /\.google\.[a-z.]+$/.test(normalized) || normalized === "googleusercontent.com" || normalized.endsWith(".googleusercontent.com");
|
|
2499
|
-
}
|
|
2500
|
-
function safeCitationTarget(rawHref) {
|
|
2501
|
-
let parsed;
|
|
2502
|
-
try {
|
|
2503
|
-
parsed = new URL(rawHref);
|
|
2504
|
-
} catch {
|
|
2505
|
-
return null;
|
|
2506
|
-
}
|
|
2507
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
2508
|
-
if (parsed.username || parsed.password) return null;
|
|
2509
|
-
if (isGoogleHost(parsed.hostname)) {
|
|
2510
|
-
if (parsed.pathname !== "/url") return null;
|
|
2511
|
-
const redirected = parsed.searchParams.get("q") ?? parsed.searchParams.get("url");
|
|
2512
|
-
if (!redirected) return null;
|
|
2513
|
-
try {
|
|
2514
|
-
parsed = new URL(redirected);
|
|
2515
|
-
} catch {
|
|
2516
|
-
return null;
|
|
2517
|
-
}
|
|
2518
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
2519
|
-
if (parsed.username || parsed.password) return null;
|
|
2520
|
-
}
|
|
2521
|
-
if (isGoogleHost(parsed.hostname)) return null;
|
|
2522
|
-
return { href: parsed.href, host: parsed.hostname };
|
|
2523
|
-
}
|
|
2524
2885
|
return Array.from(document.querySelectorAll(selectors.item)).filter((pair) => !onlyQuestion2 || (pair.getAttribute(selectors.itemDataQ) || pair.getAttribute(selectors.itemDataInitQ) || pair.querySelector(selectors.itemQuestionEl)?.innerText?.trim() || "") === onlyQuestion2).map((pair) => {
|
|
2525
2886
|
const clickTarget = pair.querySelector(selectors.clickTarget);
|
|
2526
2887
|
const expanded = pair.classList.contains(selectors.expandedClass) || clickTarget?.getAttribute("aria-expanded") === "true";
|
|
@@ -2528,50 +2889,62 @@ var PAAExtractor = class {
|
|
|
2528
2889
|
const aiAnswerBody = answerContainer?.querySelector(selectors.aiAnswerBody);
|
|
2529
2890
|
const answerElement = aiAnswerBody ?? answerContainer;
|
|
2530
2891
|
const answerText = cleanText(answerElement);
|
|
2531
|
-
const
|
|
2892
|
+
const citationCandidates = [
|
|
2893
|
+
...Array.from((answerElement ?? pair).querySelectorAll("a[href]")),
|
|
2894
|
+
...Array.from(pair.querySelectorAll(selectors.aiCitation)),
|
|
2895
|
+
...Array.from(pair.querySelectorAll(selectors.aiSourceCard))
|
|
2896
|
+
];
|
|
2897
|
+
const hasLoadedAnswer = expanded || answerText.length > 30 && !/an error has occurred/i.test(answerText.slice(0, 80)) && citationCandidates.some((anchor) => /^https?:\/\//.test(anchor.href));
|
|
2532
2898
|
let sourceTitle = hasLoadedAnswer ? pair.querySelector(selectors.sourceTitle)?.innerText?.trim() || void 0 : void 0;
|
|
2533
2899
|
let sourceSite = hasLoadedAnswer ? pair.querySelector(selectors.sourceSite)?.innerText?.trim() || void 0 : void 0;
|
|
2534
|
-
|
|
2535
|
-
if (hasLoadedAnswer
|
|
2900
|
+
const sources = [];
|
|
2901
|
+
if (hasLoadedAnswer) {
|
|
2536
2902
|
const seenHrefs = /* @__PURE__ */ new Set();
|
|
2537
|
-
const
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
...Array.from(anchorRoot.querySelectorAll(selectors.aiSourceCard)),
|
|
2542
|
-
...Array.from(anchorRoot.querySelectorAll("a[href]"))
|
|
2543
|
-
];
|
|
2544
|
-
for (const a of prioritized) {
|
|
2545
|
-
const target = safeCitationTarget(a.href);
|
|
2546
|
-
if (!target || seenHrefs.has(target.href)) continue;
|
|
2547
|
-
seenHrefs.add(target.href);
|
|
2548
|
-
anchors.push({
|
|
2549
|
-
text: (a.textContent || "").trim(),
|
|
2550
|
-
label: cleanSourceLabel(a.getAttribute("aria-label")),
|
|
2551
|
-
href: target.href,
|
|
2552
|
-
host: target.host
|
|
2553
|
-
});
|
|
2554
|
-
}
|
|
2555
|
-
const primary = anchors.find((x) => x.label.length > 2 || x.text.length > 2) ?? anchors[0];
|
|
2556
|
-
if (primary) {
|
|
2557
|
-
sourceTitle = sourceTitle || primary.label || primary.text || void 0;
|
|
2558
|
-
sourceSite = sourceSite || primary.host.replace(/^www\./, "");
|
|
2559
|
-
sourceCite = primary.href;
|
|
2903
|
+
for (const a of citationCandidates) {
|
|
2904
|
+
if (!/^https?:\/\//.test(a.href) || seenHrefs.has(a.href)) continue;
|
|
2905
|
+
seenHrefs.add(a.href);
|
|
2906
|
+
sources.push({ title: cleanSourceLabel(a.getAttribute("aria-label")) || (a.textContent || "").trim(), rawHref: a.href });
|
|
2560
2907
|
}
|
|
2908
|
+
const primary = sources.find((x) => x.title.length > 2) ?? sources[0];
|
|
2909
|
+
if (primary) sourceTitle = sourceTitle || primary.title || void 0;
|
|
2561
2910
|
}
|
|
2562
2911
|
return {
|
|
2912
|
+
googleLinkId: pair.getAttribute("data-lk") || pair.querySelector("[data-lk]")?.getAttribute("data-lk") || null,
|
|
2913
|
+
googleEvidenceId: pair.getAttribute("data-ved") || pair.querySelector("[data-ved]")?.getAttribute("data-ved") || null,
|
|
2563
2914
|
question: pair.getAttribute(selectors.itemDataQ) || pair.getAttribute(selectors.itemDataInitQ) || pair.querySelector(selectors.itemQuestionEl)?.innerText?.trim() || "",
|
|
2564
2915
|
answer: hasLoadedAnswer ? answerText || void 0 : void 0,
|
|
2565
2916
|
sourceTitle,
|
|
2566
2917
|
sourceSite,
|
|
2567
|
-
|
|
2918
|
+
sources
|
|
2568
2919
|
};
|
|
2569
2920
|
});
|
|
2570
2921
|
}, { selectors: sels, onlyQuestion });
|
|
2922
|
+
const baseUrl = typeof page.url === "function" ? page.url() : "https://www.google.com/";
|
|
2571
2923
|
return raw.flatMap((item) => {
|
|
2924
|
+
const sources = [];
|
|
2925
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2926
|
+
for (const candidate of item.sources) {
|
|
2927
|
+
const link = classifyGoogleOutboundLink(candidate.rawHref, baseUrl);
|
|
2928
|
+
if (!link || seen.has(link.rawUrl)) continue;
|
|
2929
|
+
seen.add(link.rawUrl);
|
|
2930
|
+
let site = item.sourceSite ?? null;
|
|
2931
|
+
try {
|
|
2932
|
+
site ||= new URL(link.resolvedUrl ?? link.rawUrl).hostname.replace(/^www\./, "");
|
|
2933
|
+
} catch {
|
|
2934
|
+
}
|
|
2935
|
+
sources.push({ ...link, title: candidate.title || item.sourceTitle || null, site });
|
|
2936
|
+
}
|
|
2937
|
+
const primary = sources[0];
|
|
2572
2938
|
const cleaned = {
|
|
2573
|
-
|
|
2574
|
-
|
|
2939
|
+
questionId: questionIdFor(item.question),
|
|
2940
|
+
googleLinkId: item.googleLinkId,
|
|
2941
|
+
googleEvidenceId: item.googleEvidenceId,
|
|
2942
|
+
question: item.question,
|
|
2943
|
+
answer: cleanPAAAnswerText(item.answer, item.question, item.sourceTitle),
|
|
2944
|
+
sourceTitle: primary?.title ?? item.sourceTitle,
|
|
2945
|
+
sourceSite: primary?.site ?? item.sourceSite,
|
|
2946
|
+
sourceCite: primary?.url,
|
|
2947
|
+
sources
|
|
2575
2948
|
};
|
|
2576
2949
|
const result = RawPAAItemSchema.safeParse(cleaned);
|
|
2577
2950
|
if (!result.success) {
|
|
@@ -2596,11 +2969,15 @@ var PAAExtractor = class {
|
|
|
2596
2969
|
toFlatRow(item, depth, parentQuestion, seed) {
|
|
2597
2970
|
return {
|
|
2598
2971
|
seed_query: seed,
|
|
2972
|
+
question_id: item.questionId ?? questionIdFor(item.question),
|
|
2973
|
+
google_link_id: item.googleLinkId ?? "",
|
|
2974
|
+
google_evidence_id: item.googleEvidenceId ?? "",
|
|
2599
2975
|
question: item.question,
|
|
2600
2976
|
answer: item.answer ?? "",
|
|
2601
2977
|
source_title: item.sourceTitle ?? "",
|
|
2602
2978
|
source_site: item.sourceSite ?? "",
|
|
2603
2979
|
source_cite: item.sourceCite ?? "",
|
|
2980
|
+
sources: item.sources ?? [],
|
|
2604
2981
|
depth,
|
|
2605
2982
|
parent_question: parentQuestion ?? "",
|
|
2606
2983
|
extracted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -2621,20 +2998,23 @@ var PAAExtractor = class {
|
|
|
2621
2998
|
source_title: row.source_title || existing.source_title,
|
|
2622
2999
|
source_site: row.source_site || existing.source_site,
|
|
2623
3000
|
source_cite: row.source_cite || existing.source_cite,
|
|
3001
|
+
sources: this.mergePaaSources(existing.sources, row.sources),
|
|
2624
3002
|
extracted_at: row.extracted_at || existing.extracted_at
|
|
2625
3003
|
};
|
|
2626
|
-
const changed = merged.answer !== existing.answer || merged.source_title !== existing.source_title || merged.source_site !== existing.source_site || merged.source_cite !== existing.source_cite;
|
|
3004
|
+
const changed = merged.answer !== existing.answer || merged.source_title !== existing.source_title || merged.source_site !== existing.source_site || merged.source_cite !== existing.source_cite || JSON.stringify(merged.sources) !== JSON.stringify(existing.sources);
|
|
2627
3005
|
if (changed) this.collectedRows[index] = merged;
|
|
2628
3006
|
return changed;
|
|
2629
3007
|
}
|
|
2630
3008
|
async emitProgress(phase) {
|
|
2631
|
-
if (!this.onPaaProgress || this.collectedRows.length === 0) return;
|
|
3009
|
+
if (!this.onPaaProgress || this.collectedRows.length === 0 && !this.checkpointMaterial) return;
|
|
2632
3010
|
const snapshot = {
|
|
2633
3011
|
sequence: ++this.progressSequence,
|
|
2634
3012
|
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2635
3013
|
clickedQuestions: [...this.clickedQuestions],
|
|
2636
3014
|
phase,
|
|
2637
|
-
records: this.collectedRows.map((row) => ({ ...row }))
|
|
3015
|
+
records: this.collectedRows.map((row) => ({ ...row })),
|
|
3016
|
+
lifecycle: { ...this.paaLifecycle },
|
|
3017
|
+
...this.checkpointMaterial ? { material: structuredClone(this.checkpointMaterial) } : {}
|
|
2638
3018
|
};
|
|
2639
3019
|
try {
|
|
2640
3020
|
await this.onPaaProgress(snapshot);
|
|
@@ -2649,10 +3029,11 @@ var PAAExtractor = class {
|
|
|
2649
3029
|
}
|
|
2650
3030
|
getPartialResult() {
|
|
2651
3031
|
const ctx = this.partialContext;
|
|
2652
|
-
if (!ctx || this.collectedRows.length === 0) return null;
|
|
3032
|
+
if (!ctx || this.collectedRows.length === 0 && !this.checkpointMaterial) return null;
|
|
2653
3033
|
const flat = this.collectedRows.map((row) => ({ ...row }));
|
|
3034
|
+
const material = this.checkpointMaterial;
|
|
2654
3035
|
const paaWithoutAnswer = ctx.questionsOnly ? 0 : flat.filter((row) => !row.answer?.trim()).length;
|
|
2655
|
-
const paaWithoutSource = ctx.questionsOnly ? 0 : flat.filter((row) =>
|
|
3036
|
+
const paaWithoutSource = ctx.questionsOnly ? 0 : flat.filter((row) => (row.sources ?? []).length === 0).length;
|
|
2656
3037
|
const stats = {
|
|
2657
3038
|
seed: ctx.seed,
|
|
2658
3039
|
totalQuestions: flat.length,
|
|
@@ -2670,6 +3051,14 @@ var PAAExtractor = class {
|
|
|
2670
3051
|
resultQuality: "partial",
|
|
2671
3052
|
degradedResult: false,
|
|
2672
3053
|
retryRecommended: true,
|
|
3054
|
+
paaLifecycle: {
|
|
3055
|
+
...this.paaLifecycle,
|
|
3056
|
+
questionsFound: flat.length,
|
|
3057
|
+
targetStatus: flat.length >= (ctx.maxQuestions ?? 0) ? "met" : "not_met",
|
|
3058
|
+
discoveryStatus: "interrupted",
|
|
3059
|
+
materialStatus: paaWithoutAnswer > 0 || paaWithoutSource > 0 ? "partial" : "complete",
|
|
3060
|
+
completionReason: "The browser attempt ended before frontier exhaustion could be proven."
|
|
3061
|
+
},
|
|
2673
3062
|
completeness: {
|
|
2674
3063
|
...this.completeness,
|
|
2675
3064
|
paaWithoutAnswer,
|
|
@@ -2686,17 +3075,17 @@ var PAAExtractor = class {
|
|
|
2686
3075
|
}]
|
|
2687
3076
|
},
|
|
2688
3077
|
totalQuestions: flat.length,
|
|
2689
|
-
surface: "web",
|
|
2690
|
-
aiOverview: { detected: false, text: null, citations: [] },
|
|
2691
|
-
aiMode: { detected: false, text: null, citations: [] },
|
|
2692
|
-
whatPeopleSaying: [],
|
|
3078
|
+
surface: material?.surface ?? "web",
|
|
3079
|
+
aiOverview: material?.aiOverview ?? { detected: false, text: null, citations: [] },
|
|
3080
|
+
aiMode: material?.aiMode ?? { detected: false, text: null, citations: [] },
|
|
3081
|
+
whatPeopleSaying: material?.whatPeopleSaying ?? [],
|
|
2693
3082
|
tree: this.buildTree(flat, ctx.seed),
|
|
2694
3083
|
flat,
|
|
2695
|
-
videos: [],
|
|
2696
|
-
forums: [],
|
|
2697
|
-
organicResults: [],
|
|
2698
|
-
localPack: [],
|
|
2699
|
-
entityIds: { entities: [], kgIds: [], cids: [], gcids: [] },
|
|
3084
|
+
videos: material?.videos ?? [],
|
|
3085
|
+
forums: material?.forums ?? [],
|
|
3086
|
+
organicResults: material?.organicResults ?? [],
|
|
3087
|
+
localPack: material?.localPack ?? [],
|
|
3088
|
+
entityIds: material?.entityIds ?? { entities: [], kgIds: [], cids: [], gcids: [] },
|
|
2700
3089
|
stats
|
|
2701
3090
|
};
|
|
2702
3091
|
}
|
|
@@ -2705,12 +3094,14 @@ var PAAExtractor = class {
|
|
|
2705
3094
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
2706
3095
|
const seenQs = /* @__PURE__ */ new Set();
|
|
2707
3096
|
const orderedQs = [];
|
|
2708
|
-
this.collectedRows = [];
|
|
2709
|
-
this.progressSequence = 0;
|
|
2710
3097
|
this.clickedQuestions = /* @__PURE__ */ new Set();
|
|
2711
3098
|
const abandonedNeverReclick = /* @__PURE__ */ new Set();
|
|
2712
|
-
const
|
|
3099
|
+
const clickAttempts = /* @__PURE__ */ new Map();
|
|
3100
|
+
const processedQuestions = /* @__PURE__ */ new Set();
|
|
3101
|
+
const failedInteractions = /* @__PURE__ */ new Set();
|
|
2713
3102
|
const capturedItems = /* @__PURE__ */ new Map();
|
|
3103
|
+
const MAX_CLICK_ATTEMPTS = 2;
|
|
3104
|
+
const EXHAUSTION_CONFIRMATIONS = 3;
|
|
2714
3105
|
const itemArgs = {
|
|
2715
3106
|
sel: PAASelectors.item,
|
|
2716
3107
|
dataQ: PAASelectors.itemDataQ,
|
|
@@ -2721,15 +3112,15 @@ var PAAExtractor = class {
|
|
|
2721
3112
|
clickTarget: PAASelectors.clickTarget
|
|
2722
3113
|
};
|
|
2723
3114
|
const readItemStates = () => page.evaluate(
|
|
2724
|
-
({ sel, dataQ, dataInitQ, questionEl, expandedClass }) => {
|
|
3115
|
+
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget, answerSel }) => {
|
|
2725
3116
|
return Array.from(document.querySelectorAll(sel)).map((el) => ({
|
|
2726
3117
|
q: el.getAttribute(dataQ) || el.getAttribute(dataInitQ) || el.querySelector(questionEl)?.innerText?.trim() || "",
|
|
2727
|
-
done: el.classList.contains(expandedClass)
|
|
3118
|
+
done: el.classList.contains(expandedClass) || el.querySelector(clickTarget)?.getAttribute("aria-expanded") === "true" || Boolean((el.querySelector(answerSel)?.textContent ?? "").trim())
|
|
2728
3119
|
})).filter((s) => s.q);
|
|
2729
3120
|
},
|
|
2730
3121
|
itemArgs
|
|
2731
3122
|
);
|
|
2732
|
-
const expandOneItemSerially = async (q) => {
|
|
3123
|
+
const expandOneItemSerially = async (q, beforeCount) => {
|
|
2733
3124
|
const clickStartedMs = Date.now();
|
|
2734
3125
|
const items = page.locator(itemArgs.sel);
|
|
2735
3126
|
const itemIndex = await items.evaluateAll(
|
|
@@ -2744,49 +3135,118 @@ var PAAExtractor = class {
|
|
|
2744
3135
|
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
|
|
2745
3136
|
return true;
|
|
2746
3137
|
})().catch(() => false) : false;
|
|
2747
|
-
if (!clicked) return
|
|
3138
|
+
if (!clicked) return { dispatched: false, confirmed: false };
|
|
2748
3139
|
const remainingHumanClickDelayMs = 700 - (Date.now() - clickStartedMs);
|
|
2749
3140
|
if (remainingHumanClickDelayMs > 0) await page.waitForTimeout(remainingHumanClickDelayMs);
|
|
2750
|
-
|
|
3141
|
+
const confirmed = await page.waitForFunction(
|
|
3142
|
+
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget: clickTarget2, answerSel, target, min }) => {
|
|
3143
|
+
const items2 = Array.from(document.querySelectorAll(sel));
|
|
3144
|
+
const item = items2.find((element) => (element.getAttribute(dataQ) || element.getAttribute(dataInitQ) || element.querySelector(questionEl)?.innerText?.trim()) === target);
|
|
3145
|
+
if (!item) return items2.length > min;
|
|
3146
|
+
return item.classList.contains(expandedClass) || item.querySelector(clickTarget2)?.getAttribute("aria-expanded") === "true" || Boolean((item.querySelector(answerSel)?.textContent ?? "").trim()) || items2.length > min;
|
|
3147
|
+
},
|
|
3148
|
+
{ ...itemArgs, target: q, min: beforeCount },
|
|
3149
|
+
{ timeout: 2500, polling: 100 }
|
|
3150
|
+
).catch(() => null);
|
|
3151
|
+
return { dispatched: true, confirmed: Boolean(confirmed) };
|
|
2751
3152
|
};
|
|
2752
3153
|
let round = 0;
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
3154
|
+
expansion: while (true) {
|
|
3155
|
+
if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) {
|
|
3156
|
+
this.paaLifecycle = {
|
|
3157
|
+
...this.paaLifecycle,
|
|
3158
|
+
discoveryStatus: "interrupted",
|
|
3159
|
+
completionReason: "The execution deadline arrived before frontier exhaustion could be proven."
|
|
3160
|
+
};
|
|
3161
|
+
throw new PaaInteractionInterruptedError(this.paaLifecycle.completionReason);
|
|
3162
|
+
}
|
|
2756
3163
|
this.throwIfAborted(signal);
|
|
2757
3164
|
const states = await readItemStates();
|
|
2758
3165
|
let discovered = false;
|
|
2759
|
-
for (const
|
|
2760
|
-
if (!seenQs.has(
|
|
2761
|
-
seenQs.add(
|
|
2762
|
-
orderedQs.push(
|
|
3166
|
+
for (const state of states) {
|
|
3167
|
+
if (!seenQs.has(state.q)) {
|
|
3168
|
+
seenQs.add(state.q);
|
|
3169
|
+
orderedQs.push(state.q);
|
|
2763
3170
|
if (orderedQs.length <= options.maxQuestions) {
|
|
2764
|
-
discovered = this.upsertCollectedRow(this.toFlatRow({ question:
|
|
3171
|
+
discovered = this.upsertCollectedRow(this.toFlatRow({ questionId: questionIdFor(state.q), googleLinkId: null, googleEvidenceId: null, question: state.q, sources: [] }, 1, null, options.query)) || discovered;
|
|
2765
3172
|
}
|
|
2766
3173
|
}
|
|
3174
|
+
if (state.done) processedQuestions.add(state.q);
|
|
2767
3175
|
}
|
|
3176
|
+
this.paaLifecycle = {
|
|
3177
|
+
...this.paaLifecycle,
|
|
3178
|
+
questionsFound: Math.min(orderedQs.length, options.maxQuestions),
|
|
3179
|
+
targetStatus: orderedQs.length >= options.maxQuestions ? "met" : "not_met",
|
|
3180
|
+
knownQuestions: orderedQs.length,
|
|
3181
|
+
processedQuestions: processedQuestions.size,
|
|
3182
|
+
failedInteractions: failedInteractions.size
|
|
3183
|
+
};
|
|
2768
3184
|
if (discovered) await this.emitProgress("discovered");
|
|
2769
|
-
if (options.questionsOnly && seenQs.size >= options.maxQuestions)
|
|
3185
|
+
if (options.questionsOnly && seenQs.size >= options.maxQuestions) {
|
|
3186
|
+
this.paaLifecycle = {
|
|
3187
|
+
...this.paaLifecycle,
|
|
3188
|
+
discoveryStatus: "target_reached",
|
|
3189
|
+
targetStatus: "met",
|
|
3190
|
+
completionReason: "The requested unique-question count was observed."
|
|
3191
|
+
};
|
|
3192
|
+
break;
|
|
3193
|
+
}
|
|
2770
3194
|
const kept = new Set(orderedQs.slice(0, options.maxQuestions));
|
|
2771
|
-
const clickable = states.filter((
|
|
2772
|
-
const target = clickable
|
|
3195
|
+
const clickable = states.filter((state) => kept.has(state.q) && !state.done && (clickAttempts.get(state.q) ?? 0) < MAX_CLICK_ATTEMPTS && !abandonedNeverReclick.has(state.q));
|
|
3196
|
+
const target = clickable[0];
|
|
2773
3197
|
if (!target) {
|
|
2774
|
-
if (seenQs.size >= options.maxQuestions)
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
3198
|
+
if (seenQs.size >= options.maxQuestions) {
|
|
3199
|
+
this.paaLifecycle = {
|
|
3200
|
+
...this.paaLifecycle,
|
|
3201
|
+
discoveryStatus: "target_reached",
|
|
3202
|
+
targetStatus: "met",
|
|
3203
|
+
completionReason: "The requested unique-question count was observed."
|
|
3204
|
+
};
|
|
3205
|
+
break;
|
|
3206
|
+
}
|
|
3207
|
+
const visible = new Set(states.map((state) => state.q));
|
|
3208
|
+
const unresolved2 = orderedQs.slice(0, options.maxQuestions).filter(
|
|
3209
|
+
(question) => !processedQuestions.has(question) && (failedInteractions.has(question) || !visible.has(question))
|
|
3210
|
+
);
|
|
3211
|
+
if (unresolved2.length > 0) {
|
|
3212
|
+
this.paaLifecycle = {
|
|
3213
|
+
...this.paaLifecycle,
|
|
3214
|
+
discoveryStatus: "interrupted",
|
|
3215
|
+
failedInteractions: (/* @__PURE__ */ new Set([...failedInteractions, ...unresolved2])).size,
|
|
3216
|
+
completionReason: "At least one known PAA control failed or disappeared before it could be confirmed."
|
|
3217
|
+
};
|
|
3218
|
+
await this.emitProgress("expansion_finished");
|
|
3219
|
+
throw new PaaInteractionInterruptedError(this.paaLifecycle.completionReason);
|
|
3220
|
+
}
|
|
3221
|
+
for (let confirmation = 1; confirmation <= EXHAUSTION_CONFIRMATIONS; confirmation++) {
|
|
3222
|
+
this.throwIfAborted(signal);
|
|
3223
|
+
await this.throwIfCaptcha(page, "Google PAA exhaustion confirmation");
|
|
3224
|
+
await page.mouse.wheel(0, 320);
|
|
3225
|
+
const grew = await page.waitForFunction(
|
|
3226
|
+
({ sel, min }) => document.querySelectorAll(sel).length > min,
|
|
3227
|
+
{ sel: PAASelectors.item, min: states.length },
|
|
3228
|
+
{ timeout: 1500, polling: 100 }
|
|
3229
|
+
).catch(() => null);
|
|
3230
|
+
this.paaLifecycle = { ...this.paaLifecycle, noGrowthConfirmations: confirmation };
|
|
3231
|
+
if (grew) continue expansion;
|
|
3232
|
+
const confirmationStates = await readItemStates();
|
|
3233
|
+
if (confirmationStates.some((state) => !seenQs.has(state.q))) continue expansion;
|
|
3234
|
+
}
|
|
3235
|
+
this.paaLifecycle = {
|
|
3236
|
+
...this.paaLifecycle,
|
|
3237
|
+
discoveryStatus: "frontier_exhausted",
|
|
3238
|
+
targetStatus: "not_met",
|
|
3239
|
+
failedInteractions: 0,
|
|
3240
|
+
noGrowthConfirmations: EXHAUSTION_CONFIRMATIONS,
|
|
3241
|
+
completionReason: "Every known eligible PAA control was processed and three healthy no-growth confirmations found no additional questions."
|
|
3242
|
+
};
|
|
3243
|
+
break;
|
|
2783
3244
|
}
|
|
2784
|
-
growthWaits = 0;
|
|
2785
3245
|
this.reporter.onDepth(++round);
|
|
2786
3246
|
await this.throwIfCaptcha(page, "Google PAA expansion");
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
3247
|
+
clickAttempts.set(target.q, (clickAttempts.get(target.q) ?? 0) + 1);
|
|
3248
|
+
const expansionResult = await expandOneItemSerially(target.q, states.length);
|
|
3249
|
+
if (expansionResult.dispatched) this.clickedQuestions.add(target.q);
|
|
2790
3250
|
if (!options.questionsOnly) {
|
|
2791
3251
|
let clickedItem = (await this.extractVisibleItems(page, target.q))[0];
|
|
2792
3252
|
if (!clickedItem?.answer) {
|
|
@@ -2800,27 +3260,21 @@ var PAAExtractor = class {
|
|
|
2800
3260
|
}
|
|
2801
3261
|
}
|
|
2802
3262
|
}
|
|
2803
|
-
|
|
3263
|
+
const captured = capturedItems.get(target.q);
|
|
3264
|
+
if (expansionResult.confirmed || Boolean(captured?.answer?.trim())) {
|
|
3265
|
+
processedQuestions.add(target.q);
|
|
3266
|
+
failedInteractions.delete(target.q);
|
|
3267
|
+
} else if ((clickAttempts.get(target.q) ?? 0) >= MAX_CLICK_ATTEMPTS) {
|
|
3268
|
+
failedInteractions.add(target.q);
|
|
2804
3269
|
abandonedNeverReclick.add(target.q);
|
|
2805
3270
|
}
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
for (const state of await readItemStates()) {
|
|
2814
|
-
if (!seenQs.has(state.q)) {
|
|
2815
|
-
seenQs.add(state.q);
|
|
2816
|
-
orderedQs.push(state.q);
|
|
2817
|
-
if (orderedQs.length <= options.maxQuestions) {
|
|
2818
|
-
this.upsertCollectedRow(this.toFlatRow({ question: state.q }, 1, null, options.query));
|
|
2819
|
-
}
|
|
2820
|
-
}
|
|
2821
|
-
}
|
|
2822
|
-
await this.emitProgress("discovered");
|
|
2823
|
-
}
|
|
3271
|
+
this.paaLifecycle = {
|
|
3272
|
+
...this.paaLifecycle,
|
|
3273
|
+
processedQuestions: processedQuestions.size,
|
|
3274
|
+
failedInteractions: failedInteractions.size,
|
|
3275
|
+
noGrowthConfirmations: 0
|
|
3276
|
+
};
|
|
3277
|
+
await this.emitProgress("click_observed");
|
|
2824
3278
|
}
|
|
2825
3279
|
const itemMap = options.questionsOnly ? /* @__PURE__ */ new Map() : new Map(capturedItems);
|
|
2826
3280
|
if (!options.questionsOnly) {
|
|
@@ -2837,24 +3291,34 @@ var PAAExtractor = class {
|
|
|
2837
3291
|
const row = this.toFlatRow(item, 1, null, options.query);
|
|
2838
3292
|
this.upsertCollectedRow(row);
|
|
2839
3293
|
results.push(row);
|
|
2840
|
-
this.reporter.onQuestion({ question: item.question, answer: item.answer ?? null, sourceTitle: item.sourceTitle ?? null, sourceSite: item.sourceSite ?? null, sourceCite: item.sourceCite ?? null, depth: 1, parentQuestion: null, children: [] });
|
|
3294
|
+
this.reporter.onQuestion({ questionId: item.questionId, googleLinkId: item.googleLinkId, googleEvidenceId: item.googleEvidenceId, question: item.question, answer: item.answer ?? null, sourceTitle: item.sourceTitle ?? null, sourceSite: item.sourceSite ?? null, sourceCite: item.sourceCite ?? null, sources: item.sources, depth: 1, parentQuestion: null, children: [] });
|
|
2841
3295
|
} else {
|
|
2842
|
-
const row = this.toFlatRow({ question: q, answer: void 0, sourceTitle: void 0, sourceSite: void 0, sourceCite: void 0 }, 1, null, options.query);
|
|
3296
|
+
const row = this.toFlatRow({ questionId: questionIdFor(q), googleLinkId: null, googleEvidenceId: null, question: q, answer: void 0, sourceTitle: void 0, sourceSite: void 0, sourceCite: void 0, sources: [] }, 1, null, options.query);
|
|
2843
3297
|
this.upsertCollectedRow(row);
|
|
2844
3298
|
results.push(row);
|
|
2845
3299
|
}
|
|
2846
3300
|
}
|
|
2847
3301
|
this.completeness.paaWithoutAnswer = options.questionsOnly ? 0 : results.filter((r) => !r.answer).length;
|
|
2848
|
-
this.completeness.paaWithoutSource = options.questionsOnly ? 0 : results.filter((r) =>
|
|
3302
|
+
this.completeness.paaWithoutSource = options.questionsOnly ? 0 : results.filter((r) => (r.sources ?? []).length === 0).length;
|
|
2849
3303
|
this.completeness.paaRequested = options.maxQuestions;
|
|
2850
3304
|
this.completeness.paaReturned = results.length;
|
|
2851
3305
|
this.completeness.paaUnique = seenKeys.size;
|
|
3306
|
+
const materialPartial = !options.questionsOnly && (this.completeness.paaWithoutAnswer > 0 || this.completeness.paaWithoutSource > 0);
|
|
3307
|
+
this.paaLifecycle = {
|
|
3308
|
+
...this.paaLifecycle,
|
|
3309
|
+
questionsFound: results.length,
|
|
3310
|
+
targetStatus: results.length >= options.maxQuestions ? "met" : "not_met",
|
|
3311
|
+
materialStatus: materialPartial ? "partial" : "complete",
|
|
3312
|
+
processedQuestions: processedQuestions.size,
|
|
3313
|
+
failedInteractions: failedInteractions.size
|
|
3314
|
+
};
|
|
2852
3315
|
await this.emitProgress("fill_incomplete");
|
|
3316
|
+
await this.emitProgress("expansion_finished");
|
|
2853
3317
|
return results;
|
|
2854
3318
|
}
|
|
2855
3319
|
async fillIncompleteItems(page, orderedQs, itemMap, options, abandonedNeverReclick) {
|
|
2856
3320
|
const kept = new Set(orderedQs.slice(0, options.maxQuestions));
|
|
2857
|
-
const isIncomplete = (item) => !item?.answer ||
|
|
3321
|
+
const isIncomplete = (item) => !item?.answer || (item.sources ?? []).length === 0;
|
|
2858
3322
|
const missingBefore = new Set(
|
|
2859
3323
|
[...kept].filter((q) => isIncomplete(itemMap.get(q)))
|
|
2860
3324
|
);
|
|
@@ -2872,7 +3336,8 @@ var PAAExtractor = class {
|
|
|
2872
3336
|
answer: item.answer ?? existing?.answer,
|
|
2873
3337
|
sourceTitle: item.sourceTitle ?? existing?.sourceTitle,
|
|
2874
3338
|
sourceSite: item.sourceSite ?? existing?.sourceSite,
|
|
2875
|
-
sourceCite: item.sourceCite ?? existing?.sourceCite
|
|
3339
|
+
sourceCite: item.sourceCite ?? existing?.sourceCite,
|
|
3340
|
+
sources: this.mergePaaSources(existing?.sources, item.sources)
|
|
2876
3341
|
});
|
|
2877
3342
|
const upgraded = itemMap.get(q);
|
|
2878
3343
|
if (upgraded && this.upsertCollectedRow(this.toFlatRow(upgraded, 1, null, options.query))) {
|
|
@@ -3059,7 +3524,7 @@ var PAAExtractor = class {
|
|
|
3059
3524
|
}
|
|
3060
3525
|
async extractOrganicResults(page) {
|
|
3061
3526
|
const sels = OrganicSelectors;
|
|
3062
|
-
|
|
3527
|
+
const raw = await page.evaluate((s) => {
|
|
3063
3528
|
const out = [];
|
|
3064
3529
|
let pos = 0;
|
|
3065
3530
|
document.querySelectorAll(s.result).forEach((card) => {
|
|
@@ -3085,6 +3550,17 @@ var PAAExtractor = class {
|
|
|
3085
3550
|
});
|
|
3086
3551
|
return out;
|
|
3087
3552
|
}, sels);
|
|
3553
|
+
const baseUrl = typeof page.url === "function" ? page.url() : "https://www.google.com/";
|
|
3554
|
+
return raw.flatMap((item) => {
|
|
3555
|
+
const link = classifyGoogleOutboundLink(item.url, baseUrl);
|
|
3556
|
+
if (!link) return [];
|
|
3557
|
+
let domain = item.domain;
|
|
3558
|
+
try {
|
|
3559
|
+
domain = new URL(link.resolvedUrl ?? link.rawUrl).hostname.replace(/^www\./, "");
|
|
3560
|
+
} catch {
|
|
3561
|
+
}
|
|
3562
|
+
return [{ ...item, ...link, domain }];
|
|
3563
|
+
});
|
|
3088
3564
|
}
|
|
3089
3565
|
async extractLocalPack(page) {
|
|
3090
3566
|
const sels = LocalPackSelectors;
|
|
@@ -3236,15 +3712,21 @@ var PAAExtractor = class {
|
|
|
3236
3712
|
aim: AIModeSelectors,
|
|
3237
3713
|
expandWaitMs: 1500
|
|
3238
3714
|
});
|
|
3239
|
-
|
|
3715
|
+
const pageUrl = typeof page.url === "function" ? page.url() : "https://www.google.com/";
|
|
3716
|
+
const normalized = {
|
|
3717
|
+
surface: surfaces.surface,
|
|
3718
|
+
aiOverview: { ...surfaces.aiOverview, citations: this.normalizeAICitations(pageUrl, surfaces.aiOverview.citations) },
|
|
3719
|
+
aiMode: { ...surfaces.aiMode, citations: this.normalizeAICitations(pageUrl, surfaces.aiMode.citations) }
|
|
3720
|
+
};
|
|
3721
|
+
if (!normalized.aiOverview.detected) {
|
|
3240
3722
|
this.completeness.aioShareCaptured = null;
|
|
3241
|
-
return
|
|
3723
|
+
return normalized;
|
|
3242
3724
|
}
|
|
3243
3725
|
const shareUrl = this.aioShareUrlEarly ?? await this.captureAIOverviewShareUrl(page, options).catch(() => null);
|
|
3244
3726
|
this.completeness.aioShareCaptured = shareUrl !== null;
|
|
3245
3727
|
return {
|
|
3246
|
-
...
|
|
3247
|
-
aiOverview: { ...
|
|
3728
|
+
...normalized,
|
|
3729
|
+
aiOverview: { ...normalized.aiOverview, shareUrl }
|
|
3248
3730
|
};
|
|
3249
3731
|
}
|
|
3250
3732
|
async captureAIOverviewShareUrl(page, options) {
|
|
@@ -3289,11 +3771,15 @@ var PAAExtractor = class {
|
|
|
3289
3771
|
const nodeMap = /* @__PURE__ */ new Map();
|
|
3290
3772
|
for (const row of flat) {
|
|
3291
3773
|
const node = {
|
|
3774
|
+
questionId: row.question_id,
|
|
3775
|
+
googleLinkId: row.google_link_id || null,
|
|
3776
|
+
googleEvidenceId: row.google_evidence_id || null,
|
|
3292
3777
|
question: row.question,
|
|
3293
3778
|
answer: row.answer || null,
|
|
3294
3779
|
sourceTitle: row.source_title || null,
|
|
3295
3780
|
sourceSite: row.source_site || null,
|
|
3296
3781
|
sourceCite: row.source_cite || null,
|
|
3782
|
+
sources: row.sources ?? [],
|
|
3297
3783
|
depth: row.depth,
|
|
3298
3784
|
parentQuestion: row.parent_question || null,
|
|
3299
3785
|
children: []
|
|
@@ -3340,10 +3826,13 @@ var PAAExtractor = class {
|
|
|
3340
3826
|
async extract(options, signal) {
|
|
3341
3827
|
const startMs = Date.now();
|
|
3342
3828
|
this.completeness = { paaWithoutAnswer: 0, paaWithoutSource: 0, paaAnswersRecovered: 0, aioShareCaptured: null };
|
|
3829
|
+
this.linkDiagnostics = this.emptyLinkDiagnostics();
|
|
3343
3830
|
this.aioShareUrlEarly = null;
|
|
3344
3831
|
this.collectedRows = [];
|
|
3345
3832
|
this.progressSequence = 0;
|
|
3833
|
+
this.checkpointMaterial = null;
|
|
3346
3834
|
this.clickedQuestions = /* @__PURE__ */ new Set();
|
|
3835
|
+
this.paaLifecycle = this.emptyPaaLifecycle(options.maxQuestions);
|
|
3347
3836
|
this.partialContext = {
|
|
3348
3837
|
seed: options.query,
|
|
3349
3838
|
location: options.location ?? null,
|
|
@@ -3406,8 +3895,17 @@ var PAAExtractor = class {
|
|
|
3406
3895
|
this.throwIfAborted(signal);
|
|
3407
3896
|
const page = this.driver.getPage();
|
|
3408
3897
|
await this.throwIfCaptcha(page, "Google SERP");
|
|
3898
|
+
if (!hasPaa) {
|
|
3899
|
+
this.paaLifecycle = {
|
|
3900
|
+
...this.paaLifecycle,
|
|
3901
|
+
discoveryStatus: "no_paa_observed",
|
|
3902
|
+
materialStatus: "complete",
|
|
3903
|
+
completionReason: "A readable Google SERP was observed without a People Also Ask block."
|
|
3904
|
+
};
|
|
3905
|
+
}
|
|
3409
3906
|
if (options.questionsOnly) {
|
|
3410
3907
|
const flat2 = hasPaa ? await this.runBFS(page, executionOptions, signal) : [];
|
|
3908
|
+
await this.resolveMaterialLinks(page, flat2, [], emptyAiSurfaces.aiOverview, emptyAiSurfaces.aiMode);
|
|
3411
3909
|
const stats2 = {
|
|
3412
3910
|
seed: executionOptions.query,
|
|
3413
3911
|
totalQuestions: flat2.length,
|
|
@@ -3424,7 +3922,9 @@ var PAAExtractor = class {
|
|
|
3424
3922
|
completionStatus: hasPaa ? "paa_found" : "no_paa",
|
|
3425
3923
|
...!hasPaa ? { noPaaObserved: true } : {},
|
|
3426
3924
|
problem: null,
|
|
3925
|
+
paaLifecycle: { ...this.paaLifecycle },
|
|
3427
3926
|
completeness: { ...this.completeness },
|
|
3927
|
+
links: { ...this.linkDiagnostics },
|
|
3428
3928
|
...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, void 0, locationResolution) } : {}
|
|
3429
3929
|
},
|
|
3430
3930
|
totalQuestions: flat2.length,
|
|
@@ -3467,6 +3967,7 @@ var PAAExtractor = class {
|
|
|
3467
3967
|
locationEvidence2 = inferSerpLocationEvidence(canonicalLocation, allOrganic2, localPack2);
|
|
3468
3968
|
}
|
|
3469
3969
|
}
|
|
3970
|
+
await this.resolveMaterialLinks(page, [], allOrganic2, aiSurfaces2.aiOverview, aiSurfaces2.aiMode);
|
|
3470
3971
|
const stats2 = {
|
|
3471
3972
|
seed: executionOptions.query,
|
|
3472
3973
|
totalQuestions: 0,
|
|
@@ -3483,6 +3984,7 @@ var PAAExtractor = class {
|
|
|
3483
3984
|
completionStatus: "serp_only",
|
|
3484
3985
|
problem: null,
|
|
3485
3986
|
completeness: { ...this.completeness },
|
|
3987
|
+
links: { ...this.linkDiagnostics },
|
|
3486
3988
|
...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence2, locationResolution) } : {}
|
|
3487
3989
|
},
|
|
3488
3990
|
totalQuestions: 0,
|
|
@@ -3528,6 +4030,7 @@ var PAAExtractor = class {
|
|
|
3528
4030
|
}
|
|
3529
4031
|
}
|
|
3530
4032
|
const aiSurfaces2 = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
|
|
4033
|
+
await this.resolveMaterialLinks(page, [], noPaaOrganic, aiSurfaces2.aiOverview, aiSurfaces2.aiMode);
|
|
3531
4034
|
const stats2 = {
|
|
3532
4035
|
seed: executionOptions.query,
|
|
3533
4036
|
totalQuestions: 0,
|
|
@@ -3544,7 +4047,9 @@ var PAAExtractor = class {
|
|
|
3544
4047
|
completionStatus: "no_paa",
|
|
3545
4048
|
noPaaObserved: true,
|
|
3546
4049
|
problem: null,
|
|
4050
|
+
paaLifecycle: { ...this.paaLifecycle },
|
|
3547
4051
|
completeness: { ...this.completeness },
|
|
4052
|
+
links: { ...this.linkDiagnostics },
|
|
3548
4053
|
...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence2, locationResolution) } : {}
|
|
3549
4054
|
},
|
|
3550
4055
|
totalQuestions: 0,
|
|
@@ -3562,9 +4067,21 @@ var PAAExtractor = class {
|
|
|
3562
4067
|
stats: stats2
|
|
3563
4068
|
};
|
|
3564
4069
|
}
|
|
4070
|
+
const aiSurfaces = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
|
|
4071
|
+
this.checkpointMaterial = {
|
|
4072
|
+
surface: aiSurfaces.surface,
|
|
4073
|
+
aiOverview: aiSurfaces.aiOverview,
|
|
4074
|
+
aiMode: aiSurfaces.aiMode,
|
|
4075
|
+
whatPeopleSaying,
|
|
4076
|
+
videos,
|
|
4077
|
+
forums,
|
|
4078
|
+
organicResults,
|
|
4079
|
+
localPack,
|
|
4080
|
+
entityIds
|
|
4081
|
+
};
|
|
4082
|
+
await this.emitProgress("serp_captured");
|
|
3565
4083
|
const flat = await this.runBFS(page, executionOptions, signal);
|
|
3566
4084
|
this.throwIfAborted(signal);
|
|
3567
|
-
const aiSurfaces = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
|
|
3568
4085
|
const shortVidsParams = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", udm: ShortVideoSelectors.udm });
|
|
3569
4086
|
if (uule) shortVidsParams.set("uule", uule);
|
|
3570
4087
|
let shortVideos = [];
|
|
@@ -3597,6 +4114,15 @@ var PAAExtractor = class {
|
|
|
3597
4114
|
locationEvidence = inferSerpLocationEvidence(canonicalLocation, allOrganic, localPack);
|
|
3598
4115
|
}
|
|
3599
4116
|
}
|
|
4117
|
+
await this.resolveMaterialLinks(page, flat, allOrganic, aiSurfaces.aiOverview, aiSurfaces.aiMode);
|
|
4118
|
+
this.checkpointMaterial = {
|
|
4119
|
+
...this.checkpointMaterial,
|
|
4120
|
+
surface: aiSurfaces.surface,
|
|
4121
|
+
aiOverview: aiSurfaces.aiOverview,
|
|
4122
|
+
aiMode: aiSurfaces.aiMode,
|
|
4123
|
+
videos: [...videos, ...shortVideos],
|
|
4124
|
+
organicResults: allOrganic
|
|
4125
|
+
};
|
|
3600
4126
|
const allVideos = [...videos, ...shortVideos];
|
|
3601
4127
|
const tree = this.buildTree(flat, executionOptions.query);
|
|
3602
4128
|
const stats = {
|
|
@@ -3614,7 +4140,9 @@ var PAAExtractor = class {
|
|
|
3614
4140
|
diagnostics: {
|
|
3615
4141
|
completionStatus: "paa_found",
|
|
3616
4142
|
problem: null,
|
|
4143
|
+
paaLifecycle: { ...this.paaLifecycle },
|
|
3617
4144
|
completeness: { ...this.completeness },
|
|
4145
|
+
links: { ...this.linkDiagnostics },
|
|
3618
4146
|
...diagnosticWarnings.length > 0 ? { warnings: diagnosticWarnings } : {},
|
|
3619
4147
|
...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence, locationResolution) } : {}
|
|
3620
4148
|
},
|
|
@@ -3634,6 +4162,13 @@ var PAAExtractor = class {
|
|
|
3634
4162
|
};
|
|
3635
4163
|
} catch (err) {
|
|
3636
4164
|
errorCount++;
|
|
4165
|
+
if (this.paaLifecycle.discoveryStatus === "in_progress") {
|
|
4166
|
+
this.paaLifecycle = {
|
|
4167
|
+
...this.paaLifecycle,
|
|
4168
|
+
discoveryStatus: "interrupted",
|
|
4169
|
+
completionReason: "The browser attempt ended before frontier exhaustion could be proven."
|
|
4170
|
+
};
|
|
4171
|
+
}
|
|
3637
4172
|
await this.emitProgress("attempt_ending").catch(() => {
|
|
3638
4173
|
});
|
|
3639
4174
|
this.reporter.onError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -3682,7 +4217,10 @@ var OutputSerializer = class {
|
|
|
3682
4217
|
await import_node_fs.promises.mkdir(outputDir, { recursive: true });
|
|
3683
4218
|
const seedRaw = rows[0]?.seed_query ?? "paa";
|
|
3684
4219
|
const slug = seedRaw.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
3685
|
-
const csv = import_papaparse.default.unparse(rows
|
|
4220
|
+
const csv = import_papaparse.default.unparse(rows.map(({ sources, ...row }) => ({
|
|
4221
|
+
...row,
|
|
4222
|
+
sources_json: JSON.stringify(sources)
|
|
4223
|
+
})), { header: true });
|
|
3686
4224
|
const filename = `${slug}-${Date.now()}.csv`;
|
|
3687
4225
|
const fullPath = import_node_path.default.join(outputDir, filename);
|
|
3688
4226
|
await import_node_fs.promises.writeFile(fullPath, csv, "utf8");
|
|
@@ -3778,7 +4316,7 @@ var ProgressReporter = class {
|
|
|
3778
4316
|
var import_sdk3 = __toESM(require("@onkernel/sdk"), 1);
|
|
3779
4317
|
|
|
3780
4318
|
// src/backup-proxy.ts
|
|
3781
|
-
var
|
|
4319
|
+
var import_node_crypto4 = require("crypto");
|
|
3782
4320
|
var import_sdk2 = __toESM(require("@onkernel/sdk"), 1);
|
|
3783
4321
|
var BACKUP_PROXY_HOST = "pr.oxylabs.io";
|
|
3784
4322
|
var BACKUP_PROXY_PORT = 7777;
|
|
@@ -3800,7 +4338,7 @@ function backupProxyAvailable() {
|
|
|
3800
4338
|
}
|
|
3801
4339
|
function randomStickySessionId() {
|
|
3802
4340
|
let digits = "";
|
|
3803
|
-
for (let i = 0; i < 10; i++) digits += String((0,
|
|
4341
|
+
for (let i = 0; i < 10; i++) digits += String((0, import_node_crypto4.randomInt)(0, 10));
|
|
3804
4342
|
return digits;
|
|
3805
4343
|
}
|
|
3806
4344
|
function buildBackupUsername(opts = {}) {
|
|
@@ -4336,6 +4874,7 @@ function classifyAttemptError(err) {
|
|
|
4336
4874
|
if (err instanceof CaptchaError) return "captcha";
|
|
4337
4875
|
if (err instanceof LocationMismatchError) return "location_mismatch";
|
|
4338
4876
|
if (err instanceof RequestAbortedError) return "request_aborted";
|
|
4877
|
+
if (err instanceof PaaInteractionInterruptedError) return "browser_session_interrupted";
|
|
4339
4878
|
if (err instanceof DOMException && (err.name === "TimeoutError" || err.name === "AbortError")) return "timeout";
|
|
4340
4879
|
const message = err instanceof Error ? err.message : String(err);
|
|
4341
4880
|
if (/browser (?:has been )?closed|context (?:has been )?closed|target page, context or browser has been closed|session closed|page has been closed/i.test(message)) {
|
|
@@ -4360,7 +4899,9 @@ function withResultQuality(result, requestedPaaCount, questionsOnly) {
|
|
|
4360
4899
|
const paaWithoutSource = questionsOnly ? 0 : result.flat.filter((row) => !row.source_cite?.trim()).length;
|
|
4361
4900
|
const noPaa = result.diagnostics.completionStatus === "no_paa" && (result.diagnostics.noPaaObserved === true || hasUsableContent);
|
|
4362
4901
|
const hasPaa = result.diagnostics.completionStatus === "paa_found";
|
|
4363
|
-
const
|
|
4902
|
+
const discoveryStatus = result.diagnostics.paaLifecycle?.discoveryStatus;
|
|
4903
|
+
const discoveryIncomplete = discoveryStatus === "interrupted" || discoveryStatus === "recovery_exhausted";
|
|
4904
|
+
const paaIncomplete = hasPaa && (uniquePaaCount < requestedPaaCount && discoveryStatus !== "frontier_exhausted" || paaWithoutAnswer > 0 || paaWithoutSource > 0 || Boolean(result.diagnostics.interruption) || discoveryIncomplete);
|
|
4364
4905
|
const resultQuality = noPaa ? "complete" : hasPaa && uniquePaaCount > 0 ? paaIncomplete || hasWarnings ? "partial" : "complete" : hasUsableContent ? hasWarnings ? "partial" : "complete" : "degraded";
|
|
4365
4906
|
const degradedResult = resultQuality === "degraded";
|
|
4366
4907
|
result.diagnostics = {
|
|
@@ -4377,7 +4918,15 @@ function withResultQuality(result, requestedPaaCount, questionsOnly) {
|
|
|
4377
4918
|
paaRequested: requestedPaaCount,
|
|
4378
4919
|
paaReturned: result.flat.length,
|
|
4379
4920
|
paaUnique: uniquePaaCount
|
|
4380
|
-
}
|
|
4921
|
+
},
|
|
4922
|
+
...result.diagnostics.paaLifecycle ? {
|
|
4923
|
+
paaLifecycle: {
|
|
4924
|
+
...result.diagnostics.paaLifecycle,
|
|
4925
|
+
questionsFound: uniquePaaCount,
|
|
4926
|
+
targetStatus: uniquePaaCount >= requestedPaaCount ? "met" : "not_met",
|
|
4927
|
+
materialStatus: paaWithoutAnswer > 0 || paaWithoutSource > 0 ? "partial" : "complete"
|
|
4928
|
+
}
|
|
4929
|
+
} : {}
|
|
4381
4930
|
};
|
|
4382
4931
|
return result;
|
|
4383
4932
|
}
|