customer-chat-sdk 1.0.33 → 1.0.35

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,3 +1,5 @@
1
+ import html2canvas from 'html2canvas';
2
+
1
3
  // 直接使用base64字符串,避免打包后路径问题
2
4
  const iconImage = 'data:image/png;base64,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';
3
5
  class IconManager {
@@ -6503,13 +6505,17 @@ class ScreenshotManager {
6503
6505
  this.visibleElementsCache = new Set();
6504
6506
  // 预连接状态
6505
6507
  this.preconnected = false;
6508
+ // 图片下载队列和并发控制(防止频繁截图时重复下载)
6509
+ this.imageDownloadQueue = new Map(); // URL -> Promise,避免重复下载
6510
+ this.activeDownloads = new Set(); // 正在下载的 URL
6511
+ this.maxConcurrentImageDownloads = 5; // 最大并发下载数(降低,避免内存问题)
6506
6512
  // 全局错误处理器
6507
6513
  this.globalErrorHandler = null;
6508
6514
  this.globalRejectionHandler = null;
6509
6515
  this.targetElement = targetElement;
6510
6516
  this.options = {
6511
6517
  interval: options.interval ?? 5000,
6512
- quality: options.quality ?? 0.4,
6518
+ quality: options.quality ?? 0.3, // 降低默认质量:0.4 -> 0.3,减少 base64 大小
6513
6519
  scale: options.scale ?? 1,
6514
6520
  maxHistory: options.maxHistory ?? 10,
6515
6521
  compress: options.compress ?? false,
@@ -6533,7 +6539,9 @@ class ScreenshotManager {
6533
6539
  useIntersectionObserver: options.useIntersectionObserver ?? true, // 默认使用 Intersection Observer
6534
6540
  fetchPriority: options.fetchPriority ?? 'high', // 默认高优先级
6535
6541
  maxCacheSize: options.maxCacheSize ?? 50, // 默认最大50MB
6536
- maxCacheAge: options.maxCacheAge ?? 86400000 // 默认24小时(86400000ms)
6542
+ maxCacheAge: options.maxCacheAge ?? 86400000, // 默认24小时(86400000ms)
6543
+ maxImageSize: options.maxImageSize ?? 5, // 不使用代理时,单个图片最大尺寸(MB),默认5MB
6544
+ skipLargeImages: options.skipLargeImages ?? true // 不使用代理时,是否跳过过大的图片,默认true(跳过)
6537
6545
  };
6538
6546
  this.setupMessageListener();
6539
6547
  this.setupVisibilityChangeListener();
@@ -6894,6 +6902,9 @@ class ScreenshotManager {
6894
6902
  if (selectedEngine === 'snapdom') {
6895
6903
  dataUrl = await this.takeScreenshotWithSnapdom(this.targetElement);
6896
6904
  }
6905
+ else if (selectedEngine === 'html2canvas') {
6906
+ dataUrl = await this.takeScreenshotWithHtml2Canvas(this.targetElement);
6907
+ }
6897
6908
  else {
6898
6909
  // 默认使用 modern-screenshot
6899
6910
  dataUrl = await this.takeScreenshotWithModernScreenshot(this.targetElement);
@@ -7056,8 +7067,173 @@ class ScreenshotManager {
7056
7067
  throw error;
7057
7068
  }
7058
7069
  }
7070
+ /**
7071
+ * 使用 html2canvas 截图
7072
+ *
7073
+ * 优势:
7074
+ * - 处理 SVG 和本地资源更快(不需要复杂的 Worker 通信)
7075
+ * - 兼容性好,支持更多 CSS 特性
7076
+ * - 跨域处理相对简单
7077
+ *
7078
+ * 劣势:
7079
+ * - 在主线程执行,可能阻塞 UI(但处理速度快,影响较小)
7080
+ * - 不支持 Worker 模式
7081
+ *
7082
+ * 适用场景:
7083
+ * - 页面包含大量 SVG 图标
7084
+ * - 本地资源较多
7085
+ * - 需要快速截图
7086
+ */
7087
+ async takeScreenshotWithHtml2Canvas(element) {
7088
+ if (!this.options.silentMode) {
7089
+ console.log('📸 使用 html2canvas 引擎截图...');
7090
+ }
7091
+ try {
7092
+ // 检查元素是否存在和可见
7093
+ const rect = element.getBoundingClientRect();
7094
+ if (rect.width === 0 || rect.height === 0) {
7095
+ throw new Error('元素尺寸为 0,无法截图');
7096
+ }
7097
+ const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
7098
+ const isLowEndDevice = navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 4;
7099
+ // 计算压缩后的尺寸
7100
+ let elementWidth = element.scrollWidth || element.clientWidth || element.offsetWidth;
7101
+ let elementHeight = element.scrollHeight || element.clientHeight || element.offsetHeight;
7102
+ if (element === document.body || element === document.documentElement) {
7103
+ elementWidth = Math.max(element.scrollWidth, element.offsetWidth, document.documentElement.scrollWidth, document.documentElement.offsetWidth, window.innerWidth);
7104
+ elementHeight = Math.max(element.scrollHeight, element.offsetHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight, window.innerHeight);
7105
+ }
7106
+ const { width, height } = this.calculateCompressedSize(elementWidth, elementHeight, this.options.maxWidth, this.options.maxHeight);
7107
+ const finalWidth = width < elementWidth ? width : Math.min(elementWidth, this.options.maxWidth);
7108
+ const finalHeight = height < elementHeight ? height : Math.min(elementHeight, this.options.maxHeight);
7109
+ // html2canvas 质量设置(0-1)
7110
+ const finalQuality = isMobile || isLowEndDevice
7111
+ ? Math.max(this.options.quality * 0.65, 0.2)
7112
+ : this.options.quality;
7113
+ // html2canvas 配置选项
7114
+ const options = {
7115
+ // 基本配置
7116
+ backgroundColor: '#ffffff',
7117
+ scale: this.options.scale !== 1 ? (isMobile ? 0.7 : this.options.scale) : (isMobile ? 0.7 : 1),
7118
+ useCORS: this.options.enableCORS,
7119
+ allowTaint: !this.options.enableCORS, // 如果启用 CORS,不允许 taint
7120
+ logging: !this.options.silentMode,
7121
+ width: finalWidth,
7122
+ height: finalHeight,
7123
+ // 性能优化
7124
+ removeContainer: true, // 截图后移除临时容器
7125
+ imageTimeout: this.options.imageLoadTimeout || 5000,
7126
+ // 忽略某些元素(可选,提升性能)
7127
+ ignoreElements: (element) => {
7128
+ // 忽略隐藏元素
7129
+ const style = window.getComputedStyle(element);
7130
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
7131
+ return true;
7132
+ }
7133
+ return false;
7134
+ },
7135
+ };
7136
+ // html2canvas 不支持直接的 proxy 选项,需要通过 onclone 钩子处理图片
7137
+ // 如果配置了代理服务器,在克隆时替换图片 URL
7138
+ if (this.options.useProxy && this.options.proxyUrl && this.options.proxyUrl.trim() !== '') {
7139
+ options.onclone = (clonedDoc) => {
7140
+ // 在克隆的文档中,替换所有跨域图片的 src
7141
+ const images = clonedDoc.querySelectorAll('img');
7142
+ images.forEach((img) => {
7143
+ const originalSrc = img.getAttribute('src');
7144
+ if (!originalSrc)
7145
+ return;
7146
+ // 检查是否是跨域图片
7147
+ try {
7148
+ const imgUrl = new URL(originalSrc, window.location.href);
7149
+ if (imgUrl.origin === window.location.origin) {
7150
+ return; // 同源图片,不需要处理
7151
+ }
7152
+ }
7153
+ catch {
7154
+ // URL 解析失败,可能是相对路径,继续处理
7155
+ }
7156
+ // 检查缓存
7157
+ const cachedDataUrl = this.getCachedImage(originalSrc);
7158
+ if (cachedDataUrl) {
7159
+ img.src = cachedDataUrl;
7160
+ return;
7161
+ }
7162
+ // 对于跨域图片,使用代理 URL
7163
+ // html2canvas 会自动处理,但我们可以预先处理
7164
+ // 注意:html2canvas 会自己处理图片加载,这里主要是为了缓存
7165
+ });
7166
+ };
7167
+ }
7168
+ if (!this.options.silentMode) {
7169
+ console.log(`📸 html2canvas 配置: 尺寸 ${finalWidth}x${finalHeight}, 质量 ${finalQuality.toFixed(2)}, 缩放 ${options.scale}`);
7170
+ }
7171
+ // 执行截图
7172
+ const canvas = await html2canvas(element, options);
7173
+ // 根据输出格式转换
7174
+ let mimeType = 'image/png';
7175
+ let finalQualityForExport = undefined;
7176
+ if (this.options.outputFormat === 'webp' && !isMobile) {
7177
+ try {
7178
+ const testCanvas = document.createElement('canvas');
7179
+ testCanvas.width = 1;
7180
+ testCanvas.height = 1;
7181
+ const testDataUrl = testCanvas.toDataURL('image/webp');
7182
+ if (testDataUrl.indexOf('webp') !== -1) {
7183
+ mimeType = 'image/webp';
7184
+ finalQualityForExport = finalQuality;
7185
+ }
7186
+ }
7187
+ catch {
7188
+ mimeType = 'image/jpeg';
7189
+ finalQualityForExport = finalQuality;
7190
+ }
7191
+ }
7192
+ else if (this.options.outputFormat === 'jpeg') {
7193
+ mimeType = 'image/jpeg';
7194
+ finalQualityForExport = finalQuality;
7195
+ }
7196
+ // 转换为 data URL
7197
+ const dataUrl = mimeType === 'image/png'
7198
+ ? canvas.toDataURL(mimeType)
7199
+ : canvas.toDataURL(mimeType, finalQualityForExport);
7200
+ // 验证结果
7201
+ if (!dataUrl || dataUrl.length < 100) {
7202
+ throw new Error('生成的截图数据无效或过短');
7203
+ }
7204
+ if (!this.options.silentMode) {
7205
+ console.log(`📸 html2canvas 截图成功!格式: ${this.options.outputFormat}, 尺寸: ${canvas.width}x${canvas.height}`);
7206
+ }
7207
+ return dataUrl;
7208
+ }
7209
+ catch (error) {
7210
+ const errorMessage = error instanceof Error ? error.message : String(error);
7211
+ if (!this.options.silentMode) {
7212
+ console.error('📸 html2canvas 截图失败:', errorMessage);
7213
+ if (errorMessage.includes('CORS') || errorMessage.includes('cross-origin')) {
7214
+ console.warn('📸 💡 建议:配置 proxyUrl 选项处理跨域图片');
7215
+ }
7216
+ }
7217
+ throw error;
7218
+ }
7219
+ }
7059
7220
  /**
7060
7221
  * 使用 modern-screenshot 截图(启用 Worker)
7222
+ *
7223
+ * 优势:
7224
+ * - 使用 Worker,不阻塞主线程 UI
7225
+ * - 支持并发处理
7226
+ * - 适合复杂页面
7227
+ *
7228
+ * 劣势:
7229
+ * - 处理 SVG 和本地资源较慢(Worker 通信开销)
7230
+ * - 配置相对复杂
7231
+ * - 需要处理 Worker URL
7232
+ *
7233
+ * 适用场景:
7234
+ * - 复杂页面,需要不阻塞 UI
7235
+ * - 需要高质量截图
7236
+ * - 页面资源较少
7061
7237
  */
7062
7238
  async takeScreenshotWithModernScreenshot(element) {
7063
7239
  if (!this.options.silentMode) {
@@ -7090,13 +7266,19 @@ class ScreenshotManager {
7090
7266
  }
7091
7267
  const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
7092
7268
  const isLowEndDevice = navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 4;
7093
- const mobileQuality = isMobile || isLowEndDevice ? Math.max(this.options.quality * 0.7, 0.3) : this.options.quality;
7094
- // 不限制宽度和高度,让 modern-screenshot 自动处理完整内容
7095
- // 只在需要压缩时才应用尺寸限制
7269
+ // 进一步降低质量以减少 base64 大小
7270
+ // 桌面设备:使用配置的质量(默认 0.3)
7271
+ // 移动设备/低端设备:进一步降低到 0.2(最低)
7272
+ const finalQuality = isMobile || isLowEndDevice
7273
+ ? Math.max(this.options.quality * 0.65, 0.2) // 移动设备:质量 * 0.65,最低 0.2
7274
+ : this.options.quality; // 桌面设备:使用配置的质量(默认 0.3)
7275
+ // 计算压缩后的尺寸(对所有元素都应用,包括 document.body)
7276
+ // 这样可以避免生成过大的截图,减少 base64 大小
7096
7277
  const { width, height } = this.calculateCompressedSize(elementWidth, elementHeight, this.options.maxWidth, this.options.maxHeight);
7097
- // 如果计算后的尺寸小于元素实际尺寸,说明需要压缩,否则使用元素实际尺寸
7098
- const finalWidth = width < elementWidth ? width : undefined;
7099
- const finalHeight = height < elementHeight ? height : undefined;
7278
+ // 对于所有元素都应用尺寸限制(包括 body),避免截图过大
7279
+ // 如果计算后的尺寸小于元素实际尺寸,使用压缩尺寸;否则使用元素实际尺寸(但不超过最大值)
7280
+ const finalWidth = width < elementWidth ? width : Math.min(elementWidth, this.options.maxWidth);
7281
+ const finalHeight = height < elementHeight ? height : Math.min(elementHeight, this.options.maxHeight);
7100
7282
  // 处理跨域图片的函数
7101
7283
  const handleCrossOriginImage = async (url) => {
7102
7284
  // 如果是 data URL 或 blob URL,直接返回
@@ -7195,9 +7377,64 @@ class ScreenshotManager {
7195
7377
  return url;
7196
7378
  }
7197
7379
  }
7198
- // 如果没有配置代理,尝试使用 CORS
7380
+ // 如果没有配置代理,需要添加内存保护机制
7381
+ // 不使用代理时,modern-screenshot 会直接下载图片,可能导致内存问题
7382
+ // 由于已配置 CORS,可以直接下载并检查大小
7199
7383
  if (this.options.enableCORS) {
7200
- return url;
7384
+ // 对于不使用代理的情况,添加内存保护和缓存机制:
7385
+ // 1. 先检查内存缓存(避免重复下载)
7386
+ // 2. 检查 IndexedDB 缓存
7387
+ // 3. 使用下载队列避免并发重复下载
7388
+ // 4. 下载时检查大小,如果过大则使用占位符
7389
+ // 先检查内存缓存(优先使用缓存,避免重复下载)
7390
+ const cachedDataUrl = this.getCachedImage(url);
7391
+ if (cachedDataUrl) {
7392
+ if (!this.options.silentMode) {
7393
+ console.log(`📸 ✅ 使用内存缓存图片(无代理模式): ${url.substring(0, 50)}...`);
7394
+ }
7395
+ return cachedDataUrl;
7396
+ }
7397
+ // 检查 IndexedDB 缓存(如果启用)
7398
+ if (this.options.useIndexedDB) {
7399
+ const indexedDBCache = await this.getIndexedDBCache(url);
7400
+ if (indexedDBCache) {
7401
+ // 同步到内存缓存
7402
+ this.setCachedImage(url, indexedDBCache);
7403
+ if (!this.options.silentMode) {
7404
+ console.log(`📸 ✅ 使用 IndexedDB 缓存图片(无代理模式): ${url.substring(0, 50)}...`);
7405
+ }
7406
+ return indexedDBCache;
7407
+ }
7408
+ }
7409
+ // 检查是否正在下载(避免重复下载)
7410
+ if (this.imageDownloadQueue.has(url)) {
7411
+ // 如果已经在下载队列中,等待现有下载完成
7412
+ if (!this.options.silentMode) {
7413
+ console.log(`📸 ⏳ 等待图片下载完成: ${url.substring(0, 50)}...`);
7414
+ }
7415
+ return await this.imageDownloadQueue.get(url);
7416
+ }
7417
+ // 检查并发下载数限制
7418
+ if (this.activeDownloads.size >= this.maxConcurrentImageDownloads) {
7419
+ // 并发数已满,返回原 URL,让 modern-screenshot 自己处理(可能会失败,但不阻塞)
7420
+ if (!this.options.silentMode) {
7421
+ console.warn(`📸 ⚠️ 并发下载数已满(${this.activeDownloads.size}/${this.maxConcurrentImageDownloads}),跳过: ${url.substring(0, 50)}...`);
7422
+ }
7423
+ return url;
7424
+ }
7425
+ // 创建下载 Promise 并加入队列
7426
+ const downloadPromise = this.downloadImageWithoutProxy(url);
7427
+ this.imageDownloadQueue.set(url, downloadPromise);
7428
+ this.activeDownloads.add(url);
7429
+ try {
7430
+ const result = await downloadPromise;
7431
+ return result;
7432
+ }
7433
+ finally {
7434
+ // 下载完成后清理
7435
+ this.imageDownloadQueue.delete(url);
7436
+ this.activeDownloads.delete(url);
7437
+ }
7201
7438
  }
7202
7439
  // 默认返回原 URL
7203
7440
  return url;
@@ -7218,11 +7455,14 @@ class ScreenshotManager {
7218
7455
  }
7219
7456
  this.screenshotContext = null;
7220
7457
  }
7458
+ // Worker 数量配置:移动设备/低端设备使用 1 个 Worker,桌面设备使用 2 个
7459
+ // workerNumber > 0 会启用 Worker 模式,截图处理在后台线程执行,不会阻塞主线程 UI
7221
7460
  const workerNumber = isMobile || isLowEndDevice ? 1 : 2;
7222
7461
  // 构建 createContext 配置
7462
+ // 参考: https://github.com/qq15725/modern-screenshot/blob/main/src/options.ts
7223
7463
  const contextOptions = {
7224
- workerNumber,
7225
- quality: mobileQuality,
7464
+ workerNumber, // Worker 数量,> 0 启用 Worker 模式
7465
+ quality: finalQuality, // 图片质量(0-1),已优化为更低的值以减少 base64 大小
7226
7466
  fetchFn: handleCrossOriginImage, // 使用代理服务器处理跨域图片
7227
7467
  fetch: {
7228
7468
  requestInit: {
@@ -7230,35 +7470,45 @@ class ScreenshotManager {
7230
7470
  },
7231
7471
  bypassingCache: true,
7232
7472
  },
7473
+ // 设置最大 canvas 尺寸,防止生成过大的 canvas(避免内存问题)
7474
+ // 参考: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
7475
+ // 大多数浏览器限制为 16,777,216 像素(4096x4096),这里设置为更保守的值
7476
+ maximumCanvasSize: 16777216, // 16M 像素(约 4096x4096)
7233
7477
  };
7234
- // 对于 document.body,不设置 width/height,让 modern-screenshot 自动处理完整页面
7235
- // 只有在需要压缩且不是 body 元素时才指定尺寸
7236
- if (element !== document.body && element !== document.documentElement) {
7237
- if (finalWidth && finalHeight && (finalWidth < elementWidth || finalHeight < elementHeight)) {
7238
- contextOptions.width = finalWidth;
7239
- contextOptions.height = finalHeight;
7240
- if (!this.options.silentMode) {
7241
- console.log(`📸 使用压缩尺寸: ${finalWidth}x${finalHeight}`);
7478
+ // 对所有元素都设置尺寸限制(包括 document.body),避免截图过大
7479
+ // 这样可以减少 base64 大小,提高性能
7480
+ if (finalWidth && finalHeight) {
7481
+ contextOptions.width = finalWidth;
7482
+ contextOptions.height = finalHeight;
7483
+ if (!this.options.silentMode) {
7484
+ if (element === document.body || element === document.documentElement) {
7485
+ console.log(`📸 截取完整页面(document.body),使用压缩尺寸: ${finalWidth}x${finalHeight}`);
7242
7486
  }
7243
- }
7244
- else {
7245
- if (!this.options.silentMode) {
7246
- console.log(`📸 使用元素实际尺寸: ${elementWidth}x${elementHeight}`);
7487
+ else {
7488
+ console.log(`📸 使用压缩尺寸: ${finalWidth}x${finalHeight}`);
7247
7489
  }
7248
7490
  }
7249
7491
  }
7250
7492
  else {
7251
- // 对于 body,不设置尺寸限制,让 modern-screenshot 自动截取完整页面
7252
7493
  if (!this.options.silentMode) {
7253
- console.log(`📸 截取完整页面(document.body),不设置尺寸限制(让 modern-screenshot 自动处理)`);
7494
+ console.log(`📸 使用元素实际尺寸: ${elementWidth}x${elementHeight}`);
7254
7495
  }
7255
7496
  }
7256
- // 如果指定了缩放比例,添加缩放配置
7497
+ // 缩放配置:移动设备使用更低的缩放比例,进一步减少图片大小
7498
+ // scale < 1 会降低图片分辨率,减少 base64 大小
7257
7499
  if (this.options.scale !== 1) {
7258
- contextOptions.scale = isMobile ? 0.8 : this.options.scale;
7500
+ contextOptions.scale = isMobile ? 0.7 : this.options.scale; // 移动设备:0.8 -> 0.7
7501
+ }
7502
+ else if (isMobile) {
7503
+ // 如果未指定 scale,移动设备默认使用 0.7
7504
+ contextOptions.scale = 0.7;
7259
7505
  }
7260
- // modern-screenshot 会自动处理 worker URL,不需要手动设置
7506
+ // modern-screenshot 会自动处理 worker URL,不需要手动设置 workerUrl
7507
+ // 当 workerNumber > 0 时,截图处理会在 Worker 线程中执行,不会阻塞主线程 UI
7261
7508
  // 创建 Worker 上下文(每次截图都创建新的,确保元素状态最新)
7509
+ if (!this.options.silentMode) {
7510
+ console.log(`📸 Worker 模式: ${workerNumber} 个 Worker,质量: ${finalQuality.toFixed(2)},缩放: ${contextOptions.scale || 1}`);
7511
+ }
7262
7512
  this.screenshotContext = await createContext$1(element, contextOptions);
7263
7513
  try {
7264
7514
  // 使用 Worker 上下文进行截图
@@ -7287,7 +7537,8 @@ class ScreenshotManager {
7287
7537
  img.src = dataUrl;
7288
7538
  });
7289
7539
  let mimeType = 'image/jpeg';
7290
- let finalQuality = mobileQuality;
7540
+ // 使用与 createContext 相同的质量设置
7541
+ let conversionQuality = finalQuality;
7291
7542
  if (this.options.outputFormat === 'webp' && !isMobile) {
7292
7543
  try {
7293
7544
  const testCanvas = document.createElement('canvas');
@@ -7302,9 +7553,10 @@ class ScreenshotManager {
7302
7553
  mimeType = 'image/jpeg';
7303
7554
  }
7304
7555
  }
7556
+ // 使用优化后的质量进行格式转换
7305
7557
  const convertedDataUrl = mimeType === 'image/png'
7306
7558
  ? canvas.toDataURL(mimeType)
7307
- : canvas.toDataURL(mimeType, finalQuality);
7559
+ : canvas.toDataURL(mimeType, conversionQuality);
7308
7560
  return convertedDataUrl;
7309
7561
  }
7310
7562
  return dataUrl;
@@ -7780,6 +8032,87 @@ class ScreenshotManager {
7780
8032
  }
7781
8033
  return dataUrl;
7782
8034
  }
8035
+ /**
8036
+ * 不使用代理时下载图片(带内存保护)
8037
+ */
8038
+ async downloadImageWithoutProxy(url) {
8039
+ try {
8040
+ // 直接下载图片并检查大小
8041
+ const controller = new AbortController();
8042
+ const timeoutId = setTimeout(() => controller.abort(), this.options.imageLoadTimeout || 5000);
8043
+ const response = await fetch(url, {
8044
+ method: 'GET',
8045
+ mode: 'cors',
8046
+ credentials: 'omit',
8047
+ cache: 'no-cache',
8048
+ signal: controller.signal
8049
+ }).catch(() => null).finally(() => {
8050
+ clearTimeout(timeoutId);
8051
+ });
8052
+ if (response && response.ok) {
8053
+ // 检查 Content-Length(如果可用)
8054
+ const contentLength = response.headers.get('content-length');
8055
+ if (contentLength) {
8056
+ const sizeMB = parseInt(contentLength, 10) / (1024 * 1024);
8057
+ const maxSizeMB = this.options.maxImageSize || 5;
8058
+ if (sizeMB > maxSizeMB) {
8059
+ if (this.options.skipLargeImages) {
8060
+ // 跳过过大的图片,返回占位符
8061
+ if (!this.options.silentMode) {
8062
+ console.warn(`📸 ⚠️ 跳过过大图片(${sizeMB.toFixed(2)}MB > ${maxSizeMB}MB): ${url.substring(0, 100)}...`);
8063
+ }
8064
+ // 返回一个 1x1 的透明占位符,避免截图失败
8065
+ return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
8066
+ }
8067
+ else {
8068
+ // 不跳过,但添加警告
8069
+ if (!this.options.silentMode) {
8070
+ console.warn(`📸 ⚠️ 图片较大(${sizeMB.toFixed(2)}MB),可能导致内存问题: ${url.substring(0, 100)}...`);
8071
+ }
8072
+ }
8073
+ }
8074
+ }
8075
+ // 如果大小检查通过或无法获取大小,下载图片并转换为 data URL
8076
+ // 但为了进一步保护内存,在下载 blob 后也检查实际大小
8077
+ const blob = await response.blob();
8078
+ const blobSizeMB = blob.size / (1024 * 1024);
8079
+ const maxSizeMB = this.options.maxImageSize || 5;
8080
+ if (blobSizeMB > maxSizeMB) {
8081
+ if (this.options.skipLargeImages) {
8082
+ // 跳过过大的图片,返回占位符
8083
+ if (!this.options.silentMode) {
8084
+ console.warn(`📸 ⚠️ 跳过过大图片(实际大小 ${blobSizeMB.toFixed(2)}MB > ${maxSizeMB}MB): ${url.substring(0, 100)}...`);
8085
+ }
8086
+ return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
8087
+ }
8088
+ else {
8089
+ // 不跳过,但添加警告
8090
+ if (!this.options.silentMode) {
8091
+ console.warn(`📸 ⚠️ 图片较大(实际大小 ${blobSizeMB.toFixed(2)}MB),可能导致内存问题: ${url.substring(0, 100)}...`);
8092
+ }
8093
+ }
8094
+ }
8095
+ // 转换为 data URL
8096
+ const dataUrl = await this.blobToDataUrl(blob);
8097
+ // 缓存结果(带时间戳,10分钟有效)
8098
+ this.setCachedImage(url, dataUrl);
8099
+ // 如果启用 IndexedDB,也保存到 IndexedDB
8100
+ if (this.options.useIndexedDB) {
8101
+ await this.setIndexedDBCache(url, dataUrl);
8102
+ }
8103
+ return dataUrl;
8104
+ }
8105
+ // 如果下载失败,返回原 URL,让 modern-screenshot 自己处理
8106
+ return url;
8107
+ }
8108
+ catch (error) {
8109
+ // 下载失败,返回原 URL,让 modern-screenshot 自己处理
8110
+ if (!this.options.silentMode) {
8111
+ console.warn(`📸 ⚠️ 下载图片失败: ${url.substring(0, 100)}...`, error);
8112
+ }
8113
+ return url;
8114
+ }
8115
+ }
7783
8116
  /**
7784
8117
  * 将 blob 转换为 data URL
7785
8118
  */