@skrillex1224/playwright-toolkit 2.1.164 → 2.1.166
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/dist/index.cjs +635 -29
- package/dist/index.cjs.map +3 -3
- package/dist/index.js +635 -29
- package/dist/index.js.map +3 -3
- package/index.d.ts +2 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1387,6 +1387,27 @@ var resolveRouteByProxy = ({
|
|
|
1387
1387
|
// src/traffic-meter.js
|
|
1388
1388
|
var logger6 = createInternalLogger("TrafficMeter");
|
|
1389
1389
|
var encoder = new TextEncoder();
|
|
1390
|
+
var MAX_DOMAIN_BUCKETS = 160;
|
|
1391
|
+
var MAX_REASON_BUCKETS = 64;
|
|
1392
|
+
var MAX_TOP_ITEMS = 12;
|
|
1393
|
+
var MAX_HINT_ITEMS = 8;
|
|
1394
|
+
var UNKNOWN_DOMAIN = "(unknown)";
|
|
1395
|
+
var OTHER_DOMAINS = "(other-domains)";
|
|
1396
|
+
var OTHER_REASONS = "(other-reasons)";
|
|
1397
|
+
var STATIC_RESOURCE_TYPES = /* @__PURE__ */ new Set([
|
|
1398
|
+
"script",
|
|
1399
|
+
"stylesheet",
|
|
1400
|
+
"image",
|
|
1401
|
+
"font",
|
|
1402
|
+
"media",
|
|
1403
|
+
"manifest"
|
|
1404
|
+
]);
|
|
1405
|
+
var BACKEND_RESOURCE_TYPES = /* @__PURE__ */ new Set([
|
|
1406
|
+
"xhr",
|
|
1407
|
+
"fetch",
|
|
1408
|
+
"websocket",
|
|
1409
|
+
"eventsource"
|
|
1410
|
+
]);
|
|
1390
1411
|
var toSafeNumber = (value) => {
|
|
1391
1412
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
|
|
1392
1413
|
return Math.round(value);
|
|
@@ -1430,9 +1451,77 @@ var createTrafficState = () => ({
|
|
|
1430
1451
|
directUploadBytes: 0,
|
|
1431
1452
|
totalDownloadBytes: 0,
|
|
1432
1453
|
proxyDownloadBytes: 0,
|
|
1433
|
-
directDownloadBytes: 0
|
|
1454
|
+
directDownloadBytes: 0,
|
|
1455
|
+
totalFailedRequests: 0,
|
|
1456
|
+
proxyFailedRequests: 0,
|
|
1457
|
+
directFailedRequests: 0,
|
|
1458
|
+
totalCanceledRequests: 0,
|
|
1459
|
+
proxyCanceledRequests: 0,
|
|
1460
|
+
directCanceledRequests: 0,
|
|
1461
|
+
orphanDataReceivedBytes: 0,
|
|
1462
|
+
orphanProxyDataReceivedBytes: 0,
|
|
1463
|
+
orphanFinishDeltaBytes: 0,
|
|
1464
|
+
orphanProxyFinishDeltaBytes: 0,
|
|
1465
|
+
domainStats: /* @__PURE__ */ new Map(),
|
|
1466
|
+
typeStats: /* @__PURE__ */ new Map(),
|
|
1467
|
+
failedReasonStats: /* @__PURE__ */ new Map()
|
|
1434
1468
|
});
|
|
1435
1469
|
var ensureRoute = (route) => route === "proxy" ? "proxy" : "direct";
|
|
1470
|
+
var normalizeResourceType = (value) => {
|
|
1471
|
+
const type = String(value || "").trim().toLowerCase();
|
|
1472
|
+
if (!type) return "other";
|
|
1473
|
+
if (type === "ws") return "websocket";
|
|
1474
|
+
return type;
|
|
1475
|
+
};
|
|
1476
|
+
var parseHostname = (url = "") => {
|
|
1477
|
+
try {
|
|
1478
|
+
const hostname = new URL(String(url || "")).hostname.toLowerCase();
|
|
1479
|
+
return hostname || "";
|
|
1480
|
+
} catch {
|
|
1481
|
+
return "";
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
var normalizeDomainKey = (domain = "") => {
|
|
1485
|
+
const key = String(domain || "").trim().toLowerCase();
|
|
1486
|
+
return key || UNKNOWN_DOMAIN;
|
|
1487
|
+
};
|
|
1488
|
+
var isStaticType = (resourceType = "") => STATIC_RESOURCE_TYPES.has(normalizeResourceType(resourceType));
|
|
1489
|
+
var isBackendType = (resourceType = "") => BACKEND_RESOURCE_TYPES.has(normalizeResourceType(resourceType));
|
|
1490
|
+
var createDomainBucket = (domain) => ({
|
|
1491
|
+
domain,
|
|
1492
|
+
requests: 0,
|
|
1493
|
+
proxyRequests: 0,
|
|
1494
|
+
directRequests: 0,
|
|
1495
|
+
uploadBytes: 0,
|
|
1496
|
+
downloadBytes: 0,
|
|
1497
|
+
totalBytes: 0,
|
|
1498
|
+
proxyBytes: 0,
|
|
1499
|
+
directBytes: 0,
|
|
1500
|
+
failedRequests: 0,
|
|
1501
|
+
canceledRequests: 0,
|
|
1502
|
+
staticBytes: 0,
|
|
1503
|
+
backendBytes: 0
|
|
1504
|
+
});
|
|
1505
|
+
var createTypeBucket = (resourceType) => ({
|
|
1506
|
+
resourceType,
|
|
1507
|
+
requests: 0,
|
|
1508
|
+
proxyRequests: 0,
|
|
1509
|
+
directRequests: 0,
|
|
1510
|
+
uploadBytes: 0,
|
|
1511
|
+
downloadBytes: 0,
|
|
1512
|
+
totalBytes: 0,
|
|
1513
|
+
proxyBytes: 0,
|
|
1514
|
+
directBytes: 0,
|
|
1515
|
+
failedRequests: 0,
|
|
1516
|
+
canceledRequests: 0
|
|
1517
|
+
});
|
|
1518
|
+
var createReasonBucket = (reason) => ({
|
|
1519
|
+
reason,
|
|
1520
|
+
count: 0,
|
|
1521
|
+
canceledCount: 0,
|
|
1522
|
+
proxyCount: 0,
|
|
1523
|
+
directCount: 0
|
|
1524
|
+
});
|
|
1436
1525
|
var formatBytes = (bytes = 0) => {
|
|
1437
1526
|
const value = toSafeNumber(bytes);
|
|
1438
1527
|
if (value <= 0) return "0B";
|
|
@@ -1479,6 +1568,190 @@ var addDownloadBytes = (state, route, bytes = 0) => {
|
|
|
1479
1568
|
}
|
|
1480
1569
|
state.directDownloadBytes += b;
|
|
1481
1570
|
};
|
|
1571
|
+
var incrementBucketRequests = (bucket, route) => {
|
|
1572
|
+
if (!bucket) return;
|
|
1573
|
+
bucket.requests += 1;
|
|
1574
|
+
if (ensureRoute(route) === "proxy") {
|
|
1575
|
+
bucket.proxyRequests += 1;
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
bucket.directRequests += 1;
|
|
1579
|
+
};
|
|
1580
|
+
var incrementBucketBytes = (bucket, route, bytes = 0, direction = "download", resourceType = "other") => {
|
|
1581
|
+
if (!bucket) return;
|
|
1582
|
+
const b = toSafeNumber(bytes);
|
|
1583
|
+
if (b <= 0) return;
|
|
1584
|
+
if (direction === "upload") {
|
|
1585
|
+
bucket.uploadBytes += b;
|
|
1586
|
+
} else {
|
|
1587
|
+
bucket.downloadBytes += b;
|
|
1588
|
+
}
|
|
1589
|
+
bucket.totalBytes += b;
|
|
1590
|
+
if (ensureRoute(route) === "proxy") {
|
|
1591
|
+
bucket.proxyBytes += b;
|
|
1592
|
+
} else {
|
|
1593
|
+
bucket.directBytes += b;
|
|
1594
|
+
}
|
|
1595
|
+
if (bucket.domain !== void 0) {
|
|
1596
|
+
if (isStaticType(resourceType)) {
|
|
1597
|
+
bucket.staticBytes += b;
|
|
1598
|
+
} else if (isBackendType(resourceType)) {
|
|
1599
|
+
bucket.backendBytes += b;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
var incrementBucketFailed = (bucket, canceled = false) => {
|
|
1604
|
+
if (!bucket) return;
|
|
1605
|
+
bucket.failedRequests += 1;
|
|
1606
|
+
if (canceled) {
|
|
1607
|
+
bucket.canceledRequests += 1;
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
var ensureReasonText = (reason) => {
|
|
1611
|
+
const normalized = String(reason || "").trim().toLowerCase();
|
|
1612
|
+
return normalized || "unknown";
|
|
1613
|
+
};
|
|
1614
|
+
var pickDomainBucket = (state, domain = "") => {
|
|
1615
|
+
const domainKey = normalizeDomainKey(domain);
|
|
1616
|
+
const knownBucket = state.domainStats.get(domainKey);
|
|
1617
|
+
if (knownBucket) return knownBucket;
|
|
1618
|
+
if (state.domainStats.size < MAX_DOMAIN_BUCKETS) {
|
|
1619
|
+
const bucket2 = createDomainBucket(domainKey);
|
|
1620
|
+
state.domainStats.set(domainKey, bucket2);
|
|
1621
|
+
return bucket2;
|
|
1622
|
+
}
|
|
1623
|
+
const overflow = state.domainStats.get(OTHER_DOMAINS);
|
|
1624
|
+
if (overflow) return overflow;
|
|
1625
|
+
const bucket = createDomainBucket(OTHER_DOMAINS);
|
|
1626
|
+
state.domainStats.set(OTHER_DOMAINS, bucket);
|
|
1627
|
+
return bucket;
|
|
1628
|
+
};
|
|
1629
|
+
var pickTypeBucket = (state, resourceType = "other") => {
|
|
1630
|
+
const typeKey = normalizeResourceType(resourceType);
|
|
1631
|
+
const knownBucket = state.typeStats.get(typeKey);
|
|
1632
|
+
if (knownBucket) return knownBucket;
|
|
1633
|
+
const bucket = createTypeBucket(typeKey);
|
|
1634
|
+
state.typeStats.set(typeKey, bucket);
|
|
1635
|
+
return bucket;
|
|
1636
|
+
};
|
|
1637
|
+
var pickReasonBucket = (state, reason = "") => {
|
|
1638
|
+
const reasonKey = ensureReasonText(reason);
|
|
1639
|
+
const knownBucket = state.failedReasonStats.get(reasonKey);
|
|
1640
|
+
if (knownBucket) return knownBucket;
|
|
1641
|
+
if (state.failedReasonStats.size < MAX_REASON_BUCKETS) {
|
|
1642
|
+
const bucket2 = createReasonBucket(reasonKey);
|
|
1643
|
+
state.failedReasonStats.set(reasonKey, bucket2);
|
|
1644
|
+
return bucket2;
|
|
1645
|
+
}
|
|
1646
|
+
const overflow = state.failedReasonStats.get(OTHER_REASONS);
|
|
1647
|
+
if (overflow) return overflow;
|
|
1648
|
+
const bucket = createReasonBucket(OTHER_REASONS);
|
|
1649
|
+
state.failedReasonStats.set(OTHER_REASONS, bucket);
|
|
1650
|
+
return bucket;
|
|
1651
|
+
};
|
|
1652
|
+
var addRequestProfile = (state, route, domain, resourceType, uploadBytes = 0) => {
|
|
1653
|
+
const domainBucket = pickDomainBucket(state, domain);
|
|
1654
|
+
const typeBucket = pickTypeBucket(state, resourceType);
|
|
1655
|
+
incrementBucketRequests(domainBucket, route);
|
|
1656
|
+
incrementBucketRequests(typeBucket, route);
|
|
1657
|
+
incrementBucketBytes(domainBucket, route, uploadBytes, "upload", resourceType);
|
|
1658
|
+
incrementBucketBytes(typeBucket, route, uploadBytes, "upload", resourceType);
|
|
1659
|
+
};
|
|
1660
|
+
var addUploadProfile = (state, route, domain, resourceType, uploadBytes = 0) => {
|
|
1661
|
+
const domainBucket = pickDomainBucket(state, domain);
|
|
1662
|
+
const typeBucket = pickTypeBucket(state, resourceType);
|
|
1663
|
+
incrementBucketBytes(domainBucket, route, uploadBytes, "upload", resourceType);
|
|
1664
|
+
incrementBucketBytes(typeBucket, route, uploadBytes, "upload", resourceType);
|
|
1665
|
+
};
|
|
1666
|
+
var addDownloadProfile = (state, route, domain, resourceType, downloadBytes = 0) => {
|
|
1667
|
+
const domainBucket = pickDomainBucket(state, domain);
|
|
1668
|
+
const typeBucket = pickTypeBucket(state, resourceType);
|
|
1669
|
+
incrementBucketBytes(domainBucket, route, downloadBytes, "download", resourceType);
|
|
1670
|
+
incrementBucketBytes(typeBucket, route, downloadBytes, "download", resourceType);
|
|
1671
|
+
};
|
|
1672
|
+
var addFailedProfile = (state, route, domain, resourceType, reason = "unknown", canceled = false) => {
|
|
1673
|
+
const domainBucket = pickDomainBucket(state, domain);
|
|
1674
|
+
const typeBucket = pickTypeBucket(state, resourceType);
|
|
1675
|
+
const reasonBucket = pickReasonBucket(state, reason);
|
|
1676
|
+
incrementBucketFailed(domainBucket, canceled);
|
|
1677
|
+
incrementBucketFailed(typeBucket, canceled);
|
|
1678
|
+
reasonBucket.count += 1;
|
|
1679
|
+
if (ensureRoute(route) === "proxy") {
|
|
1680
|
+
reasonBucket.proxyCount += 1;
|
|
1681
|
+
} else {
|
|
1682
|
+
reasonBucket.directCount += 1;
|
|
1683
|
+
}
|
|
1684
|
+
if (canceled) {
|
|
1685
|
+
reasonBucket.canceledCount += 1;
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
var toRoundedRatio = (numerator, denominator) => {
|
|
1689
|
+
if (denominator <= 0) return 0;
|
|
1690
|
+
return Number((numerator / denominator * 100).toFixed(2));
|
|
1691
|
+
};
|
|
1692
|
+
var sortByBytesAndRequests = (left, right) => {
|
|
1693
|
+
if (right.totalBytes !== left.totalBytes) return right.totalBytes - left.totalBytes;
|
|
1694
|
+
if (right.requests !== left.requests) return right.requests - left.requests;
|
|
1695
|
+
return String(left.domain || left.resourceType || "").localeCompare(String(right.domain || right.resourceType || ""));
|
|
1696
|
+
};
|
|
1697
|
+
var buildTopDomains = (state) => {
|
|
1698
|
+
return Array.from(state.domainStats.values()).filter((item) => item && item.totalBytes > 0).sort(sortByBytesAndRequests).slice(0, MAX_TOP_ITEMS).map((item) => ({
|
|
1699
|
+
domain: item.domain,
|
|
1700
|
+
requests: item.requests,
|
|
1701
|
+
proxyRequests: item.proxyRequests,
|
|
1702
|
+
directRequests: item.directRequests,
|
|
1703
|
+
uploadBytes: item.uploadBytes,
|
|
1704
|
+
downloadBytes: item.downloadBytes,
|
|
1705
|
+
totalBytes: item.totalBytes,
|
|
1706
|
+
proxyBytes: item.proxyBytes,
|
|
1707
|
+
directBytes: item.directBytes,
|
|
1708
|
+
failedRequests: item.failedRequests,
|
|
1709
|
+
canceledRequests: item.canceledRequests,
|
|
1710
|
+
staticBytes: item.staticBytes,
|
|
1711
|
+
backendBytes: item.backendBytes
|
|
1712
|
+
}));
|
|
1713
|
+
};
|
|
1714
|
+
var buildTopResourceTypes = (state) => {
|
|
1715
|
+
return Array.from(state.typeStats.values()).filter((item) => item && (item.totalBytes > 0 || item.requests > 0)).sort(sortByBytesAndRequests).slice(0, MAX_TOP_ITEMS).map((item) => ({
|
|
1716
|
+
resourceType: item.resourceType,
|
|
1717
|
+
requests: item.requests,
|
|
1718
|
+
proxyRequests: item.proxyRequests,
|
|
1719
|
+
directRequests: item.directRequests,
|
|
1720
|
+
uploadBytes: item.uploadBytes,
|
|
1721
|
+
downloadBytes: item.downloadBytes,
|
|
1722
|
+
totalBytes: item.totalBytes,
|
|
1723
|
+
proxyBytes: item.proxyBytes,
|
|
1724
|
+
directBytes: item.directBytes,
|
|
1725
|
+
failedRequests: item.failedRequests,
|
|
1726
|
+
canceledRequests: item.canceledRequests
|
|
1727
|
+
}));
|
|
1728
|
+
};
|
|
1729
|
+
var buildFailedReasons = (state) => {
|
|
1730
|
+
return Array.from(state.failedReasonStats.values()).filter((item) => item && item.count > 0).sort((left, right) => {
|
|
1731
|
+
if (right.count !== left.count) return right.count - left.count;
|
|
1732
|
+
return String(left.reason || "").localeCompare(String(right.reason || ""));
|
|
1733
|
+
}).slice(0, MAX_TOP_ITEMS).map((item) => ({
|
|
1734
|
+
reason: item.reason,
|
|
1735
|
+
count: item.count,
|
|
1736
|
+
canceledCount: item.canceledCount,
|
|
1737
|
+
proxyCount: item.proxyCount,
|
|
1738
|
+
directCount: item.directCount
|
|
1739
|
+
}));
|
|
1740
|
+
};
|
|
1741
|
+
var buildProxyByPassHints = (state) => {
|
|
1742
|
+
return Array.from(state.domainStats.values()).filter((item) => item && item.domain !== UNKNOWN_DOMAIN && item.domain !== OTHER_DOMAINS).filter((item) => item.proxyBytes >= 128 * 1024 && item.proxyRequests >= 2 && item.totalBytes > 0).filter((item) => item.staticBytes > item.backendBytes && toRoundedRatio(item.staticBytes, item.totalBytes) >= 80).sort((left, right) => {
|
|
1743
|
+
if (right.proxyBytes !== left.proxyBytes) return right.proxyBytes - left.proxyBytes;
|
|
1744
|
+
return right.proxyRequests - left.proxyRequests;
|
|
1745
|
+
}).slice(0, MAX_HINT_ITEMS).map((item) => ({
|
|
1746
|
+
domain: item.domain,
|
|
1747
|
+
proxyBytes: item.proxyBytes,
|
|
1748
|
+
proxyRequests: item.proxyRequests,
|
|
1749
|
+
totalBytes: item.totalBytes,
|
|
1750
|
+
staticBytes: item.staticBytes,
|
|
1751
|
+
backendBytes: item.backendBytes,
|
|
1752
|
+
staticRatioPct: toRoundedRatio(item.staticBytes, item.totalBytes)
|
|
1753
|
+
}));
|
|
1754
|
+
};
|
|
1482
1755
|
var createTrafficMeter = ({
|
|
1483
1756
|
enableProxy = false,
|
|
1484
1757
|
byPassRules = [],
|
|
@@ -1486,7 +1759,10 @@ var createTrafficMeter = ({
|
|
|
1486
1759
|
} = {}) => {
|
|
1487
1760
|
const state = createTrafficState();
|
|
1488
1761
|
const requestMap = /* @__PURE__ */ new Map();
|
|
1762
|
+
const orphanReceivedMap = /* @__PURE__ */ new Map();
|
|
1489
1763
|
const wsRouteMap = /* @__PURE__ */ new Map();
|
|
1764
|
+
const attachedPages = /* @__PURE__ */ new WeakSet();
|
|
1765
|
+
const attachedContexts = /* @__PURE__ */ new WeakSet();
|
|
1490
1766
|
const resolveRoute = (url = "") => {
|
|
1491
1767
|
return resolveRouteByProxy({
|
|
1492
1768
|
requestUrl: url,
|
|
@@ -1494,62 +1770,169 @@ var createTrafficMeter = ({
|
|
|
1494
1770
|
byPassRules
|
|
1495
1771
|
}).route;
|
|
1496
1772
|
};
|
|
1773
|
+
const fallbackRoute = () => enableProxy ? "proxy" : "direct";
|
|
1497
1774
|
const debugLog = (message) => {
|
|
1498
1775
|
if (!debugMode) return;
|
|
1499
1776
|
logger6.info(`[\u9010\u8BF7\u6C42\u8C03\u8BD5] ${message}`);
|
|
1500
1777
|
};
|
|
1778
|
+
const addFailed = (route, canceled = false) => {
|
|
1779
|
+
const normalizedRoute = ensureRoute(route);
|
|
1780
|
+
state.totalFailedRequests += 1;
|
|
1781
|
+
if (normalizedRoute === "proxy") {
|
|
1782
|
+
state.proxyFailedRequests += 1;
|
|
1783
|
+
} else {
|
|
1784
|
+
state.directFailedRequests += 1;
|
|
1785
|
+
}
|
|
1786
|
+
if (!canceled) return;
|
|
1787
|
+
state.totalCanceledRequests += 1;
|
|
1788
|
+
if (normalizedRoute === "proxy") {
|
|
1789
|
+
state.proxyCanceledRequests += 1;
|
|
1790
|
+
} else {
|
|
1791
|
+
state.directCanceledRequests += 1;
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
const finalizeByEncodedLength = (requestId, encodedDataLength, source = "finished") => {
|
|
1795
|
+
const safeRequestId = String(requestId || "");
|
|
1796
|
+
if (!safeRequestId) return;
|
|
1797
|
+
const requestState = requestMap.get(safeRequestId);
|
|
1798
|
+
const orphanReceived = toSafeNumber(orphanReceivedMap.get(safeRequestId));
|
|
1799
|
+
const encoded = toSafeNumber(encodedDataLength);
|
|
1800
|
+
if (requestState) {
|
|
1801
|
+
const routed2 = ensureRoute(requestState.route);
|
|
1802
|
+
const downloaded = toSafeNumber(requestState.downloadBytes);
|
|
1803
|
+
const delta2 = Math.max(0, encoded - downloaded);
|
|
1804
|
+
if (delta2 > 0) {
|
|
1805
|
+
addDownloadBytes(state, routed2, delta2);
|
|
1806
|
+
addDownloadProfile(state, routed2, requestState.domain, requestState.resourceType, delta2);
|
|
1807
|
+
requestState.downloadBytes = downloaded + delta2;
|
|
1808
|
+
}
|
|
1809
|
+
const uploadBytes = toSafeNumber(requestState.uploadBytes);
|
|
1810
|
+
const total = uploadBytes + toSafeNumber(requestState.downloadBytes);
|
|
1811
|
+
debugLog(
|
|
1812
|
+
`final id=${safeRequestId} source=${source} status=ok route=${routed2} type=${requestState.resourceType || "other"} upload=${formatBytes(uploadBytes)} (${uploadBytes}) download=${formatBytes(requestState.downloadBytes)} (${requestState.downloadBytes}) total=${formatBytes(total)} (${total}) url=${requestState.url || "-"}`
|
|
1813
|
+
);
|
|
1814
|
+
requestMap.delete(safeRequestId);
|
|
1815
|
+
orphanReceivedMap.delete(safeRequestId);
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
const routed = fallbackRoute();
|
|
1819
|
+
const delta = Math.max(0, encoded - orphanReceived);
|
|
1820
|
+
if (delta > 0) {
|
|
1821
|
+
addDownloadBytes(state, routed, delta);
|
|
1822
|
+
addDownloadProfile(state, routed, UNKNOWN_DOMAIN, "other", delta);
|
|
1823
|
+
}
|
|
1824
|
+
state.orphanFinishDeltaBytes += delta;
|
|
1825
|
+
if (routed === "proxy") {
|
|
1826
|
+
state.orphanProxyFinishDeltaBytes += delta;
|
|
1827
|
+
}
|
|
1828
|
+
debugLog(
|
|
1829
|
+
`final id=${safeRequestId} source=${source} status=orphan route=${routed} encoded=${formatBytes(encoded)} (${encoded}) delta=${formatBytes(delta)} (${delta})`
|
|
1830
|
+
);
|
|
1831
|
+
orphanReceivedMap.delete(safeRequestId);
|
|
1832
|
+
};
|
|
1501
1833
|
const recordRequest = (params = {}) => {
|
|
1502
1834
|
const requestId = String(params.requestId || "");
|
|
1503
1835
|
const request = params.request && typeof params.request === "object" ? params.request : {};
|
|
1504
1836
|
const url = String(request.url || "");
|
|
1505
1837
|
const route = resolveRoute(url);
|
|
1506
|
-
const resourceType =
|
|
1838
|
+
const resourceType = normalizeResourceType(params.type || request.type || "other");
|
|
1839
|
+
const domain = normalizeDomainKey(parseHostname(url));
|
|
1840
|
+
if (requestId && requestMap.has(requestId)) {
|
|
1841
|
+
const redirectResponse = params.redirectResponse && typeof params.redirectResponse === "object" ? params.redirectResponse : null;
|
|
1842
|
+
finalizeByEncodedLength(
|
|
1843
|
+
requestId,
|
|
1844
|
+
redirectResponse ? redirectResponse.encodedDataLength : 0,
|
|
1845
|
+
"redirect"
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1507
1848
|
addRequests(state, route, 1);
|
|
1508
1849
|
const uploadBytes = estimateRequestBytes(request);
|
|
1509
1850
|
addUploadBytes(state, route, uploadBytes);
|
|
1851
|
+
addRequestProfile(state, route, domain, resourceType, uploadBytes);
|
|
1510
1852
|
if (requestId) {
|
|
1511
1853
|
requestMap.set(requestId, {
|
|
1512
|
-
route,
|
|
1854
|
+
route: ensureRoute(route),
|
|
1513
1855
|
resourceType,
|
|
1856
|
+
domain,
|
|
1514
1857
|
url,
|
|
1515
|
-
uploadBytes
|
|
1858
|
+
uploadBytes,
|
|
1859
|
+
downloadBytes: 0
|
|
1516
1860
|
});
|
|
1517
1861
|
}
|
|
1518
1862
|
debugLog(
|
|
1519
1863
|
`request id=${requestId || "-"} route=${route} type=${resourceType} upload=${formatBytes(uploadBytes)} (${uploadBytes}) method=${String(request.method || "GET")} url=${url || "-"}`
|
|
1520
1864
|
);
|
|
1521
1865
|
};
|
|
1522
|
-
const
|
|
1866
|
+
const recordDataReceived = (params = {}) => {
|
|
1523
1867
|
const requestId = String(params.requestId || "");
|
|
1524
|
-
const
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
requestMap.delete(requestId);
|
|
1868
|
+
const bytes = toSafeNumber(params.encodedDataLength);
|
|
1869
|
+
if (bytes <= 0) return;
|
|
1870
|
+
if (!requestId) {
|
|
1871
|
+
const routed2 = fallbackRoute();
|
|
1872
|
+
addDownloadBytes(state, routed2, bytes);
|
|
1873
|
+
addDownloadProfile(state, routed2, UNKNOWN_DOMAIN, "other", bytes);
|
|
1874
|
+
state.orphanDataReceivedBytes += bytes;
|
|
1875
|
+
if (routed2 === "proxy") {
|
|
1876
|
+
state.orphanProxyDataReceivedBytes += bytes;
|
|
1877
|
+
}
|
|
1878
|
+
return;
|
|
1536
1879
|
}
|
|
1880
|
+
const requestState = requestMap.get(requestId);
|
|
1881
|
+
if (requestState) {
|
|
1882
|
+
requestState.downloadBytes = toSafeNumber(requestState.downloadBytes) + bytes;
|
|
1883
|
+
addDownloadBytes(state, requestState.route, bytes);
|
|
1884
|
+
addDownloadProfile(state, requestState.route, requestState.domain, requestState.resourceType, bytes);
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
const prev = toSafeNumber(orphanReceivedMap.get(requestId));
|
|
1888
|
+
orphanReceivedMap.set(requestId, prev + bytes);
|
|
1889
|
+
const routed = fallbackRoute();
|
|
1890
|
+
addDownloadBytes(state, routed, bytes);
|
|
1891
|
+
addDownloadProfile(state, routed, UNKNOWN_DOMAIN, "other", bytes);
|
|
1892
|
+
state.orphanDataReceivedBytes += bytes;
|
|
1893
|
+
if (routed === "proxy") {
|
|
1894
|
+
state.orphanProxyDataReceivedBytes += bytes;
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
const recordLoadingFinished = (params = {}) => {
|
|
1898
|
+
finalizeByEncodedLength(params.requestId, params.encodedDataLength, "loadingFinished");
|
|
1537
1899
|
};
|
|
1538
1900
|
const recordRequestFailed = (params = {}) => {
|
|
1539
1901
|
const requestId = String(params.requestId || "");
|
|
1540
1902
|
const requestState = requestId ? requestMap.get(requestId) : null;
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
);
|
|
1903
|
+
const canceled = Boolean(params.canceled);
|
|
1904
|
+
const reason = ensureReasonText(params.errorText);
|
|
1905
|
+
const routed = ensureRoute(requestState?.route || fallbackRoute());
|
|
1906
|
+
const resourceType = normalizeResourceType(requestState?.resourceType || "other");
|
|
1907
|
+
const domain = normalizeDomainKey(requestState?.domain || "");
|
|
1908
|
+
if (requestState) {
|
|
1909
|
+
addFailed(routed, canceled);
|
|
1910
|
+
addFailedProfile(state, routed, domain, resourceType, reason, canceled);
|
|
1911
|
+
const uploadBytes = toSafeNumber(requestState.uploadBytes);
|
|
1912
|
+
const downloadBytes = toSafeNumber(requestState.downloadBytes);
|
|
1913
|
+
const totalBytes = uploadBytes + downloadBytes;
|
|
1914
|
+
debugLog(
|
|
1915
|
+
`final id=${requestId || "-"} source=loadingFailed status=failed route=${routed} type=${requestState.resourceType || "other"} upload=${formatBytes(uploadBytes)} (${uploadBytes}) download=${formatBytes(downloadBytes)} (${downloadBytes}) total=${formatBytes(totalBytes)} (${totalBytes}) canceled=${canceled} reason=${reason} url=${requestState.url || "-"}`
|
|
1916
|
+
);
|
|
1917
|
+
} else {
|
|
1918
|
+
const orphanDownload = toSafeNumber(orphanReceivedMap.get(requestId));
|
|
1919
|
+
addFailed(routed, canceled);
|
|
1920
|
+
addFailedProfile(state, routed, UNKNOWN_DOMAIN, "other", reason, canceled);
|
|
1921
|
+
debugLog(
|
|
1922
|
+
`final id=${requestId || "-"} source=loadingFailed status=orphan-failed route=${routed} upload=0B (0) download=${formatBytes(orphanDownload)} (${orphanDownload}) total=${formatBytes(orphanDownload)} (${orphanDownload}) canceled=${canceled} reason=${reason} url=-`
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1544
1925
|
if (requestId) {
|
|
1545
1926
|
requestMap.delete(requestId);
|
|
1927
|
+
orphanReceivedMap.delete(requestId);
|
|
1546
1928
|
}
|
|
1547
1929
|
};
|
|
1548
1930
|
const bindWebSocketRoute = (params = {}) => {
|
|
1549
1931
|
const requestId = String(params.requestId || "");
|
|
1550
1932
|
if (!requestId) return;
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1933
|
+
const url = String(params.url || "");
|
|
1934
|
+
const route = resolveRoute(url);
|
|
1935
|
+
wsRouteMap.set(requestId, { route, url, domain: normalizeDomainKey(parseHostname(url)) });
|
|
1553
1936
|
};
|
|
1554
1937
|
const clearWebSocketRoute = (params = {}) => {
|
|
1555
1938
|
const requestId = String(params.requestId || "");
|
|
@@ -1557,48 +1940,64 @@ var createTrafficMeter = ({
|
|
|
1557
1940
|
wsRouteMap.delete(requestId);
|
|
1558
1941
|
};
|
|
1559
1942
|
const getWebSocketMeta = (requestId = "") => {
|
|
1560
|
-
if (!requestId) return { route:
|
|
1943
|
+
if (!requestId) return { route: fallbackRoute(), url: "", domain: UNKNOWN_DOMAIN };
|
|
1561
1944
|
const meta = wsRouteMap.get(requestId);
|
|
1562
1945
|
if (!meta || typeof meta !== "object") {
|
|
1563
|
-
return { route:
|
|
1946
|
+
return { route: fallbackRoute(), url: "", domain: UNKNOWN_DOMAIN };
|
|
1564
1947
|
}
|
|
1565
1948
|
return {
|
|
1566
1949
|
route: ensureRoute(meta.route),
|
|
1567
|
-
url: String(meta.url || "")
|
|
1950
|
+
url: String(meta.url || ""),
|
|
1951
|
+
domain: normalizeDomainKey(meta.domain)
|
|
1568
1952
|
};
|
|
1569
1953
|
};
|
|
1570
1954
|
const recordWebSocketFrameSent = (params = {}) => {
|
|
1571
1955
|
const requestId = String(params.requestId || "");
|
|
1572
|
-
const { route, url } = getWebSocketMeta(requestId);
|
|
1956
|
+
const { route, url, domain } = getWebSocketMeta(requestId);
|
|
1573
1957
|
const payload = params.response && typeof params.response === "object" ? params.response.payloadData : "";
|
|
1574
1958
|
const bytes = byteLength(payload || "");
|
|
1575
1959
|
addUploadBytes(state, route, bytes);
|
|
1960
|
+
addUploadProfile(state, route, domain, "websocket", bytes);
|
|
1576
1961
|
debugLog(`ws-send id=${requestId || "-"} route=${route} bytes=${formatBytes(bytes)} (${bytes}) url=${url || "-"}`);
|
|
1577
1962
|
};
|
|
1578
1963
|
const recordWebSocketFrameReceived = (params = {}) => {
|
|
1579
1964
|
const requestId = String(params.requestId || "");
|
|
1580
|
-
const { route, url } = getWebSocketMeta(requestId);
|
|
1965
|
+
const { route, url, domain } = getWebSocketMeta(requestId);
|
|
1581
1966
|
const payload = params.response && typeof params.response === "object" ? params.response.payloadData : "";
|
|
1582
1967
|
const bytes = byteLength(payload || "");
|
|
1583
1968
|
addDownloadBytes(state, route, bytes);
|
|
1969
|
+
addDownloadProfile(state, route, domain, "websocket", bytes);
|
|
1584
1970
|
debugLog(`ws-recv id=${requestId || "-"} route=${route} bytes=${formatBytes(bytes)} (${bytes}) url=${url || "-"}`);
|
|
1585
1971
|
};
|
|
1586
1972
|
const attachPage = async (page) => {
|
|
1587
1973
|
if (!page || typeof page.context !== "function") return;
|
|
1974
|
+
if (attachedPages.has(page)) return;
|
|
1975
|
+
attachedPages.add(page);
|
|
1588
1976
|
try {
|
|
1589
1977
|
const context = page.context();
|
|
1590
1978
|
if (!context || typeof context.newCDPSession !== "function") {
|
|
1591
1979
|
return;
|
|
1592
1980
|
}
|
|
1981
|
+
if (!attachedContexts.has(context) && typeof context.on === "function") {
|
|
1982
|
+
attachedContexts.add(context);
|
|
1983
|
+
context.on("page", (nextPage) => {
|
|
1984
|
+
if (!nextPage) return;
|
|
1985
|
+
attachPage(nextPage).catch((error) => {
|
|
1986
|
+
logger6.warn(`\u5B50\u9875\u9762 CDP \u76D1\u542C\u6CE8\u518C\u5931\u8D25: ${error?.message || error}`);
|
|
1987
|
+
});
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1593
1990
|
const session = await context.newCDPSession(page);
|
|
1594
1991
|
await session.send("Network.enable");
|
|
1595
1992
|
session.on("Network.requestWillBeSent", recordRequest);
|
|
1993
|
+
session.on("Network.dataReceived", recordDataReceived);
|
|
1596
1994
|
session.on("Network.loadingFinished", recordLoadingFinished);
|
|
1597
1995
|
session.on("Network.loadingFailed", recordRequestFailed);
|
|
1598
1996
|
session.on("Network.webSocketCreated", bindWebSocketRoute);
|
|
1599
1997
|
session.on("Network.webSocketClosed", clearWebSocketRoute);
|
|
1600
1998
|
session.on("Network.webSocketFrameSent", recordWebSocketFrameSent);
|
|
1601
1999
|
session.on("Network.webSocketFrameReceived", recordWebSocketFrameReceived);
|
|
2000
|
+
debugLog("CDP \u76D1\u542C\u5DF2\u6CE8\u518C");
|
|
1602
2001
|
} catch (error) {
|
|
1603
2002
|
logger6.warn(`CDP \u76D1\u542C\u6CE8\u518C\u5931\u8D25: ${error?.message || error}`);
|
|
1604
2003
|
}
|
|
@@ -1608,7 +2007,7 @@ var createTrafficMeter = ({
|
|
|
1608
2007
|
const proxyBytes = state.proxyUploadBytes + state.proxyDownloadBytes;
|
|
1609
2008
|
const directBytes = state.directUploadBytes + state.directDownloadBytes;
|
|
1610
2009
|
return {
|
|
1611
|
-
meter: "cdp",
|
|
2010
|
+
meter: "cdp-data-received-v3",
|
|
1612
2011
|
totalRequests: state.totalRequests,
|
|
1613
2012
|
proxyRequests: state.proxyRequests,
|
|
1614
2013
|
directRequests: state.directRequests,
|
|
@@ -1620,12 +2019,29 @@ var createTrafficMeter = ({
|
|
|
1620
2019
|
directDownloadBytes: state.directDownloadBytes,
|
|
1621
2020
|
totalBytes,
|
|
1622
2021
|
proxyBytes,
|
|
1623
|
-
directBytes
|
|
2022
|
+
directBytes,
|
|
2023
|
+
totalFailedRequests: state.totalFailedRequests,
|
|
2024
|
+
proxyFailedRequests: state.proxyFailedRequests,
|
|
2025
|
+
directFailedRequests: state.directFailedRequests,
|
|
2026
|
+
totalCanceledRequests: state.totalCanceledRequests,
|
|
2027
|
+
proxyCanceledRequests: state.proxyCanceledRequests,
|
|
2028
|
+
directCanceledRequests: state.directCanceledRequests,
|
|
2029
|
+
orphanDataReceivedBytes: state.orphanDataReceivedBytes,
|
|
2030
|
+
orphanProxyDataReceivedBytes: state.orphanProxyDataReceivedBytes,
|
|
2031
|
+
orphanFinishDeltaBytes: state.orphanFinishDeltaBytes,
|
|
2032
|
+
orphanProxyFinishDeltaBytes: state.orphanProxyFinishDeltaBytes,
|
|
2033
|
+
openRequestCount: requestMap.size,
|
|
2034
|
+
orphanOpenCount: orphanReceivedMap.size,
|
|
2035
|
+
topDomains: buildTopDomains(state),
|
|
2036
|
+
topResourceTypes: buildTopResourceTypes(state),
|
|
2037
|
+
failedReasons: buildFailedReasons(state),
|
|
2038
|
+
proxyBypassHints: buildProxyByPassHints(state)
|
|
1624
2039
|
};
|
|
1625
2040
|
};
|
|
1626
2041
|
const reset = () => {
|
|
1627
2042
|
Object.assign(state, createTrafficState());
|
|
1628
2043
|
requestMap.clear();
|
|
2044
|
+
orphanReceivedMap.clear();
|
|
1629
2045
|
wsRouteMap.clear();
|
|
1630
2046
|
};
|
|
1631
2047
|
return {
|
|
@@ -2360,6 +2776,196 @@ var Mutation = {
|
|
|
2360
2776
|
logger11.success("waitForStable", `DOM \u7A33\u5B9A, \u603B\u5171 ${result.mutationCount} \u6B21\u53D8\u5316${result.wasPaused ? ", \u66FE\u6682\u505C\u8BA1\u65F6" : ""}`);
|
|
2361
2777
|
return result;
|
|
2362
2778
|
},
|
|
2779
|
+
/**
|
|
2780
|
+
* 等待跨 root DOM 元素稳定(主文档 + iframe 内容)
|
|
2781
|
+
* 通过轮询快照检测变化,适配 iframe / shadow 场景
|
|
2782
|
+
*
|
|
2783
|
+
* @param {import('playwright').Page} page - Playwright page 对象
|
|
2784
|
+
* @param {string | string[]} selectors - 要监控的 CSS 选择器,单个或多个(建议传 iframe 选择器)
|
|
2785
|
+
* @param {Object} [options] - 配置选项(签名与 waitForStable 保持一致)
|
|
2786
|
+
* @param {number} [options.initialTimeout] - 等待元素出现的超时 (毫秒, 默认: 60000)
|
|
2787
|
+
* @param {number} [options.stableTime] - 无变化持续时间后 resolve (毫秒, 默认: 10000)
|
|
2788
|
+
* @param {number} [options.timeout] - 整体超时时间 (毫秒, 默认: 180000)
|
|
2789
|
+
* @param {Function} [options.onMutation] - 变化时的回调钩子
|
|
2790
|
+
* @returns {Promise<{ mutationCount: number, stableTime: number, wasPaused: boolean }>}
|
|
2791
|
+
*/
|
|
2792
|
+
async waitForStableAcrossRoots(page, selectors, options = {}) {
|
|
2793
|
+
const selectorList = Array.isArray(selectors) ? selectors : [selectors];
|
|
2794
|
+
const selectorQuery = selectorList.join(",");
|
|
2795
|
+
const initialTimeout = options.initialTimeout ?? 60 * 1e3;
|
|
2796
|
+
const waitForStableTime = options.stableTime ?? 10 * 1e3;
|
|
2797
|
+
const overallTimeout = options.timeout ?? 180 * 1e3;
|
|
2798
|
+
const onMutation = options.onMutation;
|
|
2799
|
+
const pollInterval = 500;
|
|
2800
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
2801
|
+
setTimeout(resolve, ms);
|
|
2802
|
+
});
|
|
2803
|
+
const truncate = (value, max = 800) => {
|
|
2804
|
+
const text = String(value || "");
|
|
2805
|
+
if (text.length <= max) return text;
|
|
2806
|
+
return `${text.slice(0, max)}...`;
|
|
2807
|
+
};
|
|
2808
|
+
const buildState = async () => {
|
|
2809
|
+
return await page.evaluate(({ selectorList: selectorList2 }) => {
|
|
2810
|
+
const normalizeText = (value) => String(value || "").replace(/\s+/g, " ").trim();
|
|
2811
|
+
const tail = (value, max = 512) => {
|
|
2812
|
+
const text = String(value || "");
|
|
2813
|
+
if (text.length <= max) return text;
|
|
2814
|
+
return text.slice(text.length - max);
|
|
2815
|
+
};
|
|
2816
|
+
const safeFrameId = (frameEl) => {
|
|
2817
|
+
const id = String(frameEl?.id || "").trim();
|
|
2818
|
+
if (id) return id;
|
|
2819
|
+
const name = String(frameEl?.name || "").trim();
|
|
2820
|
+
if (name) return name;
|
|
2821
|
+
return "no-id";
|
|
2822
|
+
};
|
|
2823
|
+
const items = [];
|
|
2824
|
+
selectorList2.forEach((selector) => {
|
|
2825
|
+
let nodes = [];
|
|
2826
|
+
try {
|
|
2827
|
+
nodes = Array.from(document.querySelectorAll(selector));
|
|
2828
|
+
} catch {
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
nodes.forEach((node, index) => {
|
|
2832
|
+
const isIframe = node?.tagName === "IFRAME";
|
|
2833
|
+
let text = "";
|
|
2834
|
+
let html = "";
|
|
2835
|
+
let source = "main";
|
|
2836
|
+
let path = `${selector}[${index}]`;
|
|
2837
|
+
if (isIframe) {
|
|
2838
|
+
source = "iframe";
|
|
2839
|
+
path = `${selector}[${index}]::iframe(${safeFrameId(node)})`;
|
|
2840
|
+
try {
|
|
2841
|
+
const frameDoc = node.contentDocument;
|
|
2842
|
+
const frameRoot = frameDoc?.body || frameDoc?.documentElement;
|
|
2843
|
+
if (frameRoot) {
|
|
2844
|
+
text = normalizeText(frameRoot.innerText || frameRoot.textContent || "");
|
|
2845
|
+
html = normalizeText(frameRoot.innerHTML || "");
|
|
2846
|
+
}
|
|
2847
|
+
} catch {
|
|
2848
|
+
}
|
|
2849
|
+
} else {
|
|
2850
|
+
text = normalizeText(node?.innerText || node?.textContent || "");
|
|
2851
|
+
html = normalizeText(node?.innerHTML || node?.outerHTML || "");
|
|
2852
|
+
}
|
|
2853
|
+
const snapshot = text || html;
|
|
2854
|
+
items.push({
|
|
2855
|
+
selector,
|
|
2856
|
+
source,
|
|
2857
|
+
path,
|
|
2858
|
+
text,
|
|
2859
|
+
html,
|
|
2860
|
+
snapshot
|
|
2861
|
+
});
|
|
2862
|
+
});
|
|
2863
|
+
});
|
|
2864
|
+
const snapshotKey = items.map((item) => `${item.path}:${item.snapshot.length}:${tail(item.snapshot, 512)}`).join("||");
|
|
2865
|
+
const summaryText = items.map((item) => item.snapshot || item.text).join("\n").trim();
|
|
2866
|
+
const summaryHtml = items.map((item) => item.html).join("\n");
|
|
2867
|
+
const hasMatched = items.length > 0;
|
|
2868
|
+
const primary = items.length > 0 ? items[items.length - 1] : null;
|
|
2869
|
+
return {
|
|
2870
|
+
hasMatched,
|
|
2871
|
+
snapshotKey,
|
|
2872
|
+
text: summaryText,
|
|
2873
|
+
html: summaryHtml,
|
|
2874
|
+
snapshotLength: summaryText.length,
|
|
2875
|
+
itemCount: items.length,
|
|
2876
|
+
primaryPath: primary?.path || "",
|
|
2877
|
+
mutationNodes: items.map((item) => ({
|
|
2878
|
+
html: item.html,
|
|
2879
|
+
text: item.snapshot || item.text,
|
|
2880
|
+
mutationType: item.source
|
|
2881
|
+
}))
|
|
2882
|
+
};
|
|
2883
|
+
}, { selectorList });
|
|
2884
|
+
};
|
|
2885
|
+
const invokeMutationCallback = async (context) => {
|
|
2886
|
+
if (!onMutation) return "__CONTINUE__";
|
|
2887
|
+
try {
|
|
2888
|
+
const result = await onMutation(context);
|
|
2889
|
+
return result === null || result === void 0 ? "__CONTINUE__" : "__PAUSE__";
|
|
2890
|
+
} catch {
|
|
2891
|
+
return "__CONTINUE__";
|
|
2892
|
+
}
|
|
2893
|
+
};
|
|
2894
|
+
logger11.start(
|
|
2895
|
+
"waitForStableAcrossRoots",
|
|
2896
|
+
`\u76D1\u63A7 ${selectorList.length} \u4E2A\u9009\u62E9\u5668(\u8DE8 root), \u7A33\u5B9A\u65F6\u95F4=${waitForStableTime}ms`
|
|
2897
|
+
);
|
|
2898
|
+
if (initialTimeout > 0) {
|
|
2899
|
+
try {
|
|
2900
|
+
await page.waitForSelector(selectorQuery, { timeout: initialTimeout });
|
|
2901
|
+
logger11.info(`waitForStableAcrossRoots \u5DF2\u68C0\u6D4B\u5230\u5143\u7D20: ${selectorQuery}`);
|
|
2902
|
+
} catch (e) {
|
|
2903
|
+
logger11.warning(`waitForStableAcrossRoots \u521D\u59CB\u7B49\u5F85\u8D85\u65F6 (${initialTimeout}ms): ${selectorQuery}`);
|
|
2904
|
+
throw e;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
let state = await buildState();
|
|
2908
|
+
if (!state?.hasMatched) {
|
|
2909
|
+
logger11.warning("waitForStableAcrossRoots \u672A\u627E\u5230\u53EF\u76D1\u63A7\u7684\u5143\u7D20");
|
|
2910
|
+
return { mutationCount: 0, stableTime: 0, wasPaused: false };
|
|
2911
|
+
}
|
|
2912
|
+
let mutationCount = 0;
|
|
2913
|
+
let stableSince = 0;
|
|
2914
|
+
let isPaused = false;
|
|
2915
|
+
let wasPaused = false;
|
|
2916
|
+
let lastSnapshotKey = state.snapshotKey;
|
|
2917
|
+
const applyPauseSignal = (signal) => {
|
|
2918
|
+
const nextPaused = signal === "__PAUSE__";
|
|
2919
|
+
if (nextPaused) {
|
|
2920
|
+
if (!isPaused) wasPaused = true;
|
|
2921
|
+
isPaused = true;
|
|
2922
|
+
stableSince = 0;
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
isPaused = false;
|
|
2926
|
+
stableSince = Date.now();
|
|
2927
|
+
};
|
|
2928
|
+
const initialSignal = await invokeMutationCallback({
|
|
2929
|
+
mutationCount: 0,
|
|
2930
|
+
html: state.html || "",
|
|
2931
|
+
text: state.text || "",
|
|
2932
|
+
mutationNodes: state.mutationNodes || []
|
|
2933
|
+
});
|
|
2934
|
+
applyPauseSignal(initialSignal);
|
|
2935
|
+
const deadline = Date.now() + overallTimeout;
|
|
2936
|
+
let lastState = state;
|
|
2937
|
+
while (Date.now() < deadline) {
|
|
2938
|
+
await sleep(pollInterval);
|
|
2939
|
+
lastState = await buildState();
|
|
2940
|
+
if (!lastState?.hasMatched) {
|
|
2941
|
+
continue;
|
|
2942
|
+
}
|
|
2943
|
+
if (lastState.snapshotKey !== lastSnapshotKey) {
|
|
2944
|
+
lastSnapshotKey = lastState.snapshotKey;
|
|
2945
|
+
mutationCount += 1;
|
|
2946
|
+
logger11.info(
|
|
2947
|
+
`waitForStableAcrossRoots \u53D8\u5316#${mutationCount}, len=${lastState.snapshotLength}, path=${lastState.primaryPath || "unknown"}, preview="${truncate(lastState.text, 120)}"`
|
|
2948
|
+
);
|
|
2949
|
+
const signal = await invokeMutationCallback({
|
|
2950
|
+
mutationCount,
|
|
2951
|
+
html: lastState.html || "",
|
|
2952
|
+
text: lastState.text || "",
|
|
2953
|
+
mutationNodes: lastState.mutationNodes || []
|
|
2954
|
+
});
|
|
2955
|
+
applyPauseSignal(signal);
|
|
2956
|
+
continue;
|
|
2957
|
+
}
|
|
2958
|
+
if (!isPaused && stableSince > 0 && Date.now() - stableSince >= waitForStableTime) {
|
|
2959
|
+
logger11.success("waitForStableAcrossRoots", `DOM \u7A33\u5B9A, \u603B\u5171 ${mutationCount} \u6B21\u53D8\u5316${wasPaused ? ", \u66FE\u6682\u505C\u8BA1\u65F6" : ""}`);
|
|
2960
|
+
return {
|
|
2961
|
+
mutationCount,
|
|
2962
|
+
stableTime: waitForStableTime,
|
|
2963
|
+
wasPaused
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
throw new Error(`waitForStableAcrossRoots \u8D85\u65F6 (${overallTimeout}ms), \u5DF2\u68C0\u6D4B\u5230 ${mutationCount} \u6B21\u53D8\u5316, isPaused=${isPaused}`);
|
|
2968
|
+
},
|
|
2363
2969
|
/**
|
|
2364
2970
|
* 创建一个持续监控 DOM 变化的监控器(默认仅监听新增 DOM)
|
|
2365
2971
|
*
|