// 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(/___+/, `${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
}