customer-chat-sdk 1.0.71 → 1.0.73

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.
@@ -14318,11 +14318,6 @@ class ScreenshotManager {
14318
14318
  this.screenshotTimer = null;
14319
14319
  // modern-screenshot Worker 上下文(用于复用,避免频繁创建和销毁)
14320
14320
  this.screenshotContext = null;
14321
- this.contextElement = null; // 当前 context 对应的元素
14322
- this.contextOptionsHash = ''; // context 配置的哈希值,用于判断是否需要重新创建
14323
- this.contextContentHash = ''; // DOM 内容哈希值,用于检测内容变化
14324
- this.contextLastUpdateTime = 0; // context 最后更新时间
14325
- this.contextMaxAge = 5000; // context 最大存活时间(5秒),超过后强制刷新(缩短到5秒,确保内容及时更新)
14326
14321
  // 截图锁,防止并发截图
14327
14322
  this.isScreenshotInProgress = false;
14328
14323
  // 截图队列(用于处理频繁的截图请求)
@@ -14380,7 +14375,10 @@ class ScreenshotManager {
14380
14375
  fetchPriority: options.fetchPriority ?? 'high', // 默认高优先级
14381
14376
  maxImageSize: options.maxImageSize ?? 5, // 不使用代理时,单个图片最大尺寸(MB),默认5MB
14382
14377
  skipLargeImages: options.skipLargeImages ?? true, // 不使用代理时,是否跳过过大的图片,默认true(跳过)
14383
- workerNumber: options.workerNumber ?? undefined // modern-screenshot Worker 数量,默认自动计算(undefined 表示自动)
14378
+ workerNumber: options.workerNumber ?? undefined, // modern-screenshot Worker 数量,默认自动计算(undefined 表示自动)
14379
+ workerUrl: options.workerUrl ?? undefined, // modern-screenshot Worker URL,默认自动
14380
+ drawImageInterval: options.drawImageInterval ?? 20, // modern-screenshot drawImageInterval,默认 20ms
14381
+ backgroundColor: options.backgroundColor ?? '#ffffff', // modern-screenshot backgroundColor,默认 '#ffffff'
14384
14382
  };
14385
14383
  this.setupMessageListener();
14386
14384
  this.setupVisibilityChangeListener();
@@ -14418,82 +14416,10 @@ class ScreenshotManager {
14418
14416
  // 忽略清理错误
14419
14417
  }
14420
14418
  this.screenshotContext = null;
14421
- this.contextElement = null;
14422
- this.contextOptionsHash = '';
14423
- this.contextContentHash = '';
14424
- this.contextLastUpdateTime = 0;
14425
14419
  }
14426
14420
  }
14427
14421
  this.targetElement = element;
14428
14422
  }
14429
- /**
14430
- * 计算 DOM 内容哈希(用于检测内容变化)
14431
- * 通过检测图片 URL、尺寸、文本内容等来判断内容是否变化
14432
- */
14433
- calculateContentHash(element) {
14434
- try {
14435
- // 收集关键内容信息
14436
- const contentInfo = {
14437
- // 收集所有图片 URL 和尺寸(用于检测图片变化)
14438
- // 只收集可见的图片,避免隐藏图片影响哈希
14439
- images: Array.from(element.querySelectorAll('img'))
14440
- .filter(img => {
14441
- const style = window.getComputedStyle(img);
14442
- return style.display !== 'none' && style.visibility !== 'hidden';
14443
- })
14444
- .map(img => ({
14445
- src: img.src,
14446
- currentSrc: img.currentSrc || img.src, // 使用 currentSrc 检测响应式图片变化
14447
- naturalWidth: img.naturalWidth,
14448
- naturalHeight: img.naturalHeight,
14449
- complete: img.complete // 检测图片是否加载完成
14450
- })),
14451
- // 收集关键文本内容(前 500 个字符,减少计算量)
14452
- text: element.innerText?.substring(0, 500) || '',
14453
- // 收集关键元素的类名和 ID(用于检测结构变化)
14454
- // 只收集前 30 个,减少计算量
14455
- structure: Array.from(element.querySelectorAll('[class], [id]'))
14456
- .slice(0, 30)
14457
- .map(el => ({
14458
- tag: el.tagName,
14459
- class: el.className,
14460
- id: el.id
14461
- })),
14462
- // 收集背景图片 URL(只收集前 10 个)
14463
- backgrounds: Array.from(element.querySelectorAll('[style*="background"]'))
14464
- .slice(0, 10)
14465
- .map(el => {
14466
- try {
14467
- const style = window.getComputedStyle(el);
14468
- return {
14469
- backgroundImage: style.backgroundImage,
14470
- backgroundSize: style.backgroundSize
14471
- };
14472
- }
14473
- catch {
14474
- return null;
14475
- }
14476
- })
14477
- .filter(Boolean)
14478
- };
14479
- // 生成哈希值(简单的 JSON 字符串哈希)
14480
- const hashString = JSON.stringify(contentInfo);
14481
- // 使用简单的哈希算法(FNV-1a)
14482
- let hash = 2166136261;
14483
- for (let i = 0; i < hashString.length; i++) {
14484
- hash ^= hashString.charCodeAt(i);
14485
- hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
14486
- }
14487
- return hash.toString(36);
14488
- }
14489
- catch (error) {
14490
- // 如果计算失败,使用时间戳作为后备(强制刷新)
14491
- if (!this.options.silentMode) {
14492
- console.warn('📸 计算内容哈希失败,使用时间戳:', error);
14493
- }
14494
- return Date.now().toString();
14495
- }
14496
- }
14497
14423
  /**
14498
14424
  * 设置消息监听
14499
14425
  */
