init commit
This commit is contained in:
commit
534c570866
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
.env
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
dist
|
||||||
24
.env.example
Normal file
24
.env.example
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# 存储配置
|
||||||
|
STORAGE_TYPE=local # 可选值: cos(腾讯云), oss(阿里云), local(本地存储)
|
||||||
|
|
||||||
|
# COS 配置 (腾讯云对象存储)
|
||||||
|
COS_SECRET_ID=
|
||||||
|
COS_SECRET_KEY=
|
||||||
|
COS_BUCKET=
|
||||||
|
COS_REGION=
|
||||||
|
COS_DOMAIN=
|
||||||
|
|
||||||
|
# OSS 配置 (阿里云对象存储)
|
||||||
|
OSS_REGION=
|
||||||
|
OSS_ACCESS_KEY_ID=
|
||||||
|
OSS_ACCESS_KEY_SECRET=
|
||||||
|
OSS_BUCKET=
|
||||||
|
|
||||||
|
# 本地存储配置
|
||||||
|
LOCAL_DOMAIN=http://localhost:3000
|
||||||
|
|
||||||
|
# 上传路径配置
|
||||||
|
UPLOAD_PATH=uploads
|
||||||
|
|
||||||
|
# 服务端口
|
||||||
|
PORT=3000
|
||||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
.cache
|
||||||
|
.idea
|
||||||
|
|
||||||
|
uploads/pdfs/*
|
||||||
|
uploads/posters/*
|
||||||
15
.puppeteerrc.cjs
Normal file
15
.puppeteerrc.cjs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
const { join } = require('path');
|
||||||
|
|
||||||
|
// console.log('puppeteer config: ', process.env);
|
||||||
|
/**
|
||||||
|
* @type {import("puppeteer").Configuration}
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
|
||||||
|
chrome: {
|
||||||
|
downloadBaseUrl: 'https://registry.npmmirror.com/-/binary/chrome-for-testing'
|
||||||
|
},
|
||||||
|
"chrome-headless-shell": {
|
||||||
|
downloadBaseUrl: 'https://registry.npmmirror.com/-/binary/chrome-for-testing'
|
||||||
|
}
|
||||||
|
};
|
||||||
28
Dockerfile
Normal file
28
Dockerfile
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
FROM node:20
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN set -ex \
|
||||||
|
&& sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources \
|
||||||
|
&& apt update \
|
||||||
|
&& apt install -y --no-install-recommends libasound2 libgbm1 libxrandr2 libxfixes3 libxdamage1 libxcomposite1 libxkbcommon0 libcups2 libatk-bridge2.0-0 libdbus-1-3 libnss3 libnspr4 libcairo2 libpango-1.0-0 fonts-noto-cjk fonts-wqy-zenhei fonts-liberation \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& apt clean
|
||||||
|
|
||||||
|
RUN npm config set registry https://registry.npmmirror.com \
|
||||||
|
&& npm install -g pnpm
|
||||||
|
|
||||||
|
COPY ./.puppeteerrc.cjs ./
|
||||||
|
COPY ./package.json ./
|
||||||
|
|
||||||
|
RUN pnpm install \
|
||||||
|
&& pnpm approve-builds
|
||||||
|
|
||||||
|
RUN ./node_modules/.bin/puppeteer browsers install chrome --base-url=https://registry.npmmirror.com/-/binary/chrome-for-testing
|
||||||
|
|
||||||
|
COPY ./lib ./lib
|
||||||
|
COPY ./server.mjs ./
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "server.mjs"]
|
||||||
144
README.md
Normal file
144
README.md
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
# ERPTurbo Poster
|
||||||
|
|
||||||
|
基于 Puppeteer 的海报生成服务,支持多种云存储后端。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── lib
|
||||||
|
│ ├── browser.js # 浏览器管理模块
|
||||||
|
│ ├── storage.js # 存储服务模块
|
||||||
|
│ ├── params.js # 参数处理模块
|
||||||
|
│ ├── utils.js # 工具类模块
|
||||||
|
│ ├── routes.js # 路由处理模块
|
||||||
|
│ └── constants.js # 常量定义模块
|
||||||
|
├── .env # 环境变量配置文件
|
||||||
|
├── server.mjs # 主应用入口
|
||||||
|
└── package.json # 项目依赖配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境变量配置
|
||||||
|
|
||||||
|
项目使用 `.env` 文件配置环境变量,请根据需要修改 `.env` 文件中的配置项:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# 存储配置
|
||||||
|
STORAGE_TYPE=local # 可选值: cos(腾讯云), oss(阿里云), local(本地存储)
|
||||||
|
|
||||||
|
# COS 配置 (腾讯云对象存储)
|
||||||
|
COS_SECRET_ID=AKIDCGvLmwUmQr3RxKACF1XKrGb1FFA1I2D8
|
||||||
|
COS_SECRET_KEY=1TYMagMxIxRFMNQEYcQxEhvyuiEY4mIa
|
||||||
|
COS_BUCKET=qimai-1251581441
|
||||||
|
COS_REGION=ap-shanghai
|
||||||
|
COS_DOMAIN=https://qimai-1251581441.cos.ap-shanghai.myqcloud.com/
|
||||||
|
|
||||||
|
# OSS 配置 (阿里云对象存储)
|
||||||
|
OSS_REGION=oss-cn-hangzhou
|
||||||
|
OSS_ACCESS_KEY_ID=
|
||||||
|
OSS_ACCESS_KEY_SECRET=
|
||||||
|
OSS_BUCKET=
|
||||||
|
|
||||||
|
# 本地存储配置
|
||||||
|
LOCAL_DOMAIN=http://localhost:3000
|
||||||
|
|
||||||
|
# 上传路径配置
|
||||||
|
UPLOAD_PATH=uploads/posters
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接口说明
|
||||||
|
|
||||||
|
### 1. 健康检查接口
|
||||||
|
|
||||||
|
- URL: `/status`
|
||||||
|
- Method: GET
|
||||||
|
- Description: 检查服务运行状态
|
||||||
|
|
||||||
|
### 2. 海报生成接口
|
||||||
|
|
||||||
|
- URL: `/api/v1/poster`
|
||||||
|
- Method: POST
|
||||||
|
- Description: 生成海报并上传到云存储
|
||||||
|
- Request Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"webpage": "需要截图的网页URL",
|
||||||
|
"device": "设备缩放因子",
|
||||||
|
"width": "截图宽度",
|
||||||
|
"height": "截图高度",
|
||||||
|
"type": "图片类型(png/jpeg)",
|
||||||
|
"encoding": "编码方式(binary/base64)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. PDF下载接口
|
||||||
|
|
||||||
|
- URL: `/api/v1/pdf`
|
||||||
|
- Method: POST
|
||||||
|
- Description: 将网页保存为PDF并上传到云存储
|
||||||
|
- Request Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"webpage": "需要生成PDF的网页URL",
|
||||||
|
"device": "设备缩放因子",
|
||||||
|
"width": "页面宽度",
|
||||||
|
"height": "页面高度",
|
||||||
|
"encoding": "编码方式(binary/base64)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 模块说明
|
||||||
|
|
||||||
|
### browser.js
|
||||||
|
|
||||||
|
浏览器管理模块,负责:
|
||||||
|
- 浏览器实例的创建和销毁
|
||||||
|
- 页面资源的管理和复用
|
||||||
|
- 浏览器连接的维护
|
||||||
|
|
||||||
|
### storage.js
|
||||||
|
|
||||||
|
存储服务模块,支持:
|
||||||
|
- 腾讯云COS对象存储
|
||||||
|
- 阿里云OSS对象存储
|
||||||
|
- 本地存储
|
||||||
|
- 统一的上传接口
|
||||||
|
- 自动根据配置选择存储类型
|
||||||
|
|
||||||
|
### params.js
|
||||||
|
|
||||||
|
参数处理模块,负责:
|
||||||
|
- 处理截图参数
|
||||||
|
- 处理上传参数
|
||||||
|
|
||||||
|
### utils.js
|
||||||
|
|
||||||
|
工具类模块,包含:
|
||||||
|
- 随机字符串生成函数
|
||||||
|
|
||||||
|
### routes.js
|
||||||
|
|
||||||
|
路由处理模块,负责:
|
||||||
|
- 处理健康检查接口
|
||||||
|
- 处理海报生成接口
|
||||||
|
- 处理PDF下载接口
|
||||||
|
|
||||||
|
### constants.js
|
||||||
|
|
||||||
|
常量定义模块,包含:
|
||||||
|
- 默认配置常量
|
||||||
|
- 样式定义常量
|
||||||
|
- HTTP状态码常量
|
||||||
|
- 字符串常量
|
||||||
25
docker-compose.yml
Normal file
25
docker-compose.yml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "${PORT:-3000}:${PORT:-3000}"
|
||||||
|
environment:
|
||||||
|
- PORT=${PORT:-3000}
|
||||||
|
- STORAGE_TYPE=${STORAGE_TYPE:-local}
|
||||||
|
- COS_SECRET_ID=${COS_SECRET_ID}
|
||||||
|
- COS_SECRET_KEY=${COS_SECRET_KEY}
|
||||||
|
- COS_BUCKET=${COS_BUCKET}
|
||||||
|
- COS_REGION=${COS_REGION}
|
||||||
|
- COS_DOMAIN=${COS_DOMAIN}
|
||||||
|
- OSS_REGION=${OSS_REGION}
|
||||||
|
- OSS_ACCESS_KEY_ID=${OSS_ACCESS_KEY_ID}
|
||||||
|
- OSS_ACCESS_KEY_SECRET=${OSS_ACCESS_KEY_SECRET}
|
||||||
|
- OSS_BUCKET=${OSS_BUCKET}
|
||||||
|
- LOCAL_DOMAIN=${LOCAL_DOMAIN:-http://localhost:3000}
|
||||||
|
- UPLOAD_PATH=${UPLOAD_PATH:-uploads}
|
||||||
|
volumes:
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
- ./lib:/app/lib
|
||||||
|
restart: unless-stopped
|
||||||
214
lib/browser.js
Normal file
214
lib/browser.js
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import {existsSync} from 'fs';
|
||||||
|
|
||||||
|
const browserArgs = [
|
||||||
|
'--disable-gpu',
|
||||||
|
'--disable-dev-shm-usage',
|
||||||
|
'--disable-setuid-sandbox',
|
||||||
|
'--no-first-run',
|
||||||
|
'--no-sandbox',
|
||||||
|
'--no-zygote',
|
||||||
|
// '--single-process',
|
||||||
|
'--disable-background-networking',
|
||||||
|
'--enable-features=NetworkService,NetworkServiceInProcess',
|
||||||
|
'--disable-background-timer-throttling',
|
||||||
|
'--disable-backgrounding-occluded-windows',
|
||||||
|
'--disable-breakpad',
|
||||||
|
'--disable-client-side-phishing-detection',
|
||||||
|
'--disable-component-extensions-with-background-pages',
|
||||||
|
'--disable-default-apps',
|
||||||
|
'--disable-extensions',
|
||||||
|
'--disable-features=TranslateUI',
|
||||||
|
'--disable-hang-monitor',
|
||||||
|
'--disable-ipc-flooding-protection',
|
||||||
|
'--disable-popup-blocking',
|
||||||
|
'--disable-prompt-on-repost',
|
||||||
|
'--disable-renderer-backgrounding',
|
||||||
|
'--disable-sync',
|
||||||
|
'--force-color-profile=srgb',
|
||||||
|
'--metrics-recording-only',
|
||||||
|
'--no-first-run',
|
||||||
|
'--enable-automation',
|
||||||
|
'--password-store=basic',
|
||||||
|
'--use-mock-keychain',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 尝试查找系统中已安装的 Chrome 或 Chromium 可执行文件路径
|
||||||
|
function getChromeExecutablePath() {
|
||||||
|
// 检查环境变量中是否指定了 Chromium 路径
|
||||||
|
if (process.env.PUPPETEER_EXECUTABLE_PATH) {
|
||||||
|
return process.env.PUPPETEER_EXECUTABLE_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paths = [
|
||||||
|
// Windows
|
||||||
|
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||||
|
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
||||||
|
// macOS
|
||||||
|
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
// Linux
|
||||||
|
'/usr/bin/google-chrome',
|
||||||
|
'/usr/bin/chromium-browser',
|
||||||
|
'/usr/bin/chromium'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const path of paths) {
|
||||||
|
if (existsSync(path)) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BrowserManager {
|
||||||
|
constructor() {
|
||||||
|
this.browserList = [];
|
||||||
|
this.unusedBrowser = [];
|
||||||
|
this.pages = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async createBrowser(max_wse) {
|
||||||
|
// 尝试使用系统已安装的 Chrome/Chromium
|
||||||
|
const executablePath = getChromeExecutablePath();
|
||||||
|
const launchOptions = {
|
||||||
|
headless: false, // 默认使用无头模式
|
||||||
|
args: browserArgs
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果找到了系统 Chrome,则使用它
|
||||||
|
if (executablePath) {
|
||||||
|
console.log('Using system Chrome:', executablePath);
|
||||||
|
launchOptions.executablePath = executablePath;
|
||||||
|
} else {
|
||||||
|
console.log('Using bundled Chromium');
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch(launchOptions);
|
||||||
|
|
||||||
|
let pageCount = 0;
|
||||||
|
|
||||||
|
// 监听到打开了一个新窗口
|
||||||
|
browser.on("targetcreated", (data) => {
|
||||||
|
pageCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.on("targetdestroyed", () => {
|
||||||
|
pageCount--;
|
||||||
|
|
||||||
|
if (pageCount === 0 && this.unusedBrowser.includes(browser.wsEndpoint())) {
|
||||||
|
console.log("命中unusedBrowser");
|
||||||
|
this.delBrowser(browser.wsEndpoint());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// max_wse
|
||||||
|
await browser.newPage();
|
||||||
|
await browser.newPage();
|
||||||
|
await browser.newPage();
|
||||||
|
|
||||||
|
const pages = await browser.pages();
|
||||||
|
for (const page of pages) {
|
||||||
|
this.pages.push(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.browserList.push(browser.wsEndpoint());
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initBrowser(max_wse) {
|
||||||
|
try {
|
||||||
|
await this.createBrowser(max_wse).then(() => {
|
||||||
|
console.log("初始化浏览器成功");
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("初始化浏览器失败:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每隔24小时去创建一个新的Browser实例,并销毁第一个实例
|
||||||
|
setInterval(() => {
|
||||||
|
console.log(this.browserList.join(','));
|
||||||
|
console.log(this.unusedBrowser.join(','));
|
||||||
|
this.createBrowser().then(() => {
|
||||||
|
// 把第一个浏览器置为不可用
|
||||||
|
this.getFirstBrowser().then(browser => {
|
||||||
|
browser.pages().then((pages) => {
|
||||||
|
console.log(`第一个浏览器实例打开的页面数为${pages.length}`);
|
||||||
|
|
||||||
|
// 如果当前没有打开的页面,创建Browser实例时会默认初始化一个页面,所以这里判断的是<=1
|
||||||
|
if (pages.length <= 1) {
|
||||||
|
this.delBrowser(browser.wsEndpoint()).then(() => console.log("关闭浏览器成功"));
|
||||||
|
} else {
|
||||||
|
this.unusedBrowser.push(browser.wsEndpoint());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 24 * 60 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取页面
|
||||||
|
*/
|
||||||
|
getTargetPage() {
|
||||||
|
while (true) {
|
||||||
|
const page = this.pages.pop();
|
||||||
|
if (page !== undefined) {
|
||||||
|
// 检查页面是否仍然有效
|
||||||
|
if (!page.isClosed()) {
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
// 如果页面已关闭,则丢弃并继续获取下一个
|
||||||
|
console.log('发现已关闭的页面,已丢弃');
|
||||||
|
} else {
|
||||||
|
// 如果没有可用页面,则创建新页面
|
||||||
|
console.log('没有可用页面,需要创建新页面');
|
||||||
|
// 这里应该触发创建新浏览器实例的逻辑
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归还页面
|
||||||
|
*/
|
||||||
|
returnTargetPage(target) {
|
||||||
|
// 只有当页面未关闭时才归还
|
||||||
|
if (target && !target.isClosed()) {
|
||||||
|
this.pages.push(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从browserList和unusedBrowser中删除
|
||||||
|
* @param browser
|
||||||
|
*/
|
||||||
|
async delBrowser(browser) {
|
||||||
|
this.browserList = this.browserList.filter((item) => item !== browser);
|
||||||
|
this.unusedBrowser = this.unusedBrowser.filter((item) => item !== browser);
|
||||||
|
|
||||||
|
const browserWSEndpoint = browser;
|
||||||
|
const browserInstance = await puppeteer.connect({browserWSEndpoint});
|
||||||
|
await browserInstance.close();
|
||||||
|
console.log("删除浏览器 %o %o", this.browserList.length, this.unusedBrowser.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 始终返回最后一个浏览器实例
|
||||||
|
*/
|
||||||
|
async getBrowser() {
|
||||||
|
const browserWSEndpoint = this.browserList[this.browserList.length - 1];
|
||||||
|
return puppeteer.connect({browserWSEndpoint});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 始终返回第一个浏览器实例
|
||||||
|
*/
|
||||||
|
async getFirstBrowser() {
|
||||||
|
const browserWSEndpoint = this.browserList[0];
|
||||||
|
return puppeteer.connect({browserWSEndpoint});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BrowserManager;
|
||||||
35
lib/constants.js
Normal file
35
lib/constants.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* 应用常量定义
|
||||||
|
*/
|
||||||
|
// 样式定义
|
||||||
|
const FONT_STYLE = '@font-face {\n' +
|
||||||
|
' font-family: pingfang;\n' +
|
||||||
|
' font-display: swap;\n' +
|
||||||
|
' src: url(\'//at.alicdn.com/t/webfont_iteuzxytem.eot\'); /* IE9*/\n' +
|
||||||
|
' src: url(\'//at.alicdn.com/t/webfont_iteuzxytem.eot?#iefix\') format(\'embedded-opentype\'), /* IE6-IE8 */\n' +
|
||||||
|
' url(\'//at.alicdn.com/t/webfont_iteuzxytem.woff2\') format(\'woff2\'),\n' +
|
||||||
|
' url(\'//at.alicdn.com/t/webfont_iteuzxytem.woff\') format(\'woff\'), /* chrome、firefox */\n' +
|
||||||
|
' url(\'//at.alicdn.com/t/webfont_iteuzxytem.ttf\') format(\'truetype\'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/\n' +
|
||||||
|
' url(\'//at.alicdn.com/t/webfont_iteuzxytem.svg#NotoSansHans-DemiLight\') format(\'svg\'); /* iOS 4.1- */\n' +
|
||||||
|
'}';
|
||||||
|
|
||||||
|
// HTTP状态码
|
||||||
|
const HTTP_STATUS_OK = 200;
|
||||||
|
const HTTP_STATUS_ERROR = 500;
|
||||||
|
|
||||||
|
// 字符串常量
|
||||||
|
const RANDOM_STRING_CHARS = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
|
||||||
|
const RANDOM_STRING_DEFAULT_LENGTH = 32;
|
||||||
|
|
||||||
|
export {
|
||||||
|
// 样式定义
|
||||||
|
FONT_STYLE,
|
||||||
|
|
||||||
|
// HTTP状态码
|
||||||
|
HTTP_STATUS_OK,
|
||||||
|
HTTP_STATUS_ERROR,
|
||||||
|
|
||||||
|
// 字符串常量
|
||||||
|
RANDOM_STRING_CHARS,
|
||||||
|
RANDOM_STRING_DEFAULT_LENGTH
|
||||||
|
};
|
||||||
76
lib/params.js
Normal file
76
lib/params.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* 获取截图参数
|
||||||
|
* @param req 请求对象
|
||||||
|
* @param filename 文件名
|
||||||
|
* @param upload_path 上传路径
|
||||||
|
* @returns 截图参数
|
||||||
|
*/
|
||||||
|
function getParam(req, filename, upload_path) {
|
||||||
|
const clip = req.body.clip;
|
||||||
|
const quality = Math.min(Math.max(req.body.quality || 100, 0), 100); // 限制质量在0-100之间
|
||||||
|
const omit_background = req.body.omit_background || true;
|
||||||
|
const encoding = req.body.encoding || 'binary';
|
||||||
|
const type = req.body.type || 'png';
|
||||||
|
|
||||||
|
// 验证type参数
|
||||||
|
if (!['png', 'jpeg'].includes(type)) {
|
||||||
|
throw new Error('Invalid image type. Supported types: png, jpeg');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证encoding参数
|
||||||
|
if (!['binary', 'base64'].includes(encoding)) {
|
||||||
|
throw new Error('Invalid encoding. Supported encodings: binary, base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = {
|
||||||
|
type: type,
|
||||||
|
clip: clip,
|
||||||
|
omitBackground: omit_background,
|
||||||
|
encoding: encoding,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type === 'jpeg') {
|
||||||
|
params = Object.assign(params, {quality: quality});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding === 'binary') {
|
||||||
|
params = Object.assign(params, {path: `${upload_path}/${filename}`});
|
||||||
|
}
|
||||||
|
console.log("params", new Date().getMilliseconds(), JSON.stringify(params));
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件
|
||||||
|
* @param res 响应对象
|
||||||
|
* @param encoding 编码方式
|
||||||
|
* @param base64 文件base64数据
|
||||||
|
* @param type 文件类型
|
||||||
|
* @param filename 文件名
|
||||||
|
* @param storageManager 存储管理器
|
||||||
|
* @param basePath 应用根路径
|
||||||
|
* @param upload_path 上传路径
|
||||||
|
* @returns 上传结果
|
||||||
|
*/
|
||||||
|
async function upload(res, encoding, base64, type, filename, storageManager, basePath, upload_path) {
|
||||||
|
if (encoding === 'base64' && typeof base64 === 'string') {
|
||||||
|
return res.status(200).json({
|
||||||
|
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);
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
getParam,
|
||||||
|
upload
|
||||||
|
};
|
||||||
165
lib/routes.js
Normal file
165
lib/routes.js
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import {fileURLToPath} from 'url';
|
||||||
|
import {dirname} from 'path';
|
||||||
|
import {getParam, upload} from './params.js';
|
||||||
|
import {randomString, jsonToHtml} from './utils.js';
|
||||||
|
import {FONT_STYLE} from './constants.js';
|
||||||
|
|
||||||
|
// 获取当前模块的目录名
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const basePath = dirname(dirname(__filename)); // 项目根目录
|
||||||
|
|
||||||
|
// 延迟获取 upload_path,确保环境变量已被加载
|
||||||
|
let upload_path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 健康检查接口
|
||||||
|
* @param {*} req
|
||||||
|
* @param {*} res
|
||||||
|
*/
|
||||||
|
function statusHandler(req, res) {
|
||||||
|
console.log("健康检查", new Date().getMilliseconds());
|
||||||
|
return res.status(200).json({});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 海报生成接口
|
||||||
|
* @param {*} req
|
||||||
|
* @param {*} res
|
||||||
|
* @param {*} browserManager
|
||||||
|
* @param {*} storageManager
|
||||||
|
*/
|
||||||
|
async function posterHandler(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
browserManager,
|
||||||
|
storageManager
|
||||||
|
) {
|
||||||
|
// 确保在使用时获取最新的环境变量值
|
||||||
|
upload_path = process.env.UPLOAD_PATH || 'uploads';
|
||||||
|
upload_path = upload_path + '/posters'
|
||||||
|
|
||||||
|
// 参数校验
|
||||||
|
if (!req.body || !req.body.webpage) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Missing required parameter: webpage'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const webpage = req.body.webpage;
|
||||||
|
const device = req.body.device || 1;
|
||||||
|
const width = req.body.width || 1920;
|
||||||
|
const height = req.body.height || 1080;
|
||||||
|
const type = req.body.type || 'png';
|
||||||
|
const encoding = req.body.encoding || 'binary';
|
||||||
|
const filename = 'poster_' + randomString(20) + '.' + type;
|
||||||
|
let param = getParam(req, filename, upload_path);
|
||||||
|
let base64;
|
||||||
|
|
||||||
|
const page = browserManager.getTargetPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.setViewport({width: width, height: height, deviceScaleFactor: device, isMobile: true})
|
||||||
|
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);
|
||||||
|
// 状态码在主应用中处理
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Poster generation error:', err);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'Internal server error',
|
||||||
|
message: err.toString()
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
// 确保只归还有效的页面
|
||||||
|
if (page && !page.isClosed()) {
|
||||||
|
browserManager.returnTargetPage(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PDF下载接口
|
||||||
|
* @param {*} req
|
||||||
|
* @param {*} res
|
||||||
|
* @param {*} browserManager
|
||||||
|
* @param {*} storageManager
|
||||||
|
*/
|
||||||
|
async function pdfHandler(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
browserManager,
|
||||||
|
storageManager
|
||||||
|
) {
|
||||||
|
// 确保在使用时获取最新的环境变量值
|
||||||
|
upload_path = process.env.UPLOAD_PATH || 'uploads';
|
||||||
|
upload_path = upload_path + '/pdfs'
|
||||||
|
|
||||||
|
// 参数校验
|
||||||
|
if (!req.body || (!req.body.webpage && !req.body.html)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Missing required parameter: webpage or html'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const webpage = req.body.webpage;
|
||||||
|
let html = req.body.html;
|
||||||
|
const filename = 'pdf_' + randomString(20) + '.pdf';
|
||||||
|
let base64;
|
||||||
|
|
||||||
|
const page = browserManager.getTargetPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 根据参数选择加载方式
|
||||||
|
if (html) {
|
||||||
|
// 检查html是否为JSON格式,如果是则转换为HTML字符串
|
||||||
|
if (typeof html === 'object' && html !== null) {
|
||||||
|
html = jsonToHtml(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接使用HTML内容
|
||||||
|
await page.setContent(html, {
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 导航到网页
|
||||||
|
await page.goto(webpage, {
|
||||||
|
timeout: 30000,
|
||||||
|
waitUntil: 'networkidle0'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.addStyleTag({content: FONT_STYLE});
|
||||||
|
// 生成PDF
|
||||||
|
await page.pdf({
|
||||||
|
format: 'A4',
|
||||||
|
printBackground: true,
|
||||||
|
path: `${upload_path}/${filename}`
|
||||||
|
});
|
||||||
|
// 复用上传函数处理PDF文件
|
||||||
|
await upload(res, 'binary', base64, 'pdf', filename, storageManager, basePath, upload_path);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('PDF generation error:', err);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'Internal server error',
|
||||||
|
message: err.toString()
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
// 确保只归还有效的页面
|
||||||
|
if (page && !page.isClosed()) {
|
||||||
|
browserManager.returnTargetPage(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
statusHandler,
|
||||||
|
posterHandler,
|
||||||
|
pdfHandler
|
||||||
|
};
|
||||||
172
lib/storage.js
Normal file
172
lib/storage.js
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import COS from 'cos-nodejs-sdk-v5';
|
||||||
|
import OSS from 'ali-oss';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
class StorageManager {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
this.storageType = (config.STORAGE_TYPE || 'local').toLowerCase();
|
||||||
|
|
||||||
|
// 初始化COS客户端
|
||||||
|
if (this.storageType === 'cos' && config.COS_SECRET_ID && config.COS_SECRET_KEY) {
|
||||||
|
const cosConfig = {
|
||||||
|
SecretId: config.COS_SECRET_ID,
|
||||||
|
SecretKey: config.COS_SECRET_KEY,
|
||||||
|
};
|
||||||
|
this.cosClient = new COS(cosConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化OSS客户端
|
||||||
|
if (this.storageType === 'oss') {
|
||||||
|
const ossConfig = {
|
||||||
|
region: config.OSS_REGION,
|
||||||
|
accessKeyId: config.OSS_ACCESS_KEY_ID,
|
||||||
|
accessKeySecret: config.OSS_ACCESS_KEY_SECRET,
|
||||||
|
bucket: config.OSS_BUCKET
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ossConfig.accessKeyId && ossConfig.accessKeySecret && ossConfig.region && ossConfig.bucket) {
|
||||||
|
this.ossClient = new OSS(ossConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件
|
||||||
|
* @param basePath 应用根路径
|
||||||
|
* @param upload_path 上传路径
|
||||||
|
* @param filename 文件名
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
async upload(basePath, upload_path, filename) {
|
||||||
|
switch (this.storageType.toLowerCase()) {
|
||||||
|
case 'oss':
|
||||||
|
return this.uploadToOSS(basePath, upload_path, filename);
|
||||||
|
|
||||||
|
case 'cos':
|
||||||
|
return this.uploadToCOS(basePath, upload_path, filename);
|
||||||
|
|
||||||
|
case 'local':
|
||||||
|
return this.uploadToLocal(basePath, upload_path, filename);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return this.uploadToCOS(basePath, upload_path, filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件到本地
|
||||||
|
* @param basePath 应用根路径
|
||||||
|
* @param upload_path 上传路径
|
||||||
|
* @param filename 文件名
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
async uploadToLocal(basePath, upload_path, filename) {
|
||||||
|
const date = new Date();
|
||||||
|
const pathDir = `${upload_path}/${date.getFullYear()}${date.getMonth() + 1}/${date.getDate()}`;
|
||||||
|
const localFilePath = `${basePath}/${upload_path}/${filename}`;
|
||||||
|
const targetDir = `${basePath}/${pathDir}`;
|
||||||
|
|
||||||
|
// 确保目标目录存在
|
||||||
|
await fs.promises.mkdir(targetDir, { recursive: true });
|
||||||
|
|
||||||
|
// 移动文件到目标位置
|
||||||
|
const targetPath = `${targetDir}/${filename}`;
|
||||||
|
await fs.promises.rename(localFilePath, targetPath);
|
||||||
|
|
||||||
|
// 构造访问URL
|
||||||
|
const localDomain = process.env.LOCAL_DOMAIN || 'http://localhost:3000';
|
||||||
|
const url = `${localDomain}/${pathDir}/${filename}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: filename,
|
||||||
|
path: url
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件到COS
|
||||||
|
* @param basePath 应用根路径
|
||||||
|
* @param upload_path 上传路径
|
||||||
|
* @param filename 文件名
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
async uploadToCOS(basePath, upload_path, filename) {
|
||||||
|
const date = new Date();
|
||||||
|
const path = `${upload_path}/${date.getFullYear()}${date.getMonth() + 1}/${date.getDate()}`;
|
||||||
|
const localFilePath = `${basePath}/${upload_path}/${filename}`;
|
||||||
|
const cosDomain = this.config.COS_DOMAIN; // 保存this.config的引用
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.cosClient.putObject({
|
||||||
|
Bucket: this.config.COS_BUCKET,
|
||||||
|
Region: this.config.COS_REGION,
|
||||||
|
Key: `${path}/${filename}`,
|
||||||
|
StorageClass: 'STANDARD',
|
||||||
|
Body: fs.createReadStream(localFilePath),
|
||||||
|
}, function (err, data) {
|
||||||
|
fs.unlink(localFilePath, function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Failed to delete local file:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
const response = {
|
||||||
|
name: filename,
|
||||||
|
path: `${cosDomain}/${path}/${filename}`,
|
||||||
|
data: data
|
||||||
|
};
|
||||||
|
resolve(response);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件到OSS
|
||||||
|
* @param basePath 应用根路径
|
||||||
|
* @param upload_path 上传路径
|
||||||
|
* @param filename 文件名
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
async uploadToOSS(basePath, upload_path, filename) {
|
||||||
|
if (!this.ossClient) {
|
||||||
|
throw new Error('OSS client not initialized. Please check OSS configuration.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
const path = `${upload_path}/${date.getFullYear()}${date.getMonth() + 1}/${date.getDate()}`;
|
||||||
|
const fullPath = `${path}/${filename}`;
|
||||||
|
const localFilePath = `${basePath}/${upload_path}/${filename}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.ossClient.put(fullPath, localFilePath);
|
||||||
|
// 上传完成后删除本地文件
|
||||||
|
fs.unlink(localFilePath, function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Failed to delete local file:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: filename,
|
||||||
|
path: result.url,
|
||||||
|
data: result
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
// 出错时也删除本地文件
|
||||||
|
fs.unlink(localFilePath, function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Failed to delete local file:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StorageManager;
|
||||||
118
lib/utils.js
Normal file
118
lib/utils.js
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* 将JSON格式的HTML结构转换为HTML字符串
|
||||||
|
* @param {Object} json - JSON格式的HTML结构
|
||||||
|
* @returns {string} HTML字符串
|
||||||
|
*/
|
||||||
|
function jsonToHtml(json) {
|
||||||
|
if (!json || typeof json !== 'object') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseElement(obj) {
|
||||||
|
if (obj === null || obj === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map(item => parseElement(item)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为文本节点 (#text)
|
||||||
|
if ('#text' in obj) {
|
||||||
|
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);
|
||||||
|
attributes += ` ${attrName}="${obj[key]}"`;
|
||||||
|
} else if (['div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'section', 'header', 'footer', 'main', 'article', 'aside'].includes(key)) {
|
||||||
|
// 子元素标签
|
||||||
|
tagName = key;
|
||||||
|
content = parseElement(obj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<${tagName}${attributes}>${content}</${tagName}>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通元素节点
|
||||||
|
let tagName = 'div'; // 默认标签
|
||||||
|
let attributes = '';
|
||||||
|
let content = '';
|
||||||
|
|
||||||
|
for (const key in obj) {
|
||||||
|
if (key.startsWith('@')) {
|
||||||
|
// 属性 (@class, @id等)
|
||||||
|
const attrName = key.substring(1);
|
||||||
|
attributes += ` ${attrName}="${obj[key]}"`;
|
||||||
|
} else if (['div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'section', 'header', 'footer', 'main', 'article', 'aside', 'ul', 'ol', 'li', 'table', 'thead', 'tbody', 'tr', 'td', 'th'].includes(key)) {
|
||||||
|
// 明确指定的标签类型
|
||||||
|
tagName = key;
|
||||||
|
content = parseElement(obj[key]);
|
||||||
|
} else if (key === 'style') {
|
||||||
|
// style标签特殊处理
|
||||||
|
content += `<style>${obj[key]}</style>`;
|
||||||
|
} else if (key === 'title') {
|
||||||
|
// title标签特殊处理
|
||||||
|
content += `<title>${obj[key]}</title>`;
|
||||||
|
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||||
|
// 其他嵌套对象
|
||||||
|
content += parseElement(obj[key]);
|
||||||
|
} else if (key !== '#text') {
|
||||||
|
// 其他属性
|
||||||
|
attributes += ` ${key}="${obj[key]}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<${tagName}${attributes}>${content}</${tagName}>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理顶层html结构
|
||||||
|
if (json.html) {
|
||||||
|
const headContent = json.html.head ? parseElement(json.html.head) : '';
|
||||||
|
const bodyContent = json.html.body ? parseElement(json.html.body) : '';
|
||||||
|
const headSection = headContent ? `<head>${headContent}</head>` : '<head></head>';
|
||||||
|
const bodySection = bodyContent ? `<body>${bodyContent}</body>` : '<body></body>';
|
||||||
|
return `<!DOCTYPE html><html>${headSection}${bodySection}</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.head || json.body) {
|
||||||
|
const headContent = json.head ? parseElement(json.head) : '';
|
||||||
|
const bodyContent = json.body ? parseElement(json.body) : '';
|
||||||
|
const headSection = headContent ? `<head>${headContent}</head>` : '<head></head>';
|
||||||
|
const bodySection = bodyContent ? `<body>${bodyContent}</body>` : '<body></body>';
|
||||||
|
return `<!DOCTYPE html><html>${headSection}${bodySection}</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseElement(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机字符串
|
||||||
|
* @param {number} length - 字符串长度
|
||||||
|
* @returns {string} 随机字符串
|
||||||
|
*/
|
||||||
|
function randomString(length) {
|
||||||
|
let result = '';
|
||||||
|
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
const charactersLength = characters.length;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
jsonToHtml,
|
||||||
|
randomString
|
||||||
|
};
|
||||||
22
package.json
Normal file
22
package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "puppeteer",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "server.mjs",
|
||||||
|
"type": "module",
|
||||||
|
"bin": "index.mjs",
|
||||||
|
"dependencies": {
|
||||||
|
"ali-oss": "^6.17.1",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"cos-nodejs-sdk-v5": "^2.8.3",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
|
"express": "5.0.0",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
|
"puppeteer": "24.29.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "node server.mjs"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
||||||
2156
pnpm-lock.yaml
Normal file
2156
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
64
server.mjs
Normal file
64
server.mjs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
// 配置 dotenv
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
import BrowserManager from './lib/browser.js';
|
||||||
|
import StorageManager from './lib/storage.js';
|
||||||
|
import {pdfHandler, posterHandler, statusHandler} from './lib/routes.js';
|
||||||
|
|
||||||
|
// 初始化存储管理器
|
||||||
|
const storageManager = new StorageManager(process.env);
|
||||||
|
|
||||||
|
// 初始化浏览器管理器
|
||||||
|
const browserManager = new BrowserManager();
|
||||||
|
await browserManager.initBrowser(2);
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(bodyParser.json({limit: '10mb'}));
|
||||||
|
// 提供上传文件的静态访问
|
||||||
|
app.use('/uploads', express.static('uploads'));
|
||||||
|
// 健康检查接口
|
||||||
|
app.get('/status', statusHandler);
|
||||||
|
|
||||||
|
// 添加安全头
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||||
|
res.setHeader('X-Frame-Options', 'DENY');
|
||||||
|
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 海报生成接口
|
||||||
|
app.post('/api/v1/poster', 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'});
|
||||||
|
}
|
||||||
|
|
||||||
|
await posterHandler(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
browserManager,
|
||||||
|
storageManager
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// PDF生成接口
|
||||||
|
app.post('/api/v1/pdf', 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'});
|
||||||
|
}
|
||||||
|
|
||||||
|
await pdfHandler(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
browserManager,
|
||||||
|
storageManager
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
app.listen(port, () => console.log(`Puppeteer app listening on port ${port}!`));
|
||||||
Loading…
Reference in New Issue
Block a user