customer-chat-sdk 1.0.27 → 1.0.29

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.
@@ -926,28 +926,6 @@ class IframeManager {
926
926
  }
927
927
 
928
928
  // @ts-ignore - modern-screenshot may not have type definitions
929
- // Worker URL 将在创建上下文时动态获取
930
- // 在 Vite 环境中可以使用: import workerUrl from 'modern-screenshot/worker?url'
931
- // 在 Rollup 中,modern-screenshot 会自动处理 worker URL
932
- let workerUrl = undefined;
933
- // 尝试动态获取 worker URL(仅在支持的环境中)
934
- async function getWorkerUrl() {
935
- if (workerUrl) {
936
- return workerUrl;
937
- }
938
- try {
939
- // 尝试使用 Vite 的 ?url 语法(仅在 Vite 环境中有效)
940
- // @ts-ignore - Vite 特有的 ?url 语法
941
- const workerModule = await import('modern-screenshot/worker?url');
942
- workerUrl = workerModule.default || workerModule;
943
- return workerUrl;
944
- }
945
- catch {
946
- // Rollup 或其他构建工具不支持 ?url 语法
947
- // modern-screenshot 会自动处理 worker URL,返回 undefined 即可
948
- return undefined;
949
- }
950
- }
951
929
  /**
952
930
  * 截图管理器
953
931
  * 负责页面截图、压缩和上传功能
@@ -977,8 +955,16 @@ class ScreenshotManager {
977
955
  this.dynamicInterval = null;
978
956
  // 过期定时器
979
957
  this.expirationTimer = null;
980
- // 图片代理缓存
958
+ // 图片代理缓存(带过期时间)
981
959
  this.imageProxyCache = new Map();
960
+ // IndexedDB 缓存(持久化)
961
+ this.indexedDBCache = null;
962
+ this.indexedDBReady = false;
963
+ // Intersection Observer(用于高效检测可见性)
964
+ this.intersectionObserver = null;
965
+ this.visibleElementsCache = new Set();
966
+ // 预连接状态
967
+ this.preconnected = false;
982
968
  // 全局错误处理器
983
969
  this.globalErrorHandler = null;
984
970
  this.globalRejectionHandler = null;
@@ -997,10 +983,40 @@ class ScreenshotManager {
997
983
  engine: options.engine ?? 'modern-screenshot',
998
984
  corsMode: options.corsMode ?? 'canvas-proxy',
999
985
  silentMode: options.silentMode ?? false,
1000
- maxRetries: options.maxRetries ?? 2
986
+ maxRetries: options.maxRetries ?? 2,
987
+ preloadImages: options.preloadImages ?? false, // 默认不预加载,按需加载
988
+ maxConcurrentDownloads: options.maxConcurrentDownloads ?? 10, // 增加并发数
989
+ onlyVisibleImages: options.onlyVisibleImages ?? true, // 默认只处理可视区域
990
+ imageCacheTTL: options.imageCacheTTL ?? 600000, // 默认10分钟(600000ms)
991
+ useIndexedDB: options.useIndexedDB ?? true, // 默认启用 IndexedDB 持久化缓存
992
+ usePreconnect: options.usePreconnect ?? true, // 默认预连接代理服务器
993
+ imageLoadTimeout: options.imageLoadTimeout ?? 5000, // 默认5秒超时
994
+ useIntersectionObserver: options.useIntersectionObserver ?? true, // 默认使用 Intersection Observer
995
+ fetchPriority: options.fetchPriority ?? 'high', // 默认高优先级
996
+ maxCacheSize: options.maxCacheSize ?? 50, // 默认最大50MB
997
+ maxCacheAge: options.maxCacheAge ?? 86400000 // 默认24小时(86400000ms)
1001
998
  };
1002
999
  this.setupMessageListener();
1003
1000
  this.setupVisibilityChangeListener();
1001
+ // 启动缓存清理定时器
1002
+ this.startCacheCleanup();
1003
+ // 预连接代理服务器(如果启用)
1004
+ if (this.options.usePreconnect && this.options.proxyUrl) {
1005
+ this.preconnectProxy();
1006
+ }
1007
+ // 初始化 Intersection Observer(如果启用)
1008
+ if (this.options.useIntersectionObserver && this.options.onlyVisibleImages) {
1009
+ this.initIntersectionObserver();
1010
+ }
1011
+ // 初始化 IndexedDB(如果启用)
1012
+ if (this.options.useIndexedDB) {
1013
+ this.initIndexedDB().catch(() => {
1014
+ // IndexedDB 初始化失败,回退到内存缓存
1015
+ if (!this.options.silentMode) {
1016
+ console.warn('📸 IndexedDB 初始化失败,使用内存缓存');
1017
+ }
1018
+ });
1019
+ }
1004
1020
  }
1005
1021
  /**
1006
1022
  * 设置目标元素
@@ -1309,14 +1325,28 @@ class ScreenshotManager {
1309
1325
  if (!this.options.silentMode) {
1310
1326
  console.log(`📸 使用截图引擎: ${selectedEngine} (Worker 模式)`);
1311
1327
  }
1312
- // 预处理网络图片
1313
- if (this.options.enableCORS) {
1328
+ // 优化:如果启用预加载,才预处理图片;否则让 modern-screenshot 按需加载
1329
+ if (this.options.preloadImages && this.options.enableCORS) {
1330
+ // 清理过期缓存
1331
+ this.cleanExpiredCache();
1332
+ // 如果启用 IndexedDB,也清理 IndexedDB 缓存
1333
+ if (this.options.useIndexedDB) {
1334
+ await this.cleanIndexedDBCache();
1335
+ }
1314
1336
  await this.preprocessNetworkImages(this.targetElement);
1315
1337
  await this.waitForImagesToLoad(this.targetElement);
1316
1338
  }
1339
+ else {
1340
+ // 即使不预加载,也清理一下过期缓存
1341
+ this.cleanExpiredCache();
1342
+ // 如果启用 IndexedDB,也清理 IndexedDB 缓存
1343
+ if (this.options.useIndexedDB) {
1344
+ await this.cleanIndexedDBCache();
1345
+ }
1346
+ }
1317
1347
  let dataUrl;
1318
- // 等待一小段时间,确保 DOM 更新完成
1319
- await new Promise(resolve => setTimeout(resolve, 100));
1348
+ // 等待一小段时间,确保 DOM 更新完成(减少等待时间)
1349
+ await new Promise(resolve => setTimeout(resolve, 50));
1320
1350
  // 使用 modern-screenshot 截图(启用 Worker)
1321
1351
  dataUrl = await this.takeScreenshotWithModernScreenshot(this.targetElement);
1322
1352
  const timestamp = Date.now();
@@ -1413,9 +1443,25 @@ class ScreenshotManager {
1413
1443
  }
1414
1444
  // 如果配置了代理服务器,使用代理处理跨域图片
1415
1445
  if (this.options.proxyUrl && this.options.proxyUrl.trim() !== '') {
1416
- // 检查缓存
1417
- if (this.imageProxyCache.has(url)) {
1418
- return this.imageProxyCache.get(url);
1446
+ // 检查内存缓存(优先使用缓存,带过期时间检查)
1447
+ const cachedDataUrl = this.getCachedImage(url);
1448
+ if (cachedDataUrl) {
1449
+ if (!this.options.silentMode) {
1450
+ console.log(`📸 ✅ 使用内存缓存图片: ${url.substring(0, 50)}...`);
1451
+ }
1452
+ return cachedDataUrl;
1453
+ }
1454
+ // 检查 IndexedDB 缓存(如果启用)
1455
+ if (this.options.useIndexedDB) {
1456
+ const indexedDBCache = await this.getIndexedDBCache(url);
1457
+ if (indexedDBCache) {
1458
+ // 同步到内存缓存
1459
+ this.setCachedImage(url, indexedDBCache);
1460
+ if (!this.options.silentMode) {
1461
+ console.log(`📸 ✅ 使用 IndexedDB 缓存图片: ${url.substring(0, 50)}...`);
1462
+ }
1463
+ return indexedDBCache;
1464
+ }
1419
1465
  }
1420
1466
  try {
1421
1467
  // 构建代理请求参数
@@ -1429,24 +1475,43 @@ class ScreenshotManager {
1429
1475
  let baseUrl = this.options.proxyUrl;
1430
1476
  baseUrl = baseUrl.replace(/[?&]$/, '');
1431
1477
  const proxyUrl = `${baseUrl}?${params.toString()}`;
1432
- // 请求代理服务器
1433
- const response = await fetch(proxyUrl, {
1434
- method: 'GET',
1435
- mode: 'cors',
1436
- credentials: 'omit',
1437
- headers: {
1438
- 'Accept': 'image/*'
1439
- },
1440
- cache: 'no-cache'
1441
- });
1442
- if (!response.ok) {
1443
- throw new Error(`代理请求失败: ${response.status}`);
1478
+ // 请求代理服务器(优化:添加超时控制和优先级)
1479
+ const controller = new AbortController();
1480
+ const timeoutId = setTimeout(() => controller.abort(), this.options.imageLoadTimeout);
1481
+ try {
1482
+ const fetchOptions = {
1483
+ method: 'GET',
1484
+ mode: 'cors',
1485
+ credentials: 'omit',
1486
+ headers: {
1487
+ 'Accept': 'image/*'
1488
+ },
1489
+ cache: 'no-cache',
1490
+ signal: controller.signal
1491
+ };
1492
+ // 添加 fetch priority(如果支持)
1493
+ if ('priority' in fetchOptions) {
1494
+ fetchOptions.priority = this.options.fetchPriority;
1495
+ }
1496
+ const response = await fetch(proxyUrl, fetchOptions);
1497
+ clearTimeout(timeoutId);
1498
+ if (!response.ok) {
1499
+ throw new Error(`代理请求失败: ${response.status}`);
1500
+ }
1501
+ const blob = await response.blob();
1502
+ const dataUrl = await this.blobToDataUrl(blob);
1503
+ // 缓存结果(带时间戳,10分钟有效)
1504
+ this.setCachedImage(url, dataUrl);
1505
+ // 如果启用 IndexedDB,也保存到 IndexedDB
1506
+ if (this.options.useIndexedDB) {
1507
+ await this.setIndexedDBCache(url, dataUrl);
1508
+ }
1509
+ return dataUrl;
1510
+ }
1511
+ catch (fetchError) {
1512
+ clearTimeout(timeoutId);
1513
+ throw fetchError;
1444
1514
  }
1445
- const blob = await response.blob();
1446
- const dataUrl = await this.blobToDataUrl(blob);
1447
- // 缓存结果
1448
- this.imageProxyCache.set(url, dataUrl);
1449
- return dataUrl;
1450
1515
  }
1451
1516
  catch (error) {
1452
1517
  if (!this.options.silentMode) {
@@ -1487,11 +1552,7 @@ class ScreenshotManager {
1487
1552
  if (this.options.scale !== 1) {
1488
1553
  contextOptions.scale = isMobile ? 0.8 : this.options.scale;
1489
1554
  }
1490
- // 尝试设置 workerUrl(如果可用)
1491
- const resolvedWorkerUrl = await getWorkerUrl();
1492
- if (resolvedWorkerUrl) {
1493
- contextOptions.workerUrl = resolvedWorkerUrl;
1494
- }
1555
+ // modern-screenshot 会自动处理 worker URL,不需要手动设置
1495
1556
  // 创建 Worker 上下文
1496
1557
  this.screenshotContext = await modernScreenshot.createContext(element, contextOptions);
1497
1558
  }
@@ -1555,76 +1616,399 @@ class ScreenshotManager {
1555
1616
  }
1556
1617
  }
1557
1618
  /**
1558
- * 预处理网络图片
1619
+ * 预连接代理服务器(优化网络性能)
1620
+ */
1621
+ preconnectProxy() {
1622
+ if (this.preconnected || !this.options.proxyUrl) {
1623
+ return;
1624
+ }
1625
+ try {
1626
+ const proxyOrigin = new URL(this.options.proxyUrl).origin;
1627
+ const link = document.createElement('link');
1628
+ link.rel = 'preconnect';
1629
+ link.href = proxyOrigin;
1630
+ link.crossOrigin = 'anonymous';
1631
+ document.head.appendChild(link);
1632
+ this.preconnected = true;
1633
+ if (!this.options.silentMode) {
1634
+ console.log(`📸 ✅ 已预连接代理服务器: ${proxyOrigin}`);
1635
+ }
1636
+ }
1637
+ catch (e) {
1638
+ // 预连接失败,不影响功能
1639
+ }
1640
+ }
1641
+ /**
1642
+ * 初始化 Intersection Observer(优化可见性检测)
1643
+ */
1644
+ initIntersectionObserver() {
1645
+ if (!('IntersectionObserver' in window)) {
1646
+ return;
1647
+ }
1648
+ this.intersectionObserver = new IntersectionObserver((entries) => {
1649
+ entries.forEach((entry) => {
1650
+ if (entry.isIntersecting) {
1651
+ this.visibleElementsCache.add(entry.target);
1652
+ }
1653
+ else {
1654
+ this.visibleElementsCache.delete(entry.target);
1655
+ }
1656
+ });
1657
+ }, {
1658
+ root: null,
1659
+ rootMargin: '50px', // 提前50px开始加载
1660
+ threshold: 0.01
1661
+ });
1662
+ }
1663
+ /**
1664
+ * 初始化 IndexedDB(持久化缓存)
1665
+ */
1666
+ async initIndexedDB() {
1667
+ return new Promise((resolve, reject) => {
1668
+ const request = indexedDB.open('screenshot-cache', 1);
1669
+ request.onerror = () => reject(request.error);
1670
+ request.onsuccess = () => {
1671
+ this.indexedDBCache = request.result;
1672
+ this.indexedDBReady = true;
1673
+ // 初始化后立即清理过期和超大小的缓存
1674
+ this.cleanIndexedDBCache().catch(() => {
1675
+ // 清理失败不影响功能
1676
+ });
1677
+ resolve();
1678
+ };
1679
+ request.onupgradeneeded = (event) => {
1680
+ const db = event.target.result;
1681
+ if (!db.objectStoreNames.contains('images')) {
1682
+ const store = db.createObjectStore('images', { keyPath: 'url' });
1683
+ // 创建索引用于按时间排序
1684
+ store.createIndex('timestamp', 'timestamp', { unique: false });
1685
+ }
1686
+ };
1687
+ });
1688
+ }
1689
+ /**
1690
+ * 从 IndexedDB 获取缓存
1691
+ */
1692
+ async getIndexedDBCache(url) {
1693
+ if (!this.indexedDBReady || !this.indexedDBCache) {
1694
+ return null;
1695
+ }
1696
+ try {
1697
+ const transaction = this.indexedDBCache.transaction(['images'], 'readonly');
1698
+ const store = transaction.objectStore('images');
1699
+ const request = store.get(url);
1700
+ return new Promise((resolve) => {
1701
+ request.onsuccess = () => {
1702
+ const result = request.result;
1703
+ if (result) {
1704
+ const now = Date.now();
1705
+ const age = now - result.timestamp;
1706
+ // 检查是否超过最大缓存时间
1707
+ if (age > this.options.maxCacheAge) {
1708
+ // 缓存过期,删除
1709
+ this.deleteIndexedDBCache(url);
1710
+ resolve(null);
1711
+ return;
1712
+ }
1713
+ // 返回缓存数据(即使超过 imageCacheTTL,只要未超过 maxCacheAge 仍可使用)
1714
+ resolve(result.dataUrl);
1715
+ }
1716
+ else {
1717
+ resolve(null);
1718
+ }
1719
+ };
1720
+ request.onerror = () => resolve(null);
1721
+ });
1722
+ }
1723
+ catch {
1724
+ return null;
1725
+ }
1726
+ }
1727
+ /**
1728
+ * 设置 IndexedDB 缓存(带大小和时间控制)
1729
+ */
1730
+ async setIndexedDBCache(url, dataUrl) {
1731
+ if (!this.indexedDBReady || !this.indexedDBCache) {
1732
+ return;
1733
+ }
1734
+ try {
1735
+ // 计算当前缓存大小
1736
+ const currentSize = await this.getIndexedDBCacheSize();
1737
+ const newItemSize = this.estimateDataUrlSize(dataUrl);
1738
+ const maxSizeBytes = (this.options.maxCacheSize || 50) * 1024 * 1024; // 转换为字节
1739
+ // 如果添加新项后超过限制,清理最旧的数据
1740
+ if (currentSize + newItemSize > maxSizeBytes) {
1741
+ await this.cleanIndexedDBCacheBySize(maxSizeBytes - newItemSize);
1742
+ }
1743
+ const transaction = this.indexedDBCache.transaction(['images'], 'readwrite');
1744
+ const store = transaction.objectStore('images');
1745
+ store.put({ url, dataUrl, timestamp: Date.now() });
1746
+ }
1747
+ catch {
1748
+ // IndexedDB 写入失败,忽略
1749
+ }
1750
+ }
1751
+ /**
1752
+ * 估算 data URL 的大小(字节)
1753
+ */
1754
+ estimateDataUrlSize(dataUrl) {
1755
+ // Base64 编码后的大小约为原始大小的 4/3
1756
+ // data:image/xxx;base64, 前缀约 22-30 字节
1757
+ const base64Data = dataUrl.split(',')[1] || '';
1758
+ return Math.ceil(base64Data.length * 0.75) + 30;
1759
+ }
1760
+ /**
1761
+ * 获取 IndexedDB 当前缓存大小(字节)
1762
+ */
1763
+ async getIndexedDBCacheSize() {
1764
+ if (!this.indexedDBReady || !this.indexedDBCache) {
1765
+ return 0;
1766
+ }
1767
+ try {
1768
+ const transaction = this.indexedDBCache.transaction(['images'], 'readonly');
1769
+ const store = transaction.objectStore('images');
1770
+ const request = store.getAll();
1771
+ return new Promise((resolve) => {
1772
+ request.onsuccess = () => {
1773
+ const items = request.result || [];
1774
+ let totalSize = 0;
1775
+ items.forEach((item) => {
1776
+ totalSize += this.estimateDataUrlSize(item.dataUrl);
1777
+ });
1778
+ resolve(totalSize);
1779
+ };
1780
+ request.onerror = () => resolve(0);
1781
+ });
1782
+ }
1783
+ catch {
1784
+ return 0;
1785
+ }
1786
+ }
1787
+ /**
1788
+ * 清理 IndexedDB 缓存(按时间和大小)
1789
+ */
1790
+ async cleanIndexedDBCache() {
1791
+ if (!this.indexedDBReady || !this.indexedDBCache) {
1792
+ return;
1793
+ }
1794
+ try {
1795
+ const transaction = this.indexedDBCache.transaction(['images'], 'readwrite');
1796
+ const store = transaction.objectStore('images');
1797
+ const index = store.index('timestamp');
1798
+ const request = index.getAll();
1799
+ return new Promise((resolve) => {
1800
+ request.onsuccess = () => {
1801
+ const items = request.result || [];
1802
+ const now = Date.now();
1803
+ const expiredUrls = [];
1804
+ let currentSize = 0;
1805
+ const maxSizeBytes = (this.options.maxCacheSize || 50) * 1024 * 1024;
1806
+ // 按时间排序(最旧的在前)
1807
+ items.sort((a, b) => a.timestamp - b.timestamp);
1808
+ // 清理过期数据
1809
+ items.forEach((item) => {
1810
+ const age = now - item.timestamp;
1811
+ if (age > this.options.maxCacheAge) {
1812
+ expiredUrls.push(item.url);
1813
+ }
1814
+ else {
1815
+ currentSize += this.estimateDataUrlSize(item.dataUrl);
1816
+ }
1817
+ });
1818
+ // 如果仍然超过大小限制,删除最旧的数据
1819
+ const urlsToDelete = [...expiredUrls];
1820
+ if (currentSize > maxSizeBytes) {
1821
+ for (const item of items) {
1822
+ if (expiredUrls.includes(item.url))
1823
+ continue;
1824
+ currentSize -= this.estimateDataUrlSize(item.dataUrl);
1825
+ urlsToDelete.push(item.url);
1826
+ if (currentSize <= maxSizeBytes) {
1827
+ break;
1828
+ }
1829
+ }
1830
+ }
1831
+ // 删除过期和超大小的数据
1832
+ urlsToDelete.forEach((url) => {
1833
+ store.delete(url);
1834
+ });
1835
+ if (urlsToDelete.length > 0 && !this.options.silentMode) {
1836
+ console.log(`📸 IndexedDB 清理了 ${urlsToDelete.length} 个缓存项(过期或超大小)`);
1837
+ }
1838
+ resolve();
1839
+ };
1840
+ request.onerror = () => resolve();
1841
+ });
1842
+ }
1843
+ catch {
1844
+ // 清理失败,忽略
1845
+ }
1846
+ }
1847
+ /**
1848
+ * 按大小清理 IndexedDB 缓存
1849
+ */
1850
+ async cleanIndexedDBCacheBySize(targetSize) {
1851
+ if (!this.indexedDBReady || !this.indexedDBCache) {
1852
+ return;
1853
+ }
1854
+ try {
1855
+ const transaction = this.indexedDBCache.transaction(['images'], 'readwrite');
1856
+ const store = transaction.objectStore('images');
1857
+ const index = store.index('timestamp');
1858
+ const request = index.getAll();
1859
+ return new Promise((resolve) => {
1860
+ request.onsuccess = () => {
1861
+ const items = request.result || [];
1862
+ // 按时间排序(最旧的在前)
1863
+ items.sort((a, b) => a.timestamp - b.timestamp);
1864
+ let currentSize = 0;
1865
+ const urlsToDelete = [];
1866
+ // 计算当前大小
1867
+ items.forEach((item) => {
1868
+ currentSize += this.estimateDataUrlSize(item.dataUrl);
1869
+ });
1870
+ // 如果超过目标大小,删除最旧的数据
1871
+ if (currentSize > targetSize) {
1872
+ for (const item of items) {
1873
+ if (currentSize <= targetSize) {
1874
+ break;
1875
+ }
1876
+ currentSize -= this.estimateDataUrlSize(item.dataUrl);
1877
+ urlsToDelete.push(item.url);
1878
+ }
1879
+ }
1880
+ // 删除数据
1881
+ urlsToDelete.forEach((url) => {
1882
+ store.delete(url);
1883
+ });
1884
+ if (urlsToDelete.length > 0 && !this.options.silentMode) {
1885
+ console.log(`📸 IndexedDB 清理了 ${urlsToDelete.length} 个缓存项(超过大小限制)`);
1886
+ }
1887
+ resolve();
1888
+ };
1889
+ request.onerror = () => resolve();
1890
+ });
1891
+ }
1892
+ catch {
1893
+ // 清理失败,忽略
1894
+ }
1895
+ }
1896
+ /**
1897
+ * 删除 IndexedDB 缓存
1898
+ */
1899
+ async deleteIndexedDBCache(url) {
1900
+ if (!this.indexedDBReady || !this.indexedDBCache) {
1901
+ return;
1902
+ }
1903
+ try {
1904
+ const transaction = this.indexedDBCache.transaction(['images'], 'readwrite');
1905
+ const store = transaction.objectStore('images');
1906
+ store.delete(url);
1907
+ }
1908
+ catch {
1909
+ // 忽略错误
1910
+ }
1911
+ }
1912
+ /**
1913
+ * 检查元素是否在可视区域内(优化:使用 Intersection Observer 或 getBoundingClientRect)
1914
+ */
1915
+ isElementVisible(element, container) {
1916
+ if (!this.options.onlyVisibleImages) {
1917
+ return true; // 如果禁用可视区域检测,返回 true
1918
+ }
1919
+ // 如果使用 Intersection Observer 且元素已在缓存中,直接返回
1920
+ if (this.options.useIntersectionObserver && this.intersectionObserver) {
1921
+ if (this.visibleElementsCache.has(element)) {
1922
+ return true;
1923
+ }
1924
+ // 观察元素(如果还没有观察)
1925
+ this.intersectionObserver.observe(element);
1926
+ }
1927
+ // 回退到 getBoundingClientRect
1928
+ try {
1929
+ const rect = element.getBoundingClientRect();
1930
+ const containerRect = container.getBoundingClientRect();
1931
+ // 检查元素是否与容器有交集(考虑滚动位置)
1932
+ const isIntersecting = !(rect.bottom < containerRect.top ||
1933
+ rect.top > containerRect.bottom ||
1934
+ rect.right < containerRect.left ||
1935
+ rect.left > containerRect.right) && (rect.width > 0 && rect.height > 0 && // 元素有尺寸
1936
+ window.getComputedStyle(element).display !== 'none' && // 元素可见
1937
+ window.getComputedStyle(element).visibility !== 'hidden' &&
1938
+ window.getComputedStyle(element).opacity !== '0');
1939
+ // 更新 Intersection Observer 缓存
1940
+ if (this.options.useIntersectionObserver && this.intersectionObserver) {
1941
+ if (isIntersecting) {
1942
+ this.visibleElementsCache.add(element);
1943
+ }
1944
+ else {
1945
+ this.visibleElementsCache.delete(element);
1946
+ }
1947
+ }
1948
+ return isIntersecting;
1949
+ }
1950
+ catch {
1951
+ return true; // 出错时返回 true,保守处理
1952
+ }
1953
+ }
1954
+ /**
1955
+ * 预处理网络图片(优化:只处理可视区域内的图片,增加并发数)
1559
1956
  */
