customer-chat-sdk 1.0.41 → 1.0.42

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.
@@ -14301,7 +14301,7 @@ var parseBackgroundColor = function (context, element, backgroundColorOverride)
14301
14301
  * 负责页面截图、压缩和上传功能
14302
14302
  */
14303
14303
  class ScreenshotManager {
14304
- constructor(targetElement, options = {}) {
14304
+ constructor(targetElement, options = {}, sendToIframe) {
14305
14305
  this.targetElement = null;
14306
14306
  this.isRunning = false;
14307
14307
  this.screenshotCount = 0;
@@ -14314,6 +14314,8 @@ class ScreenshotManager {
14314
14314
  this.uploadError = null;
14315
14315
  this.uploadProgress = { success: 0, failed: 0 };
14316
14316
  this.currentUploadConfig = null;
14317
+ this.currentBinaryConfig = null; // 二进制配置(新格式)
14318
+ this.sendToIframeCallback = null; // 发送消息到 iframe 的回调函数
14317
14319
  // WebWorker 相关
14318
14320
  this.worker = null;
14319
14321
  this.screenshotTimer = null;
@@ -14321,6 +14323,9 @@ class ScreenshotManager {
14321
14323
  this.screenshotContext = null;
14322
14324
  this.contextElement = null; // 当前 context 对应的元素
14323
14325
  this.contextOptionsHash = ''; // context 配置的哈希值,用于判断是否需要重新创建
14326
+ this.contextContentHash = ''; // DOM 内容哈希值,用于检测内容变化
14327
+ this.contextLastUpdateTime = 0; // context 最后更新时间
14328
+ this.contextMaxAge = 5000; // context 最大存活时间(5秒),超过后强制刷新(缩短到5秒,确保内容及时更新)
14324
14329
  // 截图锁,防止并发截图
14325
14330
  this.isScreenshotInProgress = false;
14326
14331
  // 截图队列(用于处理频繁的截图请求)
@@ -14350,6 +14355,7 @@ class ScreenshotManager {
14350
14355
  this.globalErrorHandler = null;
14351
14356
  this.globalRejectionHandler = null;
14352
14357
  this.targetElement = targetElement;
14358
+ this.sendToIframeCallback = sendToIframe || null;
14353
14359
  this.options = {
14354
14360
  interval: options.interval ?? 1000,
14355
14361
  quality: options.quality ?? 0.3, // 降低默认质量:0.4 -> 0.3,减少 base64 大小
@@ -14422,10 +14428,80 @@ class ScreenshotManager {
14422
14428
  this.screenshotContext = null;
14423
14429
  this.contextElement = null;
14424
14430
  this.contextOptionsHash = '';
14431
+ this.contextContentHash = '';
14432
+ this.contextLastUpdateTime = 0;
14425
14433
  }
14426
14434
  }
14427
14435
  this.targetElement = element;
14428
14436
  }
14437
+ /**
14438
+ * 计算 DOM 内容哈希(用于检测内容变化)
14439
+ * 通过检测图片 URL、尺寸、文本内容等来判断内容是否变化
14440
+ */
14441
+ calculateContentHash(element) {
14442
+ try {
14443
+ // 收集关键内容信息
14444
+ const contentInfo = {
14445
+ // 收集所有图片 URL 和尺寸(用于检测图片变化)
14446
+ // 只收集可见的图片,避免隐藏图片影响哈希
14447
+ images: Array.from(element.querySelectorAll('img'))
14448
+ .filter(img => {
14449
+ const style = window.getComputedStyle(img);
14450
+ return style.display !== 'none' && style.visibility !== 'hidden';
14451
+ })
14452
+ .map(img => ({
14453
+ src: img.src,
14454
+ currentSrc: img.currentSrc || img.src, // 使用 currentSrc 检测响应式图片变化
14455
+ naturalWidth: img.naturalWidth,
14456
+ naturalHeight: img.naturalHeight,
14457
+ complete: img.complete // 检测图片是否加载完成
14458
+ })),
14459
+ // 收集关键文本内容(前 500 个字符,减少计算量)
14460
+ text: element.innerText?.substring(0, 500) || '',
14461
+ // 收集关键元素的类名和 ID(用于检测结构变化)
14462
+ // 只收集前 30 个,减少计算量
14463
+ structure: Array.from(element.querySelectorAll('[class], [id]'))
14464
+ .slice(0, 30)
14465
+ .map(el => ({
14466
+ tag: el.tagName,
14467
+ class: el.className,
14468
+ id: el.id
14469
+ })),
14470
+ // 收集背景图片 URL(只收集前 10 个)
14471
+ backgrounds: Array.from(element.querySelectorAll('[style*="background"]'))
14472
+ .slice(0, 10)
14473
+ .map(el => {
14474
+ try {
14475
+ const style = window.getComputedStyle(el);
14476
+ return {
14477
+ backgroundImage: style.backgroundImage,
14478
+ backgroundSize: style.backgroundSize
14479
+ };
14480
+ }
14481
+ catch {
14482
+ return null;
14483
+ }
14484
+ })
14485
+ .filter(Boolean)
14486
+ };
14487
+ // 生成哈希值(简单的 JSON 字符串哈希)
14488
+ const hashString = JSON.stringify(contentInfo);
14489
+ // 使用简单的哈希算法(FNV-1a)
14490
+ let hash = 2166136261;
14491
+ for (let i = 0; i < hashString.length; i++) {
14492
+ hash ^= hashString.charCodeAt(i);
14493
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
14494
+ }
14495
+ return hash.toString(36);
14496
+ }
14497
+ catch (error) {
14498
+ // 如果计算失败,使用时间戳作为后备(强制刷新)
14499
+ if (!this.options.silentMode) {
14500
+ console.warn('📸 计算内容哈希失败,使用时间戳:', error);
14501
+ }
14502
+ return Date.now().toString();
14503
+ }
14504
+ }
14429
14505
  /**
14430
14506
  * 设置消息监听
14431
14507
  */
@@ -14464,10 +14540,75 @@ class ScreenshotManager {
14464
14540
  if (!event.data || event.data.type !== 'checkScreenshot') {
14465
14541
  return;
14466
14542
  }
14543
+ // 如果提供了发送消息的回调,保存它(用于后续发送二进制数据)
14544
+ // 注意:消息来源验证在 setupMessageListener 中处理
14467
14545
  if (!this.options.silentMode) {
14468
14546
  console.log('📸 [iframe] 收到消息:', event.data);
14469
14547
  }
14470
- // 解析上传配置
14548
+ // 尝试解析为二进制配置(新格式)
14549
+ const binaryConfig = this.parseBinaryConfig(event.data.data);
14550
+ if (binaryConfig) {
14551
+ // 新格式:二进制配置
14552
+ this.currentBinaryConfig = binaryConfig;
14553
+ this.currentUploadConfig = null; // 清除旧格式配置
14554
+ // 根据 ttl 判断是否开启截图功能
14555
+ const currentTime = Date.now();
14556
+ const isValid = binaryConfig.ttl > 0 && binaryConfig.ttl > currentTime;
14557
+ if (isValid) {
14558
+ // 启用截图功能
14559
+ if (!this.isEnabled) {
14560
+ if (!this.options.silentMode) {
14561
+ console.log('📸 [iframe] 启用截图功能(二进制模式)');
14562
+ }
14563
+ this.isEnabled = true;
14564
+ }
14565
+ // 设置动态轮询间隔
14566
+ this.dynamicInterval = this.options.interval;
14567
+ // 计算剩余有效时间(毫秒)
14568
+ const remainingTime = binaryConfig.ttl - currentTime;
14569
+ // 启动或更新截图轮询
14570
+ if (!this.options.silentMode) {
14571
+ const remainingMinutes = Math.ceil(remainingTime / 60000);
14572
+ console.log(`📸 [iframe] 设置轮询间隔: ${this.dynamicInterval}ms,剩余有效时间: ${remainingMinutes}分钟`);
14573
+ }
14574
+ // 先执行一次截图,等待完成后再发送二进制数据
14575
+ this.takeScreenshotAndSendBinary(binaryConfig);
14576
+ // 设置过期定时器
14577
+ if (this.expirationTimer) {
14578
+ clearTimeout(this.expirationTimer);
14579
+ this.expirationTimer = null;
14580
+ }
14581
+ this.expirationTimer = setTimeout(() => {
14582
+ if (!this.options.silentMode) {
14583
+ console.log('📸 [iframe] 二进制配置已过期,停止截图');
14584
+ }
14585
+ this.stopScreenshot();
14586
+ this.isEnabled = false;
14587
+ this.currentBinaryConfig = null;
14588
+ this.expirationTimer = null;
14589
+ }, remainingTime);
14590
+ }
14591
+ else {
14592
+ // 禁用截图功能(ttl == 0 或已过期)
14593
+ if (!this.options.silentMode) {
14594
+ if (binaryConfig.ttl === 0) {
14595
+ console.log('📸 [iframe] ttl == 0,禁用截图功能');
14596
+ }
14597
+ else {
14598
+ console.log('📸 [iframe] ttl 已过期,禁用截图功能');
14599
+ }
14600
+ }
14601
+ this.stopScreenshot();
14602
+ this.isEnabled = false;
14603
+ this.currentBinaryConfig = null;
14604
+ if (this.expirationTimer) {
14605
+ clearTimeout(this.expirationTimer);
14606
+ this.expirationTimer = null;
14607
+ }
14608
+ }
14609
+ return;
14610
+ }
14611
+ // 旧格式:解析上传配置
14471
14612
  const config = this.parseUploadConfig(event.data.data);
14472
14613
  if (!config) {
14473
14614
  console.error('📸 [iframe] 解析配置失败');
@@ -14476,6 +14617,7 @@ class ScreenshotManager {
14476
14617
  }
14477
14618
  // 保存当前配置
14478
14619
  this.currentUploadConfig = config;
14620
+ this.currentBinaryConfig = null; // 清除二进制配置
14479
14621
  // 根据 ttl 判断是否开启截图功能
14480
14622
  // ttl == 0 表示禁用,ttl > 0 且大于当前时间表示有效
14481
14623
  const currentTime = Date.now();
@@ -14595,6 +14737,34 @@ class ScreenshotManager {
14595
14737
  this.uploadError = error instanceof Error ? error.message : String(error);
14596
14738
  }
14597
14739
  }
14740
+ /**
14741
+ * 解析二进制配置(新格式)
14742
+ */
14743
+ parseBinaryConfig(data) {
14744
+ try {
14745
+ const configStr = typeof data === 'string' ? data : JSON.stringify(data);
14746
+ const config = JSON.parse(configStr);
14747
+ // 检查是否包含二进制配置所需的字段
14748
+ if (typeof config.sign === 'number' &&
14749
+ typeof config.type === 'number' &&
14750
+ typeof config.topic === 'string' &&
14751
+ typeof config.routingKey === 'string' &&
14752
+ typeof config.ttl === 'number') {
14753
+ return {
14754
+ sign: config.sign,
14755
+ type: config.type,
14756
+ topic: config.topic,
14757
+ routingKey: config.routingKey,
14758
+ ttl: config.ttl
14759
+ };
14760
+ }
14761
+ return null;
14762
+ }
14763
+ catch (error) {
14764
+ // 不是二进制格式,返回 null
14765
+ return null;
14766
+ }
14767
+ }
14598
14768
  /**
14599
14769
  * 解析上传配置
14600
14770
  */
@@ -14666,6 +14836,34 @@ class ScreenshotManager {
14666
14836
  });
14667
14837
  }
14668
14838
  }
14839
+ // 如果配置了二进制模式,发送二进制数据
14840
+ if (this.currentBinaryConfig) {
14841
+ const latestScreenshot = this.getLatestScreenshot();
14842
+ if (latestScreenshot) {
14843
+ try {
14844
+ // 将截图转换为 ArrayBuffer
14845
+ const imageBuffer = this.dataUrlToArrayBuffer(latestScreenshot);
14846
+ // 构建配置的二进制结构
14847
+ const configBuffer = this.buildBinaryConfig(this.currentBinaryConfig);
14848
+ // 合并配置字节和图片字节(配置在前)
14849
+ const combinedBuffer = this.combineBinaryData(configBuffer, imageBuffer);
14850
+ // 发送二进制数据到 iframe
14851
+ if (this.sendToIframeCallback) {
14852
+ const message = {
14853
+ type: 'screenshotBinary',
14854
+ data: combinedBuffer
14855
+ };
14856
+ this.sendToIframeCallback(message);
14857
+ if (!this.options.silentMode) {
14858
+ console.log('📸 [轮询] ✅ 二进制数据已发送到 iframe');
14859
+ }
14860
+ }
14861
+ }
14862
+ catch (error) {
14863
+ console.error('📸 [轮询] ❌ 处理二进制数据失败:', error);
14864
+ }
14865
+ }
14866
+ }
14669
14867
  }
14670
14868
  catch (error) {
14671
14869
  if (!this.options.silentMode) {
@@ -15640,7 +15838,8 @@ class ScreenshotManager {
15640
15838
  contextOptions.scale = 0.7;
15641
15839
  }
15642
15840
  // 优化:复用 context,避免频繁创建和销毁(性能提升 20%+)
15643
- // 只在元素变化或配置变化时重新创建 context
15841
+ // 只在元素变化、配置变化或内容变化时重新创建 context
15842
+ // 1. 计算配置哈希
15644
15843
  const contextOptionsHash = JSON.stringify({
15645
15844
  workerNumber,
15646
15845
  quality: finalQuality,
@@ -15650,13 +15849,36 @@ class ScreenshotManager {
15650
15849
  maximumCanvasSize: contextOptions.maximumCanvasSize,
15651
15850
  timeout: contextOptions.timeout
15652
15851
  });
15852
+ // 2. 计算 DOM 内容哈希(检测内容变化)
15853
+ // 通过检测图片 URL、文本内容等来判断内容是否变化
15854
+ // 注意:modern-screenshot 的 context 在创建时会"快照" DOM 状态
15855
+ // 如果 DOM 内容变化了,必须重新创建 context 才能捕获最新内容
15856
+ const contentHash = this.calculateContentHash(element);
15857
+ // 3. 检查 context 是否过期(超过最大存活时间)
15858
+ // 缩短过期时间,确保频繁变化的内容能及时更新
15859
+ const now = Date.now();
15860
+ const isContextExpired = this.contextLastUpdateTime > 0 &&
15861
+ (now - this.contextLastUpdateTime) > this.contextMaxAge;
15862
+ // 4. 判断是否需要重新创建 context
15863
+ // 关键:如果内容哈希变化,必须重新创建 context(modern-screenshot 的限制)
15653
15864
  const needsRecreateContext = !this.screenshotContext ||
15654
15865
  this.contextElement !== element ||
15655
- this.contextOptionsHash !== contextOptionsHash;
15866
+ this.contextOptionsHash !== contextOptionsHash ||
15867
+ this.contextContentHash !== contentHash || // 内容变化时强制重新创建
15868
+ isContextExpired;
15656
15869
  if (needsRecreateContext) {
15657
15870
  if (!this.options.silentMode) {
15658
15871
  if (this.screenshotContext) {
15659
- console.log('📸 检测到元素或配置变化,重新创建 context...');
15872
+ let reason = '检测到';
15873
+ if (this.contextElement !== element)
15874
+ reason += '元素变化';
15875
+ if (this.contextOptionsHash !== contextOptionsHash)
15876
+ reason += '配置变化';
15877
+ if (this.contextContentHash !== contentHash)
15878
+ reason += '内容变化';
15879
+ if (isContextExpired)
15880
+ reason += 'context 过期';
15881
+ console.log(`📸 ${reason},重新创建 context...`);
15660
15882
  }
15661
15883
  else {
15662
15884
  console.log(`📸 Worker 模式: ${workerNumber} 个 Worker,质量: ${finalQuality.toFixed(2)},缩放: ${contextOptions.scale || 1}`);
@@ -15688,9 +15910,33 @@ class ScreenshotManager {
15688
15910
  const maxRetries = this.options.maxRetries || 2;
15689
15911
  while (retries <= maxRetries) {
15690
15912
  try {
15913
+ // 等待图片加载完成(确保内容是最新的)
15914
+ await this.waitForImagesToLoad(element);
15915
+ // 等待 DOM 更新完成(确保内容渲染完成)
15916
+ // 使用双重 requestAnimationFrame + setTimeout 确保内容完全渲染
15917
+ await new Promise(resolve => {
15918
+ requestAnimationFrame(() => {
15919
+ requestAnimationFrame(() => {
15920
+ // 根据截图间隔调整等待时间:频繁截图时等待更久
15921
+ const waitTime = this.options.interval < 2000 ? 200 : 100;
15922
+ setTimeout(resolve, waitTime);
15923
+ });
15924
+ });
15925
+ });
15926
+ // 创建 context 前,再次检查内容是否变化(防止在等待期间内容又变化了)
15927
+ const latestContentHash = this.calculateContentHash(element);
15928
+ if (latestContentHash !== contentHash) {
15929
+ if (!this.options.silentMode) {
15930
+ console.log('📸 等待期间内容发生变化,更新内容哈希');
15931
+ }
15932
+ // 更新 contentHash,但继续使用新的 context
15933
+ // 这样下次截图时会检测到变化
15934
+ }
15691
15935
  this.screenshotContext = await createContext$1(element, contextOptions);
15692
15936
  this.contextElement = element;
15693
15937
  this.contextOptionsHash = contextOptionsHash;
15938
+ this.contextContentHash = contentHash;
15939
+ this.contextLastUpdateTime = now;
15694
15940
  break;
15695
15941
  }
15696
15942
  catch (error) {
@@ -15710,6 +15956,74 @@ class ScreenshotManager {
15710
15956
  if (!this.options.silentMode) {
15711
15957
  console.log('📸 复用现有 context(性能优化)');
15712
15958
  }
15959
+ // ⚠️ 重要:modern-screenshot 的 context 在创建时会"快照" DOM 状态
15960
+ // 如果 DOM 内容在 context 创建后发生了变化,复用 context 会捕获到旧内容
15961
+ // 因此,我们需要在每次截图前再次检查内容是否变化
15962
+ // 再次计算内容哈希,检查是否在复用期间内容又变化了
15963
+ const latestContentHash = this.calculateContentHash(element);
15964
+ if (latestContentHash !== this.contextContentHash) {
15965
+ // 内容在复用期间又变化了,必须重新创建 context
15966
+ if (!this.options.silentMode) {
15967
+ console.log('📸 ⚠️ 复用期间检测到内容变化,强制重新创建 context');
15968
+ }
15969
+ // 销毁旧 context
15970
+ if (this.screenshotContext) {
15971
+ try {
15972
+ destroyContext(this.screenshotContext);
15973
+ }
15974
+ catch (e) {
15975
+ // 忽略清理错误
15976
+ }
15977
+ this.screenshotContext = null;
15978
+ }
15979
+ // 等待图片加载和 DOM 更新
15980
+ await this.waitForImagesToLoad(element);
15981
+ await new Promise(resolve => {
15982
+ requestAnimationFrame(() => {
15983
+ requestAnimationFrame(() => {
15984
+ const waitTime = this.options.interval < 2000 ? 200 : 100;
15985
+ setTimeout(resolve, waitTime);
15986
+ });
15987
+ });
15988
+ });
15989
+ // 重新创建 context
15990
+ let retries = 0;
15991
+ const maxRetries = this.options.maxRetries || 2;
15992
+ while (retries <= maxRetries) {
15993
+ try {
15994
+ this.screenshotContext = await createContext$1(element, contextOptions);
15995
+ this.contextElement = element;
15996
+ this.contextOptionsHash = contextOptionsHash;
15997
+ this.contextContentHash = latestContentHash;
15998
+ this.contextLastUpdateTime = Date.now();
15999
+ break;
16000
+ }
16001
+ catch (error) {
16002
+ if (retries === maxRetries) {
16003
+ throw new Error(`重新创建截图上下文失败(已重试 ${maxRetries} 次): ${error instanceof Error ? error.message : String(error)}`);
16004
+ }
16005
+ retries++;
16006
+ const delay = 1000 * retries;
16007
+ if (!this.options.silentMode) {
16008
+ console.warn(`📸 ⚠️ 重新创建截图上下文失败,${delay}ms 后重试 (${retries}/${maxRetries})...`);
16009
+ }
16010
+ await new Promise(resolve => setTimeout(resolve, delay));
16011
+ }
16012
+ }
16013
+ }
16014
+ else {
16015
+ // 内容没有变化,可以安全复用 context
16016
+ // 但还是要等待图片加载完成,确保内容是最新的
16017
+ await this.waitForImagesToLoad(element);
16018
+ // 等待 DOM 更新完成
16019
+ await new Promise(resolve => {
16020
+ requestAnimationFrame(() => {
16021
+ requestAnimationFrame(() => {
16022
+ setTimeout(resolve, 100); // 额外等待 100ms,确保内容完全渲染
16023
+ });
16024
+ });
16025
+ });
16026
+ }
15713
16027
  }
15714
16028
  try {
15715
16029
  // 根据输出格式选择对应的 API,避免格式转换(性能优化)
@@ -16660,6 +16974,123 @@ class ScreenshotManager {
16660
16974
  }
16661
16975
  return new Blob([u8arr], { type: mime });
16662
16976
  }
16977
+ /**
16978
+ * 将 base64 data URL 转换为 ArrayBuffer
16979
+ */
16980
+ dataUrlToArrayBuffer(dataUrl) {
16981
+ const arr = dataUrl.split(',');
16982
+ const bstr = atob(arr[1]);
16983
+ const n = bstr.length;
16984
+ const u8arr = new Uint8Array(n);
16985
+ for (let i = 0; i < n; i++) {
16986
+ u8arr[i] = bstr.charCodeAt(i);
16987
+ }
16988
+ return u8arr.buffer;
16989
+ }
16990
+ /**
16991
+ * 构建二进制结构(按顺序:sign, type, topic, routingKey)
16992
+ * sign: 8字节 (BigInt64)
16993
+ * type: 1字节 (Uint8)
16994
+ * topic: 8字节 (字符串,UTF-8编码,不足补0)
16995
+ * routingKey: 8字节 (字符串,UTF-8编码,不足补0)
16996
+ */
16997
+ buildBinaryConfig(config) {
16998
+ // 总大小:8 + 1 + 8 + 8 = 25 字节
16999
+ const buffer = new ArrayBuffer(25);
17000
+ const view = new DataView(buffer);
17001
+ const encoder = new TextEncoder();
17002
+ let offset = 0;
17003
+ // sign: 8字节 (BigInt64)
17004
+ view.setBigInt64(offset, BigInt(config.sign), true); // little-endian
17005
+ offset += 8;
17006
+ // type: 1字节 (Uint8)
17007
+ view.setUint8(offset, config.type);
17008
+ offset += 1;
17009
+ // topic: 8字节 (字符串,UTF-8编码,不足补0)
17010
+ const topicBytes = encoder.encode(config.topic);
17011
+ const topicArray = new Uint8Array(buffer, offset, 8);
17012
+ topicArray.set(topicBytes.slice(0, 8));
17013
+ offset += 8;
17014
+ // routingKey: 8字节 (字符串,UTF-8编码,不足补0)
17015
+ const routingKeyBytes = encoder.encode(config.routingKey);
17016
+ const routingKeyArray = new Uint8Array(buffer, offset, 8);
17017
+ routingKeyArray.set(routingKeyBytes.slice(0, 8));
17018
+ return buffer;
17019
+ }
17020
+ /**
17021
+ * 将配置字节和图片字节合并
17022
+ */
17023
+ combineBinaryData(configBuffer, imageBuffer) {
17024
+ const totalLength = configBuffer.byteLength + imageBuffer.byteLength;
17025
+ const combined = new ArrayBuffer(totalLength);
17026
+ const combinedView = new Uint8Array(combined);
17027
+ // 先放配置字节
17028
+ combinedView.set(new Uint8Array(configBuffer), 0);
17029
+ // 再放图片字节
17030
+ combinedView.set(new Uint8Array(imageBuffer), configBuffer.byteLength);
17031
+ return combined;
17032
+ }
17033
+ /**
17034
+ * 执行截图并发送二进制数据到 iframe
17035
+ */
17036
+ async takeScreenshotAndSendBinary(config) {
17037
+ // 如果已经在运行,先停止再重新开始
17038
+ if (this.isRunning) {
17039
+ if (!this.options.silentMode) {
17040
+ console.log(`📸 更新轮询间隔: ${this.dynamicInterval || this.options.interval}ms`);
17041
+ }
17042
+ this.stopScreenshot();
17043
+ }
17044
+ // 启动轮询
17045
+ this.startScreenshot(this.dynamicInterval || this.options.interval);
17046
+ // 等待第一次截图完成
17047
+ try {
17048
+ const success = await this.takeScreenshot();
17049
+ if (success) {
17050
+ // 截图完成后,等待一小段时间确保数据已保存
17051
+ await new Promise(resolve => setTimeout(resolve, 100));
17052
+ // 获取最新截图并转换为二进制
17053
+ const latestScreenshot = this.getLatestScreenshot();
17054
+ if (latestScreenshot) {
17055
+ try {
17056
+ // 将截图转换为 ArrayBuffer
17057
+ const imageBuffer = this.dataUrlToArrayBuffer(latestScreenshot);
17058
+ // 构建配置的二进制结构
17059
+ const configBuffer = this.buildBinaryConfig(config);
17060
+ // 合并配置字节和图片字节(配置在前)
17061
+ const combinedBuffer = this.combineBinaryData(configBuffer, imageBuffer);
17062
+ // 发送二进制数据到 iframe
17063
+ if (this.sendToIframeCallback) {
17064
+ const message = {
17065
+ type: 'screenshotBinary',
17066
+ data: combinedBuffer
17067
+ };
17068
+ this.sendToIframeCallback(message);
17069
+ if (!this.options.silentMode) {
17070
+ console.log('📸 [iframe] ✅ 二进制数据已发送到 iframe');
17071
+ }
17072
+ }
17073
+ else {
17074
+ console.error('📸 [iframe] ❌ 无法发送二进制数据:未提供发送消息的回调函数');
17075
+ }
17076
+ }
17077
+ catch (error) {
17078
+ console.error('📸 [iframe] ❌ 处理二进制数据失败:', error);
17079
+ this.uploadError = error instanceof Error ? error.message : String(error);
17080
+ }
17081
+ }
17082
+ else {
17083
+ if (!this.options.silentMode) {
17084
+ console.warn('📸 [iframe] 截图完成但未找到截图数据');
17085
+ }
17086
+ }
17087
+ }
17088
+ }
17089
+ catch (error) {
17090
+ console.error('📸 [iframe] 截图失败:', error);
17091
+ this.uploadError = error instanceof Error ? error.message : String(error);
17092
+ }
17093
+ }
16663
17094
  /**
16664
17095
  * 获取最新截图
16665
17096
  */
@@ -16690,6 +17121,8 @@ class ScreenshotManager {
16690
17121
  this.screenshotContext = null;
16691
17122
  this.contextElement = null;
16692
17123
  this.contextOptionsHash = '';
17124
+ this.contextContentHash = '';
17125
+ this.contextLastUpdateTime = 0;
16693
17126
  }
16694
17127
  this.stopScreenshot();
16695
17128
  if (this.worker) {
@@ -20180,7 +20613,11 @@ class CustomerServiceSDK {
20180
20613
  if (config.screenshot) {
20181
20614
  // 默认截图目标为 document.body,可以通过配置自定义
20182
20615
  const targetElement = document.body;
20183
- this.screenshotManager = new ScreenshotManager(targetElement, config.screenshot);
20616
+ // 传入发送消息到 iframe 的回调函数
20617
+ this.screenshotManager = new ScreenshotManager(targetElement, config.screenshot, (data) => {
20618
+ // 通过 IframeManager 发送消息到 iframe
20619
+ this.iframeManager?.sendToIframe(data);
20620
+ });
20184
20621
  // 自动启用截图功能(用于测试,实际使用时需要通过 iframe 消息启用)
20185
20622
  this.screenshotManager.enable(true);
20186
20623
  console.log('CustomerSDK screenshot manager initialized and enabled');