customer-chat-sdk 1.0.24 → 1.0.26

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.
@@ -1,4 +1,4 @@
1
- import html2canvas from 'html2canvas';
1
+ import { destroyContext, createContext, domToPng } from 'modern-screenshot';
2
2
 
3
3
  // 直接使用base64字符串,避免打包后路径问题
4
4
  const iconImage = 'data:image/png;base64,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';
@@ -921,7 +921,29 @@ class IframeManager {
921
921
  }
922
922
  }
923
923
 
924
- // @ts-ignore - html2canvas may not have type definitions
924
+ // @ts-ignore - modern-screenshot may not have type definitions
925
+ // Worker URL 将在创建上下文时动态获取
926
+ // 在 Vite 环境中可以使用: import workerUrl from 'modern-screenshot/worker?url'
927
+ // 在 Rollup 中,modern-screenshot 会自动处理 worker URL
928
+ let workerUrl = undefined;
929
+ // 尝试动态获取 worker URL(仅在支持的环境中)
930
+ async function getWorkerUrl() {
931
+ if (workerUrl) {
932
+ return workerUrl;
933
+ }
934
+ try {
935
+ // 尝试使用 Vite 的 ?url 语法(仅在 Vite 环境中有效)
936
+ // @ts-ignore - Vite 特有的 ?url 语法
937
+ const workerModule = await import('modern-screenshot/worker?url');
938
+ workerUrl = workerModule.default || workerModule;
939
+ return workerUrl;
940
+ }
941
+ catch {
942
+ // Rollup 或其他构建工具不支持 ?url 语法
943
+ // modern-screenshot 会自动处理 worker URL,返回 undefined 即可
944
+ return undefined;
945
+ }
946
+ }
925
947
  /**
926
948
  * 截图管理器
927
949
  * 负责页面截图、压缩和上传功能
@@ -943,6 +965,8 @@ class ScreenshotManager {
943
965
  // WebWorker 相关
944
966
  this.worker = null;
945
967
  this.screenshotTimer = null;
968
+ // modern-screenshot Worker 上下文(用于复用,避免频繁创建和销毁)
969
+ this.screenshotContext = null;
946
970
  // PostMessage 监听器
947
971
  this.messageHandler = null;
948
972
  // 动态轮询间隔(由 iframe 消息控制)
@@ -966,7 +990,7 @@ class ScreenshotManager {
966
990
  outputFormat: options.outputFormat ?? 'webp',
967
991
  enableCORS: options.enableCORS ?? true,
968
992
  proxyUrl: options.proxyUrl ?? '',
969
- engine: options.engine ?? 'html2canvas',
993
+ engine: options.engine ?? 'modern-screenshot',
970
994
  corsMode: options.corsMode ?? 'canvas-proxy',
971
995
  silentMode: options.silentMode ?? false,
972
996
  maxRetries: options.maxRetries ?? 2
@@ -978,6 +1002,16 @@ class ScreenshotManager {
978
1002
  * 设置目标元素
979
1003
  */
980
1004
  setTargetElement(element) {
1005
+ // 如果元素改变了,清理旧的 Worker 上下文
1006
+ if (this.targetElement !== element && this.screenshotContext) {
1007
+ try {
1008
+ destroyContext(this.screenshotContext);
1009
+ }
1010
+ catch (e) {
1011
+ // 忽略清理错误
1012
+ }
1013
+ this.screenshotContext = null;
1014
+ }
981
1015
  this.targetElement = element;
982
1016
  }
