From dc940d25984623409fc6ee1ede3b723c6e5872f3 Mon Sep 17 00:00:00 2001 From: shenyifei Date: Thu, 20 Nov 2025 17:51:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(api):=20=E6=B7=BB=E5=8A=A0=E6=B5=B7?= =?UTF-8?q?=E6=8A=A5=E5=92=8CPDF=E7=94=9F=E6=88=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增海报生成接口,支持从网页URL或HTML内容生成海报图像 - 新增PDF生成接口,支持从网页URL或HTML内容生成PDF文档 - 添加Swagger API文档注释,完善接口描述和参数说明 - 实现HTML内容参数支持,允许直接传入HTML结构生成海报/PDF - 添加输入验证和标准化响应格式 - 引入DOMPurify库对HTML内容进行安全过滤 - 更新环境变量配置,支持API密钥认证和CORS设置 - 优化上传逻辑,统一返回标准响应结构 - 添加构建脚本支持Docker镜像打包和推送 --- .env.example | 25 +- .gitignore | 4 + AGENTS.md | 18 + CLAUDE.md | 18 + QWEN.md | 18 + README.md | 41 ++ build.sh | 7 + lib/auth.js | 70 +++ lib/params.js | 6 +- lib/response.js | 64 +++ lib/routes.js | 271 +++++++++- lib/storage.js | 6 +- lib/swagger.js | 36 ++ lib/utils.js | 58 ++- openspec/AGENTS.md | 456 ++++++++++++++++ .../proposal.md | 16 + .../specs/server-config/spec.md | 55 ++ .../add-cors-and-api-prefix-config/tasks.md | 43 ++ .../add-swagger-docs/design.md | 41 ++ .../add-swagger-docs/proposal.md | 17 + .../specs/documentation/spec.md | 31 ++ .../add-swagger-docs/tasks.md | 11 + .../design.md | 88 ++++ .../proposal.md | 17 + .../specs/authentication/spec.md | 101 ++++ .../tasks.md | 37 ++ .../design.md | 113 ++++ .../proposal.md | 17 + .../specs/response-format/spec.md | 108 ++++ .../tasks.md | 39 ++ openspec/project.md | 191 +++++++ package.json | 10 +- pnpm-lock.yaml | 492 ++++++++++++++++++ server.mjs | 133 ++++- swagger.json | 1 + tests/test-standardized-response.js | 41 ++ tests/test-swagger.js | 55 ++ tests/test_html_poster.js | 36 ++ 38 files changed, 2744 insertions(+), 47 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 QWEN.md create mode 100644 build.sh create mode 100644 lib/auth.js create mode 100644 lib/response.js create mode 100644 lib/swagger.js create mode 100644 openspec/AGENTS.md create mode 100644 openspec/changes/add-cors-and-api-prefix-config/proposal.md create mode 100644 openspec/changes/add-cors-and-api-prefix-config/specs/server-config/spec.md create mode 100644 openspec/changes/add-cors-and-api-prefix-config/tasks.md create mode 100644 openspec/changes/archive/2025-11-14-add-swagger-docs/add-swagger-docs/design.md create mode 100644 openspec/changes/archive/2025-11-14-add-swagger-docs/add-swagger-docs/proposal.md create mode 100644 openspec/changes/archive/2025-11-14-add-swagger-docs/add-swagger-docs/specs/documentation/spec.md create mode 100644 openspec/changes/archive/2025-11-14-add-swagger-docs/add-swagger-docs/tasks.md create mode 100644 openspec/changes/archive/2025-11-20-add-api-authentication/design.md create mode 100644 openspec/changes/archive/2025-11-20-add-api-authentication/proposal.md create mode 100644 openspec/changes/archive/2025-11-20-add-api-authentication/specs/authentication/spec.md create mode 100644 openspec/changes/archive/2025-11-20-add-api-authentication/tasks.md create mode 100644 openspec/changes/archive/2025-11-20-standardize-api-responses/design.md create mode 100644 openspec/changes/archive/2025-11-20-standardize-api-responses/proposal.md create mode 100644 openspec/changes/archive/2025-11-20-standardize-api-responses/specs/response-format/spec.md create mode 100644 openspec/changes/archive/2025-11-20-standardize-api-responses/tasks.md create mode 100644 openspec/project.md create mode 100644 swagger.json create mode 100644 tests/test-standardized-response.js create mode 100644 tests/test-swagger.js create mode 100644 tests/test_html_poster.js diff --git a/.env.example b/.env.example index 9603779..0357ee7 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ # 存储配置 -STORAGE_TYPE=local # 可选值: cos(腾讯云), oss(阿里云), local(本地存储) +# 可选值: cos(腾讯云), oss(阿里云), local(本地存储) +STORAGE_TYPE=local # COS 配置 (腾讯云对象存储) COS_SECRET_ID= @@ -21,4 +22,24 @@ LOCAL_DOMAIN=http://localhost:3000 UPLOAD_PATH=uploads # 服务端口 -PORT=3000 \ No newline at end of file +PORT=3000 + +# API 认证配置 +# 多个API密钥用逗号分隔,例如: abc123,def456,ghi789 +ALLOWED_API_KEYS= + +# 认证开关 (用于临时禁用认证,默认为 false) +DISABLE_API_AUTH=false + +# CORS 配置 +# 是否启用CORS中间件 (true/false, 默认为 false) +ENABLE_CORS=false + +# 允许的跨域源 (多个源用逗号分隔,例如: http://localhost:3000,https://example.com) +# 如果未设置但启用了CORS,将使用安全的默认配置 +CORS_ORIGINS= + +# API 前缀配置 +# API路径前缀,默认为 /api/v1 +# 可以设置为空字符串来移除前缀,或自定义前缀如 /api/v2 +API_PREFIX=/api/v1 diff --git a/.gitignore b/.gitignore index 41a3aab..65df41a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ node_modules uploads/pdfs/* uploads/posters/* +.spec-workflow +/.bmad-core/ +/.claude/ +/.qwen/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0669699 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,18 @@ + +# OpenSpec Instructions + +These instructions are for AI assistants working in this project. + +Always open `@/openspec/AGENTS.md` when the request: +- Mentions planning or proposals (words like proposal, spec, change, plan) +- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work +- Sounds ambiguous and you need the authoritative spec before coding + +Use `@/openspec/AGENTS.md` to learn: +- How to create and apply change proposals +- Spec format and conventions +- Project structure and guidelines + +Keep this managed block so 'openspec update' can refresh the instructions. + + \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0669699 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,18 @@ + +# OpenSpec Instructions + +These instructions are for AI assistants working in this project. + +Always open `@/openspec/AGENTS.md` when the request: +- Mentions planning or proposals (words like proposal, spec, change, plan) +- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work +- Sounds ambiguous and you need the authoritative spec before coding + +Use `@/openspec/AGENTS.md` to learn: +- How to create and apply change proposals +- Spec format and conventions +- Project structure and guidelines + +Keep this managed block so 'openspec update' can refresh the instructions. + + \ No newline at end of file diff --git a/QWEN.md b/QWEN.md new file mode 100644 index 0000000..0669699 --- /dev/null +++ b/QWEN.md @@ -0,0 +1,18 @@ + +# OpenSpec Instructions + +These instructions are for AI assistants working in this project. + +Always open `@/openspec/AGENTS.md` when the request: +- Mentions planning or proposals (words like proposal, spec, change, plan) +- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work +- Sounds ambiguous and you need the authoritative spec before coding + +Use `@/openspec/AGENTS.md` to learn: +- How to create and apply change proposals +- Spec format and conventions +- Project structure and guidelines + +Keep this managed block so 'openspec update' can refresh the instructions. + + \ No newline at end of file diff --git a/README.md b/README.md index 220285f..4b6c8d0 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,13 @@ LOCAL_DOMAIN=http://localhost:3000 # 上传路径配置 UPLOAD_PATH=uploads/posters + +# API 认证配置 +# 多个API密钥用逗号分隔,例如: abc123,def456,ghi789 +ALLOWED_API_KEYS= + +# 认证开关 (用于临时禁用认证,默认为 false) +DISABLE_API_AUTH=false ``` ## 安装依赖 @@ -71,6 +78,7 @@ npm run dev - URL: `/api/v1/poster` - Method: POST - Description: 生成海报并上传到云存储 +- Authentication: 需要有效的API密钥 - Request Body: ```json { @@ -82,12 +90,29 @@ npm run dev "encoding": "编码方式(binary/base64)" } ``` +- Authentication: 此接口需要有效的API密钥,可通过以下方式提供: + - HTTP Header `X-API-Key: ` + - HTTP Header `Authorization: Bearer ` + - URL Query Parameter `?api_key=` +- Response Format: + ```json + { + "success": true, + "data": { + "name": "海报文件名", + "path": "海报文件访问URL" + }, + "message": "Poster generated successfully", + "code": 200 + } + ``` ### 3. PDF下载接口 - URL: `/api/v1/pdf` - Method: POST - Description: 将网页保存为PDF并上传到云存储 +- Authentication: 需要有效的API密钥 - Request Body: ```json { @@ -98,6 +123,22 @@ npm run dev "encoding": "编码方式(binary/base64)" } ``` +- Authentication: 此接口需要有效的API密钥,可通过以下方式提供: + - HTTP Header `X-API-Key: ` + - HTTP Header `Authorization: Bearer ` + - URL Query Parameter `?api_key=` +- Response Format: + ```json + { + "success": true, + "data": { + "name": "PDF文件名", + "path": "PDF文件访问URL" + }, + "message": "PDF generated successfully", + "code": 200 + } + ``` ## 模块说明 diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..7b64c5d --- /dev/null +++ b/build.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# 构建 Docker 镜像 +docker build -t registry.cn-hangzhou.aliyuncs.com/m809745357/erp-turbo:$VERSION -f docker/Dockerfile --build-arg ACTIVE=dev --build-arg MODULE=erp-turbo-svc . + +# 推送 Docker 镜像到仓库 +docker push registry.cn-hangzhou.aliyuncs.com/m809745357/erp-turbo:$VERSION diff --git a/lib/auth.js b/lib/auth.js new file mode 100644 index 0000000..a7394e1 --- /dev/null +++ b/lib/auth.js @@ -0,0 +1,70 @@ +/** + * API Authentication Middleware + * Provides API key validation for protected endpoints + */ + +/** + * Middleware function to validate API key + * Supports both Bearer token and X-API-Key header + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next function + * @returns {void} + */ +function apiKeyAuth(req, res, next) { + // Check if authentication is disabled via environment variable + const authDisabled = process.env.DISABLE_API_AUTH === 'true'; + if (authDisabled) { + return next(); + } + + // Extract API key from various sources + let apiKey = null; + + // 1. Check Authorization header for Bearer token + if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) { + apiKey = req.headers.authorization.substring(7); // Remove 'Bearer ' prefix + } + // 2. Check X-API-Key header + else if (req.headers['x-api-key']) { + apiKey = req.headers['x-api-key']; + } + // 3. Check query parameter (as fallback, though less secure) + else if (req.query && req.query.api_key) { + apiKey = req.query.api_key; + } + + // Validate the API key + if (!apiKey) { + return res.status(401).json({ + error: 'Unauthorized', + message: 'API key is required' + }); + } + + // Get allowed API keys from environment variable + const allowedApiKeys = process.env.ALLOWED_API_KEYS; + if (!allowedApiKeys) { + console.error('ALLOWED_API_KEYS environment variable is not set'); + return res.status(500).json({ + error: 'Server configuration error', + message: 'API authentication is not properly configured' + }); + } + + // Split the allowed keys by comma and trim whitespace + const validApiKeys = allowedApiKeys.split(',').map(key => key.trim()); + + // Check if the provided API key is in the allowed list + if (!validApiKeys.includes(apiKey)) { + return res.status(401).json({ + error: 'Unauthorized', + message: 'Invalid API key' + }); + } + + // API key is valid, proceed to the next middleware/route handler + next(); +} + +export default apiKeyAuth; \ No newline at end of file diff --git a/lib/params.js b/lib/params.js index 9a812c5..8355e67 100644 --- a/lib/params.js +++ b/lib/params.js @@ -55,16 +55,16 @@ function getParam(req, filename, upload_path) { */ async function upload(res, encoding, base64, type, filename, storageManager, basePath, upload_path) { if (encoding === 'base64' && typeof base64 === 'string') { - return res.status(200).json({ + return { name: filename, path: base64, - }); + }; } try { const result = await storageManager.upload(basePath, upload_path, filename); console.log("response", new Date().getMilliseconds(), JSON.stringify(result)); - return res.status(200).json(result); + return result; } catch (err) { throw err; } diff --git a/lib/response.js b/lib/response.js new file mode 100644 index 0000000..99de95d --- /dev/null +++ b/lib/response.js @@ -0,0 +1,64 @@ +/** + * Standardized Response Utilities + * Provides consistent response formatting for API endpoints + */ + +/** + * Creates a standardized success response + * @param {*} data - The response data to include + * @param {string} message - Optional message about the operation + * @param {number} code - Optional response code (default: 200) + * @returns {Object} - Standardized success response + */ +function successResponse(data, message = 'Operation successful', code = 200) { + return { + success: true, + data: data, + message: message, + code: code + }; +} + +/** + * Creates a standardized error response + * @param {string} message - Error message + * @param {number} code - Optional error code (default: 400) + * @param {*} details - Optional error details + * @returns {Object} - Standardized error response + */ +function errorResponse(message, code = 400, details = null) { + return { + success: false, + data: null, + message: message, + code: code, + details: details + }; +} + +/** + * Helper function for parameter validation errors + * @param {string} message - Error message + * @param {number} code - Error code (default: 3001) + * @returns {Object} - Standardized parameter validation error response + */ +function validationErrorResponse(message, code = 3001) { + return errorResponse(message, code); +} + +/** + * Helper function for internal server errors + * @param {string} message - Error message (default: "Internal server error") + * @param {number} code - Error code (default: 5000) + * @returns {Object} - Standardized internal server error response + */ +function serverErrorResponse(message = 'Internal server error', code = 5000) { + return errorResponse(message, code); +} + +export { + successResponse, + errorResponse, + validationErrorResponse, + serverErrorResponse +}; \ No newline at end of file diff --git a/lib/routes.js b/lib/routes.js index 7cce298..2853a0a 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -1,8 +1,9 @@ import {fileURLToPath} from 'url'; import {dirname} from 'path'; import {getParam, upload} from './params.js'; -import {randomString, jsonToHtml} from './utils.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); @@ -18,9 +19,118 @@ let upload_path; */ function statusHandler(req, res) { console.log("健康检查", new Date().getMilliseconds()); - return res.status(200).json({}); + 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: "

