- 新增海报生成接口,支持从网页URL或HTML内容生成海报图像 - 新增PDF生成接口,支持从网页URL或HTML内容生成PDF文档 - 添加Swagger API文档注释,完善接口描述和参数说明 - 实现HTML内容参数支持,允许直接传入HTML结构生成海报/PDF - 添加输入验证和标准化响应格式 - 引入DOMPurify库对HTML内容进行安全过滤 - 更新环境变量配置,支持API密钥认证和CORS设置 - 优化上传逻辑,统一返回标准响应结构 - 添加构建脚本支持Docker镜像打包和推送
385 lines
12 KiB
JavaScript
385 lines
12 KiB
JavaScript
import {fileURLToPath} from 'url';
|
||
import {dirname} from 'path';
|
||
import {getParam, upload} from './params.js';
|
||
import {randomString, jsonToHtml, sanitizeHtml} from './utils.js';
|
||
import {FONT_STYLE} from './constants.js';
|
||
import {successResponse, errorResponse, validationErrorResponse, serverErrorResponse} from './response.js';
|
||
|
||
// 获取当前模块的目录名
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const basePath = dirname(dirname(__filename)); // 项目根目录
|
||
|
||
// 延迟获取 upload_path,确保环境变量已被加载
|
||
let upload_path;
|
||
|
||
/**
|
||
* 健康检查接口
|
||
* @param {*} req
|
||
* @param {*} res
|
||
*/
|
||
function statusHandler(req, res) {
|
||
console.log("健康检查", new Date().getMilliseconds());
|
||
return res.status(200).json(successResponse({}, "Service is running", 200));
|
||
}
|
||
|
||
/**
|
||
* @swagger
|
||
* /api/v1/poster:
|
||
* post:
|
||
* summary: 生成海报
|
||
* description: 从网页URL或HTML内容生成海报图像
|
||
* tags: [Poster]
|
||
* security:
|
||
* - ApiKeyAuth: []
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* webpage:
|
||
* type: string
|
||
* description: 要生成海报的网页URL
|
||
* example: "https://example.com"
|
||
* html:
|
||
* type: string
|
||
* description: 要生成海报的HTML内容(可选,优先级高于webpage)
|
||
* example: "<html><body><h1>Hello World</h1></body></html>"
|
||
* device:
|
||
* type: number
|
||
* description: 设备缩放因子
|
||
* default: 1
|
||
* example: 1
|
||
* width:
|
||
* type: number
|
||
* description: 海报宽度
|
||
* default: 1920
|
||
* example: 1920
|
||
* height:
|
||
* type: number
|
||
* description: 海报高度
|
||
* default: 1080
|
||
* example: 1080
|
||
* type:
|
||
* type: string
|
||
* description: 输出图像类型
|
||
* default: "png"
|
||
* example: "png"
|
||
* encoding:
|
||
* type: string
|
||
* description: 编码类型
|
||
* default: "binary"
|
||
* example: "binary"
|
||
* responses:
|
||
* 200:
|
||
* description: 成功生成海报
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* success:
|
||
* type: boolean
|
||
* example: true
|
||
* description: 请求是否成功
|
||
* data:
|
||
* type: object
|
||
* properties:
|
||
* name:
|
||
* type: string
|
||
* example: "poster_abc123def456.png"
|
||
* description: 生成的海报文件名
|
||
* path:
|
||
* type: string
|
||
* example: "http://example.com/uploads/posters/2024/11/14/poster_abc123def456.png"
|
||
* description: 生成的海报文件访问路径
|
||
* message:
|
||
* type: string
|
||
* example: "Poster generated successfully"
|
||
* description: 响应消息
|
||
* code:
|
||
* type: number
|
||
* example: 200
|
||
* description: 响应代码
|
||
* 400:
|
||
* description: 请求参数错误
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* success:
|
||
* type: boolean
|
||
* example: false
|
||
* description: 请求是否成功
|
||
* data:
|
||
* type: object
|
||
* example: null
|
||
* description: 响应数据
|
||
* message:
|
||
* type: string
|
||
* example: "Missing required parameter: webpage or html"
|
||
* description: 错误消息
|
||
* code:
|
||
* type: number
|
||
* example: 3001
|
||
* description: 错误代码
|
||
* 401:
|
||
* description: 未授权访问
|
||
* 500:
|
||
* description: 服务器内部错误
|
||
*/
|
||
|
||
/**
|
||
* 海报生成接口
|
||
* @param {*} req
|
||
* @param {*} res
|
||
* @param {*} browserManager
|
||
* @param {*} storageManager
|
||
*/
|
||
async function posterHandler(
|
||
req,
|
||
res,
|
||
browserManager,
|
||
storageManager
|
||
) {
|
||
// 确保在使用时获取最新的环境变量值
|
||
upload_path = process.env.UPLOAD_PATH || 'uploads';
|
||
upload_path = upload_path + '/posters'
|
||
|
||
// 参数校验 - 现在支持webpage或html
|
||
if (!req.body || (!req.body.webpage && !req.body.html)) {
|
||
return res.status(400).json(validationErrorResponse('Missing required parameter: webpage or html', 3001));
|
||
}
|
||
|
||
const webpage = req.body.webpage;
|
||
let html = req.body.html;
|
||
const device = req.body.device || 1;
|
||
const width = req.body.width || 1920;
|
||
const height = req.body.height || 1080;
|
||
const type = req.body.type || 'png';
|
||
const encoding = req.body.encoding || 'binary';
|
||
const filename = 'poster_' + randomString(20) + '.' + type;
|
||
let param = getParam(req, filename, upload_path);
|
||
let base64;
|
||
|
||
const page = browserManager.getTargetPage();
|
||
|
||
try {
|
||
await page.setViewport({width: width, height: height, deviceScaleFactor: device, isMobile: true});
|
||
|
||
// 根据参数选择加载方式
|
||
if (html) {
|
||
// 检查html是否为JSON格式,如果是则转换为HTML字符串
|
||
if (typeof html === 'object' && html !== null) {
|
||
html = jsonToHtml(html);
|
||
}
|
||
|
||
// 直接使用HTML内容
|
||
await page.setContent(html, {
|
||
timeout: 30000,
|
||
waitUntil: 'networkidle0'
|
||
});
|
||
} else {
|
||
// 导航到网页
|
||
await page.goto(webpage, {
|
||
timeout: 30000,
|
||
waitUntil: 'networkidle0'
|
||
});
|
||
}
|
||
|
||
await page.addStyleTag({content: FONT_STYLE});
|
||
await page.screenshot(param).then((data) => {
|
||
base64 = data;
|
||
});
|
||
|
||
// Update the upload function to return standardized response
|
||
const result = await upload(res, encoding, base64, type, filename, storageManager, basePath, upload_path);
|
||
return res.status(200).json(successResponse(result, "Poster generated successfully", 200));
|
||
} catch (err) {
|
||
console.error('Poster generation error:', err);
|
||
return res.status(500).json(serverErrorResponse('Failed to generate poster', 4001));
|
||
} finally {
|
||
// 确保只归还有效的页面
|
||
if (page && !page.isClosed()) {
|
||
browserManager.returnTargetPage(page);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @swagger
|
||
* /api/v1/pdf:
|
||
* post:
|
||
* summary: 生成PDF
|
||
* description: 从网页URL或HTML内容生成PDF文档
|
||
* tags: [PDF]
|
||
* security:
|
||
* - ApiKeyAuth: []
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* webpage:
|
||
* type: string
|
||
* description: 要生成PDF的网页URL
|
||
* example: "https://example.com"
|
||
* html:
|
||
* type: string
|
||
* description: 要生成PDF的HTML内容(可选,优先级高于webpage)
|
||
* example: "<html><body><h1>Hello World</h1></body></html>"
|
||
* device:
|
||
* type: number
|
||
* description: 设备缩放因子
|
||
* default: 1
|
||
* example: 1
|
||
* width:
|
||
* type: number
|
||
* description: PDF宽度
|
||
* default: 1920
|
||
* example: 1920
|
||
* height:
|
||
* type: number
|
||
* description: PDF高度
|
||
* default: 1080
|
||
* example: 1080
|
||
* responses:
|
||
* 200:
|
||
* description: 成功生成PDF
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* success:
|
||
* type: boolean
|
||
* example: true
|
||
* description: 请求是否成功
|
||
* data:
|
||
* type: object
|
||
* properties:
|
||
* name:
|
||
* type: string
|
||
* example: "pdf_abc123def456.pdf"
|
||
* description: 生成的PDF文件名
|
||
* path:
|
||
* type: string
|
||
* example: "http://example.com/uploads/pdfs/2024/11/14/pdf_abc123def456.pdf"
|
||
* description: 生成的PDF文件访问路径
|
||
* message:
|
||
* type: string
|
||
* example: "PDF generated successfully"
|
||
* description: 响应消息
|
||
* code:
|
||
* type: number
|
||
* example: 200
|
||
* description: 响应代码
|
||
* 400:
|
||
* description: 请求参数错误
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* success:
|
||
* type: boolean
|
||
* example: false
|
||
* description: 请求是否成功
|
||
* data:
|
||
* type: object
|
||
* example: null
|
||
* description: 响应数据
|
||
* message:
|
||
* type: string
|
||
* example: "Missing required parameter: webpage or html"
|
||
* description: 错误消息
|
||
* code:
|
||
* type: number
|
||
* example: 3001
|
||
* description: 错误代码
|
||
* 401:
|
||
* description: 未授权访问
|
||
* 500:
|
||
* description: 服务器内部错误
|
||
*/
|
||
|
||
/**
|
||
* PDF下载接口
|
||
* @param {*} req
|
||
* @param {*} res
|
||
* @param {*} browserManager
|
||
* @param {*} storageManager
|
||
*/
|
||
async function pdfHandler(
|
||
req,
|
||
res,
|
||
browserManager,
|
||
storageManager
|
||
) {
|
||
// 确保在使用时获取最新的环境变量值
|
||
upload_path = process.env.UPLOAD_PATH || 'uploads';
|
||
upload_path = upload_path + '/pdfs'
|
||
|
||
// 参数校验
|
||
if (!req.body || (!req.body.webpage && !req.body.html)) {
|
||
return res.status(400).json(validationErrorResponse('Missing required parameter: webpage or html', 3001));
|
||
}
|
||
|
||
const webpage = req.body.webpage;
|
||
let html = req.body.html;
|
||
const filename = 'pdf_' + randomString(20) + '.pdf';
|
||
let base64;
|
||
|
||
const page = browserManager.getTargetPage();
|
||
|
||
try {
|
||
// 根据参数选择加载方式
|
||
if (html) {
|
||
// 检查html是否为JSON格式,如果是则转换为HTML字符串
|
||
if (typeof html === 'object' && html !== null) {
|
||
html = jsonToHtml(html);
|
||
}
|
||
|
||
// 直接使用HTML内容
|
||
await page.setContent(html, {
|
||
timeout: 30000,
|
||
});
|
||
} else {
|
||
// 导航到网页
|
||
await page.goto(webpage, {
|
||
timeout: 30000,
|
||
waitUntil: 'networkidle0'
|
||
});
|
||
}
|
||
|
||
await page.addStyleTag({content: FONT_STYLE});
|
||
// 生成PDF
|
||
await page.pdf({
|
||
format: 'A4',
|
||
printBackground: true,
|
||
path: `${upload_path}/${filename}`
|
||
});
|
||
// 复用上传函数处理PDF文件
|
||
const result = await upload(res, 'binary', base64, 'pdf', filename, storageManager, basePath, upload_path);
|
||
return res.status(200).json(successResponse(result, "PDF generated successfully", 200));
|
||
} catch (err) {
|
||
console.error('PDF generation error:', err);
|
||
return res.status(500).json(serverErrorResponse('Failed to generate PDF', 4002));
|
||
} finally {
|
||
// 确保只归还有效的页面
|
||
if (page && !page.isClosed()) {
|
||
browserManager.returnTargetPage(page);
|
||
}
|
||
}
|
||
}
|
||
|
||
export {
|
||
statusHandler,
|
||
posterHandler,
|
||
pdfHandler
|
||
};
|