983
1017
  /**
@@ -1030,8 +1064,11 @@ class ScreenshotManager {
1030
1064
  }
1031
1065
  // 保存当前配置
1032
1066
  this.currentUploadConfig = config;
1033
- // 根据 expirationMinutes 判断是否开启截图功能
1034
- if (config.expirationMinutes > 0) {
1067
+ // 根据 ttl 判断是否开启截图功能
1068
+ // ttl == 0 表示禁用,ttl > 0 且大于当前时间表示有效
1069
+ const currentTime = Date.now();
1070
+ const isValid = config.ttl > 0 && config.ttl > currentTime;
1071
+ if (isValid) {
1035
1072
  // 启用截图功能
1036
1073
  if (!this.isEnabled) {
1037
1074
  if (!this.options.silentMode) {
@@ -1041,9 +1078,12 @@ class ScreenshotManager {
1041
1078
  }
1042
1079
  // 设置动态轮询间隔(使用 duration,单位:毫秒)
1043
1080
  this.dynamicInterval = config.duration || this.options.interval;
1081
+ // 计算剩余有效时间(毫秒)
1082
+ const remainingTime = config.ttl - currentTime;
1044
1083
  // 启动或更新截图轮询
1045
1084
  if (!this.options.silentMode) {
1046
- console.log(`📸 [iframe] 设置轮询间隔: ${this.dynamicInterval}ms,过期时间: ${config.expirationMinutes}分钟`);
1085
+ const remainingMinutes = Math.ceil(remainingTime / 60000);
1086
+ console.log(`📸 [iframe] 设置轮询间隔: ${this.dynamicInterval}ms,剩余有效时间: ${remainingMinutes}分钟`);
1047
1087
  }
1048
1088
  // 先执行一次截图,等待完成后再上传
1049
1089
  this.takeScreenshotAndUpload(config);
@@ -1060,12 +1100,17 @@ class ScreenshotManager {
1060
1100
  this.isEnabled = false;
1061
1101
  this.currentUploadConfig = null;
1062
1102
  this.expirationTimer = null;
1063
- }, config.expirationMinutes * 60 * 1000);
1103
+ }, remainingTime);
1064
1104
  }
1065
1105
  else {
1066
- // 禁用截图功能
1106
+ // 禁用截图功能(ttl == 0 或已过期)
1067
1107
  if (!this.options.silentMode) {
1068
- console.log('📸 [iframe] expirationMinutes <= 0,禁用截图功能');
1108
+ if (config.ttl === 0) {
1109
+ console.log('📸 [iframe] ttl == 0,禁用截图功能');
1110
+ }
1111
+ else {
1112
+ console.log('📸 [iframe] ttl 已过期,禁用截图功能');
1113
+ }
1069
1114
  }
1070
1115
  this.stopScreenshot();
1071
1116
  this.isEnabled = false;
@@ -1149,9 +1194,20 @@ class ScreenshotManager {
1149
1194
  console.error('📸 [上传] 配置缺少必需字段:', config);
1150
1195
  return null;
1151
1196
  }
1197
+ // 确保 duration 存在,如果没有则使用默认值
1152
1198
  if (typeof config.duration !== 'number' || config.duration <= 0) {
1153
1199
  config.duration = this.options.interval;
1154
1200
  }
1201
+ // 确保 ttl 存在,如果没有则尝试从 expirationMinutes 转换(兼容旧格式)
1202
+ if (typeof config.ttl !== 'number') {
1203
+ if (typeof config.expirationMinutes === 'number' && config.expirationMinutes > 0) {
1204
+ // 兼容旧格式:将 expirationMinutes 转换为 ttl
1205
+ config.ttl = Date.now() + config.expirationMinutes * 60 * 1000;
1206
+ }
1207
+ else {
1208
+ config.ttl = 0; // 默认禁用
1209
+ }
1210
+ }
1155
1211
  return config;
1156
1212
  }
1157
1213
  catch (error) {
@@ -1244,10 +1300,10 @@ class ScreenshotManager {
1244
1300
  this.waitForStylesAndFonts(),
1245
1301
  this.waitForFonts()
1246
1302
  ]);
1247
- // 选择截图引擎(仅支持 html2canvas
1248
- const selectedEngine = 'html2canvas';
1303
+ // 选择截图引擎(仅支持 modern-screenshot
1304
+ const selectedEngine = 'modern-screenshot';
1249
1305
  if (!this.options.silentMode) {
1250
- console.log(`📸 使用截图引擎: ${selectedEngine}`);
1306
+ console.log(`📸 使用截图引擎: ${selectedEngine} (Worker 模式)`);
1251
1307
  }
1252
1308
  // 预处理网络图片
1253
1309
  if (this.options.enableCORS) {
@@ -1257,8 +1313,8 @@ class ScreenshotManager {
1257
1313
  let dataUrl;
1258
1314
  // 等待一小段时间,确保 DOM 更新完成
1259
1315
  await new Promise(resolve => setTimeout(resolve, 100));
1260
- // 使用 html2canvas 截图
1261
- dataUrl = await this.takeScreenshotWithHtml2Canvas(this.targetElement);
1316
+ // 使用 modern-screenshot 截图(启用 Worker)
1317
+ dataUrl = await this.takeScreenshotWithModernScreenshot(this.targetElement);
1262
1318
  const timestamp = Date.now();
1263
1319
  // 更新状态
1264
1320
  this.screenshotCount++;
@@ -1323,11 +1379,11 @@ class ScreenshotManager {
1323
1379
  }
1324
1380
  }
1325
1381
  /**
1326
- * 使用 html2canvas 截图
1382
+ * 使用 modern-screenshot 截图(启用 Worker)
1327
1383
  */
