// import { remark } from 'remark' // import stripMarkdown from 'strip-markdown' import { exec } from 'child_process' import lodash from 'lodash' import fs from 'node:fs' import path from 'node:path' import puppeteer from '../../../lib/puppeteer/puppeteer.js' // export function markdownToText (markdown) { // return remark() // .use(stripMarkdown) // .processSync(markdown ?? '') // .toString() // } export function escapeHtml (str) { const htmlEntities = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/' } return str.replace(/[&<>"'/]/g, (match) => htmlEntities[match]) } export function randomString (length = 5) { let str = '' for (let i = 0; i < length; i++) { str += lodash.random(36).toString(36) } return str.substr(0, length) } export async function upsertMessage (message) { await redis.set(`CHATGPT:MESSAGE:${message.id}`, JSON.stringify(message)) } export async function getMessageById (id) { let messageStr = await redis.get(`CHATGPT:MESSAGE:${id}`) return JSON.parse(messageStr) } export async function tryTimes (promiseFn, maxTries = 10) { try { return await promiseFn() } catch (e) { if (maxTries > 0) { logger.warn('Failed, retry ' + maxTries) return tryTimes(promiseFn, maxTries - 1) } throw e } } export async function makeForwardMsg (e, msg = [], dec = '') { let nickname = Bot.nickname if (e.isGroup) { let info = await Bot.getGroupMemberInfo(e.group_id, Bot.uin) nickname = info.card || info.nickname } let userInfo = { user_id: Bot.uin, nickname } let forwardMsg = [] msg.forEach(v => { forwardMsg.push({ ...userInfo, message: v }) }) /** 制作转发内容 */ if (e.isGroup) { forwardMsg = await e.group.makeForwardMsg(forwardMsg) } else if (e.friend) { forwardMsg = await e.friend.makeForwardMsg(forwardMsg) } else { return false } if (dec) { /** 处理描述 */ forwardMsg.data = forwardMsg.data .replace(/\n/g, '') .replace(/(.+?)<\/title>/g, '___') .replace(/___+/, `<title color="#777777" size="26">${dec}`) } return forwardMsg } // @see https://github.com/sindresorhus/p-timeout export async function pTimeout ( promise, options ) { const { milliseconds, fallback, message, customTimers = { setTimeout, clearTimeout } } = options let timer const cancelablePromise = new Promise((resolve, reject) => { if (typeof milliseconds !== 'number' || Math.sign(milliseconds) !== 1) { throw new TypeError( `Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\`` ) } if (milliseconds === Number.POSITIVE_INFINITY) { resolve(promise) return } if (options.signal) { const { signal } = options if (signal.aborted) { reject(getAbortedReason(signal)) } signal.addEventListener('abort', () => { reject(getAbortedReason(signal)) }) } timer = customTimers.setTimeout.call( undefined, () => { if (fallback) { try { resolve(fallback()) } catch (error) { reject(error) } return } const errorMessage = typeof message === 'string' ? message : `Promise timed out after ${milliseconds} milliseconds` const timeoutError = message instanceof Error ? message : new Error(errorMessage) if (typeof promise.cancel === 'function') { promise.cancel() } reject(timeoutError) }, milliseconds ) ;(async () => { try { resolve(await promise) } catch (error) { reject(error) } finally { customTimers.clearTimeout.call(undefined, timer) } })() }) cancelablePromise.clear = () => { customTimers.clearTimeout.call(undefined, timer) timer = undefined } return cancelablePromise } /** TODO: Remove below function and just 'reject(signal.reason)' when targeting Node 18. */ function getAbortedReason (signal) { const reason = signal.reason === undefined ? getDOMException('This operation was aborted.') : signal.reason return reason instanceof Error ? reason : getDOMException(reason) } /** TODO: Remove AbortError and just throw DOMException when targeting Node 18. */ function getDOMException (errorMessage) { return globalThis.DOMException === undefined ? new Error(errorMessage) : new DOMException(errorMessage) } export async function checkPnpm () { let npm = 'npm' let ret = await execSync('pnpm -v') if (ret.stdout) npm = 'pnpm' return npm } async function execSync (cmd) { return new Promise((resolve, reject) => { exec(cmd, { windowsHide: true }, (error, stdout, stderr) => { resolve({ error, stdout, stderr }) }) }) } export function mkdirs (dirname) { if (fs.existsSync(dirname)) { return true } else { if (mkdirs(path.dirname(dirname))) { fs.mkdirSync(dirname) return true } } } /** * * @param pluginKey plugin key * @param htmlPath html文件路径,相对于plugin resources目录 * @param data 渲染数据 * @param renderCfg 渲染配置 * @param renderCfg.retType 返回值类型 * * default/空:自动发送图片,返回true * * msgId:自动发送图片,返回msg id * * base64: 不自动发送图像,返回图像base64数据 * @param renderCfg.beforeRender({data}) 可改写渲染的data数据 * @returns {Promise} */ export async function render (e, pluginKey, htmlPath, data = {}, renderCfg = {}) { // 处理传入的path htmlPath = htmlPath.replace(/.html$/, '') let paths = lodash.filter(htmlPath.split('/'), (p) => !!p) htmlPath = paths.join('/') // 创建目录 const mkdir = (check) => { let currDir = `${process.cwd()}/data` for (let p of check.split('/')) { currDir = `${currDir}/${p}` if (!fs.existsSync(currDir)) { fs.mkdirSync(currDir) } } return currDir } mkdir(`html/${pluginKey}/${htmlPath}`) // 自动计算pluResPath let pluResPath = `../../../${lodash.repeat('../', paths.length)}plugins/${pluginKey}/resources/` // 渲染data data = { ...data, _plugin: pluginKey, _htmlPath: htmlPath, pluResPath, tplFile: `./plugins/${pluginKey}/resources/${htmlPath}.html`, saveId: data.saveId || data.save_id || paths[paths.length - 1], pageGotoParams: { waitUntil: 'networkidle0' } } // 处理beforeRender if (renderCfg.beforeRender) { data = renderCfg.beforeRender({ data }) || data } // 保存模板数据 if (process.argv.includes('web-debug')) { // debug下保存当前页面的渲染数据,方便模板编写与调试 // 由于只用于调试,开发者只关注自己当时开发的文件即可,暂不考虑app及plugin的命名冲突 let saveDir = mkdir(`ViewData/${pluginKey}`) let file = `${saveDir}/${data._htmlPath.split('/').join('_')}.json` fs.writeFileSync(file, JSON.stringify(data)) } // 截图 let base64 = await puppeteer.screenshot(`${pluginKey}/${htmlPath}`, data) if (renderCfg.retType === 'base64') { return base64 } let ret = true if (base64) { ret = await e.reply(base64) } return renderCfg.retType === 'msgId' ? ret : true }