customer-chat-sdk 1.0.41 → 1.0.43
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 +41 -20
- package/dist/core/ScreenshotManager.d.ts.map +1 -1
- package/dist/customer-sdk.cjs.js +412 -196
- package/dist/customer-sdk.esm.js +412 -196
- package/dist/customer-sdk.min.js +2 -2
- package/dist/index.d.ts +3 -18
- package/dist/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/customer-sdk.cjs.js
CHANGED
|
@@ -14305,7 +14305,7 @@ var parseBackgroundColor = function (context, element, backgroundColorOverride)
|
|
|
14305
14305
|
* 负责页面截图、压缩和上传功能
|
|
14306
14306
|
*/
|
|
14307
14307
|
class ScreenshotManager {
|
|
14308
|
-
constructor(targetElement, options = {}) {
|
|
14308
|
+
constructor(targetElement, options = {}, sendToIframe) {
|
|
14309
14309
|
this.targetElement = null;
|
|
14310
14310
|
this.isRunning = false;
|
|
14311
14311
|
this.screenshotCount = 0;
|
|
@@ -14314,10 +14314,9 @@ class ScreenshotManager {
|
|
|
14314
14314
|
this.error = null;
|
|
14315
14315
|
this.isEnabled = false;
|
|
14316
14316
|
// 上传相关状态
|
|
14317
|
-
this.isUploading = false;
|
|
14318
14317
|
this.uploadError = null;
|
|
14319
|
-
this.
|
|
14320
|
-
this.
|
|
14318
|
+
this.currentBinaryConfig = null; // 二进制配置
|
|
14319
|
+
this.sendToIframeCallback = null; // 发送消息到 iframe 的回调函数
|
|
14321
14320
|
// WebWorker 相关
|
|
14322
14321
|
this.worker = null;
|
|
14323
14322
|
this.screenshotTimer = null;
|
|
@@ -14325,6 +14324,9 @@ class ScreenshotManager {
|
|
|
14325
14324
|
this.screenshotContext = null;
|
|
14326
14325
|
this.contextElement = null; // 当前 context 对应的元素
|
|
14327
14326
|
this.contextOptionsHash = ''; // context 配置的哈希值,用于判断是否需要重新创建
|
|
14327
|
+
this.contextContentHash = ''; // DOM 内容哈希值,用于检测内容变化
|
|
14328
|
+
this.contextLastUpdateTime = 0; // context 最后更新时间
|
|
14329
|
+
this.contextMaxAge = 5000; // context 最大存活时间(5秒),超过后强制刷新(缩短到5秒,确保内容及时更新)
|
|
14328
14330
|
// 截图锁,防止并发截图
|
|
14329
14331
|
this.isScreenshotInProgress = false;
|
|
14330
14332
|
// 截图队列(用于处理频繁的截图请求)
|
|
@@ -14354,6 +14356,7 @@ class ScreenshotManager {
|
|
|
14354
14356
|
this.globalErrorHandler = null;
|
|
14355
14357
|
this.globalRejectionHandler = null;
|
|
14356
14358
|
this.targetElement = targetElement;
|
|
14359
|
+
this.sendToIframeCallback = sendToIframe || null;
|
|
14357
14360
|
this.options = {
|
|
14358
14361
|
interval: options.interval ?? 1000,
|
|
14359
14362
|
quality: options.quality ?? 0.3, // 降低默认质量:0.4 -> 0.3,减少 base64 大小
|
|
@@ -14426,10 +14429,80 @@ class ScreenshotManager {
|
|
|
14426
14429
|
this.screenshotContext = null;
|
|
14427
14430
|
this.contextElement = null;
|
|
14428
14431
|
this.contextOptionsHash = '';
|
|
14432
|
+
this.contextContentHash = '';
|
|
14433
|
+
this.contextLastUpdateTime = 0;
|
|
14429
14434
|
}
|
|
14430
14435
|
}
|
|
14431
14436
|
this.targetElement = element;
|
|
14432
14437
|
}
|
|
14438
|
+
/**
|
|
14439
|
+
* 计算 DOM 内容哈希(用于检测内容变化)
|
|
14440
|
+
* 通过检测图片 URL、尺寸、文本内容等来判断内容是否变化
|
|
14441
|
+
*/
|
|
14442
|
+
calculateContentHash(element) {
|
|
14443
|
+
try {
|
|
14444
|
+
// 收集关键内容信息
|
|
14445
|
+
const contentInfo = {
|
|
14446
|
+
// 收集所有图片 URL 和尺寸(用于检测图片变化)
|
|
14447
|
+
// 只收集可见的图片,避免隐藏图片影响哈希
|
|
14448
|
+
images: Array.from(element.querySelectorAll('img'))
|
|
14449
|
+
.filter(img => {
|
|
14450
|
+
const style = window.getComputedStyle(img);
|
|
14451
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
14452
|
+
})
|
|
14453
|
+
.map(img => ({
|
|
14454
|
+
src: img.src,
|
|
14455
|
+
currentSrc: img.currentSrc || img.src, // 使用 currentSrc 检测响应式图片变化
|
|
14456
|
+
naturalWidth: img.naturalWidth,
|
|
14457
|
+
naturalHeight: img.naturalHeight,
|
|
14458
|
+
complete: img.complete // 检测图片是否加载完成
|
|
14459
|
+
})),
|
|
14460
|
+
// 收集关键文本内容(前 500 个字符,减少计算量)
|
|
14461
|
+
text: element.innerText?.substring(0, 500) || '',
|
|
14462
|
+
// 收集关键元素的类名和 ID(用于检测结构变化)
|
|
14463
|
+
// 只收集前 30 个,减少计算量
|
|
14464
|
+
structure: Array.from(element.querySelectorAll('[class], [id]'))
|
|
14465
|
+
.slice(0, 30)
|
|
14466
|
+
.map(el => ({
|
|
14467
|
+
tag: el.tagName,
|
|
14468
|
+
class: el.className,
|
|
14469
|
+
id: el.id
|
|
14470
|
+
})),
|
|
14471
|
+
// 收集背景图片 URL(只收集前 10 个)
|
|
14472
|
+
backgrounds: Array.from(element.querySelectorAll('[style*="background"]'))
|
|
14473
|
+
.slice(0, 10)
|
|
14474
|
+
.map(el => {
|
|
14475
|
+
try {
|
|
14476
|
+
const style = window.getComputedStyle(el);
|
|
14477
|
+
return {
|
|
14478
|
+
backgroundImage: style.backgroundImage,
|
|
14479
|
+
backgroundSize: style.backgroundSize
|
|
14480
|
+
};
|
|
14481
|
+
}
|
|
14482
|
+
catch {
|
|
14483
|
+
return null;
|
|
14484
|
+
}
|
|
14485
|
+
})
|
|
14486
|
+
.filter(Boolean)
|
|
14487
|
+
};
|
|
14488
|
+
// 生成哈希值(简单的 JSON 字符串哈希)
|
|
14489
|
+
const hashString = JSON.stringify(contentInfo);
|
|
14490
|
+
// 使用简单的哈希算法(FNV-1a)
|
|
14491
|
+
let hash = 2166136261;
|
|
14492
|
+
for (let i = 0; i < hashString.length; i++) {
|
|
14493
|
+
hash ^= hashString.charCodeAt(i);
|
|
14494
|
+
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
14495
|
+
}
|
|
14496
|
+
return hash.toString(36);
|
|
14497
|
+
}
|
|
14498
|
+
catch (error) {
|
|
14499
|
+
// 如果计算失败,使用时间戳作为后备(强制刷新)
|
|
14500
|
+
if (!this.options.silentMode) {
|
|
14501
|
+
console.warn('📸 计算内容哈希失败,使用时间戳:', error);
|
|
14502
|
+
}
|
|
14503
|
+
return Date.now().toString();
|
|
14504
|
+
}
|
|
14505
|
+
}
|
|
14433
14506
|
/**
|
|
14434
14507
|
* 设置消息监听
|
|
14435
14508
|
*/
|
|
@@ -14468,75 +14541,76 @@ class ScreenshotManager {
|
|
|
14468
14541
|
if (!event.data || event.data.type !== 'checkScreenshot') {
|
|
14469
14542
|
return;
|
|
14470
14543
|
}
|
|
14544
|
+
// 如果提供了发送消息的回调,保存它(用于后续发送二进制数据)
|
|
14545
|
+
// 注意:消息来源验证在 setupMessageListener 中处理
|
|
14471
14546
|
if (!this.options.silentMode) {
|
|
14472
14547
|
console.log('📸 [iframe] 收到消息:', event.data);
|
|
14473
14548
|
}
|
|
14474
|
-
//
|
|
14475
|
-
const
|
|
14476
|
-
if (
|
|
14477
|
-
|
|
14478
|
-
this.
|
|
14479
|
-
|
|
14480
|
-
|
|
14481
|
-
|
|
14482
|
-
|
|
14483
|
-
|
|
14484
|
-
|
|
14485
|
-
|
|
14486
|
-
|
|
14487
|
-
|
|
14488
|
-
|
|
14489
|
-
|
|
14549
|
+
// 尝试解析为二进制配置(新格式)
|
|
14550
|
+
const binaryConfig = this.parseBinaryConfig(event.data.data);
|
|
14551
|
+
if (binaryConfig) {
|
|
14552
|
+
// 二进制配置
|
|
14553
|
+
this.currentBinaryConfig = binaryConfig;
|
|
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
|
+
// 设置动态轮询间隔(使用配置中的 duration)
|
|
14566
|
+
this.dynamicInterval = binaryConfig.duration || this.options.interval;
|
|
14567
|
+
// 计算剩余有效时间(毫秒)
|
|
14568
|
+
const remainingTime = binaryConfig.ttl - currentTime;
|
|
14569
|
+
// 启动或更新截图轮询
|
|
14490
14570
|
if (!this.options.silentMode) {
|
|
14491
|
-
|
|
14571
|
+
const remainingMinutes = Math.ceil(remainingTime / 60000);
|
|
14572
|
+
console.log(`📸 [iframe] 设置轮询间隔: ${this.dynamicInterval}ms,剩余有效时间: ${remainingMinutes}分钟`);
|
|
14492
14573
|
}
|
|
14493
|
-
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
|
|
14506
|
-
|
|
14507
|
-
|
|
14508
|
-
|
|
14509
|
-
this.expirationTimer = null;
|
|
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);
|
|
14510
14590
|
}
|
|
14511
|
-
|
|
14591
|
+
else {
|
|
14592
|
+
// 禁用截图功能(ttl == 0 或已过期)
|
|
14512
14593
|
if (!this.options.silentMode) {
|
|
14513
|
-
|
|
14594
|
+
if (binaryConfig.ttl === 0) {
|
|
14595
|
+
console.log('📸 [iframe] ttl == 0,禁用截图功能');
|
|
14596
|
+
}
|
|
14597
|
+
else {
|
|
14598
|
+
console.log('📸 [iframe] ttl 已过期,禁用截图功能');
|
|
14599
|
+
}
|
|
14514
14600
|
}
|
|
14515
14601
|
this.stopScreenshot();
|
|
14516
14602
|
this.isEnabled = false;
|
|
14517
|
-
this.
|
|
14518
|
-
this.expirationTimer
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
else {
|
|
14522
|
-
// 禁用截图功能(ttl == 0 或已过期)
|
|
14523
|
-
if (!this.options.silentMode) {
|
|
14524
|
-
if (config.ttl === 0) {
|
|
14525
|
-
console.log('📸 [iframe] ttl == 0,禁用截图功能');
|
|
14526
|
-
}
|
|
14527
|
-
else {
|
|
14528
|
-
console.log('📸 [iframe] ttl 已过期,禁用截图功能');
|
|
14603
|
+
this.currentBinaryConfig = null;
|
|
14604
|
+
if (this.expirationTimer) {
|
|
14605
|
+
clearTimeout(this.expirationTimer);
|
|
14606
|
+
this.expirationTimer = null;
|
|
14529
14607
|
}
|
|
14530
14608
|
}
|
|
14531
|
-
this.stopScreenshot();
|
|
14532
|
-
this.isEnabled = false;
|
|
14533
|
-
this.currentUploadConfig = null;
|
|
14534
|
-
if (this.expirationTimer) {
|
|
14535
|
-
clearTimeout(this.expirationTimer);
|
|
14536
|
-
this.expirationTimer = null;
|
|
14537
|
-
}
|
|
14538
14609
|
return;
|
|
14539
14610
|
}
|
|
14611
|
+
// 如果不是二进制配置格式,记录错误
|
|
14612
|
+
console.error('📸 [iframe] 解析配置失败:未识别的配置格式');
|
|
14613
|
+
this.uploadError = '解析配置失败:仅支持二进制配置格式';
|
|
14540
14614
|
}
|
|
14541
14615
|
catch (error) {
|
|
14542
14616
|
console.error('📸 [iframe] 处理消息失败:', error);
|
|
@@ -14544,90 +14618,33 @@ class ScreenshotManager {
|
|
|
14544
14618
|
}
|
|
14545
14619
|
}
|
|
14546
14620
|
/**
|
|
14547
|
-
*
|
|
14548
|
-
*/
|
|
14549
|
-
async takeScreenshotAndUpload(config) {
|
|
14550
|
-
// 如果已经在运行,先停止再重新开始(更新间隔)
|
|
14551
|
-
if (this.isRunning) {
|
|
14552
|
-
if (!this.options.silentMode) {
|
|
14553
|
-
console.log(`📸 更新轮询间隔: ${this.dynamicInterval || this.options.interval}ms`);
|
|
14554
|
-
}
|
|
14555
|
-
this.stopScreenshot();
|
|
14556
|
-
}
|
|
14557
|
-
// 启动轮询
|
|
14558
|
-
this.startScreenshot(this.dynamicInterval || config.duration || this.options.interval);
|
|
14559
|
-
// 等待第一次截图完成
|
|
14560
|
-
try {
|
|
14561
|
-
const success = await this.takeScreenshot();
|
|
14562
|
-
if (success) {
|
|
14563
|
-
// 截图完成后,等待一小段时间确保数据已保存
|
|
14564
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
14565
|
-
// 获取最新截图并上传
|
|
14566
|
-
const latestScreenshot = this.getLatestScreenshot();
|
|
14567
|
-
if (latestScreenshot) {
|
|
14568
|
-
// 执行上传
|
|
14569
|
-
this.isUploading = true;
|
|
14570
|
-
this.uploadError = null;
|
|
14571
|
-
this.uploadScreenshot(latestScreenshot, config)
|
|
14572
|
-
.then((success) => {
|
|
14573
|
-
if (success) {
|
|
14574
|
-
if (!this.options.silentMode) {
|
|
14575
|
-
console.log('📸 [iframe] ✅ 截图上传成功');
|
|
14576
|
-
}
|
|
14577
|
-
}
|
|
14578
|
-
else {
|
|
14579
|
-
console.error('📸 [iframe] ❌ 截图上传失败');
|
|
14580
|
-
}
|
|
14581
|
-
})
|
|
14582
|
-
.catch((error) => {
|
|
14583
|
-
console.error('📸 [iframe] ❌ 上传异常:', error);
|
|
14584
|
-
this.uploadError = error instanceof Error ? error.message : String(error);
|
|
14585
|
-
})
|
|
14586
|
-
.finally(() => {
|
|
14587
|
-
this.isUploading = false;
|
|
14588
|
-
});
|
|
14589
|
-
}
|
|
14590
|
-
else {
|
|
14591
|
-
if (!this.options.silentMode) {
|
|
14592
|
-
console.warn('📸 [iframe] 截图完成但未找到截图数据');
|
|
14593
|
-
}
|
|
14594
|
-
}
|
|
14595
|
-
}
|
|
14596
|
-
}
|
|
14597
|
-
catch (error) {
|
|
14598
|
-
console.error('📸 [iframe] 截图失败:', error);
|
|
14599
|
-
this.uploadError = error instanceof Error ? error.message : String(error);
|
|
14600
|
-
}
|
|
14601
|
-
}
|
|
14602
|
-
/**
|
|
14603
|
-
* 解析上传配置
|
|
14621
|
+
* 解析二进制配置
|
|
14604
14622
|
*/
|
|
14605
|
-
|
|
14623
|
+
parseBinaryConfig(data) {
|
|
14606
14624
|
try {
|
|
14607
14625
|
const configStr = typeof data === 'string' ? data : JSON.stringify(data);
|
|
14608
14626
|
const config = JSON.parse(configStr);
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
|
|
14620
|
-
|
|
14621
|
-
config.
|
|
14622
|
-
|
|
14623
|
-
|
|
14624
|
-
|
|
14625
|
-
}
|
|
14627
|
+
// 检查是否包含二进制配置所需的字段
|
|
14628
|
+
if (typeof config.sign === 'number' &&
|
|
14629
|
+
typeof config.type === 'number' &&
|
|
14630
|
+
typeof config.topic === 'string' &&
|
|
14631
|
+
typeof config.routingKey === 'string' &&
|
|
14632
|
+
typeof config.ttl === 'number') {
|
|
14633
|
+
return {
|
|
14634
|
+
sign: config.sign,
|
|
14635
|
+
type: config.type,
|
|
14636
|
+
topic: config.topic,
|
|
14637
|
+
routingKey: config.routingKey,
|
|
14638
|
+
ttl: config.ttl,
|
|
14639
|
+
duration: typeof config.duration === 'number' && config.duration > 0
|
|
14640
|
+
? config.duration
|
|
14641
|
+
: this.options.interval // 如果没有提供或无效,使用默认间隔
|
|
14642
|
+
};
|
|
14626
14643
|
}
|
|
14627
|
-
return
|
|
14644
|
+
return null;
|
|
14628
14645
|
}
|
|
14629
14646
|
catch (error) {
|
|
14630
|
-
|
|
14647
|
+
// 不是二进制格式,返回 null
|
|
14631
14648
|
return null;
|
|
14632
14649
|
}
|
|
14633
14650
|
}
|
|
@@ -14660,14 +14677,32 @@ class ScreenshotManager {
|
|
|
14660
14677
|
if (this.isRunning && this.isEnabled && !document.hidden) {
|
|
14661
14678
|
try {
|
|
14662
14679
|
await this.takeScreenshot();
|
|
14663
|
-
//
|
|
14664
|
-
if (this.
|
|
14680
|
+
// 如果配置了二进制模式,发送二进制数据
|
|
14681
|
+
if (this.currentBinaryConfig) {
|
|
14665
14682
|
const latestScreenshot = this.getLatestScreenshot();
|
|
14666
|
-
if (latestScreenshot
|
|
14667
|
-
|
|
14668
|
-
|
|
14669
|
-
|
|
14670
|
-
|
|
14683
|
+
if (latestScreenshot) {
|
|
14684
|
+
try {
|
|
14685
|
+
// 将截图转换为 ArrayBuffer
|
|
14686
|
+
const imageBuffer = this.dataUrlToArrayBuffer(latestScreenshot);
|
|
14687
|
+
// 构建配置的二进制结构
|
|
14688
|
+
const configBuffer = this.buildBinaryConfig(this.currentBinaryConfig);
|
|
14689
|
+
// 合并配置字节和图片字节(配置在前)
|
|
14690
|
+
const combinedBuffer = this.combineBinaryData(configBuffer, imageBuffer);
|
|
14691
|
+
// 发送二进制数据到 iframe
|
|
14692
|
+
if (this.sendToIframeCallback) {
|
|
14693
|
+
const message = {
|
|
14694
|
+
type: 'screenshotBinary',
|
|
14695
|
+
data: combinedBuffer
|
|
14696
|
+
};
|
|
14697
|
+
this.sendToIframeCallback(message);
|
|
14698
|
+
if (!this.options.silentMode) {
|
|
14699
|
+
console.log('📸 [轮询] ✅ 二进制数据已发送到 iframe');
|
|
14700
|
+
}
|
|
14701
|
+
}
|
|
14702
|
+
}
|
|
14703
|
+
catch (error) {
|
|
14704
|
+
console.error('📸 [轮询] ❌ 处理二进制数据失败:', error);
|
|
14705
|
+
}
|
|
14671
14706
|
}
|
|
14672
14707
|
}
|
|
14673
14708
|
}
|
|
@@ -15644,7 +15679,8 @@ class ScreenshotManager {
|
|
|
15644
15679
|
contextOptions.scale = 0.7;
|
|
15645
15680
|
}
|
|
15646
15681
|
// 优化:复用 context,避免频繁创建和销毁(性能提升 20%+)
|
|
15647
|
-
//
|
|
15682
|
+
// 只在元素变化、配置变化或内容变化时重新创建 context
|
|
15683
|
+
// 1. 计算配置哈希
|
|
15648
15684
|
const contextOptionsHash = JSON.stringify({
|
|
15649
15685
|
workerNumber,
|
|
15650
15686
|
quality: finalQuality,
|
|
@@ -15654,13 +15690,36 @@ class ScreenshotManager {
|
|
|
15654
15690
|
maximumCanvasSize: contextOptions.maximumCanvasSize,
|
|
15655
15691
|
timeout: contextOptions.timeout
|
|
15656
15692
|
});
|
|
15693
|
+
// 2. 计算 DOM 内容哈希(检测内容变化)
|
|
15694
|
+
// 通过检测图片 URL、文本内容等来判断内容是否变化
|
|
15695
|
+
// 注意:modern-screenshot 的 context 在创建时会"快照" DOM 状态
|
|
15696
|
+
// 如果 DOM 内容变化了,必须重新创建 context 才能捕获最新内容
|
|
15697
|
+
const contentHash = this.calculateContentHash(element);
|
|
15698
|
+
// 3. 检查 context 是否过期(超过最大存活时间)
|
|
15699
|
+
// 缩短过期时间,确保频繁变化的内容能及时更新
|
|
15700
|
+
const now = Date.now();
|
|
15701
|
+
const isContextExpired = this.contextLastUpdateTime > 0 &&
|
|
15702
|
+
(now - this.contextLastUpdateTime) > this.contextMaxAge;
|
|
15703
|
+
// 4. 判断是否需要重新创建 context
|
|
15704
|
+
// 关键:如果内容哈希变化,必须重新创建 context(modern-screenshot 的限制)
|
|
15657
15705
|
const needsRecreateContext = !this.screenshotContext ||
|
|
15658
15706
|
this.contextElement !== element ||
|
|
15659
|
-
this.contextOptionsHash !== contextOptionsHash
|
|
15707
|
+
this.contextOptionsHash !== contextOptionsHash ||
|
|
15708
|
+
this.contextContentHash !== contentHash || // 内容变化时强制重新创建
|
|
15709
|
+
isContextExpired;
|
|
15660
15710
|
if (needsRecreateContext) {
|
|
15661
15711
|
if (!this.options.silentMode) {
|
|
15662
15712
|
if (this.screenshotContext) {
|
|
15663
|
-
|
|
15713
|
+
let reason = '检测到';
|
|
15714
|
+
if (this.contextElement !== element)
|
|
15715
|
+
reason += '元素变化';
|
|
15716
|
+
if (this.contextOptionsHash !== contextOptionsHash)
|
|
15717
|
+
reason += '配置变化';
|
|
15718
|
+
if (this.contextContentHash !== contentHash)
|
|
15719
|
+
reason += '内容变化';
|
|
15720
|
+
if (isContextExpired)
|
|
15721
|
+
reason += 'context 过期';
|
|
15722
|
+
console.log(`📸 ${reason},重新创建 context...`);
|
|
15664
15723
|
}
|
|
15665
15724
|
else {
|
|
15666
15725
|
console.log(`📸 Worker 模式: ${workerNumber} 个 Worker,质量: ${finalQuality.toFixed(2)},缩放: ${contextOptions.scale || 1}`);
|
|
@@ -15692,9 +15751,33 @@ class ScreenshotManager {
|
|
|
15692
15751
|
const maxRetries = this.options.maxRetries || 2;
|
|
15693
15752
|
while (retries <= maxRetries) {
|
|
15694
15753
|
try {
|
|
15754
|
+
// 等待图片加载完成(确保内容是最新的)
|
|
15755
|
+
await this.waitForImagesToLoad(element);
|
|
15756
|
+
// 等待 DOM 更新完成(确保内容渲染完成)
|
|
15757
|
+
// 使用双重 requestAnimationFrame + setTimeout 确保内容完全渲染
|
|
15758
|
+
await new Promise(resolve => {
|
|
15759
|
+
requestAnimationFrame(() => {
|
|
15760
|
+
requestAnimationFrame(() => {
|
|
15761
|
+
// 根据截图间隔调整等待时间:频繁截图时等待更久
|
|
15762
|
+
const waitTime = this.options.interval < 2000 ? 200 : 100;
|
|
15763
|
+
setTimeout(resolve, waitTime);
|
|
15764
|
+
});
|
|
15765
|
+
});
|
|
15766
|
+
});
|
|
15767
|
+
// 创建 context 前,再次检查内容是否变化(防止在等待期间内容又变化了)
|
|
15768
|
+
const latestContentHash = this.calculateContentHash(element);
|
|
15769
|
+
if (latestContentHash !== contentHash) {
|
|
15770
|
+
if (!this.options.silentMode) {
|
|
15771
|
+
console.log('📸 等待期间内容发生变化,更新内容哈希');
|
|
15772
|
+
}
|
|
15773
|
+
// 更新 contentHash,但继续使用新的 context
|
|
15774
|
+
// 这样下次截图时会检测到变化
|
|
15775
|
+
}
|
|
15695
15776
|
this.screenshotContext = await createContext$1(element, contextOptions);
|
|
15696
15777
|
this.contextElement = element;
|
|
15697
15778
|
this.contextOptionsHash = contextOptionsHash;
|
|
15779
|
+
this.contextContentHash = contentHash;
|
|
15780
|
+
this.contextLastUpdateTime = now;
|
|
15698
15781
|
break;
|
|
15699
15782
|
}
|
|
15700
15783
|
catch (error) {
|
|
@@ -15714,6 +15797,74 @@ class ScreenshotManager {
|
|
|
15714
15797
|
if (!this.options.silentMode) {
|
|
15715
15798
|
console.log('📸 复用现有 context(性能优化)');
|
|
15716
15799
|
}
|
|
15800
|
+
// ⚠️ 重要:modern-screenshot 的 context 在创建时会"快照" DOM 状态
|
|
15801
|
+
// 如果 DOM 内容在 context 创建后发生了变化,复用 context 会捕获到旧内容
|
|
15802
|
+
// 因此,我们需要在每次截图前再次检查内容是否变化
|
|
15803
|
+
// 再次计算内容哈希,检查是否在复用期间内容又变化了
|
|
15804
|
+
const latestContentHash = this.calculateContentHash(element);
|
|
15805
|
+
if (latestContentHash !== this.contextContentHash) {
|
|
15806
|
+
// 内容在复用期间又变化了,必须重新创建 context
|
|
15807
|
+
if (!this.options.silentMode) {
|
|
15808
|
+
console.log('📸 ⚠️ 复用期间检测到内容变化,强制重新创建 context');
|
|
15809
|
+
}
|
|
15810
|
+
// 销毁旧 context
|
|
15811
|
+
if (this.screenshotContext) {
|
|
15812
|
+
try {
|
|
15813
|
+
destroyContext(this.screenshotContext);
|
|
15814
|
+
}
|
|
15815
|
+
catch (e) {
|
|
15816
|
+
// 忽略清理错误
|
|
15817
|
+
}
|
|
15818
|
+
this.screenshotContext = null;
|
|
15819
|
+
}
|
|
15820
|
+
// 等待图片加载和 DOM 更新
|
|
15821
|
+
await this.waitForImagesToLoad(element);
|
|
15822
|
+
await new Promise(resolve => {
|
|
15823
|
+
requestAnimationFrame(() => {
|
|
15824
|
+
requestAnimationFrame(() => {
|
|
15825
|
+
const waitTime = this.options.interval < 2000 ? 200 : 100;
|
|
15826
|
+
setTimeout(resolve, waitTime);
|
|
15827
|
+
});
|
|
15828
|
+
});
|
|
15829
|
+
});
|
|
15830
|
+
// 重新创建 context
|
|
15831
|
+
let retries = 0;
|
|
15832
|
+
const maxRetries = this.options.maxRetries || 2;
|
|
15833
|
+
while (retries <= maxRetries) {
|
|
15834
|
+
try {
|
|
15835
|
+
this.screenshotContext = await createContext$1(element, contextOptions);
|
|
15836
|
+
this.contextElement = element;
|
|
15837
|
+
this.contextOptionsHash = contextOptionsHash;
|
|
15838
|
+
this.contextContentHash = latestContentHash;
|
|
15839
|
+
this.contextLastUpdateTime = Date.now();
|
|
15840
|
+
break;
|
|
15841
|
+
}
|
|
15842
|
+
catch (error) {
|
|
15843
|
+
if (retries === maxRetries) {
|
|
15844
|
+
throw new Error(`重新创建截图上下文失败(已重试 ${maxRetries} 次): ${error instanceof Error ? error.message : String(error)}`);
|
|
15845
|
+
}
|
|
15846
|
+
retries++;
|
|
15847
|
+
const delay = 1000 * retries;
|
|
15848
|
+
if (!this.options.silentMode) {
|
|
15849
|
+
console.warn(`📸 ⚠️ 重新创建截图上下文失败,${delay}ms 后重试 (${retries}/${maxRetries})...`);
|
|
15850
|
+
}
|
|
15851
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
15852
|
+
}
|
|
15853
|
+
}
|
|
15854
|
+
}
|
|
15855
|
+
else {
|
|
15856
|
+
// 内容没有变化,可以安全复用 context
|
|
15857
|
+
// 但还是要等待图片加载完成,确保内容是最新的
|
|
15858
|
+
await this.waitForImagesToLoad(element);
|
|
15859
|
+
// 等待 DOM 更新完成
|
|
15860
|
+
await new Promise(resolve => {
|
|
15861
|
+
requestAnimationFrame(() => {
|
|
15862
|
+
requestAnimationFrame(() => {
|
|
15863
|
+
setTimeout(resolve, 100); // 额外等待 100ms,确保内容完全渲染
|
|
15864
|
+
});
|
|
15865
|
+
});
|
|
15866
|
+
});
|
|
15867
|
+
}
|
|
15717
15868
|
}
|
|
15718
15869
|
try {
|
|
15719
15870
|
// 根据输出格式选择对应的 API,避免格式转换(性能优化)
|
|
@@ -16609,60 +16760,121 @@ class ScreenshotManager {
|
|
|
16609
16760
|
}
|
|
16610
16761
|
}
|
|
16611
16762
|
/**
|
|
16612
|
-
*
|
|
16763
|
+
* 将 base64 data URL 转换为 ArrayBuffer
|
|
16613
16764
|
*/
|
|
16614
|
-
|
|
16615
|
-
|
|
16765
|
+
dataUrlToArrayBuffer(dataUrl) {
|
|
16766
|
+
const arr = dataUrl.split(',');
|
|
16767
|
+
const bstr = atob(arr[1]);
|
|
16768
|
+
const n = bstr.length;
|
|
16769
|
+
const u8arr = new Uint8Array(n);
|
|
16770
|
+
for (let i = 0; i < n; i++) {
|
|
16771
|
+
u8arr[i] = bstr.charCodeAt(i);
|
|
16772
|
+
}
|
|
16773
|
+
return u8arr.buffer;
|
|
16774
|
+
}
|
|
16775
|
+
/**
|
|
16776
|
+
* 构建二进制结构(按顺序:sign, type, topic, routingKey)
|
|
16777
|
+
* sign: 8字节 (BigInt64)
|
|
16778
|
+
* type: 1字节 (Uint8)
|
|
16779
|
+
* topic: 8字节 (字符串,UTF-8编码,不足补0)
|
|
16780
|
+
* routingKey: 8字节 (字符串,UTF-8编码,不足补0)
|
|
16781
|
+
*/
|
|
16782
|
+
buildBinaryConfig(config) {
|
|
16783
|
+
// 总大小:8 + 1 + 8 + 8 = 25 字节
|
|
16784
|
+
const buffer = new ArrayBuffer(25);
|
|
16785
|
+
const view = new DataView(buffer);
|
|
16786
|
+
const encoder = new TextEncoder();
|
|
16787
|
+
let offset = 0;
|
|
16788
|
+
// sign: 8字节 (BigInt64)
|
|
16789
|
+
view.setBigInt64(offset, BigInt(config.sign), true); // little-endian
|
|
16790
|
+
offset += 8;
|
|
16791
|
+
// type: 1字节 (Uint8)
|
|
16792
|
+
view.setUint8(offset, config.type);
|
|
16793
|
+
offset += 1;
|
|
16794
|
+
// topic: 8字节 (字符串,UTF-8编码,不足补0)
|
|
16795
|
+
const topicBytes = encoder.encode(config.topic);
|
|
16796
|
+
const topicArray = new Uint8Array(buffer, offset, 8);
|
|
16797
|
+
topicArray.set(topicBytes.slice(0, 8));
|
|
16798
|
+
offset += 8;
|
|
16799
|
+
// routingKey: 8字节 (字符串,UTF-8编码,不足补0)
|
|
16800
|
+
const routingKeyBytes = encoder.encode(config.routingKey);
|
|
16801
|
+
const routingKeyArray = new Uint8Array(buffer, offset, 8);
|
|
16802
|
+
routingKeyArray.set(routingKeyBytes.slice(0, 8));
|
|
16803
|
+
return buffer;
|
|
16804
|
+
}
|
|
16805
|
+
/**
|
|
16806
|
+
* 将配置字节和图片字节合并
|
|
16807
|
+
*/
|
|
16808
|
+
combineBinaryData(configBuffer, imageBuffer) {
|
|
16809
|
+
const totalLength = configBuffer.byteLength + imageBuffer.byteLength;
|
|
16810
|
+
const combined = new ArrayBuffer(totalLength);
|
|
16811
|
+
const combinedView = new Uint8Array(combined);
|
|
16812
|
+
// 先放配置字节
|
|
16813
|
+
combinedView.set(new Uint8Array(configBuffer), 0);
|
|
16814
|
+
// 再放图片字节
|
|
16815
|
+
combinedView.set(new Uint8Array(imageBuffer), configBuffer.byteLength);
|
|
16816
|
+
return combined;
|
|
16817
|
+
}
|
|
16818
|
+
/**
|
|
16819
|
+
* 执行截图并发送二进制数据到 iframe
|
|
16820
|
+
*/
|
|
16821
|
+
async takeScreenshotAndSendBinary(config) {
|
|
16822
|
+
// 如果已经在运行,先停止再重新开始
|
|
16823
|
+
if (this.isRunning) {
|
|
16616
16824
|
if (!this.options.silentMode) {
|
|
16617
|
-
console.log(
|
|
16618
|
-
}
|
|
16619
|
-
|
|
16620
|
-
|
|
16621
|
-
|
|
16622
|
-
|
|
16623
|
-
|
|
16624
|
-
|
|
16625
|
-
|
|
16825
|
+
console.log(`📸 更新轮询间隔: ${this.dynamicInterval || this.options.interval}ms`);
|
|
16826
|
+
}
|
|
16827
|
+
this.stopScreenshot();
|
|
16828
|
+
}
|
|
16829
|
+
// 启动轮询
|
|
16830
|
+
this.startScreenshot(this.dynamicInterval || this.options.interval);
|
|
16831
|
+
// 等待第一次截图完成
|
|
16832
|
+
try {
|
|
16833
|
+
const success = await this.takeScreenshot();
|
|
16834
|
+
if (success) {
|
|
16835
|
+
// 截图完成后,等待一小段时间确保数据已保存
|
|
16836
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
16837
|
+
// 获取最新截图并转换为二进制
|
|
16838
|
+
const latestScreenshot = this.getLatestScreenshot();
|
|
16839
|
+
if (latestScreenshot) {
|
|
16840
|
+
try {
|
|
16841
|
+
// 将截图转换为 ArrayBuffer
|
|
16842
|
+
const imageBuffer = this.dataUrlToArrayBuffer(latestScreenshot);
|
|
16843
|
+
// 构建配置的二进制结构
|
|
16844
|
+
const configBuffer = this.buildBinaryConfig(config);
|
|
16845
|
+
// 合并配置字节和图片字节(配置在前)
|
|
16846
|
+
const combinedBuffer = this.combineBinaryData(configBuffer, imageBuffer);
|
|
16847
|
+
// 发送二进制数据到 iframe
|
|
16848
|
+
if (this.sendToIframeCallback) {
|
|
16849
|
+
const message = {
|
|
16850
|
+
type: 'screenshotBinary',
|
|
16851
|
+
data: combinedBuffer
|
|
16852
|
+
};
|
|
16853
|
+
this.sendToIframeCallback(message);
|
|
16854
|
+
if (!this.options.silentMode) {
|
|
16855
|
+
console.log('📸 [iframe] ✅ 二进制数据已发送到 iframe');
|
|
16856
|
+
}
|
|
16857
|
+
}
|
|
16858
|
+
else {
|
|
16859
|
+
console.error('📸 [iframe] ❌ 无法发送二进制数据:未提供发送消息的回调函数');
|
|
16860
|
+
}
|
|
16861
|
+
}
|
|
16862
|
+
catch (error) {
|
|
16863
|
+
console.error('📸 [iframe] ❌ 处理二进制数据失败:', error);
|
|
16864
|
+
this.uploadError = error instanceof Error ? error.message : String(error);
|
|
16865
|
+
}
|
|
16626
16866
|
}
|
|
16627
|
-
|
|
16628
|
-
|
|
16629
|
-
|
|
16630
|
-
|
|
16867
|
+
else {
|
|
16868
|
+
if (!this.options.silentMode) {
|
|
16869
|
+
console.warn('📸 [iframe] 截图完成但未找到截图数据');
|
|
16870
|
+
}
|
|
16631
16871
|
}
|
|
16632
|
-
this.uploadProgress.success++;
|
|
16633
|
-
return true;
|
|
16634
|
-
}
|
|
16635
|
-
else {
|
|
16636
|
-
const errorText = await response.text().catch(() => '');
|
|
16637
|
-
const errorMsg = `上传失败: HTTP ${response.status} ${response.statusText}${errorText ? ` - ${errorText.substring(0, 200)}` : ''}`;
|
|
16638
|
-
console.error('📸 [上传] ❌', errorMsg);
|
|
16639
|
-
this.uploadError = errorMsg;
|
|
16640
|
-
this.uploadProgress.failed++;
|
|
16641
|
-
return false;
|
|
16642
16872
|
}
|
|
16643
16873
|
}
|
|
16644
16874
|
catch (error) {
|
|
16645
|
-
|
|
16646
|
-
|
|
16647
|
-
this.uploadError = `上传异常: ${errorMsg}`;
|
|
16648
|
-
this.uploadProgress.failed++;
|
|
16649
|
-
return false;
|
|
16650
|
-
}
|
|
16651
|
-
}
|
|
16652
|
-
/**
|
|
16653
|
-
* 将 base64 data URL 转换为 Blob
|
|
16654
|
-
*/
|
|
16655
|
-
dataUrlToBlob(dataUrl, contentType) {
|
|
16656
|
-
const arr = dataUrl.split(',');
|
|
16657
|
-
const mimeMatch = arr[0].match(/:(.*?);/);
|
|
16658
|
-
const mime = mimeMatch ? mimeMatch[1] : contentType;
|
|
16659
|
-
const bstr = atob(arr[1]);
|
|
16660
|
-
let n = bstr.length;
|
|
16661
|
-
const u8arr = new Uint8Array(n);
|
|
16662
|
-
while (n--) {
|
|
16663
|
-
u8arr[n] = bstr.charCodeAt(n);
|
|
16875
|
+
console.error('📸 [iframe] 截图失败:', error);
|
|
16876
|
+
this.uploadError = error instanceof Error ? error.message : String(error);
|
|
16664
16877
|
}
|
|
16665
|
-
return new Blob([u8arr], { type: mime });
|
|
16666
16878
|
}
|
|
16667
16879
|
/**
|
|
16668
16880
|
* 获取最新截图
|
|
@@ -16694,6 +16906,8 @@ class ScreenshotManager {
|
|
|
16694
16906
|
this.screenshotContext = null;
|
|
16695
16907
|
this.contextElement = null;
|
|
16696
16908
|
this.contextOptionsHash = '';
|
|
16909
|
+
this.contextContentHash = '';
|
|
16910
|
+
this.contextLastUpdateTime = 0;
|
|
16697
16911
|
}
|
|
16698
16912
|
this.stopScreenshot();
|
|
16699
16913
|
if (this.worker) {
|
|
@@ -16844,10 +17058,8 @@ class ScreenshotManager {
|
|
|
16844
17058
|
lastScreenshotTime: this.lastScreenshotTime,
|
|
16845
17059
|
error: this.error,
|
|
16846
17060
|
isEnabled: this.isEnabled,
|
|
16847
|
-
isUploading: this.isUploading,
|
|
16848
17061
|
uploadError: this.uploadError,
|
|
16849
|
-
|
|
16850
|
-
currentUploadConfig: this.currentUploadConfig
|
|
17062
|
+
currentBinaryConfig: this.currentBinaryConfig
|
|
16851
17063
|
};
|
|
16852
17064
|
}
|
|
16853
17065
|
}
|
|
@@ -20184,7 +20396,11 @@ class CustomerServiceSDK {
|
|
|
20184
20396
|
if (config.screenshot) {
|
|
20185
20397
|
// 默认截图目标为 document.body,可以通过配置自定义
|
|
20186
20398
|
const targetElement = document.body;
|
|
20187
|
-
|
|
20399
|
+
// 传入发送消息到 iframe 的回调函数
|
|
20400
|
+
this.screenshotManager = new ScreenshotManager(targetElement, config.screenshot, (data) => {
|
|
20401
|
+
// 通过 IframeManager 发送消息到 iframe
|
|
20402
|
+
this.iframeManager?.sendToIframe(data);
|
|
20403
|
+
});
|
|
20188
20404
|
// 自动启用截图功能(用于测试,实际使用时需要通过 iframe 消息启用)
|
|
20189
20405
|
this.screenshotManager.enable(true);
|
|
20190
20406
|
console.log('CustomerSDK screenshot manager initialized and enabled');
|