🐳 chore: rc2-分离webui到新的项目仓库可自行部署

This commit is contained in:
zhiyu1998 2024-12-01 14:21:43 +08:00
parent 59bbe8cbe1
commit db5f57a73c
51 changed files with 7 additions and 3127 deletions

View File

@ -102,7 +102,10 @@ sudo apt-get install ffmpeg
| 春日野穹OvO | 25 | | 春日野穹OvO | 25 |
| MiX | 30 | | MiX | 30 |
| AO | 26 | | AO | 26 |
| Chino | 30 | | Chino | 80 |
| 辰 | 50 |
| 非酋 | 1杯瑞幸 |
| 白洲梓 | 1杯瑞幸 |

View File

@ -1,68 +0,0 @@
import { REDIS_YUNZAI_WEBUI } from "../constants/constant.js";
import config from "../model/config.js";
import { constructPublicIPsMsg } from "../utils/network.js";
import { redisSetKey } from "../utils/redis-util.js";
import { buildNextJs } from "../utils/start-nextjs.js";
import { getBotLoginInfo, getBotStatus, getBotVersionInfo, sendPrivateMsg } from "../utils/yunzai-util.js";
export class WebUI extends plugin {
constructor() {
super({
name: "R插件 WebUI 开关",
dsc: "R插件 WebUI 开关",
event: "message",
priority: 4000,
rule: [
{
reg: "^#(r|R)wss$",
fnc: "rWebSwitch",
permission: "master",
},
{
reg: "^#(r|R)ws$",
fnc: "rWebStatus",
permission: "master",
}
]
});
// 配置文件
this.toolsConfig = config.getConfig("tools");
// 加载WebUI开关
this.isOpenWebUI = this.toolsConfig.isOpenWebUI;
}
async initData(e, realIsOpenWebUI) {
if (realIsOpenWebUI) {
Promise.all([getBotStatus(e), getBotVersionInfo(e), getBotLoginInfo(e)]).then(values => {
const status = values[0].data;
const versionInfo = values[1].data;
const loginInfo = values[2].data;
redisSetKey(REDIS_YUNZAI_WEBUI, {
...status,
...versionInfo,
...loginInfo
});
});
}
}
async rWebSwitch(e) {
config.updateField("tools", "isOpenWebUI", !this.isOpenWebUI);
const realIsOpenWebUI = config.getConfig("tools").isOpenWebUI;
if (realIsOpenWebUI) {
// 初始化数据
await this.initData(e, realIsOpenWebUI);
e.reply(`R插件可视化面板正在构建中请稍等...`);
// 动态编译生产环境
await buildNextJs();
await sendPrivateMsg(e, constructPublicIPsMsg());
}
e.reply(`R插件可视化面板${ realIsOpenWebUI ? "✅已开启" : "❌已关闭" },重启后生效`);
return true;
}
async rWebStatus(e) {
e.reply(`R插件可视化面板\n状态:${ this.toolsConfig.isOpenWebUI ? "✅开启" : "❌关闭" }\n地址:******:4016`);
return true;
}
}

View File

@ -1,4 +1,3 @@
isOpenWebUI: false # 是否开启webui
defaultPath: './data/rcmp4/' # 保存视频的位置 defaultPath: './data/rcmp4/' # 保存视频的位置
videoSizeLimit: 70 # 视频大小限制单位MB超过大小则转换成群文件上传 videoSizeLimit: 70 # 视频大小限制单位MB超过大小则转换成群文件上传
proxyAddr: '127.0.0.1' # 魔法地址 proxyAddr: '127.0.0.1' # 魔法地址

View File

