mcp-scraper 0.79.1 → 0.79.2
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 +21 -1
- package/README.md +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-core.js +4 -4
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.js +4 -4
- package/dist/bin/paa-harvest.js +1 -1
- package/dist/{chunk-OIGM6VKQ.js → chunk-2RTELL6M.js} +1 -1
- package/dist/{chunk-R4YRH5PG.js → chunk-6FL7ZZCF.js} +1 -1
- package/dist/{chunk-NH3EUCZL.js → chunk-JECPQBLZ.js} +1 -1
- package/dist/{chunk-UFJJGXVO.js → chunk-WEEAXPWK.js} +209 -51
- package/dist/{chunk-QVW5S6BD.js → chunk-XUM2LEGU.js} +3 -3
- package/dist/index.cjs +209 -51
- package/dist/index.js +1 -1
- package/dist/{server-MNSYIX42.js → server-5OSHEEVT.js} +4 -4
- package/dist/{worker-VU6JCCLG.js → worker-2TYCYFPM.js} +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to MCP Scraper are documented here. The format is based on [
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Routed canonical `/assistant` page loads to the web app and the redacted private Assistant readiness endpoint to the main API function, preventing production 404s after the 0.79.1 launch.
|
|
10
|
+
|
|
11
|
+
## [0.79.2] - 2026-08-28
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Rejected Google loading and error placeholders as PAA answers, retried interrupted controls without misreporting frontier exhaustion, preserved complete answer/source material from retained DOM, and resolved surviving Google `/goto` evidence after a late browser disconnect without visiting publisher pages.
|
|
16
|
+
- Advanced requested question counts through bounded newest-frontier click batches with 0.7–1.4 second observation windows, reducing a verified 35-question complete harvest from 347 seconds to 83 seconds while retaining actual multi-question click growth.
|
|
17
|
+
|
|
7
18
|
## [0.79.1] - 2026-08-28
|
|
8
19
|
|
|
9
20
|
### Fixed
|
|
@@ -1478,7 +1489,16 @@ All notable changes to MCP Scraper are documented here. The format is based on [
|
|
|
1478
1489
|
- Write actions remain unavailable until the account owner explicitly enables them.
|
|
1479
1490
|
- Provider-specific connection data is normalized into one agent-facing contract.
|
|
1480
1491
|
|
|
1481
|
-
[Unreleased]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.
|
|
1492
|
+
[Unreleased]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.79.2...HEAD
|
|
1493
|
+
[0.79.2]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.79.1...v0.79.2
|
|
1494
|
+
[0.79.1]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.79.0...v0.79.1
|
|
1495
|
+
[0.79.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.78.0...v0.79.0
|
|
1496
|
+
[0.78.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.77.0...v0.78.0
|
|
1497
|
+
[0.77.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.76.0...v0.77.0
|
|
1498
|
+
[0.76.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.75.1...v0.76.0
|
|
1499
|
+
[0.75.1]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.75.0...v0.75.1
|
|
1500
|
+
[0.75.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.74.0...v0.75.0
|
|
1501
|
+
[0.74.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.73.5...v0.74.0
|
|
1482
1502
|
[0.73.5]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.73.4...v0.73.5
|
|
1483
1503
|
[0.73.4]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.73.3...v0.73.4
|
|
1484
1504
|
[0.73.3]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.73.2...v0.73.3
|
package/README.md
CHANGED
|
@@ -173,7 +173,7 @@ Build the branded one-click bundle:
|
|
|
173
173
|
npm run build:mcpb
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
-
The generated bundle is written to `build/mcpb/mcp-scraper-<version>.mcpb` and copied to `public/downloads/` for the hosted download. The current public bundle is `https://mcpscraper.dev/downloads/mcp-scraper.mcpb` (`0.79.
|
|
176
|
+
The generated bundle is written to `build/mcpb/mcp-scraper-<version>.mcpb` and copied to `public/downloads/` for the hosted download. The current public bundle is `https://mcpscraper.dev/downloads/mcp-scraper.mcpb` (`0.79.2`, SHA-256 `5d9ac8818c9dc6e8667b5734bd4253d3012df82a6976e45eeee22f37bb2026a1`). Install it by opening or dragging it into Claude Desktop. Claude displays the `MCP Scraper` install card, icon, API-key configuration field, and manually curated current-release message from the bundle manifest.
|
|
177
177
|
|
|
178
178
|
The MCPB install exposes every tool — web-intelligence plus all `browser_*` tools — through the one `mcp-scraper` server.
|
|
179
179
|
|
package/dist/bin/api-server.js
CHANGED
|
@@ -17,8 +17,8 @@ loadDotEnv();
|
|
|
17
17
|
async function main() {
|
|
18
18
|
const [{ serve }, { app, personalAssistantProductionStartup }, { startWorker }, { migrate }] = await Promise.all([
|
|
19
19
|
import("@hono/node-server"),
|
|
20
|
-
import("../server-
|
|
21
|
-
import("../worker-
|
|
20
|
+
import("../server-5OSHEEVT.js"),
|
|
21
|
+
import("../worker-2TYCYFPM.js"),
|
|
22
22
|
import("../db-TA567JLA.js")
|
|
23
23
|
]);
|
|
24
24
|
const PORT = parseInt(process.env.PORT ?? "3001");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runMcpScraperStdio
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-XUM2LEGU.js";
|
|
5
|
+
import "../chunk-JECPQBLZ.js";
|
|
6
6
|
import "../chunk-PGJQDMC2.js";
|
|
7
7
|
import "../chunk-DNM65UCK.js";
|
|
8
8
|
import "../chunk-T3MZISOF.js";
|
|
@@ -10,10 +10,10 @@ import "../chunk-B6L73AO4.js";
|
|
|
10
10
|
import "../chunk-VFCUGTCF.js";
|
|
11
11
|
import "../chunk-OM7HVEJ3.js";
|
|
12
12
|
import "../chunk-NGLZ6UJR.js";
|
|
13
|
-
import "../chunk-
|
|
13
|
+
import "../chunk-6FL7ZZCF.js";
|
|
14
14
|
import "../chunk-VXLU74YZ.js";
|
|
15
15
|
import "../chunk-GGZEC22A.js";
|
|
16
|
-
import "../chunk-
|
|
16
|
+
import "../chunk-2RTELL6M.js";
|
|
17
17
|
import "../chunk-OU4FV2RP.js";
|
|
18
18
|
|
|
19
19
|
// src/mcp/thorbit-restricted-tool-surface.ts
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
renderInstallTerminal
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-6FL7ZZCF.js";
|
|
5
5
|
import {
|
|
6
6
|
PACKAGE_VERSION
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-2RTELL6M.js";
|
|
8
8
|
|
|
9
9
|
// bin/mcp-scraper-install.ts
|
|
10
10
|
var noColor = process.argv.includes("--no-color") || process.env.NO_COLOR !== void 0 || process.env.FORCE_COLOR === "0" || !process.stdout.isTTY;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runMcpScraperStdio
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-XUM2LEGU.js";
|
|
5
|
+
import "../chunk-JECPQBLZ.js";
|
|
6
6
|
import "../chunk-PGJQDMC2.js";
|
|
7
7
|
import "../chunk-DNM65UCK.js";
|
|
8
8
|
import "../chunk-T3MZISOF.js";
|
|
@@ -10,10 +10,10 @@ import "../chunk-B6L73AO4.js";
|
|
|
10
10
|
import "../chunk-VFCUGTCF.js";
|
|
11
11
|
import "../chunk-OM7HVEJ3.js";
|
|
12
12
|
import "../chunk-NGLZ6UJR.js";
|
|
13
|
-
import "../chunk-
|
|
13
|
+
import "../chunk-6FL7ZZCF.js";
|
|
14
14
|
import "../chunk-VXLU74YZ.js";
|
|
15
15
|
import "../chunk-GGZEC22A.js";
|
|
16
|
-
import "../chunk-
|
|
16
|
+
import "../chunk-2RTELL6M.js";
|
|
17
17
|
import "../chunk-OU4FV2RP.js";
|
|
18
18
|
|
|
19
19
|
// bin/mcp-stdio-server.ts
|
package/dist/bin/paa-harvest.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// release-message.json
|
|
2
2
|
var release_message_default = {
|
|
3
|
-
message: "PAA harvests now
|
|
3
|
+
message: "PAA harvests now reject Google error placeholders, retry interrupted controls without claiming the frontier ended, advance the newest question frontier in bounded batches, preserve complete answers and sources, and resolve retained Google redirect evidence even after the browser session closes."
|
|
4
4
|
};
|
|
5
5
|
|
|
6
6
|
// src/install-terminal.ts
|
|
@@ -2054,6 +2054,93 @@ async function resolveGoogleGotoUrls(rawUrls, options = {}) {
|
|
|
2054
2054
|
await Promise.all(Array.from({ length: Math.min(concurrency, inputs.length) }, () => worker()));
|
|
2055
2055
|
return { resolutions: [...records.values()], nonGoogleRequestsObserved: 0 };
|
|
2056
2056
|
}
|
|
2057
|
+
function resolvedSite(link, fallback) {
|
|
2058
|
+
try {
|
|
2059
|
+
return new URL(link.resolvedUrl ?? link.url).hostname.replace(/^www\./, "");
|
|
2060
|
+
} catch {
|
|
2061
|
+
return fallback;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
async function resolveHarvestResultGoogleGotoLinks(result, options = {}) {
|
|
2065
|
+
const treeNodes = [];
|
|
2066
|
+
const visit = (nodes) => {
|
|
2067
|
+
for (const node of nodes) {
|
|
2068
|
+
treeNodes.push(node);
|
|
2069
|
+
visit(node.children ?? []);
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2072
|
+
visit(result.tree ?? []);
|
|
2073
|
+
const candidates = [
|
|
2074
|
+
...result.flat.flatMap((row) => row.sources ?? []),
|
|
2075
|
+
...treeNodes.flatMap((node) => node.sources),
|
|
2076
|
+
...result.organicResults ?? [],
|
|
2077
|
+
...(result.aiOverview?.citations ?? []).map(({ text: _text, href, ...link }) => ({ ...link, url: href })),
|
|
2078
|
+
...(result.aiMode?.citations ?? []).map(({ text: _text, href, ...link }) => ({ ...link, url: href }))
|
|
2079
|
+
];
|
|
2080
|
+
const unique = new Map(candidates.filter((link) => Boolean(link?.rawUrl)).map((link) => [link.rawUrl, link]));
|
|
2081
|
+
const gotoUrls = [...unique.values()].filter((link) => link.linkType === "google_goto_redirect" && link.resolutionStatus !== "resolved").map((link) => link.rawUrl);
|
|
2082
|
+
if (gotoUrls.length > 0) {
|
|
2083
|
+
const batch = await resolveGoogleGotoUrls(gotoUrls, options);
|
|
2084
|
+
for (const resolution2 of batch.resolutions) {
|
|
2085
|
+
const link = unique.get(resolution2.rawUrl);
|
|
2086
|
+
if (!link) continue;
|
|
2087
|
+
unique.set(resolution2.rawUrl, resolution2.resolutionStatus === "resolved" && resolution2.resolvedUrl ? { ...link, url: resolution2.resolvedUrl, resolvedUrl: resolution2.resolvedUrl, resolutionStatus: "resolved" } : { ...link, resolutionStatus: resolution2.resolutionStatus });
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
const replace = (link) => ({
|
|
2091
|
+
...link,
|
|
2092
|
+
...unique.get(link.rawUrl) ?? link
|
|
2093
|
+
});
|
|
2094
|
+
const replaceSources = (sources) => sources.map((source) => {
|
|
2095
|
+
const link = replace(source);
|
|
2096
|
+
return { ...link, site: resolvedSite(link, source.site) };
|
|
2097
|
+
});
|
|
2098
|
+
for (const row of result.flat) {
|
|
2099
|
+
row.sources = replaceSources(row.sources ?? []);
|
|
2100
|
+
const primary = row.sources[0];
|
|
2101
|
+
row.source_title = primary?.title ?? row.source_title;
|
|
2102
|
+
row.source_site = primary?.site ?? row.source_site;
|
|
2103
|
+
row.source_cite = primary?.url ?? row.source_cite;
|
|
2104
|
+
}
|
|
2105
|
+
for (const node of treeNodes) {
|
|
2106
|
+
node.sources = replaceSources(node.sources ?? []);
|
|
2107
|
+
const primary = node.sources[0];
|
|
2108
|
+
node.sourceTitle = primary?.title ?? node.sourceTitle;
|
|
2109
|
+
node.sourceSite = primary?.site ?? node.sourceSite;
|
|
2110
|
+
node.sourceCite = primary?.url ?? node.sourceCite;
|
|
2111
|
+
}
|
|
2112
|
+
for (let index = 0; index < (result.organicResults?.length ?? 0); index++) {
|
|
2113
|
+
const link = replace(result.organicResults[index]);
|
|
2114
|
+
result.organicResults[index] = { ...link, domain: resolvedSite(link, result.organicResults[index].domain) ?? "" };
|
|
2115
|
+
}
|
|
2116
|
+
const replaceCitation = (citation) => {
|
|
2117
|
+
const link = replace({
|
|
2118
|
+
url: citation.href,
|
|
2119
|
+
rawUrl: citation.rawUrl,
|
|
2120
|
+
resolvedUrl: citation.resolvedUrl,
|
|
2121
|
+
linkType: citation.linkType,
|
|
2122
|
+
resolutionStatus: citation.resolutionStatus
|
|
2123
|
+
});
|
|
2124
|
+
const { url, ...metadata } = link;
|
|
2125
|
+
return { ...citation, ...metadata, href: url };
|
|
2126
|
+
};
|
|
2127
|
+
if (result.aiOverview) result.aiOverview.citations = (result.aiOverview.citations ?? []).map(replaceCitation);
|
|
2128
|
+
if (result.aiMode) result.aiMode.citations = (result.aiMode.citations ?? []).map(replaceCitation);
|
|
2129
|
+
const values = [...unique.values()];
|
|
2130
|
+
result.diagnostics.links = {
|
|
2131
|
+
linkCandidatesObserved: unique.size,
|
|
2132
|
+
plainLinksObserved: values.filter((link) => link.linkType === "plain").length,
|
|
2133
|
+
googleUrlRedirectsObserved: values.filter((link) => link.linkType === "google_url_redirect").length,
|
|
2134
|
+
googleGotoRedirectsObserved: values.filter((link) => link.linkType === "google_goto_redirect").length,
|
|
2135
|
+
redirectsResolved: values.filter((link) => link.linkType !== "plain" && link.resolutionStatus === "resolved").length,
|
|
2136
|
+
redirectsUnresolved: values.filter((link) => link.resolutionStatus === "unresolved").length,
|
|
2137
|
+
redirectsRejected: values.filter((link) => link.resolutionStatus === "rejected").length,
|
|
2138
|
+
resolverNonGoogleRequestsObserved: 0,
|
|
2139
|
+
paaSourcesPreserved: result.flat.reduce((total, row) => total + row.sources.length, 0),
|
|
2140
|
+
aiOverviewLabelValidated: result.aiOverview?.detected ?? false
|
|
2141
|
+
};
|
|
2142
|
+
return result;
|
|
2143
|
+
}
|
|
2057
2144
|
async function resolveGoogleOutboundLinks(page, values, options = {}) {
|
|
2058
2145
|
const byRawUrl = /* @__PURE__ */ new Map();
|
|
2059
2146
|
for (const value of values) byRawUrl.set(value.rawUrl, value);
|
|
@@ -2174,6 +2261,10 @@ async function resolveGoogleOutboundLinks(page, values, options = {}) {
|
|
|
2174
2261
|
}
|
|
2175
2262
|
|
|
2176
2263
|
// src/extractor/PAAExtractor.ts
|
|
2264
|
+
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]*$`;
|
|
2265
|
+
var PAA_CLICK_EARLIEST_CONFIRMATION_MS = 700;
|
|
2266
|
+
var PAA_CLICK_ATTEMPT_BUDGET_MS = 1400;
|
|
2267
|
+
var PAA_FRONTIER_BATCH_SIZE = 3;
|
|
2177
2268
|
var PAAExtractor = class {
|
|
2178
2269
|
constructor(driver, reporter, onPaaProgress) {
|
|
2179
2270
|
this.driver = driver;
|
|
@@ -2318,7 +2409,7 @@ var PAAExtractor = class {
|
|
|
2318
2409
|
}
|
|
2319
2410
|
async extractVisibleItems(page, onlyQuestion) {
|
|
2320
2411
|
const sels = PAASelectors;
|
|
2321
|
-
const raw = await page.evaluate(({ selectors, onlyQuestion: onlyQuestion2 }) => {
|
|
2412
|
+
const raw = await page.evaluate(({ selectors, onlyQuestion: onlyQuestion2, answerPlaceholderPattern }) => {
|
|
2322
2413
|
function cleanText(el) {
|
|
2323
2414
|
if (!el) return "";
|
|
2324
2415
|
const parts = [];
|
|
@@ -2345,12 +2436,13 @@ var PAAExtractor = class {
|
|
|
2345
2436
|
const aiAnswerBody = answerContainer?.querySelector(selectors.aiAnswerBody);
|
|
2346
2437
|
const answerElement = aiAnswerBody ?? answerContainer;
|
|
2347
2438
|
const answerText = cleanText(answerElement);
|
|
2439
|
+
const answerIsPlaceholder = new RegExp(answerPlaceholderPattern, "i").test(answerText);
|
|
2348
2440
|
const citationCandidates = [
|
|
2349
2441
|
...Array.from((answerElement ?? pair).querySelectorAll("a[href]")),
|
|
2350
2442
|
...Array.from(pair.querySelectorAll(selectors.aiCitation)),
|
|
2351
2443
|
...Array.from(pair.querySelectorAll(selectors.aiSourceCard))
|
|
2352
2444
|
];
|
|
2353
|
-
const hasLoadedAnswer = expanded
|
|
2445
|
+
const hasLoadedAnswer = !answerIsPlaceholder && (expanded && answerText.length >= 10 || answerText.length > 30 && citationCandidates.some((anchor) => /^https?:\/\//.test(anchor.href)));
|
|
2354
2446
|
let sourceTitle = hasLoadedAnswer ? pair.querySelector(selectors.sourceTitle)?.innerText?.trim() || void 0 : void 0;
|
|
2355
2447
|
let sourceSite = hasLoadedAnswer ? pair.querySelector(selectors.sourceSite)?.innerText?.trim() || void 0 : void 0;
|
|
2356
2448
|
const sources = [];
|
|
@@ -2374,7 +2466,7 @@ var PAAExtractor = class {
|
|
|
2374
2466
|
sources
|
|
2375
2467
|
};
|
|
2376
2468
|
});
|
|
2377
|
-
}, { selectors: sels, onlyQuestion });
|
|
2469
|
+
}, { selectors: sels, onlyQuestion, answerPlaceholderPattern: PAA_ANSWER_PLACEHOLDER_PATTERN });
|
|
2378
2470
|
const baseUrl = typeof page.url === "function" ? page.url() : "https://www.google.com/";
|
|
2379
2471
|
return raw.flatMap((item) => {
|
|
2380
2472
|
const sources = [];
|
|
@@ -2565,44 +2657,52 @@ var PAAExtractor = class {
|
|
|
2565
2657
|
questionEl: PAASelectors.itemQuestionEl,
|
|
2566
2658
|
answerSel: PAASelectors.answerContainer,
|
|
2567
2659
|
expandedClass: PAASelectors.expandedClass,
|
|
2568
|
-
clickTarget: PAASelectors.clickTarget
|
|
2660
|
+
clickTarget: PAASelectors.clickTarget,
|
|
2661
|
+
answerPlaceholderPattern: PAA_ANSWER_PLACEHOLDER_PATTERN,
|
|
2662
|
+
questionsOnly: options.questionsOnly
|
|
2569
2663
|
};
|
|
2570
2664
|
const readItemStates = () => page.evaluate(
|
|
2571
|
-
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget, answerSel }) => {
|
|
2572
|
-
return Array.from(document.querySelectorAll(sel)).map((el) =>
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2665
|
+
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget, answerSel, answerPlaceholderPattern, questionsOnly }) => {
|
|
2666
|
+
return Array.from(document.querySelectorAll(sel)).map((el, index) => {
|
|
2667
|
+
const answerText = (el.querySelector(answerSel)?.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
2668
|
+
const expanded = el.classList.contains(expandedClass) || el.querySelector(clickTarget)?.getAttribute("aria-expanded") === "true";
|
|
2669
|
+
const answerReady = answerText.length >= 10 && !new RegExp(answerPlaceholderPattern, "i").test(answerText);
|
|
2670
|
+
return {
|
|
2671
|
+
q: el.getAttribute(dataQ) || el.getAttribute(dataInitQ) || el.querySelector(questionEl)?.innerText?.trim() || "",
|
|
2672
|
+
done: expanded && (questionsOnly || answerReady),
|
|
2673
|
+
expanded,
|
|
2674
|
+
index
|
|
2675
|
+
};
|
|
2676
|
+
}).filter((s) => s.q);
|
|
2576
2677
|
},
|
|
2577
2678
|
itemArgs
|
|
2578
2679
|
);
|
|
2579
|
-
const expandOneItemSerially = async (q, beforeCount) => {
|
|
2680
|
+
const expandOneItemSerially = async (q, beforeCount, itemIndex, wasExpanded) => {
|
|
2580
2681
|
const clickStartedMs = Date.now();
|
|
2581
|
-
const
|
|
2582
|
-
const itemIndex = await items.evaluateAll(
|
|
2583
|
-
(elements, { dataQ, dataInitQ, target }) => elements.findIndex((element) => (element.getAttribute(dataQ) || element.getAttribute(dataInitQ)) === target),
|
|
2584
|
-
{ dataQ: itemArgs.dataQ, dataInitQ: itemArgs.dataInitQ, target: q }
|
|
2585
|
-
).catch(() => -1);
|
|
2586
|
-
const clickTarget = itemIndex >= 0 ? items.nth(itemIndex).locator(itemArgs.clickTarget) : null;
|
|
2682
|
+
const clickTarget = itemIndex >= 0 ? page.locator(itemArgs.sel).nth(itemIndex).locator(itemArgs.clickTarget) : null;
|
|
2587
2683
|
const clicked = clickTarget ? await (async () => {
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2684
|
+
if (wasExpanded) {
|
|
2685
|
+
await clickTarget.click({ timeout: 2e3, force: true });
|
|
2686
|
+
await page.waitForTimeout(250);
|
|
2687
|
+
}
|
|
2688
|
+
await clickTarget.click({ timeout: 2e3, force: true });
|
|
2592
2689
|
return true;
|
|
2593
2690
|
})().catch(() => false) : false;
|
|
2594
2691
|
if (!clicked) return { dispatched: false, confirmed: false };
|
|
2595
|
-
const
|
|
2596
|
-
if (remainingHumanClickDelayMs > 0) await page.waitForTimeout(remainingHumanClickDelayMs);
|
|
2692
|
+
const remainingConfirmationMs = Math.max(100, PAA_CLICK_ATTEMPT_BUDGET_MS - (Date.now() - clickStartedMs));
|
|
2597
2693
|
const confirmed = await page.waitForFunction(
|
|
2598
|
-
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget: clickTarget2, answerSel, target, min }) => {
|
|
2599
|
-
|
|
2600
|
-
const
|
|
2601
|
-
|
|
2602
|
-
|
|
2694
|
+
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget: clickTarget2, answerSel, answerPlaceholderPattern, questionsOnly, target, min, earliestConfirmationAt }) => {
|
|
2695
|
+
if (Date.now() < earliestConfirmationAt) return false;
|
|
2696
|
+
const items = Array.from(document.querySelectorAll(sel));
|
|
2697
|
+
const item = items.find((element) => (element.getAttribute(dataQ) || element.getAttribute(dataInitQ) || element.querySelector(questionEl)?.innerText?.trim()) === target);
|
|
2698
|
+
if (!item) return false;
|
|
2699
|
+
const expanded = item.classList.contains(expandedClass) || item.querySelector(clickTarget2)?.getAttribute("aria-expanded") === "true";
|
|
2700
|
+
if (questionsOnly) return expanded || items.length > min;
|
|
2701
|
+
const answerText = (item.querySelector(answerSel)?.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
2702
|
+
return answerText.length >= 10 && !new RegExp(answerPlaceholderPattern, "i").test(answerText);
|
|
2603
2703
|
},
|
|
2604
|
-
{ ...itemArgs, target: q, min: beforeCount },
|
|
2605
|
-
{ timeout:
|
|
2704
|
+
{ ...itemArgs, target: q, min: beforeCount, earliestConfirmationAt: clickStartedMs + PAA_CLICK_EARLIEST_CONFIRMATION_MS },
|
|
2705
|
+
{ timeout: remainingConfirmationMs, polling: 100 }
|
|
2606
2706
|
).catch(() => null);
|
|
2607
2707
|
return { dispatched: true, confirmed: Boolean(confirmed) };
|
|
2608
2708
|
};
|
|
@@ -2647,10 +2747,43 @@ var PAAExtractor = class {
|
|
|
2647
2747
|
};
|
|
2648
2748
|
break;
|
|
2649
2749
|
}
|
|
2750
|
+
if (!options.questionsOnly && seenQs.size >= options.maxQuestions) {
|
|
2751
|
+
let materialChanged = false;
|
|
2752
|
+
const keptQuestions = new Set(orderedQs.slice(0, options.maxQuestions));
|
|
2753
|
+
for (const item of await this.extractVisibleItems(page)) {
|
|
2754
|
+
if (!keptQuestions.has(item.question)) continue;
|
|
2755
|
+
const existing = capturedItems.get(item.question);
|
|
2756
|
+
const merged = {
|
|
2757
|
+
...item,
|
|
2758
|
+
answer: item.answer ?? existing?.answer,
|
|
2759
|
+
sourceTitle: item.sourceTitle ?? existing?.sourceTitle,
|
|
2760
|
+
sourceSite: item.sourceSite ?? existing?.sourceSite,
|
|
2761
|
+
sourceCite: item.sourceCite ?? existing?.sourceCite,
|
|
2762
|
+
sources: this.mergePaaSources(existing?.sources, item.sources)
|
|
2763
|
+
};
|
|
2764
|
+
capturedItems.set(item.question, merged);
|
|
2765
|
+
materialChanged = this.upsertCollectedRow(this.toFlatRow(merged, 1, null, options.query)) || materialChanged;
|
|
2766
|
+
}
|
|
2767
|
+
if (materialChanged) await this.emitProgress("answer_captured");
|
|
2768
|
+
if ([...keptQuestions].every((question) => {
|
|
2769
|
+
const item = capturedItems.get(question);
|
|
2770
|
+
return Boolean(item?.answer?.trim() && item.sources.length > 0);
|
|
2771
|
+
})) {
|
|
2772
|
+
failedInteractions.clear();
|
|
2773
|
+
this.paaLifecycle = {
|
|
2774
|
+
...this.paaLifecycle,
|
|
2775
|
+
discoveryStatus: "target_reached",
|
|
2776
|
+
targetStatus: "met",
|
|
2777
|
+
failedInteractions: 0,
|
|
2778
|
+
completionReason: "The requested unique-question count and its answer/source material were observed."
|
|
2779
|
+
};
|
|
2780
|
+
break;
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2650
2783
|
const kept = new Set(orderedQs.slice(0, options.maxQuestions));
|
|
2651
2784
|
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
|
|
2653
|
-
if (
|
|
2785
|
+
const targets = clickable.slice(-PAA_FRONTIER_BATCH_SIZE);
|
|
2786
|
+
if (targets.length === 0) {
|
|
2654
2787
|
if (seenQs.size >= options.maxQuestions) {
|
|
2655
2788
|
this.paaLifecycle = {
|
|
2656
2789
|
...this.paaLifecycle,
|
|
@@ -2698,31 +2831,43 @@ var PAAExtractor = class {
|
|
|
2698
2831
|
};
|
|
2699
2832
|
break;
|
|
2700
2833
|
}
|
|
2701
|
-
this.reporter.onDepth(++round);
|
|
2702
2834
|
await this.throwIfCaptcha(page, "Google PAA expansion");
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
clickedItem = (await this.extractVisibleItems(page, target.q))[0];
|
|
2835
|
+
for (const target of targets) {
|
|
2836
|
+
this.reporter.onDepth(++round);
|
|
2837
|
+
if ((clickAttempts.get(target.q) ?? 0) > 0) {
|
|
2838
|
+
this.paaLifecycle = {
|
|
2839
|
+
...this.paaLifecycle,
|
|
2840
|
+
automaticRetries: this.paaLifecycle.automaticRetries + 1
|
|
2841
|
+
};
|
|
2711
2842
|
}
|
|
2712
|
-
|
|
2713
|
-
|
|
2843
|
+
clickAttempts.set(target.q, (clickAttempts.get(target.q) ?? 0) + 1);
|
|
2844
|
+
}
|
|
2845
|
+
const expansionResults = await Promise.all(targets.map(async (target) => ({
|
|
2846
|
+
target,
|
|
2847
|
+
result: await expandOneItemSerially(target.q, states.length, target.index, target.expanded)
|
|
2848
|
+
})));
|
|
2849
|
+
for (const { target, result } of expansionResults) {
|
|
2850
|
+
if (result.dispatched) this.clickedQuestions.add(target.q);
|
|
2851
|
+
}
|
|
2852
|
+
if (!options.questionsOnly) {
|
|
2853
|
+
const targetQuestions = new Set(targets.map((target) => target.q));
|
|
2854
|
+
for (const clickedItem of await this.extractVisibleItems(page)) {
|
|
2855
|
+
if (!targetQuestions.has(clickedItem.question)) continue;
|
|
2856
|
+
capturedItems.set(clickedItem.question, clickedItem);
|
|
2714
2857
|
if (this.upsertCollectedRow(this.toFlatRow(clickedItem, 1, null, options.query))) {
|
|
2715
2858
|
await this.emitProgress("answer_captured");
|
|
2716
2859
|
}
|
|
2717
2860
|
}
|
|
2718
2861
|
}
|
|
2719
|
-
const
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2862
|
+
for (const { target, result } of expansionResults) {
|
|
2863
|
+
const captured = capturedItems.get(target.q);
|
|
2864
|
+
if (result.confirmed || Boolean(captured?.answer?.trim())) {
|
|
2865
|
+
processedQuestions.add(target.q);
|
|
2866
|
+
failedInteractions.delete(target.q);
|
|
2867
|
+
} else if ((clickAttempts.get(target.q) ?? 0) >= MAX_CLICK_ATTEMPTS) {
|
|
2868
|
+
failedInteractions.add(target.q);
|
|
2869
|
+
abandonedNeverReclick.add(target.q);
|
|
2870
|
+
}
|
|
2726
2871
|
}
|
|
2727
2872
|
this.paaLifecycle = {
|
|
2728
2873
|
...this.paaLifecycle,
|
|
@@ -4430,6 +4575,17 @@ function stripInternalDebug(result, keepDebug) {
|
|
|
4430
4575
|
delete diagnostics.debug;
|
|
4431
4576
|
return { ...result, diagnostics };
|
|
4432
4577
|
}
|
|
4578
|
+
async function resolveSurvivingGotoLinks(result) {
|
|
4579
|
+
try {
|
|
4580
|
+
return await resolveHarvestResultGoogleGotoLinks(result, { totalBudgetMs: 1e4 });
|
|
4581
|
+
} catch (error) {
|
|
4582
|
+
console.warn(JSON.stringify({
|
|
4583
|
+
event: "harvest_goto_resolution_failed",
|
|
4584
|
+
message: errorMessage(error)
|
|
4585
|
+
}));
|
|
4586
|
+
return result;
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4433
4589
|
async function cleanupDisposableProxy(kernelApiKey, proxyId) {
|
|
4434
4590
|
if (!kernelApiKey || !proxyId) return;
|
|
4435
4591
|
try {
|
|
@@ -4644,7 +4800,8 @@ async function harvest(rawOptions) {
|
|
|
4644
4800
|
lastError = err;
|
|
4645
4801
|
if (willRetry2) continue;
|
|
4646
4802
|
if (attempt.result) {
|
|
4647
|
-
|
|
4803
|
+
const resolvedResult2 = await resolveSurvivingGotoLinks(attempt.result);
|
|
4804
|
+
return withResultQuality(stripInternalDebug(resolvedResult2, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
|
|
4648
4805
|
}
|
|
4649
4806
|
break;
|
|
4650
4807
|
}
|
|
@@ -4679,7 +4836,8 @@ async function harvest(rawOptions) {
|
|
|
4679
4836
|
if (willRetry2) continue;
|
|
4680
4837
|
break;
|
|
4681
4838
|
}
|
|
4682
|
-
const
|
|
4839
|
+
const resolvedResult = await resolveSurvivingGotoLinks(result);
|
|
4840
|
+
const finalResult = withResultQuality(stripInternalDebug(resolvedResult, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
|
|
4683
4841
|
const outcome = classifyAttemptResult(finalResult);
|
|
4684
4842
|
const willRetry = outcome === "degraded_result" && i < maxAttempts - 1;
|
|
4685
4843
|
const resultError = outcome === "degraded_result" ? degradedResultMessage(finalResult) : null;
|
|
@@ -12,13 +12,13 @@ import {
|
|
|
12
12
|
registerScheduledResultsMcpTools,
|
|
13
13
|
registerSerpIntelligenceCaptureTools,
|
|
14
14
|
resolveDeploymentProfile
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-JECPQBLZ.js";
|
|
16
16
|
import {
|
|
17
17
|
renderInstallTerminal
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-6FL7ZZCF.js";
|
|
19
19
|
import {
|
|
20
20
|
PACKAGE_VERSION
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-2RTELL6M.js";
|
|
22
22
|
|
|
23
23
|
// src/mcp/stdio-runtime.ts
|
|
24
24
|
import { readFileSync } from "fs";
|
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,6 +2805,10 @@ 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_ATTEMPT_BUDGET_MS = 1400;
|
|
2811
|
+
var PAA_FRONTIER_BATCH_SIZE = 3;
|
|
2721
2812
|
var PAAExtractor = class {
|
|
2722
2813
|
constructor(driver, reporter, onPaaProgress) {
|
|
2723
2814
|
this.driver = driver;
|
|
@@ -2862,7 +2953,7 @@ var PAAExtractor = class {
|
|
|
2862
2953
|
}
|
|
2863
2954
|
async extractVisibleItems(page, onlyQuestion) {
|
|
2864
2955
|
const sels = PAASelectors;
|
|
2865
|
-
const raw = await page.evaluate(({ selectors, onlyQuestion: onlyQuestion2 }) => {
|
|
2956
|
+
const raw = await page.evaluate(({ selectors, onlyQuestion: onlyQuestion2, answerPlaceholderPattern }) => {
|
|
2866
2957
|
function cleanText(el) {
|
|
2867
2958
|
if (!el) return "";
|
|
2868
2959
|
const parts = [];
|
|
@@ -2889,12 +2980,13 @@ var PAAExtractor = class {
|
|
|
2889
2980
|
const aiAnswerBody = answerContainer?.querySelector(selectors.aiAnswerBody);
|
|
2890
2981
|
const answerElement = aiAnswerBody ?? answerContainer;
|
|
2891
2982
|
const answerText = cleanText(answerElement);
|
|
2983
|
+
const answerIsPlaceholder = new RegExp(answerPlaceholderPattern, "i").test(answerText);
|
|
2892
2984
|
const citationCandidates = [
|
|
2893
2985
|
...Array.from((answerElement ?? pair).querySelectorAll("a[href]")),
|
|
2894
2986
|
...Array.from(pair.querySelectorAll(selectors.aiCitation)),
|
|
2895
2987
|
...Array.from(pair.querySelectorAll(selectors.aiSourceCard))
|
|
2896
2988
|
];
|
|
2897
|
-
const hasLoadedAnswer = expanded
|
|
2989
|
+
const hasLoadedAnswer = !answerIsPlaceholder && (expanded && answerText.length >= 10 || answerText.length > 30 && citationCandidates.some((anchor) => /^https?:\/\//.test(anchor.href)));
|
|
2898
2990
|
let sourceTitle = hasLoadedAnswer ? pair.querySelector(selectors.sourceTitle)?.innerText?.trim() || void 0 : void 0;
|
|
2899
2991
|
let sourceSite = hasLoadedAnswer ? pair.querySelector(selectors.sourceSite)?.innerText?.trim() || void 0 : void 0;
|
|
2900
2992
|
const sources = [];
|
|
@@ -2918,7 +3010,7 @@ var PAAExtractor = class {
|
|
|
2918
3010
|
sources
|
|
2919
3011
|
};
|
|
2920
3012
|
});
|
|
2921
|
-
}, { selectors: sels, onlyQuestion });
|
|
3013
|
+
}, { selectors: sels, onlyQuestion, answerPlaceholderPattern: PAA_ANSWER_PLACEHOLDER_PATTERN });
|
|
2922
3014
|
const baseUrl = typeof page.url === "function" ? page.url() : "https://www.google.com/";
|
|
2923
3015
|
return raw.flatMap((item) => {
|
|
2924
3016
|
const sources = [];
|
|
@@ -3109,44 +3201,52 @@ var PAAExtractor = class {
|
|
|
3109
3201
|
questionEl: PAASelectors.itemQuestionEl,
|
|
3110
3202
|
answerSel: PAASelectors.answerContainer,
|
|
3111
3203
|
expandedClass: PAASelectors.expandedClass,
|
|
3112
|
-
clickTarget: PAASelectors.clickTarget
|
|
3204
|
+
clickTarget: PAASelectors.clickTarget,
|
|
3205
|
+
answerPlaceholderPattern: PAA_ANSWER_PLACEHOLDER_PATTERN,
|
|
3206
|
+
questionsOnly: options.questionsOnly
|
|
3113
3207
|
};
|
|
3114
3208
|
const readItemStates = () => page.evaluate(
|
|
3115
|
-
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget, answerSel }) => {
|
|
3116
|
-
return Array.from(document.querySelectorAll(sel)).map((el) =>
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3209
|
+
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget, answerSel, answerPlaceholderPattern, questionsOnly }) => {
|
|
3210
|
+
return Array.from(document.querySelectorAll(sel)).map((el, index) => {
|
|
3211
|
+
const answerText = (el.querySelector(answerSel)?.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
3212
|
+
const expanded = el.classList.contains(expandedClass) || el.querySelector(clickTarget)?.getAttribute("aria-expanded") === "true";
|
|
3213
|
+
const answerReady = answerText.length >= 10 && !new RegExp(answerPlaceholderPattern, "i").test(answerText);
|
|
3214
|
+
return {
|
|
3215
|
+
q: el.getAttribute(dataQ) || el.getAttribute(dataInitQ) || el.querySelector(questionEl)?.innerText?.trim() || "",
|
|
3216
|
+
done: expanded && (questionsOnly || answerReady),
|
|
3217
|
+
expanded,
|
|
3218
|
+
index
|
|
3219
|
+
};
|
|
3220
|
+
}).filter((s) => s.q);
|
|
3120
3221
|
},
|
|
3121
3222
|
itemArgs
|
|
3122
3223
|
);
|
|
3123
|
-
const expandOneItemSerially = async (q, beforeCount) => {
|
|
3224
|
+
const expandOneItemSerially = async (q, beforeCount, itemIndex, wasExpanded) => {
|
|
3124
3225
|
const clickStartedMs = Date.now();
|
|
3125
|
-
const
|
|
3126
|
-
const itemIndex = await items.evaluateAll(
|
|
3127
|
-
(elements, { dataQ, dataInitQ, target }) => elements.findIndex((element) => (element.getAttribute(dataQ) || element.getAttribute(dataInitQ)) === target),
|
|
3128
|
-
{ dataQ: itemArgs.dataQ, dataInitQ: itemArgs.dataInitQ, target: q }
|
|
3129
|
-
).catch(() => -1);
|
|
3130
|
-
const clickTarget = itemIndex >= 0 ? items.nth(itemIndex).locator(itemArgs.clickTarget) : null;
|
|
3226
|
+
const clickTarget = itemIndex >= 0 ? page.locator(itemArgs.sel).nth(itemIndex).locator(itemArgs.clickTarget) : null;
|
|
3131
3227
|
const clicked = clickTarget ? await (async () => {
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3228
|
+
if (wasExpanded) {
|
|
3229
|
+
await clickTarget.click({ timeout: 2e3, force: true });
|
|
3230
|
+
await page.waitForTimeout(250);
|
|
3231
|
+
}
|
|
3232
|
+
await clickTarget.click({ timeout: 2e3, force: true });
|
|
3136
3233
|
return true;
|
|
3137
3234
|
})().catch(() => false) : false;
|
|
3138
3235
|
if (!clicked) return { dispatched: false, confirmed: false };
|
|
3139
|
-
const
|
|
3140
|
-
if (remainingHumanClickDelayMs > 0) await page.waitForTimeout(remainingHumanClickDelayMs);
|
|
3236
|
+
const remainingConfirmationMs = Math.max(100, PAA_CLICK_ATTEMPT_BUDGET_MS - (Date.now() - clickStartedMs));
|
|
3141
3237
|
const confirmed = await page.waitForFunction(
|
|
3142
|
-
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget: clickTarget2, answerSel, target, min }) => {
|
|
3143
|
-
|
|
3144
|
-
const
|
|
3145
|
-
|
|
3146
|
-
|
|
3238
|
+
({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget: clickTarget2, answerSel, answerPlaceholderPattern, questionsOnly, target, min, earliestConfirmationAt }) => {
|
|
3239
|
+
if (Date.now() < earliestConfirmationAt) return false;
|
|
3240
|
+
const items = Array.from(document.querySelectorAll(sel));
|
|
3241
|
+
const item = items.find((element) => (element.getAttribute(dataQ) || element.getAttribute(dataInitQ) || element.querySelector(questionEl)?.innerText?.trim()) === target);
|
|
3242
|
+
if (!item) return false;
|
|
3243
|
+
const expanded = item.classList.contains(expandedClass) || item.querySelector(clickTarget2)?.getAttribute("aria-expanded") === "true";
|
|
3244
|
+
if (questionsOnly) return expanded || items.length > min;
|
|
3245
|
+
const answerText = (item.querySelector(answerSel)?.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
3246
|
+
return answerText.length >= 10 && !new RegExp(answerPlaceholderPattern, "i").test(answerText);
|
|
3147
3247
|
},
|
|
3148
|
-
{ ...itemArgs, target: q, min: beforeCount },
|
|
3149
|
-
{ timeout:
|
|
3248
|
+
{ ...itemArgs, target: q, min: beforeCount, earliestConfirmationAt: clickStartedMs + PAA_CLICK_EARLIEST_CONFIRMATION_MS },
|
|
3249
|
+
{ timeout: remainingConfirmationMs, polling: 100 }
|
|
3150
3250
|
).catch(() => null);
|
|
3151
3251
|
return { dispatched: true, confirmed: Boolean(confirmed) };
|
|
3152
3252
|
};
|
|
@@ -3191,10 +3291,43 @@ var PAAExtractor = class {
|
|
|
3191
3291
|
};
|
|
3192
3292
|
break;
|
|
3193
3293
|
}
|
|
3294
|
+
if (!options.questionsOnly && seenQs.size >= options.maxQuestions) {
|
|
3295
|
+
let materialChanged = false;
|
|
3296
|
+
const keptQuestions = new Set(orderedQs.slice(0, options.maxQuestions));
|
|
3297
|
+
for (const item of await this.extractVisibleItems(page)) {
|
|
3298
|
+
if (!keptQuestions.has(item.question)) continue;
|
|
3299
|
+
const existing = capturedItems.get(item.question);
|
|
3300
|
+
const merged = {
|
|
3301
|
+
...item,
|
|
3302
|
+
answer: item.answer ?? existing?.answer,
|
|
3303
|
+
sourceTitle: item.sourceTitle ?? existing?.sourceTitle,
|
|
3304
|
+
sourceSite: item.sourceSite ?? existing?.sourceSite,
|
|
3305
|
+
sourceCite: item.sourceCite ?? existing?.sourceCite,
|
|
3306
|
+
sources: this.mergePaaSources(existing?.sources, item.sources)
|
|
3307
|
+
};
|
|
3308
|
+
capturedItems.set(item.question, merged);
|
|
3309
|
+
materialChanged = this.upsertCollectedRow(this.toFlatRow(merged, 1, null, options.query)) || materialChanged;
|
|
3310
|
+
}
|
|
3311
|
+
if (materialChanged) await this.emitProgress("answer_captured");
|
|
3312
|
+
if ([...keptQuestions].every((question) => {
|
|
3313
|
+
const item = capturedItems.get(question);
|
|
3314
|
+
return Boolean(item?.answer?.trim() && item.sources.length > 0);
|
|
3315
|
+
})) {
|
|
3316
|
+
failedInteractions.clear();
|
|
3317
|
+
this.paaLifecycle = {
|
|
3318
|
+
...this.paaLifecycle,
|
|
3319
|
+
discoveryStatus: "target_reached",
|
|
3320
|
+
targetStatus: "met",
|
|
3321
|
+
failedInteractions: 0,
|
|
3322
|
+
completionReason: "The requested unique-question count and its answer/source material were observed."
|
|
3323
|
+
};
|
|
3324
|
+
break;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3194
3327
|
const kept = new Set(orderedQs.slice(0, options.maxQuestions));
|
|
3195
3328
|
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
|
-
if (
|
|
3329
|
+
const targets = clickable.slice(-PAA_FRONTIER_BATCH_SIZE);
|
|
3330
|
+
if (targets.length === 0) {
|
|
3198
3331
|
if (seenQs.size >= options.maxQuestions) {
|
|
3199
3332
|
this.paaLifecycle = {
|
|
3200
3333
|
...this.paaLifecycle,
|
|
@@ -3242,31 +3375,43 @@ var PAAExtractor = class {
|
|
|
3242
3375
|
};
|
|
3243
3376
|
break;
|
|
3244
3377
|
}
|
|
3245
|
-
this.reporter.onDepth(++round);
|
|
3246
3378
|
await this.throwIfCaptcha(page, "Google PAA expansion");
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
clickedItem = (await this.extractVisibleItems(page, target.q))[0];
|
|
3379
|
+
for (const target of targets) {
|
|
3380
|
+
this.reporter.onDepth(++round);
|
|
3381
|
+
if ((clickAttempts.get(target.q) ?? 0) > 0) {
|
|
3382
|
+
this.paaLifecycle = {
|
|
3383
|
+
...this.paaLifecycle,
|
|
3384
|
+
automaticRetries: this.paaLifecycle.automaticRetries + 1
|
|
3385
|
+
};
|
|
3255
3386
|
}
|
|
3256
|
-
|
|
3257
|
-
|
|
3387
|
+
clickAttempts.set(target.q, (clickAttempts.get(target.q) ?? 0) + 1);
|
|
3388
|
+
}
|
|
3389
|
+
const expansionResults = await Promise.all(targets.map(async (target) => ({
|
|
3390
|
+
target,
|
|
3391
|
+
result: await expandOneItemSerially(target.q, states.length, target.index, target.expanded)
|
|
3392
|
+
})));
|
|
3393
|
+
for (const { target, result } of expansionResults) {
|
|
3394
|
+
if (result.dispatched) this.clickedQuestions.add(target.q);
|
|
3395
|
+
}
|
|
3396
|
+
if (!options.questionsOnly) {
|
|
3397
|
+
const targetQuestions = new Set(targets.map((target) => target.q));
|
|
3398
|
+
for (const clickedItem of await this.extractVisibleItems(page)) {
|
|
3399
|
+
if (!targetQuestions.has(clickedItem.question)) continue;
|
|
3400
|
+
capturedItems.set(clickedItem.question, clickedItem);
|
|
3258
3401
|
if (this.upsertCollectedRow(this.toFlatRow(clickedItem, 1, null, options.query))) {
|
|
3259
3402
|
await this.emitProgress("answer_captured");
|
|
3260
3403
|
}
|
|
3261
3404
|
}
|
|
3262
3405
|
}
|
|
3263
|
-
const
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3406
|
+
for (const { target, result } of expansionResults) {
|
|
3407
|
+
const captured = capturedItems.get(target.q);
|
|
3408
|
+
if (result.confirmed || Boolean(captured?.answer?.trim())) {
|
|
3409
|
+
processedQuestions.add(target.q);
|
|
3410
|
+
failedInteractions.delete(target.q);
|
|
3411
|
+
} else if ((clickAttempts.get(target.q) ?? 0) >= MAX_CLICK_ATTEMPTS) {
|
|
3412
|
+
failedInteractions.add(target.q);
|
|
3413
|
+
abandonedNeverReclick.add(target.q);
|
|
3414
|
+
}
|
|
3270
3415
|
}
|
|
3271
3416
|
this.paaLifecycle = {
|
|
3272
3417
|
...this.paaLifecycle,
|
|
@@ -4970,6 +5115,17 @@ function stripInternalDebug(result, keepDebug) {
|
|
|
4970
5115
|
delete diagnostics.debug;
|
|
4971
5116
|
return { ...result, diagnostics };
|
|
4972
5117
|
}
|
|
5118
|
+
async function resolveSurvivingGotoLinks(result) {
|
|
5119
|
+
try {
|
|
5120
|
+
return await resolveHarvestResultGoogleGotoLinks(result, { totalBudgetMs: 1e4 });
|
|
5121
|
+
} catch (error) {
|
|
5122
|
+
console.warn(JSON.stringify({
|
|
5123
|
+
event: "harvest_goto_resolution_failed",
|
|
5124
|
+
message: errorMessage(error)
|
|
5125
|
+
}));
|
|
5126
|
+
return result;
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
4973
5129
|
async function cleanupDisposableProxy(kernelApiKey, proxyId) {
|
|
4974
5130
|
if (!kernelApiKey || !proxyId) return;
|
|
4975
5131
|
try {
|
|
@@ -5184,7 +5340,8 @@ async function harvest(rawOptions) {
|
|
|
5184
5340
|
lastError = err;
|
|
5185
5341
|
if (willRetry2) continue;
|
|
5186
5342
|
if (attempt.result) {
|
|
5187
|
-
|
|
5343
|
+
const resolvedResult2 = await resolveSurvivingGotoLinks(attempt.result);
|
|
5344
|
+
return withResultQuality(stripInternalDebug(resolvedResult2, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
|
|
5188
5345
|
}
|
|
5189
5346
|
break;
|
|
5190
5347
|
}
|
|
@@ -5219,7 +5376,8 @@ async function harvest(rawOptions) {
|
|
|
5219
5376
|
if (willRetry2) continue;
|
|
5220
5377
|
break;
|
|
5221
5378
|
}
|
|
5222
|
-
const
|
|
5379
|
+
const resolvedResult = await resolveSurvivingGotoLinks(result);
|
|
5380
|
+
const finalResult = withResultQuality(stripInternalDebug(resolvedResult, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
|
|
5223
5381
|
const outcome = classifyAttemptResult(finalResult);
|
|
5224
5382
|
const willRetry = outcome === "degraded_result" && i < maxAttempts - 1;
|
|
5225
5383
|
const resultError = outcome === "degraded_result" ? degradedResultMessage(finalResult) : null;
|
package/dist/index.js
CHANGED
|
@@ -229,7 +229,7 @@ import {
|
|
|
229
229
|
serpArrivalUrl,
|
|
230
230
|
validateProviderSessionId,
|
|
231
231
|
withRuntimeUserAgent
|
|
232
|
-
} from "./chunk-
|
|
232
|
+
} from "./chunk-WEEAXPWK.js";
|
|
233
233
|
import {
|
|
234
234
|
AnalyticsConfirmedPersonProjectionSchema,
|
|
235
235
|
AnalyticsCrmFieldSchema,
|
|
@@ -316,7 +316,7 @@ import {
|
|
|
316
316
|
resolveDeploymentProfile,
|
|
317
317
|
resolveLocalSourcebookSchemaType,
|
|
318
318
|
transcribeMediaUrl
|
|
319
|
-
} from "./chunk-
|
|
319
|
+
} from "./chunk-JECPQBLZ.js";
|
|
320
320
|
import {
|
|
321
321
|
auditImageUrls,
|
|
322
322
|
auditImages,
|
|
@@ -435,7 +435,7 @@ import {
|
|
|
435
435
|
} from "./chunk-GGZEC22A.js";
|
|
436
436
|
import {
|
|
437
437
|
PACKAGE_VERSION
|
|
438
|
-
} from "./chunk-
|
|
438
|
+
} from "./chunk-2RTELL6M.js";
|
|
439
439
|
import {
|
|
440
440
|
CaptchaError,
|
|
441
441
|
MapsSearchIncompletePageError,
|
|
@@ -77738,7 +77738,7 @@ app.get("/cron/tick", async (c) => {
|
|
|
77738
77738
|
return c.json({ error: "Unauthorized" }, 401);
|
|
77739
77739
|
}
|
|
77740
77740
|
const startedAt = Date.now();
|
|
77741
|
-
const { drainQueue } = await import("./worker-
|
|
77741
|
+
const { drainQueue } = await import("./worker-2TYCYFPM.js");
|
|
77742
77742
|
const budget = { maxJobs: 10, deadlineMs: startedAt + CRON_TICK_DRAIN_BUDGET_MS };
|
|
77743
77743
|
const origin = `${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`;
|
|
77744
77744
|
const unfinished = [];
|