mcp-scraper 0.79.1 → 0.79.3
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 +25 -1
- package/README.md +1 -1
- package/dist/{analytics-repository-VEUDEPWI.js → analytics-repository-YOY5KMOD.js} +3 -3
- package/dist/bin/api-server.js +4 -4
- package/dist/bin/mcp-scraper-cli.js +1 -1
- 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 +3 -3
- package/dist/{chunk-LB2GNACG.js → chunk-3423R23T.js} +3 -3
- package/dist/{chunk-NGLZ6UJR.js → chunk-5UAFBKJR.js} +1 -1
- package/dist/{chunk-B6L73AO4.js → chunk-6HAV7LCE.js} +3 -1
- package/dist/{chunk-NH3EUCZL.js → chunk-7UZAVM4J.js} +138 -11
- package/dist/{chunk-Q5L2OCOQ.js → chunk-C5Z4OFKW.js} +1 -1
- package/dist/{chunk-KAGHJQFO.js → chunk-EQGTEHLZ.js} +1 -1
- package/dist/{chunk-BTQ23KCT.js → chunk-F5GQJWZU.js} +1 -1
- package/dist/{chunk-DUWZQSSJ.js → chunk-HEUMDX4I.js} +1 -1
- package/dist/{chunk-OIGM6VKQ.js → chunk-IHYG52ZW.js} +1 -1
- package/dist/{chunk-OUBH6XMY.js → chunk-K3Z5AQYE.js} +1 -1
- package/dist/{chunk-UFJJGXVO.js → chunk-KNGTQD6U.js} +396 -67
- package/dist/{chunk-R4YRH5PG.js → chunk-MVB25X55.js} +1 -1
- package/dist/{chunk-47ZCUBM5.js → chunk-NVUKO5NN.js} +12 -1
- package/dist/{chunk-QVW5S6BD.js → chunk-TCYZ7HMU.js} +3 -3
- package/dist/{chunk-SL7YVA4S.js → chunk-YBR32LOD.js} +1 -1
- package/dist/{chunk-OU4FV2RP.js → chunk-YXNDOQXN.js} +49 -0
- package/dist/{db-TA567JLA.js → db-Z34LPZNR.js} +5 -1
- package/dist/{extract-bundle-I2GNC25L.js → extract-bundle-S3ZGNFML.js} +3 -3
- package/dist/{gmail-service-GGQVJUC3.js → gmail-service-YFS2OV4S.js} +4 -4
- package/dist/index.cjs +394 -65
- package/dist/index.d.cts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +3 -3
- package/dist/{lead-list-enrichment-repository-5LEWW4X7.js → lead-list-enrichment-repository-36RPVV6N.js} +2 -2
- package/dist/{location-data-repository-AHKDXTEQ.js → location-data-repository-WPRG62GE.js} +2 -2
- package/dist/{server-MNSYIX42.js → server-MDWFOUTS.js} +73 -28
- package/dist/{site-extract-repository-KF736IR6.js → site-extract-repository-WAJMIDPJ.js} +2 -2
- package/dist/{worker-VU6JCCLG.js → worker-3LUFQWWJ.js} +5 -5
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2598,6 +2598,93 @@ async function resolveGoogleGotoUrls(rawUrls, options = {}) {
|
|
|
2598
2598
|
await Promise.all(Array.from({ length: Math.min(concurrency, inputs.length) }, () => worker()));
|
|
2599
2599
|
return { resolutions: [...records.values()], nonGoogleRequestsObserved: 0 };
|
|
2600
2600
|
}
|
|
2601
|
+
function resolvedSite(link, fallback) {
|
|
2602
|
+
try {
|
|
2603
|
+
return new URL(link.resolvedUrl ?? link.url).hostname.replace(/^www\./, "");
|
|
2604
|
+
} catch {
|
|
2605
|
+
return fallback;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
async function resolveHarvestResultGoogleGotoLinks(result, options = {}) {
|
|
2609
|
+
const treeNodes = [];
|
|
2610
|
+
const visit = (nodes) => {
|
|
2611
|
+
for (const node of nodes) {
|
|
2612
|
+
treeNodes.push(node);
|
|
2613
|
+
visit(node.children ?? []);
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
visit(result.tree ?? []);
|
|
2617
|
+
const candidates = [
|
|
2618
|
+
...result.flat.flatMap((row) => row.sources ?? []),
|
|
2619
|
+
...treeNodes.flatMap((node) => node.sources),
|
|
2620
|
+
...result.organicResults ?? [],
|
|
2621
|
+
...(result.aiOverview?.citations ?? []).map(({ text: _text, href, ...link }) => ({ ...link, url: href })),
|
|
2622
|
+
...(result.aiMode?.citations ?? []).map(({ text: _text, href, ...link }) => ({ ...link, url: href }))
|
|
2623
|
+
];
|
|
2624
|
+
const unique = new Map(candidates.filter((link) => Boolean(link?.rawUrl)).map((link) => [link.rawUrl, link]));
|
|
2625
|
+
const gotoUrls = [...unique.values()].filter((link) => link.linkType === "google_goto_redirect" && link.resolutionStatus !== "resolved").map((link) => link.rawUrl);
|
|
2626
|
+
if (gotoUrls.length > 0) {
|
|
2627
|
+
const batch = await resolveGoogleGotoUrls(gotoUrls, options);
|
|
2628
|
+
for (const resolution2 of batch.resolutions) {
|
|
2629
|
+
const link = unique.get(resolution2.rawUrl);
|
|
2630
|
+
if (!link) continue;
|
|
2631
|
+
unique.set(resolution2.rawUrl, resolution2.resolutionStatus === "resolved" && resolution2.resolvedUrl ? { ...link, url: resolution2.resolvedUrl, resolvedUrl: resolution2.resolvedUrl, resolutionStatus: "resolved" } : { ...link, resolutionStatus: resolution2.resolutionStatus });
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
const replace = (link) => ({
|
|
2635
|
+
...link,
|
|
2636
|
+
...unique.get(link.rawUrl) ?? link
|
|
2637
|
+
});
|
|
2638
|
+
const replaceSources = (sources) => sources.map((source) => {
|
|
2639
|
+
const link = replace(source);
|
|
2640
|
+
return { ...link, site: resolvedSite(link, source.site) };
|
|
2641
|
+
});
|
|
2642
|
+
for (const row of result.flat) {
|
|
2643
|
+
row.sources = replaceSources(row.sources ?? []);
|
|
2644
|
+
const primary = row.sources[0];
|
|
2645
|
+
row.source_title = primary?.title ?? row.source_title;
|
|
2646
|
+
row.source_site = primary?.site ?? row.source_site;
|
|
2647
|
+
row.source_cite = primary?.url ?? row.source_cite;
|
|
2648
|
+
}
|
|
2649
|
+
for (const node of treeNodes) {
|
|
2650
|
+
node.sources = replaceSources(node.sources ?? []);
|
|
2651
|
+
const primary = node.sources[0];
|
|
2652
|
+
node.sourceTitle = primary?.title ?? node.sourceTitle;
|
|
2653
|
+
node.sourceSite = primary?.site ?? node.sourceSite;
|
|
2654
|
+
node.sourceCite = primary?.url ?? node.sourceCite;
|
|
2655
|
+
}
|
|
2656
|
+
for (let index = 0; index < (result.organicResults?.length ?? 0); index++) {
|
|
2657
|
+
const link = replace(result.organicResults[index]);
|
|
2658
|
+
result.organicResults[index] = { ...link, domain: resolvedSite(link, result.organicResults[index].domain) ?? "" };
|
|
2659
|
+
}
|
|
2660
|
+
const replaceCitation = (citation) => {
|
|
2661
|
+
const link = replace({
|
|
2662
|
+
url: citation.href,
|
|
2663
|
+
rawUrl: citation.rawUrl,
|
|
2664
|
+
resolvedUrl: citation.resolvedUrl,
|
|
2665
|
+
linkType: citation.linkType,
|
|
2666
|
+
resolutionStatus: citation.resolutionStatus
|
|
2667
|
+
});
|
|
2668
|
+
const { url, ...metadata } = link;
|
|
2669
|
+
return { ...citation, ...metadata, href: url };
|
|
2670
|
+
};
|
|
2671
|
+
if (result.aiOverview) result.aiOverview.citations = (result.aiOverview.citations ?? []).map(replaceCitation);
|
|
2672
|
+
if (result.aiMode) result.aiMode.citations = (result.aiMode.citations ?? []).map(replaceCitation);
|
|
2673
|
+
const values = [...unique.values()];
|
|
2674
|
+
result.diagnostics.links = {
|
|
2675
|
+
linkCandidatesObserved: unique.size,
|
|
2676
|
+
plainLinksObserved: values.filter((link) => link.linkType === "plain").length,
|
|
2677
|
+
googleUrlRedirectsObserved: values.filter((link) => link.linkType === "google_url_redirect").length,
|
|
2678
|
+
googleGotoRedirectsObserved: values.filter((link) => link.linkType === "google_goto_redirect").length,
|
|
2679
|
+
redirectsResolved: values.filter((link) => link.linkType !== "plain" && link.resolutionStatus === "resolved").length,
|
|
2680
|
+
redirectsUnresolved: values.filter((link) => link.resolutionStatus === "unresolved").length,
|
|
2681
|
+
redirectsRejected: values.filter((link) => link.resolutionStatus === "rejected").length,
|
|
2682
|
+
resolverNonGoogleRequestsObserved: 0,
|
|
2683
|
+
paaSourcesPreserved: result.flat.reduce((total, row) => total + row.sources.length, 0),
|
|
2684
|
+
aiOverviewLabelValidated: result.aiOverview?.detected ?? false
|
|
2685
|
+
};
|
|
2686
|
+
return result;
|
|
2687
|
+
}
|
|
2601
2688
|
async function resolveGoogleOutboundLinks(page, values, options = {}) {
|
|
2602
2689
|
const byRawUrl = /* @__PURE__ */ new Map();
|
|
2603
2690
|
for (const value of values) byRawUrl.set(value.rawUrl, value);
|
|
@@ -2718,15 +2805,24 @@ async function resolveGoogleOutboundLinks(page, values, options = {}) {
|
|
|
2718
2805
|
}
|
|
2719
2806
|
|
|
2720
2807
|
// src/extractor/PAAExtractor.ts
|
|
2808
|
+
var PAA_ANSWER_PLACEHOLDER_PATTERN = String.raw`^(?:an error has occurred(?:[.!…]\s*)?(?:please try again later[.!…]?)?|loading|please wait|try again|something went wrong)[.!…\s]*$`;
|
|
2809
|
+
var PAA_CLICK_EARLIEST_CONFIRMATION_MS = 700;
|
|
2810
|
+
var PAA_CLICK_MID_CONFIRMATION_MS = 1e3;
|
|
2811
|
+
var PAA_CLICK_ATTEMPT_BUDGET_MS = 1400;
|
|
2812
|
+
var PAA_CLICK_DISPATCH_TIMEOUT_MS = 900;
|
|
2813
|
+
var PAA_FRONTIER_BATCH_SIZE = 3;
|
|
2814
|
+
var MAX_RETAINED_INTERACTION_OBSERVATIONS = 250;
|
|
2721
2815
|
var PAAExtractor = class {
|
|
2722
|
-
constructor(driver, reporter, onPaaProgress) {
|
|
2816
|
+
constructor(driver, reporter, onPaaProgress, onProviderSession) {
|
|
2723
2817
|
this.driver = driver;
|
|
2724
2818
|
this.reporter = reporter;
|
|
2725
2819
|
this.onPaaProgress = onPaaProgress;
|
|
2820
|
+
this.onProviderSession = onProviderSession;
|
|
2726
2821
|
}
|
|
2727
2822
|
driver;
|
|
2728
2823
|
reporter;
|
|
2729
2824
|
onPaaProgress;
|
|
2825
|
+
onProviderSession;
|
|
2730
2826
|
completeness = {
|
|
2731
2827
|
paaWithoutAnswer: 0,
|
|
2732
2828
|
paaWithoutSource: 0,
|
|
@@ -2741,6 +2837,7 @@ var PAAExtractor = class {
|
|
|
2741
2837
|
clickedQuestions = /* @__PURE__ */ new Set();
|
|
2742
2838
|
paaLifecycle = this.emptyPaaLifecycle(0);
|
|
2743
2839
|
checkpointMaterial = null;
|
|
2840
|
+
interactionObservations = [];
|
|
2744
2841
|
emptyPaaLifecycle(requestedQuestions) {
|
|
2745
2842
|
return {
|
|
2746
2843
|
requestedQuestions,
|
|
@@ -2753,7 +2850,12 @@ var PAAExtractor = class {
|
|
|
2753
2850
|
knownQuestions: 0,
|
|
2754
2851
|
processedQuestions: 0,
|
|
2755
2852
|
failedInteractions: 0,
|
|
2756
|
-
noGrowthConfirmations: 0
|
|
2853
|
+
noGrowthConfirmations: 0,
|
|
2854
|
+
visibleControls: 0,
|
|
2855
|
+
eligibleControls: 0,
|
|
2856
|
+
dispatchedInteractions: 0,
|
|
2857
|
+
confirmedInteractions: 0,
|
|
2858
|
+
unconfirmedInteractions: 0
|
|
2757
2859
|
};
|
|
2758
2860
|
}
|
|
2759
2861
|
emptyLinkDiagnostics() {
|
|
@@ -2862,7 +2964,7 @@ var PAAExtractor = class {
|
|
|
2862
2964
|
}
|
|
2863
2965
|
async extractVisibleItems(page, onlyQuestion) {
|
|
2864
2966
|
const sels = PAASelectors;
|
|
2865
|
-
const raw = await page.evaluate(({ selectors, onlyQuestion: onlyQuestion2 }) => {
|
|
2967
|
+
const raw = await page.evaluate(({ selectors, onlyQuestion: onlyQuestion2, answerPlaceholderPattern }) => {
|
|
2866
2968
|
function cleanText(el) {
|
|
2867
2969
|
if (!el) return "";
|
|
2868
2970
|
const parts = [];
|
|
@@ -2889,12 +2991,13 @@ var PAAExtractor = class {
|
|
|
2889
2991
|
const aiAnswerBody = answerContainer?.querySelector(selectors.aiAnswerBody);
|
|
2890
2992
|
const answerElement = aiAnswerBody ?? answerContainer;
|
|
2891
2993
|
const answerText = cleanText(answerElement);
|
|
2994
|
+
const answerIsPlaceholder = new RegExp(answerPlaceholderPattern, "i").test(answerText);
|
|
2892
2995
|
const citationCandidates = [
|
|
2893
2996
|
...Array.from((answerElement ?? pair).querySelectorAll("a[href]")),
|
|
2894
2997
|
...Array.from(pair.querySelectorAll(selectors.aiCitation)),
|
|
2895
2998
|
...Array.from(pair.querySelectorAll(selectors.aiSourceCard))
|
|
2896
2999
|
];
|
|
2897
|
-
const hasLoadedAnswer = expanded
|
|
3000
|
+
const hasLoadedAnswer = !answerIsPlaceholder && (expanded && answerText.length >= 10 || answerText.length > 30 && citationCandidates.some((anchor) => /^https?:\/\//.test(anchor.href)));
|
|
2898
3001
|
let sourceTitle = hasLoadedAnswer ? pair.querySelector(selectors.sourceTitle)?.innerText?.trim() || void 0 : void 0;
|
|
2899
3002
|
let sourceSite = hasLoadedAnswer ? pair.querySelector(selectors.sourceSite)?.innerText?.trim() || void 0 : void 0;
|
|
2900
3003
|
const sources = [];
|
|
@@ -2918,7 +3021,7 @@ var PAAExtractor = class {
|
|
|
2918
3021
|
sources
|
|
2919
3022
|
};
|
|
2920
3023
|
});
|
|
2921
|
-
}, { selectors: sels, onlyQuestion });
|
|
3024
|
+
}, { selectors: sels, onlyQuestion, answerPlaceholderPattern: PAA_ANSWER_PLACEHOLDER_PATTERN });
|
|
2922
3025
|
const baseUrl = typeof page.url === "function" ? page.url() : "https://www.google.com/";
|
|
2923
3026
|
return raw.flatMap((item) => {
|
|
2924
3027
|
const sources = [];
|
|
@@ -3014,7 +3117,11 @@ var PAAExtractor = class {
|
|
|
3014
3117
|
phase,
|
|
3015
3118
|
records: this.collectedRows.map((row) => ({ ...row })),
|
|
3016
3119
|
lifecycle: { ...this.paaLifecycle },
|
|
3017
|
-
...this.checkpointMaterial ? { material: structuredClone(this.checkpointMaterial) } : {}
|
|
3120
|
+
...this.checkpointMaterial ? { material: structuredClone(this.checkpointMaterial) } : {},
|
|
3121
|
+
interactions: this.interactionObservations.map((observation) => ({
|
|
3122
|
+
...observation,
|
|
3123
|
+
confirmationSignals: [...observation.confirmationSignals]
|
|
3124
|
+
}))
|
|
3018
3125
|
};
|
|
3019
3126
|
try {
|
|
3020
3127
|
await this.onPaaProgress(snapshot);
|
|
@@ -3095,6 +3202,7 @@ var PAAExtractor = class {
|
|
|
3095
3202
|
const seenQs = /* @__PURE__ */ new Set();
|
|
3096
3203
|
const orderedQs = [];
|
|
3097
3204
|
this.clickedQuestions = /* @__PURE__ */ new Set();
|
|
3205
|
+
this.interactionObservations = [];
|
|
3098
3206
|
const abandonedNeverReclick = /* @__PURE__ */ new Set();
|
|
3099
3207
|
const clickAttempts = /* @__PURE__ */ new Map();
|
|
3100
3208
|
const processedQuestions = /* @__PURE__ */ new Set();
|
|
@@ -3102,6 +3210,7 @@ var PAAExtractor = class {
|
|
|
3102
3210
|
const capturedItems = /* @__PURE__ */ new Map();
|
|
3103
3211
|
const MAX_CLICK_ATTEMPTS = 2;
|
|
3104
3212
|
const EXHAUSTION_CONFIRMATIONS = 3;
|
|
3213
|
+
let batchNumber = 0;
|
|
3105
3214
|
const itemArgs = {
|
|
3106
3215
|
sel: PAASelectors.item,
|
|
3107
3216
|
dataQ: PAASelectors.itemDataQ,
|
|
@@ -3109,46 +3218,171 @@ var PAAExtractor = class {
|
|
|
3109
3218
|
questionEl: PAASelectors.itemQuestionEl,
|
|
3110
3219
|
answerSel: PAASelectors.answerContainer,
|
|
3111
3220
|
expandedClass: PAASelectors.expandedClass,
|
|
3112
|
-
clickTarget: PAASelectors.clickTarget
|
|
3221
|
+
clickTarget: PAASelectors.clickTarget,
|
|
3222
|
+
answerPlaceholderPattern: PAA_ANSWER_PLACEHOLDER_PATTERN,
|
|
3223
|
+
questionsOnly: options.questionsOnly,
|
|
3224
|
+
sourceSel: "a[href]"
|
|
3113
3225
|
};
|
|
3114
3226
|
const readItemStates = () => page.evaluate(
|
|
3115
|
-
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget, answerSel }) => {
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3227
|
+
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget, answerSel, answerPlaceholderPattern, questionsOnly }) => {
|
|
3228
|
+
const cleanVisibleMaterial = (element) => {
|
|
3229
|
+
if (!element) return "";
|
|
3230
|
+
const parts = [];
|
|
3231
|
+
for (const node of element.childNodes) {
|
|
3232
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
3233
|
+
const text2 = node.textContent?.trim();
|
|
3234
|
+
if (text2) parts.push(text2);
|
|
3235
|
+
continue;
|
|
3236
|
+
}
|
|
3237
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
3238
|
+
const child = node;
|
|
3239
|
+
if (child.tagName === "STYLE" || child.tagName === "SCRIPT" || child.getAttribute("aria-hidden") === "true") continue;
|
|
3240
|
+
const text = cleanVisibleMaterial(child);
|
|
3241
|
+
if (text) parts.push(text);
|
|
3242
|
+
}
|
|
3243
|
+
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
3244
|
+
};
|
|
3245
|
+
return Array.from(document.querySelectorAll(sel)).map((el, index) => {
|
|
3246
|
+
const answerText = cleanVisibleMaterial(el.querySelector(answerSel)).replace(/An AI Overview is not available for this search/gi, " ").replace(/Can't generate an AI overview right now\.?\s*Try again later\.?/gi, " ").replace(/\bAI Overview\b/gi, " ").replace(/\s+/g, " ").trim();
|
|
3247
|
+
const expanded = el.classList.contains(expandedClass) || el.querySelector(clickTarget)?.getAttribute("aria-expanded") === "true";
|
|
3248
|
+
const answerReady = answerText.length >= 10 && !new RegExp(answerPlaceholderPattern, "i").test(answerText);
|
|
3249
|
+
return {
|
|
3250
|
+
q: el.getAttribute(dataQ) || el.getAttribute(dataInitQ) || el.querySelector(questionEl)?.innerText?.trim() || "",
|
|
3251
|
+
done: expanded && (questionsOnly || answerReady),
|
|
3252
|
+
expanded,
|
|
3253
|
+
index
|
|
3254
|
+
};
|
|
3255
|
+
}).filter((s) => s.q);
|
|
3120
3256
|
},
|
|
3121
3257
|
itemArgs
|
|
3122
3258
|
);
|
|
3123
|
-
const expandOneItemSerially = async (q, beforeCount) => {
|
|
3259
|
+
const expandOneItemSerially = async (q, beforeCount, itemIndex, wasExpanded, controlAttempt, batchNumber2) => {
|
|
3124
3260
|
const clickStartedMs = Date.now();
|
|
3125
|
-
const
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3261
|
+
const baseObservation = {
|
|
3262
|
+
id: `${batchNumber2}:${questionIdFor(q)}:${controlAttempt}`,
|
|
3263
|
+
questionId: questionIdFor(q),
|
|
3264
|
+
question: q,
|
|
3265
|
+
batchNumber: batchNumber2,
|
|
3266
|
+
controlAttempt,
|
|
3267
|
+
observedAt: new Date(clickStartedMs).toISOString(),
|
|
3268
|
+
beforeQuestionCount: beforeCount,
|
|
3269
|
+
expandedBefore: wasExpanded
|
|
3270
|
+
};
|
|
3271
|
+
const clickTarget = itemIndex >= 0 ? page.locator(itemArgs.sel).nth(itemIndex).locator(itemArgs.clickTarget) : null;
|
|
3272
|
+
let dispatchErrorCode = null;
|
|
3131
3273
|
const clicked = clickTarget ? await (async () => {
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3274
|
+
if (wasExpanded) {
|
|
3275
|
+
await clickTarget.click({ timeout: PAA_CLICK_DISPATCH_TIMEOUT_MS, force: true });
|
|
3276
|
+
await page.waitForTimeout(120);
|
|
3277
|
+
}
|
|
3278
|
+
await clickTarget.click({ timeout: PAA_CLICK_DISPATCH_TIMEOUT_MS, force: true });
|
|
3136
3279
|
return true;
|
|
3137
|
-
})().catch(() =>
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
const
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3280
|
+
})().catch((error) => {
|
|
3281
|
+
dispatchErrorCode = error instanceof Error && (error.name === "TimeoutError" || /timeout/i.test(error.message)) ? "click_ack_timeout" : "click_failed";
|
|
3282
|
+
return false;
|
|
3283
|
+
}) : false;
|
|
3284
|
+
const dispatchDurationMs = Date.now() - clickStartedMs;
|
|
3285
|
+
if (!clicked && dispatchErrorCode !== "click_ack_timeout") {
|
|
3286
|
+
const observation2 = {
|
|
3287
|
+
...baseObservation,
|
|
3288
|
+
dispatched: false,
|
|
3289
|
+
outcome: "dispatch_failed",
|
|
3290
|
+
confirmationSignals: [],
|
|
3291
|
+
confirmationCheckpointMs: null,
|
|
3292
|
+
confirmedAtMs: null,
|
|
3293
|
+
dispatchDurationMs,
|
|
3294
|
+
totalDurationMs: dispatchDurationMs,
|
|
3295
|
+
afterQuestionCount: beforeCount,
|
|
3296
|
+
newQuestionCount: 0,
|
|
3297
|
+
expandedAfter: wasExpanded,
|
|
3298
|
+
answerObserved: false,
|
|
3299
|
+
sourceCount: 0,
|
|
3300
|
+
errorCode: itemIndex < 0 ? "control_missing" : dispatchErrorCode ?? "click_failed"
|
|
3301
|
+
};
|
|
3302
|
+
return { dispatched: false, confirmed: false, observation: observation2 };
|
|
3303
|
+
}
|
|
3304
|
+
let latest = {
|
|
3305
|
+
exists: true,
|
|
3306
|
+
expanded: wasExpanded,
|
|
3307
|
+
answerReady: false,
|
|
3308
|
+
sourceCount: 0,
|
|
3309
|
+
questionCount: beforeCount
|
|
3310
|
+
};
|
|
3311
|
+
let signals = [];
|
|
3312
|
+
let confirmationCheckpointMs = null;
|
|
3313
|
+
let confirmedAtMs = null;
|
|
3314
|
+
let confirmed = false;
|
|
3315
|
+
for (const checkpointMs of [PAA_CLICK_EARLIEST_CONFIRMATION_MS, PAA_CLICK_MID_CONFIRMATION_MS, PAA_CLICK_ATTEMPT_BUDGET_MS]) {
|
|
3316
|
+
const remainingMs = clickStartedMs + checkpointMs - Date.now();
|
|
3317
|
+
if (remainingMs > 0) await page.waitForTimeout(remainingMs);
|
|
3318
|
+
latest = await page.evaluate(
|
|
3319
|
+
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget: clickTarget2, answerSel, answerPlaceholderPattern, sourceSel, target }) => {
|
|
3320
|
+
const cleanVisibleMaterial = (element) => {
|
|
3321
|
+
if (!element) return "";
|
|
3322
|
+
const parts = [];
|
|
3323
|
+
for (const node of element.childNodes) {
|
|
3324
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
3325
|
+
const text2 = node.textContent?.trim();
|
|
3326
|
+
if (text2) parts.push(text2);
|
|
3327
|
+
continue;
|
|
3328
|
+
}
|
|
3329
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
3330
|
+
const child = node;
|
|
3331
|
+
if (child.tagName === "STYLE" || child.tagName === "SCRIPT" || child.getAttribute("aria-hidden") === "true") continue;
|
|
3332
|
+
const text = cleanVisibleMaterial(child);
|
|
3333
|
+
if (text) parts.push(text);
|
|
3334
|
+
}
|
|
3335
|
+
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
3336
|
+
};
|
|
3337
|
+
const items = Array.from(document.querySelectorAll(sel));
|
|
3338
|
+
const item = items.find((element) => (element.getAttribute(dataQ) || element.getAttribute(dataInitQ) || element.querySelector(questionEl)?.innerText?.trim()) === target);
|
|
3339
|
+
if (!item) return { exists: false, expanded: false, answerReady: false, sourceCount: 0, questionCount: items.length };
|
|
3340
|
+
const expanded = item.classList.contains(expandedClass) || item.querySelector(clickTarget2)?.getAttribute("aria-expanded") === "true";
|
|
3341
|
+
const answerElement = item.querySelector(answerSel);
|
|
3342
|
+
const answerText = cleanVisibleMaterial(answerElement);
|
|
3343
|
+
const confirmationText = answerText.replace(/An AI Overview is not available for this search/gi, " ").replace(/Can't generate an AI overview right now\.?\s*Try again later\.?/gi, " ").replace(/\bAI Overview\b/gi, " ").replace(/\s+/g, " ").trim();
|
|
3344
|
+
const answerReady = confirmationText.length >= 10 && !new RegExp(answerPlaceholderPattern, "i").test(confirmationText);
|
|
3345
|
+
return {
|
|
3346
|
+
exists: true,
|
|
3347
|
+
expanded,
|
|
3348
|
+
answerReady,
|
|
3349
|
+
sourceCount: answerReady ? answerElement?.querySelectorAll(sourceSel).length ?? 0 : 0,
|
|
3350
|
+
questionCount: items.length
|
|
3351
|
+
};
|
|
3352
|
+
},
|
|
3353
|
+
{ ...itemArgs, target: q }
|
|
3354
|
+
).catch(() => latest);
|
|
3355
|
+
signals = [
|
|
3356
|
+
...latest.expanded ? ["expanded"] : [],
|
|
3357
|
+
...latest.answerReady ? ["answer"] : [],
|
|
3358
|
+
...latest.sourceCount > 0 ? ["source"] : [],
|
|
3359
|
+
...latest.questionCount > beforeCount ? ["question_growth"] : []
|
|
3360
|
+
];
|
|
3361
|
+
if (latest.answerReady || latest.sourceCount > 0 || latest.questionCount > beforeCount) {
|
|
3362
|
+
confirmed = true;
|
|
3363
|
+
confirmationCheckpointMs = checkpointMs;
|
|
3364
|
+
confirmedAtMs = Date.now() - clickStartedMs;
|
|
3365
|
+
break;
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
const totalDurationMs = Date.now() - clickStartedMs;
|
|
3369
|
+
const observation = {
|
|
3370
|
+
...baseObservation,
|
|
3371
|
+
dispatched: clicked,
|
|
3372
|
+
outcome: confirmed ? "confirmed" : clicked ? "unconfirmed" : "dispatch_failed",
|
|
3373
|
+
confirmationSignals: signals,
|
|
3374
|
+
confirmationCheckpointMs,
|
|
3375
|
+
confirmedAtMs,
|
|
3376
|
+
dispatchDurationMs,
|
|
3377
|
+
totalDurationMs,
|
|
3378
|
+
afterQuestionCount: latest.questionCount,
|
|
3379
|
+
newQuestionCount: Math.max(0, latest.questionCount - beforeCount),
|
|
3380
|
+
expandedAfter: latest.expanded,
|
|
3381
|
+
answerObserved: latest.answerReady,
|
|
3382
|
+
sourceCount: latest.sourceCount,
|
|
3383
|
+
errorCode: !clicked ? dispatchErrorCode : confirmed ? null : latest.exists ? "confirmation_timeout" : "control_missing"
|
|
3384
|
+
};
|
|
3385
|
+
return { dispatched: clicked, confirmed, observation };
|
|
3152
3386
|
};
|
|
3153
3387
|
let round = 0;
|
|
3154
3388
|
expansion: while (true) {
|
|
@@ -3179,7 +3413,8 @@ var PAAExtractor = class {
|
|
|
3179
3413
|
targetStatus: orderedQs.length >= options.maxQuestions ? "met" : "not_met",
|
|
3180
3414
|
knownQuestions: orderedQs.length,
|
|
3181
3415
|
processedQuestions: processedQuestions.size,
|
|
3182
|
-
failedInteractions: failedInteractions.size
|
|
3416
|
+
failedInteractions: failedInteractions.size,
|
|
3417
|
+
visibleControls: states.length
|
|
3183
3418
|
};
|
|
3184
3419
|
if (discovered) await this.emitProgress("discovered");
|
|
3185
3420
|
if (options.questionsOnly && seenQs.size >= options.maxQuestions) {
|
|
@@ -3191,10 +3426,44 @@ var PAAExtractor = class {
|
|
|
3191
3426
|
};
|
|
3192
3427
|
break;
|
|
3193
3428
|
}
|
|
3429
|
+
if (!options.questionsOnly && seenQs.size >= options.maxQuestions) {
|
|
3430
|
+
let materialChanged = false;
|
|
3431
|
+
const keptQuestions = new Set(orderedQs.slice(0, options.maxQuestions));
|
|
3432
|
+
for (const item of await this.extractVisibleItems(page)) {
|
|
3433
|
+
if (!keptQuestions.has(item.question)) continue;
|
|
3434
|
+
const existing = capturedItems.get(item.question);
|
|
3435
|
+
const merged = {
|
|
3436
|
+
...item,
|
|
3437
|
+
answer: item.answer ?? existing?.answer,
|
|
3438
|
+
sourceTitle: item.sourceTitle ?? existing?.sourceTitle,
|
|
3439
|
+
sourceSite: item.sourceSite ?? existing?.sourceSite,
|
|
3440
|
+
sourceCite: item.sourceCite ?? existing?.sourceCite,
|
|
3441
|
+
sources: this.mergePaaSources(existing?.sources, item.sources)
|
|
3442
|
+
};
|
|
3443
|
+
capturedItems.set(item.question, merged);
|
|
3444
|
+
materialChanged = this.upsertCollectedRow(this.toFlatRow(merged, 1, null, options.query)) || materialChanged;
|
|
3445
|
+
}
|
|
3446
|
+
if (materialChanged) await this.emitProgress("answer_captured");
|
|
3447
|
+
if ([...keptQuestions].every((question) => {
|
|
3448
|
+
const item = capturedItems.get(question);
|
|
3449
|
+
return Boolean(item?.answer?.trim() && item.sources.length > 0);
|
|
3450
|
+
})) {
|
|
3451
|
+
failedInteractions.clear();
|
|
3452
|
+
this.paaLifecycle = {
|
|
3453
|
+
...this.paaLifecycle,
|
|
3454
|
+
discoveryStatus: "target_reached",
|
|
3455
|
+
targetStatus: "met",
|
|
3456
|
+
failedInteractions: 0,
|
|
3457
|
+
completionReason: "The requested unique-question count and its answer/source material were observed."
|
|
3458
|
+
};
|
|
3459
|
+
break;
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3194
3462
|
const kept = new Set(orderedQs.slice(0, options.maxQuestions));
|
|
3195
3463
|
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
|
|
3197
|
-
|
|
3464
|
+
const targets = clickable.slice(-PAA_FRONTIER_BATCH_SIZE);
|
|
3465
|
+
this.paaLifecycle = { ...this.paaLifecycle, eligibleControls: clickable.length };
|
|
3466
|
+
if (targets.length === 0) {
|
|
3198
3467
|
if (seenQs.size >= options.maxQuestions) {
|
|
3199
3468
|
this.paaLifecycle = {
|
|
3200
3469
|
...this.paaLifecycle,
|
|
@@ -3242,37 +3511,64 @@ var PAAExtractor = class {
|
|
|
3242
3511
|
};
|
|
3243
3512
|
break;
|
|
3244
3513
|
}
|
|
3245
|
-
this.reporter.onDepth(++round);
|
|
3246
3514
|
await this.throwIfCaptcha(page, "Google PAA expansion");
|
|
3247
|
-
|
|
3248
|
-
const
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3515
|
+
batchNumber += 1;
|
|
3516
|
+
for (const target of targets) {
|
|
3517
|
+
this.reporter.onDepth(++round);
|
|
3518
|
+
if ((clickAttempts.get(target.q) ?? 0) > 0) {
|
|
3519
|
+
this.paaLifecycle = {
|
|
3520
|
+
...this.paaLifecycle,
|
|
3521
|
+
automaticRetries: this.paaLifecycle.automaticRetries + 1
|
|
3522
|
+
};
|
|
3255
3523
|
}
|
|
3256
|
-
|
|
3257
|
-
|
|
3524
|
+
clickAttempts.set(target.q, (clickAttempts.get(target.q) ?? 0) + 1);
|
|
3525
|
+
}
|
|
3526
|
+
const expansionResults = await Promise.all(targets.map(async (target) => ({
|
|
3527
|
+
target,
|
|
3528
|
+
result: await expandOneItemSerially(
|
|
3529
|
+
target.q,
|
|
3530
|
+
states.length,
|
|
3531
|
+
target.index,
|
|
3532
|
+
target.expanded,
|
|
3533
|
+
clickAttempts.get(target.q) ?? 1,
|
|
3534
|
+
batchNumber
|
|
3535
|
+
)
|
|
3536
|
+
})));
|
|
3537
|
+
for (const { target, result } of expansionResults) {
|
|
3538
|
+
if (result.dispatched || result.confirmed) this.clickedQuestions.add(target.q);
|
|
3539
|
+
this.interactionObservations.push(result.observation);
|
|
3540
|
+
}
|
|
3541
|
+
if (this.interactionObservations.length > MAX_RETAINED_INTERACTION_OBSERVATIONS) {
|
|
3542
|
+
this.interactionObservations.splice(0, this.interactionObservations.length - MAX_RETAINED_INTERACTION_OBSERVATIONS);
|
|
3543
|
+
}
|
|
3544
|
+
if (!options.questionsOnly) {
|
|
3545
|
+
const targetQuestions = new Set(targets.map((target) => target.q));
|
|
3546
|
+
for (const clickedItem of await this.extractVisibleItems(page)) {
|
|
3547
|
+
if (!targetQuestions.has(clickedItem.question)) continue;
|
|
3548
|
+
capturedItems.set(clickedItem.question, clickedItem);
|
|
3258
3549
|
if (this.upsertCollectedRow(this.toFlatRow(clickedItem, 1, null, options.query))) {
|
|
3259
3550
|
await this.emitProgress("answer_captured");
|
|
3260
3551
|
}
|
|
3261
3552
|
}
|
|
3262
3553
|
}
|
|
3263
|
-
const
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3554
|
+
for (const { target, result } of expansionResults) {
|
|
3555
|
+
const captured = capturedItems.get(target.q);
|
|
3556
|
+
if (result.confirmed || Boolean(captured?.answer?.trim())) {
|
|
3557
|
+
processedQuestions.add(target.q);
|
|
3558
|
+
failedInteractions.delete(target.q);
|
|
3559
|
+
} else if ((clickAttempts.get(target.q) ?? 0) >= MAX_CLICK_ATTEMPTS) {
|
|
3560
|
+
failedInteractions.add(target.q);
|
|
3561
|
+
abandonedNeverReclick.add(target.q);
|
|
3562
|
+
}
|
|
3270
3563
|
}
|
|
3271
3564
|
this.paaLifecycle = {
|
|
3272
3565
|
...this.paaLifecycle,
|
|
3273
3566
|
processedQuestions: processedQuestions.size,
|
|
3274
3567
|
failedInteractions: failedInteractions.size,
|
|
3275
|
-
noGrowthConfirmations: 0
|
|
3568
|
+
noGrowthConfirmations: 0,
|
|
3569
|
+
dispatchedInteractions: this.interactionObservations.filter((value) => value.dispatched).length,
|
|
3570
|
+
confirmedInteractions: this.interactionObservations.filter((value) => value.outcome === "confirmed").length,
|
|
3571
|
+
unconfirmedInteractions: this.interactionObservations.filter((value) => value.outcome !== "confirmed").length
|
|
3276
3572
|
};
|
|
3277
3573
|
await this.emitProgress("click_observed");
|
|
3278
3574
|
}
|
|
@@ -3866,6 +4162,18 @@ var PAAExtractor = class {
|
|
|
3866
4162
|
try {
|
|
3867
4163
|
this.throwIfAborted(signal);
|
|
3868
4164
|
await this.driver.launch(config);
|
|
4165
|
+
const providerSession = this.driver.getProviderSessionMetadata?.();
|
|
4166
|
+
if (providerSession?.providerSessionId && this.onProviderSession) {
|
|
4167
|
+
try {
|
|
4168
|
+
await this.onProviderSession(providerSession);
|
|
4169
|
+
} catch (err) {
|
|
4170
|
+
console.warn(JSON.stringify({
|
|
4171
|
+
event: "paa_provider_session_sink_failed",
|
|
4172
|
+
provider: providerSession.provider,
|
|
4173
|
+
message: err instanceof Error ? err.message : String(err)
|
|
4174
|
+
}));
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
3869
4177
|
this.throwIfAborted(signal);
|
|
3870
4178
|
const locationResolution = options.location ? resolveLocation(options.location) : null;
|
|
3871
4179
|
const canonicalLocation = options.location ? locationResolution?.canonical ?? options.location : null;
|
|
@@ -4970,6 +5278,17 @@ function stripInternalDebug(result, keepDebug) {
|
|
|
4970
5278
|
delete diagnostics.debug;
|
|
4971
5279
|
return { ...result, diagnostics };
|
|
4972
5280
|
}
|
|
5281
|
+
async function resolveSurvivingGotoLinks(result) {
|
|
5282
|
+
try {
|
|
5283
|
+
return await resolveHarvestResultGoogleGotoLinks(result, { totalBudgetMs: 1e4 });
|
|
5284
|
+
} catch (error) {
|
|
5285
|
+
console.warn(JSON.stringify({
|
|
5286
|
+
event: "harvest_goto_resolution_failed",
|
|
5287
|
+
message: errorMessage(error)
|
|
5288
|
+
}));
|
|
5289
|
+
return result;
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
4973
5292
|
async function cleanupDisposableProxy(kernelApiKey, proxyId) {
|
|
4974
5293
|
if (!kernelApiKey || !proxyId) return;
|
|
4975
5294
|
try {
|
|
@@ -4986,10 +5305,10 @@ async function cleanupDisposableProxy(kernelApiKey, proxyId) {
|
|
|
4986
5305
|
}));
|
|
4987
5306
|
}
|
|
4988
5307
|
}
|
|
4989
|
-
async function extractOnce(options, signal, forceManagedSerp = false, onPaaProgress) {
|
|
5308
|
+
async function extractOnce(options, signal, forceManagedSerp = false, onPaaProgress, onProviderSession) {
|
|
4990
5309
|
const driver = createSerpDriver(options, { forceManaged: forceManagedSerp });
|
|
4991
5310
|
const reporter = new ProgressReporter();
|
|
4992
|
-
const extractor = new PAAExtractor(driver, reporter, onPaaProgress);
|
|
5311
|
+
const extractor = new PAAExtractor(driver, reporter, onPaaProgress, onProviderSession);
|
|
4993
5312
|
if (signal?.aborted) {
|
|
4994
5313
|
return {
|
|
4995
5314
|
result: null,
|
|
@@ -5134,12 +5453,20 @@ async function harvest(rawOptions) {
|
|
|
5134
5453
|
max_questions: attemptOptions.maxQuestions
|
|
5135
5454
|
}));
|
|
5136
5455
|
const baseCtx = currentCostContext();
|
|
5456
|
+
const onProviderSession = (metadata) => emitAttemptEvent(onAttemptEvent, {
|
|
5457
|
+
type: "provider_session_started",
|
|
5458
|
+
attemptNumber,
|
|
5459
|
+
maxAttempts,
|
|
5460
|
+
provider: metadata.provider,
|
|
5461
|
+
providerSessionId: metadata.providerSessionId,
|
|
5462
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5463
|
+
});
|
|
5137
5464
|
const allowHeadlessTest = baseCtx?.harvestBrowserMode === "headless-test" && process.env[HEADLESS_HARVEST_TEST_ENV] === "true";
|
|
5138
5465
|
const attempt = baseCtx ? await runWithCostContext({
|
|
5139
5466
|
...baseCtx,
|
|
5140
5467
|
forceHeadless: allowHeadlessTest,
|
|
5141
5468
|
forceHeadful: !allowHeadlessTest
|
|
5142
|
-
}, () => extractOnce(attemptOptions, signal, forceManagedSerp, onPaaProgress)) : await extractOnce(attemptOptions, signal, forceManagedSerp, onPaaProgress);
|
|
5469
|
+
}, () => extractOnce(attemptOptions, signal, forceManagedSerp, onPaaProgress, onProviderSession)) : await extractOnce(attemptOptions, signal, forceManagedSerp, onPaaProgress, onProviderSession);
|
|
5143
5470
|
if (attempt.error) {
|
|
5144
5471
|
const err = attempt.error;
|
|
5145
5472
|
const outcome2 = classifyAttemptError(err);
|
|
@@ -5184,7 +5511,8 @@ async function harvest(rawOptions) {
|
|
|
5184
5511
|
lastError = err;
|
|
5185
5512
|
if (willRetry2) continue;
|
|
5186
5513
|
if (attempt.result) {
|
|
5187
|
-
|
|
5514
|
+
const resolvedResult2 = await resolveSurvivingGotoLinks(attempt.result);
|
|
5515
|
+
return withResultQuality(stripInternalDebug(resolvedResult2, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
|
|
5188
5516
|
}
|
|
5189
5517
|
break;
|
|
5190
5518
|
}
|
|
@@ -5219,7 +5547,8 @@ async function harvest(rawOptions) {
|
|
|
5219
5547
|
if (willRetry2) continue;
|
|
5220
5548
|
break;
|
|
5221
5549
|
}
|
|
5222
|
-
const
|
|
5550
|
+
const resolvedResult = await resolveSurvivingGotoLinks(result);
|
|
5551
|
+
const finalResult = withResultQuality(stripInternalDebug(resolvedResult, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
|
|
5223
5552
|
const outcome = classifyAttemptResult(finalResult);
|
|
5224
5553
|
const willRetry = outcome === "degraded_result" && i < maxAttempts - 1;
|
|
5225
5554
|
const resultError = outcome === "degraded_result" ? degradedResultMessage(finalResult) : null;
|