1560
1957
  async preprocessNetworkImages(element) {
1561
1958
  const images = element.querySelectorAll('img');
1959
+ // 过滤:只处理跨域图片,并且如果在可视区域内
1562
1960
  const networkImages = Array.from(images).filter(img => {
1563
1961
  const isBlob = img.src.startsWith('blob:');
1564
1962
  const isData = img.src.startsWith('data:');
1565
1963
  const isSameOrigin = img.src.startsWith(window.location.origin);
1566
- return !isBlob && !isData && !isSameOrigin;
1964
+ const isVisible = this.isElementVisible(img, element);
1965
+ return !isBlob && !isData && !isSameOrigin && isVisible;
1567
1966
  });
1568
1967
  if (networkImages.length === 0) {
1569
1968
  return;
1570
1969
  }
1571
1970
  if (!this.options.silentMode) {
1572
- console.log(`📸 发现 ${networkImages.length} 个跨域图片,开始预加载并缓存...`);
1971
+ const totalImages = images.length;
1972
+ console.log(`📸 发现 ${networkImages.length}/${totalImages} 个可视区域内的跨域图片,开始并行预加载...`);
1573
1973
  }
1574
1974
  // 如果配置了 proxyUrl,使用代理处理跨域图片
1575
1975
  if (this.options.proxyUrl && this.options.proxyUrl.trim() !== '') {
1576
1976
  if (!this.options.silentMode) {
1577
1977
  console.log(`📸 使用代理服务器处理跨域图片: ${this.options.proxyUrl}`);
1578
1978
  }
1579
- // 分批处理图片,避免同时处理太多导致主线程阻塞
1580
- const batchSize = 5;
1979
+ // 优化:增加并发数,使用更大的批次
1980
+ const batchSize = this.options.maxConcurrentDownloads || 10;
1581
1981
  const batches = [];
1582
1982
  for (let i = 0; i < networkImages.length; i += batchSize) {
1583
1983
  batches.push(networkImages.slice(i, i + batchSize));
1584
1984
  }
1585
- for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
1586
- const batch = batches[batchIndex];
1587
- // 如果不是第一批,等待浏览器空闲时间
1588
- if (batchIndex > 0) {
1589
- await new Promise((resolve) => {
1590
- if ('requestIdleCallback' in window) {
1591
- requestIdleCallback(() => resolve(), { timeout: 100 });
1592
- }
1593
- else {
1594
- setTimeout(() => resolve(), 50);
1595
- }
1596
- });
1597
- }
1985
+ // 并行处理所有批次(不再等待空闲时间)
1986
+ await Promise.all(batches.map(async (batch) => {
1598
1987
  // 并发处理当前批次
1599
1988
  await Promise.all(batch.map(async (img) => {
1600
1989
  const originalSrc = img.src;
1601
- // 检查缓存
1602
- if (this.imageProxyCache.has(originalSrc)) {
1603
- if (!this.options.silentMode) {
1604
- console.log(`📸 使用缓存的代理图片: ${originalSrc.substring(0, 100)}...`);
1605
- }
1990
+ // 检查缓存(带过期时间)
1991
+ if (this.getCachedImage(originalSrc)) {
1606
1992
  return;
1607
1993
  }
1608
1994
  try {
1609
1995
  // 使用代理服务器获取图片
1610
1996
  const dataUrl = await this.proxyImage(originalSrc);
1611
1997
  if (dataUrl) {
1612
- // 缓存 data URL
1613
- this.imageProxyCache.set(originalSrc, dataUrl);
1614
- if (!this.options.silentMode) {
1615
- console.log(`📸 ✅ 代理预加载成功(已缓存): ${originalSrc.substring(0, 100)}...`);
1616
- }
1998
+ // 缓存 data URL(带时间戳)
1999
+ this.setCachedImage(originalSrc, dataUrl);
1617
2000
  }
1618
2001
  }
1619
2002
  catch (error) {
2003
+ // 静默失败,不影响其他图片
1620
2004
  if (!this.options.silentMode) {
1621
- console.warn(`📸 ❌ 代理预加载失败: ${originalSrc.substring(0, 100)}...`, error);
2005
+ console.warn(`📸 ❌ 代理预加载失败: ${originalSrc.substring(0, 50)}...`);
1622
2006
  }
1623
2007
  }
1624
2008
  }));
1625
- }
2009
+ }));
1626
2010
  if (!this.options.silentMode) {
1627
- console.log(`📸 预处理完成,缓存了 ${this.imageProxyCache.size} 个代理图片`);
2011
+ console.log(`📸 预处理完成,缓存了 ${this.imageProxyCache.size} 个代理图片`);
1628
2012
  }
1629
2013
  }
1630
2014
  }