@ -1,5 +1,5 @@
- { - {
version: 1.10.0-rc1, version: 1.10.0-rc2,
data: data:
[ [
新增<span class="cmd">RBS查看哔哩哔哩状态</span>功能, 新增<span class="cmd">RBS查看哔哩哔哩状态</span>功能,

View File

@ -92,12 +92,6 @@ export const REDIS_YUNZAI_CLOUDSONGLIST = "Yz:rconsole:tools:cloudsonglist";
*/ */
export const REDIS_YUNZAI_WHITELIST = "Yz:rconsole:tools:whitelist"; export const REDIS_YUNZAI_WHITELIST = "Yz:rconsole:tools:whitelist";
/**
* WEBUI需要数据的缓存
* @type {string}
*/
export const REDIS_YUNZAI_WEBUI = "Yz:rconsole:tools:webui";
export const TWITTER_BEARER_TOKEN = ""; export const TWITTER_BEARER_TOKEN = "";
/** /**

View File

@ -1,16 +1,12 @@
import fs from "node:fs"; import fs from "node:fs";
import path from "path"; import path from "path";
import config from "./model/config.js"; import config from "./model/config.js";
import { constructPublicIPsMsg } from "./utils/network.js";
import { startNextJs } from "./utils/start-nextjs.js";
if (!global.segment) { if (!global.segment) {
global.segment = (await import("oicq")).segment global.segment = (await import("oicq")).segment
} }
// 加载版本号 // 加载版本号
const versionData = config.getConfig("version"); const versionData = config.getConfig("version");
// 加载是否使用WebUI
const isOpenWebUI = config.getConfig("tools").isOpenWebUI;
// 加载名称 // 加载名称
const packageJsonPath = path.join('./plugins', 'rconsole-plugin', 'package.json'); const packageJsonPath = path.join('./plugins', 'rconsole-plugin', 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
@ -40,10 +36,4 @@ for (let i in files) {
apps[name] = ret[i].value[Object.keys(ret[i].value)[0]]; apps[name] = ret[i].value[Object.keys(ret[i].value)[0]];
} }
// 检查是否启动 webui
if (isOpenWebUI) {
startNextJs('start');
logger.info(constructPublicIPsMsg());
}
export { apps }; export { apps };

View File

@ -2,33 +2,11 @@
"name": "rconsole-plugin", "name": "rconsole-plugin",
"description": "R-Plugin", "description": "R-Plugin",
"type": "module", "type": "module",
"scripts": {
"dev": "cd server && next dev -p 4016",
"dev6": "cd server && HOST=:: next dev -p 4016",
"start": "cd server && next start -p 4016",
"start6": "cd server && HOST=:: next start -p 4016",
"build": "cd server && next build"
},
"dependencies": { "dependencies": {
"axios": "^1.3.4", "axios": "^1.3.4",
"chart.js": "^4.4.6",
"form-data": "^4.0.1", "form-data": "^4.0.1",
"ioredis": "^5.4.1",
"js-yaml": "^4.1.0",
"next": "^14.2.16",
"node-id3": "^0.2.6", "node-id3": "^0.2.6",
"node-os-utils": "^1.3.7",
"os-utils": "^0.0.14",
"p-queue": "^8.0.1",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"react": "^18.3.1", "p-queue": "^8.0.1"
"react-chartjs-2": "^5.2.0",
"react-circular-progressbar": "^2.1.0",
"react-dom": "^18.3.1",
"systeminformation": "^5.23.5"
},
"devDependencies": {
"daisyui": "^4.12.14",
"tailwindcss": "^3.4.14"
} }
} }

View File

@ -1,9 +0,0 @@
import "../styles/global.css";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

View File

@ -1,12 +0,0 @@
import Header from "../components/header.jsx";
import Sidebar from "../components/sidebar.jsx";
import { DrawerProvider } from "../contexts/drawer-context.js";
export default function Page() {
return (
<DrawerProvider>
<Header/>
<Sidebar />
</DrawerProvider>
)
}

View File

@ -1,11 +0,0 @@
import { REDIS_YUNZAI_WEBUI } from "../../../../../constants/constant.js";
import { redis } from "../../../../utils/redis.js";
export async function GET(req, res) {
const botInfo = JSON.parse(await redis.get(REDIS_YUNZAI_WEBUI));
return new Response(JSON.stringify(botInfo), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}

View File

@ -1,30 +0,0 @@
async function getLatestCommit(platform = "github") {
// 构建 API URL
const baseUrl =
platform === "github"
? `https://api.github.com/repos/zhiyu1998/rconsole-plugin/commits`
: `https://gitee.com/api/v5/repos/kyrzy0416/rconsole-plugin/commits`;
try {
const response = await fetch(baseUrl);
if (!response.ok) throw new Error("获取提交信息失败");
const commits = await response.json();
const latestCommit = commits[0]; // 最新提交
const { sha, commit, html_url } = latestCommit;
return { sha, author: commit.author.name, message: commit.message, url: html_url };
} catch (error) {
console.error("无法获取最新的提交:", error.message);
return null;
}
}
export async function GET(req, res) {
const latestCommit = await getLatestCommit();
return new Response(JSON.stringify(latestCommit), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}

View File

@ -1,52 +0,0 @@
import fs from 'fs';
import yaml from 'js-yaml';
import path from 'path';
const configPath = path.join(process.cwd(), "../", 'config', 'tools.yaml');
export async function GET(req, res) {
try {
const yamlContent = await fs.promises.readFile(configPath, 'utf8');
const config = yaml.load(yamlContent);
return new Response(JSON.stringify(config), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('读取配置文件失败:', error);
return new Response(JSON.stringify({ error: '读取配置文件失败' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}
export async function POST(req, res) {
try {
const updates = await req.json();
const yamlContent = await fs.promises.readFile(configPath, 'utf8');
const currentConfig = yaml.load(yamlContent);
// 只更新指定的字段
const newConfig = { ...currentConfig, ...updates };
// 转换回YAML并保存
const newYamlContent = yaml.dump(newConfig, {
indent: 2,
lineWidth: -1,
quotingType: '"'
});
await fs.promises.writeFile(configPath, newYamlContent, 'utf8');
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('更新配置文件失败:', error);
return new Response(JSON.stringify({ error: '更新配置文件失败' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}

View File

@ -1,25 +0,0 @@
import axios from "axios";
export async function GET(request) {
const url = new URL(request.url); // 获取请求的 URL
const targetUrl = url.searchParams.get("url"); // 从查询参数中获取目标 URL
const start = Date.now(); // 记录请求开始时间
try {
await axios.get(targetUrl);
// 计算结束时间减去开始时间
return new Response(JSON.stringify({
time: Date.now() - start
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({
time: 0
}), {
status: 500,
headers: { 'Content-Type': 'application/json' },
})
}
}

View File

@ -1,103 +0,0 @@
import { unstable_noStore as noStore } from 'next/cache';
import { promises as fs } from 'fs';
import os from 'os';
import si from 'systeminformation';
let lastBytesReceived = 0;
let lastBytesSent = 0;
let lastTimestamp = Date.now();
async function getLinuxStats() {
const data = await fs.readFile('/proc/net/dev', 'utf8');
const lines = data.trim().split('\n');
let bytesReceived = 0;
let bytesSent = 0;
for (let i = 2; i < lines.length; i++) {
const line = lines[i].trim();
const parts = line.split(/\s+/);
if (parts[0].startsWith('lo:')) continue;
bytesReceived += parseInt(parts[1], 10);
bytesSent += parseInt(parts[9], 10);
}
return { bytesReceived, bytesSent };
}
async function getWindowsStats() {
const networkStats = await si.networkStats();
let bytesReceived = 0;
let bytesSent = 0;
for (const stat of networkStats) {
bytesReceived += stat.rx_bytes || 0;
bytesSent += stat.tx_bytes || 0;
}
return { bytesReceived, bytesSent };
}
async function getNetworkStats() {
try {
const platform = os.platform();
let bytesReceived = 0;
let bytesSent = 0;
if (platform === 'linux') {
const stats = await getLinuxStats();
bytesReceived = stats.bytesReceived;
bytesSent = stats.bytesSent;
} else {
const stats = await getWindowsStats();
bytesReceived = stats.bytesReceived;
bytesSent = stats.bytesSent;
}
const now = Date.now();
const timeDiff = (now - lastTimestamp) / 1000;
const downloadSpeed = Math.max(0, (bytesReceived - lastBytesReceived) / timeDiff);
const uploadSpeed = Math.max(0, (bytesSent - lastBytesSent) / timeDiff);
lastBytesReceived = bytesReceived;
lastBytesSent = bytesSent;
lastTimestamp = now;
return {
downloadSpeed: (downloadSpeed / 1024).toFixed(2),
uploadSpeed: (uploadSpeed / 1024).toFixed(2),
totalReceived: (bytesReceived / (1024 * 1024 * 1024)).toFixed(2),
totalSent: (bytesSent / (1024 * 1024 * 1024)).toFixed(2),
timestamp: now
};
} catch (error) {
console.error('获取网络统计信息失败:', error);
return {
downloadSpeed: "0",
uploadSpeed: "0",
totalReceived: "0",
totalSent: "0",
timestamp: Date.now()
};
}
}
export async function GET() {
// 这个不允许删除,否则无法做到实时获取
noStore();
try {
const stats = await getNetworkStats();
return new Response(JSON.stringify(stats), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}

View File

@ -1,49 +0,0 @@
import { REDIS_RESOLVE_CONTROLLER } from "../../../../constants/redis.js";
import { GLOBAL_RESOLE_CONTROLLER } from "../../../../constants/resolve.js";
import { redis } from "../../../../utils/redis.js";
export async function GET(req, res) {
let resolveList = await redis.get(REDIS_RESOLVE_CONTROLLER);
if (resolveList == null) {
// Redis中不存在就初始化进去
await redis.set(REDIS_RESOLVE_CONTROLLER, JSON.stringify(GLOBAL_RESOLE_CONTROLLER));
return new Response(JSON.stringify(GLOBAL_RESOLE_CONTROLLER), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
return new Response(resolveList, {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
export async function POST(req) {
try {
const data = await req.json();
const { selectedTags } = data;
// 获取所有可能的标签
const allTags = GLOBAL_RESOLE_CONTROLLER.map(item => item.label);
// 更新控制器状态
const updatedController = GLOBAL_RESOLE_CONTROLLER.map(item => ({
...item,
value: selectedTags.includes(item.label) ? 1 : 0
}));
// 保存到Redis
await redis.set(REDIS_RESOLVE_CONTROLLER, JSON.stringify(updatedController));
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({ success: false, error: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}

View File

@ -1,63 +0,0 @@
import { unstable_noStore as noStore } from 'next/cache';
import si from 'systeminformation';
import os from 'os';
export async function GET(request, { params }) {
// 这个不允许删除,否则无法做到实时获取
noStore();
try {
// 获取CPU信息
const cpuInfo = await si.cpu();
const cpuUsage = await si.currentLoad();
const totalCpuCores = cpuInfo.cores;
const cpuCoresUsed = ((cpuUsage.currentLoad / 100) * totalCpuCores).toFixed(1); // 使用的核心数
// 获取内存信息
const totalMemory = (os.totalmem() / (1024 ** 3)).toFixed(2); // 转换为 GB
const freeMemory = (os.freemem() / (1024 ** 3)).toFixed(2); // 转换为 GB
const usedMemory = (totalMemory - freeMemory).toFixed(2);
const memoryUsagePercent = ((usedMemory / totalMemory) * 100).toFixed(2);
// 获取磁盘信息
const diskInfo = await si.fsSize();
const totalDisk = (diskInfo[0].size / (1024 ** 3)).toFixed(2); // 转换为 GB
const usedDisk = (diskInfo[0].used / (1024 ** 3)).toFixed(2); // 转换为 GB
const diskUsagePercent = ((usedDisk / totalDisk) * 100).toFixed(2);
// 获取网络信息
const networkInterfaces = os.networkInterfaces();
const ipAddress = Object.values(networkInterfaces)
.flat()
.filter(detail => detail.family === 'IPv4' && !detail.internal)[0].address;
// 获取系统信息
const hostname = os.hostname();
const uptime = os.uptime();
const osInfo = await si.osInfo();
return new Response(JSON.stringify({
cpuUsage: cpuUsage.currentLoad.toFixed(2),
cpuUsageDetail: `${cpuUsage.currentLoad.toFixed(2)}%`,
totalCpuCores,
cpuCoresUsed,
memoryUsage: memoryUsagePercent,
usedMemory: `${usedMemory} GB`,
totalMemory: `${totalMemory} GB`,
diskUsage: diskUsagePercent,
usedDisk: `${usedDisk} GB`,
totalDisk: `${totalDisk} GB`,
loadAverage: cpuUsage.avgLoad.toFixed(2),
ipAddress,
hostname,
uptime: `${Math.floor(uptime / 60 / 60)} hours`,
distro: osInfo.distro,
kernelVersion: osInfo.kernel,
arch: os.arch(),
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), { status: 500 });
}
}

View File

@ -1,241 +0,0 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import fs from 'fs/promises';
import path from 'path';
import { REDIS_UPDATE_PATH, REDIS_UPDATE_STATUS } from "../../../../constants/redis.js";
import { redis } from "../../../../utils/redis.js";
const execAsync = promisify(exec);
// Git错误处理函数
function handleGitError(error, stderr) {
if (error.message.includes('RPC failed')) {
return '网络连接失败,请检查网络后重试';
}
if (error.message.includes('early EOF')) {
return '数据传输中断,请重试';
}
if (error.message.includes('fetch-pack: invalid index-pack output')) {
return '数据包错误,请重试';
}
if (error.message.includes('Timed out')) {
return '连接超时,请检查网络后重试';
}
if (error.message.includes('Could not resolveControl host')) {
return '无法解析主机地址,请检查网络';
}
if (error.message.includes('Permission denied')) {
return '权限被拒绝请检查git权限配置';
}
if (error.message.includes('be overwritten by merge')) {
return '存在冲突,请使用强制更新';
}
// 如果是其他错误,返回具体错误信息
return stderr || error.message || '未知错误';
}
async function ensureDirectory(dir) {
try {
await fs.access(dir);
} catch {
await fs.mkdir(dir, { recursive: true });
}
}
async function copyConfig(src, dest) {
try {
await ensureDirectory(path.dirname(dest));
await fs.cp(src, dest, { recursive: true });
console.log(`成功复制配置文件从 ${src}${dest}`);
return true;
} catch (error) {
console.error(`复制配置文件失败: ${error.message}`);
return false;
}
}
// 清理更新状态和临时文件
async function cleanupUpdate(tempDir) {
try {
// 清理临时文件
await fs.rm(tempDir, { recursive: true, force: true });
// 清理Redis中的更新状态
await redis.del(REDIS_UPDATE_STATUS);
await redis.del(REDIS_UPDATE_PATH);
console.log('清理完成');
} catch (error) {
console.error('清理失败:', error);
}
}
export async function GET(req) {
try {
const { searchParams } = new URL(req.url);
const isCheck = searchParams.get('check') === 'true';
const isRestore = searchParams.get('restore') === 'true';
const isForce = searchParams.get('force') === 'true';
// 如果是检查请求
if (isCheck) {
const updateStatus = await redis.get(REDIS_UPDATE_STATUS);
return new Response(JSON.stringify({
needsRestore: updateStatus === 'restoring'
}), {
headers: { 'Content-Type': 'application/json' },
});
}
// 如果是恢复请求
if (isRestore) {
const updateStatus = await redis.get(REDIS_UPDATE_STATUS);
const paths = JSON.parse(await redis.get(REDIS_UPDATE_PATH) || '{}');
if (updateStatus === 'restoring' && paths.tempDir && paths.configDir) {
try {
await copyConfig(paths.tempDir, paths.configDir);
await cleanupUpdate(paths.tempDir);
return new Response(JSON.stringify({
success: true,
message: '配置恢复完成'
}), {
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({
success: false,
message: '配置恢复失败:' + error.message
}), {
headers: { 'Content-Type': 'application/json' },
});
}
}
return new Response(JSON.stringify({
success: true,
message: '无需恢复'
}), {
headers: { 'Content-Type': 'application/json' },
});
}
const projectRoot = path.join(process.cwd(), '..');
const tempDir = path.join(projectRoot, 'temp', 'update-tmp');
const configDir = path.join(projectRoot, 'config');
// 检查是否有未完成的更新
const updateStatus = await redis.get(REDIS_UPDATE_STATUS);
if (updateStatus === 'restoring') {
// 如果有未完成的更新,尝试恢复
const paths = JSON.parse(await redis.get(REDIS_UPDATE_PATH) || '{}');
if (paths.tempDir && paths.configDir) {
try {
await copyConfig(paths.tempDir, paths.configDir);
console.log('恢复了之前未完成的配置文件更新');
} finally {
await cleanupUpdate(paths.tempDir);
}
}
}
console.log('开始新的更新流程');
// 保存路径信息到Redis
await redis.set(REDIS_UPDATE_PATH, JSON.stringify({
tempDir,
configDir
}));
await redis.set(REDIS_UPDATE_STATUS, 'started');
// 确保临时目录存在
await ensureDirectory(tempDir);
// 备份配置文件
let configBackedUp = false;
try {
await fs.access(configDir);
await copyConfig(configDir, tempDir);
configBackedUp = true;
console.log('配置文件备份成功');
await redis.set(REDIS_UPDATE_STATUS, 'backed_up');
} catch (error) {
console.log('无配置文件需要备份或备份失败:', error.message);
}
try {
// 执行git操作
if (isForce) {
console.log('执行强制更新...');
await execAsync(`git -C "${projectRoot}" checkout .`);
}
// 标记状态为需要恢复
await redis.set(REDIS_UPDATE_STATUS, 'restoring');
console.log('执行git pull...');
const { stdout } = await execAsync(`git -C "${projectRoot}" pull --no-rebase`);
// 恢复配置文件
if (configBackedUp) {
console.log('开始恢复配置文件...');
await copyConfig(tempDir, configDir);
}
// 清理所有临时文件和状态
await cleanupUpdate(tempDir);
if (stdout.includes('Already up to date') || stdout.includes('已经是最新')) {
return new Response(JSON.stringify({
success: true,
message: '已经是最新版本'
}), {
headers: { 'Content-Type': 'application/json' },
});
}
return new Response(JSON.stringify({
success: true,
message: '更新成功'
}), {
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
// 如果git操作失败也尝试恢复配置文件
if (configBackedUp) {
try {
await copyConfig(tempDir, configDir);
console.log('git操作失败但配置文件已恢复');
} catch (restoreError) {
console.error('恢复配置文件失败:', restoreError.message);
}
}
await cleanupUpdate(tempDir);
const errorMessage = handleGitError(error, error.stderr);
return new Response(JSON.stringify({
success: false,
message: errorMessage
}), {
headers: { 'Content-Type': 'application/json' },
});
}
} catch (error) {
console.error('更新过程出错:', error);
// 确保清理所有临时状态
const paths = JSON.parse(await redis.get(REDIS_UPDATE_PATH) || '{}');
if (paths.tempDir) {
await cleanupUpdate(paths.tempDir);
}
return new Response(JSON.stringify({
success: false,
message: '更新过程出错:' + (error.message || '未知错误')
}), {
headers: { 'Content-Type': 'application/json' },
});
}
}

View File

@ -1,43 +0,0 @@
async function getLatestTag() {
// GitHub 和 Gitee 的 API URL
const githubUrl = `https://api.github.com/repos/zhiyu1998/rconsole-plugin/tags`;
const giteeUrl = `https://gitee.com/api/v5/repos/kyrzy0416/rconsole-plugin/tags`;
// 定义 fetch 请求
const fetchGitHub = fetch(githubUrl).then(async (response) => {
if (!response.ok) throw new Error("GitHub请求失败");
const data = await response.json();
return { source: "GitHub", tag: data };
});
const fetchGitee = fetch(giteeUrl).then(async (response) => {
if (!response.ok) throw new Error("Gitee请求失败");
const data = await response.json();
return { source: "Gitee", tag: data };
});
// 使用 Promise.race 竞速
try {
return await Promise.race([fetchGitHub, fetchGitee]);
} catch (error) {
console.error("无法获取最新的标签:", error.message);
return null;
}
}
export async function GET(req, res) {
const tags = await getLatestTag();
console.log(tags);
let latestTag;
if (tags.source === "Gitee") {
latestTag = tags.tag[tags.length - 1];
}
latestTag = tags.tag[0];
console.log(latestTag);
return new Response(JSON.stringify(latestTag), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}

View File

@ -1,65 +0,0 @@
import React, { useState, useEffect } from "react";
const TagSelector = ({ options = [], initialTags = [], onChange }) => {
const [selectedTags, setSelectedTags] = useState(initialTags);
useEffect(() => {
setSelectedTags(initialTags);
}, [initialTags]);
const addTag = (tag) => {
if (!selectedTags.includes(tag)) {
const updatedTags = [...selectedTags, tag];
setSelectedTags(updatedTags);
if (onChange) onChange(updatedTags);
}
};
const removeTag = (tag) => {
const updatedTags = selectedTags.filter((t) => t !== tag);
setSelectedTags(updatedTags);
if (onChange) onChange(updatedTags);
};
return (
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="flex flex-wrap gap-2 mb-4 border p-2 rounded">
{selectedTags.length > 0 ? (
selectedTags.map((tag, index) => (
<span
key={index}
className="badge badge-secondary gap-2 cursor-pointer"
onClick={() => removeTag(tag)}
>
{tag}
</span>
))
) : (
<span className="text-gray-500">暂无标签请选择一个选项</span>
)}
</div>
<select
className="select select-bordered w-full"
onChange={(e) => {
if (e.target.value) {
addTag(e.target.value);
e.target.value = "";
}
}}
value=""
>
<option value="" disabled>
选择一个选项
</option>
{options.filter(option => !selectedTags.includes(option)).map((option, index) => (
<option key={index} value={option}>
{option}
</option>
))}
</select>
</div>
);
};
export default TagSelector;

View File

@ -1,23 +0,0 @@
import React, { useState } from 'react';
function ThemeToggle() {
// light
const [isDarkTheme, setIsDarkTheme] = useState(false);
//
const handleThemeChange = () => {
setIsDarkTheme(!isDarkTheme);
};
return (
<input
type="checkbox"
checked={isDarkTheme}
onChange={handleThemeChange}
className="toggle theme-controller"
value={isDarkTheme ? 'dark' : 'light'}
/>
);
}
export default ThemeToggle;

View File

@ -1,47 +0,0 @@
import React from 'react';
export const ConfigToggle = ({ label, checked, onChange }) => (
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">{label}</span>
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
className="toggle toggle-primary"
/>
</label>
</div>
);
export const ConfigInput = ({ label, value, onChange, type = "text", placeholder = "" }) => (
<div className="form-control">
<label className="label">
<span className="label-text">{label}</span>
</label>
<input
type={type}
value={value}
onChange={(e) => onChange(type === "number" ? parseInt(e.target.value) : e.target.value)}
placeholder={placeholder}
className="input input-bordered"
/>
</div>
);
export const ConfigSelect = ({ label, value, onChange, options }) => (
<div className="form-control">
<label className="label">
<span className="label-text">{label}</span>
</label>
<select
className="select select-bordered"
value={value}
onChange={(e) => onChange(parseInt(e.target.value))}
>
{options.map(item => (
<option key={item.value} value={item.value}>{item.label}</option>
))}
</select>
</div>
);

View File

@ -1,13 +0,0 @@
import { SIDEBAR_ITEMS } from "../constants/sidebar.js";
export function Content({ activeItem }) {
//
const currentItem = SIDEBAR_ITEMS.find(item => item.name === activeItem);
//
return (
<div>
{currentItem?.component || SIDEBAR_ITEMS[0].component}
</div>
);
}

View File

@ -1,156 +0,0 @@
import { useState, useEffect } from 'react';
import { BILI_CDN_SELECT_LIST, BILI_DOWNLOAD_METHOD, BILI_RESOLUTION_LIST } from "../../../constants/constant.js";
import { readYamlConfig, updateYamlConfig } from '../../utils/yamlHelper';
import Toast from "../toast.jsx";
import { ConfigToggle, ConfigInput, ConfigSelect } from '../common/ConfigItem';
//
const BILI_CONFIG = {
toggles: [
{ key: 'biliDisplayCover', label: '显示封面' },
{ key: 'biliDisplayInfo', label: '显示视频信息' },
{ key: 'biliDisplayIntro', label: '显示简介' },
{ key: 'biliDisplayOnline', label: '显示在线人数' },
{ key: 'biliDisplaySummary', label: '显示总结' },
{ key: 'biliUseBBDown', label: '使用BBDown' },
],
inputs: [
{ key: 'biliSessData', label: 'SESSDATA', type: 'text', placeholder: '请输入Bilibili SESSDATA' },
{ key: 'biliDuration', label: '视频时长限制(秒)', type: 'number' },
{ key: 'biliIntroLenLimit', label: '简介长度限制', type: 'number' },
],
selects: [
{ key: 'biliCDN', label: 'CDN选择', options: BILI_CDN_SELECT_LIST },
{ key: 'biliDownloadMethod', label: '下载方式', options: BILI_DOWNLOAD_METHOD },
{ key: 'biliResolution', label: '视频画质', options: BILI_RESOLUTION_LIST },
]
};
//
const DEFAULT_CONFIG = {
biliSessData: '',
biliDuration: 480,
biliIntroLenLimit: 50,
biliDisplayCover: true,
biliDisplayInfo: true,
biliDisplayIntro: true,
biliDisplayOnline: true,
biliDisplaySummary: false,
biliUseBBDown: false,
biliCDN: 0,
biliDownloadMethod: 0,
biliResolution: 5
};
export default function Bili() {
const [config, setConfig] = useState(DEFAULT_CONFIG);
const [loading, setLoading] = useState(false);
useEffect(() => {
const loadConfig = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
const newConfig = {};
Object.keys(DEFAULT_CONFIG).forEach(key => {
newConfig[key] = yamlConfig[key] ?? DEFAULT_CONFIG[key];
});
setConfig(newConfig);
}
};
loadConfig();
}, []);
const handleSave = async () => {
setLoading(true);
try {
const success = await updateYamlConfig(config);
if (success) {
document.getElementById('toast-success').classList.remove('hidden');
setTimeout(() => {
document.getElementById('toast-success').classList.add('hidden');
}, 3000);
}
} catch (error) {
console.error('保存配置失败:', error);
} finally {
setLoading(false);
}
};
const handleConfigChange = (key, value) => {
setConfig(prev => ({ ...prev, [key]: value }));
};
return (
<div className="p-6 mx-auto container">
<Toast id="toast-success" />
<div className="max-w-5xl mx-auto">
<h2 className="text-2xl font-bold mb-6">Bilibili 配置</h2>
<div className="card bg-base-200 shadow-xl mb-6">
<div className="card-body">
<h3 className="card-title mb-4">基础配置</h3>
{/* 输入框配置 */}
<div className="grid md:grid-cols-2 gap-4 mb-4">
{BILI_CONFIG.inputs.map(item => (
<ConfigInput
key={item.key}
label={item.label}
type={item.type}
value={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
placeholder={item.placeholder}
/>
))}
</div>
{/* 开关配置 */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
{BILI_CONFIG.toggles.map(item => (
<ConfigToggle
key={item.key}
label={item.label}
checked={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
/>
))}
</div>
{/* 选择框配置 */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
{BILI_CONFIG.selects.map(item => (
<ConfigSelect
key={item.key}
label={item.label}
value={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
options={item.options}
/>
))}
</div>
</div>
</div>
{/* 操作按钮 */}
<div className="flex justify-end gap-4">
<button
className="btn btn-ghost"
onClick={() => setConfig(DEFAULT_CONFIG)}
disabled={loading}
>
重置
</button>
<button
className="btn btn-primary"
onClick={handleSave}
disabled={loading}
>
{loading ? <span className="loading loading-spinner"></span> : '保存配置'}
</button>
</div>
</div>
</div>
);
}

View File

@ -1,359 +0,0 @@
import React, { useEffect, useState } from 'react';
import { readYamlConfig, updateYamlConfig } from '../../utils/yamlHelper';
import { ConfigInput, ConfigSelect, ConfigToggle } from '../common/ConfigItem';
import TagSelector from "../TagSelector.jsx";
import Toast from "../toast.jsx";
//
const GENERIC_CONFIG = {
basicInputs: [
{
key: 'defaultPath',
label: '视频保存路径',
type: 'text',
placeholder: '请输入视频保存路径...',
defaultValue: './data/rcmp4/'
},
{
key: 'videoSizeLimit',
label: '视频大小限制MB',
type: 'number',
hint: '超过限制转为群文件',
defaultValue: 70
}
],
proxyInputs: [
{
key: 'proxyAddr',
label: '魔法地址',
type: 'text',
placeholder: '请输入代理地址...',
defaultValue: '127.0.0.1'
},
{
key: 'proxyPort',
label: '魔法端口',
type: 'text',
placeholder: '请输入代理端口...',
defaultValue: '7890'
}
],
streamInputs: [
{
key: 'identifyPrefix',
label: '识别前缀',
type: 'text',
placeholder: '请输入识别前缀...',
defaultValue: ''
},
{
key: 'streamDuration',
label: '视频最大时长(秒)',
type: 'number',
defaultValue: 10
}
],
concurrencyInputs: [
{
key: 'queueConcurrency',
label: '队列并发数',
type: 'number',
hint: '仅影响B站下载',
defaultValue: 1
},
{
key: 'videoDownloadConcurrency',
label: '视频下载并发数',
type: 'number',
defaultValue: 1
}
],
textareas: [
{
key: 'deeplApiUrls',
label: 'DeepL API地址',
placeholder: '请输入DeepL API地址多个地址用逗号分隔...',
defaultValue: ''
}
],
toggles: [
{
key: 'streamCompatibility',
label: '兼容模式',
hint: 'NCQQ不用开启其他ICQQ、LLO需要开启',
defaultValue: false
}
],
otherInputs: [
{
key: 'xiaohongshuCookie',
label: '小红书Cookie',
type: 'text',
placeholder: '请输入小红书的Cookie...',
defaultValue: ''
},
{
key: 'autoclearTrashtime',
label: '自动清理时间',
type: 'text',
placeholder: '请输入Cron表达式...',
hint: 'Cron表达式',
defaultValue: '0 0 8 * * ?'
}
],
aiInputs: [
{
key: 'aiBaseURL',
label: 'AI接口地址',
type: 'text',
placeholder: '请输入AI接口地址...',
defaultValue: '',
hint: '用于识图的接口kimi默认接口为https://api.moonshot.cn其他服务商自己填写'
},
{
key: 'aiApiKey',
label: 'API Key',
type: 'text',
placeholder: '请输入API Key...',
defaultValue: '',
hint: '用于识图的api keykimi接口申请https://platform.moonshot.cn/console/api-keys'
}
],
aiSelects: [
{
key: 'aiModel',
label: 'AI模型',
options: [
{ value: 'moonshot-v1-8k', label: 'Moonshot V1 8K' },
{ value: 'moonshot-v1-32k', label: 'Moonshot V1 32K' },
{ value: 'moonshot-v1-128k', label: 'Moonshot V1 128K' },
//
],
defaultValue: 'moonshot-v1-8k',
hint: '模型使用kimi不用填写其他要填写'
}
]
};
//
const DEFAULT_CONFIG = Object.values(GENERIC_CONFIG).reduce((acc, group) => {
group.forEach(item => {
acc[item.key] = item.defaultValue;
});
return acc;
}, {});
export default function Generic() {
const [config, setConfig] = useState(DEFAULT_CONFIG);
const [loading, setLoading] = useState(false);
const [resolveOptions, setResolveOptions] = useState([]);
const [selectedResolveTags, setSelectedResolveTags] = useState([]);
useEffect(() => {
const loadConfig = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
const newConfig = {};
Object.keys(DEFAULT_CONFIG).forEach(key => {
newConfig[key] = yamlConfig[key] ?? DEFAULT_CONFIG[key];
});
setConfig(newConfig);
}
};
loadConfig();
}, []);
useEffect(() => {
//
const fetchResolveControl = async () => {
try {
const response = await fetch('/r/api/resolveControl');
const data = await response.json();
const enabledTags = data
.filter(item => item.value === 1)
.map(item => item.label);
setSelectedResolveTags(enabledTags);
setResolveOptions(data.map(item => item.label));
} catch (error) {
console.error('获取解析控制器配置失败:', error);
}
};
fetchResolveControl();
}, []);
const handleSave = async () => {
setLoading(true);
try {
const success = await updateYamlConfig(config);
if (success) {
document.getElementById('generic-toast-success').classList.remove('hidden');
setTimeout(() => {
document.getElementById('generic-toast-success').classList.add('hidden');
}, 3000);
}
} catch (error) {
console.error('保存配置失败:', error);
} finally {
setLoading(false);
}
};
const handleConfigChange = (key, value) => {
setConfig(prev => ({ ...prev, [key]: value }));
};
const handleResolveTagsChange = async (tags) => {
try {
const response = await fetch('/r/api/resolveControl', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ selectedTags: tags }),
});
if (response.ok) {
setSelectedResolveTags(tags);
}
} catch (error) {
console.error('更新解析控制器配置失败:', error);
}
};
//
const renderInputGroup = (inputs, title) => (
<div className="grid md:grid-cols-2 gap-4 mb-6">
{inputs.map(item => (
<div key={item.key} className="form-control">
<ConfigInput
label={item.label}
type={item.type}
value={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
placeholder={item.placeholder}
/>
{item.hint && (
<span className="text-xs text-base-content/70 mt-1">
{item.hint}
</span>
)}
</div>
))}
</div>
);
return (
<div className="p-6 mx-auto container">
<Toast id="generic-toast-success" />
<div className="max-w-5xl mx-auto">
<h2 className="text-2xl font-bold mb-6">通用配置</h2>
<div className="card bg-base-200 shadow-xl mb-6">
<div className="card-body">
<h3 className="card-title mb-4">基础配置</h3>
{/* 基础配置 */}
{renderInputGroup(GENERIC_CONFIG.basicInputs)}
{/* 解析控制 */}
<h4 className="font-semibold mt-6 mb-4">全局解析控制</h4>
<TagSelector
options={resolveOptions}
initialTags={selectedResolveTags}
onChange={handleResolveTagsChange}
/>
{/* 代理配置 */}
<h4 className="font-semibold mt-6 mb-4">代理设置</h4>
{renderInputGroup(GENERIC_CONFIG.proxyInputs)}
{/* 流媒体配置 */}
<h4 className="font-semibold mt-6 mb-4">流媒体设置</h4>
{renderInputGroup(GENERIC_CONFIG.streamInputs)}
{/* 并发配置 */}
<h4 className="font-semibold mt-6 mb-4">并发设置</h4>
{renderInputGroup(GENERIC_CONFIG.concurrencyInputs)}
{/* DeepL API配置 */}
<h4 className="font-semibold mt-6 mb-4">API设置</h4>
{GENERIC_CONFIG.textareas.map(item => (
<div key={item.key} className="form-control w-full mb-6">
<label className="label">
<span className="label-text">{item.label}</span>
</label>
<textarea
value={config[item.key]}
onChange={(e) => handleConfigChange(item.key, e.target.value)}
placeholder={item.placeholder}
className="textarea textarea-bordered h-24"
/>
</div>
))}
{/* 开关配置 */}
{GENERIC_CONFIG.toggles.map(item => (
<div key={item.key} className="form-control mb-6">
<ConfigToggle
label={item.label}
checked={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
/>
{item.hint && (
<span className="text-xs text-base-content/70 ml-2">
{item.hint}
</span>
)}
</div>
))}
{/* 其他配置 */}
<h4 className="font-semibold mt-6 mb-4">其他设置</h4>
{renderInputGroup(GENERIC_CONFIG.otherInputs)}
{/* AI配置 */}
<h4 className="font-semibold mt-6 mb-4">AI设置</h4>
{renderInputGroup(GENERIC_CONFIG.aiInputs)}
<div className="grid md:grid-cols-2 gap-4 mb-6">
{GENERIC_CONFIG.aiSelects.map(item => (
<div key={item.key} className="form-control">
<ConfigSelect
label={item.label}
value={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
options={item.options}
/>
{item.hint && (
<span className="text-xs text-base-content/70 mt-1">
{item.hint}
</span>
)}
</div>
))}
</div>
</div>
</div>
{/* 保存按钮 */}
<div className="flex justify-end gap-4">
<button
className="btn btn-ghost"
onClick={() => setConfig(DEFAULT_CONFIG)}
disabled={loading}
>
重置
</button>
<button
className="btn btn-primary"
onClick={handleSave}
disabled={loading}
>
{loading ? <span className="loading loading-spinner"></span> : '保存配置'}
</button>
</div>
</div>
</div>
);
}

View File

@ -1,14 +0,0 @@
import React from 'react';
import BotInfo from "../home/bot-info.jsx";
import Network from "../home/network.jsx";
import System from "../home/system.jsx";
export default function Home({ }) {
return (
<div className="container mx-auto p-8">
<BotInfo />
<System />
<Network />
</div>
);
}

View File

@ -1,194 +0,0 @@
import { useState, useEffect } from 'react';
import { NETEASECLOUD_QUALITY_LIST } from "../../../constants/constant.js";
import { readYamlConfig, updateYamlConfig } from '../../utils/yamlHelper';
import Toast from "../toast.jsx";
import { ConfigToggle, ConfigInput, ConfigSelect } from '../common/ConfigItem';
//
const NCM_CONFIG = {
textareas: [
{
key: 'neteaseCookie',
label: 'Cookie',
placeholder: '请输入网易云Cookie...'
}
],
inputs: [
{
key: 'neteaseCloudAPIServer',
label: '自建API服务器地址',
type: 'text',
placeholder: '请输入API服务器地址...'
},
{
key: 'neteaseUserId',
label: '用户ID',
type: 'text',
placeholder: '网易云用户ID',
hint: '不要手动更改!'
},
{
key: 'songRequestMaxList',
label: '点歌最大列表数',
type: 'number'
}
],
toggles: [
{ key: 'useLocalNeteaseAPI', label: '使用自建API' },
{ key: 'useNeteaseSongRequest', label: '开启点歌功能' },
{ key: 'isSendVocal', label: '发送群语音' }
],
selects: [
{
key: 'neteaseCloudAudioQuality',
label: '音频质量',
options: NETEASECLOUD_QUALITY_LIST
}
]
};
//
const DEFAULT_CONFIG = {
useLocalNeteaseAPI: false,
useNeteaseSongRequest: false,
isSendVocal: true,
songRequestMaxList: 10,
neteaseCookie: '',
neteaseCloudAPIServer: '',
neteaseCloudAudioQuality: 'exhigh',
neteaseUserId: ''
};
export default function Ncm() {
const [config, setConfig] = useState(DEFAULT_CONFIG);
const [loading, setLoading] = useState(false);
useEffect(() => {
const loadConfig = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
const newConfig = {};
Object.keys(DEFAULT_CONFIG).forEach(key => {
newConfig[key] = yamlConfig[key] ?? DEFAULT_CONFIG[key];
});
setConfig(newConfig);
}
};
loadConfig();
}, []);
const handleSave = async () => {
setLoading(true);
try {
const success = await updateYamlConfig(config);
if (success) {
document.getElementById('ncm-toast-success').classList.remove('hidden');
setTimeout(() => {
document.getElementById('ncm-toast-success').classList.add('hidden');
}, 3000);
}
} catch (error) {
console.error('保存配置失败:', error);
} finally {
setLoading(false);
}
};
const handleConfigChange = (key, value) => {
setConfig(prev => ({ ...prev, [key]: value }));
};
return (
<div className="p-6 mx-auto container">
<Toast id="ncm-toast-success" />
<div className="max-w-5xl mx-auto">
<h2 className="text-2xl font-bold mb-6">网易云音乐配置</h2>
<div className="card bg-base-200 shadow-xl mb-6">
<div className="card-body">
<h3 className="card-title mb-4">基础配置</h3>
{/* 文本域配置 */}
{NCM_CONFIG.textareas.map(item => (
<div key={item.key} className="form-control w-full mb-6">
<label className="label">
<span className="label-text">{item.label}</span>
</label>
<textarea
value={config[item.key]}
onChange={(e) => handleConfigChange(item.key, e.target.value)}
placeholder={item.placeholder}
className="textarea textarea-bordered h-24"
/>
</div>
))}
{/* 输入框配置 */}
<div className="space-y-4 mb-6">
{NCM_CONFIG.inputs.map(item => (
<div key={item.key} className="form-control w-full">
<ConfigInput
label={item.label}
type={item.type}
value={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
placeholder={item.placeholder}
/>
{item.hint && (
<span className="text-xs text-warning mt-1">
{item.hint}
</span>
)}
</div>
))}
</div>
{/* 开关配置 */}
<div className="grid md:grid-cols-2 gap-4 mb-6">
{NCM_CONFIG.toggles.map(item => (
<ConfigToggle
key={item.key}
label={item.label}
checked={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
/>
))}
</div>
{/* 选择框配置 */}
<div className="grid md:grid-cols-2 gap-4">
{NCM_CONFIG.selects.map(item => (
<ConfigSelect
key={item.key}
label={item.label}
value={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
options={item.options}
/>
))}
</div>
</div>
</div>
{/* 保存按钮 */}
<div className="flex justify-end gap-4">
<button
className="btn btn-ghost"
onClick={() => setConfig(DEFAULT_CONFIG)}
disabled={loading}
>
重置
</button>
<button
className="btn btn-primary"
onClick={handleSave}
disabled={loading}
>
{loading ? <span className="loading loading-spinner"></span> : '保存配置'}
</button>
</div>
</div>
</div>
);
}

View File

@ -1,147 +0,0 @@
import { useState, useEffect } from 'react';
import { readYamlConfig, updateYamlConfig } from '../../utils/yamlHelper';
import Toast from "../toast.jsx";
import { ConfigToggle } from '../common/ConfigItem';
//
const TIKTOK_CONFIG = {
textareas: [
{
key: 'douyinCookie',
label: 'Cookie',
placeholder: '请输入抖音Cookie...',
hint: '格式odin_tt=xxx;passport_fe_beating_status=xxx;...'
}
],
toggles: [
{
key: 'douyinCompression',
label: '视频压缩',
hint: '开启后使用压缩格式,加速视频发送'
},
{
key: 'douyinComments',
label: '显示评论',
hint: '是否显示视频评论'
}
]
};
//
const DEFAULT_CONFIG = {
douyinCookie: '',
douyinCompression: true,
douyinComments: false
};
export default function Tiktok() {
const [config, setConfig] = useState(DEFAULT_CONFIG);
const [loading, setLoading] = useState(false);
useEffect(() => {
const loadConfig = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
const newConfig = {};
Object.keys(DEFAULT_CONFIG).forEach(key => {
newConfig[key] = yamlConfig[key] ?? DEFAULT_CONFIG[key];
});
setConfig(newConfig);
}
};
loadConfig();
}, []);
const handleSave = async () => {
setLoading(true);
try {
const success = await updateYamlConfig(config);
if (success) {
document.getElementById('tiktok-toast-success').classList.remove('hidden');
setTimeout(() => {
document.getElementById('tiktok-toast-success').classList.add('hidden');
}, 3000);
}
} catch (error) {
console.error('保存配置失败:', error);
} finally {
setLoading(false);
}
};
const handleConfigChange = (key, value) => {
setConfig(prev => ({ ...prev, [key]: value }));
};
return (
<div className="p-6 mx-auto container">
<Toast id="tiktok-toast-success" />
<div className="max-w-5xl mx-auto">
<h2 className="text-2xl font-bold mb-6">抖音配置</h2>
<div className="card bg-base-200 shadow-xl mb-6">
<div className="card-body">
<h3 className="card-title mb-4">基础配置</h3>
{/* Cookie配置 */}
{TIKTOK_CONFIG.textareas.map(item => (
<div key={item.key} className="form-control w-full mb-6">
<label className="label">
<span className="label-text">{item.label}</span>
{item.hint && (
<span className="label-text-alt text-xs text-base-content/70">
{item.hint}
</span>
)}
</label>
<textarea
value={config[item.key]}
onChange={(e) => handleConfigChange(item.key, e.target.value)}
placeholder={item.placeholder}
className="textarea textarea-bordered h-24"
/>
</div>
))}
{/* 开关配置 */}
<div className="grid md:grid-cols-2 gap-4">
{TIKTOK_CONFIG.toggles.map(item => (
<div key={item.key}>
<ConfigToggle
label={item.label}
checked={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
/>
{item.hint && (
<span className="text-xs text-base-content/70 ml-2">
{item.hint}
</span>
)}
</div>
))}
</div>
</div>
</div>
{/* 保存按钮 */}
<div className="flex justify-end gap-4">
<button
className="btn btn-ghost"
onClick={() => setConfig(DEFAULT_CONFIG)}
disabled={loading}
>
重置
</button>
<button
className="btn btn-primary"
onClick={handleSave}
disabled={loading}
>
{loading ? <span className="loading loading-spinner"></span> : '保存配置'}
</button>
</div>
</div>
</div>
);
}

View File

@ -1,11 +0,0 @@
export default function Weekly() {
return (
<div style={ { width: '100%', height: '100vh' } }>
<iframe
src="https://rrorangeandfriends.site"
style={ { width: '100%', height: '100%', border: 'none' } }
title="External Website"
/>
</div>
)
}

View File

@ -1,126 +0,0 @@
import { useState, useEffect } from 'react';
import { YOUTUBE_GRAPHICS_LIST } from "../../../constants/constant.js";
import { readYamlConfig, updateYamlConfig } from '../../utils/yamlHelper';
import Toast from "../toast.jsx";
import { ConfigInput, ConfigSelect } from '../common/ConfigItem';
//
const YOUTUBE_CONFIG = {
inputs: [
{ key: 'youtubeCookiePath', label: 'Cookie文件路径', type: 'text', placeholder: '请输入Cookie.txt文件路径...' },
{ key: 'youtubeClipTime', label: '最大截取时长(秒)', type: 'number', hint: '建议不超过5分钟' },
{ key: 'youtubeDuration', label: '视频时长限制(秒)', type: 'number', hint: '建议不超过30分钟' }
],
selects: [
{ key: 'youtubeGraphicsOptions', label: '下载画质', options: YOUTUBE_GRAPHICS_LIST, hint: '0为原画' }
]
};
//
const DEFAULT_CONFIG = {
youtubeGraphicsOptions: 720,
youtubeClipTime: 0,
youtubeDuration: 480,
youtubeCookiePath: ''
};
export default function Youtube() {
const [config, setConfig] = useState(DEFAULT_CONFIG);
const [loading, setLoading] = useState(false);
useEffect(() => {
const loadConfig = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
const newConfig = {};
Object.keys(DEFAULT_CONFIG).forEach(key => {
newConfig[key] = yamlConfig[key] ?? DEFAULT_CONFIG[key];
});
setConfig(newConfig);
}
};
loadConfig();
}, []);
const handleSave = async () => {
setLoading(true);
try {
const success = await updateYamlConfig(config);
if (success) {
document.getElementById('youtube-toast-success').classList.remove('hidden');
setTimeout(() => {
document.getElementById('youtube-toast-success').classList.add('hidden');
}, 3000);
}
} catch (error) {
console.error('保存配置失败:', error);
} finally {
setLoading(false);
}
};
const handleConfigChange = (key, value) => {
setConfig(prev => ({ ...prev, [key]: value }));
};
return (
<div className="p-6 mx-auto container">
<Toast id="youtube-toast-success" />
<div className="max-w-5xl mx-auto">
<h2 className="text-2xl font-bold mb-6">YouTube 配置</h2>
<div className="card bg-base-200 shadow-xl mb-6">
<div className="card-body">
<h3 className="card-title mb-4">基础配置</h3>
{/* 输入框配置 */}
<div className="grid md:grid-cols-2 gap-4 mb-4">
{YOUTUBE_CONFIG.inputs.map(item => (
<ConfigInput
key={item.key}
label={item.label}
type={item.type}
value={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
placeholder={item.placeholder}
/>
))}
</div>
{/* 选择框配置 */}
<div className="grid md:grid-cols-2 gap-4">
{YOUTUBE_CONFIG.selects.map(item => (
<ConfigSelect
key={item.key}
label={item.label}
value={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
options={item.options}
/>
))}
</div>
</div>
</div>
{/* 保存按钮 */}
<div className="flex justify-end gap-4">
<button
className="btn btn-ghost"
onClick={() => setConfig(DEFAULT_CONFIG)}
disabled={loading}
>
重置
</button>
<button
className="btn btn-primary"
onClick={handleSave}
disabled={loading}
>
{loading ? <span className="loading loading-spinner"></span> : '保存配置'}
</button>
</div>
</div>
</div>
);
}

View File

@ -1,67 +0,0 @@
"use client"
import { useState, useEffect } from 'react';
import { BOT_INFO_URL } from "../constants/api.js";
import { useDrawer } from "../contexts/drawer-context.js";
import ThemeToggle from "./ThemeToggle.jsx";
export default function Header () {
const { toggleDrawer } = useDrawer();
const [user, setUser] = useState(null);
useEffect(() => {
fetch(BOT_INFO_URL)
.then(response => {
return response.json();
})
.then(data => setUser(data))
}, []);
return (
<div className="navbar bg-base-100 p-3">
<div className="navbar-start">
<button className="btn btn-square btn-ghost" onClick={ toggleDrawer }>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="inline-block h-5 w-5 stroke-current">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
<div className="navbar-center">
<div className="avatar">
<div className="w-10 rounded-full">
<img
alt="Logo"
src="https://s2.loli.net/2024/08/19/ty5K6P3hsAaXC47.webp"/>
</div>
</div>
<a className="btn btn-ghost text-xl">R插件控制台</a>
</div>
<div className="navbar-end">
<ThemeToggle />
<div className="flex flex-row">
<div tabIndex={ 0 } role="button" className="btn btn-ghost btn-circle avatar mr-2">
<div className="w-10 rounded-full">
<img
alt="头像"
src={`http://q1.qlogo.cn/g?b=qq&nk=${user?.user_id}&s=100`}/>
</div>
</div>
<div className="mt-1.5">
<div className="font-bold">{user?.nickname || "未获取"}</div>
<div className="text-sm opacity-50">{user?.user_id || "NaN"}</div>
</div>
</div>
</div>
</div>
)
};

View File

@ -1,106 +0,0 @@
import { useEffect, useState } from "react";
import { GIT_COMMIT_URL, GIT_VERSION_URL } from "../../constants/api.js";
export function BotConfig() {
const [version, setVersion] = useState("v0.0.0");
const [commit, setCommit] = useState(null);
const [updating, setUpdating] = useState(false);
const [updateMessage, setUpdateMessage] = useState("");
useEffect(() => {
fetch(GIT_VERSION_URL).then(response => response.json()).then(data => setVersion(data.name));
fetch(GIT_COMMIT_URL).then(response => response.json()).then(data => setCommit(data));
const checkUpdateStatus = async () => {
try {
const response = await fetch('/r/api/update?check=true');
const data = await response.json();
if (data.needsRestore) {
setUpdateMessage("检测到未完成的更新,正在恢复配置...");
const restoreResponse = await fetch('/r/api/update?restore=true');
const restoreData = await restoreResponse.json();
setUpdateMessage(restoreData.message);
}
} catch (error) {
console.error('检查更新状态失败:', error);
}
};
checkUpdateStatus();
}, []);
const handleUpdate = async (isForce = false) => {
try {
setUpdating(true);
setUpdateMessage("正在更新中...");
const response = await fetch(`/r/api/update?force=${isForce}`);
const data = await response.json();
if (data.success) {
setUpdateMessage(data.message);
fetch(GIT_VERSION_URL).then(response => response.json()).then(data => setVersion(data.name));
fetch(GIT_COMMIT_URL).then(response => response.json()).then(data => setCommit(data));
} else {
setUpdateMessage(`更新失败:${data.message}`);
}
} catch (error) {
setUpdateMessage(`更新出错:${error.message}`);
} finally {
setUpdating(false);
}
};
return (
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title text-lg font-bold">🔥更新看板</h2>
<div className="grid grid-cols-1 gap-2">
<div className="flex justify-between items-center">
<div>
<h3 className="font-bold">最新版本</h3>
<p>当前最新版本为{version}</p>
</div>
<button className="btn btn-ghost"
onClick={() => fetch(GIT_VERSION_URL)
.then(response => response.json())
.then(data => setVersion(data.name))}>
检查更新
</button>
</div>
<div className="flex justify-between items-center">
<div>
<h3 className="font-bold">更新操作</h3>
<p>选择更新方式进行更新</p>
{updateMessage && (
<p className={`text-sm ${updateMessage.includes('失败') || updateMessage.includes('错') ? 'text-error' : 'text-success'}`}>
{updateMessage}
</p>
)}
</div>
<div className="flex gap-2">
<button
className={`btn btn-ghost ${updating ? 'loading' : ''}`}
onClick={() => handleUpdate(false)}
disabled={updating}>
普通更新
</button>
<button
className={`btn btn-warning ${updating ? 'loading' : ''}`}
onClick={() => handleUpdate(true)}
disabled={updating}>
强制更新
</button>
</div>
</div>
<div className="flex justify-between items-center">
<div>
<h3 className="font-bold">最近更新</h3>
<span><a href={commit?.url}>[{commit?.author}]{commit?.message}</a></span>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,17 +0,0 @@
import { BotConfig } from "./bot-config.jsx";
import { BotItem } from "./bot-item.jsx";
import { BotNetwork } from "./bot-network.jsx";
export default function BotInfo() {
return (
<div className="container mx-auto p-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* 机器人信息卡片 */ }
<BotItem />
<BotNetwork />
<BotConfig />
</div>
</div>
)
}

View File

@ -1,63 +0,0 @@
import React, { useEffect, useState } from "react";
import { BOT_INFO_URL } from "../../constants/api.js";
export function BotItem() {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const fetchBotInfo = async () => {
setIsLoading(true);
try {
const response = await fetch(BOT_INFO_URL);
const data = await response.json();
setUser(data);
} catch (error) {
console.error("获取机器人信息失败:", error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchBotInfo();
}, []);
return (
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">🐔状态</h2>
<div className="flex flex-col sm:flex-row pt-5 items-center sm:items-start gap-6 sm:gap-8">
<div className={`avatar z-0 ${user?.online ? "online" : "offline"}`}>
<div className="w-24 rounded-full">
<img src={`http://q1.qlogo.cn/g?b=qq&nk=${user?.user_id}&s=100`} alt="Bot Avatar" />
</div>
</div>
<div className="flex flex-col space-y-4 text-center sm:text-left">
<div className="space-y-2">
<div className="font-bold">昵称{user?.nickname || "未获取"}</div>
<div className="text-sm opacity-50">QQ号{user?.user_id || "NaN"}</div>
</div>
<div className="space-y-2">
<div className="font-bold">协议信息</div>
<div className="flex flex-wrap justify-center sm:justify-start gap-2">
<div className="badge badge-ghost">{user?.app_name}</div>
<div className="badge badge-ghost">{user?.app_version}</div>
<div className="badge badge-ghost">{user?.protocol_version}</div>
</div>
</div>
</div>
</div>
<div className="card-actions justify-end mt-4">
<button
className={`btn btn-sm btn-ghost ${isLoading ? 'loading' : ''}`}
onClick={fetchBotInfo}
disabled={isLoading}
>
{isLoading ? '刷新中...' : '刷新信息'}
</button>
</div>
</div>
</div>
)
}

View File

@ -1,105 +0,0 @@
import React, { useEffect, useState } from "react";
import { NETWORK_BASE_URL } from "../../constants/api.js";
//
const TESTING_LINKS = [
{
name: "bilibili",
url: "https://bilibili.com/",
icon: (
<svg t="1732252062839" className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4273" width="48" height="48"><path d="M729.32864 373.94944c-9.79456-5.94432-19.06176-6.784-19.14368-6.784l-1.06496-0.0512c-57.20064-3.8656-121.1648-5.83168-190.12608-5.83168l-13.98784 0.00512c-68.95616 0-132.92544 1.96096-190.12096 5.83168l-1.06496 0.0512c-0.08192 0-9.34912 0.83968-19.14368 6.784-15.04768 9.12896-24.27392 25.94816-27.4176 49.9712-10.07104 76.91264-4.38272 173.64992 0.18944 251.392 2.93888 49.96608 33.408 62.45888 85.04832 67.1488 10.78272 0.98816 69.08928 5.86752 159.50848 5.89312v-0.00512c90.4192-0.02048 148.72576-4.90496 159.5136-5.888 51.64032-4.68992 82.10944-17.18272 85.0432-67.1488 4.57728-77.74208 10.26048-174.47936 0.18944-251.392-3.1488-24.02816-12.37504-40.84736-27.42272-49.97632z m-390.9888 172.71808a23.64928 23.64928 0 0 1-31.68768-10.84416 23.68 23.68 0 0 1 10.84416-31.68768c2.03776-1.00352 50.69312-24.72448 110.5408-43.06432a23.68 23.68 0 1 1 13.88032 45.29152c-56.2944 17.24928-103.11168 40.07424-103.5776 40.30464z m268.89728 35.88608c-0.44032 2.23232-11.26912 54.64064-50.93888 54.64064-21.44256 0-36.10112-14.04928-44.98432-26.77248-8.69376 12.70784-22.80448 26.77248-42.65472 26.77248-35.5328 0-50.13504-48.26624-51.68128-53.77024a11.3664 11.3664 0 0 1 21.87776-6.1696c2.74944 9.6512 14.1312 37.20192 29.7984 37.20192 16.37376 0 28.89216-23.64416 31.98464-31.92832a11.37152 11.37152 0 0 1 10.6496-7.38816h0.06144c4.76672 0.03072 9.0112 3.02592 10.62912 7.50592 0.10752 0.28672 11.96544 31.81568 34.31424 31.81568 20.864 0 28.56448-35.95264 28.64128-36.32128a11.34592 11.34592 0 0 1 13.35808-8.93952 11.36128 11.36128 0 0 1 8.94464 13.35296z m110.11584-46.73536a23.68 23.68 0 0 1-31.68256 10.84416c-0.47104-0.2304-47.47264-23.1168-103.57248-40.30976a23.69024 23.69024 0 0 1-15.70816-29.58336 23.66976 23.66976 0 0 1 29.57824-15.70304c59.84768 18.33984 108.49792 42.0608 110.55104 43.06432a23.68 23.68 0 0 1 10.83392 31.68768z" fill="#F16C8D" p-id="4274"></path><path d="M849.92 51.2H174.08c-67.8656 0-122.88 55.0144-122.88 122.88v675.84c0 67.87072 55.0144 122.88 122.88 122.88h675.84c67.87072 0 122.88-55.00928 122.88-122.88V174.08c0-67.86048-55.00928-122.88-122.88-122.88z m-36.60288 627.45088c-2.62656 44.57984-21.82144 78.63296-55.51616 98.48832-25.68192 15.13472-54.17472 19.48672-81.13664 21.9392-32.45568 2.94912-92.71808 6.09792-164.66432 6.1184-71.94112-0.02048-132.20864-3.16416-164.66432-6.1184-26.96192-2.45248-55.45472-6.80448-81.13152-21.9392-33.69472-19.85536-52.8896-53.90336-55.51104-98.4832-4.70528-80.13312-10.5728-179.85536 0.19456-262.10816C221.5424 335.16544 280.99072 311.57248 311.5008 310.37952a2482.64192 2482.64192 0 0 1 81.42336-4.08576c-7.53664-8.53504-19.88096-23.3216-28.81536-38.11328-13.73696-22.73792 8.52992-41.68704 8.52992-41.68704s23.68-20.36736 44.52864 5.21216c15.69792 19.26656 38.37952 55.99744 48.61952 72.95488l53.20704-0.21504c13.2608 0 26.33216 0.07168 39.2192 0.21504 10.24-16.95744 32.9216-53.6832 48.61952-72.95488 20.84352-25.57952 44.52864-5.21216 44.52864-5.21216s22.26176 18.94912 8.5248 41.68704c-8.9344 14.79168-21.27872 29.57824-28.81536 38.11328 28.35968 0.97792 55.56224 2.33984 81.42336 4.08064 30.5152 1.19808 89.9584 24.79104 100.61312 106.17344 10.7776 82.24768 4.9152 181.96992 0.20992 262.10304z" fill="#F16C8D" p-id="4275"></path></svg>
)
},
{
name: "Github",
url: "https://github.com/",
icon: (
<svg t="1732252105658" className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5325" width="48" height="48"><path d="M512 512m-512 0a512 512 0 1 0 1024 0 512 512 0 1 0-1024 0Z" fill="#161614" p-id="5326"></path><path d="M411.306667 831.146667c3.413333-5.12 6.826667-10.24 6.826666-11.946667v-69.973333c-105.813333 22.186667-128-44.373333-128-44.373334-17.066667-44.373333-42.666667-56.32-42.666666-56.32-34.133333-23.893333 3.413333-23.893333 3.413333-23.893333 37.546667 3.413333 58.026667 39.253333 58.026667 39.253333 34.133333 58.026667 88.746667 40.96 110.933333 32.426667 3.413333-23.893333 13.653333-40.96 23.893333-51.2-85.333333-10.24-174.08-42.666667-174.08-187.733333 0-40.96 15.36-75.093333 39.253334-102.4-3.413333-10.24-17.066667-47.786667 3.413333-100.693334 0 0 32.426667-10.24 104.106667 39.253334 30.72-8.533333 63.146667-11.946667 95.573333-11.946667 32.426667 0 64.853333 5.12 95.573333 11.946667 73.386667-49.493333 104.106667-39.253333 104.106667-39.253334 20.48 52.906667 8.533333 90.453333 3.413333 100.693334 23.893333 27.306667 39.253333 59.733333 39.253334 102.4 0 145.066667-88.746667 177.493333-174.08 187.733333 13.653333 11.946667 25.6 34.133333 25.6 69.973333v104.106667c0 3.413333 1.706667 6.826667 6.826666 11.946667 5.12 6.826667 3.413333 18.773333-3.413333 23.893333-3.413333 1.706667-6.826667 3.413333-10.24 3.413333h-174.08c-10.24 0-17.066667-6.826667-17.066667-17.066666 0-5.12 1.706667-8.533333 3.413334-10.24z" fill="#FFFFFF" p-id="5327"></path></svg>
)
},
{
name: "YouTube",
url: "https://youtube.com/",
icon: (
<svg t="1732252124556" className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6401" width="48" height="48"><path d="M512.254947 959.556658c247.190491 0 447.583279-200.392788 447.583279-447.569969 0-247.161822-200.392788-447.544371-447.583279-447.544371-247.188443 0-447.556658 200.382549-447.556657 447.544371 0 247.178204 200.368215 447.569968 447.556657 447.569969" fill="#E9644A" p-id="6402"></path><path d="M599.154143 512.218088l-146.531301 86.062681V426.14312l146.531301 86.074968z m136.892444 79.792407V431.989505s0-77.134401-77.14464-77.134401H365.584399s-77.094469 0-77.09447 77.134401v160.019966s0 77.12109 77.09447 77.12109h293.318572c-0.001024 0 77.143616 0 77.143616-77.120066" fill="#FFFFFF" p-id="6403"></path></svg>
)
},
{
name: "Tiktok",
url: "https://tiktok.com/",
icon: (
<svg t="1732252172100" className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7825" width="48" height="48"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m294.4 390.8c-57.8 0.6-111.8-16.8-159.4-49.2v227.3c0.2 48.8-16.2 96.2-46.5 134.4s-72.7 65-120.2 76c-133.4 30.5-246.2-66-260.4-181.3-15-115.3 58.5-216.3 171.1-239 22-4.5 54.2-4.5 72.1-0.6v121.7c-5.1-1.3-10-2.6-15.2-3.2-44-7.8-86.6 14.2-104.1 54.4-8.6 19.6-10.3 41.5-4.9 62.2 5.4 20.7 17.7 38.9 34.7 51.8 29.7 23.3 62.8 26.5 96.5 11 33.7-14.9 51.6-42.1 55.5-79 0.6-5.2 0.5-11 0.5-16.8v-437c0-12.3 0.4-12.2 12.7-12.2h96.5c7.1 0 9.7 1.2 10.4 9.7 5.1 75.1 62.2 139.1 135.3 148.8 7.8 1.3 16.3 1.9 25.3 2.5v118.5z" fill="#252F3F" p-id="7826"></path></svg>
)
},
// ...
];
export function BotNetwork() {
const [linksTime, setLinksTime] = useState(new Array(TESTING_LINKS.length).fill('NaN ms'));
const [isLoading, setIsLoading] = useState(false);
//
const testSingleLink = async (url, index) => {
try {
const response = await fetch(NETWORK_BASE_URL + url);
const data = await response.json();
setLinksTime(prev => {
const newTimes = [...prev];
newTimes[index] = `${data.time}ms`;
return newTimes;
});
} catch (error) {
console.error(`测试链接失败: ${url}`, error);
setLinksTime(prev => {
const newTimes = [...prev];
newTimes[index] = '超时';
return newTimes;
});
}
};
//
const handleTestAll = async () => {
setIsLoading(true);
setLinksTime(new Array(TESTING_LINKS.length).fill('测试中...'));
try {
await Promise.all(
TESTING_LINKS.map((link, index) =>
testSingleLink(link.url, index)
)
);
} finally {
setIsLoading(false);
}
};
return (
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<div className="flex justify-between items-center">
<h2 className="card-title">🌐网速</h2>
</div>
<div>
<div className="flex flex-row pt-5 justify-between items-center">
{ TESTING_LINKS.map((link, index) => (
<div key={ link.url } className="flex flex-col items-center space-y-4">
{ link.icon }
<span className="badge badge-ghost">{ linksTime[index] }</span>
</div>
)) }
</div>
<div className="flex flex-row pt-5 justify-center items-center">
<button
className={ `btn btn-sm btn-ghost ${ isLoading ? 'loading loading-dots loading-xs' : '' }` }
onClick={ handleTestAll }
disabled={ isLoading }
>
{ isLoading ? '' : '一键测速' }
</button>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,147 +0,0 @@
import React, { useEffect, useState } from "react";
import { Line } from "react-chartjs-2";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
} from 'chart.js';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
const MAX_DATA_POINTS = 30;
export default function Network() {
const [networkData, setNetworkData] = useState({
uploadSpeed: 0,
downloadSpeed: 0,
totalSent: 0,
totalReceived: 0
});
const [chartData, setChartData] = useState({
labels: [],
datasets: [
{
label: '上传速度 (KB/s)',
data: [],
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
},
{
label: '下载速度 (KB/s)',
data: [],
borderColor: 'rgb(255, 99, 132)',
tension: 0.1
}
]
});
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('/r/api/network2');
const data = await response.json();
setNetworkData({
uploadSpeed: data.uploadSpeed,
downloadSpeed: data.downloadSpeed,
totalSent: data.totalSent,
totalReceived: data.totalReceived
});
setChartData(prevData => {
const newLabels = [...prevData.labels, new Date().toLocaleTimeString()];
const newUploadData = [...prevData.datasets[0].data, data.uploadSpeed];
const newDownloadData = [...prevData.datasets[1].data, data.downloadSpeed];
// 30
if (newLabels.length > MAX_DATA_POINTS) {
newLabels.shift();
newUploadData.shift();
newDownloadData.shift();
}
return {
labels: newLabels,
datasets: [
{
...prevData.datasets[0],
data: newUploadData
},
{
...prevData.datasets[1],
data: newDownloadData
}
]
};
});
} catch (error) {
console.error('获取网络数据失败:', error);
}
};
//
const interval = setInterval(fetchData, 1000);
return () => clearInterval(interval);
}, []);
const chartOptions = {
responsive: true,
animation: {
duration: 0
},
scales: {
y: {
beginAtZero: true
}
},
plugins: {
legend: {
position: 'top'
}
}
};
return (
<div className="container mx-auto p-8">
<div className="grid grid-cols-1 gap-8">
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title text-lg font-bold">网络监控</h2>
<div className="flex justify-between mb-4">
<div>
<p>上传: {networkData.uploadSpeed} KB/s</p>
<p>下载: {networkData.downloadSpeed} KB/s</p>
</div>
<div>
<p>总发送: {networkData.totalSent} GB</p>
<p>总接收: {networkData.totalReceived} GB</p>
</div>
</div>
<div className="w-full h-[300px]">
<Line
data={chartData}
options={{
...chartOptions,
maintainAspectRatio: false,
}}
/>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,116 +0,0 @@
import React, { useEffect, useState } from 'react';
import { CircularProgressbar, buildStyles } from 'react-circular-progressbar';
import 'react-circular-progressbar/dist/styles.css';
import { SYSTEM_BASE_URL } from "../../constants/api.js";
export default function System() {
const [systemInfo, setSystemInfo] = useState(null);
useEffect(() => {
async function fetchSystemInfo() {
const response = await fetch(SYSTEM_BASE_URL);
const data = await response.json();
setSystemInfo(data);
}
const intervalId = setInterval(fetchSystemInfo, 5000); // 5
return () => clearInterval(intervalId); //
}, []);
return (
<div className="container mx-auto p-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* 状态卡片 */ }
<div className="card bg-base-100 shadow-xl col-span-1 lg:col-span-2">
<div className="card-body">
<h2 className="card-title text-lg font-bold">状态</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 mt-5">
<div className="flex flex-col items-center">
<div style={ { width: 120, height: 120 } }>
<CircularProgressbar
value={ systemInfo ? parseFloat(systemInfo.cpuUsage) : 0 }
text={ systemInfo ? systemInfo.cpuUsage + "%" : "" }
styles={ buildStyles({
textSize: '18px',
pathColor: `rgba(62, 152, 199, ${ systemInfo ? parseFloat(systemInfo.cpuUsage) / 100 : 0 })`,
textColor: '#3b82f6',
trailColor: '#d6d6d6',
backgroundColor: '#f0f0f0',
}) }
/>
</div>
<span className="text mt-4">CPU</span>
<span
className="text-sm mt-1">{ systemInfo ? `( ${ systemInfo.cpuCoresUsed } / ${ systemInfo.totalCpuCores } ) 核` : "" }</span>
</div>
<div className="flex flex-col items-center">
<div style={ { width: 120, height: 120 }}>
<CircularProgressbar
value={systemInfo ? parseFloat(systemInfo.memoryUsage) : 0}
text={systemInfo ? systemInfo.memoryUsage + "%" : ""}
styles={buildStyles({
textSize: '18px',
pathColor: `rgba(62, 152, 199, ${systemInfo ? parseFloat(systemInfo.memoryUsage) / 100 : 0})`,
textColor: '#3b82f6',
trailColor: '#d6d6d6',
backgroundColor: '#f0f0f0',
})}
/>
</div>
<span className="text mt-4">内存</span>
<span className="text-sm mt-1">{systemInfo ? `${systemInfo.usedMemory} / ${systemInfo.totalMemory}` : ""}</span>
</div>
<div className="flex flex-col items-center">
<div style={{ width: 120, height: 120 }}>
<CircularProgressbar
value={systemInfo ? parseFloat(systemInfo.diskUsage) : 0}
text={systemInfo ? systemInfo.diskUsage + "%" : ""}
styles={buildStyles({
textSize: '18px',
pathColor: `rgba(62, 152, 199, ${systemInfo ? parseFloat(systemInfo.diskUsage) / 100 : 0})`,
textColor: '#3b82f6',
trailColor: '#d6d6d6',
backgroundColor: '#f0f0f0',
})}
/>
</div>
<span className="text mt-4">磁盘使用</span>
<span className="text-sm mt-1">{systemInfo ? `${systemInfo.usedDisk} / ${systemInfo.totalDisk}` : ""}</span>
</div>
<div className="flex flex-col items-center">
<div style={{ width: 120, height: 120 }}>
<CircularProgressbar
value={systemInfo ? parseFloat(systemInfo.loadAverage) : 0}
text={systemInfo ? systemInfo.loadAverage + "%" : ""}
styles={buildStyles({
textSize: '18px',
pathColor: `rgba(62, 152, 199, ${systemInfo ? parseFloat(systemInfo.loadAverage) / 100 : 0})`,
textColor: '#3b82f6',
trailColor: '#d6d6d6',
backgroundColor: '#f0f0f0',
})}
/>
</div>
<span className="text mt-4">负载</span>
</div>
</div>
</div>
</div>
{/* 系统信息卡片 */ }
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title text-lg font-bold">系统信息</h2>
<p className="text">主机名称: {systemInfo ? systemInfo.hostname : ""}</p>
<p className="text">发行版本: {systemInfo ? systemInfo.distro : ""}</p>
<p className="text">内核版本: {systemInfo ? systemInfo.kernelVersion : ""}</p>
<p className="text">系统类型: {systemInfo ? systemInfo.arch : ""}</p>
<p className="text">主机地址: {systemInfo ? systemInfo.ipAddress : ""}</p>
<p className="text">运行时间: {systemInfo ? systemInfo.uptime : ""}</p>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,45 +0,0 @@
"use client"
import { useState } from "react";
import { SIDEBAR_ITEMS } from "../constants/sidebar.js";
import { useDrawer } from "../contexts/drawer-context.js";
import { Content } from "./content.jsx";
export default function Sidebar() {
const { isDrawerOpen, toggleDrawer } = useDrawer();
const [activeItem, setActiveItem] = useState("总控制台");
//
const [theme, setTheme] = useState("light");
//
const toggleTheme = (newTheme) => {
setTheme(newTheme); //
document.documentElement.setAttribute("data-theme", newTheme); //
};
return (
<div className="drawer">
<input id="my-drawer" type="checkbox" className="drawer-toggle hidden" checked={ isDrawerOpen } readOnly/>
<div className="drawer-content">
<Content activeItem={activeItem} />
</div>
<div className="drawer-side fixed top-16 left-0 h-[calc(100%-4rem)]">
<label htmlFor="my-drawer" aria-label="close sidebar" className="drawer-overlay"
onClick={ toggleDrawer }></label>
<ul className="menu bg-base-200 text-base-content w-80 p-4 h-full overflow-y-auto">
{SIDEBAR_ITEMS.map((item) => (
<li key={item.name} onClick={() => setActiveItem(item.name)}>
<a className={activeItem === item.name ? "active" : ""} onClick={() => toggleTheme(item.theme)}>
{item.icon}
{item.name}
</a>
</li>
))}
</ul>
</div>
</div>
);
}

View File

@ -1,9 +0,0 @@
export default function Toast({ id }) {
return (
<div id={ id } className="toast toast-top toast-end hidden z-[9999]">
<div className="alert alert-success">
<span>配置保存成功</span>
</div>
</div>
);
};

View File

@ -1,11 +0,0 @@
const BASE_URL = "/r/api";
export const SYSTEM_BASE_URL = `${BASE_URL}/system`;
export const NETWORK_BASE_URL = `${BASE_URL}/network?url=`;
export const BOT_INFO_URL = `${ BASE_URL }/bot`;
export const GIT_VERSION_URL = `${ BASE_URL }/version`;
export const GIT_COMMIT_URL = `${ BASE_URL }/commit`;

View File

@ -1,5 +0,0 @@
export const REDIS_UPDATE_STATUS = "Yz:rconsole:update:status"
export const REDIS_UPDATE_PATH = "Yz:rconsole:update:paths"
export const REDIS_RESOLVE_CONTROLLER = "Yz:rconsole:resolve:controller"

View File

@ -1,78 +0,0 @@
export const GLOBAL_RESOLE_CONTROLLER = [
{
label: "哔哩哔哩",
value: 1
},
{
label: "抖音",
value: 1
},
{
label: "TikTok",
value: 1
},
{
label: "YouTube",
value: 1
},
{
label: "Acfun",
value: 1
},
{
label: "小红书",
value: 1
},
{
label: "波点",
value: 1
},
{
label: "网易云音乐",
value: 1
},
{
label: "通用(包含快手等)",
value: 1
},
{
label: "Twitter",
value: 1
},
{
label: "米游社",
value: 1
},
{
label: "微博",
value: 1
},
{
label: "微视",
value: 1
},
{
label: "zuiyou",
value: 1
},
{
label: "AM+Spotify",
value: 1
},
{
label: "扣扣音乐",
value: 1
},
{
label: "汽水音乐",
value: 1
},
{
label: "小飞机",
value: 1
},
{
label: "贴吧",
value: 1
}
]

View File

@ -1,130 +0,0 @@
import Bili from "../components/contents/bili.jsx";
import Generic from "../components/contents/generic.jsx";
import Home from "../components/contents/home.jsx";
import Ncm from "../components/contents/ncm.jsx";
import Tiktok from "../components/contents/tiktok.jsx";
import Weekly from "../components/contents/weekly.jsx";
import Youtube from "../components/contents/youtube.jsx";
export const SIDEBAR_ITEMS = [
{
name: "总控制台",
icon: <svg t="1730947011086" className="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="8312" width="32" height="32">
<path
d="M806.31 1004.13H217.69c-109.1 0-197.83-88.74-197.83-197.81V217.69c0-109.07 88.74-197.82 197.83-197.82h588.62c109.1 0 197.83 88.75 197.83 197.82v588.64c0 109.07-88.73 197.8-197.83 197.8zM217.69 97.57c-66.25 0-120.13 53.88-120.13 120.11v588.64c0 66.22 53.88 120.1 120.13 120.1h588.62c66.25 0 120.13-53.88 120.13-120.1V217.69c0-66.24-53.88-120.11-120.13-120.11H217.69v-0.01z"
fill="#333333" p-id="8313"></path>
<path d="M494.93 300.08h77.71v420.77h-77.71z" fill="#333333" p-id="8314"></path>
<path d="M533.78 324.93m-81.76 0a81.76 81.76 0 1 0 163.52 0 81.76 81.76 0 1 0-163.52 0Z"
fill="#C6C6C6" p-id="8315"></path>
<path d="M730.78 300.08h77.71v420.77h-77.71z" fill="#333333" p-id="8316"></path>
<path d="M769.62 673.98m-81.76 0a81.76 81.76 0 1 0 163.52 0 81.76 81.76 0 1 0-163.52 0Z"
fill="#C6C6C6" p-id="8317"></path>
<path d="M270.58 300.08h77.71v420.77h-77.71z" fill="#333333" p-id="8318"></path>
<path d="M309.44 510.47m-81.76 0a81.76 81.76 0 1 0 163.52 0 81.76 81.76 0 1 0-163.52 0Z"
fill="#C6C6C6" p-id="8319"></path>
</svg>,
theme: "light",
component: <Home />
},
{
name: "通用及杂项",
icon: <svg t="1732247159359" className="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="7981" width="32" height="32">
<path
d="M512.1 896.4c-102.6 0-199-39.9-271.5-112.5-72.5-72.5-112.5-169-112.5-271.5 0-102.6 39.9-199 112.5-271.5 72.5-72.5 169-112.5 271.5-112.5 102.6 0 199 39.9 271.5 112.5 72.5 72.5 112.5 169 112.5 271.5 0 102.6-39.9 199-112.5 271.5-72.5 72.5-168.9 112.5-271.5 112.5z"
fill="#FFFFFF" p-id="7982"></path>
<path
d="M512.1 192.4c43.3 0 85.2 8.4 124.5 25.1 38.1 16.1 72.3 39.2 101.8 68.6 29.4 29.4 52.5 63.7 68.6 101.8 16.7 39.4 25.1 81.3 25.1 124.5s-8.4 85.2-25.1 124.5c-16.1 38.1-39.2 72.3-68.6 101.8s-63.7 52.5-101.8 68.6c-39.4 16.7-81.3 25.1-124.5 25.1-43.3 0-85.2-8.4-124.5-25.1-38.1-16.1-72.3-39.2-101.8-68.6-29.4-29.4-52.5-63.7-68.6-101.8-16.7-39.4-25.1-81.3-25.1-124.5s8.4-85.2 25.1-124.5c16.1-38.1 39.2-72.3 68.6-101.8 29.4-29.4 63.7-52.5 101.8-68.6 39.4-16.7 81.3-25.1 124.5-25.1m0-128c-247.4 0-448 200.6-448 448s200.6 448 448 448 448-200.6 448-448c0-247.5-200.6-448-448-448z"
fill="#333333" p-id="7983"></path>
</svg>,
theme: "cupcake",
component: <Generic />
},
{
name: "哔哩哔哩控制台",
icon: <svg t="1730946721024" className="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="4282" width="32" height="32">
<path
d="M729.32864 373.94944c-9.79456-5.94432-19.06176-6.784-19.14368-6.784l-1.06496-0.0512c-57.20064-3.8656-121.1648-5.83168-190.12608-5.83168l-13.98784 0.00512c-68.95616 0-132.92544 1.96096-190.12096 5.83168l-1.06496 0.0512c-0.08192 0-9.34912 0.83968-19.14368 6.784-15.04768 9.12896-24.27392 25.94816-27.4176 49.9712-10.07104 76.91264-4.38272 173.64992 0.18944 251.392 2.93888 49.96608 33.408 62.45888 85.04832 67.1488 10.78272 0.98816 69.08928 5.86752 159.50848 5.89312v-0.00512c90.4192-0.02048 148.72576-4.90496 159.5136-5.888 51.64032-4.68992 82.10944-17.18272 85.0432-67.1488 4.57728-77.74208 10.26048-174.47936 0.18944-251.392-3.1488-24.02816-12.37504-40.84736-27.42272-49.97632z m-390.9888 172.71808a23.64928 23.64928 0 0 1-31.68768-10.84416 23.68 23.68 0 0 1 10.84416-31.68768c2.03776-1.00352 50.69312-24.72448 110.5408-43.06432a23.68 23.68 0 1 1 13.88032 45.29152c-56.2944 17.24928-103.11168 40.07424-103.5776 40.30464z m268.89728 35.88608c-0.44032 2.23232-11.26912 54.64064-50.93888 54.64064-21.44256 0-36.10112-14.04928-44.98432-26.77248-8.69376 12.70784-22.80448 26.77248-42.65472 26.77248-35.5328 0-50.13504-48.26624-51.68128-53.77024a11.3664 11.3664 0 0 1 21.87776-6.1696c2.74944 9.6512 14.1312 37.20192 29.7984 37.20192 16.37376 0 28.89216-23.64416 31.98464-31.92832a11.37152 11.37152 0 0 1 10.6496-7.38816h0.06144c4.76672 0.03072 9.0112 3.02592 10.62912 7.50592 0.10752 0.28672 11.96544 31.81568 34.31424 31.81568 20.864 0 28.56448-35.95264 28.64128-36.32128a11.34592 11.34592 0 0 1 13.35808-8.93952 11.36128 11.36128 0 0 1 8.94464 13.35296z m110.11584-46.73536a23.68 23.68 0 0 1-31.68256 10.84416c-0.47104-0.2304-47.47264-23.1168-103.57248-40.30976a23.69024 23.69024 0 0 1-15.70816-29.58336 23.66976 23.66976 0 0 1 29.57824-15.70304c59.84768 18.33984 108.49792 42.0608 110.55104 43.06432a23.68 23.68 0 0 1 10.83392 31.68768z"
fill="#F16C8D" p-id="4283"></path>
<path
d="M849.92 51.2H174.08c-67.8656 0-122.88 55.0144-122.88 122.88v675.84c0 67.87072 55.0144 122.88 122.88 122.88h675.84c67.87072 0 122.88-55.00928 122.88-122.88V174.08c0-67.86048-55.00928-122.88-122.88-122.88z m-36.60288 627.45088c-2.62656 44.57984-21.82144 78.63296-55.51616 98.48832-25.68192 15.13472-54.17472 19.48672-81.13664 21.9392-32.45568 2.94912-92.71808 6.09792-164.66432 6.1184-71.94112-0.02048-132.20864-3.16416-164.66432-6.1184-26.96192-2.45248-55.45472-6.80448-81.13152-21.9392-33.69472-19.85536-52.8896-53.90336-55.51104-98.4832-4.70528-80.13312-10.5728-179.85536 0.19456-262.10816C221.5424 335.16544 280.99072 311.57248 311.5008 310.37952a2482.64192 2482.64192 0 0 1 81.42336-4.08576c-7.53664-8.53504-19.88096-23.3216-28.81536-38.11328-13.73696-22.73792 8.52992-41.68704 8.52992-41.68704s23.68-20.36736 44.52864 5.21216c15.69792 19.26656 38.37952 55.99744 48.61952 72.95488l53.20704-0.21504c13.2608 0 26.33216 0.07168 39.2192 0.21504 10.24-16.95744 32.9216-53.6832 48.61952-72.95488 20.84352-25.57952 44.52864-5.21216 44.52864-5.21216s22.26176 18.94912 8.5248 41.68704c-8.9344 14.79168-21.27872 29.57824-28.81536 38.11328 28.35968 0.97792 55.56224 2.33984 81.42336 4.08064 30.5152 1.19808 89.9584 24.79104 100.61312 106.17344 10.7776 82.24768 4.9152 181.96992 0.20992 262.10304z"
fill="#F16C8D" p-id="4284"></path>
</svg>,
theme: "valentine",
component: <Bili />
},
{
name: "抖音控制台",
icon: <svg t="1732246520808" className="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="6894" width="32" height="32">
<path
d="M208.323765 0h607.35247C922.262588 0 1008.941176 89.088 1008.941176 198.595765V822.814118c0 109.507765-86.678588 198.595765-193.264941 198.595764H208.323765C101.737412 1021.379765 15.058824 932.291765 15.058824 822.784V198.595765C15.058824 89.088 101.737412 0 208.323765 0z"
fill="#170B1A" p-id="6895"></path>
<path
d="M503.356235 309.458824c0.572235-63.427765 0-126.855529 0.572236-190.283295h128.150588c-0.572235 11.203765 1.114353 22.437647 2.770823 33.129412h-94.32847v515.312941a124.295529 124.295529 0 0 1-15.510588 62.85553c-16.655059 29.214118-47.736471 49.392941-81.016471 52.224a107.580235 107.580235 0 0 1-61.590588-12.950588A106.134588 106.134588 0 0 1 346.352941 737.249882c32.737882 18.522353 75.444706 16.835765 107.068235-3.915294 30.509176-19.094588 50.507294-55.024941 50.507295-92.069647-0.572235-110.592-0.572235-221.184-0.572236-331.776z m211.395765-36.472471c17.769412 11.203765 37.707294 20.178824 58.247529 24.696471 12.197647 2.800941 24.395294 3.915294 37.165177 3.915294v29.214117a182.302118 182.302118 0 0 1-95.412706-57.825882z"
fill="#25F4EE" p-id="6896"></path>
<path
d="M275.576471 427.459765a223.111529 223.111529 0 0 1 153.6-33.520941v31.232a267.956706 267.956706 0 0 0-42.255059 5.12 236.664471 236.664471 0 0 0-94.328471 43.730823c-30.177882 23.280941-53.217882 55.115294-69.12 90.322824a250.277647 250.277647 0 0 0-22.497882 107.911529c0 40.899765 10.962824 80.655059 29.605647 116.434824 8.794353 16.474353 18.672941 32.376471 31.834353 45.447529-26.895059-19.335529-49.392941-45.477647-65.837177-74.992941-22.497882-39.183059-33.430588-85.202824-32.37647-131.192471A256.210824 256.210824 0 0 1 198.776471 508.084706a236.212706 236.212706 0 0 1 76.8-80.624941z"
fill="#25F4EE" p-id="6897"></path>
<path
d="M540.491294 153.208471h94.780235c3.312941 18.582588 9.999059 36.050824 18.31153 52.946823 13.312 25.901176 32.135529 49.031529 56.530823 64.240941a13.071059 13.071059 0 0 1 3.915294 3.915294 181.428706 181.428706 0 0 0 95.894589 58.066824c0.542118 33.792 0 68.156235 0 101.978353a297.020235 297.020235 0 0 1-176.308706-56.922353c0 81.136941 0 162.273882 0.542117 243.380706 0 10.721882 0.572235 21.413647 0 32.677647a269.312 269.312 0 0 1-34.334117 112.700235 243.802353 243.802353 0 0 1-66.56 76.619294 211.516235 211.516235 0 0 1-121.404235 42.255059c-22.166588 0.572235-44.363294-0.572235-65.957648-5.632a235.459765 235.459765 0 0 1-84.841411-37.737412l-1.656471-1.716706c-12.769882-12.950588-23.311059-28.732235-32.165647-45.056-18.853647-34.936471-29.936941-74.932706-29.936941-115.501176a246.061176 246.061176 0 0 1 22.738823-107.038118c16.052706-34.936471 39.905882-66.499765 69.872942-89.6a241.242353 241.242353 0 0 1 95.322353-43.369411c13.854118-2.831059 28.310588-4.517647 42.706823-5.059765 0.542118 12.950588 0 25.901176 0.542118 38.309647v65.897412c-16.082824-5.632-33.822118-5.632-50.447059-1.686589a124.084706 124.084706 0 0 0-54.332235 27.045648c-9.426824 8.432941-17.769412 18.582588-23.280942 29.876705-9.999059 19.154824-13.312 41.682824-11.083294 63.096471 2.198588 20.841412 11.083294 41.110588 24.395294 56.922353 8.854588 11.233882 20.48 19.696941 32.13553 27.587765 9.426824 13.522824 21.624471 24.786824 36.050823 32.677647a111.796706 111.796706 0 0 0 61.530353 12.950588c33.28-2.258824 64.301176-23.100235 80.956236-52.404706 10.541176-19.154824 16.082824-41.110588 15.510588-63.096471 1.114353-173.537882 0.572235-345.931294 0.572235-518.324705z"
fill="#FFFFFF" p-id="6898"></path>
<path
d="M650.119529 136.192c10.992941 0.542118 21.985882 0 33.490824 0a189.138824 189.138824 0 0 0 32.948706 106.616471c2.740706 3.975529 5.481412 7.348706 8.252235 10.752-24.154353-15.239529-43.369412-38.369882-56.018823-64.331295a216.335059 216.335059 0 0 1-18.672942-53.037176z m172.965647 179.440941c12.047059 2.800941 24.154353 3.915294 36.773648 3.915294v131.493647c-62.584471 0.602353-125.168941-20.871529-176.248471-58.669176v260.698353a233.833412 233.833412 0 0 1-5.481412 58.669176c-12.047059 58.699294-46.110118 111.736471-93.334588 146.160941a225.520941 225.520941 0 0 1-83.425882 38.369883 225.942588 225.942588 0 0 1-109.808942-1.686588A230.309647 230.309647 0 0 1 280.094118 825.735529a222.780235 222.780235 0 0 0 84.028235 37.797647c21.383529 5.089882 43.369412 6.204235 65.295059 5.662118a207.932235 207.932235 0 0 0 120.259764-42.345412c26.895059-20.299294 48.850824-46.832941 65.867295-76.739764a271.962353 271.962353 0 0 0 34.032941-112.850824c0.542118-10.721882 0.542118-21.443765 0-32.737882-0.542118-81.227294-0.542118-162.484706-0.542118-243.742118a291.900235 291.900235 0 0 0 174.592 56.982588c-0.542118-33.852235 0-68.276706-0.542118-102.128941z"
fill="#FE2C55" p-id="6899"></path>
<path
d="M440.380235 425.562353c12.649412 0 25.840941 0.602353 38.490353 2.258823v134.866824a106.706824 106.706824 0 0 0-58.006588-2.258824 110.983529 110.983529 0 0 0-79.299765 69.421177c-12.649412 33.852235-7.469176 73.366588 14.366118 102.128941a120.229647 120.229647 0 0 1-33.310118-27.648 105.502118 105.502118 0 0 1-25.298823-56.982588c-2.288941-21.443765 1.174588-44.032 11.504941-63.216941 5.722353-11.264 14.366118-21.443765 24.124235-29.906824 16.082824-13.552941 36.201412-21.985882 56.32-27.075765 17.227294-3.945412 35.599059-3.945412 52.254118 1.686589v-66.017883c-1.144471-11.294118-0.572235-24.274824-1.144471-37.255529z"
fill="#FE2C55" p-id="6900"></path>
</svg>,
theme: "dark",
component: <Tiktok />
},
{
name: "油管控制台",
icon: <svg t="1732245926879" className="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="4280" width="32" height="32">
<path
d="M985.742 749.896c0 130.902-106.178 237.004-237.17 237.004H274.298c-130.954 0-237.092-106.102-237.092-237.004V276.018c0-130.902 106.138-236.966 237.092-236.966h474.274c130.992 0 237.17 106.064 237.17 236.966v473.878z"
fill="#FF312E" p-id="4281"></path>
<path
d="M801.952 389.486s-5.792-42.618-23.544-61.324c-22.54-24.568-47.82-24.76-59.358-26.148-83.02-6.248-207.416-6.248-207.416-6.248h-0.27s-124.47 0-207.422 6.248c-11.58 1.388-36.822 1.582-59.4 26.148-17.752 18.706-23.544 61.324-23.544 61.324s-5.944 49.984-5.944 99.97v46.812c0 50.06 5.944 100.044 5.944 100.044s5.792 42.542 23.544 61.286c22.578 24.608 52.22 23.796 65.382 26.382 47.434 4.744 201.556 6.208 201.556 6.208s124.55-0.23 207.57-6.44c11.538-1.428 36.818-1.542 59.358-26.15 17.754-18.744 23.544-61.286 23.544-61.286s5.944-49.984 5.944-100.044v-46.812c-0.002-49.986-5.944-99.97-5.944-99.97zM450.304 593.118l-0.04-173.546 160.172 87.088-160.132 86.458z"
fill="#FFFFFF" p-id="4282"></path>
<path
d="M511.632 295.766s124.396 0 207.416 6.248c11.538 1.388 36.818 1.582 59.358 26.148 17.754 18.706 23.544 61.324 23.544 61.324s5.944 49.984 5.944 99.97v46.812c0 50.06-5.944 100.044-5.944 100.044s-5.792 42.542-23.544 61.286c-22.54 24.608-47.82 24.722-59.358 26.15-83.02 6.208-207.57 6.44-207.57 6.44s-154.124-1.462-201.556-6.208c-13.162-2.586-42.804-1.776-65.382-26.382-17.752-18.744-23.544-61.286-23.544-61.286s-5.944-49.984-5.944-100.044v-46.812c0-49.986 5.944-99.97 5.944-99.97s5.792-42.618 23.544-61.324c22.578-24.568 47.82-24.76 59.4-26.148 82.952-6.248 207.422-6.248 207.422-6.248h0.27m-61.328 297.352l160.132-86.458-160.172-87.088 0.04 173.546m61.328-317.106h-0.27c-1.252 0-126.17 0.072-208.906 6.304-0.29 0.022-0.58 0.05-0.868 0.084-0.964 0.116-2.038 0.22-3.204 0.336-13.884 1.376-42.758 4.244-68.292 31.954-21.424 22.712-27.992 67.136-28.67 72.136l-0.042 0.328c-0.248 2.088-6.082 51.728-6.082 102.302v46.812c0 50.648 5.834 100.29 6.082 102.376l0.042 0.332c0.68 4.992 7.248 49.342 28.662 72.084 23.586 25.626 53.29 29.344 69.256 31.342 2.666 0.334 5.184 0.65 6.774 0.96 0.61 0.122 1.224 0.21 1.842 0.274 47.772 4.778 197.006 6.244 203.334 6.304h0.224c1.252-0.002 126.226-0.302 209.006-6.494 0.318-0.024 0.636-0.054 0.954-0.096 1-0.124 2.118-0.232 3.336-0.356 13.858-1.396 42.668-4.298 68.058-31.942 21.41-22.746 27.976-67.088 28.656-72.078 0.014-0.11 0.03-0.22 0.042-0.332 0.248-2.088 6.082-51.728 6.082-102.376v-46.812c0-50.574-5.834-100.214-6.082-102.302a9.188 9.188 0 0 0-0.042-0.328c-0.68-5-7.246-49.42-28.67-72.134-25.49-27.704-54.36-30.574-68.244-31.954-1.168-0.116-2.24-0.222-3.204-0.336a18.54 18.54 0 0 0-0.876-0.086c-82.802-6.23-207.646-6.302-208.898-6.302z m-41.582 283.994l-0.024-107.206 98.944 53.798-98.92 53.408z"
fill="#801917" p-id="4283"></path>
<path
d="M728.078 956.122H294.83c-124.896 0-226.506-101.636-226.506-226.566V296.358c0-124.908 101.61-226.526 226.506-226.526h433.248c124.918 0 226.544 101.62 226.544 226.526v433.198c0.002 124.928-101.626 226.566-226.544 226.566zM294.83 89.636c-113.976 0-206.702 92.734-206.702 206.72v433.198c0 114.008 92.726 206.762 206.702 206.762h433.248c113.996 0 206.74-92.754 206.74-206.762V296.358c0-113.986-92.744-206.72-206.74-206.72H294.83z"
fill="#801917" p-id="4284"></path>
</svg>,
theme: "dracula",
component: <Youtube />
},
{
name: "网易云控制台",
icon: <svg t="1732246497646" className="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="5759" width="32" height="32">
<path
d="M0 0m184.32 0l655.36 0q184.32 0 184.32 184.32l0 655.36q0 184.32-184.32 184.32l-655.36 0q-184.32 0-184.32-184.32l0-655.36q0-184.32 184.32-184.32Z"
fill="#EA3E3C" p-id="5760"></path>
<path
d="M527.616 849.43872a373.6064 373.6064 0 0 1-162.54976-39.00416c-112.36352-55.16288-180.00896-176.29184-172.55424-308.67456 7.41376-130.34496 85.10464-237.4656 202.752-279.552a35.85024 35.85024 0 0 1 24.15616 67.51232c-107.66336 38.49216-150.81472 136.86784-155.29984 216.13568-5.86752 103.51616 46.08 197.79584 132.34176 240.13824 124.69248 60.30336 216.91392 22.35392 260.82304-5.64224 59.8016-38.16448 97.86368-100.01408 96.95232-157.55264-1.024-63.72352-24.064-120.99584-63.27296-157.14304a145.408 145.408 0 0 0-65.5872-35.28704q2.82624 9.76896 5.64224 19.32288c13.38368 45.63968 24.94464 85.05344 25.6 114.40128a134.26688 134.26688 0 0 1-37.69344 97.76128 139.1104 139.1104 0 0 1-100.6592 40.45824 140.10368 140.10368 0 0 1-100.47488-42.24 169.12384 169.12384 0 0 1-46.2848-122.76736c1.19808-85.12512 80.11776-153.28256 162.816-175.104a324.80256 324.80256 0 0 1-6.71744-67.05152 92.0576 92.0576 0 0 1 69.18144-91.81184c46.21312-12.53376 104.448 5.19168 124.66176 37.888a35.84 35.84 0 0 1-11.70432 49.31584 35.84 35.84 0 0 1-49.26464-11.65312 62.34112 62.34112 0 0 0-48.45568-5.21216c-4.32128 1.71008-12.35968 4.90496-12.76928 23.10144a270.87872 270.87872 0 0 0 6.73792 58.51136 217.4976 217.4976 0 0 1 133.56032 57.6512c53.57568 49.38752 85.0432 125.46048 86.35392 208.71168 1.29024 81.85856-49.7664 167.86432-130.048 219.136a310.14912 310.14912 0 0 1-168.2432 48.65024z m23.6544-457.55392c-56.77056 15.6672-107.4688 63.03744-108.07296 106.42432a98.304 98.304 0 0 0 25.6512 71.43424 68.0448 68.0448 0 0 0 49.36704 20.87936 67.24608 67.24608 0 0 0 49.44896-18.944 63.19104 63.19104 0 0 0 17.23392-46.08c-0.4096-19.79392-11.7248-58.368-22.67136-95.6928-3.61472-12.42112-7.35232-25.14944-10.9568-38.02112z"
fill="#FFFFFF" p-id="5761"></path>
</svg>,
theme: "lofi",
component: <Ncm />
},
{
name: "周刊预览",
icon: <svg t="1730970789425" className="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="4284" width="32" height="32">
<path
d="M1024 157.866667v712.533333a80.64 80.64 0 0 0-7.68 14.08c-21.333333 68.693333-66.986667 113.066667-135.68 133.12a116.48 116.48 0 0 0-14.506667 6.4H153.6a58.88 58.88 0 0 0-11.946667-6.826667q-103.68-31.146667-134.4-134.4a58.88 58.88 0 0 0-6.826666-11.946666v-716.8a58.88 58.88 0 0 0 6.826666-11.946667Q38.4 38.4 141.653333 7.68A58.88 58.88 0 0 0 153.6 0.853333h712.533333a62.293333 62.293333 0 0 0 10.24 5.12q111.36 31.146667 142.506667 142.506667A62.293333 62.293333 0 0 0 1024 157.866667zM573.013333 720.213333c13.653333 1.706667 26.026667 4.266667 38.826667 5.12a320 320 0 0 0 44.8 0.426667c32-2.56 47.786667-19.2 47.786667-51.2 0.426667-113.92 0-227.413333 0.426666-341.333333 0-16.64-5.546667-22.613333-22.613333-22.186667q-165.12 0.853333-330.666667 0c-17.92 0-23.466667 6.826667-23.466666 23.893333 0.426667 66.986667 2.133333 133.546667-0.426667 200.533334-1.28 40.96-8.533333 81.493333-14.933333 122.453333-2.986667 18.773333-9.813333 36.693333-15.36 57.173333l40.106666 11.093334c19.2-51.2 30.293333-101.973333 29.866667-155.306667q-0.426667-100.266667 0-200.533333c0-8.533333-2.986667-20.053333 11.946667-20.053334h111.786666v33.28h-102.4v36.266667H490.666667v37.546667H389.12v34.133333h256V456.533333h-112.213333v-37.12h112.213333v-36.693333H533.333333v-32.426667c38.4 0 75.52 0.853333 112.213334-0.426666 16.213333-0.426667 20.48 4.693333 20.48 20.48-0.853333 95.146667-0.426667 190.72-0.426667 285.866666 0 25.173333-6.4 32-32 29.866667s-40.106667-6.4-60.16-9.813333z m-175.36-65.706666h237.653334v-133.12H397.226667z"
fill="#19B883" p-id="4285"></path>
<path d="M598.186667 557.653333v60.16h-162.56v-60.16z" fill="#22BB88" p-id="4286"></path>
</svg>,
theme: "retro",
component: <Weekly />
}
];

View File

@ -1,22 +0,0 @@
"use client"
import { createContext, useContext, useState } from 'react';
const DrawerContext = createContext();
export const DrawerProvider = ({ children }) => {
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const toggleDrawer = () => {
setIsDrawerOpen(prev => !prev);
};
return (
<DrawerContext.Provider value={{ isDrawerOpen, toggleDrawer }}>
{children}
</DrawerContext.Provider>
);
};
export const useDrawer = () => useContext(DrawerContext);

View File

@ -1,5 +0,0 @@
export default {
eslint: {
ignoreDuringBuilds: true, // 构建时忽略 ESLint 错误
},
};

View File

@ -1,6 +0,0 @@
// postcss.config.js
export default {
plugins: {
tailwindcss: {},
},
};

View File

@ -1,41 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/** 代码字体 */
@font-face {
font-family: "FZB";
src: url("../../resources/font/FZB.ttf");
}
* {
margin: 0;
font-family: "FZB", serif;
box-sizing: border-box;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 8px;
/* 滚动条宽度 */
}
::-webkit-scrollbar-track {
background: #f1f1f1;
/* 滚动条轨道背景色 */
}
::-webkit-scrollbar-thumb {
background: #888;
/* 滚动条滑块颜色 */
border-radius: 4px;
/* 滑块圆角 */
}
::-webkit-scrollbar-thumb:hover {
background: #555;
/* 滑块悬停时的颜色 */
}

View File

@ -1,23 +0,0 @@
import daisyui from "daisyui"
/** @type {import('tailwindcss').Config} */
const config = {
content: [
"./app/**/*.{html,js,jsx}",
"./components/**/*.{html,js,jsx}",
"./pages/**/*.{html,js,jsx}",
"./styles/**/*.{html,js,jsx}"
],
theme: {
extend: {},
},
darkMode: "class",
plugins: [
daisyui,
],
daisyui: {
themes: ["light", "dark", "valentine", "retro", "lofi", "dracula", "aqua", "cupcake"],
},
};
export default config;

View File

@ -1,17 +0,0 @@
import fs from "fs";
import Redis from "ioredis";
import yaml from "js-yaml";
import path from "path";
const configPath = path.join(process.cwd(), "../../../", "config", 'config', 'redis.yaml');
const yamlContent = await fs.promises.readFile(configPath, 'utf8');
const config = yaml.load(yamlContent);
export const redis = new Redis({
port: config.port,
host: config.host,
username: config.username,
password: config.password,
db: config.db,
})

View File

@ -1,29 +0,0 @@
export const readYamlConfig = async () => {
try {
const response = await fetch('/r/api/config');
if (!response.ok) throw new Error('获取配置失败');
return await response.json();
} catch (error) {
console.error('读取配置文件失败:', error);
return null;
}
};
export const updateYamlConfig = async (updates) => {
try {
const response = await fetch('/r/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updates)
});
if (!response.ok) throw new Error('更新配置失败');
const result = await response.json();
return result.success;
} catch (error) {
console.error('更新配置文件失败:', error);
return false;
}
};

View File

@ -1,109 +0,0 @@
import os from 'os';
/**
* 判断是否是公网地址
* @param ip
* @returns {boolean}
*/
function isPublicIP(ip) {
if (ip.includes(':')) {
// IPv6 检测
if (ip.startsWith('fe80') || ip.startsWith('fc00')) {
return false; // 本地链路或私有 IPv6
}
return true; // 其他 IPv6 认为是公网
} else {
// IPv4 检测
const parts = ip.split('.').map(Number);
if (
(parts[0] === 10) || // 10.0.0.0/8
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) || // 172.16.0.0/12
(parts[0] === 192 && parts[1] === 168) // 192.168.0.0/16
) {
return false; // 私有 IPv4
}
return true; // 其他 IPv4 认为是公网
}
}
/**
* 判断是否有公网 IPv6
* @returns {boolean}
*/
export function hasIPv6Only() {
const interfaces = os.networkInterfaces();
let hasPublicIPv4 = false;
let hasPublicIPv6 = false;
for (const iface of Object.values(interfaces)) {
for (const config of iface) {
if (!config.internal && isPublicIP(config.address)) {
if (config.family === 'IPv4') {
hasPublicIPv4 = true;
}
if (config.family === 'IPv6') {
hasPublicIPv6 = true;
}
}
}
}
if (hasPublicIPv6 && !hasPublicIPv4) {
logger.info('[R插件][公网检测]服务器仅拥有一个公网IPv6地址');
return true;
} else if (hasPublicIPv4 && hasPublicIPv6) {
logger.info('[R插件][公网检测]服务器同时拥有公共IPv4和IPv6地址');
return false;
} else if (hasPublicIPv4) {
logger.info('[R插件][公网检测]服务器仅拥有一个公网IPv4地址');
return false;
} else {
logger.info('[R插件][公网检测]服务器未配置公网IP地址');
return false;
}
}
/**
* 获取所有公网IP地址
* @returns {*[]}
*/
export function getPublicIPs() {
const interfaces = os.networkInterfaces();
const publicIPs = [];
for (const [name, iface] of Object.entries(interfaces)) {
for (const config of iface) {
if (!config.internal && isPublicIP(config.address)) {
publicIPs.push({
interface: name,
address: config.address,
family: config.family,
});
}
}
}
return publicIPs;
}
/**
* 构造内网公网消息
* @returns {`R插件可视化面板内网地址${string}:4016`}
*/
export function constructPublicIPsMsg() {
const networkInterfaces = os.networkInterfaces();
const ipAddress = Object.values(networkInterfaces)
.flat()
.filter(detail => detail.family === 'IPv4' && !detail.internal)[0].address;
const publicIPs = getPublicIPs();
let publicIPsStr = '';
// 如果有公网地址
if (publicIPs.length > 0) {
publicIPsStr = `\n公网地址:${ getPublicIPs().map(item => {
logger.info('[R插件][公网检测]公网IP地址', item.address);
return `${ item.address }:4016\n`;
}) }`;
}
publicIPsStr = `R插件可视化面板内网地址${ ipAddress }:4016${ publicIPsStr }`;
return publicIPsStr;
}

View File

@ -1,72 +0,0 @@
import { spawn } from 'child_process';
import { hasIPv6Only } from "./network.js";
logger.mark(`[R插件][WebUI], 父进程 PID: ${process.pid}`);
let nextjsProcess = null;
// 构建应用程序
export const buildNextJs = () => {
logger.info(logger.yellow('[R插件][WebUI],正在构建 Next.js 应用...'));
return new Promise((resolve, reject) => {
const buildProcess = spawn('pnpm', ['run', 'build'], {
cwd: './plugins/rconsole-plugin/server',
stdio: 'ignore',
shell: true,
});
buildProcess.on('close', (code) => {
if (code === 0) {
logger.info(logger.yellow('[R插件][Next.js监测],构建完成。'));
resolve();
} else {
logger.error(`[R插件][WebUI监测],构建失败,退出码:${code}`);
reject(new Error('Build failed'));
}
});
});
};
// 启动子进程运行 Next.js
export const startNextJs = (mode = 'start') => {
let script = mode === 'start' ? 'start' : 'dev';
logger.info(logger.yellow(`[R插件][WebUI监测],启动 WebUI ${mode} 进程...`));
// 判断是不是只有ipv6地址
if (hasIPv6Only()) {
script = 'start6';
}
nextjsProcess = spawn('pnpm', ['run', script], {
cwd: './plugins/rconsole-plugin', // 指定工作目录
stdio: 'ignore',
shell: true,
});
// 子进程异常退出时捕获信号
nextjsProcess.on('close', (code) => {
logger.error(`[R插件][WebUI监测]WebUI 进程发生异常 ${code}`);
nextjsProcess = null;
});
nextjsProcess.on('error', (err) => {
logger.error(`[R插件][WebUI监测] 子进程错误: ${err.message}`);
});
};
// 捕获父进程退出信号
export const cleanup = () => {
logger.info(logger.yellow('[R插件][WebUI监测] 父进程退出,终止子进程...'));
if (nextjsProcess) {
// 终止子进程
nextjsProcess.kill();
nextjsProcess = null;
}
process.exit();
};
// 绑定父进程的退出信号
process.on('SIGINT', cleanup); // Ctrl+C 信号
process.on('SIGTERM', cleanup); // kill 命令信号
process.on('exit', cleanup); // 正常退出