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
|
@@ -8,22 +8,23 @@ import {
|
|
|
8
8
|
currentCostContext,
|
|
9
9
|
recordKernelSession,
|
|
10
10
|
runWithCostContext
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-NGLZ6UJR.js";
|
|
12
12
|
import {
|
|
13
13
|
DEFAULT_PROXY_MODE,
|
|
14
14
|
HarvestOptionsSchema,
|
|
15
15
|
RawPAAItemSchema,
|
|
16
16
|
postToMemoryLibrary,
|
|
17
17
|
recencyToTbs
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-OWF2JJKN.js";
|
|
19
19
|
import {
|
|
20
20
|
CaptchaError,
|
|
21
21
|
ExtractionError,
|
|
22
22
|
LocationMismatchError,
|
|
23
|
+
PaaInteractionInterruptedError,
|
|
23
24
|
RECAPTCHA_INSTRUCTIONS,
|
|
24
25
|
RequestAbortedError,
|
|
25
26
|
sanitizeVendorName
|
|
26
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-OU4FV2RP.js";
|
|
27
28
|
|
|
28
29
|
// src/driver/BrowserDriver.ts
|
|
29
30
|
import { chromium } from "playwright-extra";
|
|
@@ -1724,16 +1725,15 @@ async function extractAISurfacesFromDocument(config) {
|
|
|
1724
1725
|
if (textOf(heading) === "AI Overview") return true;
|
|
1725
1726
|
const header = el.querySelector(selectors.aio.header);
|
|
1726
1727
|
if (textOf(header).split(/\n|\s{2,}/).some((part) => part.trim() === "AI Overview")) return true;
|
|
1727
|
-
return
|
|
1728
|
+
return false;
|
|
1728
1729
|
}
|
|
1729
1730
|
function findAIORoot() {
|
|
1730
1731
|
const primaryRoots = Array.from(document.querySelectorAll(selectors.aio.root));
|
|
1731
1732
|
const labeledPrimary = primaryRoots.find(hasAIOverviewLabel);
|
|
1732
1733
|
if (labeledPrimary) return labeledPrimary;
|
|
1733
|
-
if (primaryRoots.length > 0) return primaryRoots[0];
|
|
1734
1734
|
if (selectors.aio.legacyRoot) {
|
|
1735
1735
|
const legacy = document.querySelector(selectors.aio.legacyRoot);
|
|
1736
|
-
if (legacy) return legacy;
|
|
1736
|
+
if (legacy && hasAIOverviewLabel(legacy)) return legacy;
|
|
1737
1737
|
}
|
|
1738
1738
|
const headings = document.querySelectorAll(`${selectors.aio.heading}, h1, h2, h3, [role="heading"]`);
|
|
1739
1739
|
for (const h of headings) {
|
|
@@ -1812,26 +1812,12 @@ async function extractAISurfacesFromDocument(config) {
|
|
|
1812
1812
|
if (!raw || /not available|try again|can't generate/i.test(raw)) return null;
|
|
1813
1813
|
return raw;
|
|
1814
1814
|
}
|
|
1815
|
-
function
|
|
1815
|
+
function absoluteHref(rawHref) {
|
|
1816
1816
|
if (!rawHref || rawHref.startsWith("javascript:")) return null;
|
|
1817
|
-
let href = rawHref;
|
|
1818
1817
|
try {
|
|
1819
1818
|
const absolute = new URL(rawHref, window.location.href);
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
href = q;
|
|
1823
|
-
} else {
|
|
1824
|
-
href = absolute.href;
|
|
1825
|
-
}
|
|
1826
|
-
} catch {
|
|
1827
|
-
return null;
|
|
1828
|
-
}
|
|
1829
|
-
if (!/^https?:\/\//i.test(href)) return null;
|
|
1830
|
-
try {
|
|
1831
|
-
const url = new URL(href);
|
|
1832
|
-
const isGoogleInternal = /(\.|^)google\./i.test(url.hostname);
|
|
1833
|
-
if (isGoogleInternal) return null;
|
|
1834
|
-
return url.href;
|
|
1819
|
+
if (!["http:", "https:"].includes(absolute.protocol) || absolute.username || absolute.password) return null;
|
|
1820
|
+
return { href: absolute.href, rawHref: absolute.href };
|
|
1835
1821
|
} catch {
|
|
1836
1822
|
return null;
|
|
1837
1823
|
}
|
|
@@ -1841,17 +1827,18 @@ async function extractAISurfacesFromDocument(config) {
|
|
|
1841
1827
|
const seen = /* @__PURE__ */ new Set();
|
|
1842
1828
|
const citations = [];
|
|
1843
1829
|
for (const a of Array.from(root.querySelectorAll("a[href]"))) {
|
|
1844
|
-
const
|
|
1845
|
-
if (!
|
|
1846
|
-
seen.add(
|
|
1830
|
+
const link = absoluteHref(a.getAttribute("href") ?? "");
|
|
1831
|
+
if (!link || seen.has(link.rawHref)) continue;
|
|
1832
|
+
seen.add(link.rawHref);
|
|
1847
1833
|
let fallbackHost = "";
|
|
1848
1834
|
try {
|
|
1849
|
-
fallbackHost = new URL(href).hostname.replace(/^www\./, "");
|
|
1835
|
+
fallbackHost = new URL(link.href).hostname.replace(/^www\./, "");
|
|
1850
1836
|
} catch {
|
|
1851
1837
|
}
|
|
1852
1838
|
citations.push({
|
|
1853
|
-
text: textOf(a) || fallbackHost || href,
|
|
1854
|
-
href
|
|
1839
|
+
text: textOf(a) || fallbackHost || link.href,
|
|
1840
|
+
href: link.href,
|
|
1841
|
+
rawHref: link.rawHref
|
|
1855
1842
|
});
|
|
1856
1843
|
}
|
|
1857
1844
|
return citations;
|
|
@@ -1906,6 +1893,286 @@ async function extractAISurfacesFromDocument(config) {
|
|
|
1906
1893
|
};
|
|
1907
1894
|
}
|
|
1908
1895
|
|
|
1896
|
+
// src/extractor/google-outbound-links.ts
|
|
1897
|
+
import { createHash } from "crypto";
|
|
1898
|
+
var MAX_LINK_LENGTH = 8e3;
|
|
1899
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
1900
|
+
var DEFAULT_TOTAL_BUDGET_MS = 1e4;
|
|
1901
|
+
var DEFAULT_HTTP_CONCURRENCY = 8;
|
|
1902
|
+
function isGoogleHost(hostname) {
|
|
1903
|
+
return /(^|\.)google\.(?:com|[a-z]{2,3}(?:\.[a-z]{2})?)$/i.test(hostname);
|
|
1904
|
+
}
|
|
1905
|
+
function safeHttpUrl(value) {
|
|
1906
|
+
if (!value || value.length > MAX_LINK_LENGTH) return null;
|
|
1907
|
+
try {
|
|
1908
|
+
const url = new URL(value);
|
|
1909
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
1910
|
+
if (url.username || url.password) return null;
|
|
1911
|
+
return url;
|
|
1912
|
+
} catch {
|
|
1913
|
+
return null;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
function absoluteHttpUrl(value, baseUrl) {
|
|
1917
|
+
if (!value || value.length > MAX_LINK_LENGTH || /^javascript:/i.test(value)) return null;
|
|
1918
|
+
try {
|
|
1919
|
+
const absolute = new URL(value, baseUrl);
|
|
1920
|
+
return safeHttpUrl(absolute.href);
|
|
1921
|
+
} catch {
|
|
1922
|
+
return null;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
function parsedGoogleGoto(value) {
|
|
1926
|
+
const url = safeHttpUrl(value);
|
|
1927
|
+
if (!url || url.protocol !== "https:" || !isGoogleHost(url.hostname) || url.pathname !== "/goto") return null;
|
|
1928
|
+
return url.searchParams.get("url") ? url : null;
|
|
1929
|
+
}
|
|
1930
|
+
function questionIdFor(question) {
|
|
1931
|
+
const normalized = question.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^\p{L}\p{N}\s]/gu, "").replace(/\s+/g, " ").trim();
|
|
1932
|
+
return `paa_${createHash("sha256").update(normalized).digest("hex").slice(0, 16)}`;
|
|
1933
|
+
}
|
|
1934
|
+
function classifyGoogleOutboundLink(rawHref, baseUrl) {
|
|
1935
|
+
const absolute = absoluteHttpUrl(rawHref, baseUrl);
|
|
1936
|
+
if (!absolute) return null;
|
|
1937
|
+
const rawUrl = absolute.href;
|
|
1938
|
+
if (!isGoogleHost(absolute.hostname)) {
|
|
1939
|
+
return {
|
|
1940
|
+
url: rawUrl,
|
|
1941
|
+
rawUrl,
|
|
1942
|
+
resolvedUrl: rawUrl,
|
|
1943
|
+
linkType: "plain",
|
|
1944
|
+
resolutionStatus: "not_needed"
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
if (absolute.pathname === "/url") {
|
|
1948
|
+
const target = safeHttpUrl(absolute.searchParams.get("q") ?? absolute.searchParams.get("url") ?? "");
|
|
1949
|
+
if (!target || isGoogleHost(target.hostname)) return null;
|
|
1950
|
+
return {
|
|
1951
|
+
url: target.href,
|
|
1952
|
+
rawUrl,
|
|
1953
|
+
resolvedUrl: target.href,
|
|
1954
|
+
linkType: "google_url_redirect",
|
|
1955
|
+
resolutionStatus: "resolved"
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
if (absolute.pathname === "/goto" && absolute.searchParams.get("url")) {
|
|
1959
|
+
return {
|
|
1960
|
+
url: rawUrl,
|
|
1961
|
+
rawUrl,
|
|
1962
|
+
resolvedUrl: null,
|
|
1963
|
+
linkType: "google_goto_redirect",
|
|
1964
|
+
resolutionStatus: "unresolved"
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
return null;
|
|
1968
|
+
}
|
|
1969
|
+
function tokenFor(value) {
|
|
1970
|
+
return parsedGoogleGoto(value)?.searchParams.get("url") ?? null;
|
|
1971
|
+
}
|
|
1972
|
+
function externalRedirectTarget(source, location) {
|
|
1973
|
+
try {
|
|
1974
|
+
const sourceUrl = new URL(source);
|
|
1975
|
+
const target = safeHttpUrl(new URL(location, sourceUrl).href);
|
|
1976
|
+
if (!target || isGoogleHost(target.hostname)) return null;
|
|
1977
|
+
if (!target.hash && sourceUrl.hash) target.hash = sourceUrl.hash;
|
|
1978
|
+
return target.href;
|
|
1979
|
+
} catch {
|
|
1980
|
+
return null;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
async function resolveGoogleGotoUrls(rawUrls, options = {}) {
|
|
1984
|
+
const inputs = [...new Set([...rawUrls].flatMap((value) => parsedGoogleGoto(value) ? [value] : []))];
|
|
1985
|
+
const records = new Map(inputs.map((rawUrl) => [rawUrl, {
|
|
1986
|
+
rawUrl,
|
|
1987
|
+
resolvedUrl: null,
|
|
1988
|
+
resolutionStatus: "unresolved"
|
|
1989
|
+
}]));
|
|
1990
|
+
const fallbackFetch = typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : null;
|
|
1991
|
+
const fetchImpl = options.fetchImpl ?? fallbackFetch;
|
|
1992
|
+
if (inputs.length === 0 || !fetchImpl) {
|
|
1993
|
+
return { resolutions: [...records.values()], nonGoogleRequestsObserved: 0 };
|
|
1994
|
+
}
|
|
1995
|
+
const timeoutMs = Math.max(1, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
1996
|
+
const deadlineMs = Date.now() + Math.max(1, options.totalBudgetMs ?? DEFAULT_TOTAL_BUDGET_MS);
|
|
1997
|
+
const concurrency = Math.max(1, Math.min(16, Math.floor(options.concurrency ?? DEFAULT_HTTP_CONCURRENCY)));
|
|
1998
|
+
let cursor = 0;
|
|
1999
|
+
const worker = async () => {
|
|
2000
|
+
while (cursor < inputs.length) {
|
|
2001
|
+
const rawUrl = inputs[cursor++];
|
|
2002
|
+
const source = parsedGoogleGoto(rawUrl);
|
|
2003
|
+
const expectedToken = source?.searchParams.get("url");
|
|
2004
|
+
if (!source || !expectedToken) {
|
|
2005
|
+
records.set(rawUrl, { rawUrl, resolvedUrl: null, resolutionStatus: "rejected" });
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
source.hash = "";
|
|
2009
|
+
let currentUrl = source.href;
|
|
2010
|
+
for (let hop = 0; hop < 2; hop++) {
|
|
2011
|
+
const remainingMs = deadlineMs - Date.now();
|
|
2012
|
+
if (remainingMs <= 0) return;
|
|
2013
|
+
const controller = new AbortController();
|
|
2014
|
+
const timer = setTimeout(() => controller.abort(), Math.min(timeoutMs, remainingMs));
|
|
2015
|
+
let response;
|
|
2016
|
+
try {
|
|
2017
|
+
response = await fetchImpl(currentUrl, {
|
|
2018
|
+
method: "GET",
|
|
2019
|
+
redirect: "manual",
|
|
2020
|
+
headers: {
|
|
2021
|
+
accept: "*/*",
|
|
2022
|
+
"user-agent": "Mozilla/5.0"
|
|
2023
|
+
},
|
|
2024
|
+
signal: controller.signal
|
|
2025
|
+
});
|
|
2026
|
+
} catch {
|
|
2027
|
+
break;
|
|
2028
|
+
} finally {
|
|
2029
|
+
clearTimeout(timer);
|
|
2030
|
+
}
|
|
2031
|
+
if (response.status < 300 || response.status >= 400) break;
|
|
2032
|
+
const location = response.headers.get("location");
|
|
2033
|
+
if (!location) break;
|
|
2034
|
+
const destination = externalRedirectTarget(rawUrl, location);
|
|
2035
|
+
if (destination) {
|
|
2036
|
+
records.set(rawUrl, { rawUrl, resolvedUrl: destination, resolutionStatus: "resolved" });
|
|
2037
|
+
break;
|
|
2038
|
+
}
|
|
2039
|
+
let next;
|
|
2040
|
+
try {
|
|
2041
|
+
next = new URL(location, currentUrl);
|
|
2042
|
+
} catch {
|
|
2043
|
+
records.set(rawUrl, { rawUrl, resolvedUrl: null, resolutionStatus: "rejected" });
|
|
2044
|
+
break;
|
|
2045
|
+
}
|
|
2046
|
+
if (!isGoogleHost(next.hostname) || tokenFor(next.href) !== expectedToken) {
|
|
2047
|
+
records.set(rawUrl, { rawUrl, resolvedUrl: null, resolutionStatus: "rejected" });
|
|
2048
|
+
break;
|
|
2049
|
+
}
|
|
2050
|
+
currentUrl = next.href;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
};
|
|
2054
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, inputs.length) }, () => worker()));
|
|
2055
|
+
return { resolutions: [...records.values()], nonGoogleRequestsObserved: 0 };
|
|
2056
|
+
}
|
|
2057
|
+
async function resolveGoogleOutboundLinks(page, values, options = {}) {
|
|
2058
|
+
const byRawUrl = /* @__PURE__ */ new Map();
|
|
2059
|
+
for (const value of values) byRawUrl.set(value.rawUrl, value);
|
|
2060
|
+
const inputs = [...byRawUrl.values()].filter((value) => value.linkType === "google_goto_redirect");
|
|
2061
|
+
if (inputs.length === 0) return byRawUrl;
|
|
2062
|
+
const deadlineMs = Date.now() + (options.totalBudgetMs ?? DEFAULT_TOTAL_BUDGET_MS);
|
|
2063
|
+
const direct = await resolveGoogleGotoUrls(inputs.map((value) => value.rawUrl), options);
|
|
2064
|
+
for (const record of direct.resolutions) {
|
|
2065
|
+
const input = byRawUrl.get(record.rawUrl);
|
|
2066
|
+
if (!input) continue;
|
|
2067
|
+
byRawUrl.set(record.rawUrl, record.resolutionStatus === "resolved" && record.resolvedUrl ? { ...input, url: record.resolvedUrl, resolvedUrl: record.resolvedUrl, resolutionStatus: "resolved" } : { ...input, resolutionStatus: record.resolutionStatus });
|
|
2068
|
+
}
|
|
2069
|
+
const unresolvedInputs = inputs.filter((input) => byRawUrl.get(input.rawUrl)?.resolutionStatus === "unresolved");
|
|
2070
|
+
if (unresolvedInputs.length === 0 || options.browserFallback === false || Date.now() >= deadlineMs) {
|
|
2071
|
+
options.onNetworkAudit?.({ nonGoogleRequestsObserved: 0 });
|
|
2072
|
+
return byRawUrl;
|
|
2073
|
+
}
|
|
2074
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2075
|
+
const resolverPage = await page.context().newPage().catch(() => null);
|
|
2076
|
+
if (!resolverPage) {
|
|
2077
|
+
options.onNetworkAudit?.({ nonGoogleRequestsObserved: 0 });
|
|
2078
|
+
return byRawUrl;
|
|
2079
|
+
}
|
|
2080
|
+
let active = null;
|
|
2081
|
+
let nonGoogleRequestsObserved = 0;
|
|
2082
|
+
try {
|
|
2083
|
+
await resolverPage.route("**/*", async (route) => {
|
|
2084
|
+
const requestUrl = route.request().url();
|
|
2085
|
+
try {
|
|
2086
|
+
if (!isGoogleHost(new URL(requestUrl).hostname)) nonGoogleRequestsObserved += 1;
|
|
2087
|
+
} catch {
|
|
2088
|
+
}
|
|
2089
|
+
const requestToken = tokenFor(requestUrl);
|
|
2090
|
+
if (!active || requestToken !== active.token) {
|
|
2091
|
+
await route.abort("blockedbyclient").catch(() => {
|
|
2092
|
+
});
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
const target = active;
|
|
2096
|
+
try {
|
|
2097
|
+
const response = await route.fetch({ maxRedirects: 0, timeout: target.timeoutMs });
|
|
2098
|
+
target.location = response.headers().location ?? null;
|
|
2099
|
+
} finally {
|
|
2100
|
+
await route.abort("blockedbyclient").catch(() => {
|
|
2101
|
+
});
|
|
2102
|
+
target.finish();
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
for (const input of unresolvedInputs) {
|
|
2106
|
+
const remainingMs = deadlineMs - Date.now();
|
|
2107
|
+
if (remainingMs <= 0) break;
|
|
2108
|
+
const source = parsedGoogleGoto(input.rawUrl);
|
|
2109
|
+
const expectedToken = source?.searchParams.get("url");
|
|
2110
|
+
if (!source || !expectedToken) {
|
|
2111
|
+
byRawUrl.set(input.rawUrl, { ...input, resolutionStatus: "rejected" });
|
|
2112
|
+
continue;
|
|
2113
|
+
}
|
|
2114
|
+
const attemptTimeoutMs = Math.min(timeoutMs, remainingMs);
|
|
2115
|
+
source.hash = "";
|
|
2116
|
+
let currentUrl = source.href;
|
|
2117
|
+
let rejected = false;
|
|
2118
|
+
for (let hop = 0; hop < 2; hop++) {
|
|
2119
|
+
let finish;
|
|
2120
|
+
const intercepted = new Promise((resolve) => {
|
|
2121
|
+
finish = resolve;
|
|
2122
|
+
});
|
|
2123
|
+
const frameId = `mcp-goto-${Math.random().toString(36).slice(2)}`;
|
|
2124
|
+
active = { token: expectedToken, timeoutMs: attemptTimeoutMs, location: null, finish };
|
|
2125
|
+
await resolverPage.evaluate(({ url, id }) => {
|
|
2126
|
+
const frame = document.createElement("iframe");
|
|
2127
|
+
frame.id = id;
|
|
2128
|
+
frame.hidden = true;
|
|
2129
|
+
frame.src = url;
|
|
2130
|
+
document.body.append(frame);
|
|
2131
|
+
}, { url: currentUrl, id: frameId }).catch(() => null);
|
|
2132
|
+
await Promise.race([
|
|
2133
|
+
intercepted,
|
|
2134
|
+
new Promise((resolve) => setTimeout(resolve, attemptTimeoutMs))
|
|
2135
|
+
]);
|
|
2136
|
+
const location = active.location;
|
|
2137
|
+
active = null;
|
|
2138
|
+
await resolverPage.evaluate((id) => document.getElementById(id)?.remove(), frameId).catch(() => null);
|
|
2139
|
+
if (!location) break;
|
|
2140
|
+
const destination = externalRedirectTarget(input.rawUrl, location);
|
|
2141
|
+
if (destination) {
|
|
2142
|
+
byRawUrl.set(input.rawUrl, {
|
|
2143
|
+
...input,
|
|
2144
|
+
url: destination,
|
|
2145
|
+
resolvedUrl: destination,
|
|
2146
|
+
resolutionStatus: "resolved"
|
|
2147
|
+
});
|
|
2148
|
+
break;
|
|
2149
|
+
}
|
|
2150
|
+
let next;
|
|
2151
|
+
try {
|
|
2152
|
+
next = new URL(location, currentUrl);
|
|
2153
|
+
} catch {
|
|
2154
|
+
rejected = true;
|
|
2155
|
+
break;
|
|
2156
|
+
}
|
|
2157
|
+
if (!isGoogleHost(next.hostname) || tokenFor(next.href) !== expectedToken) {
|
|
2158
|
+
rejected = true;
|
|
2159
|
+
break;
|
|
2160
|
+
}
|
|
2161
|
+
currentUrl = next.href;
|
|
2162
|
+
}
|
|
2163
|
+
if (rejected && byRawUrl.get(input.rawUrl)?.resolutionStatus !== "resolved") {
|
|
2164
|
+
byRawUrl.set(input.rawUrl, { ...input, resolutionStatus: "rejected" });
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
} finally {
|
|
2168
|
+
active = null;
|
|
2169
|
+
await resolverPage.close().catch(() => {
|
|
2170
|
+
});
|
|
2171
|
+
options.onNetworkAudit?.({ nonGoogleRequestsObserved });
|
|
2172
|
+
}
|
|
2173
|
+
return byRawUrl;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
1909
2176
|
// src/extractor/PAAExtractor.ts
|
|
1910
2177
|
var PAAExtractor = class {
|
|
1911
2178
|
constructor(driver, reporter, onPaaProgress) {
|
|
@@ -1923,10 +2190,118 @@ var PAAExtractor = class {
|
|
|
1923
2190
|
aioShareCaptured: null
|
|
1924
2191
|
};
|
|
1925
2192
|
aioShareUrlEarly = null;
|
|
2193
|
+
linkDiagnostics = this.emptyLinkDiagnostics();
|
|
1926
2194
|
collectedRows = [];
|
|
1927
2195
|
partialContext = null;
|
|
1928
2196
|
progressSequence = 0;
|
|
1929
2197
|
clickedQuestions = /* @__PURE__ */ new Set();
|
|
2198
|
+
paaLifecycle = this.emptyPaaLifecycle(0);
|
|
2199
|
+
checkpointMaterial = null;
|
|
2200
|
+
emptyPaaLifecycle(requestedQuestions) {
|
|
2201
|
+
return {
|
|
2202
|
+
requestedQuestions,
|
|
2203
|
+
questionsFound: 0,
|
|
2204
|
+
targetStatus: "not_met",
|
|
2205
|
+
discoveryStatus: "in_progress",
|
|
2206
|
+
materialStatus: "partial",
|
|
2207
|
+
completionReason: "PAA expansion is still running.",
|
|
2208
|
+
automaticRetries: 0,
|
|
2209
|
+
knownQuestions: 0,
|
|
2210
|
+
processedQuestions: 0,
|
|
2211
|
+
failedInteractions: 0,
|
|
2212
|
+
noGrowthConfirmations: 0
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
emptyLinkDiagnostics() {
|
|
2216
|
+
return {
|
|
2217
|
+
linkCandidatesObserved: 0,
|
|
2218
|
+
plainLinksObserved: 0,
|
|
2219
|
+
googleUrlRedirectsObserved: 0,
|
|
2220
|
+
googleGotoRedirectsObserved: 0,
|
|
2221
|
+
redirectsResolved: 0,
|
|
2222
|
+
redirectsUnresolved: 0,
|
|
2223
|
+
redirectsRejected: 0,
|
|
2224
|
+
resolverNonGoogleRequestsObserved: 0,
|
|
2225
|
+
paaSourcesPreserved: 0,
|
|
2226
|
+
aiOverviewLabelValidated: false
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
mergePaaSources(existing = [], incoming = []) {
|
|
2230
|
+
const byRawUrl = /* @__PURE__ */ new Map();
|
|
2231
|
+
for (const source of [...existing, ...incoming]) {
|
|
2232
|
+
const current = byRawUrl.get(source.rawUrl);
|
|
2233
|
+
if (!current) {
|
|
2234
|
+
byRawUrl.set(source.rawUrl, source);
|
|
2235
|
+
continue;
|
|
2236
|
+
}
|
|
2237
|
+
const selected = source.resolutionStatus === "resolved" && current.resolutionStatus !== "resolved" ? source : current;
|
|
2238
|
+
byRawUrl.set(source.rawUrl, { ...selected, title: selected.title ?? current.title ?? source.title, site: selected.site ?? current.site ?? source.site });
|
|
2239
|
+
}
|
|
2240
|
+
return [...byRawUrl.values()];
|
|
2241
|
+
}
|
|
2242
|
+
normalizeAICitations(pageUrl, citations) {
|
|
2243
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2244
|
+
return citations.flatMap((citation) => {
|
|
2245
|
+
const link = classifyGoogleOutboundLink(citation.rawHref ?? citation.href, pageUrl);
|
|
2246
|
+
if (!link || seen.has(link.rawUrl)) return [];
|
|
2247
|
+
seen.add(link.rawUrl);
|
|
2248
|
+
const { url, ...metadata } = link;
|
|
2249
|
+
return [{ ...metadata, text: citation.text, href: url }];
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
async resolveMaterialLinks(page, flat, organicResults, aiOverview, aiMode) {
|
|
2253
|
+
const candidates = [
|
|
2254
|
+
...flat.flatMap((row) => row.sources ?? []),
|
|
2255
|
+
...organicResults,
|
|
2256
|
+
...aiOverview.citations.map(({ text: _text, href, ...link }) => ({ ...link, url: href })),
|
|
2257
|
+
...aiMode.citations.map(({ text: _text, href, ...link }) => ({ ...link, url: href }))
|
|
2258
|
+
];
|
|
2259
|
+
const unique = new Map(candidates.map((link) => [link.rawUrl, link]));
|
|
2260
|
+
let resolverNonGoogleRequestsObserved = 0;
|
|
2261
|
+
const resolved = await resolveGoogleOutboundLinks(page, unique.values(), {
|
|
2262
|
+
onNetworkAudit: (audit) => {
|
|
2263
|
+
resolverNonGoogleRequestsObserved = audit.nonGoogleRequestsObserved;
|
|
2264
|
+
}
|
|
2265
|
+
});
|
|
2266
|
+
const replace = (link) => ({ ...link, ...resolved.get(link.rawUrl) ?? link });
|
|
2267
|
+
for (const row of flat) {
|
|
2268
|
+
row.sources = (row.sources ?? []).map(replace);
|
|
2269
|
+
const primary = row.sources[0];
|
|
2270
|
+
row.source_title = primary?.title ?? row.source_title;
|
|
2271
|
+
row.source_site = primary?.site ?? row.source_site;
|
|
2272
|
+
row.source_cite = primary?.url ?? row.source_cite;
|
|
2273
|
+
}
|
|
2274
|
+
for (let index = 0; index < organicResults.length; index++) {
|
|
2275
|
+
const next = replace(organicResults[index]);
|
|
2276
|
+
if (next.resolvedUrl) {
|
|
2277
|
+
try {
|
|
2278
|
+
next.domain = new URL(next.resolvedUrl).hostname.replace(/^www\./, "");
|
|
2279
|
+
} catch {
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
organicResults[index] = next;
|
|
2283
|
+
}
|
|
2284
|
+
const replaceCitation = (citation) => {
|
|
2285
|
+
const link = replace({ url: citation.href, rawUrl: citation.rawUrl, resolvedUrl: citation.resolvedUrl, linkType: citation.linkType, resolutionStatus: citation.resolutionStatus });
|
|
2286
|
+
const { url, ...metadata } = link;
|
|
2287
|
+
return { ...metadata, text: citation.text, href: url };
|
|
2288
|
+
};
|
|
2289
|
+
aiOverview.citations = aiOverview.citations.map(replaceCitation);
|
|
2290
|
+
aiMode.citations = aiMode.citations.map(replaceCitation);
|
|
2291
|
+
const values = [...resolved.values()];
|
|
2292
|
+
this.linkDiagnostics = {
|
|
2293
|
+
linkCandidatesObserved: unique.size,
|
|
2294
|
+
plainLinksObserved: values.filter((link) => link.linkType === "plain").length,
|
|
2295
|
+
googleUrlRedirectsObserved: values.filter((link) => link.linkType === "google_url_redirect").length,
|
|
2296
|
+
googleGotoRedirectsObserved: values.filter((link) => link.linkType === "google_goto_redirect").length,
|
|
2297
|
+
redirectsResolved: values.filter((link) => link.linkType !== "plain" && link.resolutionStatus === "resolved").length,
|
|
2298
|
+
redirectsUnresolved: values.filter((link) => link.resolutionStatus === "unresolved").length,
|
|
2299
|
+
redirectsRejected: values.filter((link) => link.resolutionStatus === "rejected").length,
|
|
2300
|
+
resolverNonGoogleRequestsObserved,
|
|
2301
|
+
paaSourcesPreserved: flat.reduce((total, row) => total + (row.sources ?? []).length, 0),
|
|
2302
|
+
aiOverviewLabelValidated: aiOverview.detected
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
1930
2305
|
normalizeQuestion(q) {
|
|
1931
2306
|
return q.toLowerCase().replace(/[^\w\s]/g, "").replace(/\s+/g, " ").trim();
|
|
1932
2307
|
}
|
|
@@ -1963,34 +2338,6 @@ var PAAExtractor = class {
|
|
|
1963
2338
|
function cleanSourceLabel(value) {
|
|
1964
2339
|
return (value ?? "").replace(/\s*\(\+\d+\)\s*-\s*View related links.*$/i, "").replace(/\s*-\s*Opens in new tab.*$/i, "").trim();
|
|
1965
2340
|
}
|
|
1966
|
-
function isGoogleHost(host) {
|
|
1967
|
-
const normalized = host.toLowerCase().replace(/\.$/, "");
|
|
1968
|
-
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");
|
|
1969
|
-
}
|
|
1970
|
-
function safeCitationTarget(rawHref) {
|
|
1971
|
-
let parsed;
|
|
1972
|
-
try {
|
|
1973
|
-
parsed = new URL(rawHref);
|
|
1974
|
-
} catch {
|
|
1975
|
-
return null;
|
|
1976
|
-
}
|
|
1977
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
1978
|
-
if (parsed.username || parsed.password) return null;
|
|
1979
|
-
if (isGoogleHost(parsed.hostname)) {
|
|
1980
|
-
if (parsed.pathname !== "/url") return null;
|
|
1981
|
-
const redirected = parsed.searchParams.get("q") ?? parsed.searchParams.get("url");
|
|
1982
|
-
if (!redirected) return null;
|
|
1983
|
-
try {
|
|
1984
|
-
parsed = new URL(redirected);
|
|
1985
|
-
} catch {
|
|
1986
|
-
return null;
|
|
1987
|
-
}
|
|
1988
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
1989
|
-
if (parsed.username || parsed.password) return null;
|
|
1990
|
-
}
|
|
1991
|
-
if (isGoogleHost(parsed.hostname)) return null;
|
|
1992
|
-
return { href: parsed.href, host: parsed.hostname };
|
|
1993
|
-
}
|
|
1994
2341
|
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) => {
|
|
1995
2342
|
const clickTarget = pair.querySelector(selectors.clickTarget);
|
|
1996
2343
|
const expanded = pair.classList.contains(selectors.expandedClass) || clickTarget?.getAttribute("aria-expanded") === "true";
|
|
@@ -1998,50 +2345,62 @@ var PAAExtractor = class {
|
|
|
1998
2345
|
const aiAnswerBody = answerContainer?.querySelector(selectors.aiAnswerBody);
|
|
1999
2346
|
const answerElement = aiAnswerBody ?? answerContainer;
|
|
2000
2347
|
const answerText = cleanText(answerElement);
|
|
2001
|
-
const
|
|
2348
|
+
const citationCandidates = [
|
|
2349
|
+
...Array.from((answerElement ?? pair).querySelectorAll("a[href]")),
|
|
2350
|
+
...Array.from(pair.querySelectorAll(selectors.aiCitation)),
|
|
2351
|
+
...Array.from(pair.querySelectorAll(selectors.aiSourceCard))
|
|
2352
|
+
];
|
|
2353
|
+
const hasLoadedAnswer = expanded || answerText.length > 30 && !/an error has occurred/i.test(answerText.slice(0, 80)) && citationCandidates.some((anchor) => /^https?:\/\//.test(anchor.href));
|
|
2002
2354
|
let sourceTitle = hasLoadedAnswer ? pair.querySelector(selectors.sourceTitle)?.innerText?.trim() || void 0 : void 0;
|
|
2003
2355
|
let sourceSite = hasLoadedAnswer ? pair.querySelector(selectors.sourceSite)?.innerText?.trim() || void 0 : void 0;
|
|
2004
|
-
|
|
2005
|
-
if (hasLoadedAnswer
|
|
2356
|
+
const sources = [];
|
|
2357
|
+
if (hasLoadedAnswer) {
|
|
2006
2358
|
const seenHrefs = /* @__PURE__ */ new Set();
|
|
2007
|
-
const
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
...Array.from(anchorRoot.querySelectorAll(selectors.aiSourceCard)),
|
|
2012
|
-
...Array.from(anchorRoot.querySelectorAll("a[href]"))
|
|
2013
|
-
];
|
|
2014
|
-
for (const a of prioritized) {
|
|
2015
|
-
const target = safeCitationTarget(a.href);
|
|
2016
|
-
if (!target || seenHrefs.has(target.href)) continue;
|
|
2017
|
-
seenHrefs.add(target.href);
|
|
2018
|
-
anchors.push({
|
|
2019
|
-
text: (a.textContent || "").trim(),
|
|
2020
|
-
label: cleanSourceLabel(a.getAttribute("aria-label")),
|
|
2021
|
-
href: target.href,
|
|
2022
|
-
host: target.host
|
|
2023
|
-
});
|
|
2024
|
-
}
|
|
2025
|
-
const primary = anchors.find((x) => x.label.length > 2 || x.text.length > 2) ?? anchors[0];
|
|
2026
|
-
if (primary) {
|
|
2027
|
-
sourceTitle = sourceTitle || primary.label || primary.text || void 0;
|
|
2028
|
-
sourceSite = sourceSite || primary.host.replace(/^www\./, "");
|
|
2029
|
-
sourceCite = primary.href;
|
|
2359
|
+
for (const a of citationCandidates) {
|
|
2360
|
+
if (!/^https?:\/\//.test(a.href) || seenHrefs.has(a.href)) continue;
|
|
2361
|
+
seenHrefs.add(a.href);
|
|
2362
|
+
sources.push({ title: cleanSourceLabel(a.getAttribute("aria-label")) || (a.textContent || "").trim(), rawHref: a.href });
|
|
2030
2363
|
}
|
|
2364
|
+
const primary = sources.find((x) => x.title.length > 2) ?? sources[0];
|
|
2365
|
+
if (primary) sourceTitle = sourceTitle || primary.title || void 0;
|
|
2031
2366
|
}
|
|
2032
2367
|
return {
|
|
2368
|
+
googleLinkId: pair.getAttribute("data-lk") || pair.querySelector("[data-lk]")?.getAttribute("data-lk") || null,
|
|
2369
|
+
googleEvidenceId: pair.getAttribute("data-ved") || pair.querySelector("[data-ved]")?.getAttribute("data-ved") || null,
|
|
2033
2370
|
question: pair.getAttribute(selectors.itemDataQ) || pair.getAttribute(selectors.itemDataInitQ) || pair.querySelector(selectors.itemQuestionEl)?.innerText?.trim() || "",
|
|
2034
2371
|
answer: hasLoadedAnswer ? answerText || void 0 : void 0,
|
|
2035
2372
|
sourceTitle,
|
|
2036
2373
|
sourceSite,
|
|
2037
|
-
|
|
2374
|
+
sources
|
|
2038
2375
|
};
|
|
2039
2376
|
});
|
|
2040
2377
|
}, { selectors: sels, onlyQuestion });
|
|
2378
|
+
const baseUrl = typeof page.url === "function" ? page.url() : "https://www.google.com/";
|
|
2041
2379
|
return raw.flatMap((item) => {
|
|
2380
|
+
const sources = [];
|
|
2381
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2382
|
+
for (const candidate of item.sources) {
|
|
2383
|
+
const link = classifyGoogleOutboundLink(candidate.rawHref, baseUrl);
|
|
2384
|
+
if (!link || seen.has(link.rawUrl)) continue;
|
|
2385
|
+
seen.add(link.rawUrl);
|
|
2386
|
+
let site = item.sourceSite ?? null;
|
|
2387
|
+
try {
|
|
2388
|
+
site ||= new URL(link.resolvedUrl ?? link.rawUrl).hostname.replace(/^www\./, "");
|
|
2389
|
+
} catch {
|
|
2390
|
+
}
|
|
2391
|
+
sources.push({ ...link, title: candidate.title || item.sourceTitle || null, site });
|
|
2392
|
+
}
|
|
2393
|
+
const primary = sources[0];
|
|
2042
2394
|
const cleaned = {
|
|
2043
|
-
|
|
2044
|
-
|
|
2395
|
+
questionId: questionIdFor(item.question),
|
|
2396
|
+
googleLinkId: item.googleLinkId,
|
|
2397
|
+
googleEvidenceId: item.googleEvidenceId,
|
|
2398
|
+
question: item.question,
|
|
2399
|
+
answer: cleanPAAAnswerText(item.answer, item.question, item.sourceTitle),
|
|
2400
|
+
sourceTitle: primary?.title ?? item.sourceTitle,
|
|
2401
|
+
sourceSite: primary?.site ?? item.sourceSite,
|
|
2402
|
+
sourceCite: primary?.url,
|
|
2403
|
+
sources
|
|
2045
2404
|
};
|
|
2046
2405
|
const result = RawPAAItemSchema.safeParse(cleaned);
|
|
2047
2406
|
if (!result.success) {
|
|
@@ -2066,11 +2425,15 @@ var PAAExtractor = class {
|
|
|
2066
2425
|
toFlatRow(item, depth, parentQuestion, seed) {
|
|
2067
2426
|
return {
|
|
2068
2427
|
seed_query: seed,
|
|
2428
|
+
question_id: item.questionId ?? questionIdFor(item.question),
|
|
2429
|
+
google_link_id: item.googleLinkId ?? "",
|
|
2430
|
+
google_evidence_id: item.googleEvidenceId ?? "",
|
|
2069
2431
|
question: item.question,
|
|
2070
2432
|
answer: item.answer ?? "",
|
|
2071
2433
|
source_title: item.sourceTitle ?? "",
|
|
2072
2434
|
source_site: item.sourceSite ?? "",
|
|
2073
2435
|
source_cite: item.sourceCite ?? "",
|
|
2436
|
+
sources: item.sources ?? [],
|
|
2074
2437
|
depth,
|
|
2075
2438
|
parent_question: parentQuestion ?? "",
|
|
2076
2439
|
extracted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -2091,20 +2454,23 @@ var PAAExtractor = class {
|
|
|
2091
2454
|
source_title: row.source_title || existing.source_title,
|
|
2092
2455
|
source_site: row.source_site || existing.source_site,
|
|
2093
2456
|
source_cite: row.source_cite || existing.source_cite,
|
|
2457
|
+
sources: this.mergePaaSources(existing.sources, row.sources),
|
|
2094
2458
|
extracted_at: row.extracted_at || existing.extracted_at
|
|
2095
2459
|
};
|
|
2096
|
-
const changed = merged.answer !== existing.answer || merged.source_title !== existing.source_title || merged.source_site !== existing.source_site || merged.source_cite !== existing.source_cite;
|
|
2460
|
+
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);
|
|
2097
2461
|
if (changed) this.collectedRows[index] = merged;
|
|
2098
2462
|
return changed;
|
|
2099
2463
|
}
|
|
2100
2464
|
async emitProgress(phase) {
|
|
2101
|
-
if (!this.onPaaProgress || this.collectedRows.length === 0) return;
|
|
2465
|
+
if (!this.onPaaProgress || this.collectedRows.length === 0 && !this.checkpointMaterial) return;
|
|
2102
2466
|
const snapshot = {
|
|
2103
2467
|
sequence: ++this.progressSequence,
|
|
2104
2468
|
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2105
2469
|
clickedQuestions: [...this.clickedQuestions],
|
|
2106
2470
|
phase,
|
|
2107
|
-
records: this.collectedRows.map((row) => ({ ...row }))
|
|
2471
|
+
records: this.collectedRows.map((row) => ({ ...row })),
|
|
2472
|
+
lifecycle: { ...this.paaLifecycle },
|
|
2473
|
+
...this.checkpointMaterial ? { material: structuredClone(this.checkpointMaterial) } : {}
|
|
2108
2474
|
};
|
|
2109
2475
|
try {
|
|
2110
2476
|
await this.onPaaProgress(snapshot);
|
|
@@ -2119,10 +2485,11 @@ var PAAExtractor = class {
|
|
|
2119
2485
|
}
|
|
2120
2486
|
getPartialResult() {
|
|
2121
2487
|
const ctx = this.partialContext;
|
|
2122
|
-
if (!ctx || this.collectedRows.length === 0) return null;
|
|
2488
|
+
if (!ctx || this.collectedRows.length === 0 && !this.checkpointMaterial) return null;
|
|
2123
2489
|
const flat = this.collectedRows.map((row) => ({ ...row }));
|
|
2490
|
+
const material = this.checkpointMaterial;
|
|
2124
2491
|
const paaWithoutAnswer = ctx.questionsOnly ? 0 : flat.filter((row) => !row.answer?.trim()).length;
|
|
2125
|
-
const paaWithoutSource = ctx.questionsOnly ? 0 : flat.filter((row) =>
|
|
2492
|
+
const paaWithoutSource = ctx.questionsOnly ? 0 : flat.filter((row) => (row.sources ?? []).length === 0).length;
|
|
2126
2493
|
const stats = {
|
|
2127
2494
|
seed: ctx.seed,
|
|
2128
2495
|
totalQuestions: flat.length,
|
|
@@ -2140,6 +2507,14 @@ var PAAExtractor = class {
|
|
|
2140
2507
|
resultQuality: "partial",
|
|
2141
2508
|
degradedResult: false,
|
|
2142
2509
|
retryRecommended: true,
|
|
2510
|
+
paaLifecycle: {
|
|
2511
|
+
...this.paaLifecycle,
|
|
2512
|
+
questionsFound: flat.length,
|
|
2513
|
+
targetStatus: flat.length >= (ctx.maxQuestions ?? 0) ? "met" : "not_met",
|
|
2514
|
+
discoveryStatus: "interrupted",
|
|
2515
|
+
materialStatus: paaWithoutAnswer > 0 || paaWithoutSource > 0 ? "partial" : "complete",
|
|
2516
|
+
completionReason: "The browser attempt ended before frontier exhaustion could be proven."
|
|
2517
|
+
},
|
|
2143
2518
|
completeness: {
|
|
2144
2519
|
...this.completeness,
|
|
2145
2520
|
paaWithoutAnswer,
|
|
@@ -2156,17 +2531,17 @@ var PAAExtractor = class {
|
|
|
2156
2531
|
}]
|
|
2157
2532
|
},
|
|
2158
2533
|
totalQuestions: flat.length,
|
|
2159
|
-
surface: "web",
|
|
2160
|
-
aiOverview: { detected: false, text: null, citations: [] },
|
|
2161
|
-
aiMode: { detected: false, text: null, citations: [] },
|
|
2162
|
-
whatPeopleSaying: [],
|
|
2534
|
+
surface: material?.surface ?? "web",
|
|
2535
|
+
aiOverview: material?.aiOverview ?? { detected: false, text: null, citations: [] },
|
|
2536
|
+
aiMode: material?.aiMode ?? { detected: false, text: null, citations: [] },
|
|
2537
|
+
whatPeopleSaying: material?.whatPeopleSaying ?? [],
|
|
2163
2538
|
tree: this.buildTree(flat, ctx.seed),
|
|
2164
2539
|
flat,
|
|
2165
|
-
videos: [],
|
|
2166
|
-
forums: [],
|
|
2167
|
-
organicResults: [],
|
|
2168
|
-
localPack: [],
|
|
2169
|
-
entityIds: { entities: [], kgIds: [], cids: [], gcids: [] },
|
|
2540
|
+
videos: material?.videos ?? [],
|
|
2541
|
+
forums: material?.forums ?? [],
|
|
2542
|
+
organicResults: material?.organicResults ?? [],
|
|
2543
|
+
localPack: material?.localPack ?? [],
|
|
2544
|
+
entityIds: material?.entityIds ?? { entities: [], kgIds: [], cids: [], gcids: [] },
|
|
2170
2545
|
stats
|
|
2171
2546
|
};
|
|
2172
2547
|
}
|
|
@@ -2175,12 +2550,14 @@ var PAAExtractor = class {
|
|
|
2175
2550
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
2176
2551
|
const seenQs = /* @__PURE__ */ new Set();
|
|
2177
2552
|
const orderedQs = [];
|
|
2178
|
-
this.collectedRows = [];
|
|
2179
|
-
this.progressSequence = 0;
|
|
2180
2553
|
this.clickedQuestions = /* @__PURE__ */ new Set();
|
|
2181
2554
|
const abandonedNeverReclick = /* @__PURE__ */ new Set();
|
|
2182
|
-
const
|
|
2555
|
+
const clickAttempts = /* @__PURE__ */ new Map();
|
|
2556
|
+
const processedQuestions = /* @__PURE__ */ new Set();
|
|
2557
|
+
const failedInteractions = /* @__PURE__ */ new Set();
|
|
2183
2558
|
const capturedItems = /* @__PURE__ */ new Map();
|
|
2559
|
+
const MAX_CLICK_ATTEMPTS = 2;
|
|
2560
|
+
const EXHAUSTION_CONFIRMATIONS = 3;
|
|
2184
2561
|
const itemArgs = {
|
|
2185
2562
|
sel: PAASelectors.item,
|
|
2186
2563
|
dataQ: PAASelectors.itemDataQ,
|
|
@@ -2191,15 +2568,15 @@ var PAAExtractor = class {
|
|
|
2191
2568
|
clickTarget: PAASelectors.clickTarget
|
|
2192
2569
|
};
|
|
2193
2570
|
const readItemStates = () => page.evaluate(
|
|
2194
|
-
({ sel, dataQ, dataInitQ, questionEl, expandedClass }) => {
|
|
2571
|
+
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget, answerSel }) => {
|
|
2195
2572
|
return Array.from(document.querySelectorAll(sel)).map((el) => ({
|
|
2196
2573
|
q: el.getAttribute(dataQ) || el.getAttribute(dataInitQ) || el.querySelector(questionEl)?.innerText?.trim() || "",
|
|
2197
|
-
done: el.classList.contains(expandedClass)
|
|
2574
|
+
done: el.classList.contains(expandedClass) || el.querySelector(clickTarget)?.getAttribute("aria-expanded") === "true" || Boolean((el.querySelector(answerSel)?.textContent ?? "").trim())
|
|
2198
2575
|
})).filter((s) => s.q);
|
|
2199
2576
|
},
|
|
2200
2577
|
itemArgs
|
|
2201
2578
|
);
|
|
2202
|
-
const expandOneItemSerially = async (q) => {
|
|
2579
|
+
const expandOneItemSerially = async (q, beforeCount) => {
|
|
2203
2580
|
const clickStartedMs = Date.now();
|
|
2204
2581
|
const items = page.locator(itemArgs.sel);
|
|
2205
2582
|
const itemIndex = await items.evaluateAll(
|
|
@@ -2214,49 +2591,118 @@ var PAAExtractor = class {
|
|
|
2214
2591
|
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
|
|
2215
2592
|
return true;
|
|
2216
2593
|
})().catch(() => false) : false;
|
|
2217
|
-
if (!clicked) return
|
|
2594
|
+
if (!clicked) return { dispatched: false, confirmed: false };
|
|
2218
2595
|
const remainingHumanClickDelayMs = 700 - (Date.now() - clickStartedMs);
|
|
2219
2596
|
if (remainingHumanClickDelayMs > 0) await page.waitForTimeout(remainingHumanClickDelayMs);
|
|
2220
|
-
|
|
2597
|
+
const confirmed = await page.waitForFunction(
|
|
2598
|
+
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget: clickTarget2, answerSel, target, min }) => {
|
|
2599
|
+
const items2 = Array.from(document.querySelectorAll(sel));
|
|
2600
|
+
const item = items2.find((element) => (element.getAttribute(dataQ) || element.getAttribute(dataInitQ) || element.querySelector(questionEl)?.innerText?.trim()) === target);
|
|
2601
|
+
if (!item) return items2.length > min;
|
|
2602
|
+
return item.classList.contains(expandedClass) || item.querySelector(clickTarget2)?.getAttribute("aria-expanded") === "true" || Boolean((item.querySelector(answerSel)?.textContent ?? "").trim()) || items2.length > min;
|
|
2603
|
+
},
|
|
2604
|
+
{ ...itemArgs, target: q, min: beforeCount },
|
|
2605
|
+
{ timeout: 2500, polling: 100 }
|
|
2606
|
+
).catch(() => null);
|
|
2607
|
+
return { dispatched: true, confirmed: Boolean(confirmed) };
|
|
2221
2608
|
};
|
|
2222
2609
|
let round = 0;
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2610
|
+
expansion: while (true) {
|
|
2611
|
+
if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) {
|
|
2612
|
+
this.paaLifecycle = {
|
|
2613
|
+
...this.paaLifecycle,
|
|
2614
|
+
discoveryStatus: "interrupted",
|
|
2615
|
+
completionReason: "The execution deadline arrived before frontier exhaustion could be proven."
|
|
2616
|
+
};
|
|
2617
|
+
throw new PaaInteractionInterruptedError(this.paaLifecycle.completionReason);
|
|
2618
|
+
}
|
|
2226
2619
|
this.throwIfAborted(signal);
|
|
2227
2620
|
const states = await readItemStates();
|
|
2228
2621
|
let discovered = false;
|
|
2229
|
-
for (const
|
|
2230
|
-
if (!seenQs.has(
|
|
2231
|
-
seenQs.add(
|
|
2232
|
-
orderedQs.push(
|
|
2622
|
+
for (const state of states) {
|
|
2623
|
+
if (!seenQs.has(state.q)) {
|
|
2624
|
+
seenQs.add(state.q);
|
|
2625
|
+
orderedQs.push(state.q);
|
|
2233
2626
|
if (orderedQs.length <= options.maxQuestions) {
|
|
2234
|
-
discovered = this.upsertCollectedRow(this.toFlatRow({ question:
|
|
2627
|
+
discovered = this.upsertCollectedRow(this.toFlatRow({ questionId: questionIdFor(state.q), googleLinkId: null, googleEvidenceId: null, question: state.q, sources: [] }, 1, null, options.query)) || discovered;
|
|
2235
2628
|
}
|
|
2236
2629
|
}
|
|
2630
|
+
if (state.done) processedQuestions.add(state.q);
|
|
2237
2631
|
}
|
|
2632
|
+
this.paaLifecycle = {
|
|
2633
|
+
...this.paaLifecycle,
|
|
2634
|
+
questionsFound: Math.min(orderedQs.length, options.maxQuestions),
|
|
2635
|
+
targetStatus: orderedQs.length >= options.maxQuestions ? "met" : "not_met",
|
|
2636
|
+
knownQuestions: orderedQs.length,
|
|
2637
|
+
processedQuestions: processedQuestions.size,
|
|
2638
|
+
failedInteractions: failedInteractions.size
|
|
2639
|
+
};
|
|
2238
2640
|
if (discovered) await this.emitProgress("discovered");
|
|
2239
|
-
if (options.questionsOnly && seenQs.size >= options.maxQuestions)
|
|
2641
|
+
if (options.questionsOnly && seenQs.size >= options.maxQuestions) {
|
|
2642
|
+
this.paaLifecycle = {
|
|
2643
|
+
...this.paaLifecycle,
|
|
2644
|
+
discoveryStatus: "target_reached",
|
|
2645
|
+
targetStatus: "met",
|
|
2646
|
+
completionReason: "The requested unique-question count was observed."
|
|
2647
|
+
};
|
|
2648
|
+
break;
|
|
2649
|
+
}
|
|
2240
2650
|
const kept = new Set(orderedQs.slice(0, options.maxQuestions));
|
|
2241
|
-
const clickable = states.filter((
|
|
2242
|
-
const target = clickable
|
|
2651
|
+
const clickable = states.filter((state) => kept.has(state.q) && !state.done && (clickAttempts.get(state.q) ?? 0) < MAX_CLICK_ATTEMPTS && !abandonedNeverReclick.has(state.q));
|
|
2652
|
+
const target = clickable[0];
|
|
2243
2653
|
if (!target) {
|
|
2244
|
-
if (seenQs.size >= options.maxQuestions)
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2654
|
+
if (seenQs.size >= options.maxQuestions) {
|
|
2655
|
+
this.paaLifecycle = {
|
|
2656
|
+
...this.paaLifecycle,
|
|
2657
|
+
discoveryStatus: "target_reached",
|
|
2658
|
+
targetStatus: "met",
|
|
2659
|
+
completionReason: "The requested unique-question count was observed."
|
|
2660
|
+
};
|
|
2661
|
+
break;
|
|
2662
|
+
}
|
|
2663
|
+
const visible = new Set(states.map((state) => state.q));
|
|
2664
|
+
const unresolved2 = orderedQs.slice(0, options.maxQuestions).filter(
|
|
2665
|
+
(question) => !processedQuestions.has(question) && (failedInteractions.has(question) || !visible.has(question))
|
|
2666
|
+
);
|
|
2667
|
+
if (unresolved2.length > 0) {
|
|
2668
|
+
this.paaLifecycle = {
|
|
2669
|
+
...this.paaLifecycle,
|
|
2670
|
+
discoveryStatus: "interrupted",
|
|
2671
|
+
failedInteractions: (/* @__PURE__ */ new Set([...failedInteractions, ...unresolved2])).size,
|
|
2672
|
+
completionReason: "At least one known PAA control failed or disappeared before it could be confirmed."
|
|
2673
|
+
};
|
|
2674
|
+
await this.emitProgress("expansion_finished");
|
|
2675
|
+
throw new PaaInteractionInterruptedError(this.paaLifecycle.completionReason);
|
|
2676
|
+
}
|
|
2677
|
+
for (let confirmation = 1; confirmation <= EXHAUSTION_CONFIRMATIONS; confirmation++) {
|
|
2678
|
+
this.throwIfAborted(signal);
|
|
2679
|
+
await this.throwIfCaptcha(page, "Google PAA exhaustion confirmation");
|
|
2680
|
+
await page.mouse.wheel(0, 320);
|
|
2681
|
+
const grew = await page.waitForFunction(
|
|
2682
|
+
({ sel, min }) => document.querySelectorAll(sel).length > min,
|
|
2683
|
+
{ sel: PAASelectors.item, min: states.length },
|
|
2684
|
+
{ timeout: 1500, polling: 100 }
|
|
2685
|
+
).catch(() => null);
|
|
2686
|
+
this.paaLifecycle = { ...this.paaLifecycle, noGrowthConfirmations: confirmation };
|
|
2687
|
+
if (grew) continue expansion;
|
|
2688
|
+
const confirmationStates = await readItemStates();
|
|
2689
|
+
if (confirmationStates.some((state) => !seenQs.has(state.q))) continue expansion;
|
|
2690
|
+
}
|
|
2691
|
+
this.paaLifecycle = {
|
|
2692
|
+
...this.paaLifecycle,
|
|
2693
|
+
discoveryStatus: "frontier_exhausted",
|
|
2694
|
+
targetStatus: "not_met",
|
|
2695
|
+
failedInteractions: 0,
|
|
2696
|
+
noGrowthConfirmations: EXHAUSTION_CONFIRMATIONS,
|
|
2697
|
+
completionReason: "Every known eligible PAA control was processed and three healthy no-growth confirmations found no additional questions."
|
|
2698
|
+
};
|
|
2699
|
+
break;
|
|
2253
2700
|
}
|
|
2254
|
-
growthWaits = 0;
|
|
2255
2701
|
this.reporter.onDepth(++round);
|
|
2256
2702
|
await this.throwIfCaptcha(page, "Google PAA expansion");
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2703
|
+
clickAttempts.set(target.q, (clickAttempts.get(target.q) ?? 0) + 1);
|
|
2704
|
+
const expansionResult = await expandOneItemSerially(target.q, states.length);
|
|
2705
|
+
if (expansionResult.dispatched) this.clickedQuestions.add(target.q);
|
|
2260
2706
|
if (!options.questionsOnly) {
|
|
2261
2707
|
let clickedItem = (await this.extractVisibleItems(page, target.q))[0];
|
|
2262
2708
|
if (!clickedItem?.answer) {
|
|
@@ -2270,27 +2716,21 @@ var PAAExtractor = class {
|
|
|
2270
2716
|
}
|
|
2271
2717
|
}
|
|
2272
2718
|
}
|
|
2273
|
-
|
|
2719
|
+
const captured = capturedItems.get(target.q);
|
|
2720
|
+
if (expansionResult.confirmed || Boolean(captured?.answer?.trim())) {
|
|
2721
|
+
processedQuestions.add(target.q);
|
|
2722
|
+
failedInteractions.delete(target.q);
|
|
2723
|
+
} else if ((clickAttempts.get(target.q) ?? 0) >= MAX_CLICK_ATTEMPTS) {
|
|
2724
|
+
failedInteractions.add(target.q);
|
|
2274
2725
|
abandonedNeverReclick.add(target.q);
|
|
2275
2726
|
}
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
for (const state of await readItemStates()) {
|
|
2284
|
-
if (!seenQs.has(state.q)) {
|
|
2285
|
-
seenQs.add(state.q);
|
|
2286
|
-
orderedQs.push(state.q);
|
|
2287
|
-
if (orderedQs.length <= options.maxQuestions) {
|
|
2288
|
-
this.upsertCollectedRow(this.toFlatRow({ question: state.q }, 1, null, options.query));
|
|
2289
|
-
}
|
|
2290
|
-
}
|
|
2291
|
-
}
|
|
2292
|
-
await this.emitProgress("discovered");
|
|
2293
|
-
}
|
|
2727
|
+
this.paaLifecycle = {
|
|
2728
|
+
...this.paaLifecycle,
|
|
2729
|
+
processedQuestions: processedQuestions.size,
|
|
2730
|
+
failedInteractions: failedInteractions.size,
|
|
2731
|
+
noGrowthConfirmations: 0
|
|
2732
|
+
};
|
|
2733
|
+
await this.emitProgress("click_observed");
|
|
2294
2734
|
}
|
|
2295
2735
|
const itemMap = options.questionsOnly ? /* @__PURE__ */ new Map() : new Map(capturedItems);
|
|
2296
2736
|
if (!options.questionsOnly) {
|
|
@@ -2307,24 +2747,34 @@ var PAAExtractor = class {
|
|
|
2307
2747
|
const row = this.toFlatRow(item, 1, null, options.query);
|
|
2308
2748
|
this.upsertCollectedRow(row);
|
|
2309
2749
|
results.push(row);
|
|
2310
|
-
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: [] });
|
|
2750
|
+
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: [] });
|
|
2311
2751
|
} else {
|
|
2312
|
-
const row = this.toFlatRow({ question: q, answer: void 0, sourceTitle: void 0, sourceSite: void 0, sourceCite: void 0 }, 1, null, options.query);
|
|
2752
|
+
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);
|
|
2313
2753
|
this.upsertCollectedRow(row);
|
|
2314
2754
|
results.push(row);
|
|
2315
2755
|
}
|
|
2316
2756
|
}
|
|
2317
2757
|
this.completeness.paaWithoutAnswer = options.questionsOnly ? 0 : results.filter((r) => !r.answer).length;
|
|
2318
|
-
this.completeness.paaWithoutSource = options.questionsOnly ? 0 : results.filter((r) =>
|
|
2758
|
+
this.completeness.paaWithoutSource = options.questionsOnly ? 0 : results.filter((r) => (r.sources ?? []).length === 0).length;
|
|
2319
2759
|
this.completeness.paaRequested = options.maxQuestions;
|
|
2320
2760
|
this.completeness.paaReturned = results.length;
|
|
2321
2761
|
this.completeness.paaUnique = seenKeys.size;
|
|
2762
|
+
const materialPartial = !options.questionsOnly && (this.completeness.paaWithoutAnswer > 0 || this.completeness.paaWithoutSource > 0);
|
|
2763
|
+
this.paaLifecycle = {
|
|
2764
|
+
...this.paaLifecycle,
|
|
2765
|
+
questionsFound: results.length,
|
|
2766
|
+
targetStatus: results.length >= options.maxQuestions ? "met" : "not_met",
|
|
2767
|
+
materialStatus: materialPartial ? "partial" : "complete",
|
|
2768
|
+
processedQuestions: processedQuestions.size,
|
|
2769
|
+
failedInteractions: failedInteractions.size
|
|
2770
|
+
};
|
|
2322
2771
|
await this.emitProgress("fill_incomplete");
|
|
2772
|
+
await this.emitProgress("expansion_finished");
|
|
2323
2773
|
return results;
|
|
2324
2774
|
}
|
|
2325
2775
|
async fillIncompleteItems(page, orderedQs, itemMap, options, abandonedNeverReclick) {
|
|
2326
2776
|
const kept = new Set(orderedQs.slice(0, options.maxQuestions));
|
|
2327
|
-
const isIncomplete = (item) => !item?.answer ||
|
|
2777
|
+
const isIncomplete = (item) => !item?.answer || (item.sources ?? []).length === 0;
|
|
2328
2778
|
const missingBefore = new Set(
|
|
2329
2779
|
[...kept].filter((q) => isIncomplete(itemMap.get(q)))
|
|
2330
2780
|
);
|
|
@@ -2342,7 +2792,8 @@ var PAAExtractor = class {
|
|
|
2342
2792
|
answer: item.answer ?? existing?.answer,
|
|
2343
2793
|
sourceTitle: item.sourceTitle ?? existing?.sourceTitle,
|
|
2344
2794
|
sourceSite: item.sourceSite ?? existing?.sourceSite,
|
|
2345
|
-
sourceCite: item.sourceCite ?? existing?.sourceCite
|
|
2795
|
+
sourceCite: item.sourceCite ?? existing?.sourceCite,
|
|
2796
|
+
sources: this.mergePaaSources(existing?.sources, item.sources)
|
|
2346
2797
|
});
|
|
2347
2798
|
const upgraded = itemMap.get(q);
|
|
2348
2799
|
if (upgraded && this.upsertCollectedRow(this.toFlatRow(upgraded, 1, null, options.query))) {
|
|
@@ -2529,7 +2980,7 @@ var PAAExtractor = class {
|
|
|
2529
2980
|
}
|
|
2530
2981
|
async extractOrganicResults(page) {
|
|
2531
2982
|
const sels = OrganicSelectors;
|
|
2532
|
-
|
|
2983
|
+
const raw = await page.evaluate((s) => {
|
|
2533
2984
|
const out = [];
|
|
2534
2985
|
let pos = 0;
|
|
2535
2986
|
document.querySelectorAll(s.result).forEach((card) => {
|
|
@@ -2555,6 +3006,17 @@ var PAAExtractor = class {
|
|
|
2555
3006
|
});
|
|
2556
3007
|
return out;
|
|
2557
3008
|
}, sels);
|
|
3009
|
+
const baseUrl = typeof page.url === "function" ? page.url() : "https://www.google.com/";
|
|
3010
|
+
return raw.flatMap((item) => {
|
|
3011
|
+
const link = classifyGoogleOutboundLink(item.url, baseUrl);
|
|
3012
|
+
if (!link) return [];
|
|
3013
|
+
let domain = item.domain;
|
|
3014
|
+
try {
|
|
3015
|
+
domain = new URL(link.resolvedUrl ?? link.rawUrl).hostname.replace(/^www\./, "");
|
|
3016
|
+
} catch {
|
|
3017
|
+
}
|
|
3018
|
+
return [{ ...item, ...link, domain }];
|
|
3019
|
+
});
|
|
2558
3020
|
}
|
|
2559
3021
|
async extractLocalPack(page) {
|
|
2560
3022
|
const sels = LocalPackSelectors;
|
|
@@ -2706,15 +3168,21 @@ var PAAExtractor = class {
|
|
|
2706
3168
|
aim: AIModeSelectors,
|
|
2707
3169
|
expandWaitMs: 1500
|
|
2708
3170
|
});
|
|
2709
|
-
|
|
3171
|
+
const pageUrl = typeof page.url === "function" ? page.url() : "https://www.google.com/";
|
|
3172
|
+
const normalized = {
|
|
3173
|
+
surface: surfaces.surface,
|
|
3174
|
+
aiOverview: { ...surfaces.aiOverview, citations: this.normalizeAICitations(pageUrl, surfaces.aiOverview.citations) },
|
|
3175
|
+
aiMode: { ...surfaces.aiMode, citations: this.normalizeAICitations(pageUrl, surfaces.aiMode.citations) }
|
|
3176
|
+
};
|
|
3177
|
+
if (!normalized.aiOverview.detected) {
|
|
2710
3178
|
this.completeness.aioShareCaptured = null;
|
|
2711
|
-
return
|
|
3179
|
+
return normalized;
|
|
2712
3180
|
}
|
|
2713
3181
|
const shareUrl = this.aioShareUrlEarly ?? await this.captureAIOverviewShareUrl(page, options).catch(() => null);
|
|
2714
3182
|
this.completeness.aioShareCaptured = shareUrl !== null;
|
|
2715
3183
|
return {
|
|
2716
|
-
...
|
|
2717
|
-
aiOverview: { ...
|
|
3184
|
+
...normalized,
|
|
3185
|
+
aiOverview: { ...normalized.aiOverview, shareUrl }
|
|
2718
3186
|
};
|
|
2719
3187
|
}
|
|
2720
3188
|
async captureAIOverviewShareUrl(page, options) {
|
|
@@ -2759,11 +3227,15 @@ var PAAExtractor = class {
|
|
|
2759
3227
|
const nodeMap = /* @__PURE__ */ new Map();
|
|
2760
3228
|
for (const row of flat) {
|
|
2761
3229
|
const node = {
|
|
3230
|
+
questionId: row.question_id,
|
|
3231
|
+
googleLinkId: row.google_link_id || null,
|
|
3232
|
+
googleEvidenceId: row.google_evidence_id || null,
|
|
2762
3233
|
question: row.question,
|
|
2763
3234
|
answer: row.answer || null,
|
|
2764
3235
|
sourceTitle: row.source_title || null,
|
|
2765
3236
|
sourceSite: row.source_site || null,
|
|
2766
3237
|
sourceCite: row.source_cite || null,
|
|
3238
|
+
sources: row.sources ?? [],
|
|
2767
3239
|
depth: row.depth,
|
|
2768
3240
|
parentQuestion: row.parent_question || null,
|
|
2769
3241
|
children: []
|
|
@@ -2810,10 +3282,13 @@ var PAAExtractor = class {
|
|
|
2810
3282
|
async extract(options, signal) {
|
|
2811
3283
|
const startMs = Date.now();
|
|
2812
3284
|
this.completeness = { paaWithoutAnswer: 0, paaWithoutSource: 0, paaAnswersRecovered: 0, aioShareCaptured: null };
|
|
3285
|
+
this.linkDiagnostics = this.emptyLinkDiagnostics();
|
|
2813
3286
|
this.aioShareUrlEarly = null;
|
|
2814
3287
|
this.collectedRows = [];
|
|
2815
3288
|
this.progressSequence = 0;
|
|
3289
|
+
this.checkpointMaterial = null;
|
|
2816
3290
|
this.clickedQuestions = /* @__PURE__ */ new Set();
|
|
3291
|
+
this.paaLifecycle = this.emptyPaaLifecycle(options.maxQuestions);
|
|
2817
3292
|
this.partialContext = {
|
|
2818
3293
|
seed: options.query,
|
|
2819
3294
|
location: options.location ?? null,
|
|
@@ -2876,8 +3351,17 @@ var PAAExtractor = class {
|
|
|
2876
3351
|
this.throwIfAborted(signal);
|
|
2877
3352
|
const page = this.driver.getPage();
|
|
2878
3353
|
await this.throwIfCaptcha(page, "Google SERP");
|
|
3354
|
+
if (!hasPaa) {
|
|
3355
|
+
this.paaLifecycle = {
|
|
3356
|
+
...this.paaLifecycle,
|
|
3357
|
+
discoveryStatus: "no_paa_observed",
|
|
3358
|
+
materialStatus: "complete",
|
|
3359
|
+
completionReason: "A readable Google SERP was observed without a People Also Ask block."
|
|
3360
|
+
};
|
|
3361
|
+
}
|
|
2879
3362
|
if (options.questionsOnly) {
|
|
2880
3363
|
const flat2 = hasPaa ? await this.runBFS(page, executionOptions, signal) : [];
|
|
3364
|
+
await this.resolveMaterialLinks(page, flat2, [], emptyAiSurfaces.aiOverview, emptyAiSurfaces.aiMode);
|
|
2881
3365
|
const stats2 = {
|
|
2882
3366
|
seed: executionOptions.query,
|
|
2883
3367
|
totalQuestions: flat2.length,
|
|
@@ -2894,7 +3378,9 @@ var PAAExtractor = class {
|
|
|
2894
3378
|
completionStatus: hasPaa ? "paa_found" : "no_paa",
|
|
2895
3379
|
...!hasPaa ? { noPaaObserved: true } : {},
|
|
2896
3380
|
problem: null,
|
|
3381
|
+
paaLifecycle: { ...this.paaLifecycle },
|
|
2897
3382
|
completeness: { ...this.completeness },
|
|
3383
|
+
links: { ...this.linkDiagnostics },
|
|
2898
3384
|
...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, void 0, locationResolution) } : {}
|
|
2899
3385
|
},
|
|
2900
3386
|
totalQuestions: flat2.length,
|
|
@@ -2937,6 +3423,7 @@ var PAAExtractor = class {
|
|
|
2937
3423
|
locationEvidence2 = inferSerpLocationEvidence(canonicalLocation, allOrganic2, localPack2);
|
|
2938
3424
|
}
|
|
2939
3425
|
}
|
|
3426
|
+
await this.resolveMaterialLinks(page, [], allOrganic2, aiSurfaces2.aiOverview, aiSurfaces2.aiMode);
|
|
2940
3427
|
const stats2 = {
|
|
2941
3428
|
seed: executionOptions.query,
|
|
2942
3429
|
totalQuestions: 0,
|
|
@@ -2953,6 +3440,7 @@ var PAAExtractor = class {
|
|
|
2953
3440
|
completionStatus: "serp_only",
|
|
2954
3441
|
problem: null,
|
|
2955
3442
|
completeness: { ...this.completeness },
|
|
3443
|
+
links: { ...this.linkDiagnostics },
|
|
2956
3444
|
...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence2, locationResolution) } : {}
|
|
2957
3445
|
},
|
|
2958
3446
|
totalQuestions: 0,
|
|
@@ -2998,6 +3486,7 @@ var PAAExtractor = class {
|
|
|
2998
3486
|
}
|
|
2999
3487
|
}
|
|
3000
3488
|
const aiSurfaces2 = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
|
|
3489
|
+
await this.resolveMaterialLinks(page, [], noPaaOrganic, aiSurfaces2.aiOverview, aiSurfaces2.aiMode);
|
|
3001
3490
|
const stats2 = {
|
|
3002
3491
|
seed: executionOptions.query,
|
|
3003
3492
|
totalQuestions: 0,
|
|
@@ -3014,7 +3503,9 @@ var PAAExtractor = class {
|
|
|
3014
3503
|
completionStatus: "no_paa",
|
|
3015
3504
|
noPaaObserved: true,
|
|
3016
3505
|
problem: null,
|
|
3506
|
+
paaLifecycle: { ...this.paaLifecycle },
|
|
3017
3507
|
completeness: { ...this.completeness },
|
|
3508
|
+
links: { ...this.linkDiagnostics },
|
|
3018
3509
|
...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence2, locationResolution) } : {}
|
|
3019
3510
|
},
|
|
3020
3511
|
totalQuestions: 0,
|
|
@@ -3032,9 +3523,21 @@ var PAAExtractor = class {
|
|
|
3032
3523
|
stats: stats2
|
|
3033
3524
|
};
|
|
3034
3525
|
}
|
|
3526
|
+
const aiSurfaces = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
|
|
3527
|
+
this.checkpointMaterial = {
|
|
3528
|
+
surface: aiSurfaces.surface,
|
|
3529
|
+
aiOverview: aiSurfaces.aiOverview,
|
|
3530
|
+
aiMode: aiSurfaces.aiMode,
|
|
3531
|
+
whatPeopleSaying,
|
|
3532
|
+
videos,
|
|
3533
|
+
forums,
|
|
3534
|
+
organicResults,
|
|
3535
|
+
localPack,
|
|
3536
|
+
entityIds
|
|
3537
|
+
};
|
|
3538
|
+
await this.emitProgress("serp_captured");
|
|
3035
3539
|
const flat = await this.runBFS(page, executionOptions, signal);
|
|
3036
3540
|
this.throwIfAborted(signal);
|
|
3037
|
-
const aiSurfaces = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
|
|
3038
3541
|
const shortVidsParams = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", udm: ShortVideoSelectors.udm });
|
|
3039
3542
|
if (uule) shortVidsParams.set("uule", uule);
|
|
3040
3543
|
let shortVideos = [];
|
|
@@ -3067,6 +3570,15 @@ var PAAExtractor = class {
|
|
|
3067
3570
|
locationEvidence = inferSerpLocationEvidence(canonicalLocation, allOrganic, localPack);
|
|
3068
3571
|
}
|
|
3069
3572
|
}
|
|
3573
|
+
await this.resolveMaterialLinks(page, flat, allOrganic, aiSurfaces.aiOverview, aiSurfaces.aiMode);
|
|
3574
|
+
this.checkpointMaterial = {
|
|
3575
|
+
...this.checkpointMaterial,
|
|
3576
|
+
surface: aiSurfaces.surface,
|
|
3577
|
+
aiOverview: aiSurfaces.aiOverview,
|
|
3578
|
+
aiMode: aiSurfaces.aiMode,
|
|
3579
|
+
videos: [...videos, ...shortVideos],
|
|
3580
|
+
organicResults: allOrganic
|
|
3581
|
+
};
|
|
3070
3582
|
const allVideos = [...videos, ...shortVideos];
|
|
3071
3583
|
const tree = this.buildTree(flat, executionOptions.query);
|
|
3072
3584
|
const stats = {
|
|
@@ -3084,7 +3596,9 @@ var PAAExtractor = class {
|
|
|
3084
3596
|
diagnostics: {
|
|
3085
3597
|
completionStatus: "paa_found",
|
|
3086
3598
|
problem: null,
|
|
3599
|
+
paaLifecycle: { ...this.paaLifecycle },
|
|
3087
3600
|
completeness: { ...this.completeness },
|
|
3601
|
+
links: { ...this.linkDiagnostics },
|
|
3088
3602
|
...diagnosticWarnings.length > 0 ? { warnings: diagnosticWarnings } : {},
|
|
3089
3603
|
...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence, locationResolution) } : {}
|
|
3090
3604
|
},
|
|
@@ -3104,6 +3618,13 @@ var PAAExtractor = class {
|
|
|
3104
3618
|
};
|
|
3105
3619
|
} catch (err) {
|
|
3106
3620
|
errorCount++;
|
|
3621
|
+
if (this.paaLifecycle.discoveryStatus === "in_progress") {
|
|
3622
|
+
this.paaLifecycle = {
|
|
3623
|
+
...this.paaLifecycle,
|
|
3624
|
+
discoveryStatus: "interrupted",
|
|
3625
|
+
completionReason: "The browser attempt ended before frontier exhaustion could be proven."
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3107
3628
|
await this.emitProgress("attempt_ending").catch(() => {
|
|
3108
3629
|
});
|
|
3109
3630
|
this.reporter.onError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -3130,7 +3651,10 @@ var OutputSerializer = class {
|
|
|
3130
3651
|
await fs.mkdir(outputDir, { recursive: true });
|
|
3131
3652
|
const seedRaw = rows[0]?.seed_query ?? "paa";
|
|
3132
3653
|
const slug = seedRaw.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
3133
|
-
const csv = Papa.unparse(rows
|
|
3654
|
+
const csv = Papa.unparse(rows.map(({ sources, ...row }) => ({
|
|
3655
|
+
...row,
|
|
3656
|
+
sources_json: JSON.stringify(sources)
|
|
3657
|
+
})), { header: true });
|
|
3134
3658
|
const filename = `${slug}-${Date.now()}.csv`;
|
|
3135
3659
|
const fullPath = path.join(outputDir, filename);
|
|
3136
3660
|
await fs.writeFile(fullPath, csv, "utf8");
|
|
@@ -3810,6 +4334,7 @@ function classifyAttemptError(err) {
|
|
|
3810
4334
|
if (err instanceof CaptchaError) return "captcha";
|
|
3811
4335
|
if (err instanceof LocationMismatchError) return "location_mismatch";
|
|
3812
4336
|
if (err instanceof RequestAbortedError) return "request_aborted";
|
|
4337
|
+
if (err instanceof PaaInteractionInterruptedError) return "browser_session_interrupted";
|
|
3813
4338
|
if (err instanceof DOMException && (err.name === "TimeoutError" || err.name === "AbortError")) return "timeout";
|
|
3814
4339
|
const message = err instanceof Error ? err.message : String(err);
|
|
3815
4340
|
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)) {
|
|
@@ -3834,7 +4359,9 @@ function withResultQuality(result, requestedPaaCount, questionsOnly) {
|
|
|
3834
4359
|
const paaWithoutSource = questionsOnly ? 0 : result.flat.filter((row) => !row.source_cite?.trim()).length;
|
|
3835
4360
|
const noPaa = result.diagnostics.completionStatus === "no_paa" && (result.diagnostics.noPaaObserved === true || hasUsableContent);
|
|
3836
4361
|
const hasPaa = result.diagnostics.completionStatus === "paa_found";
|
|
3837
|
-
const
|
|
4362
|
+
const discoveryStatus = result.diagnostics.paaLifecycle?.discoveryStatus;
|
|
4363
|
+
const discoveryIncomplete = discoveryStatus === "interrupted" || discoveryStatus === "recovery_exhausted";
|
|
4364
|
+
const paaIncomplete = hasPaa && (uniquePaaCount < requestedPaaCount && discoveryStatus !== "frontier_exhausted" || paaWithoutAnswer > 0 || paaWithoutSource > 0 || Boolean(result.diagnostics.interruption) || discoveryIncomplete);
|
|
3838
4365
|
const resultQuality = noPaa ? "complete" : hasPaa && uniquePaaCount > 0 ? paaIncomplete || hasWarnings ? "partial" : "complete" : hasUsableContent ? hasWarnings ? "partial" : "complete" : "degraded";
|
|
3839
4366
|
const degradedResult = resultQuality === "degraded";
|
|
3840
4367
|
result.diagnostics = {
|
|
@@ -3851,7 +4378,15 @@ function withResultQuality(result, requestedPaaCount, questionsOnly) {
|
|
|
3851
4378
|
paaRequested: requestedPaaCount,
|
|
3852
4379
|
paaReturned: result.flat.length,
|
|
3853
4380
|
paaUnique: uniquePaaCount
|
|
3854
|
-
}
|
|
4381
|
+
},
|
|
4382
|
+
...result.diagnostics.paaLifecycle ? {
|
|
4383
|
+
paaLifecycle: {
|
|
4384
|
+
...result.diagnostics.paaLifecycle,
|
|
4385
|
+
questionsFound: uniquePaaCount,
|
|
4386
|
+
targetStatus: uniquePaaCount >= requestedPaaCount ? "met" : "not_met",
|
|
4387
|
+
materialStatus: paaWithoutAnswer > 0 || paaWithoutSource > 0 ? "partial" : "complete"
|
|
4388
|
+
}
|
|
4389
|
+
} : {}
|
|
3855
4390
|
};
|
|
3856
4391
|
return result;
|
|
3857
4392
|
}
|
|
@@ -4258,6 +4793,8 @@ export {
|
|
|
4258
4793
|
BrowserDriver,
|
|
4259
4794
|
validateProviderSessionId,
|
|
4260
4795
|
brightDataSerpEnabled,
|
|
4796
|
+
questionIdFor,
|
|
4797
|
+
classifyGoogleOutboundLink,
|
|
4261
4798
|
BACKUP_LAST_RESORT_ATTEMPTS,
|
|
4262
4799
|
backupProxyAvailable,
|
|
4263
4800
|
createBackupProxyIdSafe,
|