From 4efd53f59b985c6cae92e51b8cd72a9511dfbd20 Mon Sep 17 00:00:00 2001 From: Kokomi <102026640+yoimiya-kokomi@users.noreply.github.com> Date: Tue, 18 Apr 2023 02:37:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=86=E6=B8=B2=E6=9F=93=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=EF=BC=8C=E6=94=AF=E6=8C=81=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 + lib/puppeteer/puppeteer.js | 74 ++----- lib/renderer/Renderer.js | 40 ++++ package.json | 2 +- renderers/.gitignore | 2 +- renderers/base_renderer.js | 28 --- renderers/puppeteer/config_default.yaml | 15 ++ renderers/puppeteer/index.js | 25 ++- .../puppeteer.js} | 205 +++++++----------- 9 files changed, 187 insertions(+), 210 deletions(-) create mode 100644 lib/renderer/Renderer.js delete mode 100644 renderers/base_renderer.js create mode 100644 renderers/puppeteer/config_default.yaml rename renderers/puppeteer/{puppeteer_renderer.js => lib/puppeteer.js} (74%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ff6b03..adb19cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 3.0.2 + +* 3.6卡池以及图像武器别名等数据更新 **@cvs** +* 将渲染逻辑独立,支持扩展渲染器 **@ikuaki** +* icqq等依赖版本升级 + # 3.0.1 * 添加`#版本`命令,用于查看更新记录 diff --git a/lib/puppeteer/puppeteer.js b/lib/puppeteer/puppeteer.js index b3a2e94..9ca241a 100644 --- a/lib/puppeteer/puppeteer.js +++ b/lib/puppeteer/puppeteer.js @@ -1,60 +1,28 @@ -import fs from 'node:fs' -import path from 'path' -import { fileURLToPath } from 'url' -import cfg from '../config/config.js' +import Renderer from '../renderer/Renderer.js' -const __dirname = path.dirname(fileURLToPath(import.meta.url)) - -const rendererDir = path.join(__dirname, "../../renderers/"); - -let rendererBackends = {}; - -const rendererProxyHandler = { - get(target, prop, receiver) { - if (!(prop in receiver)) { - logger.fatal("在类 " + target.constructor.name + " 上访问了未实现的方法或属性 " + prop + ", 请报告错误!"); - return undefined; - } - return Reflect.get(...arguments); +/** + * 暂时保留对手工引用puppeteer.js的兼容 + * 后期会逐步废弃 + * 只提供截图及分片截图功能 + */ +export default { + // 截图 + async screenshot (name, data = {}) { + let renderer = Renderer.getRenderer() + let img = await renderer.render(name, data) + return img ? segment.image(img) : img }, -} -async function registerRendererBackends() { - const subFolders = fs.readdirSync(rendererDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()); - for (let subFolder of subFolders) { - const newRendererBackends = (await import(path.join(rendererDir, subFolder.name, 'index.js'))).default; - for (let rendererBackendName in newRendererBackends) { - rendererBackends[rendererBackendName] = newRendererBackends[rendererBackendName]; - logger.mark("[渲染后端加载]: 导入 " + rendererBackendName); + // 分片截图 + async screenshots (name, data = {}) { + let renderer = Renderer.getRenderer() + data.multiPage = true + let imgs = await renderer.render(name, data) || [] + let ret = [] + for (let img of imgs) { + ret.push(img ? segment.image(img) : img) } + return ret.length > 0 ? ret : false } } -function selectRendererBackend() { - let rendererBackendName = cfg.renderer?.name; - // 未指定,回退到 puppeteer - if (!rendererBackendName) rendererBackendName = "puppeteer"; - let rendererBackendConstructor = rendererBackends[rendererBackendName]; - if (!rendererBackendConstructor) { - logger.warn("未知的渲染后端 " + rendererBackendName + ", 回退到 puppeteer"); - rendererBackendName = "puppeteer"; - rendererBackendConstructor = rendererBackends[rendererBackendName]; - } - if (!rendererBackendConstructor.isImportable()) { - logger.warn("渲染后端 " + rendererBackendName + " 不可用 (导入失败), 回退到 puppeteer"); - rendererBackendName = "puppeteer"; - rendererBackendConstructor = rendererBackends[rendererBackendName]; - } - logger.mark("当前渲染后端:" + rendererBackendName); - return rendererBackendConstructor; -} - -async function newRenderer() { - // 自动扫描,并注册渲染后端 - await registerRendererBackends(); - // 选择渲染后端 - let rendererBackendConstructor = selectRendererBackend(); - return new rendererBackendConstructor(); -} - -export default new Proxy(await newRenderer(), rendererProxyHandler); diff --git a/lib/renderer/Renderer.js b/lib/renderer/Renderer.js new file mode 100644 index 0000000..cc50e93 --- /dev/null +++ b/lib/renderer/Renderer.js @@ -0,0 +1,40 @@ +import fs from 'node:fs' +import yaml from 'yaml' +import lodash from 'lodash' +import cfg from '../config/config.js' +import { Data } from '#miao' + +let rendererBackends = {} + +async function registerRendererBackends () { + const subFolders = fs.readdirSync(`${process.cwd()}/renderers`, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()) + for (let subFolder of subFolders) { + let name = subFolder.name + const rendererFn = await Data.importDefault(`/renderers/${name}/index.js`) + let configFile = `./renderers/${name}/config.yaml` + let rendererCfg = {} + if (fs.existsSync(configFile)) { + try { + rendererCfg = yaml.parse(fs.readFileSync(configFile, 'utf8')) + } catch (e) { + rendererCfg = {} + } + } + let renderer = rendererFn(rendererCfg) + if (!renderer.id || !renderer.type || !renderer.render || !lodash.isFunction(renderer.render)) { + logger.warn('渲染后端 ' + (renderer.id || subFolder.name) + ' 不可用') + } + rendererBackends[renderer.id] = renderer + logger.mark('[渲染后端加载]: 导入 ' + renderer.id) + } +} + +await registerRendererBackends() + +export default { + getRenderer () { + // TODO 渲染器降级 + return rendererBackends[cfg.renderer?.name || 'puppeteer'] + } +} + diff --git a/package.json b/package.json index e306471..36ccd4d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "miao-yunzai", - "version": "3.0.1", + "version": "3.0.2", "author": "Yoimiya-Kokomi, Le-niao", "description": "QQ group Bot", "main": "app.js", diff --git a/renderers/.gitignore b/renderers/.gitignore index f356a70..6ecbe42 100644 --- a/renderers/.gitignore +++ b/renderers/.gitignore @@ -1,6 +1,6 @@ * !.gitignore -!base_renderer.js !puppeteer !puppeteer/** +puppeteer/config.yaml \ No newline at end of file diff --git a/renderers/base_renderer.js b/renderers/base_renderer.js deleted file mode 100644 index f86ec9a..0000000 --- a/renderers/base_renderer.js +++ /dev/null @@ -1,28 +0,0 @@ -export default class BaseRenderer { - /** - * 截图 - */ - async screenshot(name, data = {}) { - this.constructor._WARNING('screenshot(name, data)'); - return false; - } - - /** - * 分片截图 - */ - async screenshots(name, data = {}) { - this.constructor._WARNING('screenshots(name, data)'); - return false; - } - - /** - * 该渲染后端是否可导入 - */ - static isImportable() { - return false; - } - - static _WARNING(fName) { - logger.fatal('方法 "' + fName + ' 在类 ' + this.name + ' 中未被实现,请报告错误!'); - } -} diff --git a/renderers/puppeteer/config_default.yaml b/renderers/puppeteer/config_default.yaml new file mode 100644 index 0000000..3a2f542 --- /dev/null +++ b/renderers/puppeteer/config_default.yaml @@ -0,0 +1,15 @@ +# 如需自定义,复制此文件为 config.yaml 进行配置 +# 更新配置后需要重启 + +# chromium 地址 +chromiumPath: + +# headless +headless: true + +# puppeteer启动args +args: + - --disable-gpu + - --disable-setuid-sandbox + - --no-sandbox + - --no-zygote \ No newline at end of file diff --git a/renderers/puppeteer/index.js b/renderers/puppeteer/index.js index 8f61c53..ecb4dc3 100644 --- a/renderers/puppeteer/index.js +++ b/renderers/puppeteer/index.js @@ -1,7 +1,22 @@ -import PuppeteerRenderer from './puppeteer_renderer.js'; +import Puppeteer from './lib/puppeteer.js' -const newRendererBackends = { - 'puppeteer': PuppeteerRenderer -}; +/** + * + * @param config 本地config.yaml的配置内容 + * @returns renderer 渲染器对象 + * @returns renderer.id 渲染器ID,对应renderer中选择的id + * @returns renderer.type 渲染类型,保留字段,暂时支持image + * @returns renderer.render 渲染入口 + */ +export default function (config) { + // TODO Puppeteer待简化重构 + const PuppeteerRender = new Puppeteer(config) -export default newRendererBackends; + return { + id: 'puppeteer', + type: 'image', + async render (name, data) { + return await PuppeteerRender.screenshot(name, data) + } + } +} \ No newline at end of file diff --git a/renderers/puppeteer/puppeteer_renderer.js b/renderers/puppeteer/lib/puppeteer.js similarity index 74% rename from renderers/puppeteer/puppeteer_renderer.js rename to renderers/puppeteer/lib/puppeteer.js index ba727f7..a485b2e 100644 --- a/renderers/puppeteer/puppeteer_renderer.js +++ b/renderers/puppeteer/lib/puppeteer.js @@ -3,9 +3,9 @@ import os from 'node:os' import lodash from 'lodash' import template from 'art-template' import chokidar from 'chokidar' +// 暂时保留对原config的兼容 import cfg from '../../lib/config/config.js' -import common from '../../lib/common/common.js' -import BaseRenderer from '../base_renderer.js' +import { Data } from '#miao' const _path = process.cwd() @@ -14,9 +14,8 @@ let puppeteer = {} // mac地址 let mac = '' -export default class PuppeteerRenderer extends BaseRenderer { - constructor() { - super() +export default class PuppeteerRenderer { + constructor (config) { this.browser = false this.lock = false this.shoting = [] @@ -25,18 +24,17 @@ export default class PuppeteerRenderer extends BaseRenderer { /** 截图次数 */ this.renderNum = 0 this.config = { - headless: true, - args: [ + headless: Data.def(config.headless, true), + args: Data.def(config.args, [ '--disable-gpu', '--disable-setuid-sandbox', '--no-sandbox', '--no-zygote' - ] + ]) } - - if (cfg.bot?.chromium_path) { + if (cfg?.bot?.chromium_path || config.chromiumPath) { /** chromium其他路径 */ - this.config.executablePath = cfg.bot.chromium_path + this.config.executablePath = cfg.bot?.chromium_path || config.chromiumPath } this.html = {} @@ -44,15 +42,13 @@ export default class PuppeteerRenderer extends BaseRenderer { this.createDir('./temp/html') } - async initPupp() { + async initPupp () { if (!lodash.isEmpty(puppeteer)) return puppeteer - puppeteer = (await import('puppeteer')).default - return puppeteer } - createDir(dir) { + createDir (dir) { if (!fs.existsSync(dir)) { let dirs = dir.split('/') for (let idx = 1; idx <= dirs.length; idx++) { @@ -67,7 +63,7 @@ export default class PuppeteerRenderer extends BaseRenderer { /** * 初始化chromium */ - async browserInit() { + async browserInit () { await this.initPupp() if (this.browser) return this.browser if (this.lock) return false @@ -150,7 +146,7 @@ export default class PuppeteerRenderer extends BaseRenderer { } // 获取Mac地址 - async getMac() { + async getMac () { // 获取Mac地址 let mac = '00:00:00:00:00:00' try { @@ -185,12 +181,16 @@ export default class PuppeteerRenderer extends BaseRenderer { * @param data.quality screenshot参数,图片质量 0-100,jpeg是可传,默认90 * @param data.omitBackground screenshot参数,隐藏默认的白色背景,背景透明。默认不透明 * @param data.path screenshot参数,截图保存路径。截图图片类型将从文件扩展名推断出来。如果是相对路径,则从当前路径解析。如果没有指定路径,图片将不会保存到硬盘。 - * @return icqq img + * @param data.multiPage 是否分页截图,默认false + * @param data.multiPageHeight 分页状态下页面高度,默认4000 + * @param data.pageGotoParams 页面goto时的参数 + * @return img/[]img 不做segment包裹 */ - async screenshot(name, data = {}) { + async screenshot (name, data = {}) { if (!await this.browserInit()) { return false } + const pageHeight = data.multiPageHeight || 4000 let savePath = this.dealTpl(name, data) if (!savePath) return false @@ -198,26 +198,80 @@ export default class PuppeteerRenderer extends BaseRenderer { let buff = '' let start = Date.now() + let ret = [] this.shoting.push(name) try { const page = await this.browser.newPage() - await page.goto(`file://${_path}${lodash.trim(savePath, '.')}`, data.pageGotoParams || {}) + let pageGotoParams = lodash.extend({ timeout: 120000 }, data.pageGotoParams || {}) + await page.goto(`file://${_path}${lodash.trim(savePath, '.')}`, pageGotoParams) let body = await page.$('#container') || await page.$('body') + // 计算页面高度 + const boundingBox = await body.boundingBox() + // 分页数 + let num = 1 + let randData = { - // encoding: 'base64', type: data.imgType || 'jpeg', omitBackground: data.omitBackground || false, quality: data.quality || 90, path: data.path || '' } - if (data.imgType === 'png') delete randData.quality + if (data.multiPage) { + randData.type = 'jpeg' + num = Math.round(boundingBox.height / pageHeight) || 1 + } - buff = await body.screenshot(randData) + if (data.imgType === 'png') { + delete randData.quality + } + if (!data.multiPage) { + buff = await body.screenshot(randData) + /** 计算图片大小 */ + const kb = (buff.length / 1024).toFixed(2) + 'kb' + logger.mark(`[图片生成][${name}][${this.renderNum}次] ${kb} ${logger.green(`${Date.now() - start}ms`)}`) + this.renderNum++ + ret.push(buff) + } else { + // 分片截图 + if (num > 1) { + await page.setViewport({ + width: boundingBox.width, + height: pageHeight + 100 + }) + } + for (let i = 1; i <= num; i++) { + if (i !== 1 && i === num) { + await page.setViewport({ + width: boundingBox.width, + height: parseInt(boundingBox.height) - pageHeight * (num - 1) + }) + } + if (i !== 1 && i <= num) { + await page.evaluate(() => window.scrollBy(0, 7000)) + } + if (num === 1) { + buff = await body.screenshot(randData) + } else { + buff = await page.screenshot(randData) + } + if (num > 2) await Data.sleep(200) + this.renderNum++ + + /** 计算图片大小 */ + const kb = (buff.length / 1024).toFixed(2) + 'kb' + logger.mark(`[图片生成][${name}][${i}/${num}] ${kb}`) + ret.push(buff) + } + if (num > 1) { + logger.mark(`[图片生成][${name}] 处理完成`) + } + } page.close().catch((err) => logger.error(err)) + } catch (error) { logger.error(`图片生成失败:${name}:${error}`) /** 关闭浏览器 */ @@ -225,112 +279,24 @@ export default class PuppeteerRenderer extends BaseRenderer { await this.browser.close().catch((err) => logger.error(err)) } this.browser = false - buff = '' + ret = [] return false } this.shoting.pop() - if (!buff) { + if (ret.length === 0 || !ret[0]) { logger.error(`图片生成为空:${name}`) return false } - this.renderNum++ - - /** 计算图片大小 */ - let kb = (buff.length / 1024).toFixed(2) + 'kb' - - logger.mark(`[图片生成][${name}][${this.renderNum}次] ${kb} ${logger.green(`${Date.now() - start}ms`)}`) - this.restart() - return segment.image(buff) - } - - /** - * `chromium` 分片截图 - */ - async screenshots(name, data = {}) { - // FIXME: pageHeight 作为参数? - const pageHeight = 7000 - - await this.browserInit() - - if (!this.browser) return false - - const savePath = this.dealTpl(name, data) - if (!savePath) return false - - const page = await this.browser.newPage() - try { - await page.goto(`file://${_path}${lodash.trim(savePath, '.')}`, { timeout: 120000 }) - const body = await page.$('#container') || await page.$('body') - const boundingBox = await body.boundingBox() - - const num = Math.round(boundingBox.height / pageHeight) || 1 - - if (num > 1) { - await page.setViewport({ - width: boundingBox.width, - height: pageHeight + 100 - }) - } - - const img = [] - for (let i = 1; i <= num; i++) { - const randData = { - type: 'jpeg', - quality: 90 - } - - if (i !== 1 && i === num) { - await page.setViewport({ - width: boundingBox.width, - height: parseInt(boundingBox.height) - pageHeight * (num - 1) - }) - } - - if (i !== 1 && i <= num) { - await page.evaluate(() => window.scrollBy(0, 7000)) - } - - let buff - if (num === 1) { - buff = await body.screenshot(randData) - } else { - buff = await page.screenshot(randData) - } - - if (num > 2) await common.sleep(200) - - this.renderNum++ - /** 计算图片大小 */ - const kb = (buff.length / 1024).toFixed(2) + 'kb' - - logger.mark(`[图片生成][${name}][${this.renderNum}次] ${kb}`) - - img.push(segment.image(buff)) - } - - await page.close().catch((err) => logger.error(err)) - - if (num > 1) { - logger.mark(`[图片生成][${name}] 处理完成`) - } - return img - } catch (error) { - logger.error(`图片生成失败:${name}:${error}`) - /** 关闭浏览器 */ - if (this.browser) { - await this.browser.close().catch((err) => logger.error(err)) - } - this.browser = false - } + return data.multiPage ? ret : ret[0] } /** 模板 */ - dealTpl(name, data) { + dealTpl (name, data) { let { tplFile, saveId = name } = data let savePath = `./temp/html/${name}/${saveId}.html` @@ -362,7 +328,7 @@ export default class PuppeteerRenderer extends BaseRenderer { } /** 监听配置文件 */ - watch(tplFile) { + watch (tplFile) { if (this.watcher[tplFile]) return const watcher = chokidar.watch(tplFile) @@ -375,7 +341,7 @@ export default class PuppeteerRenderer extends BaseRenderer { } /** 重启 */ - restart() { + restart () { /** 截图超过重启数时,自动关闭重启浏览器,避免生成速度越来越慢 */ if (this.renderNum % this.restartNum === 0) { if (this.shoting.length <= 0) { @@ -389,10 +355,5 @@ export default class PuppeteerRenderer extends BaseRenderer { } } } - - static isImportable() { - // XXX: Puppeteer 为默认的,因此总可以被导入 - return true; - } }