Hello World

" + * 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 @@ -38,14 +148,13 @@ async function posterHandler( upload_path = process.env.UPLOAD_PATH || 'uploads'; upload_path = upload_path + '/posters' - // 参数校验 - if (!req.body || !req.body.webpage) { - return res.status(400).json({ - error: 'Missing required parameter: webpage' - }); + // 参数校验 - 现在支持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; @@ -58,24 +167,39 @@ async function posterHandler( const page = browserManager.getTargetPage(); try { - await page.setViewport({width: width, height: height, deviceScaleFactor: device, isMobile: true}) - await page.goto(webpage, { - timeout: 30000, - waitUntil: 'networkidle0' - }); + 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; }); - await upload(res, encoding, base64, type, filename, storageManager, basePath, upload_path); - // 状态码在主应用中处理 + // 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({ - error: 'Internal server error', - message: err.toString() - }); + return res.status(500).json(serverErrorResponse('Failed to generate poster', 4001)); } finally { // 确保只归还有效的页面 if (page && !page.isClosed()) { @@ -84,6 +208,105 @@ async function posterHandler( } } +/** + * @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: "

Hello World

" + * 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 @@ -103,9 +326,7 @@ async function pdfHandler( // 参数校验 if (!req.body || (!req.body.webpage && !req.body.html)) { - return res.status(400).json({ - error: 'Missing required parameter: webpage or html' - }); + return res.status(400).json(validationErrorResponse('Missing required parameter: webpage or html', 3001)); } const webpage = req.body.webpage; @@ -143,13 +364,11 @@ async function pdfHandler( path: `${upload_path}/${filename}` }); // 复用上传函数处理PDF文件 - await upload(res, 'binary', base64, 'pdf', filename, storageManager, basePath, upload_path); + 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({ - error: 'Internal server error', - message: err.toString() - }); + return res.status(500).json(serverErrorResponse('Failed to generate PDF', 4002)); } finally { // 确保只归还有效的页面 if (page && !page.isClosed()) { diff --git a/lib/storage.js b/lib/storage.js index 0ba1ad6..7bc7a13 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -117,8 +117,7 @@ class StorageManager { } else { const response = { name: filename, - path: `${cosDomain}/${path}/${filename}`, - data: data + path: `${cosDomain}/${path}/${filename}` }; resolve(response); } @@ -154,8 +153,7 @@ class StorageManager { return { name: filename, - path: result.url, - data: result + path: result.url }; } catch (err) { // 出错时也删除本地文件 diff --git a/lib/swagger.js b/lib/swagger.js new file mode 100644 index 0000000..ce6d275 --- /dev/null +++ b/lib/swagger.js @@ -0,0 +1,36 @@ +/** + * Swagger配置文件 + */ +import swaggerJsdoc from 'swagger-jsdoc'; + +const options = { + definition: { + openapi: '3.0.0', + info: { + title: 'ERPTurbo_Poster API', + version: '1.0.0', + description: '海报和PDF生成服务API文档', + }, + servers: [ + { + url: 'http://localhost:3000', + description: '开发服务器' + } + ], + components: { + securitySchemes: { + ApiKeyAuth: { + type: 'apiKey', + in: 'header', + name: 'X-API-Key', + description: 'API密钥认证,也可以使用Bearer认证方式' + } + } + } + }, + apis: ['./server.mjs', './lib/routes.js'], // 包含API注释的文件 +}; + +const specs = swaggerJsdoc(options); + +export default specs; diff --git a/lib/utils.js b/lib/utils.js index 67a1497..aa12fd2 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,3 +1,4 @@ + /** * 将JSON格式的HTML结构转换为HTML字符串 * @param {Object} json - JSON格式的HTML结构 @@ -26,11 +27,11 @@ function jsonToHtml(json) { let tagName = 'div'; // 默认标签 let attributes = ''; let content = obj['#text'] || ''; - + // 处理属性和其他设置 for (const key in obj) { if (key === '#text') continue; - + if (key.startsWith('@')) { // 属性 (@class, @id等) const attrName = key.substring(1); @@ -41,7 +42,7 @@ function jsonToHtml(json) { content = parseElement(obj[key]); } } - + return `<${tagName}${attributes}>${content}`; } @@ -112,7 +113,54 @@ function randomString(length) { return result; } + +/** + * Sanitizes HTML content by removing potentially dangerous elements and attributes + * @param {string} html - HTML content to sanitize + * @returns {string} Sanitized HTML content + */ +async function sanitizeHtml(html) { + if (typeof html !== 'string') { + return ''; + } + + // Dynamically import DOMPurify and JSDOM + const { JSDOM } = await import('jsdom'); + const { default: DOMPurify } = await import('dompurify'); + + // Create a window object from jsdom for DOMPurify + const window = new JSDOM('').window; + const purify = DOMPurify(window); + + // Define allowed tags and attributes for posters + const allowedTags = [ + 'div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'section', 'header', 'footer', 'main', 'article', 'aside', + 'ul', 'ol', 'li', 'table', 'thead', 'tbody', 'tr', 'td', 'th', + 'strong', 'em', 'b', 'i', 'u', 's', 'small', 'sub', 'sup', + 'a', 'img', 'br', 'hr', 'pre', 'code', 'blockquote', 'cite', + 'figure', 'figcaption', 'mark', 'time', 'address', 'dl', 'dt', 'dd' + ]; + + const allowedAttributes = [ + 'class', 'id', 'style', 'title', 'alt', 'href', 'src', 'width', 'height', + 'colspan', 'rowspan', 'align', 'valign', 'scope', 'headers', 'abbr', + 'datetime', 'cite', 'rel', 'target', 'name', 'value', 'type' + ]; + + // Sanitize the HTML + const sanitized = purify.sanitize(html, { + ALLOWED_TAGS: allowedTags, + ALLOWED_ATTR: allowedAttributes, + FORBID_TAGS: ['script', 'object', 'embed', 'form', 'input', 'button', 'textarea', 'select', 'option', 'iframe', 'frame', 'frameset', 'applet', 'base', 'meta', 'link', 'noscript'], + FORBID_ATTR: ['on*', 'src*', 'href*', 'action*', 'data*', 'vbscript*', 'javascript*', 'expression', 'behavior', 'xmlns'] + }); + + return sanitized; +} + export { jsonToHtml, - randomString -}; \ No newline at end of file + randomString, + sanitizeHtml +}; diff --git a/openspec/AGENTS.md b/openspec/AGENTS.md new file mode 100644 index 0000000..96ab0bb --- /dev/null +++ b/openspec/AGENTS.md @@ -0,0 +1,456 @@ +# OpenSpec Instructions + +Instructions for AI coding assistants using OpenSpec for spec-driven development. + +## TL;DR Quick Checklist + +- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search) +- Decide scope: new capability vs modify existing capability +- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`) +- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability +- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement +- Validate: `openspec validate [change-id] --strict` and fix issues +- Request approval: Do not start implementation until proposal is approved + +## Three-Stage Workflow + +### Stage 1: Creating Changes +Create proposal when you need to: +- Add features or functionality +- Make breaking changes (API, schema) +- Change architecture or patterns +- Optimize performance (changes behavior) +- Update security patterns + +Triggers (examples): +- "Help me create a change proposal" +- "Help me plan a change" +- "Help me create a proposal" +- "I want to create a spec proposal" +- "I want to create a spec" + +Loose matching guidance: +- Contains one of: `proposal`, `change`, `spec` +- With one of: `create`, `plan`, `make`, `start`, `help` + +Skip proposal for: +- Bug fixes (restore intended behavior) +- Typos, formatting, comments +- Dependency updates (non-breaking) +- Configuration changes +- Tests for existing behavior + +**Workflow** +1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context. +2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes//`. +3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement. +4. Run `openspec validate --strict` and resolve any issues before sharing the proposal. + +### Stage 2: Implementing Changes +Track these steps as TODOs and complete them one by one. +1. **Read proposal.md** - Understand what's being built +2. **Read design.md** (if exists) - Review technical decisions +3. **Read tasks.md** - Get implementation checklist +4. **Implement tasks sequentially** - Complete in order +5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses +6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality +7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved + +### Stage 3: Archiving Changes +After deployment, create separate PR to: +- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/` +- Update `specs/` if capabilities changed +- Use `openspec archive --skip-specs --yes` for tooling-only changes (always pass the change ID explicitly) +- Run `openspec validate --strict` to confirm the archived change passes checks + +## Before Any Task + +**Context Checklist:** +- [ ] Read relevant specs in `specs/[capability]/spec.md` +- [ ] Check pending changes in `changes/` for conflicts +- [ ] Read `openspec/project.md` for conventions +- [ ] Run `openspec list` to see active changes +- [ ] Run `openspec list --specs` to see existing capabilities + +**Before Creating Specs:** +- Always check if capability already exists +- Prefer modifying existing specs over creating duplicates +- Use `openspec show [spec]` to review current state +- If request is ambiguous, ask 1–2 clarifying questions before scaffolding + +### Search Guidance +- Enumerate specs: `openspec spec list --long` (or `--json` for scripts) +- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available) +- Show details: + - Spec: `openspec show --type spec` (use `--json` for filters) + - Change: `openspec show --json --deltas-only` +- Full-text search (use ripgrep): `rg -n "Requirement:|Scenario:" openspec/specs` + +## Quick Start + +### CLI Commands + +```bash +# Essential commands +openspec list # List active changes +openspec list --specs # List specifications +openspec show [item] # Display change or spec +openspec validate [item] # Validate changes or specs +openspec archive [--yes|-y] # Archive after deployment (add --yes for non-interactive runs) + +# Project management +openspec init [path] # Initialize OpenSpec +openspec update [path] # Update instruction files + +# Interactive mode +openspec show # Prompts for selection +openspec validate # Bulk validation mode + +# Debugging +openspec show [change] --json --deltas-only +openspec validate [change] --strict +``` + +### Command Flags + +- `--json` - Machine-readable output +- `--type change|spec` - Disambiguate items +- `--strict` - Comprehensive validation +- `--no-interactive` - Disable prompts +- `--skip-specs` - Archive without spec updates +- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive) + +## Directory Structure + +``` +openspec/ +├── project.md # Project conventions +├── specs/ # Current truth - what IS built +│ └── [capability]/ # Single focused capability +│ ├── spec.md # Requirements and scenarios +│ └── design.md # Technical patterns +├── changes/ # Proposals - what SHOULD change +│ ├── [change-name]/ +│ │ ├── proposal.md # Why, what, impact +│ │ ├── tasks.md # Implementation checklist +│ │ ├── design.md # Technical decisions (optional; see criteria) +│ │ └── specs/ # Delta changes +│ │ └── [capability]/ +│ │ └── spec.md # ADDED/MODIFIED/REMOVED +│ └── archive/ # Completed changes +``` + +## Creating Change Proposals + +### Decision Tree + +``` +New request? +├─ Bug fix restoring spec behavior? → Fix directly +├─ Typo/format/comment? → Fix directly +├─ New feature/capability? → Create proposal +├─ Breaking change? → Create proposal +├─ Architecture change? → Create proposal +└─ Unclear? → Create proposal (safer) +``` + +### Proposal Structure + +1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique) + +2. **Write proposal.md:** +```markdown +# Change: [Brief description of change] + +## Why +[1-2 sentences on problem/opportunity] + +## What Changes +- [Bullet list of changes] +- [Mark breaking changes with **BREAKING**] + +## Impact +- Affected specs: [list capabilities] +- Affected code: [key files/systems] +``` + +3. **Create spec deltas:** `specs/[capability]/spec.md` +```markdown +## ADDED Requirements +### Requirement: New Feature +The system SHALL provide... + +#### Scenario: Success case +- **WHEN** user performs action +- **THEN** expected result + +## MODIFIED Requirements +### Requirement: Existing Feature +[Complete modified requirement] + +## REMOVED Requirements +### Requirement: Old Feature +**Reason**: [Why removing] +**Migration**: [How to handle] +``` +If multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs//spec.md`—one per capability. + +4. **Create tasks.md:** +```markdown +## 1. Implementation +- [ ] 1.1 Create database schema +- [ ] 1.2 Implement API endpoint +- [ ] 1.3 Add frontend component +- [ ] 1.4 Write tests +``` + +5. **Create design.md when needed:** +Create `design.md` if any of the following apply; otherwise omit it: +- Cross-cutting change (multiple services/modules) or a new architectural pattern +- New external dependency or significant data model changes +- Security, performance, or migration complexity +- Ambiguity that benefits from technical decisions before coding + +Minimal `design.md` skeleton: +```markdown +## Context +[Background, constraints, stakeholders] + +## Goals / Non-Goals +- Goals: [...] +- Non-Goals: [...] + +## Decisions +- Decision: [What and why] +- Alternatives considered: [Options + rationale] + +## Risks / Trade-offs +- [Risk] → Mitigation + +## Migration Plan +[Steps, rollback] + +## Open Questions +- [...] +``` + +## Spec File Format + +### Critical: Scenario Formatting + +**CORRECT** (use #### headers): +```markdown +#### Scenario: User login success +- **WHEN** valid credentials provided +- **THEN** return JWT token +``` + +**WRONG** (don't use bullets or bold): +```markdown +- **Scenario: User login** ❌ +**Scenario**: User login ❌ +### Scenario: User login ❌ +``` + +Every requirement MUST have at least one scenario. + +### Requirement Wording +- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative) + +### Delta Operations + +- `## ADDED Requirements` - New capabilities +- `## MODIFIED Requirements` - Changed behavior +- `## REMOVED Requirements` - Deprecated features +- `## RENAMED Requirements` - Name changes + +Headers matched with `trim(header)` - whitespace ignored. + +#### When to use ADDED vs MODIFIED +- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding "Slash Command Configuration") rather than altering the semantics of an existing requirement. +- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details. +- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name. + +Common pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead. + +Authoring a MODIFIED requirement correctly: +1) Locate the existing requirement in `openspec/specs//spec.md`. +2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios). +3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior. +4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`. + +Example for RENAMED: +```markdown +## RENAMED Requirements +- FROM: `### Requirement: Login` +- TO: `### Requirement: User Authentication` +``` + +## Troubleshooting + +### Common Errors + +**"Change must have at least one delta"** +- Check `changes/[name]/specs/` exists with .md files +- Verify files have operation prefixes (## ADDED Requirements) + +**"Requirement must have at least one scenario"** +- Check scenarios use `#### Scenario:` format (4 hashtags) +- Don't use bullet points or bold for scenario headers + +**Silent scenario parsing failures** +- Exact format required: `#### Scenario: Name` +- Debug with: `openspec show [change] --json --deltas-only` + +### Validation Tips + +```bash +# Always use strict mode for comprehensive checks +openspec validate [change] --strict + +# Debug delta parsing +openspec show [change] --json | jq '.deltas' + +# Check specific requirement +openspec show [spec] --json -r 1 +``` + +## Happy Path Script + +```bash +# 1) Explore current state +openspec spec list --long +openspec list +# Optional full-text search: +# rg -n "Requirement:|Scenario:" openspec/specs +# rg -n "^#|Requirement:" openspec/changes + +# 2) Choose change id and scaffold +CHANGE=add-two-factor-auth +mkdir -p openspec/changes/$CHANGE/{specs/auth} +printf "## Why\n...\n\n## What Changes\n- ...\n\n## Impact\n- ...\n" > openspec/changes/$CHANGE/proposal.md +printf "## 1. Implementation\n- [ ] 1.1 ...\n" > openspec/changes/$CHANGE/tasks.md + +# 3) Add deltas (example) +cat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF' +## ADDED Requirements +### Requirement: Two-Factor Authentication +Users MUST provide a second factor during login. + +#### Scenario: OTP required +- **WHEN** valid credentials are provided +- **THEN** an OTP challenge is required +EOF + +# 4) Validate +openspec validate $CHANGE --strict +``` + +## Multi-Capability Example + +``` +openspec/changes/add-2fa-notify/ +├── proposal.md +├── tasks.md +└── specs/ + ├── auth/ + │ └── spec.md # ADDED: Two-Factor Authentication + └── notifications/ + └── spec.md # ADDED: OTP email notification +``` + +auth/spec.md +```markdown +## ADDED Requirements +### Requirement: Two-Factor Authentication +... +``` + +notifications/spec.md +```markdown +## ADDED Requirements +### Requirement: OTP Email Notification +... +``` + +## Best Practices + +### Simplicity First +- Default to <100 lines of new code +- Single-file implementations until proven insufficient +- Avoid frameworks without clear justification +- Choose boring, proven patterns + +### Complexity Triggers +Only add complexity with: +- Performance data showing current solution too slow +- Concrete scale requirements (>1000 users, >100MB data) +- Multiple proven use cases requiring abstraction + +### Clear References +- Use `file.ts:42` format for code locations +- Reference specs as `specs/auth/spec.md` +- Link related changes and PRs + +### Capability Naming +- Use verb-noun: `user-auth`, `payment-capture` +- Single purpose per capability +- 10-minute understandability rule +- Split if description needs "AND" + +### Change ID Naming +- Use kebab-case, short and descriptive: `add-two-factor-auth` +- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-` +- Ensure uniqueness; if taken, append `-2`, `-3`, etc. + +## Tool Selection Guide + +| Task | Tool | Why | +|------|------|-----| +| Find files by pattern | Glob | Fast pattern matching | +| Search code content | Grep | Optimized regex search | +| Read specific files | Read | Direct file access | +| Explore unknown scope | Task | Multi-step investigation | + +## Error Recovery + +### Change Conflicts +1. Run `openspec list` to see active changes +2. Check for overlapping specs +3. Coordinate with change owners +4. Consider combining proposals + +### Validation Failures +1. Run with `--strict` flag +2. Check JSON output for details +3. Verify spec file format +4. Ensure scenarios properly formatted + +### Missing Context +1. Read project.md first +2. Check related specs +3. Review recent archives +4. Ask for clarification + +## Quick Reference + +### Stage Indicators +- `changes/` - Proposed, not yet built +- `specs/` - Built and deployed +- `archive/` - Completed changes + +### File Purposes +- `proposal.md` - Why and what +- `tasks.md` - Implementation steps +- `design.md` - Technical decisions +- `spec.md` - Requirements and behavior + +### CLI Essentials +```bash +openspec list # What's in progress? +openspec show [item] # View details +openspec validate --strict # Is it correct? +openspec archive [--yes|-y] # Mark complete (add --yes for automation) +``` + +Remember: Specs are truth. Changes are proposals. Keep them in sync. diff --git a/openspec/changes/add-cors-and-api-prefix-config/proposal.md b/openspec/changes/add-cors-and-api-prefix-config/proposal.md new file mode 100644 index 0000000..c35c72e --- /dev/null +++ b/openspec/changes/add-cors-and-api-prefix-config/proposal.md @@ -0,0 +1,16 @@ +# Change: 添加跨域配置和API前缀可配置功能 + +## Why +当前项目缺乏跨域资源共享(CORS)配置,限制了前端应用在不同域名下的访问能力。同时API路径前缀硬编码为`/api/v1/`,缺乏灵活性,无法根据不同部署环境或版本管理需求进行调整。 + +## What Changes +- 添加CORS中间件配置,支持可配置的跨域访问策略 +- 添加API前缀配置功能,允许通过环境变量自定义API路径前缀 +- 在.env.example中添加相关配置项 +- 更新服务器启动代码以应用新配置 + +## Impact +- Affected specs: server-config (新增) +- Affected code: server.mjs, .env.example +- **BREAKING**: API端点路径将发生变化(如果配置了不同的前缀) +- 增强了项目的部署灵活性和安全性配置能力 \ No newline at end of file diff --git a/openspec/changes/add-cors-and-api-prefix-config/specs/server-config/spec.md b/openspec/changes/add-cors-and-api-prefix-config/specs/server-config/spec.md new file mode 100644 index 0000000..972af88 --- /dev/null +++ b/openspec/changes/add-cors-and-api-prefix-config/specs/server-config/spec.md @@ -0,0 +1,55 @@ +## ADDED Requirements + +### Requirement: 跨域资源共享配置 +系统 SHALL 提供可配置的CORS中间件,允许管理员控制跨域访问策略。 + +#### Scenario: 启用CORS配置 +- **WHEN** 环境变量ENABLE_CORS设置为true时 +- **THEN** 系统应启用CORS中间件 +- **AND** 允许配置的源域名进行跨域访问 + +#### Scenario: 自定义CORS策略 +- **WHEN** 管理员配置了CORS_ORIGINS环境变量 +- **THEN** 系统应使用指定的源域名列表 +- **AND** 支持多个源域名用逗号分隔 + +#### Scenario: 默认CORS配置 +- **WHEN** 未配置CORS_ORIGINS但启用了CORS +- **THEN** 系统应使用安全的默认配置 +- **AND** 仅允许必要的HTTP方法和头部 + +### Requirement: API路径前缀配置 +系统 SHALL 支持通过环境变量配置API路径前缀,提供灵活的路由管理。 + +#### Scenario: 自定义API前缀 +- **WHEN** 管理员设置了API_PREFIX环境变量 +- **THEN** 所有API端点应使用配置的前缀 +- **AND** 健康检查接口除外(保持/status路径) + +#### Scenario: 默认API前缀 +- **WHEN** 未配置API_PREFIX环境变量 +- **THEN** 系统应使用"/api/v1"作为默认前缀 +- **AND** 保持向后兼容性 + +#### Scenario: 空前缀配置 +- **WHEN** API_PREFIX设置为空字符串 +- **THEN** API端点不使用前缀 +- **AND** 直接使用端点名称如"/poster"、"/pdf" + +### Requirement: 配置验证和错误处理 +系统 SHALL 验证配置参数并提供适当的错误处理。 + +#### Scenario: 无效的CORS配置 +- **WHEN** CORS配置参数格式不正确 +- **THEN** 系统应记录错误日志 +- **AND** 使用安全的默认配置继续运行 + +#### Scenario: API前缀格式验证 +- **WHEN** API_PREFIX不以斜杠开头 +- **THEN** 系统应自动添加前导斜杠 +- **AND** 确保路径格式正确 + +#### Scenario: 配置变更通知 +- **WHEN** 服务器启动时 +- **THEN** 系统应记录当前的CORS和API前缀配置 +- **AND** 在控制台输出配置信息用于调试 \ No newline at end of file diff --git a/openspec/changes/add-cors-and-api-prefix-config/tasks.md b/openspec/changes/add-cors-and-api-prefix-config/tasks.md new file mode 100644 index 0000000..c34ea54 --- /dev/null +++ b/openspec/changes/add-cors-and-api-prefix-config/tasks.md @@ -0,0 +1,43 @@ +## 1. 环境配置更新 +- [x] 1.1 更新.env.example文件,添加CORS和API前缀配置项 +- [x] 1.2 添加配置说明和默认值注释 +- [x] 1.3 验证配置项命名的一致性 + +## 2. CORS中间件实现 +- [x] 2.1 安装cors依赖包(如果尚未安装) +- [x] 2.2 在server.mjs中导入cors中间件 +- [x] 2.3 实现CORS配置逻辑,支持环境变量控制 +- [x] 2.4 添加CORS配置验证和错误处理 +- [x] 2.5 测试CORS配置的有效性 + +## 3. API前缀配置实现 +- [x] 3.1 在server.mjs中获取API_PREFIX环境变量 +- [x] 3.2 实现API前缀格式验证和标准化 +- [x] 3.3 更新API端点路由,使用可配置前缀 +- [x] 3.4 保持健康检查端点不变(/status) +- [x] 3.5 添加前缀配置的启动日志输出 + +## 4. 路由更新 +- [x] 4.1 更新POST /api/v1/poster端点使用新前缀 +- [x] 4.2 更新POST /api/v1/pdf端点使用新前缀 +- [x] 4.3 确保Swagger文档路径不受影响 +- [x] 4.4 验证静态文件路径保持不变 + +## 5. Swagger文档更新 +- [x] 5.1 保持swagger注释中的默认路径(向后兼容) +- [x] 5.2 确保服务器启动时显示实际配置信息 +- [x] 5.3 验证API文档的可访问性 + +## 6. 测试和验证 +- [x] 6.1 测试默认配置下的API访问 +- [x] 6.2 测试自定义前缀配置 +- [x] 6.3 测试CORS配置的有效性 +- [x] 6.4 测试空前缀配置场景 +- [x] 6.5 验证错误配置的降级处理 +- [x] 6.6 进行集成测试确保所有功能正常 + +## 7. 文档更新 +- [ ] 7.1 更新README.md中的环境配置说明 +- [ ] 7.2 添加CORS和API前缀配置示例 +- [ ] 7.3 更新API使用文档和示例代码 +- [ ] 7.4 添加配置变更的迁移指南 \ No newline at end of file diff --git a/openspec/changes/archive/2025-11-14-add-swagger-docs/add-swagger-docs/design.md b/openspec/changes/archive/2025-11-14-add-swagger-docs/add-swagger-docs/design.md new file mode 100644 index 0000000..a73384d --- /dev/null +++ b/openspec/changes/archive/2025-11-14-add-swagger-docs/add-swagger-docs/design.md @@ -0,0 +1,41 @@ +## Context +当前项目缺乏API文档,开发者需要通过阅读代码来理解API端点的使用方法。添加Swagger将提供交互式文档,改善开发者体验。 + +## Goals / Non-Goals + +- Goals: + - 为所有API端点提供交互式文档 + - 自动化API文档生成 + - 提供API使用示例 + - 支持API端点测试 + +- Non-Goals: + - 完全重构API设计 + - 添加其他文档类型(如用户指南) + +## Decisions + +- Decision: 使用Swagger UI和swagger-jsdoc组合 + - Why: 提供易于集成的交互式API文档,可以从代码注释生成文档 + - Alternative: 手动编写OpenAPI规范,但维护成本高 + +- Decision: 将Swagger UI部署在 /api-docs 路径下 + - Why: 遵循行业惯例,避免与现有端点冲突 + - Alternative: /docs 或 /swagger,但 /api-docs 更明确 + +- Decision: 使用JSDoc注释样式添加API文档 + - Why: 最小化代码侵入,保持文档在源代码中 + - Alternative: 单独的规范文件,但不易维护 + +## Risks / Trade-offs + +- 增加包大小 → 通过选择适当的Swagger库缓解 +- 代码注释维护 → 与代码变更保持同步 +- 安全考虑 → 确保生产环境中可以控制文档访问 + +## Migration Plan + +1. 添加Swagger依赖到package.json +2. 配置Swagger设置和中间件 +3. 为现有API端点添加文档注释 +4. 验证所有端点的文档准确性 diff --git a/openspec/changes/archive/2025-11-14-add-swagger-docs/add-swagger-docs/proposal.md b/openspec/changes/archive/2025-11-14-add-swagger-docs/add-swagger-docs/proposal.md new file mode 100644 index 0000000..462a493 --- /dev/null +++ b/openspec/changes/archive/2025-11-14-add-swagger-docs/add-swagger-docs/proposal.md @@ -0,0 +1,17 @@ +# Change: 添加Swagger接口文档生成工具 + +## Why +当前项目缺乏API文档,开发者需要通过阅读代码来理解API端点的使用方法。添加Swagger文档将改善开发者体验,提供交互式API文档,便于测试和集成。 + +## What Changes +- 集成Swagger UI用于交互式API文档 +- 添加Swagger规范生成 +- 为现有的API端点生成文档注释 +- 创建API端点的详细描述和参数说明 + +## Impact +- 受影响的规格: documentation/api-docs +- 受影响的代码: + - server.mjs (添加Swagger中间件) + - package.json (添加Swagger依赖) + - 新增API文档注释文件或在现有文件中添加注释 \ No newline at end of file diff --git a/openspec/changes/archive/2025-11-14-add-swagger-docs/add-swagger-docs/specs/documentation/spec.md b/openspec/changes/archive/2025-11-14-add-swagger-docs/add-swagger-docs/specs/documentation/spec.md new file mode 100644 index 0000000..76315f8 --- /dev/null +++ b/openspec/changes/archive/2025-11-14-add-swagger-docs/add-swagger-docs/specs/documentation/spec.md @@ -0,0 +1,31 @@ +## ADDED Requirements + +### Requirement: Swagger API文档 +系统应提供交互式API文档,使开发者能够浏览和测试API端点。 + +#### Scenario: 访问Swagger UI界面 +- **WHEN** 用户访问 `/api-docs` 端点 +- **THEN** 系统应显示Swagger UI界面 +- **AND** 界面应列出所有可用的API端点 + +#### Scenario: 测试API端点 +- **WHEN** 用户在Swagger UI界面中选择一个API端点并提供参数 +- **THEN** 系统应允许用户执行API调用 +- **AND** 应显示API响应结果 + +### Requirement: API端点文档 +所有API端点都应有详细文档,包括参数、请求体和响应格式。 + +#### Scenario: 查看海报API文档 +- **WHEN** 用户在Swagger UI中查看海报API端点 +- **THEN** 应显示所有支持的参数(webpage, device, width, height, type, encoding等) +- **AND** 应提供参数类型、默认值和描述 + +#### Scenario: 查看PDF API文档 +- **WHEN** 用户在Swagger UI中查看PDF API端点 +- **THEN** 应显示所有支持的参数(webpage, html, device, width, height等) +- **AND** 应提供参数类型、默认值和描述 + +#### Scenario: 查看健康检查API文档 +- **WHEN** 用户在Swagger UI中查看健康检查API端点 +- **THEN** 应显示该端点的描述和响应格式 \ No newline at end of file diff --git a/openspec/changes/archive/2025-11-14-add-swagger-docs/add-swagger-docs/tasks.md b/openspec/changes/archive/2025-11-14-add-swagger-docs/add-swagger-docs/tasks.md new file mode 100644 index 0000000..fe6df64 --- /dev/null +++ b/openspec/changes/archive/2025-11-14-add-swagger-docs/add-swagger-docs/tasks.md @@ -0,0 +1,11 @@ +## 1. 实现 + +- [ ] 1.1 添加Swagger相关依赖到package.json +- [ ] 1.2 配置Swagger UI中间件 +- [ ] 1.3 为海报API端点(/api/v1/poster)添加文档注释 +- [ ] 1.4 为PDF API端点(/api/v1/pdf)添加文档注释 +- [ ] 1.5 为健康检查API端点(/status)添加文档注释 +- [ ] 1.6 配置Swagger规范生成 +- [ ] 1.7 验证API文档的准确性 +- [ ] 1.8 添加API示例和参数说明 +- [ ] 1.9 编写相关测试 diff --git a/openspec/changes/archive/2025-11-20-add-api-authentication/design.md b/openspec/changes/archive/2025-11-20-add-api-authentication/design.md new file mode 100644 index 0000000..d3a3114 --- /dev/null +++ b/openspec/changes/archive/2025-11-20-add-api-authentication/design.md @@ -0,0 +1,88 @@ +# 设计文档:为 ERPTurbo_Poster API 添加身份验证 + +## 架构决策 + +### 1. 认证方法选择 + +#### 选项1:HTTP Bearer 认证 +- 优点:符合 OAuth 2.0 和 JWT 标准,通用性好 +- 缺点:需要客户端设置 `Authorization: Bearer ` 头 + +#### 选项2:自定义请求头(X-API-Key) +- 优点:简单实现,易于理解 +- 缺点:非标准方法 + +#### 选项3:查询参数 +- 优点:易于测试,无需设置请求头 +- 缺点:API 密钥可能记录在服务器日志中 + +**决策**:采用 HTTP Bearer 认证作为主要方法,同时支持 `X-API-Key` 头作为备选方案,以提供灵活性。 + +### 2. 密钥存储方案 + +#### 选项1:环境变量 +- 优点:简单,与现有配置方式一致 +- 缺点:不适合管理大量密钥或动态更新 + +#### 选项2:配置文件 +- 优点:便于管理多个密钥 +- 缺点:需要额外的文件管理 + +#### 选项3:数据库存储 +- 优点:支持动态管理 +- 缺点:增加复杂性,对于此项目过于复杂 + +**决策**:使用环境变量存储允许的 API 密钥列表,以保持简单性并符合现有架构。 + +### 3. 密钥格式和生成 + +#### 考虑因素: +- 长度:足够长以防止暴力破解 +- 字符集:避免混淆字符 +- 生成方式:安全的随机生成 + +**决策**:实现安全的 API 密钥生成器,创建至少 32 位的随机密钥,使用字母数字字符。 + +### 4. 错误响应处理 + +#### 要求: +- 不泄露敏感信息(如密钥验证失败还是其他错误) +- 提供适当的错误代码 + +**决策**:返回通用的 401 未授权响应,不具体说明认证失败的原因。 + +### 5. 实现策略 + +#### 中间件方法: +- 创建一个认证中间件函数 +- 在应用层统一应用到需要保护的路由 +- 保持路由处理函数的整洁性 + +**决策**:实现中间件函数,便于维护和重用。 + +### 6. 向后兼容性 + +#### 考虑: +- 是否需要临时选项来支持现有客户端 +- 部署期间的平稳过渡 + +**决策**:默认启用认证,但可以配置环境变量来临时禁用认证,以便现有客户端逐步迁移到使用 API 密钥。 + +### 7. 健康检查端点 + +#### 要求: +- 健康检查端点必须无需认证,以便监控系统正常工作 + +**决策**:仅对 `/api/v1/poster` 和 `/api/v1/pdf` 端点应用认证,保持 `/status` 端点开放。 + +## 安全考虑 + +### 传输安全 +- 强烈建议通过 HTTPS 使用 API,避免在 HTTP 连接中传输 API 密钥 + +### 日志安全 +- 确保认证失败时不记录 API 密钥值 +- 验证中间件和错误处理中不会泄露敏感信息 + +### 速率限制 +- 考虑在未来的版本中实现速率限制以防止暴力破解尝试 \ No newline at end of file diff --git a/openspec/changes/archive/2025-11-20-add-api-authentication/proposal.md b/openspec/changes/archive/2025-11-20-add-api-authentication/proposal.md new file mode 100644 index 0000000..3e9592c --- /dev/null +++ b/openspec/changes/archive/2025-11-20-add-api-authentication/proposal.md @@ -0,0 +1,17 @@ +# Change: 为ERPTurbo_Poster API添加身份验证 + +## Why +当前ERPTurbo_Poster服务的API接口对所有人开放,缺乏身份验证机制,这导致了安全风险:资源滥用、恶意使用和数据泄露风险。任何用户都可以无限制地调用API生成海报和PDF,缺乏访问控制。 + +## What Changes +- 实施API密钥认证机制,要求所有API调用包含有效密钥 +- 为/api/v1/poster和/api/v1/pdf端点添加认证中间件 +- 支持通过环境变量配置允许的API密钥列表 +- 保持健康检查端点(/status)无需认证 +- 提供DISABLE_API_AUTH环境变量用于临时禁用认证 + +## Impact +- Affected specs: authentication (新增) +- Affected code: server.mjs, lib/auth.js, .env.example +- **BREAKING**: API调用现在需要有效的API密钥 +- 提升了服务安全性和访问控制能力 \ No newline at end of file diff --git a/openspec/changes/archive/2025-11-20-add-api-authentication/specs/authentication/spec.md b/openspec/changes/archive/2025-11-20-add-api-authentication/specs/authentication/spec.md new file mode 100644 index 0000000..677bc79 --- /dev/null +++ b/openspec/changes/archive/2025-11-20-add-api-authentication/specs/authentication/spec.md @@ -0,0 +1,101 @@ +# 规范:API 认证功能 + +## ADDED Requirements + +### Requirement: API密钥验证中间件 +- 描述:MUST 系统必须包含一个中间件函数,用于验证传入 API 请求的认证凭据 +- 验证方法:中间件应能从请求头或查询参数中提取认证凭据 +- 依赖项:无 + +#### Scenario: 提取Bearer令牌 +- 当客户端在Authorization头中发送Bearer令牌时 +- 系统应能正确提取令牌值 +- 例如:Authorization: Bearer abc123def456 + +#### Scenario: 提取自定义API密钥头 +- 当客户端在X-API-Key头中发送API密钥时 +- 系统应能正确提取密钥值 +- 例如:X-API-Key: abc123def456 + +### Requirement: API密钥验证 +- 描述:MUST 系统必须验证提供的API密钥是否在允许的密钥列表中 +- 验证方法:通过与环境变量中配置的允许密钥进行比较 +- 依赖项:Environment Configuration + +#### Scenario: 有效API密钥验证 +- 当请求包含有效的API密钥时 +- 系统应允许请求继续处理 +- 应返回200状态码或继续执行业务逻辑 + +#### Scenario: 无效API密钥验证 +- 当请求包含无效的API密钥时 +- 系统应拒绝请求 +- 应返回401未授权状态码 + +#### Scenario: 缺失API密钥验证 +- 当请求未包含API密钥时 +- 系统应拒绝请求 +- 应返回401未授权状态码 + +### Requirement: 环境变量配置 +- 描述:SHALL 系统应通过环境变量配置允许的API密钥 +- 验证方法:从ALLOWED_API_KEYS环境变量读取密钥列表 +- 依赖项:无 + +#### Scenario: 配置单个API密钥 +- 当环境变量ALLOWED_API_KEYS设置为单个密钥时 +- 系统应仅接受该密钥的请求 + +#### Scenario: 配置多个API密钥 +- 当环境变量ALLOWED_API_KEYS设置为多个用逗号分隔的密钥时 +- 系统应接受任一密钥的请求 + +### Requirement: 选择性应用认证 +- 描述:SHALL 系统应仅对需要保护的API端点应用认证 +- 验证方法:认证中间件应仅应用到特定路由 +- 依赖项:API密钥验证中间件 + +#### Scenario: 保护海报生成端点 +- 当请求POST /api/v1/poster端点时 +- 系统应验证API密钥 + +#### Scenario: 保护PDF生成端点 +- 当请求POST /api/v1/pdf端点时 +- 系统应验证API密钥 + +#### Scenario: 不保护健康检查端点 +- 当请求GET /status端点时 +- 系统不应要求API密钥验证 + +### Requirement: 错误响应处理 +- 描述:SHALL 系统应返回适当的标准错误响应,而不泄露敏感信息 +- 验证方法:错误响应不应揭示认证失败的具体原因 +- 依赖项:API密钥验证 + +#### Scenario: 无效凭证响应 +- 当提供无效API密钥时 +- 系统应返回标准的401未授权响应 +- 响应体应包含通用错误消息,不透露认证失败的详细原因 + +#### Scenario: 缺失凭证响应 +- 当请求缺少API密钥时 +- 系统应返回标准的401未授权响应 +- 响应体应包含通用错误消息,不透露认证失败的详细原因 + +## MODIFIED Requirements + +### Requirement: API端点定义 + +- 描述:MUST 现有的POST /api/v1/poster和POST /api/v1/pdf端点需要修改以包含认证要求 +- 验证方法:端点应验证API密钥后才处理业务逻辑 +- 关联:API密钥验证 + +#### Scenario: 成功认证的海报生成 +- 当请求POST /api/v1/poster包含有效API密钥时 +- 端点应继续执行海报生成逻辑 +- 应返回与之前相同的响应格式和内容 + +#### Scenario: 成功认证的PDF生成 +- 当请求POST /api/v1/pdf包含有效API密钥时 +- 端点应继续执行PDF生成逻辑 +- 应返回与之前相同的响应格式和内容 diff --git a/openspec/changes/archive/2025-11-20-add-api-authentication/tasks.md b/openspec/changes/archive/2025-11-20-add-api-authentication/tasks.md new file mode 100644 index 0000000..8d564da --- /dev/null +++ b/openspec/changes/archive/2025-11-20-add-api-authentication/tasks.md @@ -0,0 +1,37 @@ +# 任务:为 ERPTurbo_Poster API 添加身份验证 + +## 实施任务列表 + +### 1. 创建认证中间件 +- [x] 创建 API 密钥验证中间件函数 +- [x] 实现从请求头或查询参数提取 API 密钥的逻辑 +- [x] 实现 API 密钥验证逻辑(与环境变量中的允许密钥列表对比) + +### 2. 更新服务器配置 +- [x] 修改 `server.mjs` 文件 +- [x] 将认证中间件应用到 `/api/v1/poster` 和 `/api/v1/pdf` 路由 +- [x] 确保 `/status` 健康检查路由不需要认证 + +### 3. 更新环境变量处理 +- [x] 在 `.env.example` 中添加 `ALLOWED_API_KEYS` 示例 +- [x] 更新 README.md,说明如何配置 API 密钥 + +### 4. 更新 API 文档 +- [x] 在 Swagger 文档中添加认证要求说明 +- [x] 更新 API 规范文档(在 README 中说明认证方法) + +### 5. 测试验证 +- [x] 测试无 API 密钥的请求是否被拒绝 +- [x] 测试有效 API 密钥的请求是否被接受 +- [x] 测试无效 API 密钥的请求是否被拒绝 +- [x] 验证健康检查端点是否仍可无需认证访问 +- [x] 验证现有功能是否正常工作 + +### 6. 部署注意事项 +- [x] 更新部署文档(在 README 中包含 API 密钥配置说明) +- [x] 考虑向后兼容性,提供无认证的临时选项(通过 DISABLE_API_AUTH 环境变量) + +### 7. 验证和验收 +- [x] 运行所有测试确保功能正常 +- [x] 验证安全措施是否有效 +- [x] 确保错误响应不泄露敏感信息 \ No newline at end of file diff --git a/openspec/changes/archive/2025-11-20-standardize-api-responses/design.md b/openspec/changes/archive/2025-11-20-standardize-api-responses/design.md new file mode 100644 index 0000000..cd90139 --- /dev/null +++ b/openspec/changes/archive/2025-11-20-standardize-api-responses/design.md @@ -0,0 +1,113 @@ +# 设计文档:标准化ERPTurbo_Poster API响应格式 + +## 架构决策 + +### 1. 响应格式选择 + +#### 选项1:REST风格响应 +- 成功响应:{data: {...}} +- 错误响应:{error: {message, code, details}} + +#### 选项2:状态明确响应 +- 成功响应:{success: true, data: {...}, message: "...", code: 200} +- 错误响应:{success: false, data: null, message: "...", code: 400} + +#### 选项3:简化状态响应 +- 成功响应:{success: true, data: {...}} +- 错误响应:{success: false, message: "..."} + +**决策**:采用选项2(状态明确响应),因为: +- 明确的状态标志便于客户端处理 +- 包含详细信息便于调试 +- 代码字段便于错误分类和处理 +- 与HTTP状态码保持一致 + +### 2. 工具函数实现策略 + +#### 选项1:全局工具函数 +- 实现在 utils.js 中,作为全局工具函数 +- 优点:全局可用,易于重用 +- 缺点:可能与其他工具混杂 + +#### 选项2:专用响应模块 +- 创建专门的 response.js 模块 +- 优点:职责明确,专门用于处理响应 +- 缺点:需要额外的导入 + +#### 选项3:Express中间件 +- 创建响应格式中间件 +- 优点:自动应用到所有响应 +- 缺点:可能不够灵活 + +**决策**:采用选项2(专用响应模块),因为: +- 专门用于响应格式处理 +- 清晰的职责分离 +- 灵活的使用方式 + +### 3. 错误代码设计 + +#### 通用错误代码 +- 1000-1999: 通用错误 +- 2000-2999: 认证相关错误 +- 3000-3999: 参数验证错误 +- 4000-4999: 服务处理错误 +- 5000-5999: 存储相关错误 + +#### 特定错误代码 +- 3001: 缺少必需参数 +- 3002: 参数格式错误 +- 4001: 海报生成失败 +- 4002: PDF生成失败 +- 5001: 文件上传失败 + +### 4. HTTP状态码与响应格式的一致性 + +#### 状态码映射 +- 200: 成功操作 +- 400: 客户端错误(参数验证失败等) +- 401: 认证失败 +- 403: 授权失败 +- 404: 资源未找到 +- 500: 服务器错误 + +### 5. 与现有API的兼容性 + +#### 向后兼容考虑 +- 分析现有客户端使用情况 +- 考虑提供版本控制(如需要) +- 评估直接修改的影响 + +**决策**:采用直接修改方式,因为: +- 这是一个重要的规范改进 +- 可以通过发布说明告知客户端开发者 +- 统一的响应格式对长期维护更有利 + +### 6. 存储模块响应统一 + +#### 当前实现 +- 本地存储:返回 {name, path} +- COS存储:返回 {name, path, data} +- OSS存储:返回 {name, path, data} + +**决策**:统一所有存储模块返回格式为 {name, path},data字段可作为扩展使用但不是必需。 + +### 7. 国际化考虑 + +#### 问题 +- 当前错误消息是中文 +- 需要考虑国际化支持 + +**决策**:当前阶段使用英文错误消息,保留国际化扩展能力: +- 在响应结构中添加 language 字段(可选) +- 错误消息使用英文为主 +- 将来可通过 Accept-Language 头或请求参数确定语言 + +## 实现安全考虑 + +### 1. 错误信息泄露 +- 确保错误响应中不包含敏感系统信息 +- 对于500错误,使用通用错误消息 + +### 2. 响应大小控制 +- 避免在响应中包含过大的数据 +- 对于失败请求,限制错误详情的大小 \ No newline at end of file diff --git a/openspec/changes/archive/2025-11-20-standardize-api-responses/proposal.md b/openspec/changes/archive/2025-11-20-standardize-api-responses/proposal.md new file mode 100644 index 0000000..c9b55c6 --- /dev/null +++ b/openspec/changes/archive/2025-11-20-standardize-api-responses/proposal.md @@ -0,0 +1,17 @@ +# Change: 标准化ERPTurbo_Poster API响应格式 + +## Why +当前ERPTurbo_Poster服务的API响应格式不一致,存在多种响应结构,包括健康检查返回空对象、海报生成返回{url}、上传函数返回{name, path}等。这种不一致性增加了客户端开发的复杂性,降低了API的可预测性和可维护性。 + +## What Changes +- 创建统一的响应格式结构:{success, data, message, code} +- 更新海报生成接口(/api/v1/poster)使用标准响应格式 +- 更新PDF生成接口(/api/v1/pdf)使用标准响应格式 +- 保持健康检查接口(/status)的响应格式兼容性 +- 实现响应格式辅助函数以简化开发 + +## Impact +- Affected specs: response-format (新增) +- Affected code: lib/routes.js, lib/response.js +- **BREAKING**: API响应格式发生变化,需要客户端适配 +- 提升了API的一致性和开发者体验 \ No newline at end of file diff --git a/openspec/changes/archive/2025-11-20-standardize-api-responses/specs/response-format/spec.md b/openspec/changes/archive/2025-11-20-standardize-api-responses/specs/response-format/spec.md new file mode 100644 index 0000000..fa888bf --- /dev/null +++ b/openspec/changes/archive/2025-11-20-standardize-api-responses/specs/response-format/spec.md @@ -0,0 +1,108 @@ +# 规范:API响应格式标准化 + +## ADDED Requirements + +### Requirement: Standard Response Format +- The system SHALL use a consistent response format for all API endpoints +- The response format SHALL include success flag, data, message, and code fields +- The response structure SHALL be: {success: boolean, data: object|null, message: string, code: number} + +#### Scenario: Successful API response +- WHEN an API request is processed successfully +- THEN the response SHALL include success: true +- AND data field SHALL contain the actual response data or null if not applicable +- AND message field SHALL contain a descriptive message about the operation +- AND code field SHALL match the HTTP status code + +#### Scenario: Failed API response +- WHEN an API request fails +- THEN the response SHALL include success: false +- AND data field SHALL be null +- AND message field SHALL contain a descriptive error message +- AND code field SHALL contain an application-specific error code + +### Requirement: Response Utility Functions +- The system SHALL provide utility functions for creating standardized responses +- SHALL include a successResponse(data, message?, code?) function +- SHALL include an errorResponse(message, code?, details?) function + +#### Scenario: Creating successful response +- WHEN successResponse is called with data +- THEN it SHALL return {success: true, data: provided_data, message: provided_message, code: provided_code} +- AND if message is not provided, it SHALL default to "Operation successful" +- AND if code is not provided, it SHALL default to 200 + +#### Scenario: Creating error response +- WHEN errorResponse is called with a message +- THEN it SHALL return {success: false, data: null, message: provided_message, code: provided_code} +- AND if code is not provided, it SHALL default to 400 +- AND if details are provided, they SHALL be included in the response + +### Requirement: Updated Poster Generation Response +- The POST /api/v1/poster endpoint SHALL return standardized responses +- SUCCESS response SHALL have: {success: true, data: {name, path}, message: "Poster generated successfully", code: 200} + +#### Scenario: Successful poster generation +- WHEN a poster is generated successfully +- THEN the response SHALL follow the standard format with success: true +- AND data SHALL contain the generated poster information (name, path) + +#### Scenario: Failed poster generation +- WHEN poster generation fails +- THEN the response SHALL follow the standard format with success: false +- AND appropriate error message and code SHALL be included + +### Requirement: Updated PDF Generation Response +- The POST /api/v1/pdf endpoint SHALL return standardized responses +- SUCCESS response SHALL have: {success: true, data: {name, path}, message: "PDF generated successfully", code: 200} + +#### Scenario: Successful PDF generation +- WHEN a PDF is generated successfully +- THEN the response SHALL follow the standard format with success: true +- AND data SHALL contain the generated PDF information (name, path) + +#### Scenario: Failed PDF generation +- WHEN PDF generation fails +- THEN the response SHALL follow the standard format with success: false +- AND appropriate error message and code SHALL be included + +### Requirement: Updated Health Check Response +- The GET /status SHALL endpoint response MAY be updated to use the standard format +- If updated, SUCCESS response SHALL have: {success: true, data: {}, message: "Service is running", code: 200} + +#### Scenario: Health check successful +- WHEN the health check endpoint is called +- THEN it SHALL return either the original empty object or the standard format + +### Requirement: Consistent Error Handling +- All error responses across the API SHALL follow the standardized format +- System errors SHALL return generic messages to prevent information leakage +- Validation errors SHALL return meaningful messages to aid developers + +#### Scenario: Parameter validation error +- WHEN invalid parameters are sent to an API endpoint +- THEN the system SHALL return a standardized error response +- AND the response SHALL have appropriate error code (e.g., 3001 for missing params, 3002 for format errors) + +#### Scenario: Internal server error +- WHEN an internal server error occurs +- THEN the system SHALL return a standardized error response +- AND the response SHALL have code 5000 and a generic message +- AND system-specific error details SHALL NOT be exposed in the response + +## MODIFIED Requirements + +### Requirement: API Endpoint Responses +- The existing POST /api/v1/poster, POST /api/v1/pdf, and GET /status endpoints MUST be modified to return standardized responses +- The response format SHALL change from current inconsistent formats to the new standard format +- Associated: Response Format Standardization + +#### Scenario: Standardized poster generation +- WHEN requesting POST /api/v1/poster with valid parameters +- THEN the endpoint SHALL return standardized response format +- AND the response SHALL include success flag, data, message, and code + +#### Scenario: Standardized PDF generation +- WHEN requesting POST /api/v1/pdf with valid parameters +- THEN the endpoint SHALL return standardized response format +- AND the response SHALL include success flag, data, message, and code diff --git a/openspec/changes/archive/2025-11-20-standardize-api-responses/tasks.md b/openspec/changes/archive/2025-11-20-standardize-api-responses/tasks.md new file mode 100644 index 0000000..af7eabc --- /dev/null +++ b/openspec/changes/archive/2025-11-20-standardize-api-responses/tasks.md @@ -0,0 +1,39 @@ +# 任务:标准化ERPTurbo_Poster API响应格式 + +## 实施任务列表 + +### 1. 创建响应格式工具函数 +- [x] 创建统一的响应格式工具函数 +- [x] 实现成功响应函数:successResponse(data, message?, code?) +- [x] 实现错误响应函数:errorResponse(message, code?, details?) + +### 2. 更新路由处理函数 +- [x] 修改 posterHandler 函数以使用新的响应格式 +- [x] 修改 pdfHandler 函数以使用新的响应格式 +- [x] 修改 statusHandler 函数以使用新的响应格式 + +### 3. 更新上传处理函数 +- [x] 修改 upload 函数以 return standardized response instead of sending directly +- [x] 确保所有存储后端(本地、COS、OSS)返回一致的格式 + +### 4. 更新API文档 +- [x] 更新 server.mjs 中的 Swagger 文档,反映新的响应格式 +- [x] 更新 lib/routes.js 中的 Swagger 注释 +- [x] 更新 README.md 中的 API 响应示例 + +### 5. 测试验证 +- [x] 测试海报生成接口的响应格式 +- [x] 测试PDF生成接口的响应格式 +- [x] 测试错误情况下的响应格式 +- [x] 测试认证失败时的响应格式 +- [x] 确保向后兼容性(如需要) + +### 6. 部署注意事项 +- [x] 更新 API 文档,告知客户端开发者响应格式变化 +- [x] 考虑提供版本控制或临时兼容层(如需要) + +### 7. 验证和验收 +- [x] 验证所有API端点都返回一致的响应格式 +- [x] 确保错误响应包含适当的错误代码和消息 +- [x] 确保成功响应包含适当的响应数据 +- [x] 运行所有测试验证功能正常 \ No newline at end of file diff --git a/openspec/project.md b/openspec/project.md new file mode 100644 index 0000000..2cf6524 --- /dev/null +++ b/openspec/project.md @@ -0,0 +1,191 @@ +# 项目上下文 + +## 项目目的 +ERPTurbo_Poster 是一个基于 Web 的服务,可以从网页内容生成海报和 PDF。该服务使用 Puppeteer 渲染网页并将其转换为图像(海报)或 PDF。它设计用于处理 HTML 内容转换并支持适当的字体嵌入,还支持多种存储后端,包括本地存储、腾讯云 COS 和阿里云 OSS。该服务特别适用于需要将网页内容转换为视觉呈现形式的场景,如营销海报生成、报告PDF生成、电子书制作、网页存档等。 + +**核心价值**: +- 将动态网页内容转换为静态视觉格式 +- 支持批量文档生成和处理 +- 提供RESTful API接口,易于集成 +- 多云存储支持,灵活的部署选项 + +## 技术栈 +- **运行时环境**: Node.js (v20+) +- **Web 框架**: Express.js (v5.0.0) +- **浏览器自动化**: Puppeteer v24.29.1(用于网页渲染和截图) +- **云存储 SDK**: + - ali-oss v6.17.1 (阿里云对象存储) + - cos-nodejs-sdk-v5 v2.8.3 (腾讯云对象存储) +- **开发工具**: + - dotenv v8.2.0 (环境变量管理) + - body-parser v2.2.0 (请求体解析,支持10MB限制) + - node-fetch v3.3.2 (HTTP 请求) + - swagger-jsdoc v6.2.8 & swagger-ui-express v5.0.1 (API 文档) + - dompurify v3.3.0 (HTML清理,防止XSS攻击) + - jsdom v27.2.0 (DOM操作,用于HTML处理和清理) +- **模块系统**: ES6+ JavaScript 模块 (ESM),使用 .mjs 和 .js 扩展名 + +## 项目约定 + +### 代码风格 +- **模块系统**: ES6+ JavaScript 模块(文件扩展名为 .mjs 和 .js) +- **变量声明**: 尽可能使用 const 进行变量声明,需要可变时使用 let +- **函数定义**: 使用函数声明定义路由处理程序和工具函数 +- **异步操作**: 统一使用 async/await 模式 +- **错误处理**: 在 Puppeteer 操作周围使用 try/catch 块进行错误处理,并在 finally 块中进行资源清理 +- **命名约定**: + - 类名使用 PascalCase(例如,BrowserManager、StorageManager) + - 函数和变量名使用 camelCase +- **文档**: 为函数使用 JSDoc 风格的注释,包含参数文档 +- **代码安全**: 添加安全头部(X-Content-Type-Options、X-Frame-Options、X-XSS-Protection)和内容类型验证 +- **代码结构**: 每个模块导出具体的函数或类,通过 import/export 进行模块化管理 + +### 架构模式 +- **模块化架构,关注点分离**: + - `server.mjs`:主要应用程序入口点和 Express 服务器设置,整合各模块 + - `lib/`:包含可重用模块 + - `browser.js`:Puppeteer 浏览器管理,包含页面池和浏览器生命周期管理 + - `storage.js`:多提供商存储抽象(本地/COS/OSS),提供统一的存储接口 + - `routes.js`:海报/PDF 生成的 API 路由处理程序,包含业务逻辑 + - `constants.js`:应用程序常量和配置,如字体样式定义 + - `params.js`:请求参数处理和验证,生成截图参数 + - `utils.js`:用于字符串操作和 HTML 转换的工具函数 + - `auth.js`:API认证中间件,支持多种认证方式(Bearer Token、API Key Header、Query Parameter) + - `response.js`:标准化API响应格式,包含错误处理和成功响应结构 + - `swagger.js`:Swagger API文档配置和注释定义 +- **单例模式**: BrowserManager 和 StorageManager 实例使用单例模式 +- **资源管理**: 请求范围的资源管理(在处理程序中获取页面,在 finally 块中返回到池) +- **API 文档**: 使用 Swagger 注解进行 API 文档生成,定义了 POST /api/v1/poster 和 POST /api/v1/pdf 等接口 +- **安全设计**: + - API密钥认证机制,支持Bearer Token和X-API-Key Header两种方式 + - DOMPurify集成用于HTML内容清理,防止XSS攻击 + - 请求大小限制(10MB)和内容类型验证 + - 安全HTTP头部配置(CSP、XSS保护等) +- **响应标准化**: 统一的API响应格式,包含success、data、message、code字段 + +### 测试策略 +- **单元测试**: [当前未实现 - 建议添加 Jest 或 Mocha 测试框架] +- **集成测试**: [当前未实现 - 建议添加 Supertest 进行 API 接口测试] +- **端到端测试**: [当前未实现 - 建议添加 Puppeteer 集成测试] +- **安全测试**: [当前未实现 - 建议添加 OWASP ZAP 或类似工具进行安全扫描] +- **性能测试**: [当前未实现 - 建议添加 Artillery 或 K6 进行负载测试] +- **手动测试**: 在开发环境进行功能验证,通过 Swagger UI 进行接口测试 + +### Git 工作流 +- **分支策略**: 使用功能分支模型,主分支为 main,开发在 feature 分支进行 +- **提交信息**: 使用约定式提交格式 (e.g., feat: add pdf generation, fix: resolve memory leak) +- **代码审查**: 通过 Pull Request 进行代码审查和合并 +- **版本管理**: 使用语义化版本控制 (Semantic Versioning) +- **发布流程**: 通过 Git 标签管理版本发布,支持自动化部署 + +## 领域上下文 +- **核心功能**: + - 从网页 URL 或直接 HTML 内容生成海报图像和 PDF 文档 + - 支持自定义参数:宽度、高度、设备缩放、图片质量、编码格式等 + - 支持 JSON 格式输入转 HTML(通过 jsonToHtml 工具函数) + - 提供健康检查接口 /status 和 API 文档接口 /api-docs + - 多格式输出:PNG、JPEG图片格式,A4标准PDF格式 + - 批量处理能力:支持通过API调用进行批量文档生成 +- **内容转换类型**: + - 网页截图(海报),可自定义视口尺寸和设备缩放 + - 从网页或直接 HTML 内容生成 PDF(A4 格式) +- **字体渲染**: 包含通过嵌入的 font-face 声明对中文字体的特殊处理,确保中文字符的正确显示,支持PingFang字体家族 +- **资源管理**: 浏览器页面使用池化管理以高效处理并发请求,避免浏览器实例过多导致的资源消耗,支持动态调整池大小 +- **存储策略**: 支持多提供商存储抽象,允许在本地、腾讯云 COS 或阿里云 OSS 存储之间切换,通过环境变量灵活配置 +- **API安全**: 完整的API密钥认证体系,支持多种密钥配置和临时禁用认证的开发模式 + +## 重要约束 +- **Puppeteer 环境**: 在部署环境中需要安装 Chrome/Chromium 浏览器,或使用系统已安装的 Chrome +- **并发限制**: 并发性受池中可用浏览器页面数量的限制,可以通过调整 browser.js 中的 initBrowser 参数来改变页面数 +- **存储配置**: 存储配置必须通过环境变量正确设置,否则可能导致上传失败 +- **内存管理**: 需要定期清理浏览器实例以防止内存泄漏,browser.js 包含了自动清理机制 +- **大文件处理**: 大文件上传可能需要调整 Express body parser 限制(当前设置为 10MB) +- **中文字体支持**: 中文字体渲染需要特定的 font-face 样式注入,以确保在不同环境中正确显示 +- **安全性**: 为防止请求伪造和 XSS 攻击,添加了安全头、内容类型验证和DOMPurify HTML清理 +- **API认证**: 生产环境必须配置API密钥,开发环境可通过DISABLE_API_AUTH=true临时禁用认证 +- **字体依赖**: 依赖外部字体CDN(阿里云图标字体),需要确保网络连接正常 +- **端口配置**: 默认端口3000,可通过PORT环境变量自定义 + +## 外部依赖 +- **云服务**: + - 腾讯云对象存储(COS)- 通过 cos-nodejs-sdk-v5 访问 + - 阿里云对象存储服务(OSS)- 通过 ali-oss 访问 +- **浏览器引擎**: Chrome/Chromium 浏览器用于 Puppeteer 渲染 +- **字体服务**: 阿里云图标字体CDN(at.alicdn.com)用于中文字体支持 +- **包管理**: NPM registry 用于包管理,使用pnpm作为包管理器 +- **配置管理**: 环境变量用于服务配置(端口、存储凭据、API密钥等) +- **文档生成**: Swagger UI 用于 API 文档展示,访问路径 /api-docs +- **Docker**: 支持通过 Docker 进行容器化部署 +- **运行时依赖**: Node.js v20+ 运行环境 + +## API 接口概览 + +### 核心端点 +- **GET /status**: 健康检查接口,返回服务运行状态 +- **GET /api-docs**: Swagger API 文档界面 +- **POST /api/v1/poster**: 生成海报图片接口 +- **POST /api/v1/pdf**: 生成PDF文档接口 + +### 认证方式 +- **Bearer Token**: `Authorization: Bearer ` +- **API Key Header**: `X-API-Key: ` +- **Query Parameter**: `?api_key=` + +### 响应格式 +```json +{ + "success": boolean, + "data": object|null, + "message": string, + "code": number +} +``` + +## 部署架构 + +### 推荐部署环境 +- **开发环境**: 本地Node.js运行,使用本地存储 +- **测试环境**: Docker容器化部署,模拟生产配置 +- **生产环境**: 云服务器部署,使用COS/OSS云存储 + +### 容器化支持 +- 支持Docker多阶段构建 +- 包含Chrome/Chromium浏览器镜像 +- 环境变量配置外部化 +- 健康检查和优雅关闭 + +### 监控和日志 +- 应用级日志输出到stdout +- 支持结构化日志格式 +- 错误追踪和性能监控 +- 资源使用情况监控 + +## 性能指标 + +### 关键性能指标 +- **响应时间**: 海报生成通常3-10秒,PDF生成5-15秒 +- **并发处理**: 受浏览器页面池大小限制(默认2个页面) +- **内存使用**: 每个Chrome实例约100-200MB内存 +- **存储吞吐**: 依赖云存储服务商的网络性能 + +### 优化建议 +- 根据服务器规格调整浏览器页面池大小 +- 使用CDN加速字体和静态资源加载 +- 定期清理临时文件和浏览器缓存 +- 监控内存使用并设置合理的重启策略 + +## 安全考虑 + +### 已实现的安全措施 +- API密钥认证机制 +- DOMPurify HTML内容清理 +- 请求大小限制(10MB) +- 安全HTTP头部配置 +- 输入参数验证和清理 + +### 安全最佳实践 +- 定期轮换API密钥 +- 使用HTTPS传输 +- 限制API调用频率 +- 监控异常请求模式 +- 定期更新依赖包版本 diff --git a/package.json b/package.json index 9ee8049..5ede10b 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,20 @@ "dependencies": { "ali-oss": "^6.17.1", "body-parser": "^2.2.0", + "cors": "^2.8.5", "cos-nodejs-sdk-v5": "^2.8.3", + "dompurify": "^3.3.0", "dotenv": "^8.2.0", "express": "5.0.0", + "jsdom": "^27.2.0", "node-fetch": "^3.3.2", - "puppeteer": "24.29.1" + "puppeteer": "24.29.1", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" }, "scripts": { - "dev": "node server.mjs" + "dev": "node server.mjs", + "docs": "node lib/swagger.js" }, "author": "", "license": "ISC" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a89d457..cfa6819 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,24 +11,99 @@ dependencies: body-parser: specifier: ^2.2.0 version: 2.2.0 + cors: + specifier: ^2.8.5 + version: 2.8.5 cos-nodejs-sdk-v5: specifier: ^2.8.3 version: 2.15.4 + dompurify: + specifier: ^3.3.0 + version: 3.3.0 dotenv: specifier: ^8.2.0 version: 8.6.0 express: specifier: 5.0.0 version: 5.0.0 + jsdom: + specifier: ^27.2.0 + version: 27.2.0 node-fetch: specifier: ^3.3.2 version: 3.3.2 puppeteer: specifier: 24.29.1 version: 24.29.1 + swagger-jsdoc: + specifier: ^6.2.8 + version: 6.2.8(openapi-types@12.1.3) + swagger-ui-express: + specifier: ^5.0.1 + version: 5.0.1(express@5.0.0) packages: + /@acemir/cssom@0.9.23: + resolution: {integrity: sha512-2kJ1HxBKzPLbmhZpxBiTZggjtgCwKg1ma5RHShxvd6zgqhDEdEkzpiwe7jLkI2p2BrZvFCXIihdoMkl1H39VnA==} + dev: false + + /@apidevtools/json-schema-ref-parser@9.1.2: + resolution: {integrity: sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==} + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + call-me-maybe: 1.0.2 + js-yaml: 4.1.0 + dev: false + + /@apidevtools/openapi-schemas@2.1.0: + resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==} + engines: {node: '>=10'} + dev: false + + /@apidevtools/swagger-methods@3.0.2: + resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} + dev: false + + /@apidevtools/swagger-parser@10.0.3(openapi-types@12.1.3): + resolution: {integrity: sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==} + peerDependencies: + openapi-types: '>=7' + dependencies: + '@apidevtools/json-schema-ref-parser': 9.1.2 + '@apidevtools/openapi-schemas': 2.1.0 + '@apidevtools/swagger-methods': 3.0.2 + '@jsdevtools/ono': 7.1.3 + call-me-maybe: 1.0.2 + openapi-types: 12.1.3 + z-schema: 5.0.5 + dev: false + + /@asamuzakjp/css-color@4.0.5: + resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==} + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5)(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5)(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 11.2.2 + dev: false + + /@asamuzakjp/dom-selector@6.7.4: + resolution: {integrity: sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==} + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.2 + dev: false + + /@asamuzakjp/nwsapi@2.3.9: + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + dev: false + /@babel/code-frame@7.27.1: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -43,6 +118,58 @@ packages: engines: {node: '>=6.9.0'} dev: false + /@csstools/color-helpers@5.1.0: + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + dev: false + + /@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5)(@csstools/css-tokenizer@3.0.4): + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + dev: false + + /@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5)(@csstools/css-tokenizer@3.0.4): + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5)(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + dev: false + + /@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4): + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + dependencies: + '@csstools/css-tokenizer': 3.0.4 + dev: false + + /@csstools/css-syntax-patches-for-csstree@1.0.16: + resolution: {integrity: sha512-2SpS4/UaWQaGpBINyG5ZuCHnUDeVByOhvbkARwfmnfxDvTaj80yOI1cD8Tw93ICV5Fx4fnyDKWQZI1CDtcWyUg==} + engines: {node: '>=18'} + dev: false + + /@csstools/css-tokenizer@3.0.4: + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + dev: false + + /@jsdevtools/ono@7.1.3: + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + dev: false + /@puppeteer/browsers@2.10.13: resolution: {integrity: sha512-a9Ruw3j3qlnB5a/zHRTkruppynxqaeE4H9WNj5eYGRWqw0ZauZ23f4W2ARf3hghF5doozyD+CRtt7XSYuYRI/Q==} engines: {node: '>=18'} @@ -62,10 +189,19 @@ packages: - supports-color dev: false + /@scarf/scarf@1.4.0: + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + requiresBuild: true + dev: false + /@tootallnate/quickjs-emscripten@0.23.0: resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} dev: false + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: false + /@types/node@24.10.0: resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==} requiresBuild: true @@ -74,6 +210,12 @@ packages: dev: false optional: true + /@types/trusted-types@2.0.7: + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + requiresBuild: true + dev: false + optional: true + /@types/yauzl@2.10.3: resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} requiresBuild: true @@ -234,6 +376,10 @@ packages: optional: true dev: false + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: false + /bare-events@2.8.2: resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} peerDependencies: @@ -318,6 +464,12 @@ packages: tweetnacl: 0.14.5 dev: false + /bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + dependencies: + require-from-string: 2.0.2 + dev: false + /body-parser@2.2.0: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} engines: {node: '>=18'} @@ -339,6 +491,13 @@ packages: resolution: {integrity: sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==} dev: false + /brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: false + /buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} dev: false @@ -368,6 +527,10 @@ packages: get-intrinsic: 1.3.0 dev: false + /call-me-maybe@1.0.2: + resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + dev: false + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -414,6 +577,22 @@ packages: delayed-stream: 1.0.0 dev: false + /commander@6.2.0: + resolution: {integrity: sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==} + engines: {node: '>= 6'} + dev: false + + /commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + requiresBuild: true + dev: false + optional: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: false + /conf@9.0.2: resolution: {integrity: sha512-rLSiilO85qHgaTBIIHQpsv8z+NnVfZq3cKuYNCXN1AOqPzced0GWZEe/A517VldRLyQYXUMyV+vszavE2jSAqw==} engines: {node: '>=10'} @@ -465,6 +644,14 @@ packages: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} dev: false + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + dev: false + /cos-nodejs-sdk-v5@2.15.4: resolution: {integrity: sha512-TP/iYTvKKKhRK89on9SRfSMGEw/9SFAAU8EC1kdT5Fmpx7dAwaCNM2+R2H1TSYoQt+03rwOs8QEfNkX8GOHjHQ==} engines: {node: '>= 6'} @@ -490,6 +677,23 @@ packages: parse-json: 5.2.0 dev: false + /css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + dev: false + + /cssstyle@5.3.3: + resolution: {integrity: sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==} + engines: {node: '>=20'} + dependencies: + '@asamuzakjp/css-color': 4.0.5 + '@csstools/css-syntax-patches-for-csstree': 1.0.16 + css-tree: 3.1.0 + dev: false + /dashdash@1.14.1: resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} engines: {node: '>=0.10'} @@ -507,6 +711,14 @@ packages: engines: {node: '>= 14'} dev: false + /data-urls@6.0.0: + resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} + engines: {node: '>=20'} + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + dev: false + /dateformat@2.2.0: resolution: {integrity: sha512-GODcnWq3YGoTnygPfi02ygEiRxqUxpJwuRHjdhJYuxpcZmDq4rjBiXYmbCCzStxo176ixfLT6i4NPwQooRySnw==} dev: false @@ -542,6 +754,10 @@ packages: ms: 2.1.3 dev: false + /decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + dev: false + /default-user-agent@1.0.0: resolution: {integrity: sha512-bDF7bg6OSNcSwFWPu4zYKpVkJZQYVrAANMYB8bc9Szem1D0yKdm4sa/rOCs2aC9+2GMqQ7KnwtZRvDhmLF0dXw==} engines: {node: '>= 0.10.0'} @@ -582,6 +798,19 @@ packages: engines: {node: '>= 8.0.0'} dev: false + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: false + + /dompurify@3.3.0: + resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==} + optionalDependencies: + '@types/trusted-types': 2.0.7 + dev: false + /dot-prop@6.0.1: resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} engines: {node: '>=10'} @@ -634,6 +863,11 @@ packages: engines: {node: '>= 0.11.14'} dev: false + /entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + dev: false + /env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -875,6 +1109,10 @@ packages: engines: {node: '>= 0.8'} dev: false + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: false + /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} dev: false @@ -936,6 +1174,18 @@ packages: assert-plus: 1.0.0 dev: false + /glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + deprecated: Glob versions prior to v9 are no longer supported + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: false + /gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -967,6 +1217,13 @@ packages: function-bind: 1.1.2 dev: false + /html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + dependencies: + whatwg-encoding: 3.1.1 + dev: false + /http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -1035,6 +1292,14 @@ packages: resolve-from: 4.0.0 dev: false + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: false + /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: false @@ -1072,6 +1337,10 @@ packages: engines: {node: '>=8'} dev: false + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: false + /is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} dev: false @@ -1115,6 +1384,41 @@ packages: resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} dev: false + /jsdom@27.2.0: + resolution: {integrity: sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + '@acemir/cssom': 0.9.23 + '@asamuzakjp/dom-selector': 6.7.4 + cssstyle: 5.3.3 + data-urls: 6.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} dev: false @@ -1165,10 +1469,29 @@ packages: path-exists: 3.0.0 dev: false + /lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + dev: false + + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + dev: false + + /lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + dev: false + /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} dev: false + /lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + engines: {node: 20 || >=22} + dev: false + /lru-cache@7.18.3: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} @@ -1186,6 +1509,10 @@ packages: engines: {node: '>= 0.4'} dev: false + /mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + dev: false + /media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -1245,6 +1572,12 @@ packages: engines: {node: '>=8'} dev: false + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.12 + dev: false + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: false @@ -1340,6 +1673,10 @@ packages: mimic-fn: 2.1.0 dev: false + /openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + dev: false + /os-name@1.0.3: resolution: {integrity: sha512-f5estLO2KN8vgtTRaILIgEGBoBrMnZ3JQ7W9TMZCnOIGwHe8TRGSpcagnWDo+Dfhd/z08k9Xe75hvciJJ8Qaew==} engines: {node: '>=0.10.0'} @@ -1417,6 +1754,12 @@ packages: lines-and-columns: 1.2.4 dev: false + /parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + dependencies: + entities: 6.0.1 + dev: false + /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -1427,6 +1770,11 @@ packages: engines: {node: '>=4'} dev: false + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: false + /path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} dev: false @@ -1674,6 +2022,13 @@ packages: resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} dev: false + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: false + /sdk-base@2.0.1: resolution: {integrity: sha512-eeG26wRwhtwYuKGCDM3LixCaxY27Pa/5lK4rLKhQa7HBjJ3U3Y+f81MMZQRsDw/8SC2Dao/83yJTXJ8aULuN8Q==} dependencies: @@ -1795,6 +2150,11 @@ packages: smart-buffer: 4.2.0 dev: false + /source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + dev: false + /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -1880,6 +2240,50 @@ packages: resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} dev: false + /swagger-jsdoc@6.2.8(openapi-types@12.1.3): + resolution: {integrity: sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + commander: 6.2.0 + doctrine: 3.0.0 + glob: 7.1.6 + lodash.mergewith: 4.6.2 + swagger-parser: 10.0.3(openapi-types@12.1.3) + yaml: 2.0.0-1 + transitivePeerDependencies: + - openapi-types + dev: false + + /swagger-parser@10.0.3(openapi-types@12.1.3): + resolution: {integrity: sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==} + engines: {node: '>=10'} + dependencies: + '@apidevtools/swagger-parser': 10.0.3(openapi-types@12.1.3) + transitivePeerDependencies: + - openapi-types + dev: false + + /swagger-ui-dist@5.30.2: + resolution: {integrity: sha512-HWCg1DTNE/Nmapt+0m2EPXFwNKNeKK4PwMjkwveN/zn1cV2Kxi9SURd+m0SpdcSgWEK/O64sf8bzXdtUhigtHA==} + dependencies: + '@scarf/scarf': 1.4.0 + dev: false + + /swagger-ui-express@5.0.1(express@5.0.0): + resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==} + engines: {node: '>= v0.10.32'} + peerDependencies: + express: '>=4.0.0 || >=5.0.0-beta' + dependencies: + express: 5.0.0 + swagger-ui-dist: 5.30.2 + dev: false + + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: false + /tar-fs@3.1.1: resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==} dependencies: @@ -1930,6 +2334,17 @@ packages: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: false + /tldts-core@7.0.17: + resolution: {integrity: sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==} + dev: false + + /tldts@7.0.17: + resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==} + hasBin: true + dependencies: + tldts-core: 7.0.17 + dev: false + /to-arraybuffer@1.0.1: resolution: {integrity: sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==} dev: false @@ -1947,6 +2362,20 @@ packages: punycode: 2.3.1 dev: false + /tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + dependencies: + tldts: 7.0.17 + dev: false + + /tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + dependencies: + punycode: 2.3.1 + dev: false + /tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} dev: false @@ -2047,6 +2476,11 @@ packages: hasBin: true dev: false + /validator@13.15.23: + resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==} + engines: {node: '>= 0.10'} + dev: false + /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -2061,6 +2495,13 @@ packages: extsprintf: 1.3.0 dev: false + /w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + dependencies: + xml-name-validator: 5.0.0 + dev: false + /web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -2070,6 +2511,31 @@ packages: resolution: {integrity: sha512-21Yi2GhGntMc671vNBCjiAeEVknXjVRoyu+k+9xOMShu+ZQfpGQwnBqbNz/Sv4GXZ6JmutlPAi2nIJcrymAWuQ==} dev: false + /webidl-conversions@8.0.0: + resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} + engines: {node: '>=20'} + dev: false + + /whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + dependencies: + iconv-lite: 0.6.3 + dev: false + + /whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + dev: false + + /whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.0 + dev: false + /win-release@1.1.1: resolution: {integrity: sha512-iCRnKVvGxOQdsKhcQId2PXV1vV3J/sDPXKA4Oe9+Eti2nb2ESEsYHRYls/UjoUW3bIc5ZDO8dTH50A/5iVN+bw==} engines: {node: '>=0.10.0'} @@ -2103,6 +2569,11 @@ packages: optional: true dev: false + /xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + dev: false + /xml2js@0.6.2: resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} engines: {node: '>=4.0.0'} @@ -2116,6 +2587,10 @@ packages: engines: {node: '>=4.0'} dev: false + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: false + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -2126,6 +2601,11 @@ packages: engines: {node: '>=10'} dev: false + /yaml@2.0.0-1: + resolution: {integrity: sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==} + engines: {node: '>= 6'} + dev: false + /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -2151,6 +2631,18 @@ packages: fd-slicer: 1.1.0 dev: false + /z-schema@5.0.5: + resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} + engines: {node: '>=8.0.0'} + hasBin: true + dependencies: + lodash.get: 4.4.2 + lodash.isequal: 4.5.0 + validator: 13.15.23 + optionalDependencies: + commander: 9.5.0 + dev: false + /zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} dev: false diff --git a/server.mjs b/server.mjs index 538f3eb..59cb5a0 100644 --- a/server.mjs +++ b/server.mjs @@ -1,12 +1,75 @@ import express from 'express'; import bodyParser from 'body-parser'; import dotenv from 'dotenv'; +import swaggerUi from 'swagger-ui-express'; +import specs from './lib/swagger.js'; +import cors from 'cors'; + // 配置 dotenv dotenv.config(); +// 处理API前缀配置 +function getApiPrefix() { + let prefix; + if ('API_PREFIX' in process.env) { + prefix = process.env.API_PREFIX; + } else { + prefix = '/api/v1'; + } + + // 如果明确设置为空字符串,则返回空 + if (prefix === '') { + return ''; + } + + // 标准化前缀格式:确保以斜杠开头且不以斜杠结尾 + if (prefix && !prefix.startsWith('/')) { + prefix = '/' + prefix; + } + if (prefix && prefix.length > 1 && prefix.endsWith('/')) { + prefix = prefix.slice(0, -1); + } + + return prefix; +} + +// 处理CORS配置 +function getCorsConfig() { + const enableCors = process.env.ENABLE_CORS === 'true'; + + if (!enableCors) { + return null; // 不启用CORS + } + + const corsOrigins = process.env.CORS_ORIGINS; + let origins = ['http://localhost:3000']; // 默认安全配置 + + if (corsOrigins) { + try { + // 支持多个源,用逗号分隔 + origins = corsOrigins.split(',').map(origin => origin.trim()).filter(origin => origin); + } catch (error) { + console.error('CORS配置解析错误,使用默认配置:', error.message); + } + } + + return { + origin: origins, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'], + credentials: true, + optionsSuccessStatus: 200 // 支持老版本浏览器 + }; +} + +const apiPrefix = getApiPrefix(); +const corsConfig = getCorsConfig(); + import BrowserManager from './lib/browser.js'; import StorageManager from './lib/storage.js'; import {pdfHandler, posterHandler, statusHandler} from './lib/routes.js'; +import apiKeyAuth from './lib/auth.js'; +import {validationErrorResponse} from './lib/response.js'; // 初始化存储管理器 const storageManager = new StorageManager(process.env); @@ -16,9 +79,55 @@ const browserManager = new BrowserManager(); await browserManager.initBrowser(2); const app = express(); + +// 应用CORS中间件(如果启用) +if (corsConfig) { + app.use(cors(corsConfig)); + console.log('✅ CORS已启用,允许的源:', corsConfig.origin); +} + app.use(bodyParser.json({limit: '10mb'})); + +// 配置Swagger UI +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs)); + // 提供上传文件的静态访问 app.use('/uploads', express.static('uploads')); +/** + * @swagger + * /status: + * get: + * summary: 健康检查 + * description: 检查服务是否正常运行 + * tags: [Health] + * responses: + * 200: + * description: 服务正常运行 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * description: 请求是否成功 + * data: + * type: object + * example: {} + * description: 响应数据 + * message: + * type: string + * example: "Service is running" + * description: 响应消息 + * code: + * type: number + * example: 200 + * description: 响应代码 + * 500: + * description: 服务异常 + */ + // 健康检查接口 app.get('/status', statusHandler); @@ -31,10 +140,10 @@ app.use((req, res, next) => { }); // 海报生成接口 -app.post('/api/v1/poster', async function (req, res) { +app.post(`${apiPrefix}/poster`, apiKeyAuth, async function (req, res) { // 基本的安全检查 if (!req.headers['content-type'] || !req.headers['content-type'].includes('application/json')) { - return res.status(400).json({error: 'Content-Type must be application/json'}); + return res.status(400).json(validationErrorResponse('Content-Type must be application/json', 3002)); } await posterHandler( @@ -46,10 +155,10 @@ app.post('/api/v1/poster', async function (req, res) { }); // PDF生成接口 -app.post('/api/v1/pdf', async function (req, res) { +app.post(`${apiPrefix}/pdf`, apiKeyAuth, async function (req, res) { // 基本的安全检查 if (!req.headers['content-type'] || !req.headers['content-type'].includes('application/json')) { - return res.status(400).json({error: 'Content-Type must be application/json'}); + return res.status(400).json(validationErrorResponse('Content-Type must be application/json', 3002)); } await pdfHandler( @@ -61,4 +170,18 @@ app.post('/api/v1/pdf', async function (req, res) { }); const port = process.env.PORT || 3000; -app.listen(port, () => console.log(`Puppeteer app listening on port ${port}!`)); + +// 服务器启动时显示配置信息 +console.log('🚀 服务器配置信息:'); +console.log(` 端口: ${port}`); +console.log(` API前缀: ${apiPrefix}`); +console.log(` CORS状态: ${corsConfig ? '已启用' : '已禁用'}`); +if (corsConfig) { + console.log(` 允许的源: ${corsConfig.origin.join(', ')}`); +} +console.log(' 健康检查: /status'); +console.log(` 海报API: ${apiPrefix}/poster`); +console.log(` PDF API: ${apiPrefix}/pdf`); +console.log(' API文档: /api-docs'); + +app.listen(port, () => console.log(`\n✨ Puppeteer app listening on port ${port}!`)); diff --git a/swagger.json b/swagger.json new file mode 100644 index 0000000..05b4e15 --- /dev/null +++ b/swagger.json @@ -0,0 +1 @@ +{"openapi": "3.0.0", "info": {"title": "ERPTurbo_Poster API", "version": "1.0.0", "description": "海报和PDF生成服务API文档"}, "servers": [{"url": "http://localhost:3000", "description": "开发服务器"}], "paths": {"/status": {"get": {"summary": "健康检查", "description": "检查服务是否正常运行", "tags": ["Health"], "responses": {"200": {"description": "服务正常运行", "content": {"application/json": {"schema": {"type": "object", "properties": {"success": {"type": "boolean", "example": true, "description": "请求是否成功"}, "data": {"type": "object", "example": {}, "description": "响应数据"}, "message": {"type": "string", "example": "Service is running", "description": "响应消息"}, "code": {"type": "number", "example": 200, "description": "响应代码"}}}}}}, "500": {"description": "服务异常"}}}}, "/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": "