@@ -15619,22 +15545,10 @@ class ScreenshotManager {
15619
15545
  console.log(`📸 窗口尺寸: ${window.innerWidth}x${window.innerHeight}`);
15620
15546
  }
15621
15547
  }
15622
- const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
15623
- const isLowEndDevice = navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 4;
15624
- // 进一步降低质量以减少 base64 大小
15625
- // 桌面设备:使用配置的质量(默认 0.3)
15626
- // 移动设备/低端设备:进一步降低到 0.2(最低)
15627
- const finalQuality = isMobile || isLowEndDevice
15628
- ? Math.max(this.options.quality * 0.65, 0.2) // 移动设备:质量 * 0.65,最低 0.2
15629
- : this.options.quality; // 桌面设备:使用配置的质量(默认 0.3)
15630
- // 计算压缩后的尺寸(对所有元素都应用,包括 document.body)
15631
- // 这样可以避免生成过大的截图,减少 base64 大小
15632
- const { width, height } = this.calculateCompressedSize(elementWidth, elementHeight, this.options.maxWidth, this.options.maxHeight);
15633
- // 使用计算后的压缩尺寸(确保不超过 maxWidth/maxHeight)
15634
- // 对于 body 元素,已经使用了 window.innerWidth/innerHeight,所以直接使用压缩尺寸即可
15635
- const finalWidth = width;
15636
- const finalHeight = height;
15637
- // 处理跨域图片的函数
15548
+ // 判断是否使用代理
15549
+ const shouldUseProxy = this.options.useProxy && this.options.proxyUrl && this.options.proxyUrl.trim() !== '';
15550
+ // 处理跨域图片的函数(仅在配置了代理时使用)
15551
+ // 注意:这个函数只在 shouldUseProxy 为 true 时才会被传递给 modern-screenshot
15638
15552
  const handleCrossOriginImage = async (url) => {
15639
15553
  // 如果是 data URL 或 blob URL,直接返回
15640
15554
  if (url.startsWith('data:') || url.startsWith('blob:')) {
@@ -15650,76 +15564,68 @@ class ScreenshotManager {
15650
15564
  catch (e) {
15651
15565
  // URL 解析失败,继续处理
15652
15566
  }
15653
- // 如果配置了代理服务器,使用代理处理跨域图片
15654
- // 只有当 useProxy 为 true 且 proxyUrl 存在时才使用代理
15655
- const shouldUseProxy = this.options.useProxy && this.options.proxyUrl && this.options.proxyUrl.trim() !== '';
15656
- if (shouldUseProxy) {
15657
- // 检查内存缓存(优先使用缓存,带过期时间检查)
15658
- const cachedDataUrl = this.getCachedImage(url);
15659
- if (cachedDataUrl) {
15660
- if (!this.options.silentMode) {
15661
- console.log(`📸 ✅ 使用内存缓存图片: ${url.substring(0, 50)}...`);
15662
- }
15663
- return cachedDataUrl;
15567
+ // 使用代理处理跨域图片
15568
+ // 检查内存缓存(优先使用缓存,带过期时间检查)
15569
+ const cachedDataUrl = this.getCachedImage(url);
15570
+ if (cachedDataUrl) {
15571
+ if (!this.options.silentMode) {
15572
+ console.log(`📸 使用内存缓存图片: ${url.substring(0, 50)}...`);
15664
15573
  }
15574
+ return cachedDataUrl;
15575
+ }
15576
+ try {
15577
+ // 构建代理请求参数
15578
+ const params = new URLSearchParams({
15579
+ url: url,
15580
+ maxWidth: String(this.options.maxWidth || 1600),
15581
+ maxHeight: String(this.options.maxHeight || 900),
15582
+ quality: String(Math.round((this.options.quality || 0.4) * 100)),
15583
+ format: this.options.outputFormat || 'webp'
15584
+ });
15585
+ let baseUrl = this.options.proxyUrl;
15586
+ baseUrl = baseUrl.replace(/[?&]$/, '');
15587
+ const proxyUrl = `${baseUrl}?${params.toString()}`;
15588
+ // 请求代理服务器(优化:添加超时控制和优先级)
15589
+ const controller = new AbortController();
15590
+ const timeoutId = setTimeout(() => controller.abort(), this.options.imageLoadTimeout);
15665
15591
  try {
15666
- // 构建代理请求参数
15667
- const params = new URLSearchParams({
15668
- url: url,
15669
- maxWidth: String(this.options.maxWidth || 1600),
15670
- maxHeight: String(this.options.maxHeight || 900),
15671
- quality: String(Math.round((this.options.quality || 0.4) * 100)),
15672
- format: this.options.outputFormat || 'webp'
15673
- });
15674
- let baseUrl = this.options.proxyUrl;
15675
- baseUrl = baseUrl.replace(/[?&]$/, '');
15676
- const proxyUrl = `${baseUrl}?${params.toString()}`;
15677
- // 请求代理服务器(优化:添加超时控制和优先级)
15678
- const controller = new AbortController();
15679
- const timeoutId = setTimeout(() => controller.abort(), this.options.imageLoadTimeout);
15680
- try {
15681
- const fetchOptions = {
15682
- method: 'GET',
15683
- mode: 'cors',
15684
- credentials: 'omit',
15685
- headers: {
15686
- 'Accept': 'image/*'
15687
- },
15688
- cache: 'no-cache',
15689
- signal: controller.signal
15690
- };
15691
- // 添加 fetch priority(如果支持)
15692
- if ('priority' in fetchOptions) {
15693
- fetchOptions.priority = this.options.fetchPriority;
15694
- }
15695
- const response = await fetch(proxyUrl, fetchOptions);
15696
- clearTimeout(timeoutId);
15697
- if (!response.ok) {
15698
- throw new Error(`代理请求失败: ${response.status}`);
15699
- }
15700
- const blob = await response.blob();
15701
- const dataUrl = await this.blobToDataUrl(blob);
15702
- // 缓存结果(带时间戳,10分钟有效)
15703
- this.setCachedImage(url, dataUrl);
15704
- return dataUrl;
15592
+ const fetchOptions = {
15593
+ method: 'GET',
15594
+ mode: 'cors',
15595
+ credentials: 'omit',
15596
+ headers: {
15597
+ 'Accept': 'image/*'
15598
+ },
15599
+ cache: 'no-cache',
15600
+ signal: controller.signal
15601
+ };
15602
+ // 添加 fetch priority(如果支持)
15603
+ if ('priority' in fetchOptions) {
15604
+ fetchOptions.priority = this.options.fetchPriority;
15705
15605
  }
15706
- catch (fetchError) {
15707
- clearTimeout(timeoutId);
15708
- throw fetchError;
15606
+ const response = await fetch(proxyUrl, fetchOptions);
15607
+ clearTimeout(timeoutId);
15608
+ if (!response.ok) {
15609
+ throw new Error(`代理请求失败: ${response.status}`);
15709
15610
  }
15611
+ const blob = await response.blob();
15612
+ const dataUrl = await this.blobToDataUrl(blob);
15613
+ // 缓存结果(带时间戳,10分钟有效)
15614
+ this.setCachedImage(url, dataUrl);
15615
+ return dataUrl;
15710
15616
  }
15711
- catch (error) {
15712
- if (!this.options.silentMode) {
15713
- console.warn(`📸 代理处理图片失败: ${url.substring(0, 100)}...`, error);
15714
- }
15715
- // 失败时返回原 URL
15716
- return url;
15617
+ catch (fetchError) {
15618
+ clearTimeout(timeoutId);
15619
+ throw fetchError;
15620
+ }
15621
+ }
15622
+ catch (error) {
15623
+ if (!this.options.silentMode) {
15624
+ console.warn(`📸 代理处理图片失败: ${url.substring(0, 100)}...`, error);
15717
15625
  }
15626
+ // 失败时返回原 URL,让 modern-screenshot 自己处理
15627
+ return url;
15718
15628
  }
15719
- // 如果没有配置代理,直接返回原 URL,让 modern-screenshot 自己处理
15720
- // 不再主动下载图片,避免阻塞和耗时过长
15721
- // 如果 modern-screenshot 无法处理跨域图片,建议配置代理服务器
15722
- return url;
15723
15629
  };
15724
15630
  // 检查元素是否可见且有尺寸
15725
15631
  const rect = element.getBoundingClientRect();
@@ -15737,6 +15643,8 @@ class ScreenshotManager {
15737
15643
  else {
15738
15644
  // 自动计算 workerNumber
15739
15645
  const cpuCores = navigator.hardwareConcurrency || 4; // 默认假设 4 核
15646
+ const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
15647
+ const isLowEndDevice = navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 4;
15740
15648
  if (isMobile || isLowEndDevice) {
15741
15649
  // 移动设备/低端设备:使用 1 个 Worker(避免内存压力)
15742
15650
  workerNumber = 1;
@@ -15758,279 +15666,86 @@ class ScreenshotManager {
15758
15666
  }
15759
15667
  // 限制 workerNumber 范围:1-8(避免过多 Worker 导致资源竞争)
15760
15668
  workerNumber = Math.max(1, Math.min(8, workerNumber));
15761
- // 构建 createContext 配置
15762
- // 参考: https://github.com/qq15725/modern-screenshot/blob/main/src/options.ts
15763
- const contextOptions = {
15764
- workerNumber, // Worker 数量,> 0 启用 Worker 模式
15765
- quality: finalQuality, // 图片质量(0-1),已优化为更低的值以减少 base64 大小
15766
- fetchFn: handleCrossOriginImage, // 使用代理服务器处理跨域图片
15767
- fetch: {
15768
- requestInit: {
15769
- cache: 'no-cache',
15770
- },
15771
- bypassingCache: true,
15772
- },
15773
- // 设置最大 canvas 尺寸,防止生成过大的 canvas(避免内存问题)
15774
- // 参考: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
15775
- // 大多数浏览器限制为 16,777,216 像素(4096x4096),这里设置为更保守的值
15776
- maximumCanvasSize: 16777216, // 16M 像素(约 4096x4096)
15777
- // 使用 modern-screenshot 内置的 timeout(更可靠)
15778
- timeout: Math.max(this.options.interval * 6, 5000),
15779
- };
15780
- // 限制 timeout 最多 15 秒
15781
- contextOptions.timeout = Math.min(contextOptions.timeout, 15000);
15782
- // 如果用户指定了 workerUrl,使用指定的 URL
15783
- // 否则让 modern-screenshot 自动处理(它会尝试从 node_modules 或 CDN 加载)
15784
- // 注意:在某些构建工具(如 Rollup)中,可能需要手动指定 workerUrl
15785
- if (this.options.workerUrl) {
15786
- contextOptions.workerUrl = this.options.workerUrl;
15669
+ // 按照 demo 的方式:只在 context 不存在时创建,之后一直复用
15670
+ // 不进行复杂的检测和重新创建逻辑
15671
+ if (!this.screenshotContext) {
15787
15672
  if (!this.options.silentMode) {
15788
- console.log(`📸 使用指定的 Worker URL: ${this.options.workerUrl}`);
15673
+ console.log(`📸 创建截图 Worker 上下文...`);
15674
+ console.log(`📸 Worker 模式: ${workerNumber} 个 Worker`);
15789
15675
  }
15790
- }
15791
- else {
15792
- // 未指定 workerUrl 时,modern-screenshot 会自动处理
15793
- // 但在某些构建环境中可能需要手动指定,可以使用 CDN 作为后备
15794
- // 这里不设置 workerUrl,让 modern-screenshot 自己处理
15795
- if (!this.options.silentMode) {
15796
- console.log('📸 Worker URL 未指定,modern-screenshot 将自动处理');
15797
- }
15798
- }
15799
- // 对所有元素都设置尺寸限制(包括 document.body),避免截图过大
15800
- // 这样可以减少 base64 大小,提高性能
15801
- if (finalWidth && finalHeight) {
15802
- contextOptions.width = finalWidth;
15803
- contextOptions.height = finalHeight;
15804
- if (!this.options.silentMode) {
15805
- if (element === document.body || element === document.documentElement) {
15806
- console.log(`📸 截取完整页面(document.body),使用压缩尺寸: ${finalWidth}x${finalHeight}`);
15807
- }
15808
- else {
15809
- console.log(`📸 使用压缩尺寸: ${finalWidth}x${finalHeight}`);
15810
- }
15811
- }
15812
- }
15813
- else {
15814
- if (!this.options.silentMode) {
15815
- console.log(`📸 使用元素实际尺寸: ${elementWidth}x${elementHeight}`);
15816
- }
15817
- }
15818
- // 缩放配置:使用外部传递的参数
15819
- // scale < 1 会降低图片分辨率,减少 base64 大小
15820
- if (this.options.scale !== undefined && this.options.scale !== 1) {
15821
- contextOptions.scale = this.options.scale;
15822
- }
15823
- // 优化:复用 context,避免频繁创建和销毁(性能提升 20%+)
15824
- // 只在元素变化、配置变化或内容变化时重新创建 context
15825
- // 1. 计算配置哈希
15826
- const contextOptionsHash = JSON.stringify({
15827
- workerNumber,
15828
- quality: finalQuality,
15829
- scale: contextOptions.scale,
15830
- width: contextOptions.width,
15831
- height: contextOptions.height,
15832
- maximumCanvasSize: contextOptions.maximumCanvasSize,
15833
- timeout: contextOptions.timeout
15834
- });
15835
- // 2. 计算 DOM 内容哈希(检测内容变化)
15836
- // 通过检测图片 URL、文本内容等来判断内容是否变化
15837
- // 注意:modern-screenshot 的 context 在创建时会"快照" DOM 状态
15838
- // 如果 DOM 内容变化了,必须重新创建 context 才能捕获最新内容
15839
- const contentHash = this.calculateContentHash(element);
15840
- // 3. 检查 context 是否过期(超过最大存活时间)
15841
- // 缩短过期时间,确保频繁变化的内容能及时更新
15842
- const now = Date.now();
15843
- const isContextExpired = this.contextLastUpdateTime > 0 &&
15844
- (now - this.contextLastUpdateTime) > this.contextMaxAge;
15845
- // 4. 判断是否需要重新创建 context
15846
- // 关键:如果内容哈希变化,必须重新创建 context(modern-screenshot 的限制)
15847
- const needsRecreateContext = !this.screenshotContext ||
15848
- this.contextElement !== element ||
15849
- this.contextOptionsHash !== contextOptionsHash ||
15850
- this.contextContentHash !== contentHash || // 内容变化时强制重新创建
15851
- isContextExpired;
15852
- if (needsRecreateContext) {
15853
- if (!this.options.silentMode) {
15854
- if (this.screenshotContext) {
15855
- let reason = '检测到';
15856
- if (this.contextElement !== element)
15857
- reason += '元素变化';
15858
- if (this.contextOptionsHash !== contextOptionsHash)
15859
- reason += '配置变化';
15860
- if (this.contextContentHash !== contentHash)
15861
- reason += '内容变化';
15862
- if (isContextExpired)
15863
- reason += 'context 过期';
15864
- console.log(`📸 ${reason},重新创建 context...`);
15865
- }
15866
- else {
15867
- console.log(`📸 Worker 模式: ${workerNumber} 个 Worker,质量: ${finalQuality.toFixed(2)},缩放: ${contextOptions.scale || 1}`);
15676
+ // 简化 createContext 配置,只传必要的参数(和 demo 一致)
15677
+ const simpleContextOptions = {
15678
+ workerNumber, // Worker 数量
15679
+ // 只有在配置了代理时才传递 fetchFn
15680
+ ...(shouldUseProxy ? {
15681
+ fetchFn: handleCrossOriginImage,
15682
+ fetch: {
15683
+ requestInit: {
15684
+ cache: 'no-cache',
15685
+ },
15686
+ bypassingCache: true,
15687
+ },
15688
+ } : {}),
15689
+ };
15690
+ // 如果用户指定了 workerUrl,使用指定的 URL
15691
+ if (this.options.workerUrl) {
15692
+ simpleContextOptions.workerUrl = this.options.workerUrl;
15693
+ if (!this.options.silentMode) {
15694
+ console.log(`📸 使用指定的 Worker URL: ${this.options.workerUrl}`);
15868
15695
  }
15869
15696
  }
15870
- // 销毁旧 context
15871
- if (this.screenshotContext) {
15872
- try {
15873
- destroyContext(this.screenshotContext);
15874
- }
15875
- catch (e) {
15876
- // 忽略清理错误
15697
+ else {
15698
+ if (!this.options.silentMode) {
15699
+ console.log('📸 Worker URL 未指定,modern-screenshot 将自动处理');
15877
15700
  }
15878
- this.screenshotContext = null;
15879
- }
15880
- // 添加 progress 回调(可选,用于显示进度)
15881
- if (!this.options.silentMode) {
15882
- contextOptions.progress = (current, total) => {
15883
- if (total > 0) {
15884
- const percent = Math.round((current / total) * 100);
15885
- if (percent % 25 === 0 || current === total) { // 每 25% 或完成时打印
15886
- console.log(`📸 截图进度: ${current}/${total} (${percent}%)`);
15887
- }
15888
- }
15889
- };
15890
15701
  }
15891
- // 添加重试机制创建新 context
15892
- let retries = 0;
15893
- const maxRetries = this.options.maxRetries || 2;
15894
- while (retries <= maxRetries) {
15895
- try {
15896
- // 等待图片加载完成(确保内容是最新的)
15897
- await this.waitForImagesToLoad(element);
15898
- // 等待 DOM 更新完成(确保内容渲染完成)
15899
- // 使用双重 requestAnimationFrame + setTimeout 确保内容完全渲染
15900
- await new Promise(resolve => {
15901
- requestAnimationFrame(() => {
15902
- requestAnimationFrame(() => {
15903
- // 根据截图间隔调整等待时间:频繁截图时等待更久
15904
- const waitTime = this.options.interval < 2000 ? 200 : 100;
15905
- setTimeout(resolve, waitTime);
15906
- });
15907
- });
15908
- });
15909
- // 创建 context 前,再次检查内容是否变化(防止在等待期间内容又变化了)
15910
- const latestContentHash = this.calculateContentHash(element);
15911
- if (latestContentHash !== contentHash) {
15912
- if (!this.options.silentMode) {
15913
- console.log('📸 等待期间内容发生变化,更新内容哈希');
15914
- }
15915
- // 更新 contentHash,但继续使用新的 context
15916
- // 这样下次截图时会检测到变化
15917
- }
15918
- this.screenshotContext = await createContext$1(element, contextOptions);
15919
- this.contextElement = element;
15920
- this.contextOptionsHash = contextOptionsHash;
15921
- this.contextContentHash = contentHash;
15922
- this.contextLastUpdateTime = now;
15923
- break;
15924
- }
15925
- catch (error) {
15926
- if (retries === maxRetries) {
15927
- throw new Error(`创建截图上下文失败(已重试 ${maxRetries} 次): ${error instanceof Error ? error.message : String(error)}`);
15928
- }
15929
- retries++;
15930
- const delay = 1000 * retries; // 递增延迟:1秒、2秒...
15931
- if (!this.options.silentMode) {
15932
- console.warn(`📸 ⚠️ 创建截图上下文失败,${delay}ms 后重试 (${retries}/${maxRetries})...`);
15933
- }
15934
- await new Promise(resolve => setTimeout(resolve, delay));
15702
+ try {
15703
+ this.screenshotContext = await createContext$1(element, simpleContextOptions);
15704
+ if (!this.options.silentMode) {
15705
+ console.log('📸 Worker 上下文创建成功');
15935
15706
  }
15936
15707
  }
15937
- }
15938
- else {
15939
- if (!this.options.silentMode) {
15940
- console.log('📸 复用现有 context(性能优化)');
15941
- }
15942
- // ⚠️ 重要:modern-screenshot 的 context 在创建时会"快照" DOM 状态
15943
- // 如果 DOM 内容在 context 创建后发生了变化,复用 context 会捕获到旧内容
15944
- // 因此,我们需要在每次截图前再次检查内容是否变化
15945
- // 再次计算内容哈希,检查是否在复用期间内容又变化了
15946
- const latestContentHash = this.calculateContentHash(element);
15947
- if (latestContentHash !== this.contextContentHash) {
15948
- // 内容在复用期间又变化了,必须重新创建 context
15708
+ catch (error) {
15949
15709
  if (!this.options.silentMode) {
15950
- console.log('📸 ⚠️ 复用期间检测到内容变化,强制重新创建 context');
15951
- }
15952
- // 销毁旧 context
15953
- if (this.screenshotContext) {
15954
- try {
15955
- destroyContext(this.screenshotContext);
15956
- }
15957
- catch (e) {
15958
- // 忽略清理错误
15959
- }
15960
- this.screenshotContext = null;
15961
- }
15962
- // 等待图片加载和 DOM 更新
15963
- await this.waitForImagesToLoad(element);
15964
- await new Promise(resolve => {
15965
- requestAnimationFrame(() => {
15966
- requestAnimationFrame(() => {
15967
- const waitTime = this.options.interval < 2000 ? 200 : 100;
15968
- setTimeout(resolve, waitTime);
15969
- });
15970
- });
15971
- });
15972
- // 重新创建 context
15973
- let retries = 0;
15974
- const maxRetries = this.options.maxRetries || 2;
15975
- while (retries <= maxRetries) {
15976
- try {
15977
- this.screenshotContext = await createContext$1(element, contextOptions);
15978
- this.contextElement = element;
15979
- this.contextOptionsHash = contextOptionsHash;
15980
- this.contextContentHash = latestContentHash;
15981
- this.contextLastUpdateTime = Date.now();
15982
- break;
15983
- }
15984
- catch (error) {
15985
- if (retries === maxRetries) {
15986
- throw new Error(`重新创建截图上下文失败(已重试 ${maxRetries} 次): ${error instanceof Error ? error.message : String(error)}`);
15987
- }
15988
- retries++;
15989
- const delay = 1000 * retries;
15990
- if (!this.options.silentMode) {
15991
- console.warn(`📸 ⚠️ 重新创建截图上下文失败,${delay}ms 后重试 (${retries}/${maxRetries})...`);
15992
- }
15993
- await new Promise(resolve => setTimeout(resolve, delay));
15994
- }
15710
+ console.error('📸 创建 Worker 上下文失败:', error);
15995
15711
  }
15996
- }
15997
- else {
15998
- // 内容没有变化,可以安全复用 context
15999
- // 但还是要等待图片加载完成,确保内容是最新的
16000
- await this.waitForImagesToLoad(element);
16001
- // 等待 DOM 更新完成
16002
- await new Promise(resolve => {
16003
- requestAnimationFrame(() => {
16004
- requestAnimationFrame(() => {
16005
- setTimeout(resolve, 100); // 额外等待 100ms,确保内容完全渲染
16006
- });
16007
- });
16008
- });
15712
+ throw error;
16009
15713
  }
16010
15714
  }
16011
15715
  try {
16012
- // 根据输出格式选择对应的 API,避免格式转换(性能优化)
16013
- // 注意:timeout 已经在 createContext 时设置,modern-screenshot 内部会处理超时
15716
+ // 按照 demo 的方式:使用 domToWebp 时传递配置参数
16014
15717
  let dataUrl;
16015
15718
  const outputFormat = this.options.outputFormat || 'webp';
16016
15719
  if (!this.options.silentMode) {
16017
15720
  console.log(`📸 使用 ${outputFormat.toUpperCase()} 格式截图(直接输出,无需转换)...`);
16018
15721
  }
15722
+ // 构建 domToWebp/domToJpeg/domToPng 的配置参数(和 demo 一致)
15723
+ const screenshotOptions = {
15724
+ scale: this.options.scale ?? 0.7, // 使用外部参数,默认 0.7(和 demo 一致)
15725
+ backgroundColor: this.options.backgroundColor ?? '#ffffff', // 默认白色背景
15726
+ type: `image/${outputFormat}`, // 使用配置的输出格式
15727
+ quality: this.options.quality ?? 0.6, // 使用外部参数,默认 0.6(和 demo 一致)
15728
+ drawImageInterval: this.options.drawImageInterval ?? 20, // 默认 20ms(和 demo 一致)
15729
+ features: {
15730
+ copyScrollbar: false,
15731
+ removeAbnormalAttributes: true,
15732
+ removeControlCharacter: true,
15733
+ fixSvgXmlDecode: true,
15734
+ restoreScrollPosition: false,
15735
+ },
15736
+ timeout: 10000, // 10秒超时(和 demo 一致)
15737
+ };
16019
15738
  // 尝试使用 Worker 模式(context)
16020
15739
  try {
16021
- // 根据输出格式选择对应的 API
16022
- // modern-screenshot 内部已经处理了超时,不需要额外的 Promise.race
15740
+ // 根据输出格式选择对应的 API,传递配置参数(和 demo 一致)
16023
15741
  if (outputFormat === 'webp') {
16024
- // 使用 domToWebp,直接输出 WebP 格式,无需转换
16025
- dataUrl = await domToWebp(this.screenshotContext);
15742
+ dataUrl = await domToWebp(this.screenshotContext, screenshotOptions);
16026
15743
  }
16027
15744
  else if (outputFormat === 'jpeg') {
16028
- // 使用 domToJpeg,直接输出 JPEG 格式,无需转换
16029
- dataUrl = await domToJpeg(this.screenshotContext);
15745
+ dataUrl = await domToJpeg(this.screenshotContext, screenshotOptions);
16030
15746
  }
16031
15747
  else {
16032
- // 默认使用 domToPng
16033
- dataUrl = await domToPng(this.screenshotContext);
15748
+ dataUrl = await domToPng(this.screenshotContext, screenshotOptions);
16034
15749
  }
16035
15750
  // 验证截图结果
16036
15751
  if (!dataUrl || dataUrl.length < 100) {
@@ -16042,7 +15757,7 @@ class ScreenshotManager {
16042
15757
  return dataUrl;
16043
15758
  }
16044
15759
  catch (workerError) {
16045
- // Worker 模式失败,回退到普通模式(参考用户代码)
15760
+ // Worker 模式失败,回退到普通模式(和 demo 一致)
16046
15761
  if (!this.options.silentMode) {
16047
15762
  console.warn('📸 Worker 模式失败,回退到普通模式:', workerError);
16048
15763
  }
@@ -16056,14 +15771,14 @@ class ScreenshotManager {
16056
15771
  }
16057
15772
  this.screenshotContext = null;
16058
15773
  }
16059
- // 回退到普通模式(直接使用 domToWebp,不传 context)
16060
- // 使用外部传递的参数,并给出合理的默认值
15774
+ // 回退到普通模式(直接使用 element,不传 context)
15775
+ // 使用更低的参数(和 demo 一致)
16061
15776
  const fallbackOptions = {
16062
- scale: this.options.scale ?? 1, // 使用外部参数,默认 1
16063
- backgroundColor: '#ffffff', // 默认白色背景
16064
- type: `image/${outputFormat}`, // 使用配置的输出格式
16065
- quality: this.options.quality ?? 0.8, // 使用外部参数,默认 0.4
16066
- drawImageInterval: 20, // 默认 20ms,减少主线程阻塞
15777
+ scale: 0.3, // 回退模式使用更低的 scale(和 demo 一致)
15778
+ backgroundColor: this.options.backgroundColor ?? '#ffffff',
15779
+ type: `image/${outputFormat}`,
15780
+ quality: 0.3, // 回退模式使用更低的质量(和 demo 一致)
15781
+ drawImageInterval: this.options.drawImageInterval ?? 20,
16067
15782
  features: {
16068
15783
  copyScrollbar: false,
16069
15784
  removeAbnormalAttributes: true,
@@ -16071,10 +15786,8 @@ class ScreenshotManager {
16071
15786
  fixSvgXmlDecode: true,
16072
15787
  restoreScrollPosition: false,
16073
15788
  },
16074
- timeout: Math.max((this.options.interval ?? 5000) * 6, 10000), // 使用外部参数计算超时,默认 10
15789
+ timeout: 10000, // 10秒超时(和 demo 一致)
16075
15790
  };
16076
- // 限制 timeout 最多 15 秒
16077
- fallbackOptions.timeout = Math.min(fallbackOptions.timeout, 15000);
16078
15791
  if (outputFormat === 'webp') {
16079
15792
  dataUrl = await domToWebp(element, fallbackOptions);
16080
15793
  }
@@ -17421,10 +17134,6 @@ class ScreenshotManager {
17421
17134
  // 忽略清理错误
17422
17135
  }
17423
17136
  this.screenshotContext = null;
17424
- this.contextElement = null;
17425
- this.contextOptionsHash = '';
17426
- this.contextContentHash = '';
17427
- this.contextLastUpdateTime = 0;
17428
17137
  }
17429
17138
  if (!this.options.silentMode) {
17430
17139
  console.log(`📸 截图引擎已更新: ${oldEngine} → ${newEngine}`);
@@ -17448,10 +17157,6 @@ class ScreenshotManager {
17448
17157
  // 忽略清理错误
17449
17158
  }
17450
17159
  this.screenshotContext = null;
17451
- this.contextElement = null;
17452
- this.contextOptionsHash = '';
17453
- this.contextContentHash = '';
17454
- this.contextLastUpdateTime = 0;
17455
17160
  }
17456
17161
  this.stopScreenshot();
17457
17162
  if (this.worker) {