@@ -1693,7 +2077,7 @@ class ScreenshotManager {
1693
2077
  });
1694
2078
  }
1695
2079
  /**
1696
- * 等待图片加载完成
2080
+ * 等待图片加载完成(优化:使用更短的超时和 Promise.race)
1697
2081
  */
1698
2082
  async waitForImagesToLoad(element) {
1699
2083
  const images = element.querySelectorAll('img');
@@ -1702,32 +2086,33 @@ class ScreenshotManager {
1702
2086
  if (img.complete) {
1703
2087
  return;
1704
2088
  }
1705
- imagePromises.push(new Promise((resolve) => {
1706
- const timeout = setTimeout(() => {
1707
- resolve();
1708
- }, 5000);
1709
- img.onload = () => {
1710
- clearTimeout(timeout);
1711
- resolve();
1712
- };
1713
- img.onerror = () => {
1714
- clearTimeout(timeout);
1715
- resolve();
1716
- };
1717
- }));
2089
+ // 使用更短的超时时间(可配置)
2090
+ const timeout = this.options.imageLoadTimeout || 5000;
2091
+ imagePromises.push(Promise.race([
2092
+ new Promise((resolve) => {
2093
+ img.onload = () => resolve();
2094
+ img.onerror = () => resolve(); // 失败也继续
2095
+ }),
2096
+ new Promise((resolve) => {
2097
+ setTimeout(() => resolve(), timeout);
2098
+ })
2099
+ ]));
1718
2100
  });
