CatPtain commited on
Commit
557d451
·
verified ·
1 Parent(s): 0b2f2ef

Upload 2 files

Browse files
Files changed (2) hide show
  1. Dockerfile +10 -13
  2. server.js +186 -423
Dockerfile CHANGED
@@ -1,22 +1,18 @@
1
- # HF Spaces Dockerfile - 修复 Chrome 路径问题
2
  FROM node:18-slim
3
 
4
- # 安装必要的系统依赖
5
  RUN apt-get update && apt-get install -y \
6
  wget \
7
  gnupg \
8
  ca-certificates \
9
- apt-transport-https \
10
- --no-install-recommends
11
-
12
- # 添加 Google Chrome 官方仓库
13
- RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \
14
- && echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list
15
-
16
- # 安装 Chrome 和相关依赖
17
- RUN apt-get update && apt-get install -y \
18
  google-chrome-stable \
19
- fonts-liberation \
 
 
20
  libappindicator3-1 \
21
  libasound2 \
22
  libatk-bridge2.0-0 \
@@ -32,7 +28,8 @@ RUN apt-get update && apt-get install -y \
32
  libxss1 \
33
  libgconf-2-4 \
34
  --no-install-recommends \
35
- && rm -rf /var/lib/apt/lists/*
 
36
 
37
  # 创建非 root 用户
38
  RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
 
1
+ # HF Spaces Dockerfile - 精简全球字体支持
2
  FROM node:18-slim
3
 
4
+ # 安装必要的系统依赖和 Chrome
5
  RUN apt-get update && apt-get install -y \
6
  wget \
7
  gnupg \
8
  ca-certificates \
9
+ && wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \
10
+ && echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \
11
+ && apt-get update && apt-get install -y \
 
 
 
 
 
 
12
  google-chrome-stable \
13
+ fonts-noto-core \
14
+ fonts-noto-cjk \
15
+ fonts-noto-color-emoji \
16
  libappindicator3-1 \
17
  libasound2 \
18
  libatk-bridge2.0-0 \
 
28
  libxss1 \
29
  libgconf-2-4 \
30
  --no-install-recommends \
31
+ && rm -rf /var/lib/apt/lists/* \
32
+ && fc-cache -fv
33
 
34
  # 创建非 root 用户
35
  RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
server.js CHANGED
@@ -3,142 +3,39 @@ const puppeteer = require('puppeteer');
3
  const cors = require('cors');
4
  const helmet = require('helmet');
5
  const rateLimit = require('express-rate-limit');
6
- const os = require('os');
7
 
8
  const app = express();
9
  const PORT = process.env.PORT || 7860;
10
 
11
- // 请求队列管理
12
- const requestQueue = [];
13
- let activeRequests = 0;
14
- const MAX_CONCURRENT_REQUESTS = 3;
15
-
16
- // CPU 监控
17
- let lastCpuUsage = 0;
18
- let cpuMonitorInterval;
19
-
20
- // 中间件配置
21
- app.use(helmet({
22
- contentSecurityPolicy: false
23
- }));
24
  app.use(cors());
25
- app.use(express.json({ limit: '10mb' }));
26
 
27
- // 简化的速率限制 - 所有用户统一限制
28
  const limiter = rateLimit({
29
- windowMs: 15 * 60 * 1000,
30
- max: 100, // 统一的较高限制
31
  message: {
32
- error: 'Too many requests, please try again later.'
33
- },
34
- keyGenerator: (req) => {
35
- return req.ip;
36
  }
37
  });
38
- app.use('/screenshot', limiter);
39
 
40
- // CPU 监控功能
41
- function startCpuMonitoring() {
42
- const cpus = os.cpus();
43
-
44
- cpuMonitorInterval = setInterval(() => {
45
- const cpus = os.cpus();
46
- let totalIdle = 0;
47
- let totalTick = 0;
48
-
49
- cpus.forEach(cpu => {
50
- for (const type in cpu.times) {
51
- totalTick += cpu.times[type];
52
- }
53
- totalIdle += cpu.times.idle;
54
- });
55
-
56
- const idle = totalIdle / cpus.length;
57
- const total = totalTick / cpus.length;
58
- const usage = 100 - ~~(100 * idle / total);
59
-
60
- lastCpuUsage = usage;
61
- }, 2000);
62
- }
63
-
64
- // 请求队列处理
65
- function processQueue() {
66
- if (requestQueue.length === 0 || activeRequests >= MAX_CONCURRENT_REQUESTS) {
67
- return;
68
- }
69
-
70
- if (lastCpuUsage >= 95) {
71
- return;
72
- }
73
-
74
- const { req, res, next } = requestQueue.shift();
75
- activeRequests++;
76
-
77
- const originalSend = res.send;
78
- res.send = function(...args) {
79
- activeRequests--;
80
- setImmediate(processQueue);
81
- return originalSend.apply(this, args);
82
- };
83
 
84
- next();
 
 
 
 
85
  }
86
 
87
- // 队列中间件
88
- const queueMiddleware = (req, res, next) => {
89
- if (lastCpuUsage >= 95) {
90
- return res.status(503).json({
91
- status: 'busy',
92
- error: 'Server is currently overloaded',
93
- message: 'CPU usage is too high. Please try again later.',
94
- cpuUsage: `${lastCpuUsage}%`,
95
- queueLength: requestQueue.length
96
- });
97
- }
98
-
99
- if (activeRequests >= MAX_CONCURRENT_REQUESTS) {
100
- requestQueue.push({ req, res, next });
101
-
102
- const timeout = setTimeout(() => {
103
- const index = requestQueue.findIndex(item => item.res === res);
104
- if (index !== -1) {
105
- requestQueue.splice(index, 1);
106
- res.status(503).json({
107
- status: 'busy',
108
- error: 'Request queue timeout',
109
- message: 'Request was queued too long and timed out.',
110
- queueLength: requestQueue.length
111
- });
112
- }
113
- }, 30000);
114
-
115
- req.on('close', () => {
116
- clearTimeout(timeout);
117
- const index = requestQueue.findIndex(item => item.res === res);
118
- if (index !== -1) {
119
- requestQueue.splice(index, 1);
120
- }
121
- });
122
-
123
- return;
124
- }
125
-
126
- activeRequests++;
127
-
128
- const originalSend = res.send;
129
- res.send = function(...args) {
130
- activeRequests--;
131
- setImmediate(processQueue);
132
- return originalSend.apply(this, args);
133
- };
134
-
135
- next();
136
- };
137
-
138
- // 启动 CPU 监控
139
- startCpuMonitoring();
140
-
141
- // 健康检查端点
142
  app.get('/', (req, res) => {
143
  res.json({
144
  message: 'Page Screenshot API - Hugging Face Spaces',
@@ -159,26 +56,22 @@ app.get('/', (req, res) => {
159
  });
160
  });
161
 
162
- // 服务器状态端点
163
  app.get('/status', (req, res) => {
164
- const memUsage = process.memoryUsage();
165
 
166
  res.json({
167
  status: 'running',
168
  timestamp: new Date().toISOString(),
169
  system: {
170
- cpuUsage: `${lastCpuUsage}%`,
171
- memoryUsage: {
172
- rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`,
173
- heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`,
174
- heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`
175
- },
176
  uptime: `${Math.floor(process.uptime())}s`
177
  },
178
  queue: {
179
  activeRequests,
180
  queuedRequests: requestQueue.length,
181
- maxConcurrent: MAX_CONCURRENT_REQUESTS
182
  },
183
  authentication: {
184
  type: 'HuggingFace System Level',
@@ -187,40 +80,146 @@ app.get('/status', (req, res) => {
187
  });
188
  });
189
 
190
- // 截图API端点 - 移除所有认证检查
191
- app.post('/screenshot', queueMiddleware, async (req, res) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  const { url, width = 1280, height = 720, quality = 75 } = req.body;
193
 
 
194
  if (!url) {
195
- return res.status(400).json({
196
- error: 'URL is required',
197
- example: { url: 'https://example.com', width: 1280, height: 720 }
198
- });
199
  }
200
 
201
- try {
202
- const urlObj = new URL(url);
203
- if (!['http:', 'https:'].includes(urlObj.protocol)) {
204
- return res.status(400).json({
205
- error: 'Only HTTP and HTTPS URLs are supported'
206
- });
207
- }
208
- } catch (error) {
209
- return res.status(400).json({
210
- error: 'Invalid URL format'
 
211
  });
212
  }
213
 
214
- if (width < 100 || width > 1600 || height < 100 || height > 1200) {
215
- return res.status(400).json({
216
- error: 'Width must be 100-1600px, height must be 100-1200px for HF Spaces'
 
 
 
217
  });
218
  }
219
 
 
220
  let browser;
 
221
  try {
222
- console.log(`Taking screenshot of: ${url}`);
223
-
224
  const browserOptions = {
225
  headless: 'new',
226
  executablePath: '/usr/bin/google-chrome-stable',
@@ -235,304 +234,68 @@ app.post('/screenshot', queueMiddleware, async (req, res) => {
235
  '--disable-extensions',
236
  '--disable-background-timer-throttling',
237
  '--disable-backgrounding-occluded-windows',
238
- '--disable-renderer-backgrounding'
 
 
 
 
 
239
  ]
240
  };
241
 
242
  browser = await puppeteer.launch(browserOptions);
243
  const page = await browser.newPage();
244
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  await page.setViewport({
246
  width: parseInt(width),
247
  height: parseInt(height),
248
  deviceScaleFactor: 1
249
  });
250
 
251
- await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
252
-
253
- // 拦截资源以提高性能
254
- await page.setRequestInterception(true);
255
- page.on('request', (req) => {
256
- const resourceType = req.resourceType();
257
- if (['font', 'media'].includes(resourceType)) {
258
- req.abort();
259
- } else {
260
- req.continue();
261
- }
262
- });
263
-
264
- await page.goto(url, {
265
- waitUntil: 'domcontentloaded',
266
- timeout: 15000
267
  });
268
 
269
- await page.waitForTimeout(1000);
270
-
271
  const screenshot = await page.screenshot({
272
  type: 'jpeg',
273
- quality: Math.max(10, Math.min(100, parseInt(quality))),
274
  fullPage: false
275
  });
276
 
277
- console.log(`Screenshot completed: ${screenshot.length} bytes`);
278
-
279
- res.set({
280
- 'Content-Type': 'image/jpeg',
281
- 'Content-Length': screenshot.length,
282
- 'Cache-Control': 'no-cache',
283
- 'Content-Disposition': `inline; filename="screenshot-${Date.now()}.jpg"`
284
- });
285
-
286
  res.send(screenshot);
287
 
288
  } catch (error) {
289
- console.error('Screenshot error:', error.message);
290
- const errorResponse = {
291
- error: 'Failed to capture screenshot',
292
- message: error.message
293
- };
294
-
295
- if (error.message.includes('timeout')) {
296
- errorResponse.suggestion = 'Try a simpler webpage or reduce timeout';
297
- } else if (error.message.includes('net::')) {
298
- errorResponse.suggestion = 'Check if the URL is accessible';
299
- } else if (error.message.includes('Chrome')) {
300
- errorResponse.suggestion = 'Chrome installation issue, contact admin';
301
- }
302
-
303
- res.status(500).json(errorResponse);
304
  } finally {
305
  if (browser) {
306
- try {
307
- await browser.close();
308
- } catch (closeError) {
309
- console.error('Error closing browser:', closeError.message);
310
- }
311
  }
 
312
  }
313
  });
314
 
315
- // 简化的演示界面
316
- app.get('/demo', (req, res) => {
317
- const demoHtml = `
318
- <!DOCTYPE html>
319
- <html>
320
- <head>
321
- <title>📸 Page Screenshot API Demo</title>
322
- <meta charset="utf-8">
323
- <meta name="viewport" content="width=device-width, initial-scale=1">
324
- <style>
325
- body {
326
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
327
- max-width: 800px; margin: 0 auto; padding: 20px;
328
- background: #f8f9fa;
329
- }
330
- .container { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
331
- .form-group { margin: 20px 0; }
332
- label { display: block; margin-bottom: 8px; font-weight: 600; color: #333; }
333
- input[type="text"], input[type="number"] {
334
- width: 100%; padding: 12px; border: 2px solid #e1e5e9;
335
- border-radius: 6px; font-size: 16px; box-sizing: border-box;
336
- }
337
- input:focus { border-color: #007bff; outline: none; }
338
- .input-row { display: flex; gap: 15px; }
339
- .input-row > div { flex: 1; }
340
- button {
341
- background: linear-gradient(135deg, #007bff, #0056b3);
342
- color: white; border: none; padding: 14px 28px;
343
- border-radius: 6px; cursor: pointer; font-size: 16px; font-weight: 600;
344
- transition: transform 0.2s; margin: 5px;
345
- }
346
- button:hover { transform: translateY(-1px); }
347
- button:disabled { background: #6c757d; cursor: not-allowed; transform: none; }
348
- #result { margin-top: 30px; }
349
- .loading { color: #007bff; font-weight: 500; }
350
- .error { color: #dc3545; background: #f8d7da; padding: 15px; border-radius: 6px; }
351
- .success img { max-width: 100%; border-radius: 6px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
352
- .examples { margin: 20px 0; }
353
- .example-btn {
354
- background: #e9ecef; color: #495057; border: none;
355
- padding: 8px 12px; margin: 5px; border-radius: 4px; cursor: pointer; font-size: 14px;
356
- }
357
- .example-btn:hover { background: #dee2e6; }
358
- .status-info { background: #d1ecf1; padding: 15px; border-radius: 6px; margin-bottom: 20px; border-left: 4px solid #17a2b8; }
359
- .hf-info { background: #d4edda; padding: 15px; border-radius: 6px; margin-bottom: 20px; border-left: 4px solid #28a745; }
360
- </style>
361
- </head>
362
- <body>
363
- <div class="container">
364
- <h1>📸 Page Screenshot API Demo</h1>
365
- <p>Professional screenshot service powered by Hugging Face Spaces.</p>
366
-
367
- <div class="hf-info">
368
- <strong>🔐 Access Control</strong><br>
369
- This service uses Hugging Face system-level authentication. If this Space is private, access is automatically controlled by HF platform.
370
- </div>
371
-
372
- <div class="status-info">
373
- <strong>📊 Server Status:</strong> <span id="serverStatus">Loading...</span>
374
- </div>
375
-
376
- <div class="examples">
377
- <strong>Try these examples:</strong><br>
378
- <button class="example-btn" onclick="setExample('https://www.google.com')">Google</button>
379
- <button class="example-btn" onclick="setExample('https://www.github.com')">GitHub</button>
380
- <button class="example-btn" onclick="setExample('https://www.wikipedia.org')">Wikipedia</button>
381
- <button class="example-btn" onclick="setExample('https://news.ycombinator.com')">Hacker News</button>
382
- </div>
383
-
384
- <div class="form-group">
385
- <label for="url">URL:</label>
386
- <input type="text" id="url" placeholder="https://example.com" value="https://www.google.com">
387
- </div>
388
-
389
- <div class="input-row">
390
- <div>
391
- <label for="width">Width (px):</label>
392
- <input type="number" id="width" value="1280" min="100" max="1600">
393
- </div>
394
- <div>
395
- <label for="height">Height (px):</label>
396
- <input type="number" id="height" value="720" min="100" max="1200">
397
- </div>
398
- <div>
399
- <label for="quality">Quality:</label>
400
- <input type="number" id="quality" value="75" min="10" max="100">
401
- </div>
402
- </div>
403
-
404
- <button onclick="takeScreenshot()" id="captureBtn">Take Screenshot</button>
405
- <button onclick="checkStatus()" id="statusBtn">Check Status</button>
406
-
407
- <div id="result"></div>
408
- </div>
409
-
410
- <script>
411
- async function checkStatus() {
412
- try {
413
- const response = await fetch('/status');
414
- const data = await response.json();
415
- document.getElementById('serverStatus').innerHTML =
416
- \`CPU: \${data.system.cpuUsage} | Queue: \${data.queue.queuedRequests} | Active: \${data.queue.activeRequests}\`;
417
-
418
- document.getElementById('result').innerHTML =
419
- '<h3>Server Status:</h3><pre>' + JSON.stringify(data, null, 2) + '</pre>';
420
- } catch (error) {
421
- document.getElementById('serverStatus').innerHTML = 'Error loading status';
422
- document.getElementById('result').innerHTML = '<p style="color: red;">Error: ' + error.message + '</p>';
423
- }
424
- }
425
-
426
- function setExample(url) {
427
- document.getElementById('url').value = url;
428
- }
429
-
430
- async function takeScreenshot() {
431
- const url = document.getElementById('url').value;
432
- const width = parseInt(document.getElementById('width').value);
433
- const height = parseInt(document.getElementById('height').value);
434
- const quality = parseInt(document.getElementById('quality').value);
435
- const btn = document.getElementById('captureBtn');
436
-
437
- if (!url) {
438
- document.getElementById('result').innerHTML = '<div class="error">Please enter a URL</div>';
439
- return;
440
- }
441
-
442
- btn.disabled = true;
443
- btn.textContent = 'Taking Screenshot...';
444
- document.getElementById('result').innerHTML = '<div class="loading">📸 Capturing screenshot, please wait...</div>';
445
-
446
- try {
447
- const response = await fetch('/screenshot', {
448
- method: 'POST',
449
- headers: {
450
- 'Content-Type': 'application/json'
451
- },
452
- body: JSON.stringify({ url, width, height, quality })
453
- });
454
-
455
- if (response.ok) {
456
- const blob = await response.blob();
457
- const imageUrl = URL.createObjectURL(blob);
458
- const size = (blob.size / 1024).toFixed(1);
459
-
460
- document.getElementById('result').innerHTML =
461
- '<div class="success"><h3>Screenshot Result:</h3>' +
462
- '<p>Size: ' + size + ' KB | Dimensions: ' + width + 'x' + height + '</p>' +
463
- '<img src="' + imageUrl + '" alt="Screenshot"><br><br>' +
464
- '<a href="' + imageUrl + '" download="screenshot.jpg" style="background: #28a745; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Download Image</a></div>';
465
- } else {
466
- const error = await response.json();
467
- let errorMsg = '<div class="error"><strong>Error:</strong> ' + error.error;
468
-
469
- if (error.status === 'busy') {
470
- errorMsg += '<br><strong>Status:</strong> Server is currently busy (CPU: ' + (error.cpuUsage || 'N/A') + ')';
471
- errorMsg += '<br><strong>Queue Length:</strong> ' + (error.queueLength || 0);
472
- }
473
-
474
- if (error.suggestion) {
475
- errorMsg += '<br><strong>Suggestion:</strong> ' + error.suggestion;
476
- }
477
-
478
- errorMsg += '</div>';
479
- document.getElementById('result').innerHTML = errorMsg;
480
- }
481
- } catch (error) {
482
- document.getElementById('result').innerHTML =
483
- '<div class="error"><strong>Network Error:</strong> ' + error.message + '</div>';
484
- } finally {
485
- btn.disabled = false;
486
- btn.textContent = 'Take Screenshot';
487
- }
488
- }
489
-
490
- // 页面加载时检查状态
491
- checkStatus();
492
-
493
- // 每30秒更新一次状态
494
- setInterval(checkStatus, 30000);
495
-
496
- // Enter key support
497
- document.getElementById('url').addEventListener('keypress', function(e) {
498
- if (e.key === 'Enter') {
499
- takeScreenshot();
500
- }
501
- });
502
- </script>
503
- </body>
504
- </html>
505
- `;
506
-
507
- res.send(demoHtml);
508
- });
509
-
510
- // 错误处理中间件
511
- app.use((error, req, res, next) => {
512
- console.error('Unhandled error:', error);
513
- res.status(500).json({
514
- error: 'Internal server error'
515
- });
516
- });
517
-
518
- // 404处理
519
- app.use((req, res) => {
520
- res.status(404).json({
521
- error: 'Endpoint not found'
522
- });
523
- });
524
-
525
- // 优雅关闭
526
- process.on('SIGTERM', () => {
527
- console.log('SIGTERM received, shutting down gracefully...');
528
- if (cpuMonitorInterval) {
529
- clearInterval(cpuMonitorInterval);
530
- }
531
- process.exit(0);
532
  });
533
 
534
- app.listen(PORT, '0.0.0.0', () => {
535
- console.log(`Screenshot API server running on port ${PORT} for Hugging Face Spaces`);
536
- console.log('Authentication: Handled by HuggingFace platform (system-level)');
537
- console.log('No application-level authentication required');
538
- });
 
3
  const cors = require('cors');
4
  const helmet = require('helmet');
5
  const rateLimit = require('express-rate-limit');
 
6
 
7
  const app = express();
8
  const PORT = process.env.PORT || 7860;
9
 
10
+ // 安全中间件
11
+ app.use(helmet());
 
 
 
 
 
 
 
 
 
 
 
12
  app.use(cors());
13
+ app.use(express.json());
14
 
15
+ // 速率限制
16
  const limiter = rateLimit({
17
+ windowMs: 15 * 60 * 1000, // 15分钟
18
+ max: 100, // 限制每个IP 100次请求
19
  message: {
20
+ error: 'Too many requests',
21
+ message: 'Rate limit exceeded. Please try again later.'
 
 
22
  }
23
  });
24
+ app.use(limiter);
25
 
26
+ // 队列管理
27
+ let activeRequests = 0;
28
+ const MAX_CONCURRENT = 3;
29
+ const requestQueue = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
+ // CPU 监控
32
+ function getCpuUsage() {
33
+ const usage = process.cpuUsage();
34
+ const total = usage.user + usage.system;
35
+ return Math.round((total / 1000000) % 100); // 简化的CPU使用率
36
  }
37
 
38
+ // 健康检查
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  app.get('/', (req, res) => {
40
  res.json({
41
  message: 'Page Screenshot API - Hugging Face Spaces',
 
56
  });
57
  });
58
 
59
+ // 状态检查
60
  app.get('/status', (req, res) => {
61
+ const cpuUsage = getCpuUsage();
62
 
63
  res.json({
64
  status: 'running',
65
  timestamp: new Date().toISOString(),
66
  system: {
67
+ cpuUsage: `${cpuUsage}%`,
68
+ memoryUsage: '', // HF Spaces 不提供详细内存信息
 
 
 
 
69
  uptime: `${Math.floor(process.uptime())}s`
70
  },
71
  queue: {
72
  activeRequests,
73
  queuedRequests: requestQueue.length,
74
+ maxConcurrent: MAX_CONCURRENT
75
  },
76
  authentication: {
77
  type: 'HuggingFace System Level',
 
80
  });
81
  });
82
 
83
+ // 演示页面
84
+ app.get('/demo', (req, res) => {
85
+ res.send(`
86
+ <!DOCTYPE html>
87
+ <html>
88
+ <head>
89
+ <title>Page Screenshot API Demo</title>
90
+ <style>
91
+ body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
92
+ .form-group { margin: 15px 0; }
93
+ input, button { padding: 12px; margin: 5px; border: 1px solid #ddd; border-radius: 6px; }
94
+ input[type="text"] { width: 400px; }
95
+ button { background: #007bff; color: white; border: none; cursor: pointer; }
96
+ button:hover { background: #0056b3; }
97
+ #result { margin-top: 20px; }
98
+ img { max-width: 100%; border: 1px solid #ccc; border-radius: 6px; }
99
+ .loading { color: #666; font-style: italic; }
100
+ .error { color: #dc3545; }
101
+ .success { color: #28a745; }
102
+ </style>
103
+ </head>
104
+ <body>
105
+ <h1>📸 Page Screenshot API Demo</h1>
106
+ <p>Test the screenshot API with global font support</p>
107
+
108
+ <div class="form-group">
109
+ <label>URL to screenshot:</label><br>
110
+ <input type="text" id="url" value="https://www.baidu.com" placeholder="Enter website URL">
111
+ </div>
112
+
113
+ <div class="form-group">
114
+ <label>Width:</label>
115
+ <input type="number" id="width" value="1280" min="100" max="1600">
116
+ <label>Height:</label>
117
+ <input type="number" id="height" value="720" min="100" max="1200">
118
+ <label>Quality:</label>
119
+ <input type="number" id="quality" value="75" min="10" max="100">
120
+ </div>
121
+
122
+ <button onclick="takeScreenshot()">📸 Take Screenshot</button>
123
+ <button onclick="testHealth()">🔍 Health Check</button>
124
+
125
+ <div id="result"></div>
126
+
127
+ <script>
128
+ async function testHealth() {
129
+ try {
130
+ const response = await fetch('/');
131
+ const data = await response.json();
132
+ document.getElementById('result').innerHTML =
133
+ '<h3>✅ Health Check Result:</h3><pre>' + JSON.stringify(data, null, 2) + '</pre>';
134
+ } catch (error) {
135
+ document.getElementById('result').innerHTML = '<p class="error">❌ Error: ' + error.message + '</p>';
136
+ }
137
+ }
138
+
139
+ async function takeScreenshot() {
140
+ const url = document.getElementById('url').value;
141
+ const width = parseInt(document.getElementById('width').value);
142
+ const height = parseInt(document.getElementById('height').value);
143
+ const quality = parseInt(document.getElementById('quality').value);
144
+
145
+ if (!url) {
146
+ alert('Please enter a URL');
147
+ return;
148
+ }
149
+
150
+ document.getElementById('result').innerHTML = '<p class="loading">📸 Taking screenshot...</p>';
151
+
152
+ try {
153
+ const response = await fetch('/screenshot', {
154
+ method: 'POST',
155
+ headers: {
156
+ 'Content-Type': 'application/json',
157
+ },
158
+ body: JSON.stringify({ url, width, height, quality })
159
+ });
160
+
161
+ if (response.ok) {
162
+ const blob = await response.blob();
163
+ const imageUrl = URL.createObjectURL(blob);
164
+ const size = (blob.size / 1024).toFixed(1);
165
+
166
+ document.getElementById('result').innerHTML =
167
+ '<h3 class="success">✅ Screenshot Success!</h3>' +
168
+ '<p>Size: ' + size + ' KB | Dimensions: ' + width + 'x' + height + '</p>' +
169
+ '<img src="' + imageUrl + '" alt="Screenshot"><br><br>' +
170
+ '<a href="' + imageUrl + '" download="screenshot.jpg">💾 Download Image</a>';
171
+ } else {
172
+ const error = await response.json();
173
+ document.getElementById('result').innerHTML =
174
+ '<p class="error">❌ Error: ' + (error.error || error.message) + '</p>';
175
+ }
176
+ } catch (error) {
177
+ document.getElementById('result').innerHTML =
178
+ '<p class="error">❌ Network Error: ' + error.message + '</p>';
179
+ }
180
+ }
181
+ </script>
182
+ </body>
183
+ </html>
184
+ `);
185
+ });
186
+
187
+ // 截图端点
188
+ app.post('/screenshot', async (req, res) => {
189
  const { url, width = 1280, height = 720, quality = 75 } = req.body;
190
 
191
+ // 验证参数
192
  if (!url) {
193
+ return res.status(400).json({ error: 'URL is required' });
 
 
 
194
  }
195
 
196
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
197
+ return res.status(400).json({ error: 'URL must start with http:// or https://' });
198
+ }
199
+
200
+ const cpuUsage = getCpuUsage();
201
+ if (cpuUsage > 95) {
202
+ return res.status(503).json({
203
+ status: 'busy',
204
+ error: 'Server is currently overloaded',
205
+ cpuUsage: `${cpuUsage}%`,
206
+ queueLength: requestQueue.length
207
  });
208
  }
209
 
210
+ if (activeRequests >= MAX_CONCURRENT) {
211
+ return res.status(503).json({
212
+ status: 'busy',
213
+ error: 'Too many concurrent requests',
214
+ activeRequests,
215
+ maxConcurrent: MAX_CONCURRENT
216
  });
217
  }
218
 
219
+ activeRequests++;
220
  let browser;
221
+
222
  try {
 
 
223
  const browserOptions = {
224
  headless: 'new',
225
  executablePath: '/usr/bin/google-chrome-stable',
 
234
  '--disable-extensions',
235
  '--disable-background-timer-throttling',
236
  '--disable-backgrounding-occluded-windows',
237
+ '--disable-renderer-backgrounding',
238
+ // 字体优化设置
239
+ '--font-render-hinting=none',
240
+ '--disable-font-subpixel-positioning',
241
+ '--force-color-profile=srgb',
242
+ '--disable-features=VizDisplayCompositor'
243
  ]
244
  };
245
 
246
  browser = await puppeteer.launch(browserOptions);
247
  const page = await browser.newPage();
248
 
249
+ // 设置字体回退CSS - 支持全球字体
250
+ await page.addStyleTag({
251
+ content: `
252
+ * {
253
+ font-family: "Noto Sans", "Noto Sans CJK SC", "Noto Sans CJK TC",
254
+ "Noto Sans CJK JP", "Noto Sans CJK KR",
255
+ "Noto Color Emoji", system-ui, -apple-system,
256
+ BlinkMacSystemFont, sans-serif !important;
257
+ }
258
+ `
259
+ });
260
+
261
  await page.setViewport({
262
  width: parseInt(width),
263
  height: parseInt(height),
264
  deviceScaleFactor: 1
265
  });
266
 
267
+ await page.goto(url, {
268
+ waitUntil: 'domcontentloaded',
269
+ timeout: 15000
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  });
271
 
 
 
272
  const screenshot = await page.screenshot({
273
  type: 'jpeg',
274
+ quality: parseInt(quality),
275
  fullPage: false
276
  });
277
 
278
+ res.set('Content-Type', 'image/jpeg');
 
 
 
 
 
 
 
 
279
  res.send(screenshot);
280
 
281
  } catch (error) {
282
+ console.error('Screenshot error:', error);
283
+ res.status(500).json({
284
+ error: 'Failed to capture screenshot',
285
+ message: error.message
286
+ });
 
 
 
 
 
 
 
 
 
 
287
  } finally {
288
  if (browser) {
289
+ await browser.close();
 
 
 
 
290
  }
291
+ activeRequests--;
292
  }
293
  });
294
 
295
+ app.listen(PORT, () => {
296
+ console.log(`🚀 Page Screenshot API running on port ${PORT}`);
297
+ console.log(`📸 Demo: http://localhost:${PORT}/demo`);
298
+ console.log(`🔍 Health: http://localhost:${PORT}/`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  });
300
 
301
+ module.exports = app;