customer-chat-sdk 1.0.40 → 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.
- package/dist/core/ScreenshotManager.d.ts +55 -1
- package/dist/core/ScreenshotManager.d.ts.map +1 -1
- package/dist/customer-sdk.cjs.js +681 -106
- package/dist/customer-sdk.esm.js +681 -106
- package/dist/customer-sdk.min.js +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/customer-sdk.esm.js
CHANGED
|
@@ -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,13 +14314,23 @@ 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;
|
|
14320
14322
|
// modern-screenshot Worker 上下文(用于复用,避免频繁创建和销毁)
|
|
14321
14323
|
this.screenshotContext = null;
|
|
14324
|
+
this.contextElement = null; // 当前 context 对应的元素
|
|
14325
|
+
this.contextOptionsHash = ''; // context 配置的哈希值,用于判断是否需要重新创建
|
|
14326
|
+
this.contextContentHash = ''; // DOM 内容哈希值,用于检测内容变化
|
|
14327
|
+
this.contextLastUpdateTime = 0; // context 最后更新时间
|
|
14328
|
+
this.contextMaxAge = 5000; // context 最大存活时间(5秒),超过后强制刷新(缩短到5秒,确保内容及时更新)
|
|
14322
14329
|
// 截图锁,防止并发截图
|
|
14323
14330
|
this.isScreenshotInProgress = false;
|
|
14331
|
+
// 截图队列(用于处理频繁的截图请求)
|
|
14332
|
+
this.screenshotQueue = [];
|
|
14333
|
+
this.isProcessingQueue = false;
|
|
14324
14334
|
// PostMessage 监听器
|
|
14325
14335
|
this.messageHandler = null;
|
|
14326
14336
|
// 动态轮询间隔(由 iframe 消息控制)
|
|
@@ -14345,6 +14355,7 @@ class ScreenshotManager {
|
|
|
14345
14355
|
this.globalErrorHandler = null;
|
|
14346
14356
|
this.globalRejectionHandler = null;
|
|
14347
14357
|
this.targetElement = targetElement;
|
|
14358
|
+
this.sendToIframeCallback = sendToIframe || null;
|
|
14348
14359
|
this.options = {
|
|
14349
14360
|
interval: options.interval ?? 1000,
|
|
14350
14361
|
quality: options.quality ?? 0.3, // 降低默认质量:0.4 -> 0.3,减少 base64 大小
|
|
@@ -14373,7 +14384,8 @@ class ScreenshotManager {
|
|
|
14373
14384
|
maxCacheSize: options.maxCacheSize ?? 50, // 默认最大50MB
|
|
14374
14385
|
maxCacheAge: options.maxCacheAge ?? 86400000, // 默认24小时(86400000ms)
|
|
14375
14386
|
maxImageSize: options.maxImageSize ?? 5, // 不使用代理时,单个图片最大尺寸(MB),默认5MB
|
|
14376
|
-
skipLargeImages: options.skipLargeImages ?? true // 不使用代理时,是否跳过过大的图片,默认true(跳过)
|
|
14387
|
+
skipLargeImages: options.skipLargeImages ?? true, // 不使用代理时,是否跳过过大的图片,默认true(跳过)
|
|
14388
|
+
workerNumber: options.workerNumber ?? undefined // modern-screenshot Worker 数量,默认自动计算(undefined 表示自动)
|
|
14377
14389
|
};
|
|
14378
14390
|
this.setupMessageListener();
|
|
14379
14391
|
this.setupVisibilityChangeListener();
|
|
@@ -14401,18 +14413,95 @@ class ScreenshotManager {
|
|
|
14401
14413
|
* 设置目标元素
|
|
14402
14414
|
*/
|
|
14403
14415
|
setTargetElement(element) {
|
|
14404
|
-
//
|
|
14405
|
-
if (this.targetElement !== element
|
|
14406
|
-
|
|
14407
|
-
|
|
14408
|
-
|
|
14409
|
-
|
|
14410
|
-
|
|
14416
|
+
// 如果元素变化,需要清理 context(下次截图时会重新创建)
|
|
14417
|
+
if (this.targetElement !== element) {
|
|
14418
|
+
if (this.screenshotContext) {
|
|
14419
|
+
try {
|
|
14420
|
+
destroyContext(this.screenshotContext);
|
|
14421
|
+
if (!this.options.silentMode) {
|
|
14422
|
+
console.log('📸 目标元素变化,清理 context');
|
|
14423
|
+
}
|
|
14424
|
+
}
|
|
14425
|
+
catch (e) {
|
|
14426
|
+
// 忽略清理错误
|
|
14427
|
+
}
|
|
14428
|
+
this.screenshotContext = null;
|
|
14429
|
+
this.contextElement = null;
|
|
14430
|
+
this.contextOptionsHash = '';
|
|
14431
|
+
this.contextContentHash = '';
|
|
14432
|
+
this.contextLastUpdateTime = 0;
|
|
14411
14433
|
}
|
|
14412
|
-
this.screenshotContext = null;
|
|
14413
14434
|
}
|
|
14414
14435
|
this.targetElement = element;
|
|
14415
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
|
+
}
|
|
14416
14505
|
/**
|
|
14417
14506
|
* 设置消息监听
|
|
14418
14507
|
*/
|
|
@@ -14451,10 +14540,75 @@ class ScreenshotManager {
|
|
|
14451
14540
|
if (!event.data || event.data.type !== 'checkScreenshot') {
|
|
14452
14541
|
return;
|
|
14453
14542
|
}
|
|
14543
|
+
// 如果提供了发送消息的回调,保存它(用于后续发送二进制数据)
|
|
14544
|
+
// 注意:消息来源验证在 setupMessageListener 中处理
|
|
14454
14545
|
if (!this.options.silentMode) {
|
|
14455
14546
|
console.log('📸 [iframe] 收到消息:', event.data);
|
|
14456
14547
|
}
|
|
14457
|
-
//
|
|
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
|
+
// 旧格式:解析上传配置
|
|
14458
14612
|
const config = this.parseUploadConfig(event.data.data);
|
|
14459
14613
|
if (!config) {
|
|
14460
14614
|
console.error('📸 [iframe] 解析配置失败');
|
|
@@ -14463,6 +14617,7 @@ class ScreenshotManager {
|
|
|
14463
14617
|
}
|
|
14464
14618
|
// 保存当前配置
|
|
14465
14619
|
this.currentUploadConfig = config;
|
|
14620
|
+
this.currentBinaryConfig = null; // 清除二进制配置
|
|
14466
14621
|
// 根据 ttl 判断是否开启截图功能
|
|
14467
14622
|
// ttl == 0 表示禁用,ttl > 0 且大于当前时间表示有效
|
|
14468
14623
|
const currentTime = Date.now();
|
|
@@ -14582,6 +14737,34 @@ class ScreenshotManager {
|
|
|
14582
14737
|
this.uploadError = error instanceof Error ? error.message : String(error);
|
|
14583
14738
|
}
|
|
14584
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
|
+
}
|
|
14585
14768
|
/**
|
|
14586
14769
|
* 解析上传配置
|
|
14587
14770
|
*/
|
|
@@ -14637,22 +14820,64 @@ class ScreenshotManager {
|
|
|
14637
14820
|
if (!this.worker && this.options.compress) {
|
|
14638
14821
|
this.worker = this.createWorker();
|
|
14639
14822
|
}
|
|
14640
|
-
//
|
|
14641
|
-
|
|
14823
|
+
// 设置定时器(使用递归 setTimeout,确保等待前一个完成)
|
|
14824
|
+
// 这样可以避免 setInterval 不等待异步完成的问题
|
|
14825
|
+
const scheduleNext = async () => {
|
|
14642
14826
|
if (this.isRunning && this.isEnabled && !document.hidden) {
|
|
14643
|
-
|
|
14644
|
-
|
|
14645
|
-
|
|
14646
|
-
|
|
14647
|
-
|
|
14648
|
-
|
|
14649
|
-
.
|
|
14650
|
-
|
|
14651
|
-
|
|
14827
|
+
try {
|
|
14828
|
+
await this.takeScreenshot();
|
|
14829
|
+
// 如果配置了上传,且当前有上传配置,自动上传
|
|
14830
|
+
if (this.currentUploadConfig) {
|
|
14831
|
+
const latestScreenshot = this.getLatestScreenshot();
|
|
14832
|
+
if (latestScreenshot && !this.isUploading) {
|
|
14833
|
+
this.uploadScreenshot(latestScreenshot, this.currentUploadConfig)
|
|
14834
|
+
.catch((error) => {
|
|
14835
|
+
console.error('📸 [轮询] 自动上传失败:', error);
|
|
14836
|
+
});
|
|
14837
|
+
}
|
|
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
|
+
}
|
|
14867
|
+
}
|
|
14868
|
+
catch (error) {
|
|
14869
|
+
if (!this.options.silentMode) {
|
|
14870
|
+
console.error('📸 [轮询] 截图失败:', error);
|
|
14652
14871
|
}
|
|
14653
14872
|
}
|
|
14654
14873
|
}
|
|
14655
|
-
|
|
14874
|
+
// 如果还在运行,安排下一次截图
|
|
14875
|
+
if (this.isRunning) {
|
|
14876
|
+
this.screenshotTimer = setTimeout(scheduleNext, currentInterval);
|
|
14877
|
+
}
|
|
14878
|
+
};
|
|
14879
|
+
// 立即开始第一次
|
|
14880
|
+
scheduleNext();
|
|
14656
14881
|
// 注意:不再立即执行一次,因为已经在 takeScreenshotAndUpload 中执行了
|
|
14657
14882
|
}
|
|
14658
14883
|
/**
|
|
@@ -15272,8 +15497,37 @@ class ScreenshotManager {
|
|
|
15272
15497
|
*/
|
|
15273
15498
|
async takeScreenshotWithModernScreenshot(element) {
|
|
15274
15499
|
// 检查是否有截图正在进行(防止并发冲突)
|
|
15500
|
+
// 如果正在进行,将请求加入队列,而不是直接拒绝
|
|
15275
15501
|
if (this.isScreenshotInProgress) {
|
|
15276
|
-
|
|
15502
|
+
// 队列最多保留 1 个请求,避免积压
|
|
15503
|
+
if (this.screenshotQueue.length >= 1) {
|
|
15504
|
+
if (!this.options.silentMode) {
|
|
15505
|
+
console.log('📸 截图队列已满,跳过当前请求(等待队列处理)');
|
|
15506
|
+
}
|
|
15507
|
+
// 等待队列中的请求完成
|
|
15508
|
+
return new Promise((resolve, reject) => {
|
|
15509
|
+
const checkQueue = () => {
|
|
15510
|
+
if (!this.isScreenshotInProgress && this.screenshotQueue.length === 0) {
|
|
15511
|
+
// 队列已清空,重新尝试
|
|
15512
|
+
this.takeScreenshotWithModernScreenshot(element).then(resolve).catch(reject);
|
|
15513
|
+
}
|
|
15514
|
+
else {
|
|
15515
|
+
setTimeout(checkQueue, 100); // 100ms 后再次检查
|
|
15516
|
+
}
|
|
15517
|
+
};
|
|
15518
|
+
checkQueue();
|
|
15519
|
+
});
|
|
15520
|
+
}
|
|
15521
|
+
// 将请求加入队列
|
|
15522
|
+
return new Promise((resolve, reject) => {
|
|
15523
|
+
this.screenshotQueue.push({ resolve: () => {
|
|
15524
|
+
this.takeScreenshotWithModernScreenshot(element).then(resolve).catch(reject);
|
|
15525
|
+
}, reject });
|
|
15526
|
+
// 启动队列处理(如果还没启动)
|
|
15527
|
+
if (!this.isProcessingQueue) {
|
|
15528
|
+
this.processScreenshotQueue();
|
|
15529
|
+
}
|
|
15530
|
+
});
|
|
15277
15531
|
}
|
|
15278
15532
|
this.isScreenshotInProgress = true;
|
|
15279
15533
|
if (!this.options.silentMode) {
|
|
@@ -15485,20 +15739,38 @@ class ScreenshotManager {
|
|
|
15485
15739
|
if (rect.width === 0 || rect.height === 0) {
|
|
15486
15740
|
throw new Error('元素尺寸为 0,无法截图');
|
|
15487
15741
|
}
|
|
15488
|
-
//
|
|
15489
|
-
//
|
|
15490
|
-
|
|
15491
|
-
|
|
15492
|
-
|
|
15742
|
+
// Worker 数量配置:智能计算或使用用户配置
|
|
15743
|
+
// workerNumber > 0 会启用 Worker 模式,截图处理在后台线程执行,不会阻塞主线程 UI
|
|
15744
|
+
// 如果用户指定了 workerNumber,直接使用;否则根据设备性能自动计算
|
|
15745
|
+
let workerNumber;
|
|
15746
|
+
if (this.options.workerNumber !== undefined && this.options.workerNumber > 0) {
|
|
15747
|
+
// 用户明确指定了 workerNumber
|
|
15748
|
+
workerNumber = this.options.workerNumber;
|
|
15749
|
+
}
|
|
15750
|
+
else {
|
|
15751
|
+
// 自动计算 workerNumber
|
|
15752
|
+
const cpuCores = navigator.hardwareConcurrency || 4; // 默认假设 4 核
|
|
15753
|
+
if (isMobile || isLowEndDevice) {
|
|
15754
|
+
// 移动设备/低端设备:使用 1 个 Worker(避免内存压力)
|
|
15755
|
+
workerNumber = 1;
|
|
15493
15756
|
}
|
|
15494
|
-
|
|
15495
|
-
//
|
|
15757
|
+
else if (cpuCores >= 8) {
|
|
15758
|
+
// 高性能设备(8核及以上):使用 3-4 个 Worker(充分利用多核)
|
|
15759
|
+
// 但根据截图间隔调整:频繁截图(间隔 < 2秒)时使用更多 Worker
|
|
15760
|
+
const isFrequentScreenshot = this.options.interval < 2000;
|
|
15761
|
+
workerNumber = isFrequentScreenshot ? Math.min(4, Math.floor(cpuCores / 2)) : 3;
|
|
15762
|
+
}
|
|
15763
|
+
else if (cpuCores >= 4) {
|
|
15764
|
+
// 中等性能设备(4-7核):使用 2 个 Worker
|
|
15765
|
+
workerNumber = 2;
|
|
15766
|
+
}
|
|
15767
|
+
else {
|
|
15768
|
+
// 低性能设备(< 4核):使用 1 个 Worker
|
|
15769
|
+
workerNumber = 1;
|
|
15496
15770
|
}
|
|
15497
|
-
this.screenshotContext = null;
|
|
15498
15771
|
}
|
|
15499
|
-
//
|
|
15500
|
-
|
|
15501
|
-
const workerNumber = isMobile || isLowEndDevice ? 1 : 2;
|
|
15772
|
+
// 限制 workerNumber 范围:1-8(避免过多 Worker 导致资源竞争)
|
|
15773
|
+
workerNumber = Math.max(1, Math.min(8, workerNumber));
|
|
15502
15774
|
// 构建 createContext 配置
|
|
15503
15775
|
// 参考: https://github.com/qq15725/modern-screenshot/blob/main/src/options.ts
|
|
15504
15776
|
const contextOptions = {
|
|
@@ -15515,7 +15787,28 @@ class ScreenshotManager {
|
|
|
15515
15787
|
// 参考: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
|
|
15516
15788
|
// 大多数浏览器限制为 16,777,216 像素(4096x4096),这里设置为更保守的值
|
|
15517
15789
|
maximumCanvasSize: 16777216, // 16M 像素(约 4096x4096)
|
|
15790
|
+
// 使用 modern-screenshot 内置的 timeout(更可靠)
|
|
15791
|
+
timeout: Math.max(this.options.interval * 6, 5000),
|
|
15518
15792
|
};
|
|
15793
|
+
// 限制 timeout 最多 15 秒
|
|
15794
|
+
contextOptions.timeout = Math.min(contextOptions.timeout, 15000);
|
|
15795
|
+
// 如果用户指定了 workerUrl,使用指定的 URL
|
|
15796
|
+
// 否则让 modern-screenshot 自动处理(它会尝试从 node_modules 或 CDN 加载)
|
|
15797
|
+
// 注意:在某些构建工具(如 Rollup)中,可能需要手动指定 workerUrl
|
|
15798
|
+
if (this.options.workerUrl) {
|
|
15799
|
+
contextOptions.workerUrl = this.options.workerUrl;
|
|
15800
|
+
if (!this.options.silentMode) {
|
|
15801
|
+
console.log(`📸 使用指定的 Worker URL: ${this.options.workerUrl}`);
|
|
15802
|
+
}
|
|
15803
|
+
}
|
|
15804
|
+
else {
|
|
15805
|
+
// 未指定 workerUrl 时,modern-screenshot 会自动处理
|
|
15806
|
+
// 但在某些构建环境中可能需要手动指定,可以使用 CDN 作为后备
|
|
15807
|
+
// 这里不设置 workerUrl,让 modern-screenshot 自己处理
|
|
15808
|
+
if (!this.options.silentMode) {
|
|
15809
|
+
console.log('📸 Worker URL 未指定,modern-screenshot 将自动处理');
|
|
15810
|
+
}
|
|
15811
|
+
}
|
|
15519
15812
|
// 对所有元素都设置尺寸限制(包括 document.body),避免截图过大
|
|
15520
15813
|
// 这样可以减少 base64 大小,提高性能
|
|
15521
15814
|
if (finalWidth && finalHeight) {
|
|
@@ -15544,69 +15837,215 @@ class ScreenshotManager {
|
|
|
15544
15837
|
// 如果未指定 scale,移动设备默认使用 0.7
|
|
15545
15838
|
contextOptions.scale = 0.7;
|
|
15546
15839
|
}
|
|
15547
|
-
//
|
|
15548
|
-
//
|
|
15549
|
-
//
|
|
15550
|
-
|
|
15551
|
-
|
|
15552
|
-
|
|
15553
|
-
|
|
15554
|
-
|
|
15555
|
-
|
|
15556
|
-
|
|
15557
|
-
|
|
15558
|
-
|
|
15559
|
-
|
|
15560
|
-
|
|
15561
|
-
|
|
15840
|
+
// 优化:复用 context,避免频繁创建和销毁(性能提升 20%+)
|
|
15841
|
+
// 只在元素变化、配置变化或内容变化时重新创建 context
|
|
15842
|
+
// 1. 计算配置哈希
|
|
15843
|
+
const contextOptionsHash = JSON.stringify({
|
|
15844
|
+
workerNumber,
|
|
15845
|
+
quality: finalQuality,
|
|
15846
|
+
scale: contextOptions.scale,
|
|
15847
|
+
width: contextOptions.width,
|
|
15848
|
+
height: contextOptions.height,
|
|
15849
|
+
maximumCanvasSize: contextOptions.maximumCanvasSize,
|
|
15850
|
+
timeout: contextOptions.timeout
|
|
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 的限制)
|
|
15864
|
+
const needsRecreateContext = !this.screenshotContext ||
|
|
15865
|
+
this.contextElement !== element ||
|
|
15866
|
+
this.contextOptionsHash !== contextOptionsHash ||
|
|
15867
|
+
this.contextContentHash !== contentHash || // 内容变化时强制重新创建
|
|
15868
|
+
isContextExpired;
|
|
15869
|
+
if (needsRecreateContext) {
|
|
15870
|
+
if (!this.options.silentMode) {
|
|
15871
|
+
if (this.screenshotContext) {
|
|
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...`);
|
|
15882
|
+
}
|
|
15883
|
+
else {
|
|
15884
|
+
console.log(`📸 Worker 模式: ${workerNumber} 个 Worker,质量: ${finalQuality.toFixed(2)},缩放: ${contextOptions.scale || 1}`);
|
|
15885
|
+
}
|
|
15562
15886
|
}
|
|
15563
|
-
|
|
15564
|
-
|
|
15565
|
-
|
|
15887
|
+
// 销毁旧 context
|
|
15888
|
+
if (this.screenshotContext) {
|
|
15889
|
+
try {
|
|
15890
|
+
destroyContext(this.screenshotContext);
|
|
15891
|
+
}
|
|
15892
|
+
catch (e) {
|
|
15893
|
+
// 忽略清理错误
|
|
15894
|
+
}
|
|
15895
|
+
this.screenshotContext = null;
|
|
15896
|
+
}
|
|
15897
|
+
// 添加 progress 回调(可选,用于显示进度)
|
|
15898
|
+
if (!this.options.silentMode) {
|
|
15899
|
+
contextOptions.progress = (current, total) => {
|
|
15900
|
+
if (total > 0) {
|
|
15901
|
+
const percent = Math.round((current / total) * 100);
|
|
15902
|
+
if (percent % 25 === 0 || current === total) { // 每 25% 或完成时打印
|
|
15903
|
+
console.log(`📸 截图进度: ${current}/${total} (${percent}%)`);
|
|
15904
|
+
}
|
|
15905
|
+
}
|
|
15906
|
+
};
|
|
15907
|
+
}
|
|
15908
|
+
// 添加重试机制创建新 context
|
|
15909
|
+
let retries = 0;
|
|
15910
|
+
const maxRetries = this.options.maxRetries || 2;
|
|
15911
|
+
while (retries <= maxRetries) {
|
|
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
|
+
}
|
|
15935
|
+
this.screenshotContext = await createContext$1(element, contextOptions);
|
|
15936
|
+
this.contextElement = element;
|
|
15937
|
+
this.contextOptionsHash = contextOptionsHash;
|
|
15938
|
+
this.contextContentHash = contentHash;
|
|
15939
|
+
this.contextLastUpdateTime = now;
|
|
15940
|
+
break;
|
|
15566
15941
|
}
|
|
15567
|
-
|
|
15568
|
-
|
|
15942
|
+
catch (error) {
|
|
15943
|
+
if (retries === maxRetries) {
|
|
15944
|
+
throw new Error(`创建截图上下文失败(已重试 ${maxRetries} 次): ${error instanceof Error ? error.message : String(error)}`);
|
|
15945
|
+
}
|
|
15946
|
+
retries++;
|
|
15947
|
+
const delay = 1000 * retries; // 递增延迟:1秒、2秒...
|
|
15948
|
+
if (!this.options.silentMode) {
|
|
15949
|
+
console.warn(`📸 ⚠️ 创建截图上下文失败,${delay}ms 后重试 (${retries}/${maxRetries})...`);
|
|
15950
|
+
}
|
|
15951
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
15952
|
+
}
|
|
15953
|
+
}
|
|
15954
|
+
}
|
|
15955
|
+
else {
|
|
15956
|
+
if (!this.options.silentMode) {
|
|
15957
|
+
console.log('📸 复用现有 context(性能优化)');
|
|
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
|
|
15569
15966
|
if (!this.options.silentMode) {
|
|
15570
|
-
console.
|
|
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
|
+
}
|
|
15571
16012
|
}
|
|
15572
|
-
|
|
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
|
+
});
|
|
15573
16026
|
}
|
|
15574
16027
|
}
|
|
15575
16028
|
try {
|
|
15576
16029
|
// 根据输出格式选择对应的 API,避免格式转换(性能优化)
|
|
15577
|
-
//
|
|
15578
|
-
const timeoutMs = 30000; // 30秒超时
|
|
15579
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
15580
|
-
setTimeout(() => {
|
|
15581
|
-
reject(new Error(`截图超时(${timeoutMs}ms),可能页面过大或 Worker 处理时间过长`));
|
|
15582
|
-
}, timeoutMs);
|
|
15583
|
-
});
|
|
16030
|
+
// 注意:timeout 已经在 createContext 时设置,modern-screenshot 内部会处理超时
|
|
15584
16031
|
let dataUrl;
|
|
15585
16032
|
const outputFormat = this.options.outputFormat || 'webp';
|
|
15586
16033
|
if (!this.options.silentMode) {
|
|
15587
16034
|
console.log(`📸 使用 ${outputFormat.toUpperCase()} 格式截图(直接输出,无需转换)...`);
|
|
15588
16035
|
}
|
|
15589
16036
|
// 根据输出格式选择对应的 API
|
|
16037
|
+
// modern-screenshot 内部已经处理了超时,不需要额外的 Promise.race
|
|
15590
16038
|
if (outputFormat === 'webp') {
|
|
15591
16039
|
// 使用 domToWebp,直接输出 WebP 格式,无需转换
|
|
15592
|
-
dataUrl = await
|
|
15593
|
-
domToWebp(this.screenshotContext),
|
|
15594
|
-
timeoutPromise
|
|
15595
|
-
]);
|
|
16040
|
+
dataUrl = await domToWebp(this.screenshotContext);
|
|
15596
16041
|
}
|
|
15597
16042
|
else if (outputFormat === 'jpeg') {
|
|
15598
16043
|
// 使用 domToJpeg,直接输出 JPEG 格式,无需转换
|
|
15599
|
-
dataUrl = await
|
|
15600
|
-
domToJpeg(this.screenshotContext),
|
|
15601
|
-
timeoutPromise
|
|
15602
|
-
]);
|
|
16044
|
+
dataUrl = await domToJpeg(this.screenshotContext);
|
|
15603
16045
|
}
|
|
15604
16046
|
else {
|
|
15605
16047
|
// 默认使用 domToPng
|
|
15606
|
-
dataUrl = await
|
|
15607
|
-
domToPng(this.screenshotContext),
|
|
15608
|
-
timeoutPromise
|
|
15609
|
-
]);
|
|
16048
|
+
dataUrl = await domToPng(this.screenshotContext);
|
|
15610
16049
|
}
|
|
15611
16050
|
// 验证截图结果
|
|
15612
16051
|
if (!dataUrl || dataUrl.length < 100) {
|
|
@@ -15633,37 +16072,11 @@ class ScreenshotManager {
|
|
|
15633
16072
|
throw error;
|
|
15634
16073
|
}
|
|
15635
16074
|
finally {
|
|
15636
|
-
//
|
|
15637
|
-
//
|
|
15638
|
-
|
|
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
|
-
}
|
|
16075
|
+
// 优化:不复用 context 时才清理(性能优化)
|
|
16076
|
+
// 如果元素或配置没有变化,保留 context 以便下次复用
|
|
16077
|
+
// 这样可以避免频繁创建和销毁 Worker,提升性能 20%+
|
|
15655
16078
|
// 释放截图锁
|
|
15656
16079
|
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
16080
|
}
|
|
15668
16081
|
}
|
|
15669
16082
|
catch (error) {
|
|
@@ -15679,7 +16092,34 @@ class ScreenshotManager {
|
|
|
15679
16092
|
if (this.isScreenshotInProgress) {
|
|
15680
16093
|
this.isScreenshotInProgress = false;
|
|
15681
16094
|
}
|
|
16095
|
+
// 处理队列中的下一个请求
|
|
16096
|
+
this.processScreenshotQueue();
|
|
16097
|
+
}
|
|
16098
|
+
}
|
|
16099
|
+
/**
|
|
16100
|
+
* 处理截图队列
|
|
16101
|
+
*/
|
|
16102
|
+
async processScreenshotQueue() {
|
|
16103
|
+
if (this.isProcessingQueue || this.screenshotQueue.length === 0) {
|
|
16104
|
+
return;
|
|
16105
|
+
}
|
|
16106
|
+
this.isProcessingQueue = true;
|
|
16107
|
+
while (this.screenshotQueue.length > 0 && !this.isScreenshotInProgress) {
|
|
16108
|
+
const task = this.screenshotQueue.shift();
|
|
16109
|
+
if (task) {
|
|
16110
|
+
try {
|
|
16111
|
+
task.resolve();
|
|
16112
|
+
// 等待当前截图完成
|
|
16113
|
+
while (this.isScreenshotInProgress) {
|
|
16114
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
16115
|
+
}
|
|
16116
|
+
}
|
|
16117
|
+
catch (error) {
|
|
16118
|
+
task.reject(error instanceof Error ? error : new Error(String(error)));
|
|
16119
|
+
}
|
|
16120
|
+
}
|
|
15682
16121
|
}
|
|
16122
|
+
this.isProcessingQueue = false;
|
|
15683
16123
|
}
|
|
15684
16124
|
/**
|
|
15685
16125
|
* 预连接代理服务器(优化网络性能)
|
|
@@ -16534,6 +16974,123 @@ class ScreenshotManager {
|
|
|
16534
16974
|
}
|
|
16535
16975
|
return new Blob([u8arr], { type: mime });
|
|
16536
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
|
+
}
|
|
16537
17094
|
/**
|
|
16538
17095
|
* 获取最新截图
|
|
16539
17096
|
*/
|
|
@@ -16553,6 +17110,20 @@ class ScreenshotManager {
|
|
|
16553
17110
|
* 清理资源
|
|
16554
17111
|
*/
|
|
16555
17112
|
destroy() {
|
|
17113
|
+
// 清理 modern-screenshot context
|
|
17114
|
+
if (this.screenshotContext) {
|
|
17115
|
+
try {
|
|
17116
|
+
destroyContext(this.screenshotContext);
|
|
17117
|
+
}
|
|
17118
|
+
catch (e) {
|
|
17119
|
+
// 忽略清理错误
|
|
17120
|
+
}
|
|
17121
|
+
this.screenshotContext = null;
|
|
17122
|
+
this.contextElement = null;
|
|
17123
|
+
this.contextOptionsHash = '';
|
|
17124
|
+
this.contextContentHash = '';
|
|
17125
|
+
this.contextLastUpdateTime = 0;
|
|
17126
|
+
}
|
|
16556
17127
|
this.stopScreenshot();
|
|
16557
17128
|
if (this.worker) {
|
|
16558
17129
|
this.worker.terminate();
|
|
@@ -20042,7 +20613,11 @@ class CustomerServiceSDK {
|
|
|
20042
20613
|
if (config.screenshot) {
|
|
20043
20614
|
// 默认截图目标为 document.body,可以通过配置自定义
|
|
20044
20615
|
const targetElement = document.body;
|
|
20045
|
-
|
|
20616
|
+
// 传入发送消息到 iframe 的回调函数
|
|
20617
|
+
this.screenshotManager = new ScreenshotManager(targetElement, config.screenshot, (data) => {
|
|
20618
|
+
// 通过 IframeManager 发送消息到 iframe
|
|
20619
|
+
this.iframeManager?.sendToIframe(data);
|
|
20620
|
+
});
|
|
20046
20621
|
// 自动启用截图功能(用于测试,实际使用时需要通过 iframe 消息启用)
|
|
20047
20622
|
this.screenshotManager.enable(true);
|
|
20048
20623
|
console.log('CustomerSDK screenshot manager initialized and enabled');
|