diff --git a/apps/state.js b/apps/state.js index aa15a1e..9e47db5 100644 --- a/apps/state.js +++ b/apps/state.js @@ -1,13 +1,8 @@ -import _ from 'lodash' -import { createRequire } from 'module' -import moment from 'moment' -import os from 'os' import plugin from '../../../lib/plugins/plugin.js' -import { Config, Version, Plugin_Name } from '../components/index.js' -import { status } from '../constants/other.js' -import { State, common, puppeteer } from '../model/index.js' -import formatDuration from '../tools/formatDuration.js' -const require = createRequire(import.meta.url) +import { Config } from '../components/index.js' +import { puppeteer } from '../model/index.js' +import { getData, si } from '../model/State/index.js' +import Monitor from '../model/State/Monitor.js' let interval = false export class NewState extends plugin { @@ -18,8 +13,11 @@ export class NewState extends plugin { priority: 50, rule: [ { - reg: '^#?(椰奶)?(状态|监控)(pro)?$', + reg: '^#?(椰奶)?状态(pro)?$', fnc: 'state' + }, { + reg: '^#椰奶监控$', + fnc: 'monitor' } ] @@ -28,7 +26,7 @@ export class NewState extends plugin { async monitor (e) { await puppeteer.render('state/monitor', { - chartData: JSON.stringify(State.chartData) + chartData: JSON.stringify(Monitor.chartData) }, { e, scale: 1.4 @@ -36,78 +34,15 @@ export class NewState extends plugin { } async state (e) { - if (e.msg.includes('监控')) return this.monitor(e) - if (!/椰奶/.test(e.msg) && !Config.whole.state) return false - if (!State.si) return e.reply('❎ 没有检测到systeminformation依赖,请运行:"pnpm add systeminformation -w"进行安装') + if (!si) return e.reply('❎ 没有检测到systeminformation依赖,请运行:"pnpm add systeminformation -w"进行安装') // 防止多次触发 if (interval) { return false } else interval = true - // 系统 - let otherInfo = [] - // 其他信息 - otherInfo.push({ - first: '系统', - tail: State.osInfo?.distro - }) - // 网络 - otherInfo.push(State.getnetwork) - // 插件数量 - otherInfo.push(State.getPluginNum) - let promiseTaskList = [ - State.getFastFetch(e), - State.getFsSize() - ] - // 网络测试 - let psTest = [] - let { psTestSites, psTestTimeout, backdrop } = Config.state - State.chartData.backdrop = backdrop - psTestSites && promiseTaskList.push(...psTestSites?.map(i => State.getNetworkLatency(i.url, psTestTimeout).then(res => psTest.push({ - first: i.name, - tail: res - })))) - // 执行promise任务 - let [FastFetch, HardDisk] = await Promise.all(promiseTaskList) - // 可视化数据 - let visualData = _.compact(await Promise.all([ - // CPU板块 - State.getCpuInfo(), - // 内存板块 - State.getMemUsage(), - // GPU板块 - State.getGPU(), - // Node板块 - State.getNodeInfo() - ])) - - /** bot列表 */ - let BotList = [e.self_id] - - if (e.msg.includes('pro')) { - if (Array.isArray(Bot?.uin)) { - BotList = Bot.uin - } else if (Bot?.adapter && Bot.adapter.includes(e.self_id)) { - BotList = Bot.adapter - } - } - // 渲染数据 - let data = { - BotStatus: await getBotState(BotList), - chartData: JSON.stringify(common.checkIfEmpty(State.chartData, ['echarts_theme', 'cpu', 'ram']) ? undefined : State.chartData), - // 硬盘内存 - HardDisk, - // FastFetch - FastFetch, - // 硬盘速率 - fsStats: State.DiskSpeed, - // 可视化数据 - visualData, - // 其他数据 - otherInfo: _.compact(otherInfo), - psTest: _.isEmpty(psTest) ? false : psTest - } + // 获取数据 + let data = await getData(e) // 渲染图片 await puppeteer.render('state/state', { @@ -120,49 +55,3 @@ export class NewState extends plugin { interval = false } } -const getBotState = async (botList) => { - const defaultAvatar = `../../../../../plugins/${Plugin_Name}/resources/state/img/default_avatar.jpg` - const BotName = Version.name - const systime = formatDuration(os.uptime(), 'dd天hh小时mm分', false) - const calendar = moment().format('YYYY-MM-DD HH:mm:ss') - - const dataPromises = botList.map(async (i) => { - const bot = Bot[i] - if (!bot?.uin) return '' - - const avatar = bot.avatar || (Number(bot.uin) ? `https://q1.qlogo.cn/g?b=qq&s=0&nk=${bot.uin}` : defaultAvatar) - const nickname = bot.nickname || '未知' - const onlineStatus = status[bot.status] || '在线' - const platform = bot.apk ? `${bot.apk.display} v${bot.apk.version}` : bot.version?.version || '未知' - - const sent = await redis.get(`Yz:count:send:msg:bot:${bot.uin}:total`) || await redis.get('Yz:count:sendMsg:total') - const recv = await redis.get(`Yz:count:receive:msg:bot:${bot.uin}:total`) || bot.stat?.recv_msg_cnt - const screenshot = await redis.get(`Yz:count:send:image:bot:${bot.uin}:total`) || await redis.get('Yz:count:screenshot:total') - - const friendQuantity = bot.fl?.size || 0 - const groupQuantity = bot.gl?.size || 0 - const groupMemberQuantity = Array.from(bot.gml?.values() || []).reduce((acc, curr) => acc + curr.size, 0) - const runTime = formatDuration(Date.now() / 1000 - bot.stat?.start_time, 'dd天hh小时mm分', false) - const botVersion = bot.version ? `${bot.version.name}(${bot.version.id})${bot.apk ? ` ${bot.version.version}` : ''}` : `ICQQ(QQ) v${require('icqq/package.json').version}` - - return `
-
-
- -
-
-

${nickname}

-
-

${onlineStatus}(${platform}) | ${botVersion}

-

收${recv || 0} | 发${sent || 0} | 图片${screenshot || 0} | 好友${friendQuantity} | 群${groupQuantity} | 群员${groupMemberQuantity}

-

${BotName} 已运行 ${runTime} | 系统运行 ${systime}

-

${calendar} | Node.js ${process.version} | ${process.platform} ${process.arch}

-
-
-
` - }) - - const dataArray = await Promise.all(dataPromises) - return dataArray.join('') -} diff --git a/model/State.js b/model/State.js deleted file mode 100644 index 6e4d53e..0000000 --- a/model/State.js +++ /dev/null @@ -1,476 +0,0 @@ -import os from 'os' -import _ from 'lodash' -import fs from 'fs' -import { Config, Data } from '../components/index.js' -import request from '../lib/request/request.js' -import { execSync } from '../tools/index.js' - -export default new class { - constructor () { - this.si = null - this.osInfo = null - // 是否可以获取gpu - this.isGPU = false - // 网络 - this._network = null - // 读写速率 - this._fsStats = null - // 记录60条数据一分钟记录一次 - this.chartData = { - network: { - // 上行 - upload: [], - // 下行 - download: [] - }, - fsStats: { - // 读 - readSpeed: [], - // 写 - writeSpeed: [] - }, - // cpu - cpu: [], - // 内存 - ram: [], - // 主题 - echarts_theme: Data.readJSON('resources/state/theme_westeros.json') - } - - this.valueObject = { - networkStats: 'rx_sec,tx_sec,iface', - currentLoad: 'currentLoad', - mem: 'active', - fsStats: 'wx_sec,rx_sec' - } - - this.init() - } - - set network (value) { - if (_.isNumber(value[0]?.tx_sec) && _.isNumber(value[0]?.rx_sec)) { - this._network = value - this.addData(this.chartData.network.upload, [Date.now(), value[0].tx_sec]) - this.addData(this.chartData.network.download, [Date.now(), value[0].rx_sec]) - } - } - - get network () { - return this._network - } - - set fsStats (value) { - if (_.isNumber(value?.wx_sec) && _.isNumber(value?.rx_sec)) { - this._fsStats = value - this.addData(this.chartData.fsStats.writeSpeed, [Date.now(), value.wx_sec]) - this.addData(this.chartData.fsStats.readSpeed, [Date.now(), value.rx_sec]) - } - } - - get fsStats () { - return this._fsStats - } - - async initDependence () { - try { - this.si = await import('systeminformation') - this.osInfo = await this.si.osInfo() - return this.si - } catch (error) { - if (error.stack?.includes('Cannot find package')) { - logger.warn('--------椰奶依赖缺失--------') - logger.warn(`yenai-plugin 缺少依赖将无法使用 ${logger.yellow('椰奶状态')}`) - logger.warn(`如需使用请运行:${logger.red('pnpm add systeminformation -w')}`) - logger.warn('---------------------------') - logger.debug(decodeURI(error.stack)) - } else { - logger.error(`椰奶载入依赖错误:${logger.red('systeminformation')}`) - logger.error(decodeURI(error.stack)) - } - } - } - - async init () { - if (!await this.initDependence()) return - const { controllers } = await this.si.graphics() - // 初始化GPU获取 - if (controllers?.find(item => - item.memoryUsed && item.memoryFree && item.utilizationGpu) - ) { - this.isGPU = true - } - // 给有问题的用户关闭定时器 - if (!Config.state.statusTask) return - - if (Config.state.statusPowerShellStart) this.si.powerShellStart() - this.getData() - // 网速 - const Timer = setInterval(async () => { - let data = await this.getData() - if (_.isEmpty(data)) clearInterval(Timer) - }, 60000) - } - - async getData () { - let data = await this.si.get(this.valueObject) - _.forIn(data, (value, key) => { - if (_.isEmpty(value)) { - logger.debug(`获取${key}数据失败,停止获取对应数据`) - delete this.valueObject[key] - } - }) - let { - fsStats, - networkStats, - mem: { active }, - currentLoad: { currentLoad } - } = data - this.fsStats = fsStats - this.network = networkStats - if (_.isNumber(active)) { - this.addData(this.chartData.ram, [Date.now(), active]) - } - if (_.isNumber(currentLoad)) { - this.addData(this.chartData.cpu, [Date.now(), currentLoad]) - } - return data - } - - /** - * 向数组中添加数据,如果数组长度超过允许的最大值,则删除最早添加的数据 - * @param {Array} arr - 要添加数据的数组 - * @param {*} data - 要添加的新数据 - * @param {number} [maxLen] - 数组允许的最大长度,默认值为60 - * @returns {void} - */ - addData (arr, data, maxLen = 60) { - if (data === null || data === undefined) return - // 如果数组长度超过允许的最大值,删除第一个元素 - if (arr.length >= maxLen) { - _.pullAt(arr, 0) - } - // 添加新数据 - arr.push(data) - } - - /** - * 重试获取数据,直到成功或达到最大重试次数。 - * @param {Function} fetchFunc 获取数据的函数,返回一个Promise对象。 - * @param {Array} [params] 需要执行函数的参数数组 - * @param {number} [timerId] 定时器的id,用于在获取数据失败时停止定时器 - * @param {number} [maxRetryCount] 最大重试次数。 - * @param {number} [retryInterval] 两次重试之间的等待时间,单位为毫秒。。 - * @returns {Promise} 获取到的数据。如果达到最大重试次数且获取失败,则返回null。 - */ - async fetchDataWithRetry (fetchFunc, params = [], timerId, maxRetryCount = 3, retryInterval = 1000) { - let retryCount = 0 - let data = null - while (retryCount <= maxRetryCount) { - data = await fetchFunc(...params) - if (!_.isEmpty(data)) { - break - } - retryCount++ - if (retryCount > maxRetryCount && timerId) { - logger.debug(`获取${fetchFunc.name}数据失败,停止定时器`) - clearInterval(timerId) - break - } - await new Promise(resolve => setTimeout(resolve, retryInterval)) - } - return data - } - - /** - * 将文件大小从字节转化为可读性更好的格式,例如B、KB、MB、GB、TB。 - * @param {number} size - 带转化的字节数。 - * @param {boolean} [isByte] - 如果为 true,则最终的文件大小显示保留 B 的后缀. - * @param {boolean} [isSuffix] - 如果为 true,则在所得到的大小后面加上 kb、mb、gb、tb 等后缀. - * @returns {string} 文件大小格式转换后的字符串. - */ - getFileSize (size, isByte = true, isSuffix = true) { // 把字节转换成正常文件大小 - if (size == null || size == undefined) return 0 - let num = 1024.00 // byte - if (isByte && size < num) { - return size.toFixed(2) + 'B' - } - if (size < Math.pow(num, 2)) { - return (size / num).toFixed(2) + `K${isSuffix ? 'b' : ''}` - } // kb - if (size < Math.pow(num, 3)) { - return (size / Math.pow(num, 2)).toFixed(2) + `M${isSuffix ? 'b' : ''}` - } // M - if (size < Math.pow(num, 4)) { - return (size / Math.pow(num, 3)).toFixed(2) + 'G' - } // G - return (size / Math.pow(num, 4)).toFixed(2) + 'T' // T - } - - /** - * 圆形进度条渲染 - * @param {number} res 百分比小数 - * @returns {*} css样式 - */ - Circle (res) { - let num = (res * 360).toFixed(0) - let color = 'var(--low-color)' - if (res >= 0.9) { - color = 'var(--high-color)' - } else if (res >= 0.8) { - color = 'var(--medium-color)' - } - let leftCircle = `style="transform:rotate(-180deg);background:${color};"` - let rightCircle = `style="transform:rotate(360deg);background:${color};"` - if (num > 180) { - leftCircle = `style="transform:rotate(${num}deg);background:${color};"` - } else { - rightCircle = `style="transform:rotate(-${180 - num}deg);background:${color};"` - } - return { leftCircle, rightCircle } - } - - /** 获取nodejs内存情况 */ - getNodeInfo () { - let memory = process.memoryUsage() - // 总共 - let rss = this.getFileSize(memory.rss) - // 堆 - let heapTotal = this.getFileSize(memory.heapTotal) - // 栈 - let heapUsed = this.getFileSize(memory.heapUsed) - // 占用率 - let occupy = (memory.rss / (os.totalmem() - os.freemem())).toFixed(2) - return { - ...this.Circle(occupy), - inner: Math.round(occupy * 100) + '%', - title: 'Node', - info: [ - `总 ${rss}`, - `堆 ${heapTotal}`, - `栈 ${heapUsed}` - ] - } - } - - /** 获取当前内存占用 */ - getMemUsage () { - // 内存使用率 - let MemUsage = (1 - os.freemem() / os.totalmem()).toFixed(2) - // 空闲内存 - let freemem = this.getFileSize(os.freemem()) - // 总共内存 - let totalmem = this.getFileSize(os.totalmem()) - // 使用内存 - let Usingmemory = this.getFileSize((os.totalmem() - os.freemem())) - - return { - ...this.Circle(MemUsage), - inner: Math.round(MemUsage * 100) + '%', - title: 'RAM', - info: [ - `总共 ${totalmem}`, - `已用 ${Usingmemory}`, - `空闲 ${freemem}` - ] - } - } - - /** 获取CPU占用 */ - async getCpuInfo () { - let { currentLoad: { currentLoad }, cpuCurrentSpeed } = await this.si.get({ - currentLoad: 'currentLoad', - cpuCurrentSpeed: 'max,avg' - }) - if (currentLoad == null || currentLoad == undefined) return false - // 核心 - let cores = os.cpus() - // cpu制造者 - let cpuModel = cores[0]?.model.slice(0, cores[0]?.model.indexOf(' ')) || '' - return { - ...this.Circle(currentLoad / 100), - inner: Math.round(currentLoad) + '%', - title: 'CPU', - info: [ - `${cpuModel} ${cores.length}核 ${this.osInfo?.arch}`, - `平均${cpuCurrentSpeed.avg}GHz`, - `最大${cpuCurrentSpeed.max}GHz` - ] - - } - } - - /** 获取GPU占用 */ - async getGPU () { - if (!this.isGPU) return false - try { - const { controllers } = await this.si.graphics() - let graphics = controllers?.find(item => - item.memoryUsed && item.memoryFree && item.utilizationGpu - ) - if (!graphics) { - logger.warn('[Yenai-plugin][state]状态GPU数据异常:\n', controllers) - return false - } - let { - vendor, temperatureGpu, utilizationGpu, - memoryTotal, memoryUsed, powerDraw - } = graphics - temperatureGpu && (temperatureGpu = temperatureGpu + '℃') - powerDraw && (powerDraw = powerDraw + 'W') - return { - ...this.Circle(utilizationGpu / 100), - inner: Math.round(utilizationGpu) + '%', - title: 'GPU', - info: [ - `${vendor} ${temperatureGpu} ${powerDraw}`, - `总共 ${(memoryTotal / 1024).toFixed(2)}G`, - `已用 ${(memoryUsed / 1024).toFixed(2)}G` - ] - } - } catch (e) { - logger.warn('[Yenai-Plugin][State] 获取GPU失败') - return false - } - } - - /** - * 获取硬盘 - * @returns {*} - */ - async getFsSize () { - // 去重 - let HardDisk = _.uniqWith(await this.si.fsSize(), - (a, b) => - a.used === b.used && a.size === b.size && a.use === b.use && a.available === b.available - ) - .filter(item => item.size && item.used && item.available && item.use) - // 为空返回false - if (_.isEmpty(HardDisk)) return false - // 数值转换 - return HardDisk.map(item => { - item.used = this.getFileSize(item.used) - item.size = this.getFileSize(item.size) - item.use = Math.round(item.use) - item.color = 'var(--low-color)' - if (item.use >= 90) { - item.color = 'var(--high-color)' - } else if (item.use >= 70) { - item.color = 'var(--medium-color)' - } - return item - }) - } - - /** - * 获取FastFetch - * @param e - */ - async getFastFetch (e) { - if (process.platform == 'win32' && !/pro/.test(e.msg)) return '' - let ret = await execSync('bash plugins/yenai-plugin/resources/state/state.sh') - if (ret.error) { - e.reply(`❎ 请检查是否使用git bash启动Yunzai-bot\n错误信息:${ret.stderr}`) - return '' - } - return ret.stdout.trim() - } - - // 获取读取速率 - get DiskSpeed () { - if (!this.fsStats || - this.fsStats.rx_sec == null || - this.fsStats.wx_sec == null - ) { - return false - } - return { - rx_sec: this.getFileSize(this.fsStats.rx_sec, false, false), - wx_sec: this.getFileSize(this.fsStats.wx_sec, false, false) - } - } - - /** - * 获取网速 - * @returns {object} - */ - get getnetwork () { - let network = _.cloneDeep(this.network)?.[0] - if (!network || network.rx_sec == null || network.tx_sec == null) { - return false - } - network.rx_sec = this.getFileSize(network.rx_sec, false, false) - network.tx_sec = this.getFileSize(network.tx_sec, false, false) - // return network - return { - first: network.iface, - tail: `↑${network.tx_sec}/s | ↓${network.rx_sec}/s` - } - } - - /** - * 取插件包 - * @returns {*} 插件包数量 - */ - get getPluginNum () { - let str = './plugins' - let arr = fs.readdirSync(str) - let plugin = [] - arr.forEach((val) => { - let ph = fs.statSync(str + '/' + val) - if (ph.isDirectory()) { - plugin.push(val) - } - }) - let del = ['example', 'genshin', 'other', 'system', 'bin'] - plugin = plugin.filter(item => !del.includes(item)) - const plugins = plugin?.length || 0 - const js = fs.readdirSync('./plugins/example')?.filter(item => item.includes('.js'))?.length || 0 - // return { - // plugins: plugin?.length || 0, - // js: fs.readdirSync('./plugins/example')?.filter(item => item.includes('.js'))?.length || 0 - // } - return { - first: '插件', - tail: `${plugins} plugin | ${js} js` - } - } - - async getNetworkLatency (url, timeoutTime = 5000) { - const AbortController = globalThis.AbortController || await import('abort-controller') - - const controller = new AbortController() - const timeout = setTimeout(() => { - controller.abort() - }, timeoutTime) - try { - const startTime = Date.now() - let { status } = await request.get(url, { signal: controller.signal }) - const endTime = Date.now() - let delay = endTime - startTime - let color = ''; let statusColor = '' - if (delay > 2000) { - color = '#F44336' - } else if (delay > 500) { - color = '#d68100' - } else { - color = '#188038' - } - if (status >= 500) { - statusColor = '#9C27B0' - } else if (status >= 400) { - statusColor = '#F44336' - } else if (status >= 300) { - statusColor = '#FF9800' - } else if (status >= 200) { - statusColor = '#188038' - } else if (status >= 100) { - statusColor = '#03A9F4' - } - return `${status} | ${delay}ms` - } catch { - return "timeout" - } finally { - clearTimeout(timeout) - } - } -}() diff --git a/model/State/BotState.js b/model/State/BotState.js new file mode 100644 index 0000000..062faf8 --- /dev/null +++ b/model/State/BotState.js @@ -0,0 +1,54 @@ +import { formatDuration } from '../../tools/index.js' +import { Version, Plugin_Name } from '../../components/index.js' +import moment from 'moment' +import os from 'os' +import { status } from '../../constants/other.js' +import { createRequire } from 'module' +const require = createRequire(import.meta.url) + +export default async function getBotState (botList) { + const defaultAvatar = `../../../../../plugins/${Plugin_Name}/resources/state/img/default_avatar.jpg` + const BotName = Version.name + const systime = formatDuration(os.uptime(), 'dd天hh小时mm分', false) + const calendar = moment().format('YYYY-MM-DD HH:mm:ss') + + const dataPromises = botList.map(async (i) => { + const bot = Bot[i] + if (!bot?.uin) return '' + + const avatar = bot.avatar || (Number(bot.uin) ? `https://q1.qlogo.cn/g?b=qq&s=0&nk=${bot.uin}` : defaultAvatar) + const nickname = bot.nickname || '未知' + const onlineStatus = status[bot.status] || '在线' + const platform = bot.apk ? `${bot.apk.display} v${bot.apk.version}` : bot.version?.version || '未知' + + const sent = await redis.get(`Yz:count:send:msg:bot:${bot.uin}:total`) || await redis.get('Yz:count:sendMsg:total') + const recv = await redis.get(`Yz:count:receive:msg:bot:${bot.uin}:total`) || bot.stat?.recv_msg_cnt + const screenshot = await redis.get(`Yz:count:send:image:bot:${bot.uin}:total`) || await redis.get('Yz:count:screenshot:total') + + const friendQuantity = bot.fl?.size || 0 + const groupQuantity = bot.gl?.size || 0 + const groupMemberQuantity = Array.from(bot.gml?.values() || []).reduce((acc, curr) => acc + curr.size, 0) + const runTime = formatDuration(Date.now() / 1000 - bot.stat?.start_time, 'dd天hh小时mm分', false) + const botVersion = bot.version ? `${bot.version.name}(${bot.version.id})${bot.apk ? ` ${bot.version.version}` : ''}` : `ICQQ(QQ) v${require('icqq/package.json').version}` + + return `
+
+
+ +
+
+

${nickname}

+
+

${onlineStatus}(${platform}) | ${botVersion}

+

收${recv || 0} | 发${sent || 0} | 图片${screenshot || 0} | 好友${friendQuantity} | 群${groupQuantity} | 群员${groupMemberQuantity}

+

${BotName} 已运行 ${runTime} | 系统运行 ${systime}

+

${calendar} | Node.js ${process.version} | ${process.platform} ${process.arch}

+
+
+
` + }) + + const dataArray = await Promise.all(dataPromises) + return dataArray.join('') +} diff --git a/model/State/CPU.js b/model/State/CPU.js new file mode 100644 index 0000000..66b47ff --- /dev/null +++ b/model/State/CPU.js @@ -0,0 +1,27 @@ +import os from 'os' +import { si, osInfo } from './index.js' +import { Circle } from './utils.js' + +/** 获取CPU占用 */ +export default async function getCpuInfo () { + let { currentLoad: { currentLoad }, cpuCurrentSpeed } = await si.get({ + currentLoad: 'currentLoad', + cpuCurrentSpeed: 'max,avg' + }) + if (currentLoad == null || currentLoad == undefined) return false + // 核心 + let cores = os.cpus() + // cpu制造者 + let cpuModel = cores[0]?.model.slice(0, cores[0]?.model.indexOf(' ')) || '' + return { + ...Circle(currentLoad / 100), + inner: Math.round(currentLoad) + '%', + title: 'CPU', + info: [ + `${cpuModel} ${cores.length}核 ${osInfo?.arch}`, + `平均${cpuCurrentSpeed.avg}GHz`, + `最大${cpuCurrentSpeed.max}GHz` + ] + + } +} diff --git a/model/State/DependencyChecker.js b/model/State/DependencyChecker.js new file mode 100644 index 0000000..2fc2836 --- /dev/null +++ b/model/State/DependencyChecker.js @@ -0,0 +1,24 @@ +export let si = false +export let osInfo = null + +export async function initDependence () { + if (si) return si + try { + si = await import('systeminformation') + osInfo = await si.osInfo() + return si + } catch (error) { + if (error.stack?.includes('Cannot find package')) { + logger.warn('--------椰奶依赖缺失--------') + logger.warn(`yenai-plugin 缺少依赖将无法使用 ${logger.yellow('椰奶状态')}`) + logger.warn(`如需使用请运行:${logger.red('pnpm add systeminformation -w')}`) + logger.warn('---------------------------') + logger.debug(decodeURI(error.stack)) + } else { + logger.error(`椰奶载入依赖错误:${logger.red('systeminformation')}`) + logger.error(decodeURI(error.stack)) + } + } +} + +await initDependence() diff --git a/model/State/FastFetch.js b/model/State/FastFetch.js new file mode 100644 index 0000000..116b191 --- /dev/null +++ b/model/State/FastFetch.js @@ -0,0 +1,15 @@ +import { execSync } from '../../tools/index.js' + +/** + * 获取FastFetch + * @param e + */ +export default async function getFastFetch (e) { + if (process.platform == 'win32' && !/pro/.test(e.msg)) return '' + let ret = await execSync('bash plugins/yenai-plugin/resources/state/state.sh') + if (ret.error) { + e.reply(`❎ 请检查是否使用git bash启动Yunzai-bot\n错误信息:${ret.stderr}`) + return '' + } + return ret.stdout.trim() +} diff --git a/model/State/FsSize.js b/model/State/FsSize.js new file mode 100644 index 0000000..548f771 --- /dev/null +++ b/model/State/FsSize.js @@ -0,0 +1,31 @@ +import _ from 'lodash' +import { getFileSize } from './utils.js' +import { si } from './index.js' + +/** + * 获取硬盘 + * @returns {*} + */ +export default async function getFsSize () { + // 去重 + let HardDisk = _.uniqWith(await si.fsSize(), + (a, b) => + a.used === b.used && a.size === b.size && a.use === b.use && a.available === b.available + ) + .filter(item => item.size && item.used && item.available && item.use) + // 为空返回false + if (_.isEmpty(HardDisk)) return false + // 数值转换 + return HardDisk.map(item => { + item.used = getFileSize(item.used) + item.size = getFileSize(item.size) + item.use = Math.round(item.use) + item.color = 'var(--low-color)' + if (item.use >= 90) { + item.color = 'var(--high-color)' + } else if (item.use >= 70) { + item.color = 'var(--medium-color)' + } + return item + }) +} diff --git a/model/State/GPU.js b/model/State/GPU.js new file mode 100644 index 0000000..53aba87 --- /dev/null +++ b/model/State/GPU.js @@ -0,0 +1,50 @@ +import { Circle } from './utils.js' +import { si } from './index.js' +import { initDependence } from './DependencyChecker.js' + +let isGPU = false; + +(async function initGetIsGPU () { + if (!await initDependence()) return + const { controllers } = await si.graphics() + // 初始化GPU获取 + if (controllers?.find(item => + item.memoryUsed && item.memoryFree && item.utilizationGpu) + ) { + isGPU = true + } +})() + +/** 获取GPU占用 */ +export default async function getGPU () { + if (!isGPU) return false + try { + const { controllers } = await si.graphics() + let graphics = controllers?.find(item => + item.memoryUsed && item.memoryFree && item.utilizationGpu + ) + if (!graphics) { + logger.warn('[Yenai-plugin][state]状态GPU数据异常:\n', controllers) + return false + } + let { + vendor, temperatureGpu, utilizationGpu, + memoryTotal, memoryUsed, powerDraw + } = graphics + temperatureGpu && (temperatureGpu = temperatureGpu + '℃') + powerDraw && (powerDraw = powerDraw + 'W') + return { + ...Circle(utilizationGpu / 100), + inner: Math.round(utilizationGpu) + '%', + title: 'GPU', + info: [ + `${vendor} ${temperatureGpu} ${powerDraw}`, + `总共 ${(memoryTotal / 1024).toFixed(2)}G`, + `已用 ${(memoryUsed / 1024).toFixed(2)}G` + ] + } + } catch (e) { + logger.warn('[Yenai-Plugin][State] 获取GPU失败') + return false + } +} diff --git a/model/State/Monitor.js b/model/State/Monitor.js new file mode 100644 index 0000000..540023f --- /dev/null +++ b/model/State/Monitor.js @@ -0,0 +1,140 @@ +import { Config, Data } from '../../components/index.js' +import _ from 'lodash' +import { si } from './index.js' +import { initDependence } from './DependencyChecker.js' +import { addData, getFileSize } from './utils.js' + +export default new class monitor { + constructor () { + // 网络 + this._network = null + // 读写速率 + this._fsStats = null + // 记录60条数据一分钟记录一次 + this.chartData = { + network: { + // 上行 + upload: [], + // 下行 + download: [] + }, + fsStats: { + // 读 + readSpeed: [], + // 写 + writeSpeed: [] + }, + // cpu + cpu: [], + // 内存 + ram: [], + // 主题 + echarts_theme: Data.readJSON('resources/state/theme_westeros.json'), + backdrop: Config.state.backdrop + } + this.valueObject = { + networkStats: 'rx_sec,tx_sec,iface', + currentLoad: 'currentLoad', + mem: 'active', + fsStats: 'wx_sec,rx_sec' + } + + this.init() + } + + set network (value) { + if (_.isNumber(value[0]?.tx_sec) && _.isNumber(value[0]?.rx_sec)) { + this._network = value + addData(this.chartData.network.upload, [Date.now(), value[0].tx_sec]) + addData(this.chartData.network.download, [Date.now(), value[0].rx_sec]) + } + } + + get network () { + return this._network + } + + set fsStats (value) { + if (_.isNumber(value?.wx_sec) && _.isNumber(value?.rx_sec)) { + this._fsStats = value + addData(this.chartData.fsStats.writeSpeed, [Date.now(), value.wx_sec]) + addData(this.chartData.fsStats.readSpeed, [Date.now(), value.rx_sec]) + } + } + + get fsStats () { + return this._fsStats + } + + async init () { + if (!await initDependence()) return + + // 给有问题的用户关闭定时器 + if (!Config.state.statusTask) return + + if (Config.state.statusPowerShellStart) si.powerShellStart() + this.getData() + // 网速 + const Timer = setInterval(async () => { + let data = await this.getData() + if (_.isEmpty(data)) clearInterval(Timer) + }, 60000) + } + + async getData () { + let data = await si.get(this.valueObject) + _.forIn(data, (value, key) => { + if (_.isEmpty(value)) { + logger.debug(`获取${key}数据失败,停止获取对应数据`) + delete this.valueObject[key] + } + }) + let { + fsStats, + networkStats, + mem: { active }, + currentLoad: { currentLoad } + } = data + this.fsStats = fsStats + this.network = networkStats + if (_.isNumber(active)) { + addData(this.chartData.ram, [Date.now(), active]) + } + if (_.isNumber(currentLoad)) { + addData(this.chartData.cpu, [Date.now(), currentLoad]) + } + return data + } + + // 获取读取速率 + get DiskSpeed () { + if (!this.fsStats || + this.fsStats.rx_sec == null || + this.fsStats.wx_sec == null + ) { + return false + } + return { + rx_sec: getFileSize(this.fsStats.rx_sec, false, false), + wx_sec: getFileSize(this.fsStats.wx_sec, false, false) + } + } + + /** + * 获取网速 + * @returns {object} + */ + get getNetwork () { + let network = _.cloneDeep(this.network)?.[0] + if (!network || network.rx_sec == null || network.tx_sec == null) { + return false + } + network.rx_sec = getFileSize(network.rx_sec, false, false) + network.tx_sec = getFileSize(network.tx_sec, false, false) + // return network + return { + first: network.iface, + tail: `↑${network.tx_sec}/s | ↓${network.rx_sec}/s` + } + } +}() diff --git a/model/State/NetworkLatency.js b/model/State/NetworkLatency.js new file mode 100644 index 0000000..a9528d7 --- /dev/null +++ b/model/State/NetworkLatency.js @@ -0,0 +1,61 @@ +import request from '../../lib/request/request.js' +import { Config } from '../../components/index.js' + +export default function getNetworTestList () { + let { psTestSites, psTestTimeout } = Config.state + if (psTestSites) { + let psTest = psTestSites?.map(i => getNetworkLatency(i.url, psTestTimeout).then(res => { + return { + first: i.name, + tail: res + } + })) + return Promise.all(psTest) + } else { + return [] + } +} +/** + * 网络测试 + * @param {string} url 测试的url + * @param {number} [timeoutTime] 超时时间 + * @returns {string} + */ +async function getNetworkLatency (url, timeoutTime = 5000) { + const AbortController = globalThis.AbortController || await import('abort-controller') + + const controller = new AbortController() + const timeout = setTimeout(() => { + controller.abort() + }, timeoutTime) + try { + const startTime = Date.now() + let { status } = await request.get(url, { signal: controller.signal }) + const endTime = Date.now() + let delay = endTime - startTime + let color = ''; let statusColor = '' + if (delay > 2000) { + color = '#F44336' + } else if (delay > 500) { + color = '#d68100' + } else { + color = '#188038' + } + if (status >= 500) { + statusColor = '#9C27B0' + } else if (status >= 400) { + statusColor = '#F44336' + } else if (status >= 300) { + statusColor = '#FF9800' + } else if (status >= 200) { + statusColor = '#188038' + } else if (status >= 100) { + statusColor = '#03A9F4' + } + return `${status} | ${delay}ms` + } catch { + return "timeout" + } finally { + clearTimeout(timeout) + } +} diff --git a/model/State/NodeInfo.js b/model/State/NodeInfo.js new file mode 100644 index 0000000..d742574 --- /dev/null +++ b/model/State/NodeInfo.js @@ -0,0 +1,25 @@ +import { getFileSize, Circle } from './utils.js' +import os from 'os' + +/** 获取nodejs内存情况 */ +export default function getNodeInfo () { + let memory = process.memoryUsage() + // 总共 + let rss = getFileSize(memory.rss) + // 堆 + let heapTotal = getFileSize(memory.heapTotal) + // 栈 + let heapUsed = getFileSize(memory.heapUsed) + // 占用率 + let occupy = (memory.rss / (os.totalmem() - os.freemem())).toFixed(2) + return { + ...Circle(occupy), + inner: Math.round(occupy * 100) + '%', + title: 'Node', + info: [ + `总 ${rss}`, + `堆 ${heapTotal}`, + `栈 ${heapUsed}` + ] + } +} diff --git a/model/State/PluginNum.js b/model/State/PluginNum.js new file mode 100644 index 0000000..5e7b9c3 --- /dev/null +++ b/model/State/PluginNum.js @@ -0,0 +1,25 @@ +import fs from 'fs' + +export default function getPluginNum () { + let str = './plugins' + let arr = fs.readdirSync(str) + let plugin = [] + arr.forEach((val) => { + let ph = fs.statSync(str + '/' + val) + if (ph.isDirectory()) { + plugin.push(val) + } + }) + let del = ['example', 'genshin', 'other', 'system', 'bin'] + plugin = plugin.filter(item => !del.includes(item)) + const plugins = plugin?.length || 0 + const js = fs.readdirSync('./plugins/example')?.filter(item => item.includes('.js'))?.length || 0 + // return { + // plugins: plugin?.length || 0, + // js: fs.readdirSync('./plugins/example')?.filter(item => item.includes('.js'))?.length || 0 + // } + return { + first: '插件', + tail: `${plugins} plugin | ${js} js` + } +} diff --git a/model/State/RAM.js b/model/State/RAM.js new file mode 100644 index 0000000..4e0d36a --- /dev/null +++ b/model/State/RAM.js @@ -0,0 +1,25 @@ +import { getFileSize, Circle } from './utils.js' +import os from 'os' + +/** 获取当前内存占用 */ +export default function getMemUsage () { + // 内存使用率 + let MemUsage = (1 - os.freemem() / os.totalmem()).toFixed(2) + // 空闲内存 + let freemem = getFileSize(os.freemem()) + // 总共内存 + let totalmem = getFileSize(os.totalmem()) + // 使用内存 + let Usingmemory = getFileSize((os.totalmem() - os.freemem())) + + return { + ...Circle(MemUsage), + inner: Math.round(MemUsage * 100) + '%', + title: 'RAM', + info: [ + `总共 ${totalmem}`, + `已用 ${Usingmemory}`, + `空闲 ${freemem}` + ] + } +} diff --git a/model/State/index.js b/model/State/index.js new file mode 100644 index 0000000..134b042 --- /dev/null +++ b/model/State/index.js @@ -0,0 +1,73 @@ +import _ from 'lodash' +import common from '../../lib/common/common.js' +import getBotState from './BotState.js' +import getCPU from './CPU.js' +import { osInfo, si } from './DependencyChecker.js' +import getFastFetch from './FastFetch.js' +import getFsSize from './FsSize.js' +import getGPU from './GPU.js' +import Monitor from './Monitor.js' +import getNetworTestList from './NetworkLatency.js' +import getNodeInfo from './NodeInfo.js' +import getPluginNum from './PluginNum.js' +import getRAM from './RAM.js' + +export { osInfo, si } + +export async function getData (e) { + // 可视化数据 + let visualData = _.compact(await Promise.all([ + // CPU板块 + getCPU(), + // 内存板块 + getRAM(), + // GPU板块 + getGPU(), + // Node板块 + getNodeInfo() + ])) + let promiseTaskList = [ + getFastFetch(e), + getFsSize() + ] + + let NetworTestList = getNetworTestList() + promiseTaskList.push(NetworTestList) + + let [FastFetch, HardDisk, psTest] = await Promise.all(promiseTaskList) + /** bot列表 */ + let BotList = [e.self_id] + + if (e.msg.includes('pro')) { + if (Array.isArray(Bot?.uin)) { + BotList = Bot.uin + } else if (Bot?.adapter && Bot.adapter.includes(e.self_id)) { + BotList = Bot.adapter + } + } + return { + BotStatus: await getBotState(BotList), + chartData: JSON.stringify(common.checkIfEmpty(Monitor.chartData, ['echarts_theme', 'cpu', 'ram']) ? undefined : Monitor.chartData), + visualData, + otherInfo: _getOtherInfo(), + psTest, + FastFetch, + HardDisk, + // 硬盘速率 + fsStats: Monitor.DiskSpeed + } +} + +function _getOtherInfo () { + let otherInfo = [] + // 其他信息 + otherInfo.push({ + first: '系统', + tail: osInfo?.distro + }) + // 网络 + otherInfo.push(Monitor.getNetwork) + // 插件数量 + otherInfo.push(getPluginNum()) + return otherInfo +} diff --git a/model/State/utils.js b/model/State/utils.js new file mode 100644 index 0000000..5661682 --- /dev/null +++ b/model/State/utils.js @@ -0,0 +1,66 @@ +import _ from 'lodash' + +/** + * 向数组中添加数据,如果数组长度超过允许的最大值,则删除最早添加的数据 + * @param {Array} arr - 要添加数据的数组 + * @param {*} data - 要添加的新数据 + * @param {number} [maxLen] - 数组允许的最大长度,默认值为60 + * @returns {void} + */ +export function addData (arr, data, maxLen = 60) { + if (data === null || data === undefined) return + // 如果数组长度超过允许的最大值,删除第一个元素 + if (arr.length >= maxLen) { + _.pullAt(arr, 0) + } + // 添加新数据 + arr.push(data) +} + +/** + * 将文件大小从字节转化为可读性更好的格式,例如B、KB、MB、GB、TB。 + * @param {number} size - 带转化的字节数。 + * @param {boolean} [isByte] - 如果为 true,则最终的文件大小显示保留 B 的后缀. + * @param {boolean} [isSuffix] - 如果为 true,则在所得到的大小后面加上 kb、mb、gb、tb 等后缀. + * @returns {string} 文件大小格式转换后的字符串. + */ +export function getFileSize (size, isByte = true, isSuffix = true) { // 把字节转换成正常文件大小 + if (size == null || size == undefined) return 0 + let num = 1024.00 // byte + if (isByte && size < num) { + return size.toFixed(2) + 'B' + } + if (size < Math.pow(num, 2)) { + return (size / num).toFixed(2) + `K${isSuffix ? 'b' : ''}` + } // kb + if (size < Math.pow(num, 3)) { + return (size / Math.pow(num, 2)).toFixed(2) + `M${isSuffix ? 'b' : ''}` + } // M + if (size < Math.pow(num, 4)) { + return (size / Math.pow(num, 3)).toFixed(2) + 'G' + } // G + return (size / Math.pow(num, 4)).toFixed(2) + 'T' // T +} + +/** + * 圆形进度条渲染 + * @param {number} res 百分比小数 + * @returns {*} css样式 + */ +export function Circle (res) { + let num = (res * 360).toFixed(0) + let color = 'var(--low-color)' + if (res >= 0.9) { + color = 'var(--high-color)' + } else if (res >= 0.8) { + color = 'var(--medium-color)' + } + let leftCircle = `style="transform:rotate(-180deg);background:${color};"` + let rightCircle = `style="transform:rotate(360deg);background:${color};"` + if (num > 180) { + leftCircle = `style="transform:rotate(${num}deg);background:${color};"` + } else { + rightCircle = `style="transform:rotate(-${180 - num}deg);background:${color};"` + } + return { leftCircle, rightCircle } +} diff --git a/tools/index.js b/tools/index.js index bd94e03..d0ceedc 100644 --- a/tools/index.js +++ b/tools/index.js @@ -26,4 +26,4 @@ async function execSync (cmd) { }) } -export { cronValidate, formatDuration, sagiri, translateChinaNum, uploadRecord, sleep, execSync } \ No newline at end of file +export { cronValidate, formatDuration, sagiri, translateChinaNum, uploadRecord, sleep, execSync }