mcp-scraper 0.69.0 → 0.72.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 +60 -2
- package/README.md +6 -2
- package/dist/{analytics-repository-62CAZAIP.js → analytics-repository-6YLRSWYO.js} +3 -2
- package/dist/bin/api-server.cjs +18592 -12680
- package/dist/bin/api-server.js +4 -4
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-install.cjs +3 -3
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.cjs +10492 -9129
- package/dist/bin/mcp-stdio-server.js +9 -7
- package/dist/bin/paa-harvest.cjs +308 -41
- package/dist/bin/paa-harvest.js +4 -3
- package/dist/{chunk-M7D7WO75.js → chunk-2N4V4LVU.js} +3178 -2019
- package/dist/chunk-7RQULQF3.js +263 -0
- package/dist/{chunk-UL4ZKAWZ.js → chunk-A66DGFOU.js} +1 -1
- package/dist/{chunk-ES25GP6C.js → chunk-CXY5WV45.js} +24 -34
- package/dist/{chunk-Y46YNQMM.js → chunk-F5TK4KMT.js} +1 -1
- package/dist/{chunk-62LEXS5O.js → chunk-F6MUGMRN.js} +306 -44
- package/dist/{chunk-5RFQGIYC.js → chunk-FVL4GUTP.js} +2 -2
- package/dist/{chunk-OZIUFN6B.js → chunk-I45JV4EU.js} +2 -2
- package/dist/{chunk-2LXTOAKS.js → chunk-KIPIBPUB.js} +9 -1
- package/dist/chunk-MB72PA6S.js +2003 -0
- package/dist/chunk-OM7HVEJ3.js +26 -0
- package/dist/chunk-PJEEKOUM.js +404 -0
- package/dist/{chunk-RX2QAHML.js → chunk-RAFQEPJ4.js} +2 -2
- package/dist/{chunk-KO5CK5ZT.js → chunk-SR7GSLEA.js} +202 -441
- package/dist/chunk-T3MZISOF.js +240 -0
- package/dist/{chunk-ULYOCJUJ.js → chunk-TUTQHP4O.js} +1 -1
- package/dist/{chunk-65FTMB7G.js → chunk-WEFPBAAG.js} +180 -9
- package/dist/{chunk-LN6N3YLK.js → chunk-WFQ2E4WE.js} +1 -1
- package/dist/{db-ZGZAKYBW.js → db-566MOHHD.js} +7 -1
- package/dist/{extract-bundle-SXSJCPQI.js → extract-bundle-337ZDIIQ.js} +4 -4
- package/dist/gmail-service-E276SBJU.js +25 -0
- package/dist/index.cjs +322 -49
- package/dist/index.d.cts +15 -2
- package/dist/index.d.ts +15 -2
- package/dist/index.js +4 -3
- package/dist/{lead-list-enrichment-repository-CXLVIV3R.js → lead-list-enrichment-repository-WWEMJ7EN.js} +2 -2
- package/dist/{location-data-repository-G72M7DTN.js → location-data-repository-H4B2BWGY.js} +2 -2
- package/dist/{server-WCNYXILA.js → server-UGWC5YMD.js} +8376 -6616
- package/dist/{site-extract-repository-RLB3TFJ3.js → site-extract-repository-ACNAVNDN.js} +3 -3
- package/dist/{worker-KHSJG365.js → worker-JQORPCFQ.js} +9 -7
- package/package.json +1 -1
- package/dist/chunk-2L4C4DAZ.js +0 -103
package/dist/index.cjs
CHANGED
|
@@ -443,6 +443,7 @@ var NANGO_USD_PER_CONNECTION_MONTH = envRate("NANGO_USD_PER_CONNECTION_MONTH", 1
|
|
|
443
443
|
var NANGO_USD_PER_FUNCTION_RUN = envRate("NANGO_USD_PER_FUNCTION_RUN", 1e-4);
|
|
444
444
|
var NANGO_USD_PER_PROXY_REQUEST = envRate("NANGO_USD_PER_PROXY_REQUEST", 1e-4);
|
|
445
445
|
var NANGO_USD_PER_COMPUTE_SEC = envRate("NANGO_USD_PER_COMPUTE_SEC", 2e-4);
|
|
446
|
+
var BRIGHTDATA_BROWSER_USD_PER_GB = envRate("BRIGHTDATA_BROWSER_USD_PER_GB", 0);
|
|
446
447
|
function kernelCostUsd(ms, headful) {
|
|
447
448
|
const sec = Math.max(0, ms) / 1e3;
|
|
448
449
|
return sec * (headful ? KERNEL_HEADFUL_USD_PER_SEC : KERNEL_HEADLESS_USD_PER_SEC);
|
|
@@ -460,6 +461,9 @@ function vendorCostUsd(vendor, units) {
|
|
|
460
461
|
if (vendor === "nango_function_run") return Math.max(0, units) * NANGO_USD_PER_FUNCTION_RUN;
|
|
461
462
|
if (vendor === "nango_proxy_request") return Math.max(0, units) * NANGO_USD_PER_PROXY_REQUEST;
|
|
462
463
|
if (vendor === "nango_compute") return Math.max(0, units) * NANGO_USD_PER_COMPUTE_SEC;
|
|
464
|
+
if (vendor === "brightdata_browser_api") {
|
|
465
|
+
return Math.max(0, units) / 1e9 * BRIGHTDATA_BROWSER_USD_PER_GB;
|
|
466
|
+
}
|
|
463
467
|
return 0;
|
|
464
468
|
}
|
|
465
469
|
|
|
@@ -480,7 +484,8 @@ async function runCostTelemetryMigration() {
|
|
|
480
484
|
SELECT
|
|
481
485
|
(SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name IN ('kernel_session_log', 'vendor_usage_log', 'cost_probe_runs')) = 3
|
|
482
486
|
AND (SELECT COUNT(*) FROM pragma_table_info('kernel_session_log') WHERE name IN ('proxy_source', 'proxy_type', 'method')) = 3
|
|
483
|
-
AND (SELECT COUNT(*) FROM pragma_table_info('vendor_usage_log') WHERE name
|
|
487
|
+
AND (SELECT COUNT(*) FROM pragma_table_info('vendor_usage_log') WHERE name IN ('method', 'source_key')) = 2
|
|
488
|
+
AND (SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND name = 'vendor_usage_log_vendor_source_key') = 1
|
|
484
489
|
AND (SELECT COUNT(*) FROM pragma_table_info('cost_probe_runs') WHERE name IN ('units', 'unit_type', 'mode')) = 3
|
|
485
490
|
AS ready
|
|
486
491
|
`);
|
|
@@ -536,6 +541,7 @@ async function runCostTelemetryMigration() {
|
|
|
536
541
|
unit_type TEXT,
|
|
537
542
|
est_cost_usd REAL NOT NULL DEFAULT 0,
|
|
538
543
|
error TEXT,
|
|
544
|
+
source_key TEXT,
|
|
539
545
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
540
546
|
)
|
|
541
547
|
`);
|
|
@@ -546,6 +552,11 @@ async function runCostTelemetryMigration() {
|
|
|
546
552
|
await db.execute(`ALTER TABLE vendor_usage_log ADD COLUMN method TEXT`);
|
|
547
553
|
} catch {
|
|
548
554
|
}
|
|
555
|
+
try {
|
|
556
|
+
await db.execute(`ALTER TABLE vendor_usage_log ADD COLUMN source_key TEXT`);
|
|
557
|
+
} catch {
|
|
558
|
+
}
|
|
559
|
+
await db.execute(`CREATE UNIQUE INDEX IF NOT EXISTS vendor_usage_log_vendor_source_key ON vendor_usage_log(vendor, source_key) WHERE source_key IS NOT NULL`);
|
|
549
560
|
await db.execute(`
|
|
550
561
|
CREATE TABLE IF NOT EXISTS cost_probe_runs (
|
|
551
562
|
id TEXT PRIMARY KEY,
|
|
@@ -625,26 +636,29 @@ async function recordVendorUsage(r) {
|
|
|
625
636
|
await migrateCostTelemetry();
|
|
626
637
|
const ctx = currentCostContext();
|
|
627
638
|
const db = getDb();
|
|
628
|
-
await db.execute({
|
|
629
|
-
sql: `INSERT INTO vendor_usage_log
|
|
630
|
-
(id, op, probe_run_id, user_id, vendor, model, units, unit_type, est_cost_usd, error, method)
|
|
631
|
-
VALUES (
|
|
639
|
+
const result = await db.execute({
|
|
640
|
+
sql: `INSERT OR IGNORE INTO vendor_usage_log
|
|
641
|
+
(id, op, probe_run_id, user_id, vendor, model, units, unit_type, est_cost_usd, error, method, source_key)
|
|
642
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
632
643
|
args: [
|
|
633
644
|
(0, import_node_crypto2.randomUUID)(),
|
|
634
|
-
ctx?.op ?? null,
|
|
635
|
-
ctx?.probeRunId ?? null,
|
|
636
|
-
ctx?.userId ?? null,
|
|
645
|
+
r.op ?? ctx?.op ?? null,
|
|
646
|
+
r.probeRunId ?? ctx?.probeRunId ?? null,
|
|
647
|
+
r.userId ?? ctx?.userId ?? null,
|
|
637
648
|
r.vendor,
|
|
638
649
|
r.model ?? null,
|
|
639
650
|
r.units,
|
|
640
651
|
r.unitType,
|
|
641
652
|
vendorCostUsd(r.vendor, r.units),
|
|
642
653
|
r.error ?? null,
|
|
643
|
-
ctx?.subOp ?? null
|
|
654
|
+
r.method ?? ctx?.subOp ?? null,
|
|
655
|
+
r.sourceKey ?? null
|
|
644
656
|
]
|
|
645
657
|
});
|
|
658
|
+
return result.rowsAffected === 1;
|
|
646
659
|
} catch (err) {
|
|
647
660
|
console.warn("[cost-telemetry] recordVendorUsage failed:", err instanceof Error ? err.message : String(err));
|
|
661
|
+
return false;
|
|
648
662
|
}
|
|
649
663
|
}
|
|
650
664
|
function boolToInt(v) {
|
|
@@ -1177,6 +1191,13 @@ var BrowserDriver = class {
|
|
|
1177
1191
|
getKernelSessionId() {
|
|
1178
1192
|
return this.kernelSessionId;
|
|
1179
1193
|
}
|
|
1194
|
+
getProviderSessionMetadata() {
|
|
1195
|
+
return {
|
|
1196
|
+
provider: this.kernelSessionId ? "kernel" : "local",
|
|
1197
|
+
providerSessionId: this.kernelSessionId,
|
|
1198
|
+
disconnectObservation: null
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1180
1201
|
getDebugSnapshot() {
|
|
1181
1202
|
return this.debugSnapshot;
|
|
1182
1203
|
}
|
|
@@ -1191,6 +1212,7 @@ var BrowserDriver = class {
|
|
|
1191
1212
|
const proxySource = this.kernelProxySource;
|
|
1192
1213
|
const proxyType = this.kernelProxyType;
|
|
1193
1214
|
const headlessSent = this.kernelHeadlessSent;
|
|
1215
|
+
const provider = this.getProviderSessionMetadata();
|
|
1194
1216
|
this.browser = null;
|
|
1195
1217
|
this.context = null;
|
|
1196
1218
|
this.page = null;
|
|
@@ -1225,7 +1247,11 @@ var BrowserDriver = class {
|
|
|
1225
1247
|
kernelDeleteSucceeded: deleteResult.status === "fulfilled",
|
|
1226
1248
|
kernelDeleteError: deleteResult.status === "rejected" ? deleteResult.reason instanceof Error ? deleteResult.reason.message : String(deleteResult.reason) : null,
|
|
1227
1249
|
browserCloseSucceeded: closeResult.status === "fulfilled",
|
|
1228
|
-
browserCloseError: closeResult.status === "rejected" ? closeResult.reason instanceof Error ? closeResult.reason.message : String(closeResult.reason) : null
|
|
1250
|
+
browserCloseError: closeResult.status === "rejected" ? closeResult.reason instanceof Error ? closeResult.reason.message : String(closeResult.reason) : null,
|
|
1251
|
+
provider: provider.provider,
|
|
1252
|
+
providerSessionId: provider.providerSessionId,
|
|
1253
|
+
providerDisconnectObserved: false,
|
|
1254
|
+
providerDisconnectMessage: null
|
|
1229
1255
|
};
|
|
1230
1256
|
if (deleteResult.status === "rejected") {
|
|
1231
1257
|
console.warn(JSON.stringify({
|
|
@@ -1257,7 +1283,11 @@ var BrowserDriver = class {
|
|
|
1257
1283
|
kernelDeleteSucceeded: null,
|
|
1258
1284
|
kernelDeleteError: null,
|
|
1259
1285
|
browserCloseSucceeded: true,
|
|
1260
|
-
browserCloseError: null
|
|
1286
|
+
browserCloseError: null,
|
|
1287
|
+
provider: provider.provider,
|
|
1288
|
+
providerSessionId: provider.providerSessionId,
|
|
1289
|
+
providerDisconnectObserved: false,
|
|
1290
|
+
providerDisconnectMessage: null
|
|
1261
1291
|
};
|
|
1262
1292
|
} else if (this.context) {
|
|
1263
1293
|
const ctx = this.context;
|
|
@@ -1270,7 +1300,11 @@ var BrowserDriver = class {
|
|
|
1270
1300
|
kernelDeleteSucceeded: null,
|
|
1271
1301
|
kernelDeleteError: null,
|
|
1272
1302
|
browserCloseSucceeded: true,
|
|
1273
|
-
browserCloseError: null
|
|
1303
|
+
browserCloseError: null,
|
|
1304
|
+
provider: "local",
|
|
1305
|
+
providerSessionId: null,
|
|
1306
|
+
providerDisconnectObserved: false,
|
|
1307
|
+
providerDisconnectMessage: null
|
|
1274
1308
|
};
|
|
1275
1309
|
}
|
|
1276
1310
|
return {
|
|
@@ -1279,13 +1313,64 @@ var BrowserDriver = class {
|
|
|
1279
1313
|
kernelDeleteSucceeded: null,
|
|
1280
1314
|
kernelDeleteError: null,
|
|
1281
1315
|
browserCloseSucceeded: null,
|
|
1282
|
-
browserCloseError: null
|
|
1316
|
+
browserCloseError: null,
|
|
1317
|
+
provider: "local",
|
|
1318
|
+
providerSessionId: null,
|
|
1319
|
+
providerDisconnectObserved: false,
|
|
1320
|
+
providerDisconnectMessage: null
|
|
1283
1321
|
};
|
|
1284
1322
|
}
|
|
1285
1323
|
};
|
|
1286
1324
|
|
|
1287
1325
|
// src/driver/BrightDataSerpDriver.ts
|
|
1288
1326
|
var import_playwright2 = require("playwright");
|
|
1327
|
+
|
|
1328
|
+
// src/driver/browser-provider-telemetry.ts
|
|
1329
|
+
var MAX_PROVIDER_SESSION_ID_LENGTH = 512;
|
|
1330
|
+
var MAX_PROVIDER_MESSAGE_LENGTH = 320;
|
|
1331
|
+
function boundedText(value, maxLength) {
|
|
1332
|
+
if (typeof value !== "string") return null;
|
|
1333
|
+
const normalized = value.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
|
|
1334
|
+
if (!normalized) return null;
|
|
1335
|
+
return normalized.slice(0, maxLength);
|
|
1336
|
+
}
|
|
1337
|
+
function boundedProviderMessage(value) {
|
|
1338
|
+
const message = boundedText(value, MAX_PROVIDER_MESSAGE_LENGTH);
|
|
1339
|
+
if (!message) return null;
|
|
1340
|
+
return message.replace(/\b(?:wss?|https?):\/\/\S+/gi, "[redacted endpoint]").replace(/\b(?:authorization|proxy-authorization):\s*\S+/gi, "$1: [redacted]").replace(/\bBearer\s+\S+/gi, "Bearer [redacted]").slice(0, MAX_PROVIDER_MESSAGE_LENGTH);
|
|
1341
|
+
}
|
|
1342
|
+
function sanitizeProviderLocalMessage(value) {
|
|
1343
|
+
return boundedProviderMessage(value);
|
|
1344
|
+
}
|
|
1345
|
+
function validateProviderSessionId(value) {
|
|
1346
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_PROVIDER_SESSION_ID_LENGTH || value.trim() !== value || /[\s\u0000-\u001f\u007f]/.test(value) || /:\/\//.test(value)) {
|
|
1347
|
+
throw new Error("Bright Data did not return a valid browser session ID");
|
|
1348
|
+
}
|
|
1349
|
+
return value;
|
|
1350
|
+
}
|
|
1351
|
+
function makeProviderInterruptionObservation(source, message, observedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
1352
|
+
const safeMessage = boundedProviderMessage(message);
|
|
1353
|
+
return {
|
|
1354
|
+
source,
|
|
1355
|
+
message: safeMessage ?? source.replaceAll("_", " "),
|
|
1356
|
+
observedAt
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
function projectProviderSessionPublic(metadata) {
|
|
1360
|
+
return {
|
|
1361
|
+
provider: metadata.provider,
|
|
1362
|
+
providerDisconnectObserved: metadata.disconnectObservation !== null,
|
|
1363
|
+
providerDisconnectMessage: metadata.disconnectObservation?.message ?? null
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
function projectProviderSessionPrivate(metadata) {
|
|
1367
|
+
return {
|
|
1368
|
+
...projectProviderSessionPublic(metadata),
|
|
1369
|
+
providerSessionId: metadata.providerSessionId
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// src/driver/BrightDataSerpDriver.ts
|
|
1289
1374
|
var BLOCKED_TYPES = /* @__PURE__ */ new Set(["image", "media", "font", "stylesheet", "ping"]);
|
|
1290
1375
|
var BLOCKED_HOST_PARTS = [
|
|
1291
1376
|
"doubleclick.net",
|
|
@@ -1335,8 +1420,19 @@ var BrightDataSerpDriver = class {
|
|
|
1335
1420
|
browser = null;
|
|
1336
1421
|
context = null;
|
|
1337
1422
|
page = null;
|
|
1423
|
+
cdpSession = null;
|
|
1424
|
+
providerSessionId = null;
|
|
1425
|
+
disconnectObservation = null;
|
|
1426
|
+
closing = false;
|
|
1427
|
+
observeDisconnect(source, message) {
|
|
1428
|
+
if (this.closing || this.disconnectObservation) return;
|
|
1429
|
+
this.disconnectObservation = makeProviderInterruptionObservation(source, message);
|
|
1430
|
+
}
|
|
1338
1431
|
async launch(_config) {
|
|
1339
1432
|
if (this.page) return;
|
|
1433
|
+
this.closing = false;
|
|
1434
|
+
this.providerSessionId = null;
|
|
1435
|
+
this.disconnectObservation = null;
|
|
1340
1436
|
const endpoint = brightDataBrowserEndpoint();
|
|
1341
1437
|
if (!endpoint) throw new Error("SERP browser service is not configured");
|
|
1342
1438
|
this.browser = await import_playwright2.chromium.connectOverCDP(endpoint);
|
|
@@ -1352,6 +1448,14 @@ var BrightDataSerpDriver = class {
|
|
|
1352
1448
|
}
|
|
1353
1449
|
});
|
|
1354
1450
|
this.page = this.context.pages()[0] ?? await this.context.newPage();
|
|
1451
|
+
this.browser.on("disconnected", () => this.observeDisconnect("browser_disconnected"));
|
|
1452
|
+
this.context.on("close", () => this.observeDisconnect("context_closed"));
|
|
1453
|
+
this.page.on("close", () => this.observeDisconnect("page_closed"));
|
|
1454
|
+
this.page.on("crash", () => this.observeDisconnect("page_crashed"));
|
|
1455
|
+
this.cdpSession = await this.context.newCDPSession(this.page);
|
|
1456
|
+
const customCdp = this.cdpSession;
|
|
1457
|
+
const sessionResponse = await customCdp.send("Browser.getSessionId");
|
|
1458
|
+
this.providerSessionId = validateProviderSessionId(sessionResponse?.sessionId);
|
|
1355
1459
|
await this.page.route("**/*", async (route) => {
|
|
1356
1460
|
const request = route.request();
|
|
1357
1461
|
const resourceType = request.resourceType();
|
|
@@ -1407,23 +1511,38 @@ var BrightDataSerpDriver = class {
|
|
|
1407
1511
|
getKernelSessionId() {
|
|
1408
1512
|
return null;
|
|
1409
1513
|
}
|
|
1514
|
+
getProviderSessionMetadata() {
|
|
1515
|
+
return {
|
|
1516
|
+
provider: "bright_data_browser_api",
|
|
1517
|
+
providerSessionId: this.providerSessionId,
|
|
1518
|
+
disconnectObservation: this.disconnectObservation
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1410
1521
|
getDebugSnapshot() {
|
|
1411
1522
|
return { kernel: null, context: null, networkLocation: null, serpNavigation: null };
|
|
1412
1523
|
}
|
|
1413
1524
|
async close() {
|
|
1525
|
+
this.closing = true;
|
|
1414
1526
|
let error = null;
|
|
1415
1527
|
try {
|
|
1416
1528
|
await this.browser?.close();
|
|
1417
1529
|
} catch (caught) {
|
|
1418
|
-
error = caught instanceof Error ? caught.message : String(caught);
|
|
1530
|
+
error = sanitizeProviderLocalMessage(caught instanceof Error ? caught.message : String(caught)) ?? "Bright Data browser close failed";
|
|
1531
|
+
this.disconnectObservation ??= makeProviderInterruptionObservation("browser_close_error", error);
|
|
1419
1532
|
}
|
|
1533
|
+
const provider = projectProviderSessionPrivate(this.getProviderSessionMetadata());
|
|
1534
|
+
this.browser = null;
|
|
1535
|
+
this.context = null;
|
|
1536
|
+
this.page = null;
|
|
1537
|
+
this.cdpSession = null;
|
|
1420
1538
|
return {
|
|
1421
1539
|
kernelSessionId: null,
|
|
1422
1540
|
kernelDeleteStarted: false,
|
|
1423
1541
|
kernelDeleteSucceeded: null,
|
|
1424
1542
|
kernelDeleteError: null,
|
|
1425
1543
|
browserCloseSucceeded: error === null,
|
|
1426
|
-
browserCloseError: error
|
|
1544
|
+
browserCloseError: error,
|
|
1545
|
+
...provider
|
|
1427
1546
|
};
|
|
1428
1547
|
}
|
|
1429
1548
|
};
|
|
@@ -2319,12 +2438,14 @@ async function extractAISurfacesFromDocument(config) {
|
|
|
2319
2438
|
|
|
2320
2439
|
// src/extractor/PAAExtractor.ts
|
|
2321
2440
|
var PAAExtractor = class {
|
|
2322
|
-
constructor(driver, reporter) {
|
|
2441
|
+
constructor(driver, reporter, onPaaProgress) {
|
|
2323
2442
|
this.driver = driver;
|
|
2324
2443
|
this.reporter = reporter;
|
|
2444
|
+
this.onPaaProgress = onPaaProgress;
|
|
2325
2445
|
}
|
|
2326
2446
|
driver;
|
|
2327
2447
|
reporter;
|
|
2448
|
+
onPaaProgress;
|
|
2328
2449
|
completeness = {
|
|
2329
2450
|
paaWithoutAnswer: 0,
|
|
2330
2451
|
paaWithoutSource: 0,
|
|
@@ -2334,6 +2455,8 @@ var PAAExtractor = class {
|
|
|
2334
2455
|
aioShareUrlEarly = null;
|
|
2335
2456
|
collectedRows = [];
|
|
2336
2457
|
partialContext = null;
|
|
2458
|
+
progressSequence = 0;
|
|
2459
|
+
clickedQuestions = /* @__PURE__ */ new Set();
|
|
2337
2460
|
normalizeQuestion(q) {
|
|
2338
2461
|
return q.toLowerCase().replace(/[^\w\s]/g, "").replace(/\s+/g, " ").trim();
|
|
2339
2462
|
}
|
|
@@ -2370,6 +2493,34 @@ var PAAExtractor = class {
|
|
|
2370
2493
|
function cleanSourceLabel(value) {
|
|
2371
2494
|
return (value ?? "").replace(/\s*\(\+\d+\)\s*-\s*View related links.*$/i, "").replace(/\s*-\s*Opens in new tab.*$/i, "").trim();
|
|
2372
2495
|
}
|
|
2496
|
+
function isGoogleHost(host) {
|
|
2497
|
+
const normalized = host.toLowerCase().replace(/\.$/, "");
|
|
2498
|
+
return normalized === "google.com" || normalized.endsWith(".google.com") || /^google\.[a-z.]+$/.test(normalized) || /\.google\.[a-z.]+$/.test(normalized) || normalized === "googleusercontent.com" || normalized.endsWith(".googleusercontent.com");
|
|
2499
|
+
}
|
|
2500
|
+
function safeCitationTarget(rawHref) {
|
|
2501
|
+
let parsed;
|
|
2502
|
+
try {
|
|
2503
|
+
parsed = new URL(rawHref);
|
|
2504
|
+
} catch {
|
|
2505
|
+
return null;
|
|
2506
|
+
}
|
|
2507
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
2508
|
+
if (parsed.username || parsed.password) return null;
|
|
2509
|
+
if (isGoogleHost(parsed.hostname)) {
|
|
2510
|
+
if (parsed.pathname !== "/url") return null;
|
|
2511
|
+
const redirected = parsed.searchParams.get("q") ?? parsed.searchParams.get("url");
|
|
2512
|
+
if (!redirected) return null;
|
|
2513
|
+
try {
|
|
2514
|
+
parsed = new URL(redirected);
|
|
2515
|
+
} catch {
|
|
2516
|
+
return null;
|
|
2517
|
+
}
|
|
2518
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
2519
|
+
if (parsed.username || parsed.password) return null;
|
|
2520
|
+
}
|
|
2521
|
+
if (isGoogleHost(parsed.hostname)) return null;
|
|
2522
|
+
return { href: parsed.href, host: parsed.hostname };
|
|
2523
|
+
}
|
|
2373
2524
|
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) => {
|
|
2374
2525
|
const clickTarget = pair.querySelector(selectors.clickTarget);
|
|
2375
2526
|
const expanded = pair.classList.contains(selectors.expandedClass) || clickTarget?.getAttribute("aria-expanded") === "true";
|
|
@@ -2391,21 +2542,14 @@ var PAAExtractor = class {
|
|
|
2391
2542
|
...Array.from(anchorRoot.querySelectorAll("a[href]"))
|
|
2392
2543
|
];
|
|
2393
2544
|
for (const a of prioritized) {
|
|
2394
|
-
const
|
|
2395
|
-
if (
|
|
2396
|
-
|
|
2397
|
-
try {
|
|
2398
|
-
host = new URL(href).hostname;
|
|
2399
|
-
} catch {
|
|
2400
|
-
continue;
|
|
2401
|
-
}
|
|
2402
|
-
if (/(^|\.)google(usercontent)?\.[a-z.]+$/.test(host)) continue;
|
|
2403
|
-
seenHrefs.add(href);
|
|
2545
|
+
const target = safeCitationTarget(a.href);
|
|
2546
|
+
if (!target || seenHrefs.has(target.href)) continue;
|
|
2547
|
+
seenHrefs.add(target.href);
|
|
2404
2548
|
anchors.push({
|
|
2405
2549
|
text: (a.textContent || "").trim(),
|
|
2406
2550
|
label: cleanSourceLabel(a.getAttribute("aria-label")),
|
|
2407
|
-
href,
|
|
2408
|
-
host
|
|
2551
|
+
href: target.href,
|
|
2552
|
+
host: target.host
|
|
2409
2553
|
});
|
|
2410
2554
|
}
|
|
2411
2555
|
const primary = anchors.find((x) => x.label.length > 2 || x.text.length > 2) ?? anchors[0];
|
|
@@ -2462,10 +2606,53 @@ var PAAExtractor = class {
|
|
|
2462
2606
|
extracted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2463
2607
|
};
|
|
2464
2608
|
}
|
|
2609
|
+
upsertCollectedRow(row) {
|
|
2610
|
+
const key = this.normalizeQuestion(row.question);
|
|
2611
|
+
const index = this.collectedRows.findIndex((existing2) => this.normalizeQuestion(existing2.question) === key);
|
|
2612
|
+
if (index < 0) {
|
|
2613
|
+
this.collectedRows.push({ ...row });
|
|
2614
|
+
return true;
|
|
2615
|
+
}
|
|
2616
|
+
const existing = this.collectedRows[index];
|
|
2617
|
+
const merged = {
|
|
2618
|
+
...existing,
|
|
2619
|
+
...row,
|
|
2620
|
+
answer: row.answer || existing.answer,
|
|
2621
|
+
source_title: row.source_title || existing.source_title,
|
|
2622
|
+
source_site: row.source_site || existing.source_site,
|
|
2623
|
+
source_cite: row.source_cite || existing.source_cite,
|
|
2624
|
+
extracted_at: row.extracted_at || existing.extracted_at
|
|
2625
|
+
};
|
|
2626
|
+
const changed = merged.answer !== existing.answer || merged.source_title !== existing.source_title || merged.source_site !== existing.source_site || merged.source_cite !== existing.source_cite;
|
|
2627
|
+
if (changed) this.collectedRows[index] = merged;
|
|
2628
|
+
return changed;
|
|
2629
|
+
}
|
|
2630
|
+
async emitProgress(phase) {
|
|
2631
|
+
if (!this.onPaaProgress || this.collectedRows.length === 0) return;
|
|
2632
|
+
const snapshot = {
|
|
2633
|
+
sequence: ++this.progressSequence,
|
|
2634
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2635
|
+
clickedQuestions: [...this.clickedQuestions],
|
|
2636
|
+
phase,
|
|
2637
|
+
records: this.collectedRows.map((row) => ({ ...row }))
|
|
2638
|
+
};
|
|
2639
|
+
try {
|
|
2640
|
+
await this.onPaaProgress(snapshot);
|
|
2641
|
+
} catch (err) {
|
|
2642
|
+
console.warn(JSON.stringify({
|
|
2643
|
+
event: "paa_progress_sink_failed",
|
|
2644
|
+
sequence: snapshot.sequence,
|
|
2645
|
+
phase: snapshot.phase,
|
|
2646
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2647
|
+
}));
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2465
2650
|
getPartialResult() {
|
|
2466
2651
|
const ctx = this.partialContext;
|
|
2467
2652
|
if (!ctx || this.collectedRows.length === 0) return null;
|
|
2468
|
-
const flat =
|
|
2653
|
+
const flat = this.collectedRows.map((row) => ({ ...row }));
|
|
2654
|
+
const paaWithoutAnswer = ctx.questionsOnly ? 0 : flat.filter((row) => !row.answer?.trim()).length;
|
|
2655
|
+
const paaWithoutSource = ctx.questionsOnly ? 0 : flat.filter((row) => !row.source_cite?.trim()).length;
|
|
2469
2656
|
const stats = {
|
|
2470
2657
|
seed: ctx.seed,
|
|
2471
2658
|
totalQuestions: flat.length,
|
|
@@ -2480,7 +2667,17 @@ var PAAExtractor = class {
|
|
|
2480
2667
|
diagnostics: {
|
|
2481
2668
|
completionStatus: "paa_found",
|
|
2482
2669
|
problem: null,
|
|
2483
|
-
|
|
2670
|
+
resultQuality: "partial",
|
|
2671
|
+
degradedResult: false,
|
|
2672
|
+
retryRecommended: true,
|
|
2673
|
+
completeness: {
|
|
2674
|
+
...this.completeness,
|
|
2675
|
+
paaWithoutAnswer,
|
|
2676
|
+
paaWithoutSource,
|
|
2677
|
+
paaRequested: ctx.maxQuestions,
|
|
2678
|
+
paaReturned: flat.length,
|
|
2679
|
+
paaUnique: flat.length
|
|
2680
|
+
},
|
|
2484
2681
|
warnings: [{
|
|
2485
2682
|
code: "paa_partial",
|
|
2486
2683
|
surface: "paa",
|
|
@@ -2489,6 +2686,9 @@ var PAAExtractor = class {
|
|
|
2489
2686
|
}]
|
|
2490
2687
|
},
|
|
2491
2688
|
totalQuestions: flat.length,
|
|
2689
|
+
surface: "web",
|
|
2690
|
+
aiOverview: { detected: false, text: null, citations: [] },
|
|
2691
|
+
aiMode: { detected: false, text: null, citations: [] },
|
|
2492
2692
|
whatPeopleSaying: [],
|
|
2493
2693
|
tree: this.buildTree(flat, ctx.seed),
|
|
2494
2694
|
flat,
|
|
@@ -2496,7 +2696,7 @@ var PAAExtractor = class {
|
|
|
2496
2696
|
forums: [],
|
|
2497
2697
|
organicResults: [],
|
|
2498
2698
|
localPack: [],
|
|
2499
|
-
entityIds: {},
|
|
2699
|
+
entityIds: { entities: [], kgIds: [], cids: [], gcids: [] },
|
|
2500
2700
|
stats
|
|
2501
2701
|
};
|
|
2502
2702
|
}
|
|
@@ -2506,7 +2706,8 @@ var PAAExtractor = class {
|
|
|
2506
2706
|
const seenQs = /* @__PURE__ */ new Set();
|
|
2507
2707
|
const orderedQs = [];
|
|
2508
2708
|
this.collectedRows = [];
|
|
2509
|
-
|
|
2709
|
+
this.progressSequence = 0;
|
|
2710
|
+
this.clickedQuestions = /* @__PURE__ */ new Set();
|
|
2510
2711
|
const abandonedNeverReclick = /* @__PURE__ */ new Set();
|
|
2511
2712
|
const clickedOnceEver = /* @__PURE__ */ new Set();
|
|
2512
2713
|
const capturedItems = /* @__PURE__ */ new Map();
|
|
@@ -2554,12 +2755,17 @@ var PAAExtractor = class {
|
|
|
2554
2755
|
if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
|
|
2555
2756
|
this.throwIfAborted(signal);
|
|
2556
2757
|
const states = await readItemStates();
|
|
2758
|
+
let discovered = false;
|
|
2557
2759
|
for (const s of states) {
|
|
2558
2760
|
if (!seenQs.has(s.q)) {
|
|
2559
2761
|
seenQs.add(s.q);
|
|
2560
2762
|
orderedQs.push(s.q);
|
|
2763
|
+
if (orderedQs.length <= options.maxQuestions) {
|
|
2764
|
+
discovered = this.upsertCollectedRow(this.toFlatRow({ question: s.q }, 1, null, options.query)) || discovered;
|
|
2765
|
+
}
|
|
2561
2766
|
}
|
|
2562
2767
|
}
|
|
2768
|
+
if (discovered) await this.emitProgress("discovered");
|
|
2563
2769
|
if (options.questionsOnly && seenQs.size >= options.maxQuestions) break;
|
|
2564
2770
|
const kept = new Set(orderedQs.slice(0, options.maxQuestions));
|
|
2565
2771
|
const clickable = states.filter((s) => kept.has(s.q) && !clickedOnceEver.has(s.q) && !abandonedNeverReclick.has(s.q));
|
|
@@ -2579,6 +2785,7 @@ var PAAExtractor = class {
|
|
|
2579
2785
|
this.reporter.onDepth(++round);
|
|
2580
2786
|
await this.throwIfCaptcha(page, "Google PAA expansion");
|
|
2581
2787
|
clickedOnceEver.add(target.q);
|
|
2788
|
+
this.clickedQuestions.add(target.q);
|
|
2582
2789
|
const expansionStatus = await expandOneItemSerially(target.q);
|
|
2583
2790
|
if (!options.questionsOnly) {
|
|
2584
2791
|
let clickedItem = (await this.extractVisibleItems(page, target.q))[0];
|
|
@@ -2586,7 +2793,12 @@ var PAAExtractor = class {
|
|
|
2586
2793
|
await page.waitForTimeout(1300);
|
|
2587
2794
|
clickedItem = (await this.extractVisibleItems(page, target.q))[0];
|
|
2588
2795
|
}
|
|
2589
|
-
if (clickedItem
|
|
2796
|
+
if (clickedItem) {
|
|
2797
|
+
capturedItems.set(target.q, clickedItem);
|
|
2798
|
+
if (this.upsertCollectedRow(this.toFlatRow(clickedItem, 1, null, options.query))) {
|
|
2799
|
+
await this.emitProgress("answer_captured");
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2590
2802
|
}
|
|
2591
2803
|
if (expansionStatus === "failed" && !capturedItems.has(target.q)) {
|
|
2592
2804
|
abandonedNeverReclick.add(target.q);
|
|
@@ -2602,14 +2814,19 @@ var PAAExtractor = class {
|
|
|
2602
2814
|
if (!seenQs.has(state.q)) {
|
|
2603
2815
|
seenQs.add(state.q);
|
|
2604
2816
|
orderedQs.push(state.q);
|
|
2817
|
+
if (orderedQs.length <= options.maxQuestions) {
|
|
2818
|
+
this.upsertCollectedRow(this.toFlatRow({ question: state.q }, 1, null, options.query));
|
|
2819
|
+
}
|
|
2605
2820
|
}
|
|
2606
2821
|
}
|
|
2822
|
+
await this.emitProgress("discovered");
|
|
2607
2823
|
}
|
|
2608
2824
|
}
|
|
2609
2825
|
const itemMap = options.questionsOnly ? /* @__PURE__ */ new Map() : new Map(capturedItems);
|
|
2610
2826
|
if (!options.questionsOnly) {
|
|
2611
2827
|
await this.fillIncompleteItems(page, orderedQs, itemMap, options, abandonedNeverReclick);
|
|
2612
2828
|
}
|
|
2829
|
+
const results = [];
|
|
2613
2830
|
for (const q of orderedQs) {
|
|
2614
2831
|
if (results.length >= options.maxQuestions) break;
|
|
2615
2832
|
const key = this.normalizeQuestion(q);
|
|
@@ -2617,14 +2834,22 @@ var PAAExtractor = class {
|
|
|
2617
2834
|
seenKeys.add(key);
|
|
2618
2835
|
const item = itemMap.get(q);
|
|
2619
2836
|
if (item) {
|
|
2620
|
-
|
|
2837
|
+
const row = this.toFlatRow(item, 1, null, options.query);
|
|
2838
|
+
this.upsertCollectedRow(row);
|
|
2839
|
+
results.push(row);
|
|
2621
2840
|
this.reporter.onQuestion({ question: item.question, answer: item.answer ?? null, sourceTitle: item.sourceTitle ?? null, sourceSite: item.sourceSite ?? null, sourceCite: item.sourceCite ?? null, depth: 1, parentQuestion: null, children: [] });
|
|
2622
2841
|
} else {
|
|
2623
|
-
|
|
2842
|
+
const row = this.toFlatRow({ question: q, answer: void 0, sourceTitle: void 0, sourceSite: void 0, sourceCite: void 0 }, 1, null, options.query);
|
|
2843
|
+
this.upsertCollectedRow(row);
|
|
2844
|
+
results.push(row);
|
|
2624
2845
|
}
|
|
2625
2846
|
}
|
|
2626
2847
|
this.completeness.paaWithoutAnswer = options.questionsOnly ? 0 : results.filter((r) => !r.answer).length;
|
|
2627
|
-
this.completeness.paaWithoutSource = options.questionsOnly ? 0 : results.filter((r) => !r.
|
|
2848
|
+
this.completeness.paaWithoutSource = options.questionsOnly ? 0 : results.filter((r) => !r.source_cite).length;
|
|
2849
|
+
this.completeness.paaRequested = options.maxQuestions;
|
|
2850
|
+
this.completeness.paaReturned = results.length;
|
|
2851
|
+
this.completeness.paaUnique = seenKeys.size;
|
|
2852
|
+
await this.emitProgress("fill_incomplete");
|
|
2628
2853
|
return results;
|
|
2629
2854
|
}
|
|
2630
2855
|
async fillIncompleteItems(page, orderedQs, itemMap, options, abandonedNeverReclick) {
|
|
@@ -2649,6 +2874,10 @@ var PAAExtractor = class {
|
|
|
2649
2874
|
sourceSite: item.sourceSite ?? existing?.sourceSite,
|
|
2650
2875
|
sourceCite: item.sourceCite ?? existing?.sourceCite
|
|
2651
2876
|
});
|
|
2877
|
+
const upgraded = itemMap.get(q);
|
|
2878
|
+
if (upgraded && this.upsertCollectedRow(this.toFlatRow(upgraded, 1, null, options.query))) {
|
|
2879
|
+
await this.emitProgress("fill_incomplete");
|
|
2880
|
+
}
|
|
2652
2881
|
}
|
|
2653
2882
|
this.completeness.paaAnswersRecovered = [...missingAnswersBefore].filter((q) => itemMap.get(q)?.answer).length;
|
|
2654
2883
|
}
|
|
@@ -3113,7 +3342,15 @@ var PAAExtractor = class {
|
|
|
3113
3342
|
this.completeness = { paaWithoutAnswer: 0, paaWithoutSource: 0, paaAnswersRecovered: 0, aioShareCaptured: null };
|
|
3114
3343
|
this.aioShareUrlEarly = null;
|
|
3115
3344
|
this.collectedRows = [];
|
|
3116
|
-
this.
|
|
3345
|
+
this.progressSequence = 0;
|
|
3346
|
+
this.clickedQuestions = /* @__PURE__ */ new Set();
|
|
3347
|
+
this.partialContext = {
|
|
3348
|
+
seed: options.query,
|
|
3349
|
+
location: options.location ?? null,
|
|
3350
|
+
startMs,
|
|
3351
|
+
maxQuestions: options.maxQuestions,
|
|
3352
|
+
questionsOnly: options.questionsOnly
|
|
3353
|
+
};
|
|
3117
3354
|
const isMobile = options.device === "mobile";
|
|
3118
3355
|
const config = {
|
|
3119
3356
|
headless: options.headless,
|
|
@@ -3185,6 +3422,7 @@ var PAAExtractor = class {
|
|
|
3185
3422
|
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3186
3423
|
diagnostics: {
|
|
3187
3424
|
completionStatus: hasPaa ? "paa_found" : "no_paa",
|
|
3425
|
+
...!hasPaa ? { noPaaObserved: true } : {},
|
|
3188
3426
|
problem: null,
|
|
3189
3427
|
completeness: { ...this.completeness },
|
|
3190
3428
|
...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, void 0, locationResolution) } : {}
|
|
@@ -3304,6 +3542,7 @@ var PAAExtractor = class {
|
|
|
3304
3542
|
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3305
3543
|
diagnostics: {
|
|
3306
3544
|
completionStatus: "no_paa",
|
|
3545
|
+
noPaaObserved: true,
|
|
3307
3546
|
problem: null,
|
|
3308
3547
|
completeness: { ...this.completeness },
|
|
3309
3548
|
...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence2, locationResolution) } : {}
|
|
@@ -3395,6 +3634,8 @@ var PAAExtractor = class {
|
|
|
3395
3634
|
};
|
|
3396
3635
|
} catch (err) {
|
|
3397
3636
|
errorCount++;
|
|
3637
|
+
await this.emitProgress("attempt_ending").catch(() => {
|
|
3638
|
+
});
|
|
3398
3639
|
this.reporter.onError(err instanceof Error ? err : new Error(String(err)));
|
|
3399
3640
|
throw err;
|
|
3400
3641
|
}
|
|
@@ -4074,6 +4315,11 @@ function getAttemptLogSink(rawOptions) {
|
|
|
4074
4315
|
const sink = rawOptions.onAttemptEvent;
|
|
4075
4316
|
return typeof sink === "function" ? sink : void 0;
|
|
4076
4317
|
}
|
|
4318
|
+
function getPaaProgressSink(rawOptions) {
|
|
4319
|
+
if (!rawOptions || typeof rawOptions !== "object") return void 0;
|
|
4320
|
+
const sink = rawOptions.onPaaProgress;
|
|
4321
|
+
return typeof sink === "function" ? sink : void 0;
|
|
4322
|
+
}
|
|
4077
4323
|
async function emitAttemptEvent(sink, event) {
|
|
4078
4324
|
if (!sink) return;
|
|
4079
4325
|
try {
|
|
@@ -4106,17 +4352,32 @@ function classifyAttemptResult(result) {
|
|
|
4106
4352
|
function resultHasUsableContent(result) {
|
|
4107
4353
|
return result.flat.length > 0 || result.organicResults.length > 0 || result.localPack.length > 0 || result.videos.length > 0 || result.forums.length > 0 || result.whatPeopleSaying.length > 0 || result.aiOverview.detected || result.aiMode.detected;
|
|
4108
4354
|
}
|
|
4109
|
-
function withResultQuality(result) {
|
|
4355
|
+
function withResultQuality(result, requestedPaaCount, questionsOnly) {
|
|
4110
4356
|
const hasUsableContent = resultHasUsableContent(result);
|
|
4111
4357
|
const hasWarnings = (result.diagnostics.warnings?.length ?? 0) > 0;
|
|
4112
|
-
const
|
|
4358
|
+
const uniquePaaCount = new Set(result.flat.map((row) => row.question.toLowerCase().replace(/[^\w\s]/g, "").replace(/\s+/g, " ").trim())).size;
|
|
4359
|
+
const paaWithoutAnswer = questionsOnly ? 0 : result.flat.filter((row) => !row.answer?.trim()).length;
|
|
4360
|
+
const paaWithoutSource = questionsOnly ? 0 : result.flat.filter((row) => !row.source_cite?.trim()).length;
|
|
4361
|
+
const noPaa = result.diagnostics.completionStatus === "no_paa" && (result.diagnostics.noPaaObserved === true || hasUsableContent);
|
|
4362
|
+
const hasPaa = result.diagnostics.completionStatus === "paa_found";
|
|
4363
|
+
const paaIncomplete = hasPaa && (uniquePaaCount < requestedPaaCount || paaWithoutAnswer > 0 || paaWithoutSource > 0 || Boolean(result.diagnostics.interruption));
|
|
4364
|
+
const resultQuality = noPaa ? "complete" : hasPaa && uniquePaaCount > 0 ? paaIncomplete || hasWarnings ? "partial" : "complete" : hasUsableContent ? hasWarnings ? "partial" : "complete" : "degraded";
|
|
4113
4365
|
const degradedResult = resultQuality === "degraded";
|
|
4114
4366
|
result.diagnostics = {
|
|
4115
4367
|
...result.diagnostics,
|
|
4116
4368
|
resultQuality,
|
|
4117
4369
|
degradedResult,
|
|
4118
4370
|
degradationReasons: degradedResult ? ["empty_primary_serp"] : [],
|
|
4119
|
-
retryRecommended: degradedResult
|
|
4371
|
+
retryRecommended: degradedResult || resultQuality === "partial" && (paaIncomplete || result.diagnostics.warnings?.some((warning) => warning.retryable) === true),
|
|
4372
|
+
completeness: {
|
|
4373
|
+
paaWithoutAnswer,
|
|
4374
|
+
paaWithoutSource,
|
|
4375
|
+
paaAnswersRecovered: result.diagnostics.completeness?.paaAnswersRecovered ?? 0,
|
|
4376
|
+
aioShareCaptured: result.diagnostics.completeness?.aioShareCaptured ?? null,
|
|
4377
|
+
paaRequested: requestedPaaCount,
|
|
4378
|
+
paaReturned: result.flat.length,
|
|
4379
|
+
paaUnique: uniquePaaCount
|
|
4380
|
+
}
|
|
4120
4381
|
};
|
|
4121
4382
|
return result;
|
|
4122
4383
|
}
|
|
@@ -4176,10 +4437,10 @@ async function cleanupDisposableProxy(kernelApiKey, proxyId) {
|
|
|
4176
4437
|
}));
|
|
4177
4438
|
}
|
|
4178
4439
|
}
|
|
4179
|
-
async function extractOnce(options, signal, forceManagedSerp = false) {
|
|
4440
|
+
async function extractOnce(options, signal, forceManagedSerp = false, onPaaProgress) {
|
|
4180
4441
|
const driver = createSerpDriver(options, { forceManaged: forceManagedSerp });
|
|
4181
4442
|
const reporter = new ProgressReporter();
|
|
4182
|
-
const extractor = new PAAExtractor(driver, reporter);
|
|
4443
|
+
const extractor = new PAAExtractor(driver, reporter, onPaaProgress);
|
|
4183
4444
|
if (signal?.aborted) {
|
|
4184
4445
|
return {
|
|
4185
4446
|
result: null,
|
|
@@ -4231,17 +4492,25 @@ async function extractOnce(options, signal, forceManagedSerp = false) {
|
|
|
4231
4492
|
}
|
|
4232
4493
|
if (error) {
|
|
4233
4494
|
const outcome = classifyAttemptError(error);
|
|
4234
|
-
const
|
|
4495
|
+
const interruptionCode = outcome === "timeout" || outcome === "request_aborted" || outcome === "browser_session_interrupted" ? outcome : null;
|
|
4235
4496
|
let salvaged = null;
|
|
4236
|
-
if (
|
|
4497
|
+
if (interruptionCode) {
|
|
4237
4498
|
try {
|
|
4238
4499
|
salvaged = typeof extractor.getPartialResult === "function" ? extractor.getPartialResult() : null;
|
|
4239
4500
|
} catch {
|
|
4240
4501
|
salvaged = null;
|
|
4241
4502
|
}
|
|
4242
4503
|
}
|
|
4243
|
-
if (salvaged) {
|
|
4244
|
-
|
|
4504
|
+
if (salvaged && interruptionCode) {
|
|
4505
|
+
salvaged.diagnostics = {
|
|
4506
|
+
...salvaged.diagnostics,
|
|
4507
|
+
interruption: {
|
|
4508
|
+
code: interruptionCode,
|
|
4509
|
+
reasonSource: "client_runtime",
|
|
4510
|
+
message: errorMessage(error)
|
|
4511
|
+
}
|
|
4512
|
+
};
|
|
4513
|
+
return { result: salvaged, error, cleanup, debug, salvagedFrom: error };
|
|
4245
4514
|
}
|
|
4246
4515
|
return { result: null, error, cleanup, debug };
|
|
4247
4516
|
}
|
|
@@ -4251,6 +4520,7 @@ async function harvest(rawOptions) {
|
|
|
4251
4520
|
const raw = typeof rawOptions === "object" && rawOptions !== null ? rawOptions : {};
|
|
4252
4521
|
const signal = getAbortSignal(rawOptions);
|
|
4253
4522
|
const onAttemptEvent = getAttemptLogSink(rawOptions);
|
|
4523
|
+
const onPaaProgress = getPaaProgressSink(rawOptions);
|
|
4254
4524
|
const forceManagedSerp = raw.forceManagedSerp === true;
|
|
4255
4525
|
const requestedProxyMode = raw.proxyMode;
|
|
4256
4526
|
const proxyMode = requestedProxyMode === "location" || requestedProxyMode === "none" || requestedProxyMode === "configured" ? requestedProxyMode : DEFAULT_PROXY_MODE;
|
|
@@ -4320,7 +4590,7 @@ async function harvest(rawOptions) {
|
|
|
4320
4590
|
...baseCtx,
|
|
4321
4591
|
forceHeadless: allowHeadlessTest,
|
|
4322
4592
|
forceHeadful: !allowHeadlessTest
|
|
4323
|
-
}, () => extractOnce(attemptOptions, signal, forceManagedSerp)) : await extractOnce(attemptOptions, signal, forceManagedSerp);
|
|
4593
|
+
}, () => extractOnce(attemptOptions, signal, forceManagedSerp, onPaaProgress)) : await extractOnce(attemptOptions, signal, forceManagedSerp, onPaaProgress);
|
|
4324
4594
|
if (attempt.error) {
|
|
4325
4595
|
const err = attempt.error;
|
|
4326
4596
|
const outcome2 = classifyAttemptError(err);
|
|
@@ -4353,7 +4623,7 @@ async function harvest(rawOptions) {
|
|
|
4353
4623
|
maxAttempts,
|
|
4354
4624
|
outcome: outcome2,
|
|
4355
4625
|
kernelSessionId: attempt.cleanup.kernelSessionId,
|
|
4356
|
-
questionCount: 0,
|
|
4626
|
+
questionCount: attempt.result?.totalQuestions ?? 0,
|
|
4357
4627
|
durationMs: Date.now() - startedAtMs,
|
|
4358
4628
|
error: errorMessage(err),
|
|
4359
4629
|
willRetry: willRetry2,
|
|
@@ -4364,6 +4634,9 @@ async function harvest(rawOptions) {
|
|
|
4364
4634
|
await cleanupDisposableProxy(kernelApiKey, resolution2.disposableProxyId);
|
|
4365
4635
|
lastError = err;
|
|
4366
4636
|
if (willRetry2) continue;
|
|
4637
|
+
if (attempt.result) {
|
|
4638
|
+
return withResultQuality(stripInternalDebug(attempt.result, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
|
|
4639
|
+
}
|
|
4367
4640
|
break;
|
|
4368
4641
|
}
|
|
4369
4642
|
const result = attempt.result;
|
|
@@ -4397,7 +4670,7 @@ async function harvest(rawOptions) {
|
|
|
4397
4670
|
if (willRetry2) continue;
|
|
4398
4671
|
break;
|
|
4399
4672
|
}
|
|
4400
|
-
const finalResult = withResultQuality(stripInternalDebug(result, requestedDebug));
|
|
4673
|
+
const finalResult = withResultQuality(stripInternalDebug(result, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
|
|
4401
4674
|
const outcome = classifyAttemptResult(finalResult);
|
|
4402
4675
|
const willRetry = outcome === "degraded_result" && i < maxAttempts - 1;
|
|
4403
4676
|
const resultError = outcome === "degraded_result" ? degradedResultMessage(finalResult) : null;
|