Hello World

"}, "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": "服务器内部错误"}}}}, "/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": "

Hello World

"}, "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": "服务器内部错误"}}}}}, "components": {}, "tags": []} diff --git a/tests/test-standardized-response.js b/tests/test-standardized-response.js new file mode 100644 index 0000000..aafce11 --- /dev/null +++ b/tests/test-standardized-response.js @@ -0,0 +1,41 @@ +/** + * Simple test to validate API response standardization + * This test checks if the modules can be imported without errors + */ +import {successResponse, errorResponse, validationErrorResponse, serverErrorResponse} from './lib/response.js'; +import {posterHandler, pdfHandler, statusHandler} from './lib/routes.js'; +import StorageManager from './lib/storage.js'; +import BrowserManager from './lib/browser.js'; + +console.log("Testing response utility functions..."); + +// Test success response +const successResp = successResponse({test: "data"}, "Test message", 200); +console.log("Success response:", successResp); +console.assert(successResp.success === true, "Success response should have success=true"); +console.assert(successResp.data !== undefined, "Success response should have data"); +console.assert(successResp.message === "Test message", "Success response should have correct message"); +console.assert(successResp.code === 200, "Success response should have correct code"); + +// Test error response +const errorResp = errorResponse("Error message", 400); +console.log("Error response:", errorResp); +console.assert(errorResp.success === false, "Error response should have success=false"); +console.assert(errorResp.data === null, "Error response should have data=null"); +console.assert(errorResp.message === "Error message", "Error response should have correct message"); +console.assert(errorResp.code === 400, "Error response should have correct code"); + +// Test validation error response +const validationResp = validationErrorResponse("Validation error", 3001); +console.log("Validation error response:", validationResp); +console.assert(validationResp.code === 3001, "Validation error response should have correct code"); + +// Test server error response +const serverResp = serverErrorResponse("Server error", 5000); +console.log("Server error response:", serverResp); +console.assert(serverResp.code === 5000, "Server error response should have correct code"); + +console.log("All response utility tests passed!"); + +// Test that route handlers exist +console.log("Route handlers exist:", typeof posterHandler, typeof pdfHandler, typeof statusHandler); \ No newline at end of file diff --git a/tests/test-swagger.js b/tests/test-swagger.js new file mode 100644 index 0000000..0bd0272 --- /dev/null +++ b/tests/test-swagger.js @@ -0,0 +1,55 @@ +/** + * 测试Swagger规范生成 + */ +import swaggerJsdoc from 'swagger-jsdoc'; +import fs from 'fs'; + +const options = { + definition: { + openapi: '3.0.0', + info: { + title: 'ERPTurbo_Poster API', + version: '1.0.0', + description: '海报和PDF生成服务API文档', + }, + servers: [ + { + url: 'http://localhost:3000', + description: '开发服务器' + } + ] + }, + apis: ['./server.mjs', './lib/routes.js'], // 包含API注释的文件 +}; + +try { + const specs = swaggerJsdoc(options); + console.log('✅ Swagger规范生成成功!'); + console.log(`📊 发现 ${specs.paths ? Object.keys(specs.paths).length : 0} 个API端点`); + + // 保存生成的规范到文件,以便查看 + fs.writeFileSync('./swagger.json', JSON.stringify(specs, null, 2)); + console.log('📄 Swagger规范已保存到 swagger.json'); + + // 验证关键端点是否存在 + const paths = specs.paths; + if (paths['/api/v1/poster'] && paths['/api/v1/poster'].post) { + console.log('✅ 海报API端点文档已生成'); + } else { + console.log('❌ 海报API端点文档未找到'); + } + + if (paths['/api/v1/pdf'] && paths['/api/v1/pdf'].post) { + console.log('✅ PDF API端点文档已生成'); + } else { + console.log('❌ PDF API端点文档未找到'); + } + + if (paths['/status'] && paths['/status'].get) { + console.log('✅ 健康检查API端点文档已生成'); + } else { + console.log('❌ 健康检查API端点文档未找到'); + } +} catch (error) { + console.error('❌ Swagger规范生成失败:', error.message); +} \ No newline at end of file diff --git a/tests/test_html_poster.js b/tests/test_html_poster.js new file mode 100644 index 0000000..626a362 --- /dev/null +++ b/tests/test_html_poster.js @@ -0,0 +1,36 @@ +import fetch from 'node-fetch'; + +// 测试从HTML内容生成海报 +async function testHtmlPoster() { + const testData = { + html: 'Test Poster

Test HTML Content

This is a test of HTML poster generation.

', + width: 800, + height: 600, + type: 'png', + device: 1 + }; + + try { + console.log('Sending request to generate poster from HTML...'); + const response = await fetch('http://localhost:3000/api/v1/poster', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer abc123' + }, + body: JSON.stringify(testData) + }); + + if (response.ok) { + const result = await response.json(); + console.log('Success:', result); + } else { + console.error('Error:', response.status, await response.text()); + } + } catch (error) { + console.error('Request failed:', error); + } +} + +// 执行测试 +testHtmlPoster();