From d59f2553f4472ffcc49103b172136f5849e73311 Mon Sep 17 00:00:00 2001
From: yeyang <746659424@qq.com>
Date: Tue, 26 Mar 2024 01:46:19 +0800
Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=E5=88=86=E7=A6=BB?=
=?UTF-8?q?=E7=8A=B6=E6=80=81=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/state.js | 135 +--------
model/State.js | 476 -------------------------------
model/State/BotState.js | 54 ++++
model/State/CPU.js | 27 ++
model/State/DependencyChecker.js | 24 ++
model/State/FastFetch.js | 15 +
model/State/FsSize.js | 31 ++
model/State/GPU.js | 50 ++++
model/State/Monitor.js | 140 +++++++++
model/State/NetworkLatency.js | 61 ++++
model/State/NodeInfo.js | 25 ++
model/State/PluginNum.js | 25 ++
model/State/RAM.js | 25 ++
model/State/index.js | 73 +++++
model/State/utils.js | 66 +++++
tools/index.js | 2 +-
16 files changed, 629 insertions(+), 600 deletions(-)
delete mode 100644 model/State.js
create mode 100644 model/State/BotState.js
create mode 100644 model/State/CPU.js
create mode 100644 model/State/DependencyChecker.js
create mode 100644 model/State/FastFetch.js
create mode 100644 model/State/FsSize.js
create mode 100644 model/State/GPU.js
create mode 100644 model/State/Monitor.js
create mode 100644 model/State/NetworkLatency.js
create mode 100644 model/State/NodeInfo.js
create mode 100644 model/State/PluginNum.js
create mode 100644 model/State/RAM.js
create mode 100644 model/State/index.js
create mode 100644 model/State/utils.js
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 `
-
-
-

-
-
-
-
`
- })
-
- 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 `
+
+
+

+
+
+
+
`
+ })
+
+ 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 }