1719
2101
  if (imagePromises.length > 0) {
1720
2102
  await Promise.all(imagePromises);
1721
2103
  }
1722
2104
  }
1723
2105
  /**
1724
- * 等待 CSS 和字体加载完成
2106
+ * 等待 CSS 和字体加载完成(优化:减少等待时间,使用 requestAnimationFrame)
1725
2107
  */
1726
2108
  async waitForStylesAndFonts() {
2109
+ // 使用 requestAnimationFrame 优化渲染时机
1727
2110
  return new Promise((resolve) => {
1728
- setTimeout(() => {
1729
- resolve();
1730
- }, 100);
2111
+ requestAnimationFrame(() => {
2112
+ setTimeout(() => {
2113
+ resolve();
2114
+ }, 30); // 减少到30ms
2115
+ });
1731
2116
  });
1732
2117
  }
1733
2118
  /**
@@ -1947,9 +2332,86 @@ class ScreenshotManager {
1947
2332
  this.messageHandler = null;
1948
2333
  }
1949
2334
  this.removeGlobalErrorHandlers();
2335
+ // 清理 Intersection Observer
2336
+ if (this.intersectionObserver) {
2337
+ this.intersectionObserver.disconnect();
2338
+ this.intersectionObserver = null;
2339
+ this.visibleElementsCache.clear();
2340
+ }
2341
+ // 关闭 IndexedDB
2342
+ if (this.indexedDBCache) {
2343
+ this.indexedDBCache.close();
2344
+ this.indexedDBCache = null;
2345
+ this.indexedDBReady = false;
2346
+ }
1950
2347
  // 清理图片代理缓存
1951
2348
  this.imageProxyCache.clear();
1952
2349
  }
2350
+ /**
2351
+ * 获取缓存的图片(检查是否过期)
2352
+ */
2353
+ getCachedImage(url) {
2354
+ const cached = this.imageProxyCache.get(url);
2355
+ if (!cached) {
2356
+ return null;
2357
+ }
2358
+ // 检查是否过期(10分钟)
2359
+ const now = Date.now();
2360
+ const age = now - cached.timestamp;
2361
+ if (age > this.options.imageCacheTTL) {
2362
+ // 缓存已过期,删除
2363
+ this.imageProxyCache.delete(url);
2364
+ return null;
2365
+ }
2366
+ return cached.dataUrl;
2367
+ }
2368
+ /**
2369
+ * 设置缓存的图片(带时间戳)
2370
+ */
2371
+ setCachedImage(url, dataUrl) {
2372
+ this.imageProxyCache.set(url, {
2373
+ dataUrl,
2374
+ timestamp: Date.now()
2375
+ });
2376
+ }
2377
+ /**
2378
+ * 清理过期缓存
2379
+ */
2380
+ cleanExpiredCache() {
2381
+ const now = Date.now();
2382
+ const expiredUrls = [];
2383
+ this.imageProxyCache.forEach((cached, url) => {
2384
+ const age = now - cached.timestamp;
2385
+ if (age > this.options.imageCacheTTL) {
2386
+ expiredUrls.push(url);
2387
+ }
2388
+ });
2389
+ expiredUrls.forEach(url => {
2390
+ this.imageProxyCache.delete(url);
2391
+ });
2392
+ if (expiredUrls.length > 0 && !this.options.silentMode) {
2393
+ console.log(`📸 清理了 ${expiredUrls.length} 个过期缓存`);
2394
+ }
2395
+ }
2396
+ /**
2397
+ * 定期清理过期缓存(可选,在截图时也会自动清理)
2398
+ */
2399
+ startCacheCleanup() {
2400
+ // 每5分钟清理一次过期缓存
2401
+ setInterval(() => {
2402
+ this.cleanExpiredCache();
2403
+ // 如果启用 IndexedDB,也清理 IndexedDB 缓存
2404
+ if (this.options.useIndexedDB) {
2405
+ this.cleanIndexedDBCache().catch(() => {
2406
+ // 清理失败,忽略
2407
+ });
2408
+ }
2409
+ if (!this.options.silentMode) {
2410
+ const memoryCacheSize = this.imageProxyCache.size;
2411
+ console.log(`📸 清理过期缓存,内存缓存数量: ${memoryCacheSize}`);
2412
+ }
2413
+ }, 300000); // 5分钟
2414
+ }
1953
2415
  /**
1954
2416
  * 获取状态
1955
2417
  */