const express = require('express');
const puppeteer = require('puppeteer');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const app = express();
const PORT = process.env.PORT || 7860;
const API_KEY = process.env.API_KEY; // 从环境变量获取API密钥
// 安全中间件
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "blob:"],
connectSrc: ["'self'", "https:"] // 添加 https: 允许连接外部网站
}
}
}));
app.use(cors());
app.use(express.json());
// 速率限制
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 限制每个IP 100次请求
message: {
error: 'Too many requests',
message: 'Rate limit exceeded. Please try again later.'
}
});
app.use(limiter);
// 队列管理
let activeRequests = 0;
const MAX_CONCURRENT = 3;
const requestQueue = [];
// CPU 监控
function getCpuUsage() {
const usage = process.cpuUsage();
const total = usage.user + usage.system;
return Math.round((total / 1000000) % 100); // 简化的CPU使用率
}
// API鉴权中间件
function authenticate(req, res, next) {
// 如果没有设置API_KEY环境变量,跳过鉴权(开发模式)
if (!API_KEY) {
return next();
}
const authHeader = req.headers.authorization;
const apiKey = req.headers['x-api-key'] || (authHeader && authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null);
if (!apiKey || apiKey !== API_KEY) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Valid API key required. Please provide API key in Authorization header or x-api-key header.',
hint: 'Use "Authorization: Bearer YOUR_API_KEY" or "x-api-key: YOUR_API_KEY"'
});
}
next();
}
// 健康检查
app.get('/', (req, res) => {
res.json({
message: 'Page Screenshot API - Hugging Face Spaces',
version: '1.4.0',
status: 'running',
platform: 'HuggingFace Spaces',
authentication: {
type: API_KEY ? 'API Key Required' : 'Open Access (No API Key Set)',
required: !!API_KEY,
note: API_KEY ? 'API key required for screenshot endpoint' : 'Set API_KEY environment variable to enable authentication'
},
endpoints: {
screenshot: 'POST /screenshot (requires API key if configured)',
demo: 'GET /demo',
health: 'GET /',
status: 'GET /status'
},
license: 'Proprietary - Commercial use requires license'
});
});
// 状态检查
app.get('/status', (req, res) => {
const cpuUsage = getCpuUsage();
res.json({
status: 'running',
timestamp: new Date().toISOString(),
system: {
cpuUsage: `${cpuUsage}%`,
memoryUsage: '', // HF Spaces 不提供详细内存信息
uptime: `${Math.floor(process.uptime())}s`
},
queue: {
activeRequests,
queuedRequests: requestQueue.length,
maxConcurrent: MAX_CONCURRENT
},
authentication: {
enabled: !!API_KEY,
type: API_KEY ? 'Environment Variable API Key' : 'Disabled',
note: API_KEY ? 'API key authentication is active' : 'Set API_KEY environment variable to enable authentication'
}
});
});
// 演示页面
app.get('/demo', (req, res) => {
res.send(`
Page Screenshot API Demo
📸 Page Screenshot API Demo
Test the screenshot API with global font support
${API_KEY ? `
` : `
🔓 Open Access Mode
ℹ️ No API key required - authentication is disabled for this Space.
`}
`);
});
// 截图端点
app.post('/screenshot', authenticate, async (req, res) => {
const { url, width = 1280, height = 720, quality = 75, fullPage = false, format = 'jpeg' } = req.body;
// 验证参数
if (!url) {
return res.status(400).json({ error: 'URL is required' });
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
return res.status(400).json({ error: 'URL must start with http:// or https://' });
}
// 验证图片格式
if (!['jpeg', 'png'].includes(format.toLowerCase())) {
return res.status(400).json({ error: 'Format must be either "jpeg" or "png"' });
}
const cpuUsage = getCpuUsage();
if (cpuUsage > 95) {
return res.status(503).json({
status: 'busy',
error: 'Server is currently overloaded',
cpuUsage: `${cpuUsage}%`,
queueLength: requestQueue.length
});
}
if (activeRequests >= MAX_CONCURRENT) {
return res.status(503).json({
status: 'busy',
error: 'Too many concurrent requests',
activeRequests,
maxConcurrent: MAX_CONCURRENT
});
}
activeRequests++;
let browser;
try {
const browserOptions = {
headless: 'new',
executablePath: '/usr/bin/google-chrome-stable',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--no-first-run',
'--no-zygote',
'--single-process',
'--disable-extensions',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
// 字体优化设置
'--font-render-hinting=none',
'--disable-font-subpixel-positioning',
'--force-color-profile=srgb',
'--disable-features=VizDisplayCompositor'
]
};
browser = await puppeteer.launch(browserOptions);
const page = await browser.newPage();
// 设置字体回退CSS - 支持全球字体
await page.addStyleTag({
content: `
* {
font-family: "Noto Sans", "Noto Sans CJK SC", "Noto Sans CJK TC",
"Noto Sans CJK JP", "Noto Sans CJK KR",
"Noto Color Emoji", system-ui, -apple-system,
BlinkMacSystemFont, sans-serif !important;
}
`
});
await page.setViewport({
width: parseInt(width),
height: parseInt(height),
deviceScaleFactor: 1
});
await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 15000
});
// 配置截图选项
const screenshotOptions = {
type: format.toLowerCase(),
fullPage: Boolean(fullPage)
};
// 只有 JPEG 格式才支持 quality 参数
if (format.toLowerCase() === 'jpeg') {
screenshotOptions.quality = parseInt(quality);
}
const screenshot = await page.screenshot(screenshotOptions);
// 设置正确的 Content-Type
const mimeType = format.toLowerCase() === 'png' ? 'image/png' : 'image/jpeg';
res.set('Content-Type', mimeType);
res.send(screenshot);
} catch (error) {
console.error('Screenshot error:', error);
res.status(500).json({
error: 'Failed to capture screenshot',
message: error.message
});
} finally {
if (browser) {
await browser.close();
}
activeRequests--;
}
});
app.listen(PORT, () => {
console.log(`🚀 Page Screenshot API running on port ${PORT}`);
console.log(`📸 Demo: http://localhost:${PORT}/demo`);
console.log(`🔍 Health: http://localhost:${PORT}/`);
console.log(`🔐 Authentication: ${API_KEY ? 'ENABLED' : 'DISABLED'}`);
});
module.exports = app;