Upload screenshotService.js
Browse files
backend/src/services/screenshotService.js
CHANGED
@@ -4,56 +4,127 @@ class ScreenshotService {
|
|
4 |
constructor() {
|
5 |
this.browser = null;
|
6 |
this.isInitialized = false;
|
|
|
|
|
|
|
|
|
7 |
}
|
8 |
|
9 |
async initBrowser() {
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
if (!this.browser) {
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
return this.browser;
|
47 |
}
|
48 |
|
49 |
async closeBrowser() {
|
50 |
-
if (this.browser) {
|
51 |
-
|
52 |
-
|
53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
}
|
55 |
}
|
56 |
|
|
|
|
|
|
|
|
|
|
|
57 |
// 从HTML中提取PPT尺寸信息
|
58 |
extractPPTDimensions(htmlContent) {
|
59 |
try {
|
@@ -351,11 +422,11 @@ class ScreenshotService {
|
|
351 |
return finalHtml;
|
352 |
}
|
353 |
|
354 |
-
async
|
355 |
try {
|
356 |
await this.initBrowser();
|
357 |
|
358 |
-
console.log(
|
359 |
|
360 |
// 从HTML中提取PPT的精确尺寸
|
361 |
const dimensions = this.extractPPTDimensions(htmlContent);
|
@@ -365,9 +436,14 @@ class ScreenshotService {
|
|
365 |
const optimizedHtml = this.optimizeHtmlForScreenshot(htmlContent, dimensions.width, dimensions.height);
|
366 |
|
367 |
// 创建新页面
|
368 |
-
|
369 |
-
|
370 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
371 |
// 设置精确的viewport尺寸
|
372 |
await page.setViewport({
|
373 |
width: dimensions.width,
|
@@ -377,7 +453,7 @@ class ScreenshotService {
|
|
377 |
|
378 |
// 设置页面内容
|
379 |
await page.setContent(optimizedHtml, {
|
380 |
-
waitUntil: ['load', 'domcontentloaded'
|
381 |
timeout: 30000
|
382 |
});
|
383 |
|
@@ -465,20 +541,70 @@ class ScreenshotService {
|
|
465 |
return screenshot;
|
466 |
|
467 |
} finally {
|
468 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
469 |
}
|
470 |
|
471 |
} catch (error) {
|
472 |
-
console.error(
|
473 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
474 |
}
|
475 |
}
|
476 |
|
|
|
|
|
|
|
|
|
477 |
// 兼容旧方法名
|
478 |
async captureScreenshot(htmlContent, width, height, options = {}) {
|
479 |
console.log('使用兼容方法captureScreenshot,建议使用generateScreenshot');
|
480 |
return this.generateScreenshot(htmlContent, options);
|
481 |
}
|
|
|
|
|
|
|
|
|
|
|
482 |
}
|
483 |
|
484 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
17 |
+
this.isClosing = false;
|
18 |
+
}
|
19 |
+
|
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',
|
36 |
+
'--disable-renderer-backgrounding',
|
37 |
+
'--disable-features=TranslateUI',
|
38 |
+
'--disable-extensions',
|
39 |
+
'--hide-scrollbars',
|
40 |
+
'--mute-audio',
|
41 |
+
'--no-default-browser-check',
|
42 |
+
'--disable-default-apps',
|
43 |
+
'--disable-background-networking',
|
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', () => {
|
64 |
+
console.log('Puppeteer浏览器连接断开');
|
65 |
+
this.browser = null;
|
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) {
|
82 |
+
console.error('Puppeteer浏览器初始化失败:', error);
|
83 |
+
this.browserLaunchRetries++;
|
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;
|
105 |
}
|
106 |
|
107 |
async closeBrowser() {
|
108 |
+
if (this.browser && !this.isClosing) {
|
109 |
+
try {
|
110 |
+
this.isClosing = true;
|
111 |
+
console.log('正在关闭Puppeteer浏览器...');
|
112 |
+
await this.browser.close();
|
113 |
+
console.log('Puppeteer浏览器已关闭');
|
114 |
+
} catch (error) {
|
115 |
+
console.warn('关闭浏览器时出错:', error.message);
|
116 |
+
} finally {
|
117 |
+
this.browser = null;
|
118 |
+
this.isClosing = false;
|
119 |
+
}
|
120 |
}
|
121 |
}
|
122 |
|
123 |
+
// 延迟函数
|
124 |
+
delay(ms) {
|
125 |
+
return new Promise(resolve => setTimeout(resolve, ms));
|
126 |
+
}
|
127 |
+
|
128 |
// 从HTML中提取PPT尺寸信息
|
129 |
extractPPTDimensions(htmlContent) {
|
130 |
try {
|
|
|
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);
|
|
|
436 |
const optimizedHtml = this.optimizeHtmlForScreenshot(htmlContent, dimensions.width, dimensions.height);
|
437 |
|
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,
|
|
|
453 |
|
454 |
// 设置页面内容
|
455 |
await page.setContent(optimizedHtml, {
|
456 |
+
waitUntil: ['load', 'domcontentloaded'],
|
457 |
timeout: 30000
|
458 |
});
|
459 |
|
|
|
541 |
return screenshot;
|
542 |
|
543 |
} finally {
|
544 |
+
if (page) {
|
545 |
+
try {
|
546 |
+
await page.close();
|
547 |
+
} catch (error) {
|
548 |
+
console.warn('关闭页面时出错:', error.message);
|
549 |
+
}
|
550 |
+
}
|
551 |
}
|
552 |
|
553 |
} catch (error) {
|
554 |
+
console.error(`截图生成失败 (尝试 ${retryCount + 1}):`, error.message);
|
555 |
+
|
556 |
+
// 如果是目标关闭错误或连接断开,重置浏览器
|
557 |
+
if (error.message.includes('Target closed') ||
|
558 |
+
error.message.includes('Connection closed') ||
|
559 |
+
error.message.includes('Protocol error')) {
|
560 |
+
console.log('检测到浏览器连接问题,重置浏览器实例');
|
561 |
+
this.browser = null;
|
562 |
+
this.isClosing = false;
|
563 |
+
}
|
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 |
// 兼容旧方法名
|
581 |
async captureScreenshot(htmlContent, width, height, options = {}) {
|
582 |
console.log('使用兼容方法captureScreenshot,建议使用generateScreenshot');
|
583 |
return this.generateScreenshot(htmlContent, options);
|
584 |
}
|
585 |
+
|
586 |
+
// 清理资源
|
587 |
+
async cleanup() {
|
588 |
+
await this.closeBrowser();
|
589 |
+
}
|
590 |
}
|
591 |
|
592 |
+
// 创建单例实例
|
593 |
+
const screenshotService = new ScreenshotService();
|
594 |
+
|
595 |
+
// 进程退出时清理资源
|
596 |
+
process.on('exit', async () => {
|
597 |
+
await screenshotService.cleanup();
|
598 |
+
});
|
599 |
+
|
600 |
+
process.on('SIGINT', async () => {
|
601 |
+
await screenshotService.cleanup();
|
602 |
+
process.exit(0);
|
603 |
+
});
|
604 |
+
|
605 |
+
process.on('SIGTERM', async () => {
|
606 |
+
await screenshotService.cleanup();
|
607 |
+
process.exit(0);
|
608 |
+
});
|
609 |
+
|
610 |
+
export default screenshotService;
|