customer-chat-sdk 1.0.40 → 1.0.41

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.
@@ -14319,8 +14319,13 @@ class ScreenshotManager {
14319
14319
  this.screenshotTimer = null;
14320
14320
  // modern-screenshot Worker 上下文(用于复用,避免频繁创建和销毁)
14321
14321
  this.screenshotContext = null;
14322
+ this.contextElement = null; // 当前 context 对应的元素
14323
+ this.contextOptionsHash = ''; // context 配置的哈希值,用于判断是否需要重新创建
14322
14324
  // 截图锁,防止并发截图
14323
14325
  this.isScreenshotInProgress = false;
14326
+ // 截图队列(用于处理频繁的截图请求)
14327
+ this.screenshotQueue = [];
14328
+ this.isProcessingQueue = false;
14324
14329
  // PostMessage 监听器
14325
14330
  this.messageHandler = null;
14326
14331
  // 动态轮询间隔(由 iframe 消息控制)
@@ -14373,7 +14378,8 @@ class ScreenshotManager {
14373
14378
  maxCacheSize: options.maxCacheSize ?? 50, // 默认最大50MB
14374
14379
  maxCacheAge: options.maxCacheAge ?? 86400000, // 默认24小时(86400000ms)
14375
14380
  maxImageSize: options.maxImageSize ?? 5, // 不使用代理时,单个图片最大尺寸(MB),默认5MB
14376
- skipLargeImages: options.skipLargeImages ?? true // 不使用代理时,是否跳过过大的图片,默认true(跳过)
14381
+ skipLargeImages: options.skipLargeImages ?? true, // 不使用代理时,是否跳过过大的图片,默认true(跳过)
14382
+ workerNumber: options.workerNumber ?? undefined // modern-screenshot Worker 数量,默认自动计算(undefined 表示自动)
14377
14383
  };
14378
14384
  this.setupMessageListener();
14379
14385
  this.setupVisibilityChangeListener();
@@ -14401,15 +14407,22 @@ class ScreenshotManager {
14401
14407
  * 设置目标元素
14402
14408
  */
14403
14409
  setTargetElement(element) {
14404
- // 如果元素改变了,清理旧的 Worker 上下文
14405
- if (this.targetElement !== element && this.screenshotContext) {
14406
- try {
14407
- destroyContext(this.screenshotContext);
14408
- }
14409
- catch (e) {
14410
- // 忽略清理错误
14410
+ // 如果元素变化,需要清理 context(下次截图时会重新创建)
14411
+ if (this.targetElement !== element) {
14412
+ if (this.screenshotContext) {
14413
+ try {
14414
+ destroyContext(this.screenshotContext);
14415
+ if (!this.options.silentMode) {
14416
+ console.log('📸 目标元素变化,清理 context');
14417
+ }
14418
+ }
14419
+ catch (e) {
14420
+ // 忽略清理错误
14421
+ }
14422
+ this.screenshotContext = null;
14423
+ this.contextElement = null;
14424
+ this.contextOptionsHash = '';
14411
14425
  }
14412
- this.screenshotContext = null;
14413
14426
  }
14414
14427
  this.targetElement = element;
14415
14428
  }
@@ -14637,22 +14650,36 @@ class ScreenshotManager {
14637
14650
  if (!this.worker && this.options.compress) {
14638
14651
  this.worker = this.createWorker();
14639
14652
  }
14640
- // 设置定时器
14641
- this.screenshotTimer = setInterval(async () => {
14653
+ // 设置定时器(使用递归 setTimeout,确保等待前一个完成)
14654
+ // 这样可以避免 setInterval 不等待异步完成的问题
14655
+ const scheduleNext = async () => {
14642
14656
  if (this.isRunning && this.isEnabled && !document.hidden) {
14643
- await this.takeScreenshot();
14644
- // 如果配置了上传,且当前有上传配置,自动上传
14645
- if (this.currentUploadConfig) {
14646
- const latestScreenshot = this.getLatestScreenshot();
14647
- if (latestScreenshot && !this.isUploading) {
14648
- this.uploadScreenshot(latestScreenshot, this.currentUploadConfig)
14649
- .catch((error) => {
14650
- console.error('📸 [轮询] 自动上传失败:', error);
14651
- });
14657
+ try {
14658
+ await this.takeScreenshot();
14659
+ // 如果配置了上传,且当前有上传配置,自动上传
14660
+ if (this.currentUploadConfig) {
14661
+ const latestScreenshot = this.getLatestScreenshot();
14662
+ if (latestScreenshot && !this.isUploading) {
14663
+ this.uploadScreenshot(latestScreenshot, this.currentUploadConfig)
14664
+ .catch((error) => {
14665
+ console.error('📸 [轮询] 自动上传失败:', error);
14666
+ });
14667
+ }
14668
+ }
14669
+ }
14670
+ catch (error) {
14671
+ if (!this.options.silentMode) {
14672
+ console.error('📸 [轮询] 截图失败:', error);
14652
14673
  }
14653
14674
  }
14654
14675
  }
14655
- }, currentInterval);
14676
+ // 如果还在运行,安排下一次截图
14677
+ if (this.isRunning) {
14678
+ this.screenshotTimer = setTimeout(scheduleNext, currentInterval);
14679
+ }
14680
+ };
14681
+ // 立即开始第一次
14682
+ scheduleNext();
14656
14683
  // 注意:不再立即执行一次,因为已经在 takeScreenshotAndUpload 中执行了
14657
14684
  }
14658
14685
  /**
@@ -15272,8 +15299,37 @@ class ScreenshotManager {
15272
15299
  */
15273
15300
  async takeScreenshotWithModernScreenshot(element) {
15274
15301
  // 检查是否有截图正在进行(防止并发冲突)
15302
+ // 如果正在进行,将请求加入队列,而不是直接拒绝
15275
15303
  if (this.isScreenshotInProgress) {
15276
- throw new Error('截图正在进行中,请稍后再试');
15304
+ // 队列最多保留 1 个请求,避免积压
15305
+ if (this.screenshotQueue.length >= 1) {
15306
+ if (!this.options.silentMode) {
15307
+ console.log('📸 截图队列已满,跳过当前请求(等待队列处理)');
15308
+ }
15309
+ // 等待队列中的请求完成
15310
+ return new Promise((resolve, reject) => {
15311
+ const checkQueue = () => {
15312
+ if (!this.isScreenshotInProgress && this.screenshotQueue.length === 0) {
15313
+ // 队列已清空,重新尝试
15314
+ this.takeScreenshotWithModernScreenshot(element).then(resolve).catch(reject);
15315
+ }
15316
+ else {
15317
+ setTimeout(checkQueue, 100); // 100ms 后再次检查
15318
+ }
15319
+ };
15320
+ checkQueue();
15321
+ });
15322
+ }
15323
+ // 将请求加入队列
15324
+ return new Promise((resolve, reject) => {
15325
+ this.screenshotQueue.push({ resolve: () => {
15326
+ this.takeScreenshotWithModernScreenshot(element).then(resolve).catch(reject);
15327
+ }, reject });
15328
+ // 启动队列处理(如果还没启动)
15329
+ if (!this.isProcessingQueue) {
15330
+ this.processScreenshotQueue();
15331
+ }
15332
+ });
15277
15333
  }
15278
15334
  this.isScreenshotInProgress = true;
15279
15335
  if (!this.options.silentMode) {
@@ -15485,20 +15541,38 @@ class ScreenshotManager {
15485
15541
  if (rect.width === 0 || rect.height === 0) {
15486
15542
  throw new Error('元素尺寸为 0,无法截图');
15487
15543
  }
15488
- // 每次截图都重新创建 context,确保使用最新的元素状态
15489
- // 如果已有 context,先清理
15490
- if (this.screenshotContext) {
15491
- try {
15492
- destroyContext(this.screenshotContext);
15544
+ // Worker 数量配置:智能计算或使用用户配置
15545
+ // workerNumber > 0 会启用 Worker 模式,截图处理在后台线程执行,不会阻塞主线程 UI
15546
+ // 如果用户指定了 workerNumber,直接使用;否则根据设备性能自动计算
15547
+ let workerNumber;
15548
+ if (this.options.workerNumber !== undefined && this.options.workerNumber > 0) {
15549
+ // 用户明确指定了 workerNumber
15550
+ workerNumber = this.options.workerNumber;
15551
+ }
15552
+ else {
15553
+ // 自动计算 workerNumber
15554
+ const cpuCores = navigator.hardwareConcurrency || 4; // 默认假设 4 核
15555
+ if (isMobile || isLowEndDevice) {
15556
+ // 移动设备/低端设备:使用 1 个 Worker(避免内存压力)
15557
+ workerNumber = 1;
15493
15558
  }
15494
- catch (e) {
15495
- // 忽略清理错误
15559
+ else if (cpuCores >= 8) {
15560
+ // 高性能设备(8核及以上):使用 3-4 个 Worker(充分利用多核)
15561
+ // 但根据截图间隔调整:频繁截图(间隔 < 2秒)时使用更多 Worker
15562
+ const isFrequentScreenshot = this.options.interval < 2000;
15563
+ workerNumber = isFrequentScreenshot ? Math.min(4, Math.floor(cpuCores / 2)) : 3;
15564
+ }
15565
+ else if (cpuCores >= 4) {
15566
+ // 中等性能设备(4-7核):使用 2 个 Worker
15567
+ workerNumber = 2;
15568
+ }
15569
+ else {
15570
+ // 低性能设备(< 4核):使用 1 个 Worker
15571
+ workerNumber = 1;
15496
15572
  }
15497
- this.screenshotContext = null;
15498
15573
  }
15499
- // Worker 数量配置:移动设备/低端设备使用 1 Worker,桌面设备使用 2 个
15500
- // workerNumber > 0 会启用 Worker 模式,截图处理在后台线程执行,不会阻塞主线程 UI
15501
- const workerNumber = isMobile || isLowEndDevice ? 1 : 2;
15574
+ // 限制 workerNumber 范围:1-8(避免过多 Worker 导致资源竞争)
15575
+ workerNumber = Math.max(1, Math.min(8, workerNumber));
15502
15576
  // 构建 createContext 配置
15503
15577
  // 参考: https://github.com/qq15725/modern-screenshot/blob/main/src/options.ts
15504
15578
  const contextOptions = {
@@ -15515,7 +15589,28 @@ class ScreenshotManager {
15515
15589
  // 参考: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
15516
15590
  // 大多数浏览器限制为 16,777,216 像素(4096x4096),这里设置为更保守的值
15517
15591
  maximumCanvasSize: 16777216, // 16M 像素(约 4096x4096)
15592
+ // 使用 modern-screenshot 内置的 timeout(更可靠)
15593
+ timeout: Math.max(this.options.interval * 6, 5000),
15518
15594
  };
15595
+ // 限制 timeout 最多 15 秒
15596
+ contextOptions.timeout = Math.min(contextOptions.timeout, 15000);
15597
+ // 如果用户指定了 workerUrl,使用指定的 URL
15598
+ // 否则让 modern-screenshot 自动处理(它会尝试从 node_modules 或 CDN 加载)
15599
+ // 注意:在某些构建工具(如 Rollup)中,可能需要手动指定 workerUrl
15600
+ if (this.options.workerUrl) {
15601
+ contextOptions.workerUrl = this.options.workerUrl;
15602
+ if (!this.options.silentMode) {
15603
+ console.log(`📸 使用指定的 Worker URL: ${this.options.workerUrl}`);
15604
+ }
15605
+ }
15606
+ else {
15607
+ // 未指定 workerUrl 时,modern-screenshot 会自动处理
15608
+ // 但在某些构建环境中可能需要手动指定,可以使用 CDN 作为后备
15609
+ // 这里不设置 workerUrl,让 modern-screenshot 自己处理
15610
+ if (!this.options.silentMode) {
15611
+ console.log('📸 Worker URL 未指定,modern-screenshot 将自动处理');
15612
+ }
15613
+ }
15519
15614
  // 对所有元素都设置尺寸限制(包括 document.body),避免截图过大
15520
15615
  // 这样可以减少 base64 大小,提高性能
15521
15616
  if (finalWidth && finalHeight) {
@@ -15544,69 +15639,99 @@ class ScreenshotManager {
15544
15639
  // 如果未指定 scale,移动设备默认使用 0.7
15545
15640
  contextOptions.scale = 0.7;
15546
15641
  }
15547
- // modern-screenshot 会自动处理 worker URL,不需要手动设置 workerUrl
15548
- // workerNumber > 0 时,截图处理会在 Worker 线程中执行,不会阻塞主线程 UI
15549
- // 创建 Worker 上下文(每次截图都创建新的,确保元素状态最新)
15550
- if (!this.options.silentMode) {
15551
- console.log(`📸 Worker 模式: ${workerNumber} 个 Worker,质量: ${finalQuality.toFixed(2)},缩放: ${contextOptions.scale || 1}`);
15552
- }
15553
- // 添加重试机制
15554
- let retries = 0;
15555
- const maxRetries = this.options.maxRetries || 2;
15556
- let screenshotContext = null;
15557
- while (retries <= maxRetries) {
15558
- try {
15559
- screenshotContext = await createContext$1(element, contextOptions);
15560
- this.screenshotContext = screenshotContext;
15561
- break;
15642
+ // 优化:复用 context,避免频繁创建和销毁(性能提升 20%+)
15643
+ // 只在元素变化或配置变化时重新创建 context
15644
+ const contextOptionsHash = JSON.stringify({
15645
+ workerNumber,
15646
+ quality: finalQuality,
15647
+ scale: contextOptions.scale,
15648
+ width: contextOptions.width,
15649
+ height: contextOptions.height,
15650
+ maximumCanvasSize: contextOptions.maximumCanvasSize,
15651
+ timeout: contextOptions.timeout
15652
+ });
15653
+ const needsRecreateContext = !this.screenshotContext ||
15654
+ this.contextElement !== element ||
15655
+ this.contextOptionsHash !== contextOptionsHash;
15656
+ if (needsRecreateContext) {
15657
+ if (!this.options.silentMode) {
15658
+ if (this.screenshotContext) {
15659
+ console.log('📸 检测到元素或配置变化,重新创建 context...');
15660
+ }
15661
+ else {
15662
+ console.log(`📸 Worker 模式: ${workerNumber} 个 Worker,质量: ${finalQuality.toFixed(2)},缩放: ${contextOptions.scale || 1}`);
15663
+ }
15562
15664
  }
15563
- catch (error) {
15564
- if (retries === maxRetries) {
15565
- throw new Error(`创建截图上下文失败(已重试 ${maxRetries} 次): ${error instanceof Error ? error.message : String(error)}`);
15665
+ // 销毁旧 context
15666
+ if (this.screenshotContext) {
15667
+ try {
15668
+ destroyContext(this.screenshotContext);
15566
15669
  }
15567
- retries++;
15568
- const delay = 1000 * retries; // 递增延迟:1秒、2秒...
15569
- if (!this.options.silentMode) {
15570
- console.warn(`📸 ⚠️ 创建截图上下文失败,${delay}ms 后重试 (${retries}/${maxRetries})...`);
15670
+ catch (e) {
15671
+ // 忽略清理错误
15672
+ }
15673
+ this.screenshotContext = null;
15674
+ }
15675
+ // 添加 progress 回调(可选,用于显示进度)
15676
+ if (!this.options.silentMode) {
15677
+ contextOptions.progress = (current, total) => {
15678
+ if (total > 0) {
15679
+ const percent = Math.round((current / total) * 100);
15680
+ if (percent % 25 === 0 || current === total) { // 每 25% 或完成时打印
15681
+ console.log(`📸 截图进度: ${current}/${total} (${percent}%)`);
15682
+ }
15683
+ }
15684
+ };
15685
+ }
15686
+ // 添加重试机制创建新 context
15687
+ let retries = 0;
15688
+ const maxRetries = this.options.maxRetries || 2;
15689
+ while (retries <= maxRetries) {
15690
+ try {
15691
+ this.screenshotContext = await createContext$1(element, contextOptions);
15692
+ this.contextElement = element;
15693
+ this.contextOptionsHash = contextOptionsHash;
15694
+ break;
15695
+ }
15696
+ catch (error) {
15697
+ if (retries === maxRetries) {
15698
+ throw new Error(`创建截图上下文失败(已重试 ${maxRetries} 次): ${error instanceof Error ? error.message : String(error)}`);
15699
+ }
15700
+ retries++;
15701
+ const delay = 1000 * retries; // 递增延迟:1秒、2秒...
15702
+ if (!this.options.silentMode) {
15703
+ console.warn(`📸 ⚠️ 创建截图上下文失败,${delay}ms 后重试 (${retries}/${maxRetries})...`);
15704
+ }
15705
+ await new Promise(resolve => setTimeout(resolve, delay));
15571
15706
  }
15572
- await new Promise(resolve => setTimeout(resolve, delay));
15707
+ }
15708
+ }
15709
+ else {
15710
+ if (!this.options.silentMode) {
15711
+ console.log('📸 复用现有 context(性能优化)');
15573
15712
  }
15574
15713
  }
15575
15714
  try {
15576
15715
  // 根据输出格式选择对应的 API,避免格式转换(性能优化)
15577
- // 添加超时机制,防止卡住(30秒超时)
15578
- const timeoutMs = 30000; // 30秒超时
15579
- const timeoutPromise = new Promise((_, reject) => {
15580
- setTimeout(() => {
15581
- reject(new Error(`截图超时(${timeoutMs}ms),可能页面过大或 Worker 处理时间过长`));
15582
- }, timeoutMs);
15583
- });
15716
+ // 注意:timeout 已经在 createContext 时设置,modern-screenshot 内部会处理超时
15584
15717
  let dataUrl;
15585
15718
  const outputFormat = this.options.outputFormat || 'webp';
15586
15719
  if (!this.options.silentMode) {
15587
15720
  console.log(`📸 使用 ${outputFormat.toUpperCase()} 格式截图(直接输出,无需转换)...`);
15588
15721
  }
15589
15722
  // 根据输出格式选择对应的 API
15723
+ // modern-screenshot 内部已经处理了超时,不需要额外的 Promise.race
15590
15724
  if (outputFormat === 'webp') {
15591
15725
  // 使用 domToWebp,直接输出 WebP 格式,无需转换
15592
- dataUrl = await Promise.race([
15593
- domToWebp(this.screenshotContext),
15594
- timeoutPromise
15595
- ]);
15726
+ dataUrl = await domToWebp(this.screenshotContext);
15596
15727
  }
15597
15728
  else if (outputFormat === 'jpeg') {
15598
15729
  // 使用 domToJpeg,直接输出 JPEG 格式,无需转换
15599
- dataUrl = await Promise.race([
15600
- domToJpeg(this.screenshotContext),
15601
- timeoutPromise
15602
- ]);
15730
+ dataUrl = await domToJpeg(this.screenshotContext);
15603
15731
  }
15604
15732
  else {
15605
15733
  // 默认使用 domToPng
15606
- dataUrl = await Promise.race([
15607
- domToPng(this.screenshotContext),
15608
- timeoutPromise
15609
- ]);
15734
+ dataUrl = await domToPng(this.screenshotContext);
15610
15735
  }
15611
15736
  // 验证截图结果
15612
15737
  if (!dataUrl || dataUrl.length < 100) {
@@ -15633,37 +15758,11 @@ class ScreenshotManager {
15633
15758
  throw error;
15634
15759
  }
15635
15760
  finally {
15636
- // 每次截图后立即清理 context,释放 Worker 和内存
15637
- // 这是防止内存泄漏的关键步骤
15638
- if (this.screenshotContext) {
15639
- try {
15640
- destroyContext(this.screenshotContext);
15641
- if (!this.options.silentMode) {
15642
- console.log('📸 ✅ modern-screenshot context 已清理');
15643
- }
15644
- }
15645
- catch (e) {
15646
- if (!this.options.silentMode) {
15647
- console.warn('📸 ⚠️ 清理 context 失败:', e);
15648
- }
15649
- }
15650
- finally {
15651
- // 确保 context 引用被清除
15652
- this.screenshotContext = null;
15653
- }
15654
- }
15761
+ // 优化:不复用 context 时才清理(性能优化)
15762
+ // 如果元素或配置没有变化,保留 context 以便下次复用
15763
+ // 这样可以避免频繁创建和销毁 Worker,提升性能 20%+
15655
15764
  // 释放截图锁
15656
15765
  this.isScreenshotInProgress = false;
15657
- // 强制触发垃圾回收(如果可能)
15658
- // 注意:这需要浏览器支持,不是所有浏览器都有效
15659
- if (typeof window !== 'undefined' && window.gc && typeof window.gc === 'function') {
15660
- try {
15661
- window.gc();
15662
- }
15663
- catch {
15664
- // 忽略 GC 错误
15665
- }
15666
- }
15667
15766
  }
15668
15767
  }
15669
15768
  catch (error) {
@@ -15679,7 +15778,34 @@ class ScreenshotManager {
15679
15778
  if (this.isScreenshotInProgress) {
15680
15779
  this.isScreenshotInProgress = false;
15681
15780
  }
15781
+ // 处理队列中的下一个请求
15782
+ this.processScreenshotQueue();
15783
+ }
15784
+ }
15785
+ /**
15786
+ * 处理截图队列
15787
+ */
15788
+ async processScreenshotQueue() {
15789
+ if (this.isProcessingQueue || this.screenshotQueue.length === 0) {
15790
+ return;
15791
+ }
15792
+ this.isProcessingQueue = true;
15793
+ while (this.screenshotQueue.length > 0 && !this.isScreenshotInProgress) {
15794
+ const task = this.screenshotQueue.shift();
15795
+ if (task) {
15796
+ try {
15797
+ task.resolve();
15798
+ // 等待当前截图完成
15799
+ while (this.isScreenshotInProgress) {
15800
+ await new Promise(resolve => setTimeout(resolve, 50));
15801
+ }
15802
+ }
15803
+ catch (error) {
15804
+ task.reject(error instanceof Error ? error : new Error(String(error)));
15805
+ }
15806
+ }
15682
15807
  }
15808
+ this.isProcessingQueue = false;
15683
15809
  }
15684
15810
  /**
15685
15811
  * 预连接代理服务器(优化网络性能)
@@ -16553,6 +16679,18 @@ class ScreenshotManager {
16553
16679
  * 清理资源
16554
16680
  */
16555
16681
  destroy() {
16682
+ // 清理 modern-screenshot context
16683
+ if (this.screenshotContext) {
16684
+ try {
16685
+ destroyContext(this.screenshotContext);
16686
+ }
16687
+ catch (e) {
16688
+ // 忽略清理错误
16689
+ }
16690
+ this.screenshotContext = null;
16691
+ this.contextElement = null;
16692
+ this.contextOptionsHash = '';
16693
+ }
16556
16694
  this.stopScreenshot();
16557
16695
  if (this.worker) {
16558
16696
  this.worker.terminate();