CatPtain commited on
Commit
cf5bd9a
·
verified ·
1 Parent(s): aaa3dc1

Upload screenshotService.js

Browse files
backend/src/services/screenshotService.js CHANGED
@@ -4,13 +4,15 @@ class ScreenshotService {
4
  constructor() {
5
  this.browser = null;
6
  this.isInitialized = false;
7
- this.maxRetries = 3;
8
  this.browserLaunchRetries = 0;
9
- this.maxBrowserLaunchRetries = 3;
10
  this.isClosing = false;
 
11
  }
12
 
13
  async initBrowser() {
 
14
  if (this.isClosing) {
15
  console.log('浏览器正在关闭中,等待重新初始化...');
16
  this.browser = null;
@@ -20,16 +22,17 @@ class ScreenshotService {
20
  if (!this.browser) {
21
  try {
22
  console.log('初始化Puppeteer浏览器...');
23
- this.browser = await puppeteer.launch({
 
24
  headless: 'new',
 
 
25
  args: [
26
  '--no-sandbox',
27
  '--disable-setuid-sandbox',
28
  '--disable-dev-shm-usage',
29
  '--disable-accelerated-2d-canvas',
30
  '--no-first-run',
31
- '--no-zygote',
32
- '--single-process',
33
  '--disable-gpu',
34
  '--disable-background-timer-throttling',
35
  '--disable-backgrounding-occluded-windows',
@@ -44,20 +47,31 @@ class ScreenshotService {
44
  '--disable-sync',
45
  '--metrics-recording-only',
46
  '--disable-domain-reliability',
47
- '--disable-component-extensions-with-background-pages',
48
  '--force-device-scale-factor=1',
49
- '--enable-precise-memory-info',
50
  '--disable-features=VizDisplayCompositor',
51
  '--run-all-compositor-stages-before-draw',
52
  '--disable-new-content-rendering-timeout',
53
  '--disable-ipc-flooding-protection',
54
  '--disable-hang-monitor',
55
  '--disable-prompt-on-repost',
56
- '--max_old_space_size=512'
57
- ],
58
- timeout: 30000,
59
- protocolTimeout: 30000
60
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  // 监听浏览器断开连接事件
63
  this.browser.on('disconnected', () => {
@@ -66,16 +80,6 @@ class ScreenshotService {
66
  this.isClosing = false;
67
  });
68
 
69
- // 监听目标创建事件
70
- this.browser.on('targetcreated', (target) => {
71
- console.log('新目标创建:', target.type());
72
- });
73
-
74
- // 监听目标销毁事件
75
- this.browser.on('targetdestroyed', (target) => {
76
- console.log('目标销毁:', target.type());
77
- });
78
-
79
  console.log('Puppeteer浏览器初始化完成');
80
  this.browserLaunchRetries = 0;
81
  } catch (error) {
@@ -84,21 +88,25 @@ class ScreenshotService {
84
 
85
  if (this.browserLaunchRetries <= this.maxBrowserLaunchRetries) {
86
  console.log(`尝试重新初始化浏览器 (${this.browserLaunchRetries}/${this.maxBrowserLaunchRetries})`);
87
- await this.delay(2000); // 等待2秒后重试
88
  return this.initBrowser();
89
  } else {
90
- throw new Error(`浏览器初始化失败,已重试${this.maxBrowserLaunchRetries}次: ${error.message}`);
 
 
91
  }
92
  }
93
  }
94
 
95
  // 验证浏览器是否仍然连接
96
- try {
97
- await this.browser.version();
98
- } catch (error) {
99
- console.warn('浏览器连接已断开,重新初始化:', error.message);
100
- this.browser = null;
101
- return this.initBrowser();
 
 
102
  }
103
 
104
  return this.browser;
@@ -146,15 +154,6 @@ class ScreenshotService {
146
  };
147
  }
148
 
149
- // 从CSS中提取
150
- const cssMatch = htmlContent.match(/width:\s*(\d+)px.*height:\s*(\d+)px/);
151
- if (cssMatch) {
152
- return {
153
- width: parseInt(cssMatch[1]),
154
- height: parseInt(cssMatch[2])
155
- };
156
- }
157
-
158
  // 默认尺寸
159
  return { width: 960, height: 720 };
160
  } catch (error) {
@@ -163,6 +162,31 @@ class ScreenshotService {
163
  }
164
  }
165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  // 优化HTML内容以确保精确截图
167
  optimizeHtmlForScreenshot(htmlContent, targetWidth, targetHeight) {
168
  console.log(`优化HTML for精确截图, 目标尺寸: ${targetWidth}x${targetHeight}`);
@@ -244,9 +268,7 @@ class ScreenshotService {
244
  height: 0 !important;
245
  }
246
 
247
- html {
248
- scrollbar-width: none !important;
249
- }
250
 
251
  /* 禁用用户交互 */
252
  * {
@@ -254,179 +276,22 @@ class ScreenshotService {
254
  -moz-user-select: none !important;
255
  -ms-user-select: none !important;
256
  user-select: none !important;
257
- -webkit-user-drag: none !important;
258
- -khtml-user-drag: none !important;
259
- -moz-user-drag: none !important;
260
- -o-user-drag: none !important;
261
- user-drag: none !important;
262
  pointer-events: none !important;
263
  }
264
-
265
- /* 强制移除响应式样式 */
266
- .browse-mode,
267
- .browse-mode html,
268
- .browse-mode body,
269
- .browse-mode .slide-container {
270
- display: block !important;
271
- position: fixed !important;
272
- width: ${targetWidth}px !important;
273
- height: ${targetHeight}px !important;
274
- min-width: ${targetWidth}px !important;
275
- min-height: ${targetHeight}px !important;
276
- max-width: ${targetWidth}px !important;
277
- max-height: ${targetHeight}px !important;
278
- transform: none !important;
279
- background-color: transparent !important;
280
- top: 0 !important;
281
- left: 0 !important;
282
- margin: 0 !important;
283
- padding: 0 !important;
284
- box-shadow: none !important;
285
- }
286
  </style>`
287
  );
288
 
289
- // 在</body>前插入截图专用JavaScript
290
- const finalHtml = optimizedHtml.replace(
291
- /(<\/body>)/i,
292
- `
293
- <script id="screenshot-precision-script">
294
- // 截图模式强制设置
295
- window.SCREENSHOT_EXACT_MODE = true;
296
- window.PPT_TARGET_WIDTH = ${targetWidth};
297
- window.PPT_TARGET_HEIGHT = ${targetHeight};
298
-
299
- // 立即执行强制设置,确保尺寸精确
300
- (function() {
301
- const targetW = ${targetWidth};
302
- const targetH = ${targetHeight};
303
-
304
- console.log('截图模式精确设置开始:', targetW + 'x' + targetH);
305
-
306
- const html = document.documentElement;
307
- const body = document.body;
308
-
309
- // 强制设置根元素
310
- const setElementSize = (element, width, height) => {
311
- if (!element) return;
312
-
313
- element.style.cssText = \`
314
- width: \${width}px !important;
315
- height: \${height}px !important;
316
- min-width: \${width}px !important;
317
- min-height: \${height}px !important;
318
- max-width: \${width}px !important;
319
- max-height: \${height}px !important;
320
- overflow: hidden !important;
321
- margin: 0 !important;
322
- padding: 0 !important;
323
- position: fixed !important;
324
- top: 0 !important;
325
- left: 0 !important;
326
- border: none !important;
327
- outline: none !important;
328
- transform: none !important;
329
- transform-origin: top left !important;
330
- box-sizing: border-box !important;
331
- \`;
332
- };
333
-
334
- // 设置HTML和Body
335
- setElementSize(html, targetW, targetH);
336
- setElementSize(body, targetW, targetH);
337
-
338
- // 移除可能的响应式类
339
- html.classList.remove('browse-mode');
340
- body.classList.remove('browse-mode');
341
-
342
- // 设置容器
343
- const container = document.querySelector('.slide-container');
344
- if (container) {
345
- container.style.cssText = \`
346
- width: \${targetW}px !important;
347
- height: \${targetH}px !important;
348
- min-width: \${targetW}px !important;
349
- min-height: \${targetH}px !important;
350
- max-width: \${targetW}px !important;
351
- max-height: \${targetH}px !important;
352
- position: fixed !important;
353
- top: 0 !important;
354
- left: 0 !important;
355
- overflow: hidden !important;
356
- margin: 0 !important;
357
- padding: 0 !important;
358
- border: none !important;
359
- outline: none !important;
360
- transform: none !important;
361
- box-shadow: none !important;
362
- z-index: 1 !important;
363
- \`;
364
- }
365
-
366
- console.log('截图模式精确设置完成');
367
- })();
368
-
369
- // DOM加载完成后再次确认
370
- document.addEventListener('DOMContentLoaded', function() {
371
- const html = document.documentElement;
372
- const body = document.body;
373
- const container = document.querySelector('.slide-container');
374
-
375
- // 最终强制设置,确保完全生效
376
- [html, body].forEach(element => {
377
- if (element) {
378
- element.style.width = '${targetWidth}px';
379
- element.style.height = '${targetHeight}px';
380
- element.style.minWidth = '${targetWidth}px';
381
- element.style.minHeight = '${targetHeight}px';
382
- element.style.maxWidth = '${targetWidth}px';
383
- element.style.maxHeight = '${targetHeight}px';
384
- element.style.margin = '0';
385
- element.style.padding = '0';
386
- element.style.border = 'none';
387
- element.style.outline = 'none';
388
- element.style.overflow = 'hidden';
389
- element.style.position = 'fixed';
390
- element.style.top = '0';
391
- element.style.left = '0';
392
- element.style.transform = 'none';
393
- }
394
- });
395
-
396
- if (container) {
397
- container.style.width = '${targetWidth}px';
398
- container.style.height = '${targetHeight}px';
399
- container.style.minWidth = '${targetWidth}px';
400
- container.style.minHeight = '${targetHeight}px';
401
- container.style.maxWidth = '${targetWidth}px';
402
- container.style.maxHeight = '${targetHeight}px';
403
- container.style.position = 'fixed';
404
- container.style.top = '0';
405
- container.style.left = '0';
406
- container.style.margin = '0';
407
- container.style.padding = '0';
408
- container.style.border = 'none';
409
- container.style.outline = 'none';
410
- container.style.overflow = 'hidden';
411
- container.style.transform = 'none';
412
- container.style.boxShadow = 'none';
413
- }
414
-
415
- console.log('DOM加载后的最终尺寸确认完成');
416
- });
417
- </script>
418
- $1`
419
- );
420
-
421
- console.log('HTML优化完成,已注入精确尺寸控制代码');
422
- return finalHtml;
423
  }
424
 
425
- async generateScreenshotWithRetry(htmlContent, options = {}, retryCount = 0) {
426
  try {
427
- await this.initBrowser();
 
 
 
428
 
429
- console.log(`开始生成截图... (尝试 ${retryCount + 1}/${this.maxRetries + 1})`);
430
 
431
  // 从HTML中提取PPT的精确尺寸
432
  const dimensions = this.extractPPTDimensions(htmlContent);
@@ -438,106 +303,43 @@ class ScreenshotService {
438
  // 创建新页面
439
  let page = null;
440
  try {
441
- page = await this.browser.newPage();
442
 
443
  // 设置页面超时
444
- page.setDefaultTimeout(30000);
445
- page.setDefaultNavigationTimeout(30000);
446
 
447
  // 设置精确的viewport尺寸
448
  await page.setViewport({
449
  width: dimensions.width,
450
  height: dimensions.height,
451
- deviceScaleFactor: options.deviceScaleFactor || 2, // 高清截图
452
  });
453
 
454
  // 设置页面内容
455
  await page.setContent(optimizedHtml, {
456
  waitUntil: ['load', 'domcontentloaded'],
457
- timeout: 30000
458
  });
459
 
460
  // 等待页面完全渲染
461
- await page.waitForTimeout(2000);
462
-
463
- // 执行最终的尺寸验证和强制设置
464
- await page.evaluate((targetWidth, targetHeight) => {
465
- const html = document.documentElement;
466
- const body = document.body;
467
- const container = document.querySelector('.slide-container');
468
-
469
- // 最终强制设置,确保精确尺寸
470
- const forceSize = (element, width, height) => {
471
- if (!element) return;
472
-
473
- element.style.width = width + 'px';
474
- element.style.height = height + 'px';
475
- element.style.minWidth = width + 'px';
476
- element.style.minHeight = height + 'px';
477
- element.style.maxWidth = width + 'px';
478
- element.style.maxHeight = height + 'px';
479
- element.style.margin = '0';
480
- element.style.padding = '0';
481
- element.style.border = 'none';
482
- element.style.outline = 'none';
483
- element.style.overflow = 'hidden';
484
- element.style.transform = 'none';
485
-
486
- if (element !== container) {
487
- element.style.position = 'fixed';
488
- element.style.top = '0';
489
- element.style.left = '0';
490
- }
491
- };
492
-
493
- forceSize(html, targetWidth, targetHeight);
494
- forceSize(body, targetWidth, targetHeight);
495
-
496
- if (container) {
497
- forceSize(container, targetWidth, targetHeight);
498
- container.style.position = 'fixed';
499
- container.style.top = '0';
500
- container.style.left = '0';
501
- container.style.boxShadow = 'none';
502
- container.style.zIndex = '1';
503
- }
504
-
505
- // 验证尺寸设置结果
506
- const verification = {
507
- html: html.offsetWidth + 'x' + html.offsetHeight,
508
- body: body.offsetWidth + 'x' + body.offsetHeight,
509
- container: container ? container.offsetWidth + 'x' + container.offsetHeight : 'none',
510
- target: targetWidth + 'x' + targetHeight
511
- };
512
-
513
- console.log('最终尺寸验证:', verification);
514
-
515
- // 如果尺寸不匹配,记录警告
516
- if (html.offsetWidth !== targetWidth || html.offsetHeight !== targetHeight) {
517
- console.warn('HTML尺寸不匹配目标尺寸!');
518
- }
519
-
520
- return verification;
521
- }, dimensions.width, dimensions.height);
522
-
523
- // 再次等待以确保所有更改生效
524
- await page.waitForTimeout(1000);
525
 
526
  // 执行截图,使用精确的剪裁区域
527
  const screenshot = await page.screenshot({
528
  type: options.format || 'jpeg',
529
- quality: options.quality || 95,
530
  clip: {
531
  x: 0,
532
  y: 0,
533
  width: dimensions.width,
534
  height: dimensions.height
535
  },
536
- omitBackground: false, // 包含背景
537
- captureBeyondViewport: false, // 不截取视口外内容
538
  });
539
 
540
- console.log(`截图成功生成,尺寸: ${dimensions.width}x${dimensions.height}, 数据大小: ${screenshot.length} 字节`);
541
  return screenshot;
542
 
543
  } finally {
@@ -551,7 +353,7 @@ class ScreenshotService {
551
  }
552
 
553
  } catch (error) {
554
- console.error(`截图生成失败 (尝试 ${retryCount + 1}):`, error.message);
555
 
556
  // 如果是目标关闭错误或连接断开,重置浏览器
557
  if (error.message.includes('Target closed') ||
@@ -564,17 +366,33 @@ class ScreenshotService {
564
 
565
  // 如果还有重试机会,进行重试
566
  if (retryCount < this.maxRetries) {
567
- console.log(`等待 ${(retryCount + 1) * 2} 秒后重试...`);
568
- await this.delay((retryCount + 1) * 2000);
569
- return this.generateScreenshotWithRetry(htmlContent, options, retryCount + 1);
570
  }
571
 
572
- throw new Error(`截图生成失败,已重试${this.maxRetries}次: ${error.message}`);
573
  }
574
  }
575
 
576
  async generateScreenshot(htmlContent, options = {}) {
577
- return this.generateScreenshotWithRetry(htmlContent, options, 0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
  }
579
 
580
  // 兼容旧方法名
 
4
  constructor() {
5
  this.browser = null;
6
  this.isInitialized = false;
7
+ this.maxRetries = 2;
8
  this.browserLaunchRetries = 0;
9
+ this.maxBrowserLaunchRetries = 2;
10
  this.isClosing = false;
11
+ this.isHuggingFaceSpace = process.env.SPACE_ID || process.env.HF_SPACE_ID;
12
  }
13
 
14
  async initBrowser() {
15
+ // 在Hugging Face Space环境中,Puppeteer可能不稳定,尝试更保守的配置
16
  if (this.isClosing) {
17
  console.log('浏览器正在关闭中,等待重新初始化...');
18
  this.browser = null;
 
22
  if (!this.browser) {
23
  try {
24
  console.log('初始化Puppeteer浏览器...');
25
+
26
+ const launchOptions = {
27
  headless: 'new',
28
+ timeout: 60000,
29
+ protocolTimeout: 60000,
30
  args: [
31
  '--no-sandbox',
32
  '--disable-setuid-sandbox',
33
  '--disable-dev-shm-usage',
34
  '--disable-accelerated-2d-canvas',
35
  '--no-first-run',
 
 
36
  '--disable-gpu',
37
  '--disable-background-timer-throttling',
38
  '--disable-backgrounding-occluded-windows',
 
47
  '--disable-sync',
48
  '--metrics-recording-only',
49
  '--disable-domain-reliability',
 
50
  '--force-device-scale-factor=1',
 
51
  '--disable-features=VizDisplayCompositor',
52
  '--run-all-compositor-stages-before-draw',
53
  '--disable-new-content-rendering-timeout',
54
  '--disable-ipc-flooding-protection',
55
  '--disable-hang-monitor',
56
  '--disable-prompt-on-repost',
57
+ '--memory-pressure-off',
58
+ '--max_old_space_size=1024'
59
+ ]
60
+ };
61
+
62
+ // 如果是Hugging Face Space环境,使用更保守的配置
63
+ if (this.isHuggingFaceSpace) {
64
+ console.log('检测到Hugging Face Space环境,使用保守配置');
65
+ launchOptions.args.push(
66
+ '--single-process',
67
+ '--no-zygote',
68
+ '--disable-web-security',
69
+ '--disable-features=site-per-process',
70
+ '--disable-site-isolation-trials'
71
+ );
72
+ }
73
+
74
+ this.browser = await puppeteer.launch(launchOptions);
75
 
76
  // 监听浏览器断开连接事件
77
  this.browser.on('disconnected', () => {
 
80
  this.isClosing = false;
81
  });
82
 
 
 
 
 
 
 
 
 
 
 
83
  console.log('Puppeteer浏览器初始化完成');
84
  this.browserLaunchRetries = 0;
85
  } catch (error) {
 
88
 
89
  if (this.browserLaunchRetries <= this.maxBrowserLaunchRetries) {
90
  console.log(`尝试重新初始化浏览器 (${this.browserLaunchRetries}/${this.maxBrowserLaunchRetries})`);
91
+ await this.delay(3000); // 等待3秒后重试
92
  return this.initBrowser();
93
  } else {
94
+ // 如果Puppeteer完全失败,返回null,使用fallback方法
95
+ console.warn('Puppeteer初始化完全失败,将使用fallback方法');
96
+ return null;
97
  }
98
  }
99
  }
100
 
101
  // 验证浏览器是否仍然连接
102
+ if (this.browser) {
103
+ try {
104
+ await this.browser.version();
105
+ } catch (error) {
106
+ console.warn('浏览器连接已断开,重新初始化:', error.message);
107
+ this.browser = null;
108
+ return this.initBrowser();
109
+ }
110
  }
111
 
112
  return this.browser;
 
154
  };
155
  }
156
 
 
 
 
 
 
 
 
 
 
157
  // 默认尺寸
158
  return { width: 960, height: 720 };
159
  } catch (error) {
 
162
  }
163
  }
164
 
165
+ // 生成简化的PNG图片作为fallback
166
+ generateFallbackImage(width, height, message = 'Screenshot not available') {
167
+ // 创建一个简单的SVG作为fallback
168
+ const svg = `
169
+ <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
170
+ <rect width="100%" height="100%" fill="#f0f0f0"/>
171
+ <text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle"
172
+ font-family="Arial, sans-serif" font-size="24" fill="#666">
173
+ ${message}
174
+ </text>
175
+ <text x="50%" y="60%" text-anchor="middle" dominant-baseline="middle"
176
+ font-family="Arial, sans-serif" font-size="16" fill="#999">
177
+ Size: ${width}x${height}
178
+ </text>
179
+ </svg>
180
+ `;
181
+
182
+ // 将SVG转换为base64编码的PNG
183
+ const base64Svg = Buffer.from(svg).toString('base64');
184
+ const dataUrl = `data:image/svg+xml;base64,${base64Svg}`;
185
+
186
+ // 返回简单的占位图片数据
187
+ return Buffer.from(svg, 'utf8');
188
+ }
189
+
190
  // 优化HTML内容以确保精确截图
191
  optimizeHtmlForScreenshot(htmlContent, targetWidth, targetHeight) {
192
  console.log(`优化HTML for精确截图, 目标尺寸: ${targetWidth}x${targetHeight}`);
 
268
  height: 0 !important;
269
  }
270
 
271
+ html { scrollbar-width: none !important; }
 
 
272
 
273
  /* 禁用用户交互 */
274
  * {
 
276
  -moz-user-select: none !important;
277
  -ms-user-select: none !important;
278
  user-select: none !important;
 
 
 
 
 
279
  pointer-events: none !important;
280
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  </style>`
282
  );
283
 
284
+ return optimizedHtml;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  }
286
 
287
+ async generateScreenshotWithPuppeteer(htmlContent, options = {}, retryCount = 0) {
288
  try {
289
+ const browser = await this.initBrowser();
290
+ if (!browser) {
291
+ throw new Error('浏览器初始化失败');
292
+ }
293
 
294
+ console.log(`开始生成截图... (Puppeteer尝试 ${retryCount + 1}/${this.maxRetries + 1})`);
295
 
296
  // 从HTML中提取PPT的精确尺寸
297
  const dimensions = this.extractPPTDimensions(htmlContent);
 
303
  // 创建新页面
304
  let page = null;
305
  try {
306
+ page = await browser.newPage();
307
 
308
  // 设置页面超时
309
+ page.setDefaultTimeout(45000);
310
+ page.setDefaultNavigationTimeout(45000);
311
 
312
  // 设置精确的viewport尺寸
313
  await page.setViewport({
314
  width: dimensions.width,
315
  height: dimensions.height,
316
+ deviceScaleFactor: options.deviceScaleFactor || 1, // 降低设备缩放因子以减少内存使用
317
  });
318
 
319
  // 设置页面内容
320
  await page.setContent(optimizedHtml, {
321
  waitUntil: ['load', 'domcontentloaded'],
322
+ timeout: 45000
323
  });
324
 
325
  // 等待页面完全渲染
326
+ await page.waitForTimeout(1500);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
 
328
  // 执行截图,使用精确的剪裁区域
329
  const screenshot = await page.screenshot({
330
  type: options.format || 'jpeg',
331
+ quality: options.quality || 90,
332
  clip: {
333
  x: 0,
334
  y: 0,
335
  width: dimensions.width,
336
  height: dimensions.height
337
  },
338
+ omitBackground: false,
339
+ captureBeyondViewport: false,
340
  });
341
 
342
+ console.log(`Puppeteer截图成功生成,尺寸: ${dimensions.width}x${dimensions.height}, 数据大小: ${screenshot.length} 字节`);
343
  return screenshot;
344
 
345
  } finally {
 
353
  }
354
 
355
  } catch (error) {
356
+ console.error(`Puppeteer截图生成失败 (尝试 ${retryCount + 1}):`, error.message);
357
 
358
  // 如果是目标关闭错误或连接断开,重置浏览器
359
  if (error.message.includes('Target closed') ||
 
366
 
367
  // 如果还有重试机会,进行重试
368
  if (retryCount < this.maxRetries) {
369
+ console.log(`等待 ${(retryCount + 1) * 3} 秒后重试...`);
370
+ await this.delay((retryCount + 1) * 3000);
371
+ return this.generateScreenshotWithPuppeteer(htmlContent, options, retryCount + 1);
372
  }
373
 
374
+ throw error;
375
  }
376
  }
377
 
378
  async generateScreenshot(htmlContent, options = {}) {
379
+ try {
380
+ // 首先尝试使用Puppeteer
381
+ return await this.generateScreenshotWithPuppeteer(htmlContent, options);
382
+ } catch (puppeteerError) {
383
+ console.warn('Puppeteer截图失败,使用fallback方法:', puppeteerError.message);
384
+
385
+ // 如果Puppeteer失败,生成fallback图片
386
+ const dimensions = this.extractPPTDimensions(htmlContent);
387
+ const fallbackImage = this.generateFallbackImage(
388
+ dimensions.width,
389
+ dimensions.height,
390
+ 'PPT Preview'
391
+ );
392
+
393
+ console.log(`生成fallback图片,尺寸: ${dimensions.width}x${dimensions.height}`);
394
+ return fallbackImage;
395
+ }
396
  }
397
 
398
  // 兼容旧方法名