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 ? `

🔐 API Authentication Required


ℹ️ API key is required for this Space. Contact the Space owner for access.
` : `

🔓 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;