customer-chat-sdk 1.0.35 → 1.0.37

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.
@@ -6896,28 +6896,62 @@ class ScreenshotManager {
6896
6896
  }
6897
6897
  }
6898
6898
  let dataUrl;
6899
- // 等待一小段时间,确保 DOM 更新完成(减少等待时间)
6900
- await new Promise(resolve => setTimeout(resolve, 50));
6901
6899
  // 根据选择的引擎进行截图
6902
- if (selectedEngine === 'snapdom') {
6903
- dataUrl = await this.takeScreenshotWithSnapdom(this.targetElement);
6904
- }
6905
- else if (selectedEngine === 'html2canvas') {
6900
+ if (selectedEngine === 'html2canvas') {
6901
+ // html2canvas 需要更长的等待时间确保样式加载
6902
+ // 额外等待样式和字体加载完成
6903
+ await Promise.all([
6904
+ this.waitForStylesAndFonts(),
6905
+ this.waitForFonts()
6906
+ ]);
6907
+ // 再等待一段时间,确保样式完全应用
6908
+ await new Promise(resolve => setTimeout(resolve, 200));
6906
6909
  dataUrl = await this.takeScreenshotWithHtml2Canvas(this.targetElement);
6907
6910
  }
6911
+ else if (selectedEngine === 'snapdom') {
6912
+ // 等待一小段时间,确保 DOM 更新完成
6913
+ await new Promise(resolve => setTimeout(resolve, 50));
6914
+ dataUrl = await this.takeScreenshotWithSnapdom(this.targetElement);
6915
+ }
6908
6916
  else {
6909
6917
  // 默认使用 modern-screenshot
6918
+ // 等待一小段时间,确保 DOM 更新完成
6919
+ await new Promise(resolve => setTimeout(resolve, 50));
6910
6920
  dataUrl = await this.takeScreenshotWithModernScreenshot(this.targetElement);
6911
6921
  }
6912
6922
  const timestamp = Date.now();
6913
6923
  // 更新状态
6914
6924
  this.screenshotCount++;
6915
6925
  this.lastScreenshotTime = timestamp;
6916
- // 管理历史记录
6926
+ // 管理历史记录(限制内存占用)
6927
+ // base64 字符串很大,需要严格控制历史记录数量
6917
6928
  if (this.screenshotHistory.length >= this.options.maxHistory) {
6918
- this.screenshotHistory.shift();
6929
+ // 删除最旧的截图,释放内存
6930
+ const removed = this.screenshotHistory.shift();
6931
+ // 强制 GC(如果可能)
6932
+ if (removed && removed.length > 1000000) { // 大于1MB的字符串
6933
+ if (!this.options.silentMode) {
6934
+ console.log(`📸 清理旧截图,释放内存: ${Math.round(removed.length * 0.75 / 1024)} KB`);
6935
+ }
6936
+ }
6919
6937
  }
6920
6938
  this.screenshotHistory.push(dataUrl);
6939
+ // 如果历史记录总大小超过限制,清理最旧的
6940
+ let totalSize = this.screenshotHistory.reduce((sum, item) => sum + item.length, 0);
6941
+ const maxTotalSize = 50 * 1024 * 1024; // 最大50MB
6942
+ while (this.screenshotHistory.length > 0 && totalSize > maxTotalSize) {
6943
+ const removed = this.screenshotHistory.shift();
6944
+ if (removed) {
6945
+ const removedSize = removed.length;
6946
+ totalSize -= removedSize;
6947
+ if (!this.options.silentMode) {
6948
+ console.warn(`📸 ⚠️ 历史记录总大小超过限制,清理最旧截图: ${Math.round(removedSize * 0.75 / 1024)} KB`);
6949
+ }
6950
+ }
6951
+ else {
6952
+ break;
6953
+ }
6954
+ }
6921
6955
  // 打印基本信息
6922
6956
  const base64Data = dataUrl.split(',')[1];
6923
6957
  if (!this.options.silentMode) {
@@ -7089,6 +7123,19 @@ class ScreenshotManager {
7089
7123
  console.log('📸 使用 html2canvas 引擎截图...');
7090
7124
  }
7091
7125
  try {
7126
+ // html2canvas 需要确保样式完全加载,额外等待
7127
+ // 等待所有样式表加载完成
7128
+ await this.waitForAllStylesLoaded();
7129
+ // 等待字体加载完成
7130
+ await this.waitForFonts();
7131
+ // 等待 DOM 完全渲染
7132
+ await new Promise(resolve => {
7133
+ requestAnimationFrame(() => {
7134
+ requestAnimationFrame(() => {
7135
+ setTimeout(() => resolve(), 100);
7136
+ });
7137
+ });
7138
+ });
7092
7139
  // 检查元素是否存在和可见
7093
7140
  const rect = element.getBoundingClientRect();
7094
7141
  if (rect.width === 0 || rect.height === 0) {
@@ -7120,6 +7167,37 @@ class ScreenshotManager {
7120
7167
  logging: !this.options.silentMode,
7121
7168
  width: finalWidth,
7122
7169
  height: finalHeight,
7170
+ // 关键配置:确保样式正确渲染
7171
+ // 注意:foreignObjectRendering 可能导致样式问题,改为 true 或移除此选项
7172
+ // foreignObjectRendering: false, // 移除或设为 true,让 html2canvas 自动选择最佳渲染方式
7173
+ onclone: (clonedDoc, _clonedElement) => {
7174
+ // 在克隆的文档中,确保所有样式都正确应用
7175
+ // html2canvas 会自动处理样式,但我们可以确保样式表被正确加载
7176
+ // 确保克隆文档的样式表链接正确
7177
+ const originalLinks = document.querySelectorAll('link[rel="stylesheet"]');
7178
+ const clonedLinks = clonedDoc.querySelectorAll('link[rel="stylesheet"]');
7179
+ // 如果克隆文档缺少样式表链接,从原始文档复制
7180
+ if (clonedLinks.length < originalLinks.length) {
7181
+ originalLinks.forEach((link) => {
7182
+ const href = link.getAttribute('href');
7183
+ if (href && !clonedDoc.querySelector(`link[href="${href}"]`)) {
7184
+ const newLink = clonedDoc.createElement('link');
7185
+ newLink.rel = 'stylesheet';
7186
+ newLink.href = href;
7187
+ clonedDoc.head.appendChild(newLink);
7188
+ }
7189
+ });
7190
+ }
7191
+ // 确保内联样式被正确复制
7192
+ const originalStyle = document.querySelector('style');
7193
+ if (originalStyle && originalStyle.textContent) {
7194
+ const clonedStyle = clonedDoc.createElement('style');
7195
+ clonedStyle.textContent = originalStyle.textContent;
7196
+ if (!clonedDoc.head.querySelector('style')) {
7197
+ clonedDoc.head.appendChild(clonedStyle);
7198
+ }
7199
+ }
7200
+ },
7123
7201
  // 性能优化
7124
7202
  removeContainer: true, // 截图后移除临时容器
7125
7203
  imageTimeout: this.options.imageLoadTimeout || 5000,
@@ -7136,7 +7214,13 @@ class ScreenshotManager {
7136
7214
  // html2canvas 不支持直接的 proxy 选项,需要通过 onclone 钩子处理图片
7137
7215
  // 如果配置了代理服务器,在克隆时替换图片 URL
7138
7216
  if (this.options.useProxy && this.options.proxyUrl && this.options.proxyUrl.trim() !== '') {
7139
- options.onclone = (clonedDoc) => {
7217
+ // 保存原始的 onclone
7218
+ const originalOnclone = options.onclone;
7219
+ options.onclone = (clonedDoc, clonedElement) => {
7220
+ // 先执行原始的样式处理逻辑
7221
+ if (originalOnclone) {
7222
+ originalOnclone(clonedDoc, clonedElement);
7223
+ }
7140
7224
  // 在克隆的文档中,替换所有跨域图片的 src
7141
7225
  const images = clonedDoc.querySelectorAll('img');
7142
7226
  images.forEach((img) => {
@@ -7526,37 +7610,49 @@ class ScreenshotManager {
7526
7610
  throw new Error('无法获取 canvas context');
7527
7611
  }
7528
7612
  const img = new Image();
7529
- await new Promise((resolve, reject) => {
7530
- img.onload = () => {
7531
- canvas.width = img.width;
7532
- canvas.height = img.height;
7533
- ctx.drawImage(img, 0, 0);
7534
- resolve();
7535
- };
7536
- img.onerror = reject;
7537
- img.src = dataUrl;
7538
- });
7539
- let mimeType = 'image/jpeg';
7540
- // 使用与 createContext 相同的质量设置
7541
- let conversionQuality = finalQuality;
7542
- if (this.options.outputFormat === 'webp' && !isMobile) {
7543
- try {
7544
- const testCanvas = document.createElement('canvas');
7545
- testCanvas.width = 1;
7546
- testCanvas.height = 1;
7547
- const testDataUrl = testCanvas.toDataURL('image/webp');
7548
- if (testDataUrl.indexOf('webp') !== -1) {
7549
- mimeType = 'image/webp';
7613
+ let convertedDataUrl;
7614
+ try {
7615
+ await new Promise((resolve, reject) => {
7616
+ img.onload = () => {
7617
+ canvas.width = img.width;
7618
+ canvas.height = img.height;
7619
+ ctx.drawImage(img, 0, 0);
7620
+ resolve();
7621
+ };
7622
+ img.onerror = reject;
7623
+ img.src = dataUrl;
7624
+ });
7625
+ let mimeType = 'image/jpeg';
7626
+ // 使用与 createContext 相同的质量设置
7627
+ let conversionQuality = finalQuality;
7628
+ if (this.options.outputFormat === 'webp' && !isMobile) {
7629
+ try {
7630
+ const testCanvas = document.createElement('canvas');
7631
+ testCanvas.width = 1;
7632
+ testCanvas.height = 1;
7633
+ const testDataUrl = testCanvas.toDataURL('image/webp');
7634
+ if (testDataUrl.indexOf('webp') !== -1) {
7635
+ mimeType = 'image/webp';
7636
+ }
7637
+ }
7638
+ catch {
7639
+ mimeType = 'image/jpeg';
7550
7640
  }
7551
7641
  }
7552
- catch {
7553
- mimeType = 'image/jpeg';
7554
- }
7642
+ // 使用优化后的质量进行格式转换
7643
+ convertedDataUrl = mimeType === 'image/png'
7644
+ ? canvas.toDataURL(mimeType)
7645
+ : canvas.toDataURL(mimeType, conversionQuality);
7646
+ }
7647
+ finally {
7648
+ // 清理资源,释放内存
7649
+ img.src = ''; // 清除图片引用
7650
+ img.onload = null;
7651
+ img.onerror = null;
7652
+ canvas.width = 0; // 清空 canvas
7653
+ canvas.height = 0;
7654
+ ctx.clearRect(0, 0, 0, 0); // 清除绘制内容
7555
7655
  }
7556
- // 使用优化后的质量进行格式转换
7557
- const convertedDataUrl = mimeType === 'image/png'
7558
- ? canvas.toDataURL(mimeType)
7559
- : canvas.toDataURL(mimeType, conversionQuality);
7560
7656
  return convertedDataUrl;
7561
7657
  }
7562
7658
  return dataUrl;
@@ -7577,15 +7673,34 @@ class ScreenshotManager {
7577
7673
  throw error;
7578
7674
  }
7579
7675
  finally {
7580
- // 每次截图后清理 context,下次重新创建(确保元素状态最新)
7676
+ // 每次截图后立即清理 context,释放 Worker 和内存
7677
+ // 这是防止内存泄漏的关键步骤
7581
7678
  if (this.screenshotContext) {
7582
7679
  try {
7583
7680
  destroyContext(this.screenshotContext);
7681
+ if (!this.options.silentMode) {
7682
+ console.log('📸 ✅ modern-screenshot context 已清理');
7683
+ }
7584
7684
  }
7585
7685
  catch (e) {
7586
- // 忽略清理错误
7686
+ if (!this.options.silentMode) {
7687
+ console.warn('📸 ⚠️ 清理 context 失败:', e);
7688
+ }
7689
+ }
7690
+ finally {
7691
+ // 确保 context 引用被清除
7692
+ this.screenshotContext = null;
7693
+ }
7694
+ }
7695
+ // 强制触发垃圾回收(如果可能)
7696
+ // 注意:这需要浏览器支持,不是所有浏览器都有效
7697
+ if (typeof window !== 'undefined' && window.gc && typeof window.gc === 'function') {
7698
+ try {
7699
+ window.gc();
7700
+ }
7701
+ catch {
7702
+ // 忽略 GC 错误
7587
7703
  }
7588
- this.screenshotContext = null;
7589
7704
  }
7590
7705
  }
7591
7706
  }
@@ -8160,15 +8275,42 @@ class ScreenshotManager {
8160
8275
  }
8161
8276
  }
8162
8277
  /**
8163
- * 等待 CSS 和字体加载完成(优化:减少等待时间,使用 requestAnimationFrame)
8278
+ * 等待 CSS 和字体加载完成
8279
+ * html2canvas 需要确保所有样式表都加载完成才能正确截图
8164
8280
  */
8165
8281
  async waitForStylesAndFonts() {
8166
- // 使用 requestAnimationFrame 优化渲染时机
8167
- return new Promise((resolve) => {
8168
- requestAnimationFrame(() => {
8169
- setTimeout(() => {
8282
+ // 等待所有样式表加载完成
8283
+ const styleSheets = Array.from(document.styleSheets);
8284
+ const styleSheetPromises = styleSheets.map((sheet) => {
8285
+ return new Promise((resolve) => {
8286
+ try {
8287
+ // 检查样式表是否已加载
8288
+ // 尝试访问 cssRules,如果成功说明样式表已加载
8289
+ const rules = sheet.cssRules;
8290
+ if (rules) {
8291
+ resolve();
8292
+ }
8293
+ else {
8294
+ // 如果样式表还在加载,等待一下
8295
+ setTimeout(() => resolve(), 100);
8296
+ }
8297
+ }
8298
+ catch (e) {
8299
+ // 跨域样式表可能无法访问,忽略错误
8170
8300
  resolve();
8171
- }, 30); // 减少到30ms
8301
+ }
8302
+ });
8303
+ });
8304
+ await Promise.all(styleSheetPromises);
8305
+ // 使用 requestAnimationFrame 确保 DOM 已渲染
8306
+ await new Promise((resolve) => {
8307
+ requestAnimationFrame(() => {
8308
+ requestAnimationFrame(() => {
8309
+ // 额外等待,确保样式应用完成
8310
+ setTimeout(() => {
8311
+ resolve();
8312
+ }, 100); // 增加到100ms,确保样式加载
8313
+ });
8172
8314
  });
8173
8315
  });
8174
8316
  }
@@ -8187,6 +8329,75 @@ class ScreenshotManager {
8187
8329
  // 忽略错误
8188
8330
  }
8189
8331
  }
8332
+ /**
8333
+ * 等待所有样式表加载完成(html2canvas 专用)
8334
+ * 增强版:确保所有样式表都完全加载
8335
+ */
8336
+ async waitForAllStylesLoaded() {
8337
+ const styleSheets = Array.from(document.styleSheets);
8338
+ const promises = [];
8339
+ styleSheets.forEach((sheet) => {
8340
+ promises.push(new Promise((resolve) => {
8341
+ try {
8342
+ // 尝试访问样式表规则,如果成功说明样式表已加载
8343
+ const rules = sheet.cssRules;
8344
+ if (rules && rules.length > 0) {
8345
+ // 样式表已加载且有规则
8346
+ resolve();
8347
+ }
8348
+ else if (rules) {
8349
+ // 样式表已加载但可能没有规则(空样式表)
8350
+ resolve();
8351
+ }
8352
+ else {
8353
+ // 等待样式表加载
8354
+ let checkCount = 0;
8355
+ const maxChecks = 100; // 最多检查100次(5秒)
8356
+ const checkInterval = setInterval(() => {
8357
+ checkCount++;
8358
+ try {
8359
+ const currentRules = sheet.cssRules;
8360
+ if (currentRules) {
8361
+ clearInterval(checkInterval);
8362
+ resolve();
8363
+ }
8364
+ else if (checkCount >= maxChecks) {
8365
+ // 超时,可能样式表无法访问
8366
+ clearInterval(checkInterval);
8367
+ resolve();
8368
+ }
8369
+ }
8370
+ catch {
8371
+ // 跨域样式表无法访问,直接 resolve
8372
+ clearInterval(checkInterval);
8373
+ resolve();
8374
+ }
8375
+ }, 50);
8376
+ // 超时保护(5秒)
8377
+ setTimeout(() => {
8378
+ clearInterval(checkInterval);
8379
+ resolve();
8380
+ }, 5000);
8381
+ }
8382
+ }
8383
+ catch (e) {
8384
+ // 跨域样式表无法访问,直接 resolve
8385
+ resolve();
8386
+ }
8387
+ }));
8388
+ });
8389
+ await Promise.all(promises);
8390
+ // 额外等待,确保样式应用(增加到300ms,确保样式完全应用)
8391
+ await new Promise(resolve => setTimeout(resolve, 300));
8392
+ // 再次使用 requestAnimationFrame 确保渲染完成
8393
+ await new Promise(resolve => {
8394
+ requestAnimationFrame(() => {
8395
+ requestAnimationFrame(() => {
8396
+ setTimeout(() => resolve(), 100);
8397
+ });
8398
+ });
8399
+ });
8400
+ }
8190
8401
  /**
8191
8402
  * 计算压缩后的尺寸
8192
8403
  */
@@ -8403,6 +8614,11 @@ class ScreenshotManager {
8403
8614
  }
8404
8615
  // 清理图片代理缓存
8405
8616
  this.imageProxyCache.clear();
8617
+ // 清理截图历史记录(释放大量内存)
8618
+ this.screenshotHistory.length = 0;
8619
+ // 清理图片下载队列
8620
+ this.imageDownloadQueue.clear();
8621
+ this.activeDownloads.clear();
8406
8622
  }
8407
8623
  /**
8408
8624
  * 获取缓存的图片(检查是否过期)
@@ -8424,8 +8640,34 @@ class ScreenshotManager {
8424
8640
  }
8425
8641
  /**
8426
8642
  * 设置缓存的图片(带时间戳)
8643
+ * 添加内存大小限制,防止缓存无限增长
8427
8644
  */
8428
8645
  setCachedImage(url, dataUrl) {
8646
+ // 估算当前缓存总大小(MB)
8647
+ let totalSizeMB = 0;
8648
+ this.imageProxyCache.forEach((cached) => {
8649
+ totalSizeMB += cached.dataUrl.length * 0.75 / (1024 * 1024); // base64 转字节再转MB
8650
+ });
8651
+ // 添加新项的大小
8652
+ const newItemSizeMB = dataUrl.length * 0.75 / (1024 * 1024);
8653
+ const maxCacheSizeMB = 100; // 最大100MB内存缓存
8654
+ // 如果超过限制,清理最旧的缓存
8655
+ if (totalSizeMB + newItemSizeMB > maxCacheSizeMB) {
8656
+ const sortedEntries = Array.from(this.imageProxyCache.entries())
8657
+ .sort((a, b) => a[1].timestamp - b[1].timestamp); // 按时间排序
8658
+ let currentSizeMB = totalSizeMB;
8659
+ for (const [key, value] of sortedEntries) {
8660
+ if (currentSizeMB + newItemSizeMB <= maxCacheSizeMB) {
8661
+ break;
8662
+ }
8663
+ const itemSizeMB = value.dataUrl.length * 0.75 / (1024 * 1024);
8664
+ this.imageProxyCache.delete(key);
8665
+ currentSizeMB -= itemSizeMB;
8666
+ if (!this.options.silentMode) {
8667
+ console.log(`📸 清理内存缓存(超过限制): ${key.substring(0, 50)}...`);
8668
+ }
8669
+ }
8670
+ }
8429
8671
  this.imageProxyCache.set(url, {
8430
8672
  dataUrl,
8431
8673
  timestamp: Date.now()
@@ -8452,9 +8694,10 @@ class ScreenshotManager {
8452
8694
  }
8453
8695
  /**
8454
8696
  * 定期清理过期缓存(可选,在截图时也会自动清理)
8697
+ * 增加清理频率,防止内存积累
8455
8698
  */
8456
8699
  startCacheCleanup() {
8457
- // 每5分钟清理一次过期缓存
8700
+ // 每2分钟清理一次过期缓存(从5分钟改为2分钟,更频繁)
8458
8701
  setInterval(() => {
8459
8702
  this.cleanExpiredCache();
8460
8703
  // 如果启用 IndexedDB,也清理 IndexedDB 缓存
@@ -8465,9 +8708,12 @@ class ScreenshotManager {
8465
8708
  }
8466
8709
  if (!this.options.silentMode) {
8467
8710
  const memoryCacheSize = this.imageProxyCache.size;
8468
- console.log(`📸 清理过期缓存,内存缓存数量: ${memoryCacheSize}`);
8711
+ const memoryCacheSizeMB = Array.from(this.imageProxyCache.values())
8712
+ .reduce((sum, cached) => sum + cached.dataUrl.length * 0.75 / (1024 * 1024), 0);
8713
+ const historySizeMB = this.screenshotHistory.reduce((sum, item) => sum + item.length * 0.75 / (1024 * 1024), 0);
8714
+ console.log(`📸 清理过期缓存,内存缓存: ${memoryCacheSize} 项,${memoryCacheSizeMB.toFixed(2)} MB,历史记录: ${historySizeMB.toFixed(2)} MB`);
8469
8715
  }
8470
- }, 300000); // 5分钟
8716
+ }, 120000); // 2分钟(从5分钟改为2分钟,更频繁清理)
8471
8717
  }
8472
8718
  /**
8473
8719
  * 获取状态