customer-chat-sdk 1.0.28 → 1.0.30
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/core/ScreenshotManager.d.ts +81 -4
- package/dist/core/ScreenshotManager.d.ts.map +1 -1
- package/dist/customer-sdk.cjs.js +603 -69
- package/dist/customer-sdk.esm.js +603 -69
- package/dist/customer-sdk.min.js +1 -1
- package/dist/types/index.d.ts +12 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/customer-sdk.cjs.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
5
|
var modernScreenshot = require('modern-screenshot');
|
|
6
|
+
var snapdom = require('@zumer/snapdom');
|
|
6
7
|
|
|
7
8
|
// 直接使用base64字符串,避免打包后路径问题
|
|
8
9
|
const iconImage = 'data:image/png;base64,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';
|
|
@@ -926,28 +927,6 @@ class IframeManager {
|
|
|
926
927
|
}
|
|
927
928
|
|
|
928
929
|
// @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
930
|
/**
|
|
952
931
|
* 截图管理器
|
|
953
932
|
* 负责页面截图、压缩和上传功能
|
|
@@ -977,8 +956,16 @@ class ScreenshotManager {
|
|
|
977
956
|
this.dynamicInterval = null;
|
|
978
957
|
// 过期定时器
|
|
979
958
|
this.expirationTimer = null;
|
|
980
|
-
//
|
|
959
|
+
// 图片代理缓存(带过期时间)
|
|
981
960
|
this.imageProxyCache = new Map();
|
|
961
|
+
// IndexedDB 缓存(持久化)
|
|
962
|
+
this.indexedDBCache = null;
|
|
963
|
+
this.indexedDBReady = false;
|
|
964
|
+
// Intersection Observer(用于高效检测可见性)
|
|
965
|
+
this.intersectionObserver = null;
|
|
966
|
+
this.visibleElementsCache = new Set();
|
|
967
|
+
// 预连接状态
|
|
968
|
+
this.preconnected = false;
|
|
982
969
|
// 全局错误处理器
|
|
983
970
|
this.globalErrorHandler = null;
|
|
984
971
|
this.globalRejectionHandler = null;
|
|
@@ -1000,10 +987,37 @@ class ScreenshotManager {
|
|
|
1000
987
|
maxRetries: options.maxRetries ?? 2,
|
|
1001
988
|
preloadImages: options.preloadImages ?? false, // 默认不预加载,按需加载
|
|
1002
989
|
maxConcurrentDownloads: options.maxConcurrentDownloads ?? 10, // 增加并发数
|
|
1003
|
-
onlyVisibleImages: options.onlyVisibleImages ?? true // 默认只处理可视区域
|
|
990
|
+
onlyVisibleImages: options.onlyVisibleImages ?? true, // 默认只处理可视区域
|
|
991
|
+
imageCacheTTL: options.imageCacheTTL ?? 600000, // 默认10分钟(600000ms)
|
|
992
|
+
useIndexedDB: options.useIndexedDB ?? true, // 默认启用 IndexedDB 持久化缓存
|
|
993
|
+
usePreconnect: options.usePreconnect ?? true, // 默认预连接代理服务器
|
|
994
|
+
imageLoadTimeout: options.imageLoadTimeout ?? 5000, // 默认5秒超时
|
|
995
|
+
useIntersectionObserver: options.useIntersectionObserver ?? true, // 默认使用 Intersection Observer
|
|
996
|
+
fetchPriority: options.fetchPriority ?? 'high', // 默认高优先级
|
|
997
|
+
maxCacheSize: options.maxCacheSize ?? 50, // 默认最大50MB
|
|
998
|
+
maxCacheAge: options.maxCacheAge ?? 86400000 // 默认24小时(86400000ms)
|
|
1004
999
|
};
|
|
1005
1000
|
this.setupMessageListener();
|
|
1006
1001
|
this.setupVisibilityChangeListener();
|
|
1002
|
+
// 启动缓存清理定时器
|
|
1003
|
+
this.startCacheCleanup();
|
|
1004
|
+
// 预连接代理服务器(如果启用)
|
|
1005
|
+
if (this.options.usePreconnect && this.options.proxyUrl) {
|
|
1006
|
+
this.preconnectProxy();
|
|
1007
|
+
}
|
|
1008
|
+
// 初始化 Intersection Observer(如果启用)
|
|
1009
|
+
if (this.options.useIntersectionObserver && this.options.onlyVisibleImages) {
|
|
1010
|
+
this.initIntersectionObserver();
|
|
1011
|
+
}
|
|
1012
|
+
// 初始化 IndexedDB(如果启用)
|
|
1013
|
+
if (this.options.useIndexedDB) {
|
|
1014
|
+
this.initIndexedDB().catch(() => {
|
|
1015
|
+
// IndexedDB 初始化失败,回退到内存缓存
|
|
1016
|
+
if (!this.options.silentMode) {
|
|
1017
|
+
console.warn('📸 IndexedDB 初始化失败,使用内存缓存');
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1007
1021
|
}
|
|
1008
1022
|
/**
|
|
1009
1023
|
* 设置目标元素
|
|
@@ -1307,21 +1321,41 @@ class ScreenshotManager {
|
|
|
1307
1321
|
this.waitForStylesAndFonts(),
|
|
1308
1322
|
this.waitForFonts()
|
|
1309
1323
|
]);
|
|
1310
|
-
//
|
|
1311
|
-
const selectedEngine = 'modern-screenshot';
|
|
1324
|
+
// 选择截图引擎
|
|
1325
|
+
const selectedEngine = this.options.engine || 'modern-screenshot';
|
|
1312
1326
|
if (!this.options.silentMode) {
|
|
1313
|
-
console.log(`📸 使用截图引擎: ${selectedEngine}
|
|
1327
|
+
console.log(`📸 使用截图引擎: ${selectedEngine}`);
|
|
1314
1328
|
}
|
|
1315
|
-
//
|
|
1316
|
-
if (this.options.preloadImages && this.options.enableCORS) {
|
|
1329
|
+
// 优化:如果启用预加载,才预处理图片;否则让引擎按需加载
|
|
1330
|
+
if (this.options.preloadImages && this.options.enableCORS && selectedEngine === 'modern-screenshot') {
|
|
1331
|
+
// 清理过期缓存
|
|
1332
|
+
this.cleanExpiredCache();
|
|
1333
|
+
// 如果启用 IndexedDB,也清理 IndexedDB 缓存
|
|
1334
|
+
if (this.options.useIndexedDB) {
|
|
1335
|
+
await this.cleanIndexedDBCache();
|
|
1336
|
+
}
|
|
1317
1337
|
await this.preprocessNetworkImages(this.targetElement);
|
|
1318
1338
|
await this.waitForImagesToLoad(this.targetElement);
|
|
1319
1339
|
}
|
|
1340
|
+
else {
|
|
1341
|
+
// 即使不预加载,也清理一下过期缓存
|
|
1342
|
+
this.cleanExpiredCache();
|
|
1343
|
+
// 如果启用 IndexedDB,也清理 IndexedDB 缓存
|
|
1344
|
+
if (this.options.useIndexedDB) {
|
|
1345
|
+
await this.cleanIndexedDBCache();
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1320
1348
|
let dataUrl;
|
|
1321
1349
|
// 等待一小段时间,确保 DOM 更新完成(减少等待时间)
|
|
1322
1350
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1323
|
-
//
|
|
1324
|
-
|
|
1351
|
+
// 根据选择的引擎进行截图
|
|
1352
|
+
if (selectedEngine === 'snapdom') {
|
|
1353
|
+
dataUrl = await this.takeScreenshotWithSnapdom(this.targetElement);
|
|
1354
|
+
}
|
|
1355
|
+
else {
|
|
1356
|
+
// 默认使用 modern-screenshot
|
|
1357
|
+
dataUrl = await this.takeScreenshotWithModernScreenshot(this.targetElement);
|
|
1358
|
+
}
|
|
1325
1359
|
const timestamp = Date.now();
|
|
1326
1360
|
// 更新状态
|
|
1327
1361
|
this.screenshotCount++;
|
|
@@ -1385,6 +1419,94 @@ class ScreenshotManager {
|
|
|
1385
1419
|
this.removeGlobalErrorHandlers();
|
|
1386
1420
|
}
|
|
1387
1421
|
}
|
|
1422
|
+
/**
|
|
1423
|
+
* 使用 snapdom 截图
|
|
1424
|
+
* 参考: https://snapdom.dev/#cors
|
|
1425
|
+
* 参考: https://github.com/zumerlab/snapdom/blob/main/README_CN.md
|
|
1426
|
+
*
|
|
1427
|
+
* 注意:snapdom 内部使用 worker 进行截图处理,会在后台线程执行,不会阻塞主线程
|
|
1428
|
+
*/
|
|
1429
|
+
async takeScreenshotWithSnapdom(element) {
|
|
1430
|
+
if (!this.options.silentMode) {
|
|
1431
|
+
console.log('📸 使用 snapdom 引擎截图...');
|
|
1432
|
+
}
|
|
1433
|
+
try {
|
|
1434
|
+
// 检查元素是否存在和可见
|
|
1435
|
+
const rect = element.getBoundingClientRect();
|
|
1436
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
1437
|
+
throw new Error('元素尺寸为 0,无法截图');
|
|
1438
|
+
}
|
|
1439
|
+
// 构建 snapdom 选项
|
|
1440
|
+
const options = {};
|
|
1441
|
+
// 如果配置了代理服务器,使用 useProxy 选项处理跨域图片和字体
|
|
1442
|
+
// 参考: https://github.com/zumerlab/snapdom/blob/main/README_CN.md#跨域图片和字体-useproxy
|
|
1443
|
+
if (this.options.proxyUrl && this.options.proxyUrl.trim() !== '') {
|
|
1444
|
+
// snapdom 的 useProxy 是字符串格式的代理 URL 模板
|
|
1445
|
+
// 它会自动将跨域图片 URL 附加到代理 URL 后面
|
|
1446
|
+
let proxyUrl = this.options.proxyUrl.trim();
|
|
1447
|
+
// 确保代理 URL 以 ?url= 结尾(snapdom 会自动附加图片 URL)
|
|
1448
|
+
if (!proxyUrl.includes('?url=')) {
|
|
1449
|
+
proxyUrl = proxyUrl.endsWith('?') ? proxyUrl + 'url=' : proxyUrl + '?url=';
|
|
1450
|
+
}
|
|
1451
|
+
options.useProxy = proxyUrl;
|
|
1452
|
+
if (!this.options.silentMode) {
|
|
1453
|
+
console.log(`📸 使用代理服务器处理跨域图片: ${proxyUrl}`);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
else {
|
|
1457
|
+
if (!this.options.silentMode) {
|
|
1458
|
+
console.log('📸 不使用代理,直接截图(可能遇到 CORS 问题)');
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
// 根据输出格式选择对应的快捷方法
|
|
1462
|
+
// 参考: https://github.com/zumerlab/snapdom/blob/main/README_CN.md#快捷方法
|
|
1463
|
+
const outputFormat = this.options.outputFormat || 'png';
|
|
1464
|
+
let img;
|
|
1465
|
+
switch (outputFormat) {
|
|
1466
|
+
case 'jpeg':
|
|
1467
|
+
// 使用 toJpg 快捷方法(snapdom 使用 jpg 作为方法名)
|
|
1468
|
+
img = await snapdom.toJpg(element, options);
|
|
1469
|
+
break;
|
|
1470
|
+
case 'webp':
|
|
1471
|
+
// 使用 toWebp 快捷方法
|
|
1472
|
+
img = await snapdom.toWebp(element, options);
|
|
1473
|
+
break;
|
|
1474
|
+
case 'png':
|
|
1475
|
+
default:
|
|
1476
|
+
// 使用 toPng 快捷方法(默认)
|
|
1477
|
+
img = await snapdom.toPng(element, options);
|
|
1478
|
+
break;
|
|
1479
|
+
}
|
|
1480
|
+
// 获取 base64 数据(HTMLImageElement 的 src 属性包含 data URL)
|
|
1481
|
+
const dataUrl = img.src;
|
|
1482
|
+
// 验证 base64 是否有效
|
|
1483
|
+
if (!dataUrl || dataUrl.length < 100) {
|
|
1484
|
+
throw new Error('生成的 base64 数据无效或过短');
|
|
1485
|
+
}
|
|
1486
|
+
if (!this.options.silentMode) {
|
|
1487
|
+
console.log(`📸 snapdom 截图成功!格式: ${outputFormat}, 尺寸: ${img.width}x${img.height}`);
|
|
1488
|
+
}
|
|
1489
|
+
return dataUrl;
|
|
1490
|
+
}
|
|
1491
|
+
catch (error) {
|
|
1492
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1493
|
+
const errorName = error instanceof Error ? error.name : 'Unknown';
|
|
1494
|
+
// 针对不同类型的错误给出具体提示
|
|
1495
|
+
if (errorName === 'EncodingError' || errorMessage.includes('cannot be decoded')) {
|
|
1496
|
+
if (!this.options.silentMode) {
|
|
1497
|
+
console.warn('📸 ⚠️ 图片解码失败 - 这通常是因为跨域图片无法访问');
|
|
1498
|
+
console.warn('📸 💡 解决方案:配置 proxyUrl 选项');
|
|
1499
|
+
console.warn('📸 📖 参考: https://snapdom.dev/#cors');
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
else if (errorMessage.includes('CORS') || errorMessage.includes('cross-origin')) {
|
|
1503
|
+
if (!this.options.silentMode) {
|
|
1504
|
+
console.warn('📸 ⚠️ 检测到 CORS 错误,建议配置 proxyUrl 选项');
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
throw error;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1388
1510
|
/**
|
|
1389
1511
|
* 使用 modern-screenshot 截图(启用 Worker)
|
|
1390
1512
|
*/
|
|
@@ -1416,9 +1538,25 @@ class ScreenshotManager {
|
|
|
1416
1538
|
}
|
|
1417
1539
|
// 如果配置了代理服务器,使用代理处理跨域图片
|
|
1418
1540
|
if (this.options.proxyUrl && this.options.proxyUrl.trim() !== '') {
|
|
1419
|
-
//
|
|
1420
|
-
|
|
1421
|
-
|
|
1541
|
+
// 检查内存缓存(优先使用缓存,带过期时间检查)
|
|
1542
|
+
const cachedDataUrl = this.getCachedImage(url);
|
|
1543
|
+
if (cachedDataUrl) {
|
|
1544
|
+
if (!this.options.silentMode) {
|
|
1545
|
+
console.log(`📸 ✅ 使用内存缓存图片: ${url.substring(0, 50)}...`);
|
|
1546
|
+
}
|
|
1547
|
+
return cachedDataUrl;
|
|
1548
|
+
}
|
|
1549
|
+
// 检查 IndexedDB 缓存(如果启用)
|
|
1550
|
+
if (this.options.useIndexedDB) {
|
|
1551
|
+
const indexedDBCache = await this.getIndexedDBCache(url);
|
|
1552
|
+
if (indexedDBCache) {
|
|
1553
|
+
// 同步到内存缓存
|
|
1554
|
+
this.setCachedImage(url, indexedDBCache);
|
|
1555
|
+
if (!this.options.silentMode) {
|
|
1556
|
+
console.log(`📸 ✅ 使用 IndexedDB 缓存图片: ${url.substring(0, 50)}...`);
|
|
1557
|
+
}
|
|
1558
|
+
return indexedDBCache;
|
|
1559
|
+
}
|
|
1422
1560
|
}
|
|
1423
1561
|
try {
|
|
1424
1562
|
// 构建代理请求参数
|
|
@@ -1432,11 +1570,11 @@ class ScreenshotManager {
|
|
|
1432
1570
|
let baseUrl = this.options.proxyUrl;
|
|
1433
1571
|
baseUrl = baseUrl.replace(/[?&]$/, '');
|
|
1434
1572
|
const proxyUrl = `${baseUrl}?${params.toString()}`;
|
|
1435
|
-
//
|
|
1573
|
+
// 请求代理服务器(优化:添加超时控制和优先级)
|
|
1436
1574
|
const controller = new AbortController();
|
|
1437
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
1575
|
+
const timeoutId = setTimeout(() => controller.abort(), this.options.imageLoadTimeout);
|
|
1438
1576
|
try {
|
|
1439
|
-
const
|
|
1577
|
+
const fetchOptions = {
|
|
1440
1578
|
method: 'GET',
|
|
1441
1579
|
mode: 'cors',
|
|
1442
1580
|
credentials: 'omit',
|
|
@@ -1445,15 +1583,24 @@ class ScreenshotManager {
|
|
|
1445
1583
|
},
|
|
1446
1584
|
cache: 'no-cache',
|
|
1447
1585
|
signal: controller.signal
|
|
1448
|
-
}
|
|
1586
|
+
};
|
|
1587
|
+
// 添加 fetch priority(如果支持)
|
|
1588
|
+
if ('priority' in fetchOptions) {
|
|
1589
|
+
fetchOptions.priority = this.options.fetchPriority;
|
|
1590
|
+
}
|
|
1591
|
+
const response = await fetch(proxyUrl, fetchOptions);
|
|
1449
1592
|
clearTimeout(timeoutId);
|
|
1450
1593
|
if (!response.ok) {
|
|
1451
1594
|
throw new Error(`代理请求失败: ${response.status}`);
|
|
1452
1595
|
}
|
|
1453
1596
|
const blob = await response.blob();
|
|
1454
1597
|
const dataUrl = await this.blobToDataUrl(blob);
|
|
1455
|
-
//
|
|
1456
|
-
this.
|
|
1598
|
+
// 缓存结果(带时间戳,10分钟有效)
|
|
1599
|
+
this.setCachedImage(url, dataUrl);
|
|
1600
|
+
// 如果启用 IndexedDB,也保存到 IndexedDB
|
|
1601
|
+
if (this.options.useIndexedDB) {
|
|
1602
|
+
await this.setIndexedDBCache(url, dataUrl);
|
|
1603
|
+
}
|
|
1457
1604
|
return dataUrl;
|
|
1458
1605
|
}
|
|
1459
1606
|
catch (fetchError) {
|
|
@@ -1500,11 +1647,7 @@ class ScreenshotManager {
|
|
|
1500
1647
|
if (this.options.scale !== 1) {
|
|
1501
1648
|
contextOptions.scale = isMobile ? 0.8 : this.options.scale;
|
|
1502
1649
|
}
|
|
1503
|
-
//
|
|
1504
|
-
const resolvedWorkerUrl = await getWorkerUrl();
|
|
1505
|
-
if (resolvedWorkerUrl) {
|
|
1506
|
-
contextOptions.workerUrl = resolvedWorkerUrl;
|
|
1507
|
-
}
|
|
1650
|
+
// modern-screenshot 会自动处理 worker URL,不需要手动设置
|
|
1508
1651
|
// 创建 Worker 上下文
|
|
1509
1652
|
this.screenshotContext = await modernScreenshot.createContext(element, contextOptions);
|
|
1510
1653
|
}
|
|
@@ -1568,23 +1711,336 @@ class ScreenshotManager {
|
|
|
1568
1711
|
}
|
|
1569
1712
|
}
|
|
1570
1713
|
/**
|
|
1571
|
-
*
|
|
1714
|
+
* 预连接代理服务器(优化网络性能)
|
|
1715
|
+
*/
|
|
1716
|
+
preconnectProxy() {
|
|
1717
|
+
if (this.preconnected || !this.options.proxyUrl) {
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
try {
|
|
1721
|
+
const proxyOrigin = new URL(this.options.proxyUrl).origin;
|
|
1722
|
+
const link = document.createElement('link');
|
|
1723
|
+
link.rel = 'preconnect';
|
|
1724
|
+
link.href = proxyOrigin;
|
|
1725
|
+
link.crossOrigin = 'anonymous';
|
|
1726
|
+
document.head.appendChild(link);
|
|
1727
|
+
this.preconnected = true;
|
|
1728
|
+
if (!this.options.silentMode) {
|
|
1729
|
+
console.log(`📸 ✅ 已预连接代理服务器: ${proxyOrigin}`);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
catch (e) {
|
|
1733
|
+
// 预连接失败,不影响功能
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* 初始化 Intersection Observer(优化可见性检测)
|
|
1738
|
+
*/
|
|
1739
|
+
initIntersectionObserver() {
|
|
1740
|
+
if (!('IntersectionObserver' in window)) {
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
this.intersectionObserver = new IntersectionObserver((entries) => {
|
|
1744
|
+
entries.forEach((entry) => {
|
|
1745
|
+
if (entry.isIntersecting) {
|
|
1746
|
+
this.visibleElementsCache.add(entry.target);
|
|
1747
|
+
}
|
|
1748
|
+
else {
|
|
1749
|
+
this.visibleElementsCache.delete(entry.target);
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
}, {
|
|
1753
|
+
root: null,
|
|
1754
|
+
rootMargin: '50px', // 提前50px开始加载
|
|
1755
|
+
threshold: 0.01
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* 初始化 IndexedDB(持久化缓存)
|
|
1760
|
+
*/
|
|
1761
|
+
async initIndexedDB() {
|
|
1762
|
+
return new Promise((resolve, reject) => {
|
|
1763
|
+
const request = indexedDB.open('screenshot-cache', 1);
|
|
1764
|
+
request.onerror = () => reject(request.error);
|
|
1765
|
+
request.onsuccess = () => {
|
|
1766
|
+
this.indexedDBCache = request.result;
|
|
1767
|
+
this.indexedDBReady = true;
|
|
1768
|
+
// 初始化后立即清理过期和超大小的缓存
|
|
1769
|
+
this.cleanIndexedDBCache().catch(() => {
|
|
1770
|
+
// 清理失败不影响功能
|
|
1771
|
+
});
|
|
1772
|
+
resolve();
|
|
1773
|
+
};
|
|
1774
|
+
request.onupgradeneeded = (event) => {
|
|
1775
|
+
const db = event.target.result;
|
|
1776
|
+
if (!db.objectStoreNames.contains('images')) {
|
|
1777
|
+
const store = db.createObjectStore('images', { keyPath: 'url' });
|
|
1778
|
+
// 创建索引用于按时间排序
|
|
1779
|
+
store.createIndex('timestamp', 'timestamp', { unique: false });
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* 从 IndexedDB 获取缓存
|
|
1786
|
+
*/
|
|
1787
|
+
async getIndexedDBCache(url) {
|
|
1788
|
+
if (!this.indexedDBReady || !this.indexedDBCache) {
|
|
1789
|
+
return null;
|
|
1790
|
+
}
|
|
1791
|
+
try {
|
|
1792
|
+
const transaction = this.indexedDBCache.transaction(['images'], 'readonly');
|
|
1793
|
+
const store = transaction.objectStore('images');
|
|
1794
|
+
const request = store.get(url);
|
|
1795
|
+
return new Promise((resolve) => {
|
|
1796
|
+
request.onsuccess = () => {
|
|
1797
|
+
const result = request.result;
|
|
1798
|
+
if (result) {
|
|
1799
|
+
const now = Date.now();
|
|
1800
|
+
const age = now - result.timestamp;
|
|
1801
|
+
// 检查是否超过最大缓存时间
|
|
1802
|
+
if (age > this.options.maxCacheAge) {
|
|
1803
|
+
// 缓存过期,删除
|
|
1804
|
+
this.deleteIndexedDBCache(url);
|
|
1805
|
+
resolve(null);
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
// 返回缓存数据(即使超过 imageCacheTTL,只要未超过 maxCacheAge 仍可使用)
|
|
1809
|
+
resolve(result.dataUrl);
|
|
1810
|
+
}
|
|
1811
|
+
else {
|
|
1812
|
+
resolve(null);
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
request.onerror = () => resolve(null);
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
catch {
|
|
1819
|
+
return null;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* 设置 IndexedDB 缓存(带大小和时间控制)
|
|
1824
|
+
*/
|
|
1825
|
+
async setIndexedDBCache(url, dataUrl) {
|
|
1826
|
+
if (!this.indexedDBReady || !this.indexedDBCache) {
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
try {
|
|
1830
|
+
// 计算当前缓存大小
|
|
1831
|
+
const currentSize = await this.getIndexedDBCacheSize();
|
|
1832
|
+
const newItemSize = this.estimateDataUrlSize(dataUrl);
|
|
1833
|
+
const maxSizeBytes = (this.options.maxCacheSize || 50) * 1024 * 1024; // 转换为字节
|
|
1834
|
+
// 如果添加新项后超过限制,清理最旧的数据
|
|
1835
|
+
if (currentSize + newItemSize > maxSizeBytes) {
|
|
1836
|
+
await this.cleanIndexedDBCacheBySize(maxSizeBytes - newItemSize);
|
|
1837
|
+
}
|
|
1838
|
+
const transaction = this.indexedDBCache.transaction(['images'], 'readwrite');
|
|
1839
|
+
const store = transaction.objectStore('images');
|
|
1840
|
+
store.put({ url, dataUrl, timestamp: Date.now() });
|
|
1841
|
+
}
|
|
1842
|
+
catch {
|
|
1843
|
+
// IndexedDB 写入失败,忽略
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* 估算 data URL 的大小(字节)
|
|
1848
|
+
*/
|
|
1849
|
+
estimateDataUrlSize(dataUrl) {
|
|
1850
|
+
// Base64 编码后的大小约为原始大小的 4/3
|
|
1851
|
+
// data:image/xxx;base64, 前缀约 22-30 字节
|
|
1852
|
+
const base64Data = dataUrl.split(',')[1] || '';
|
|
1853
|
+
return Math.ceil(base64Data.length * 0.75) + 30;
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* 获取 IndexedDB 当前缓存大小(字节)
|
|
1857
|
+
*/
|
|
1858
|
+
async getIndexedDBCacheSize() {
|
|
1859
|
+
if (!this.indexedDBReady || !this.indexedDBCache) {
|
|
1860
|
+
return 0;
|
|
1861
|
+
}
|
|
1862
|
+
try {
|
|
1863
|
+
const transaction = this.indexedDBCache.transaction(['images'], 'readonly');
|
|
1864
|
+
const store = transaction.objectStore('images');
|
|
1865
|
+
const request = store.getAll();
|
|
1866
|
+
return new Promise((resolve) => {
|
|
1867
|
+
request.onsuccess = () => {
|
|
1868
|
+
const items = request.result || [];
|
|
1869
|
+
let totalSize = 0;
|
|
1870
|
+
items.forEach((item) => {
|
|
1871
|
+
totalSize += this.estimateDataUrlSize(item.dataUrl);
|
|
1872
|
+
});
|
|
1873
|
+
resolve(totalSize);
|
|
1874
|
+
};
|
|
1875
|
+
request.onerror = () => resolve(0);
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
catch {
|
|
1879
|
+
return 0;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* 清理 IndexedDB 缓存(按时间和大小)
|
|
1884
|
+
*/
|
|
1885
|
+
async cleanIndexedDBCache() {
|
|
1886
|
+
if (!this.indexedDBReady || !this.indexedDBCache) {
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
try {
|
|
1890
|
+
const transaction = this.indexedDBCache.transaction(['images'], 'readwrite');
|
|
1891
|
+
const store = transaction.objectStore('images');
|
|
1892
|
+
const index = store.index('timestamp');
|
|
1893
|
+
const request = index.getAll();
|
|
1894
|
+
return new Promise((resolve) => {
|
|
1895
|
+
request.onsuccess = () => {
|
|
1896
|
+
const items = request.result || [];
|
|
1897
|
+
const now = Date.now();
|
|
1898
|
+
const expiredUrls = [];
|
|
1899
|
+
let currentSize = 0;
|
|
1900
|
+
const maxSizeBytes = (this.options.maxCacheSize || 50) * 1024 * 1024;
|
|
1901
|
+
// 按时间排序(最旧的在前)
|
|
1902
|
+
items.sort((a, b) => a.timestamp - b.timestamp);
|
|
1903
|
+
// 清理过期数据
|
|
1904
|
+
items.forEach((item) => {
|
|
1905
|
+
const age = now - item.timestamp;
|
|
1906
|
+
if (age > this.options.maxCacheAge) {
|
|
1907
|
+
expiredUrls.push(item.url);
|
|
1908
|
+
}
|
|
1909
|
+
else {
|
|
1910
|
+
currentSize += this.estimateDataUrlSize(item.dataUrl);
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
// 如果仍然超过大小限制,删除最旧的数据
|
|
1914
|
+
const urlsToDelete = [...expiredUrls];
|
|
1915
|
+
if (currentSize > maxSizeBytes) {
|
|
1916
|
+
for (const item of items) {
|
|
1917
|
+
if (expiredUrls.includes(item.url))
|
|
1918
|
+
continue;
|
|
1919
|
+
currentSize -= this.estimateDataUrlSize(item.dataUrl);
|
|
1920
|
+
urlsToDelete.push(item.url);
|
|
1921
|
+
if (currentSize <= maxSizeBytes) {
|
|
1922
|
+
break;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
// 删除过期和超大小的数据
|
|
1927
|
+
urlsToDelete.forEach((url) => {
|
|
1928
|
+
store.delete(url);
|
|
1929
|
+
});
|
|
1930
|
+
if (urlsToDelete.length > 0 && !this.options.silentMode) {
|
|
1931
|
+
console.log(`📸 IndexedDB 清理了 ${urlsToDelete.length} 个缓存项(过期或超大小)`);
|
|
1932
|
+
}
|
|
1933
|
+
resolve();
|
|
1934
|
+
};
|
|
1935
|
+
request.onerror = () => resolve();
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
catch {
|
|
1939
|
+
// 清理失败,忽略
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* 按大小清理 IndexedDB 缓存
|
|
1944
|
+
*/
|
|
1945
|
+
async cleanIndexedDBCacheBySize(targetSize) {
|
|
1946
|
+
if (!this.indexedDBReady || !this.indexedDBCache) {
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
try {
|
|
1950
|
+
const transaction = this.indexedDBCache.transaction(['images'], 'readwrite');
|
|
1951
|
+
const store = transaction.objectStore('images');
|
|
1952
|
+
const index = store.index('timestamp');
|
|
1953
|
+
const request = index.getAll();
|
|
1954
|
+
return new Promise((resolve) => {
|
|
1955
|
+
request.onsuccess = () => {
|
|
1956
|
+
const items = request.result || [];
|
|
1957
|
+
// 按时间排序(最旧的在前)
|
|
1958
|
+
items.sort((a, b) => a.timestamp - b.timestamp);
|
|
1959
|
+
let currentSize = 0;
|
|
1960
|
+
const urlsToDelete = [];
|
|
1961
|
+
// 计算当前大小
|
|
1962
|
+
items.forEach((item) => {
|
|
1963
|
+
currentSize += this.estimateDataUrlSize(item.dataUrl);
|
|
1964
|
+
});
|
|
1965
|
+
// 如果超过目标大小,删除最旧的数据
|
|
1966
|
+
if (currentSize > targetSize) {
|
|
1967
|
+
for (const item of items) {
|
|
1968
|
+
if (currentSize <= targetSize) {
|
|
1969
|
+
break;
|
|
1970
|
+
}
|
|
1971
|
+
currentSize -= this.estimateDataUrlSize(item.dataUrl);
|
|
1972
|
+
urlsToDelete.push(item.url);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
// 删除数据
|
|
1976
|
+
urlsToDelete.forEach((url) => {
|
|
1977
|
+
store.delete(url);
|
|
1978
|
+
});
|
|
1979
|
+
if (urlsToDelete.length > 0 && !this.options.silentMode) {
|
|
1980
|
+
console.log(`📸 IndexedDB 清理了 ${urlsToDelete.length} 个缓存项(超过大小限制)`);
|
|
1981
|
+
}
|
|
1982
|
+
resolve();
|
|
1983
|
+
};
|
|
1984
|
+
request.onerror = () => resolve();
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
catch {
|
|
1988
|
+
// 清理失败,忽略
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* 删除 IndexedDB 缓存
|
|
1993
|
+
*/
|
|
1994
|
+
async deleteIndexedDBCache(url) {
|
|
1995
|
+
if (!this.indexedDBReady || !this.indexedDBCache) {
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
try {
|
|
1999
|
+
const transaction = this.indexedDBCache.transaction(['images'], 'readwrite');
|
|
2000
|
+
const store = transaction.objectStore('images');
|
|
2001
|
+
store.delete(url);
|
|
2002
|
+
}
|
|
2003
|
+
catch {
|
|
2004
|
+
// 忽略错误
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
/**
|
|
2008
|
+
* 检查元素是否在可视区域内(优化:使用 Intersection Observer 或 getBoundingClientRect)
|
|
1572
2009
|
*/
|
|
1573
2010
|
isElementVisible(element, container) {
|
|
1574
2011
|
if (!this.options.onlyVisibleImages) {
|
|
1575
2012
|
return true; // 如果禁用可视区域检测,返回 true
|
|
1576
2013
|
}
|
|
2014
|
+
// 如果使用 Intersection Observer 且元素已在缓存中,直接返回
|
|
2015
|
+
if (this.options.useIntersectionObserver && this.intersectionObserver) {
|
|
2016
|
+
if (this.visibleElementsCache.has(element)) {
|
|
2017
|
+
return true;
|
|
2018
|
+
}
|
|
2019
|
+
// 观察元素(如果还没有观察)
|
|
2020
|
+
this.intersectionObserver.observe(element);
|
|
2021
|
+
}
|
|
2022
|
+
// 回退到 getBoundingClientRect
|
|
1577
2023
|
try {
|
|
1578
2024
|
const rect = element.getBoundingClientRect();
|
|
1579
2025
|
const containerRect = container.getBoundingClientRect();
|
|
1580
2026
|
// 检查元素是否与容器有交集(考虑滚动位置)
|
|
1581
|
-
|
|
2027
|
+
const isIntersecting = !(rect.bottom < containerRect.top ||
|
|
1582
2028
|
rect.top > containerRect.bottom ||
|
|
1583
2029
|
rect.right < containerRect.left ||
|
|
1584
2030
|
rect.left > containerRect.right) && (rect.width > 0 && rect.height > 0 && // 元素有尺寸
|
|
1585
2031
|
window.getComputedStyle(element).display !== 'none' && // 元素可见
|
|
1586
2032
|
window.getComputedStyle(element).visibility !== 'hidden' &&
|
|
1587
2033
|
window.getComputedStyle(element).opacity !== '0');
|
|
2034
|
+
// 更新 Intersection Observer 缓存
|
|
2035
|
+
if (this.options.useIntersectionObserver && this.intersectionObserver) {
|
|
2036
|
+
if (isIntersecting) {
|
|
2037
|
+
this.visibleElementsCache.add(element);
|
|
2038
|
+
}
|
|
2039
|
+
else {
|
|
2040
|
+
this.visibleElementsCache.delete(element);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
return isIntersecting;
|
|
1588
2044
|
}
|
|
1589
2045
|
catch {
|
|
1590
2046
|
return true; // 出错时返回 true,保守处理
|
|
@@ -1626,16 +2082,16 @@ class ScreenshotManager {
|
|
|
1626
2082
|
// 并发处理当前批次
|
|
1627
2083
|
await Promise.all(batch.map(async (img) => {
|
|
1628
2084
|
const originalSrc = img.src;
|
|
1629
|
-
//
|
|
1630
|
-
if (this.
|
|
2085
|
+
// 检查缓存(带过期时间)
|
|
2086
|
+
if (this.getCachedImage(originalSrc)) {
|
|
1631
2087
|
return;
|
|
1632
2088
|
}
|
|
1633
2089
|
try {
|
|
1634
2090
|
// 使用代理服务器获取图片
|
|
1635
2091
|
const dataUrl = await this.proxyImage(originalSrc);
|
|
1636
2092
|
if (dataUrl) {
|
|
1637
|
-
// 缓存 data URL
|
|
1638
|
-
this.
|
|
2093
|
+
// 缓存 data URL(带时间戳)
|
|
2094
|
+
this.setCachedImage(originalSrc, dataUrl);
|
|
1639
2095
|
}
|
|
1640
2096
|
}
|
|
1641
2097
|
catch (error) {
|
|
@@ -1716,7 +2172,7 @@ class ScreenshotManager {
|
|
|
1716
2172
|
});
|
|
1717
2173
|
}
|
|
1718
2174
|
/**
|
|
1719
|
-
*
|
|
2175
|
+
* 等待图片加载完成(优化:使用更短的超时和 Promise.race)
|
|
1720
2176
|
*/
|
|
1721
2177
|
async waitForImagesToLoad(element) {
|
|
1722
2178
|
const images = element.querySelectorAll('img');
|
|
@@ -1725,32 +2181,33 @@ class ScreenshotManager {
|
|
|
1725
2181
|
if (img.complete) {
|
|
1726
2182
|
return;
|
|
1727
2183
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
};
|
|
1740
|
-
}));
|
|
2184
|
+
// 使用更短的超时时间(可配置)
|
|
2185
|
+
const timeout = this.options.imageLoadTimeout || 5000;
|
|
2186
|
+
imagePromises.push(Promise.race([
|
|
2187
|
+
new Promise((resolve) => {
|
|
2188
|
+
img.onload = () => resolve();
|
|
2189
|
+
img.onerror = () => resolve(); // 失败也继续
|
|
2190
|
+
}),
|
|
2191
|
+
new Promise((resolve) => {
|
|
2192
|
+
setTimeout(() => resolve(), timeout);
|
|
2193
|
+
})
|
|
2194
|
+
]));
|
|
1741
2195
|
});
|
|
1742
2196
|
if (imagePromises.length > 0) {
|
|
1743
2197
|
await Promise.all(imagePromises);
|
|
1744
2198
|
}
|
|
1745
2199
|
}
|
|
1746
2200
|
/**
|
|
1747
|
-
* 等待 CSS
|
|
2201
|
+
* 等待 CSS 和字体加载完成(优化:减少等待时间,使用 requestAnimationFrame)
|
|
1748
2202
|
*/
|
|
1749
2203
|
async waitForStylesAndFonts() {
|
|
2204
|
+
// 使用 requestAnimationFrame 优化渲染时机
|
|
1750
2205
|
return new Promise((resolve) => {
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
2206
|
+
requestAnimationFrame(() => {
|
|
2207
|
+
setTimeout(() => {
|
|
2208
|
+
resolve();
|
|
2209
|
+
}, 30); // 减少到30ms
|
|
2210
|
+
});
|
|
1754
2211
|
});
|
|
1755
2212
|
}
|
|
1756
2213
|
/**
|
|
@@ -1970,9 +2427,86 @@ class ScreenshotManager {
|
|
|
1970
2427
|
this.messageHandler = null;
|
|
1971
2428
|
}
|
|
1972
2429
|
this.removeGlobalErrorHandlers();
|
|
2430
|
+
// 清理 Intersection Observer
|
|
2431
|
+
if (this.intersectionObserver) {
|
|
2432
|
+
this.intersectionObserver.disconnect();
|
|
2433
|
+
this.intersectionObserver = null;
|
|
2434
|
+
this.visibleElementsCache.clear();
|
|
2435
|
+
}
|
|
2436
|
+
// 关闭 IndexedDB
|
|
2437
|
+
if (this.indexedDBCache) {
|
|
2438
|
+
this.indexedDBCache.close();
|
|
2439
|
+
this.indexedDBCache = null;
|
|
2440
|
+
this.indexedDBReady = false;
|
|
2441
|
+
}
|
|
1973
2442
|
// 清理图片代理缓存
|
|
1974
2443
|
this.imageProxyCache.clear();
|
|
1975
2444
|
}
|
|
2445
|
+
/**
|
|
2446
|
+
* 获取缓存的图片(检查是否过期)
|
|
2447
|
+
*/
|
|
2448
|
+
getCachedImage(url) {
|
|
2449
|
+
const cached = this.imageProxyCache.get(url);
|
|
2450
|
+
if (!cached) {
|
|
2451
|
+
return null;
|
|
2452
|
+
}
|
|
2453
|
+
// 检查是否过期(10分钟)
|
|
2454
|
+
const now = Date.now();
|
|
2455
|
+
const age = now - cached.timestamp;
|
|
2456
|
+
if (age > this.options.imageCacheTTL) {
|
|
2457
|
+
// 缓存已过期,删除
|
|
2458
|
+
this.imageProxyCache.delete(url);
|
|
2459
|
+
return null;
|
|
2460
|
+
}
|
|
2461
|
+
return cached.dataUrl;
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* 设置缓存的图片(带时间戳)
|
|
2465
|
+
*/
|
|
2466
|
+
setCachedImage(url, dataUrl) {
|
|
2467
|
+
this.imageProxyCache.set(url, {
|
|
2468
|
+
dataUrl,
|
|
2469
|
+
timestamp: Date.now()
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* 清理过期缓存
|
|
2474
|
+
*/
|
|
2475
|
+
cleanExpiredCache() {
|
|
2476
|
+
const now = Date.now();
|
|
2477
|
+
const expiredUrls = [];
|
|
2478
|
+
this.imageProxyCache.forEach((cached, url) => {
|
|
2479
|
+
const age = now - cached.timestamp;
|
|
2480
|
+
if (age > this.options.imageCacheTTL) {
|
|
2481
|
+
expiredUrls.push(url);
|
|
2482
|
+
}
|
|
2483
|
+
});
|
|
2484
|
+
expiredUrls.forEach(url => {
|
|
2485
|
+
this.imageProxyCache.delete(url);
|
|
2486
|
+
});
|
|
2487
|
+
if (expiredUrls.length > 0 && !this.options.silentMode) {
|
|
2488
|
+
console.log(`📸 清理了 ${expiredUrls.length} 个过期缓存`);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
/**
|
|
2492
|
+
* 定期清理过期缓存(可选,在截图时也会自动清理)
|
|
2493
|
+
*/
|
|
2494
|
+
startCacheCleanup() {
|
|
2495
|
+
// 每5分钟清理一次过期缓存
|
|
2496
|
+
setInterval(() => {
|
|
2497
|
+
this.cleanExpiredCache();
|
|
2498
|
+
// 如果启用 IndexedDB,也清理 IndexedDB 缓存
|
|
2499
|
+
if (this.options.useIndexedDB) {
|
|
2500
|
+
this.cleanIndexedDBCache().catch(() => {
|
|
2501
|
+
// 清理失败,忽略
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
if (!this.options.silentMode) {
|
|
2505
|
+
const memoryCacheSize = this.imageProxyCache.size;
|
|
2506
|
+
console.log(`📸 清理过期缓存,内存缓存数量: ${memoryCacheSize}`);
|
|
2507
|
+
}
|
|
2508
|
+
}, 300000); // 5分钟
|
|
2509
|
+
}
|
|
1976
2510
|
/**
|
|
1977
2511
|
* 获取状态
|
|
1978
2512
|
*/
|