1328
- async takeScreenshotWithHtml2Canvas(element) {
1384
+ async takeScreenshotWithModernScreenshot(element) {
1329
1385
  if (!this.options.silentMode) {
1330
- console.log('📸 使用 html2canvas 引擎截图...');
1386
+ console.log('📸 使用 modern-screenshot 引擎截图(Worker 模式)...');
1331
1387
  }
1332
1388
  const { width, height } = this.calculateCompressedSize(element.scrollWidth, element.scrollHeight, this.options.maxWidth, this.options.maxHeight);
1333
1389
  const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
@@ -1335,85 +1391,164 @@ class ScreenshotManager {
1335
1391
  const mobileQuality = isMobile || isLowEndDevice ? Math.max(this.options.quality * 0.7, 0.3) : this.options.quality;
1336
1392
  const mobileWidth = isMobile ? Math.min(width, 1280) : width;
1337
1393
  const mobileHeight = isMobile ? Math.min(height, 720) : height;
1338
- const canvas = await html2canvas(element, {
1339
- useCORS: this.options.enableCORS,
1340
- allowTaint: true,
1341
- scale: isMobile ? 0.8 : this.options.scale,
1342
- backgroundColor: '#ffffff',
1343
- logging: false,
1344
- width: mobileWidth,
1345
- height: mobileHeight,
1346
- scrollX: 0,
1347
- scrollY: 0,
1348
- removeContainer: false,
1349
- foreignObjectRendering: false,
1350
- onclone: (clonedDoc) => {
1351
- const clonedImages = clonedDoc.querySelectorAll('img');
1352
- clonedImages.forEach((img) => {
1353
- if (img.src && !img.src.startsWith('data:') && !img.src.startsWith('blob:')) {
1354
- try {
1355
- const imgUrl = new URL(img.src, window.location.href);
1356
- const currentOrigin = window.location.origin;
1357
- if (this.options.proxyUrl && img.src.includes(this.options.proxyUrl.split('/api/image-proxy')[0])) {
1358
- return;
1359
- }
1360
- if (imgUrl.origin !== currentOrigin) {
1361
- if (this.options.corsMode === 'canvas-proxy') {
1362
- const cachedDataUrl = this.imageProxyCache.get(img.src);
1363
- if (cachedDataUrl) {
1364
- img.src = cachedDataUrl;
1365
- }
1366
- else if (img.crossOrigin) {
1367
- img.removeAttribute('crossOrigin');
1368
- }
1369
- }
1370
- else if (!img.crossOrigin) {
1371
- img.crossOrigin = 'anonymous';
1372
- }
1373
- }
1374
- }
1375
- catch (e) {
1376
- // URL 解析失败,跳过
1377
- }
1378
- }
1379
- });
1380
- },
1381
- ignoreElements: (element) => {
1382
- const htmlElement = element;
1383
- return element.classList.contains('van-popup') ||
1384
- element.classList.contains('van-overlay') ||
1385
- element.classList.contains('van-toast') ||
1386
- element.classList.contains('van-dialog') ||
1387
- element.classList.contains('van-loading') ||
1388
- htmlElement.style.display === 'none' ||
1389
- htmlElement.style.visibility === 'hidden';
1390
- },
1391
- imageTimeout: 15000
1392
- });
1393
- let mimeType = 'image/jpeg';
1394
- let finalQuality = mobileQuality;
1395
- if (this.options.outputFormat === 'webp' && !isMobile) {
1394
+ // 处理跨域图片的函数
1395
+ const handleCrossOriginImage = async (url) => {
1396
+ // 如果是 data URL 或 blob URL,直接返回
1397
+ if (url.startsWith('data:') || url.startsWith('blob:')) {
1398
+ return url;
1399
+ }
1400
+ // 如果是同源图片,直接返回
1396
1401
  try {
1397
- const testCanvas = document.createElement('canvas');
1398
- testCanvas.width = 1;
1399
- testCanvas.height = 1;
1400
- const testDataUrl = testCanvas.toDataURL('image/webp');
1401
- if (testDataUrl.indexOf('webp') !== -1) {
1402
- mimeType = 'image/webp';
1402
+ const imgUrl = new URL(url, window.location.href);
1403
+ if (imgUrl.origin === window.location.origin) {
1404
+ return url;
1405
+ }
1406
+ }
1407
+ catch (e) {
1408
+ // URL 解析失败,继续处理
1409
+ }
1410
+ // 如果配置了代理服务器,使用代理处理跨域图片
1411
+ if (this.options.proxyUrl && this.options.proxyUrl.trim() !== '') {
1412
+ // 检查缓存
1413
+ if (this.imageProxyCache.has(url)) {
1414
+ return this.imageProxyCache.get(url);
1415
+ }
1416
+ try {
1417
+ // 构建代理请求参数
1418
+ const params = new URLSearchParams({
1419
+ url: url,
1420
+ maxWidth: String(this.options.maxWidth || 1600),
1421
+ maxHeight: String(this.options.maxHeight || 900),
1422
+ quality: String(Math.round((this.options.quality || 0.4) * 100)),
1423
+ format: this.options.outputFormat || 'webp'
1424
+ });
1425
+ let baseUrl = this.options.proxyUrl;
1426
+ baseUrl = baseUrl.replace(/[?&]$/, '');
1427
+ const proxyUrl = `${baseUrl}?${params.toString()}`;
1428
+ // 请求代理服务器
1429
+ const response = await fetch(proxyUrl, {
1430
+ method: 'GET',
1431
+ mode: 'cors',
1432
+ credentials: 'omit',
1433
+ headers: {
1434
+ 'Accept': 'image/*'
1435
+ },
1436
+ cache: 'no-cache'
1437
+ });
1438
+ if (!response.ok) {
1439
+ throw new Error(`代理请求失败: ${response.status}`);
1440
+ }
1441
+ const blob = await response.blob();
1442
+ const dataUrl = await this.blobToDataUrl(blob);
1443
+ // 缓存结果
1444
+ this.imageProxyCache.set(url, dataUrl);
1445
+ return dataUrl;
1446
+ }
1447
+ catch (error) {
1448
+ if (!this.options.silentMode) {
1449
+ console.warn(`📸 代理处理图片失败: ${url.substring(0, 100)}...`, error);
1450
+ }
1451
+ // 失败时返回原 URL
1452
+ return url;
1403
1453
  }
1404
1454
  }
1405
- catch {
1406
- mimeType = 'image/jpeg';
1455
+ // 如果没有配置代理,尝试使用 CORS
1456
+ if (this.options.enableCORS) {
1457
+ return url;
1458
+ }
1459
+ // 默认返回原 URL
1460
+ return url;
1461
+ };
1462
+ // 如果还没有创建 Worker 上下文,则创建
1463
+ if (!this.screenshotContext) {
1464
+ const workerNumber = isMobile || isLowEndDevice ? 1 : 2;
1465
+ // 构建 createContext 配置
1466
+ const contextOptions = {
1467
+ workerNumber,
1468
+ quality: mobileQuality,
1469
+ fetchFn: handleCrossOriginImage, // 使用代理服务器处理跨域图片
1470
+ fetch: {
1471
+ requestInit: {
1472
+ cache: 'no-cache',
1473
+ },
1474
+ bypassingCache: true,
1475
+ },
1476
+ };
1477
+ // 如果指定了尺寸,添加尺寸配置
1478
+ if (mobileWidth && mobileHeight) {
1479
+ contextOptions.width = mobileWidth;
1480
+ contextOptions.height = mobileHeight;
1481
+ }
1482
+ // 如果指定了缩放比例,添加缩放配置
1483
+ if (this.options.scale !== 1) {
1484
+ contextOptions.scale = isMobile ? 0.8 : this.options.scale;
1485
+ }
1486
+ // 尝试设置 workerUrl(如果可用)
1487
+ const resolvedWorkerUrl = await getWorkerUrl();
1488
+ if (resolvedWorkerUrl) {
1489
+ contextOptions.workerUrl = resolvedWorkerUrl;
1490
+ }
1491
+ // 创建 Worker 上下文
1492
+ this.screenshotContext = await createContext(element, contextOptions);
1493
+ }
1494
+ try {
1495
+ // 使用 Worker 上下文进行截图
1496
+ const dataUrl = await domToPng(this.screenshotContext);
1497
+ // 根据输出格式转换
1498
+ if (this.options.outputFormat !== 'png') {
1499
+ // modern-screenshot 默认输出 PNG,如果需要其他格式,需要转换
1500
+ const canvas = document.createElement('canvas');
1501
+ const ctx = canvas.getContext('2d');
1502
+ if (!ctx) {
1503
+ throw new Error('无法获取 canvas context');
1504
+ }
1505
+ const img = new Image();
1506
+ await new Promise((resolve, reject) => {
1507
+ img.onload = () => {
1508
+ canvas.width = img.width;
1509
+ canvas.height = img.height;
1510
+ ctx.drawImage(img, 0, 0);
1511
+ resolve();
1512
+ };
1513
+ img.onerror = reject;
1514
+ img.src = dataUrl;
1515
+ });
1516
+ let mimeType = 'image/jpeg';
1517
+ let finalQuality = mobileQuality;
1518
+ if (this.options.outputFormat === 'webp' && !isMobile) {
1519
+ try {
1520
+ const testCanvas = document.createElement('canvas');
1521
+ testCanvas.width = 1;
1522
+ testCanvas.height = 1;
1523
+ const testDataUrl = testCanvas.toDataURL('image/webp');
1524
+ if (testDataUrl.indexOf('webp') !== -1) {
1525
+ mimeType = 'image/webp';
1526
+ }
1527
+ }
1528
+ catch {
1529
+ mimeType = 'image/jpeg';
1530
+ }
1531
+ }
1532
+ const convertedDataUrl = mimeType === 'image/png'
1533
+ ? canvas.toDataURL(mimeType)
1534
+ : canvas.toDataURL(mimeType, finalQuality);
1535
+ return convertedDataUrl;
1407
1536
  }
1537
+ return dataUrl;
1408
1538
  }
1409
- else if (this.options.outputFormat === 'png') {
1410
- mimeType = 'image/png';
1411
- finalQuality = undefined;
1539
+ catch (error) {
1540
+ // 如果截图失败,清理上下文以便下次重新创建
1541
+ if (this.screenshotContext) {
1542
+ try {
1543
+ destroyContext(this.screenshotContext);
1544
+ }
1545
+ catch (e) {
1546
+ // 忽略清理错误
1547
+ }
1548
+ this.screenshotContext = null;
1549
+ }
1550
+ throw error;
1412
1551
  }
1413
- const dataUrl = mimeType === 'image/png'
1414
- ? canvas.toDataURL(mimeType)
1415
- : canvas.toDataURL(mimeType, finalQuality);
1416
- return dataUrl;
1417
1552
  }
1418
1553
  /**
1419
1554
  * 预处理网络图片
@@ -1526,7 +1661,7 @@ class ScreenshotManager {
1526
1661
  throw new Error(`代理请求失败: ${response.status} ${response.statusText}${errorText ? ` - ${errorText.substring(0, 200)}` : ''}`);
1527
1662
  }
1528
1663
  const blob = await response.blob();
1529
- // 将 blob 转换为 data URL(用于 html2canvas 兼容性)
1664
+ // 将 blob 转换为 data URL(用于 modern-screenshot 兼容性)
1530
1665
  const dataUrl = await this.blobToDataUrl(blob);
1531
1666
  if (!this.options.silentMode) {
1532
1667
  console.log(`📸 ✅ 代理模式成功(已转换为 data URL): ${imageUrl.substring(0, 100)}...`);
@@ -1656,7 +1791,9 @@ class ScreenshotManager {
1656
1791
  newWorker.onerror = (e) => {
1657
1792
  console.error('📸 WebWorker 错误:', e);
1658
1793
  };
1659
- URL.revokeObjectURL(workerUrl);
1794
+ // 注意:不要立即 revokeObjectURL,因为 Worker 需要这个 URL 保持有效
1795
+ // 在 destroy() 方法中清理 Worker 时再 revoke
1796
+ // URL.revokeObjectURL(workerUrl) // 已移除,在 destroy 时清理
1660
1797
  return newWorker;
1661
1798
  }
1662
1799
  catch (err) {
@@ -1787,6 +1924,16 @@ class ScreenshotManager {
1787
1924
  this.worker.terminate();
1788
1925
  this.worker = null;
1789
1926
  }
1927
+ // 清理 modern-screenshot Worker 上下文
1928
+ if (this.screenshotContext) {
1929
+ try {
1930
+ destroyContext(this.screenshotContext);
1931
+ }
1932
+ catch (e) {
1933
+ // 忽略清理错误
1934
+ }
1935
+ this.screenshotContext = null;
1936
+ }
1790
1937
  if (this.expirationTimer) {
1791
1938
  clearTimeout(this.expirationTimer);
1792
1939
  this.expirationTimer = null;
@@ -1796,6 +1943,8 @@ class ScreenshotManager {
1796
1943
  this.messageHandler = null;
1797
1944
  }
1798
1945
  this.removeGlobalErrorHandlers();
1946
+ // 清理图片代理缓存
1947
+ this.imageProxyCache.clear();
1799
1948
  }
1800
1949
  /**
1801
1950
  * 获取状态