From 503ec55dfca25203adeb526a0eca496ce0a4231a Mon Sep 17 00:00:00 2001 From: zhiyu <542716863@qq.com> Date: Fri, 10 May 2024 18:54:54 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20=E6=96=B0=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E9=80=82=E9=85=8D=E6=8B=89=E6=A0=BC=E6=9C=97=E6=97=A5=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 适配拉格朗日上传文件,解决部分用户使用拉格朗日无法观看视频问题 2. 新增switchers,存储一些开关例如:海外解析、拉格朗日判断等 3. 新增ws用于连接拉格朗日 4. 新增配置文件配置项`lagrangeForwardWebSocket`,用于配置拉格朗日正向地址 --- apps/switchers.js | 108 +++++++++++++++++++++++++++++ apps/tools.js | 99 ++++++++++----------------- config/tools.yaml | 4 +- config/version.yaml | 3 +- constants/constant.js | 6 ++ guoba.support.js | 11 +++ package.json | 3 +- utils/lagrange-adapter.js | 138 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 306 insertions(+), 66 deletions(-) create mode 100644 apps/switchers.js create mode 100644 utils/lagrange-adapter.js diff --git a/apps/switchers.js b/apps/switchers.js new file mode 100644 index 0000000..048e507 --- /dev/null +++ b/apps/switchers.js @@ -0,0 +1,108 @@ +import config from "../model/index.js"; +import { REDIS_YUNZAI_ISOVERSEA, REDIS_YUNZAI_LAGRANGE } from "../constants/constant.js"; +import { deleteFolderRecursive, readCurrentDir } from "../utils/file.js"; + +export class switchers extends plugin { + constructor() { + super({ + name: "R插件开关类", + dsc: "内含一些和Redis相关的开关类", + event: "message.group", + priority: 300, + rule: [ + { + reg: "^#设置海外解析$", + fnc: "setOversea", + permission: "master", + }, + { + reg: "^#设置拉格朗日$", + fnc: "setLagrange", + permission: "master", + }, + { + reg: "^清理data垃圾$", + fnc: "clearTrash", + permission: "master", + }, + ] + }); + // 配置文件 + this.toolsConfig = config.getConfig("tools"); + // 视频保存路径 + this.defaultPath = this.toolsConfig.defaultPath; + } + + /** + * 设置海外模式 + * @param e + * @returns {Promise} + */ + async setOversea(e) { + // 查看当前设置 + let os; + if ((await redis.exists(REDIS_YUNZAI_ISOVERSEA))) { + os = JSON.parse(await redis.get(REDIS_YUNZAI_ISOVERSEA)).os; + } + // 设置 + os = ~os + await redis.set( + REDIS_YUNZAI_ISOVERSEA, + JSON.stringify({ + os: os, + }), + ); + e.reply(`当前服务器:${ os ? '海外服务器' : '国内服务器' }`) + return true; + } + + async setLagrange(e) { + // 查看当前设置 + let driver; + if ((await redis.exists(REDIS_YUNZAI_LAGRANGE))) { + driver = JSON.parse(await redis.get(REDIS_YUNZAI_LAGRANGE)).driver; + } + // 设置 + driver = ~driver + await redis.set( + REDIS_YUNZAI_LAGRANGE, + JSON.stringify({ + driver: driver, + }), + ); + e.reply(`当前驱动:${ driver ? '拉格朗日' : '其他驱动' }`) + return true; + } + + /** + * 清理垃圾文件 + * @param e + * @returns {Promise} + */ + async clearTrash(e) { + const dataDirectory = "./data/"; + + // 删除Yunzai遗留问题的合成视频垃圾文件 + try { + const files = await readCurrentDir(dataDirectory); + let dataClearFileLen = 0; + for (const file of files) { + // 如果文件名符合规则,执行删除操作 + if (/^[0-9a-f]{32}$/.test(file)) { + await fs.promises.unlink(dataDirectory + file); + dataClearFileLen++; + } + } + // 删除R插件临时文件 + const rTempFileLen = await deleteFolderRecursive(this.defaultPath) + e.reply( + `数据统计:\n` + + `- 当前清理了${ dataDirectory }下总计:${ dataClearFileLen } 个垃圾文件\n` + + `- 当前清理了${ this.toolsConfig.defaultPath }下文件夹:${ rTempFileLen } 个群的所有临时文件` + ); + } catch (err) { + logger.error(err); + await e.reply("清理失败,重试或者手动清理即可"); + } + } +} \ No newline at end of file diff --git a/apps/tools.js b/apps/tools.js index 7883d4d..e7aa24a 100644 --- a/apps/tools.js +++ b/apps/tools.js @@ -8,7 +8,7 @@ import _ from "lodash"; import tunnel from "tunnel"; import HttpProxyAgent from "https-proxy-agent"; import { exec, execSync } from "child_process"; -import { checkAndRemoveFile, deleteFolderRecursive, mkdirIfNotExists, readCurrentDir } from "../utils/file.js"; +import { checkAndRemoveFile, mkdirIfNotExists } from "../utils/file.js"; import { downloadBFile, getBiliAudio, @@ -26,6 +26,7 @@ import { DIVIDING_LINE, douyinTypeMap, REDIS_YUNZAI_ISOVERSEA, + REDIS_YUNZAI_LAGRANGE, transMap, TWITTER_BEARER_TOKEN, XHS_NO_WATERMARK_HEADER, @@ -65,6 +66,7 @@ import { processTikTokUrl } from "../utils/tiktok.js"; import { getDS } from "../utils/mihoyo.js"; import GeneralLinkAdapter from "../utils/general-link-adapter.js"; import { mid2id } from "../utils/weibo.js"; +import { LagrangeAdapter } from "../utils/lagrange-adapter.js"; export class tools extends plugin { /** @@ -124,16 +126,6 @@ export class tools extends plugin { reg: "(instagram.com)", fnc: "instagram", }, - { - reg: "^清理data垃圾$", - fnc: "clearTrash", - permission: "master", - }, - { - reg: "^#设置海外解析$", - fnc: "setOversea", - permission: "master", - }, { reg: "(h5app.kuwo.cn)", fnc: "bodianMusic", @@ -851,34 +843,6 @@ export class tools extends plugin { return true; } - // 清理垃圾文件 - async clearTrash(e) { - const dataDirectory = "./data/"; - - // 删除Yunzai遗留问题的合成视频垃圾文件 - try { - const files = await readCurrentDir(dataDirectory); - let dataClearFileLen = 0; - for (const file of files) { - // 如果文件名符合规则,执行删除操作 - if (/^[0-9a-f]{32}$/.test(file)) { - await fs.promises.unlink(dataDirectory + file); - dataClearFileLen++; - } - } - // 删除R插件临时文件 - const rTempFileLen = await deleteFolderRecursive(this.defaultPath) - e.reply( - `数据统计:\n` + - `- 当前清理了${ dataDirectory }下总计:${ dataClearFileLen } 个垃圾文件\n` + - `- 当前清理了${ this.toolsConfig.defaultPath }下文件夹:${ rTempFileLen } 个群的所有临时文件` - ); - } catch (err) { - logger.error(err); - await e.reply("清理失败,重试或者手动清理即可"); - } - } - // ins解析 async instagram(e) { let suffix = e.msg.match(/(?<=com\/)[\/a-z0-9A-Z].*/)[0]; @@ -1710,29 +1674,6 @@ export class tools extends plugin { } } - /** - * 设置海外模式 - * @param e - * @returns {Promise} - */ - async setOversea(e) { - // 查看当前设置 - let os; - if ((await redis.exists(REDIS_YUNZAI_ISOVERSEA))) { - os = JSON.parse(await redis.get(REDIS_YUNZAI_ISOVERSEA)).os; - } - // 设置 - os = ~os - await redis.set( - REDIS_YUNZAI_ISOVERSEA, - JSON.stringify({ - os: os, - }), - ); - e.reply(`当前服务器:${ os ? '海外服务器' : '国内服务器' }`) - return true; - } - /** * 判断是否是海外服务器 * @return {Promise} @@ -1752,6 +1693,25 @@ export class tools extends plugin { return JSON.parse((await redis.get(REDIS_YUNZAI_ISOVERSEA))).os; } + /** + * 判断是否是拉格朗日驱动 + * @returns {Promise} + */ + async isLagRangeDriver() { + // 如果第一次使用没有值就设置 + if (!(await redis.exists(REDIS_YUNZAI_LAGRANGE))) { + await redis.set( + REDIS_YUNZAI_ISOVERSEA, + JSON.stringify({ + driver: false, + }), + ); + return true; + } + // 如果有就取出来 + return JSON.parse((await redis.get(REDIS_YUNZAI_LAGRANGE))).driver; + } + /** * 限制用户调用 * @param e @@ -1773,7 +1733,19 @@ export class tools extends plugin { * @param videoSizeLimit 发送转上传视频的大小限制,默认70MB */ async sendVideoToUpload(e, path, videoSizeLimit = 70) { - if (!fs.existsSync(path)) return e.reply('视频不存在'); + // 判断文件是否存在 + if (!fs.existsSync(path)) { + return e.reply('视频不存在'); + } + // 判断是否是拉格朗日 + if (await this.isLagRangeDriver()) { + // 构造拉格朗日适配器 + const lagrange = new LagrangeAdapter(this.toolsConfig.lagrangeForwardWebSocket); + // 上传群文件 + await lagrange.uploadGroupFile(e.user_id || e.sender.card, e.group_id, path); + // 上传完直接返回 + return; + } const stats = fs.statSync(path); const videoSize = (stats.size / (1024 * 1024)).toFixed(2); if (videoSize > videoSizeLimit) { @@ -1791,6 +1763,7 @@ export class tools extends plugin { * @return {Promise} */ async uploadGroupFile(e, path) { + // 判断是否是ICQQ if (e.bot?.sendUni) { await e.group.fs.upload(path); } else { diff --git a/config/tools.yaml b/config/tools.yaml index 1af98dd..e72f5a2 100644 --- a/config/tools.yaml +++ b/config/tools.yaml @@ -13,4 +13,6 @@ douyinCookie: '' # douyin's cookie, 格式:odin_tt=xxx;passport_fe_beating_sta queueConcurrency: 1 # 【目前只涉及哔哩哔哩的下载】根据服务器性能设置可以并发下载的个数,如果你的服务器比较强劲,就选择4~12,较弱就一个一个下载,选择1 -videoDownloadConcurrency: 1 # 下载视频是否使用多线程,如果不使用默认是1,如果使用根据服务器进行选择,如果不确定是否可以用4即可,高性能服务器随意4~12都可以,看CPU的实力 \ No newline at end of file +videoDownloadConcurrency: 1 # 下载视频是否使用多线程,如果不使用默认是1,如果使用根据服务器进行选择,如果不确定是否可以用4即可,高性能服务器随意4~12都可以,看CPU的实力 + +lagrangeForwardWebSocket: 'ws://127.0.0.1:9091/' # 格式:ws://地址:端口/,拉格朗日正向连接地址,用于适配拉格朗日上传群文件,解决部分用户无法查看视频问题 \ No newline at end of file diff --git a/config/version.yaml b/config/version.yaml index 15a88c1..e4faafc 100644 --- a/config/version.yaml +++ b/config/version.yaml @@ -1,7 +1,8 @@ - { - version: 1.6.7, + version: 1.7.0, data: [ + 新增适配拉格朗日上传功能, 新增超过文件大小转上传功能, 新增B站下载功能, 新增B站扫码功能, diff --git a/constants/constant.js b/constants/constant.js index 64c7dac..6e58ee0 100644 --- a/constants/constant.js +++ b/constants/constant.js @@ -71,6 +71,12 @@ export const DIVIDING_LINE = "\n------------------{}------------------" */ export const REDIS_YUNZAI_ISOVERSEA = "Yz:rconsole:tools:oversea"; +/** + * 保存判断机子是否使用的是拉格朗日 + * @type {string} + */ +export const REDIS_YUNZAI_LAGRANGE = "Yz:rconsole:tools:lagrange"; + export const TWITTER_BEARER_TOKEN = ""; /** diff --git a/guoba.support.js b/guoba.support.js index 741ba86..0ad8b85 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -141,6 +141,17 @@ export function supportGuoba() { placeholder: "不确定用1即可,高性能服务器随意4~12都可以,看CPU的实力", }, }, + { + field: "tools.lagrangeForwardWebSocket", + label: "拉格朗日正向WebSocket连接地址", + bottomHelpMessage: + "格式:ws://地址:端口/,拉格朗日正向连接地址,用于适配拉格朗日上传群文件,解决部分用户无法查看视频问题", + component: "Input", + required: false, + componentProps: { + placeholder: "请输入拉格朗日正向WebSocket连接地址", + }, + } ], getConfigData() { const toolsData = { diff --git a/package.json b/package.json index 46188d9..770224e 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "axios": "^1.3.4", "tunnel": "^0.0.6", "qrcode": "^1.5.3", - "p-queue": "^8.0.1" + "p-queue": "^8.0.1", + "ws": "^8.17.0" } } diff --git a/utils/lagrange-adapter.js b/utils/lagrange-adapter.js new file mode 100644 index 0000000..e462f1b --- /dev/null +++ b/utils/lagrange-adapter.js @@ -0,0 +1,138 @@ +import { randomUUID } from 'crypto' +import path from "path"; +import fs from 'fs' +import { WebSocket } from 'ws' + +export class LagrangeAdapter { + /** + * 构造拉格朗日适配器 + * @param wsAddr 形如:ws://127.0.0.1:9091/ + */ + constructor(wsAddr) { + this.ws = new WebSocket(wsAddr) + } + + /** + * 上传群文件 + * @param bot_id - 云崽机器人id + * @param group_id - 群号 + * @param file - 文件所在位置 + * @returns {Promise} + */ + async uploadGroupFile(bot_id, group_id, file) { + file = await this.formatFile(file) + if (!file.match(/^file:\/\//)) { + file = await this.fileToPath(file) + file = await this.formatFile(file) + } + file = file.replace(/^file:\/\//, '') + const name = path.basename(file) || Date.now() + path.extname(file) + logger.info("[R插件][拉格朗日适配器] 连接到拉格朗日"); + logger.info(bot_id, group_id, file, name); + this.ws.on("open", () => { + this.upload_private_file_api(bot_id, group_id, file, name); + }) + } + + /** + * 上传群文件的拉格朗日API + * @param {string} id - 机器人QQ 通过e.bot、Bot调用无需传入 + * @param {number} group_id - 群号 + * @param {string} file - 本地文件路径 + * @param {string} name - 储存名称 + * @param {string} folder - 目标文件夹 默认群文件根目录 + */ + async upload_private_file_api(id, group_id, file, name, folder = '/') { + const params = { group_id, file, name, folder } + const echo = randomUUID() + /** 序列化 */ + const log = JSON.stringify({ echo, action: "upload_group_file", params }) + logger.info("[R插件][拉格朗日适配器] 发送视频中..."); + /** 发送到拉格朗日 */ + this.ws.send(log); + } + + /** + * 处理segment中的i||i.file,主要用于一些sb字段,标准化他们 + * @param {string|object} file - i.file + */ + async formatFile(file) { + const str = function () { + if (file.includes('gchat.qpic.cn') && !file.startsWith('https://')) { + return `https://${ file }` + } else if (file.startsWith('base64://')) { + return file + } else if (file.startsWith('http://') || file.startsWith('https://')) { + return file + } else if (fs.existsSync(path.resolve(file.replace(/^file:\/\//, '')))) { + return `file://${ path.resolve(file.replace(/^file:\/\//, '')) }` + } else if (fs.existsSync(path.resolve(file.replace(/^file:\/\/\//, '')))) { + return `file://${ path.resolve(file.replace(/^file:\/\/\//, '')) }` + } + return file + } + + switch (typeof file) { + case 'object': + /** 这里会有复读这样的直接原样不动把message发过来... */ + if (file.url) { + if (file?.url?.includes('gchat.qpic.cn') && !file?.url?.startsWith('https://')) return `https://${ file.url }` + return file.url + } + + /** 老插件渲染出来的图有这个字段 */ + if (file?.type === 'Buffer') return Buffer.from(file?.data) + if (Buffer.isBuffer(file) || file instanceof Uint8Array) return file + + /** 流 */ + if (file instanceof fs.ReadStream) return await Bot.Stream(file, { base: true }) + + /** i.file */ + if (file.file) return str(file.file) + return file + case 'string': + return str(file) + default: + return file + } + } + + /** + * 传入文件,返回本地路径 + * 可以是http://、file://、base64://、buffer + * @param {file://|base64://|http://|buffer} file + * @param {string} _path - 可选,不传默认为图片 + */ + async fileToPath(file, _path) { + if (!_path) _path = `./temp/FileToUrl/${ Date.now() }.png` + if (Buffer.isBuffer(file) || file instanceof Uint8Array) { + fs.writeFileSync(_path, file) + return _path + } else if (file instanceof fs.ReadStream) { + const buffer = await Bot.Stream(file) + fs.writeFileSync(_path, buffer) + return _path + } else if (fs.existsSync(file.replace(/^file:\/\//, ''))) { + fs.copyFileSync(file.replace(/^file:\/\//, ''), _path) + return _path + } else if (fs.existsSync(file.replace(/^file:\/\/\//, ''))) { + fs.copyFileSync(file.replace(/^file:\/\/\//, ''), _path) + return _path + } else if (file.startsWith('base64://')) { + const buffer = Buffer.from(file.replace(/^base64:\/\//, ''), 'base64') + fs.writeFileSync(_path, buffer) + return _path + } else if (/^http(s)?:\/\//.test(file)) { + const res = await fetch(file) + if (!res.ok) { + throw new Error(`请求错误!状态码: ${ res.status }`) + } else { + const buffer = Buffer.from(await res.arrayBuffer()) + fs.writeFileSync(_path, buffer) + return _path + } + } else { + throw new Error('传入的文件类型不符合规则,只接受url、buffer、file://路径或者base64编码的图片') + } + } +} \ No newline at end of file