feat: 新版本适配拉格朗日上传

1. 适配拉格朗日上传文件,解决部分用户使用拉格朗日无法观看视频问题
2. 新增switchers,存储一些开关例如:海外解析、拉格朗日判断等
3. 新增ws用于连接拉格朗日
4. 新增配置文件配置项`lagrangeForwardWebSocket`,用于配置拉格朗日正向地址
This commit is contained in:
zhiyu 2024-05-10 18:54:54 +08:00
parent 012213d871
commit 503ec55dfc
8 changed files with 306 additions and 66 deletions

108
apps/switchers.js Normal file
View File

@ -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<boolean>}
*/
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<void>}
*/
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("清理失败,重试或者手动清理即可");
}
}
}

View File

@ -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<boolean>}
*/
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<Boolean>}
@ -1752,6 +1693,25 @@ export class tools extends plugin {
return JSON.parse((await redis.get(REDIS_YUNZAI_ISOVERSEA))).os;
}
/**
* 判断是否是拉格朗日驱动
* @returns {Promise<Boolean>}
*/
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<void>}
*/
async uploadGroupFile(e, path) {
// 判断是否是ICQQ
if (e.bot?.sendUni) {
await e.group.fs.upload(path);
} else {

View File

@ -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的实力
videoDownloadConcurrency: 1 # 下载视频是否使用多线程如果不使用默认是1如果使用根据服务器进行选择如果不确定是否可以用4即可高性能服务器随意4~12都可以看CPU的实力
lagrangeForwardWebSocket: 'ws://127.0.0.1:9091/' # 格式ws://地址:端口/,拉格朗日正向连接地址,用于适配拉格朗日上传群文件,解决部分用户无法查看视频问题

View File

@ -1,7 +1,8 @@
- {
version: 1.6.7,
version: 1.7.0,
data:
[
新增<span class="cmd">适配拉格朗日上传</span>功能,
新增<span class="cmd">超过文件大小转上传</span>功能,
新增<span class="cmd">B站下载</span>功能,
新增<span class="cmd">B站扫码</span>功能,

View File

@ -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 = "";
/**

View File

@ -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 = {

View File

@ -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"
}
}

138
utils/lagrange-adapter.js Normal file
View File

@ -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<void>}
*/
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.botBot调用无需传入
* @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编码的图片')
}
}
}