Merge branch 'refs/heads/web-preview' into dev

This commit is contained in:
zhiyu1998 2024-11-22 22:15:39 +08:00
commit 3a2bca2f70
43 changed files with 2614 additions and 52 deletions

56
apps/webUI.js Normal file
View File

@ -0,0 +1,56 @@
import { REDIS_YUNZAI_WEBUI } from "../constants/constant.js";
import config from "../model/config.js";
import { redisSetKey } from "../utils/redis-util.js";
import { getBotLoginInfo, getBotStatus, getBotVersionInfo } 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 rWebSwitch(e) {
config.updateField("tools", "isOpenWebUI", !this.isOpenWebUI);
const realIsOpenWebUI = config.getConfig("tools").isOpenWebUI;
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
})
})
}
// 这里有点延迟,需要写反
e.reply(`R插件 WebUI${ realIsOpenWebUI ? "开启\n🚀 请重启以启动 WebUI" : "关闭" }`);
return true;
}
async rWebStatus(e) {
e.reply(`R插件 WebUI${ this.toolsConfig.isOpenWebUI ? "开启" : "关闭" }`);
return true;
}
}

View File

@ -1,3 +1,4 @@
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

@ -93,10 +93,10 @@ 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} * @type {string}
*/ */
export const REDIS_YUNZAI_ANIMELIST = "Yz:rconsole:tools:anime"; export const REDIS_YUNZAI_WEBUI = "Yz:rconsole:tools:webui";
export const TWITTER_BEARER_TOKEN = ""; export const TWITTER_BEARER_TOKEN = "";

View File

@ -1,12 +1,15 @@
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 { buildNextJs, startNextJs } from "./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'));
@ -35,4 +38,11 @@ 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) {
buildNextJs()
.then(() => startNextJs('start'))
}
export { apps }; export { apps };

View File

@ -2,11 +2,31 @@
"name": "rconsole-plugin", "name": "rconsole-plugin",
"description": "R-Plugin", "description": "R-Plugin",
"type": "module", "type": "module",
"scripts": {
"dev": "cd server && next dev -p 4016",
"start": "cd server && 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",
"p-queue": "^8.0.1" "react": "^18.3.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"
} }
} }

9
server/app/layout.jsx Normal file
View File

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

12
server/app/page.jsx Normal file
View File

@ -0,0 +1,12 @@
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

@ -0,0 +1,12 @@
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

@ -0,0 +1,30 @@
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

@ -0,0 +1,52 @@
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

@ -0,0 +1,25 @@
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

@ -0,0 +1,116 @@
import { promises as fs } from 'fs';
import os from 'os';
import si from 'systeminformation';
let lastBytesReceived = 0;
let lastBytesSent = 0;
let lastTimestamp = Date.now();
let isFirstRun = true;
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;
let downloadSpeed = 0;
let uploadSpeed = 0;
if (!isFirstRun) {
// 检查是否发生了计数器重置或异常值
if (bytesReceived >= lastBytesReceived && bytesSent >= lastBytesSent) {
downloadSpeed = (bytesReceived - lastBytesReceived) / timeDiff;
uploadSpeed = (bytesSent - lastBytesSent) / timeDiff;
// 设置合理的上限值(比如 1GB/s
const MAX_SPEED = 1024 * 1024 * 1024; // 1 GB/s
downloadSpeed = Math.min(downloadSpeed, MAX_SPEED);
uploadSpeed = Math.min(uploadSpeed, MAX_SPEED);
}
}
// 更新状态
lastBytesReceived = bytesReceived;
lastBytesSent = bytesSent;
lastTimestamp = now;
isFirstRun = false;
return {
downloadSpeed: (downloadSpeed / 1024).toFixed(2), // KB/s
uploadSpeed: (uploadSpeed / 1024).toFixed(2), // KB/s
totalReceived: (bytesReceived / (1024 * 1024 * 1024)).toFixed(2), // GB
totalSent: (bytesSent / (1024 * 1024 * 1024)).toFixed(2), // GB
timestamp: now
};
} catch (error) {
console.error('获取网络统计信息失败:', error);
return {
downloadSpeed: "0",
uploadSpeed: "0",
totalReceived: "0",
totalSent: "0",
timestamp: Date.now()
};
}
}
export async function GET() {
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

@ -0,0 +1,60 @@
import si from 'systeminformation';
import os from 'os';
export async function GET(request, { params }) {
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

@ -0,0 +1,43 @@
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

@ -0,0 +1,23 @@
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

@ -0,0 +1,13 @@
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

@ -0,0 +1,306 @@
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';
export default function Bili() {
const [config, setConfig] = useState({
biliSessData: '',
biliDuration: 480,
biliIntroLenLimit: 50,
biliDisplayCover: true,
biliDisplayInfo: true,
biliDisplayIntro: true,
biliDisplayOnline: true,
biliDisplaySummary: false,
biliUseBBDown: false,
biliCDN: 0,
biliDownloadMethod: 0,
biliResolution: 5
});
const [loading, setLoading] = useState(false);
//
useEffect(() => {
const loadConfig = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
biliSessData: yamlConfig.biliSessData || '',
biliDuration: yamlConfig.biliDuration || 480,
biliIntroLenLimit: yamlConfig.biliIntroLenLimit || 50,
biliDisplayCover: yamlConfig.biliDisplayCover ?? true,
biliDisplayInfo: yamlConfig.biliDisplayInfo ?? true,
biliDisplayIntro: yamlConfig.biliDisplayIntro ?? true,
biliDisplayOnline: yamlConfig.biliDisplayOnline ?? true,
biliDisplaySummary: yamlConfig.biliDisplaySummary ?? false,
biliUseBBDown: yamlConfig.biliUseBBDown ?? false,
biliCDN: yamlConfig.biliCDN || 0,
biliDownloadMethod: yamlConfig.biliDownloadMethod || 0,
biliResolution: yamlConfig.biliResolution || 5
});
}
};
loadConfig();
}, []);
//
const handleSave = async () => {
setLoading(true);
try {
const success = await updateYamlConfig({
biliSessData: config.biliSessData,
biliDuration: config.biliDuration,
biliIntroLenLimit: config.biliIntroLenLimit,
biliDisplayCover: config.biliDisplayCover,
biliDisplayInfo: config.biliDisplayInfo,
biliDisplayIntro: config.biliDisplayIntro,
biliDisplayOnline: config.biliDisplayOnline,
biliDisplaySummary: config.biliDisplaySummary,
biliUseBBDown: config.biliUseBBDown,
biliCDN: config.biliCDN,
biliDownloadMethod: config.biliDownloadMethod,
biliResolution: config.biliResolution
});
if (success) {
// 使 daisyUI toast
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 handleReset = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
biliSessData: yamlConfig.biliSessData || '',
biliDuration: yamlConfig.biliDuration || 480,
biliIntroLenLimit: yamlConfig.biliIntroLenLimit || 50,
biliDisplayCover: yamlConfig.biliDisplayCover ?? true,
biliDisplayInfo: yamlConfig.biliDisplayInfo ?? true,
biliDisplayIntro: yamlConfig.biliDisplayIntro ?? true,
biliDisplayOnline: yamlConfig.biliDisplayOnline ?? true,
biliDisplaySummary: yamlConfig.biliDisplaySummary ?? false,
biliUseBBDown: yamlConfig.biliUseBBDown ?? false,
biliCDN: yamlConfig.biliCDN || 0,
biliDownloadMethod: yamlConfig.biliDownloadMethod || 0,
biliResolution: yamlConfig.biliResolution || 5
});
}
};
return (
<div className="p-6 mx-auto container">
{/* 成功提示 */}
<div id="toast-success" className="toast toast-top toast-end hidden">
<div className="alert alert-success">
<span>配置保存成功</span>
</div>
</div>
<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>
{/* SESSDATA配置 */}
<div className="form-control w-full mb-4">
<label className="label">
<span className="label-text">SESSDATA</span>
</label>
<input
type="text"
value={config.biliSessData}
onChange={(e) => setConfig({ ...config, biliSessData: e.target.value })}
placeholder="请输入Bilibili SESSDATA"
className="input input-bordered w-full"
/>
</div>
{/* 数值配置部分 */}
<div className="grid md:grid-cols-2 gap-4 mb-4">
<div className="form-control">
<label className="label">
<span className="label-text">视频时长限制</span>
</label>
<input
type="number"
value={config.biliDuration}
onChange={(e) => setConfig({ ...config, biliDuration: parseInt(e.target.value) })}
className="input input-bordered"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">简介长度限制</span>
</label>
<input
type="number"
value={config.biliIntroLenLimit}
onChange={(e) => setConfig({ ...config, biliIntroLenLimit: parseInt(e.target.value) })}
className="input input-bordered"
/>
</div>
</div>
{/* 开关配置部分 */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">显示封面</span>
<input
type="checkbox"
checked={config.biliDisplayCover}
onChange={(e) => setConfig({ ...config, biliDisplayCover: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">显示视频信息</span>
<input
type="checkbox"
checked={config.biliDisplayInfo}
onChange={(e) => setConfig({ ...config, biliDisplayInfo: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">显示简介</span>
<input
type="checkbox"
checked={config.biliDisplayIntro}
onChange={(e) => setConfig({ ...config, biliDisplayIntro: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">显示在线人数</span>
<input
type="checkbox"
checked={config.biliDisplayOnline}
onChange={(e) => setConfig({ ...config, biliDisplayOnline: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">显示总结</span>
<input
type="checkbox"
checked={config.biliDisplaySummary}
onChange={(e) => setConfig({ ...config, biliDisplaySummary: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">使用BBDown</span>
<input
type="checkbox"
checked={config.biliUseBBDown}
onChange={(e) => setConfig({ ...config, biliUseBBDown: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
</div>
{/* 下拉选择配置部分 */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="form-control">
<label className="label">
<span className="label-text">CDN选择</span>
</label>
<select
className="select select-bordered"
value={config.biliCDN}
onChange={(e) => setConfig({ ...config, biliCDN: parseInt(e.target.value) })}>
{
BILI_CDN_SELECT_LIST.map(item => {
return (
<option value={ item.value }>{ item.label }</option>
)
})
}
</select>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">下载方式</span>
</label>
<select
className="select select-bordered"
value={config.biliDownloadMethod}
onChange={(e) => setConfig({ ...config, biliDownloadMethod: parseInt(e.target.value) })}>
{
BILI_DOWNLOAD_METHOD.map(item => {
return (
<option value={ item.value }>{ item.label }</option>
)
})
}
</select>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">视频画质</span>
</label>
<select
className="select select-bordered"
value={config.biliResolution}
onChange={(e) => setConfig({ ...config, biliResolution: parseInt(e.target.value) })}>
{
BILI_RESOLUTION_LIST.map(item => {
return (
<option value={ item.value }>{ item.label }</option>
)
})
}
</select>
</div>
</div>
</div>
</div>
{/* 保存按钮 */}
<div className="flex justify-end gap-4">
<button
className="btn btn-ghost"
onClick={handleReset}
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

@ -0,0 +1,289 @@
import { useState, useEffect } from 'react';
import { readYamlConfig, updateYamlConfig } from '../../utils/yamlHelper';
export default function Generic() {
const [config, setConfig] = useState({
defaultPath: './data/rcmp4/',
videoSizeLimit: 70,
proxyAddr: '127.0.0.1',
proxyPort: '7890',
identifyPrefix: '',
streamDuration: 10,
streamCompatibility: false,
queueConcurrency: 1,
videoDownloadConcurrency: 1,
autoclearTrashtime: '0 0 8 * * ?',
deeplApiUrls: ''
});
const [loading, setLoading] = useState(false);
//
useEffect(() => {
const loadConfig = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
defaultPath: yamlConfig.defaultPath || './data/rcmp4/',
videoSizeLimit: yamlConfig.videoSizeLimit || 70,
proxyAddr: yamlConfig.proxyAddr || '127.0.0.1',
proxyPort: yamlConfig.proxyPort || '7890',
identifyPrefix: yamlConfig.identifyPrefix || '',
streamDuration: yamlConfig.streamDuration || 10,
streamCompatibility: yamlConfig.streamCompatibility ?? false,
queueConcurrency: yamlConfig.queueConcurrency || 1,
videoDownloadConcurrency: yamlConfig.videoDownloadConcurrency || 1,
autoclearTrashtime: yamlConfig.autoclearTrashtime || '0 0 8 * * ?',
deeplApiUrls: yamlConfig.deeplApiUrls || ''
});
}
};
loadConfig();
}, []);
//
const handleSave = async () => {
setLoading(true);
try {
const success = await updateYamlConfig({
defaultPath: config.defaultPath,
videoSizeLimit: config.videoSizeLimit,
proxyAddr: config.proxyAddr,
proxyPort: config.proxyPort,
identifyPrefix: config.identifyPrefix,
streamDuration: config.streamDuration,
streamCompatibility: config.streamCompatibility,
queueConcurrency: config.queueConcurrency,
videoDownloadConcurrency: config.videoDownloadConcurrency,
autoclearTrashtime: config.autoclearTrashtime,
deeplApiUrls: config.deeplApiUrls
});
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 handleReset = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
defaultPath: yamlConfig.defaultPath || './data/rcmp4/',
videoSizeLimit: yamlConfig.videoSizeLimit || 70,
proxyAddr: yamlConfig.proxyAddr || '127.0.0.1',
proxyPort: yamlConfig.proxyPort || '7890',
identifyPrefix: yamlConfig.identifyPrefix || '',
streamDuration: yamlConfig.streamDuration || 10,
streamCompatibility: yamlConfig.streamCompatibility ?? false,
queueConcurrency: yamlConfig.queueConcurrency || 1,
videoDownloadConcurrency: yamlConfig.videoDownloadConcurrency || 1,
autoclearTrashtime: yamlConfig.autoclearTrashtime || '0 0 8 * * ?',
deeplApiUrls: yamlConfig.deeplApiUrls || ''
});
}
};
return (
<div className="p-6 mx-auto container">
{/* 成功提示 */}
<div id="generic-toast-success" className="toast toast-top toast-end hidden">
<div className="alert alert-success">
<span>配置保存成功</span>
</div>
</div>
<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>
{/* 路径和大小限制配置 */}
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="form-control">
<label className="label">
<span className="label-text">视频保存路径</span>
</label>
<input
type="text"
value={config.defaultPath}
onChange={(e) => setConfig({ ...config, defaultPath: e.target.value })}
placeholder="请输入视频保存路径..."
className="input input-bordered"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">视频大小限制MB</span>
<span className="label-text-alt text-xs">超过限制转为群文件</span>
</label>
<input
type="number"
value={config.videoSizeLimit}
onChange={(e) => setConfig({ ...config, videoSizeLimit: parseInt(e.target.value) })}
className="input input-bordered"
/>
</div>
</div>
{/* 代理配置 */}
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="form-control">
<label className="label">
<span className="label-text">代理地址</span>
</label>
<input
type="text"
value={config.proxyAddr}
onChange={(e) => setConfig({ ...config, proxyAddr: e.target.value })}
placeholder="请输入代理地址..."
className="input input-bordered"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">代理端口</span>
</label>
<input
type="text"
value={config.proxyPort}
onChange={(e) => setConfig({ ...config, proxyPort: e.target.value })}
placeholder="请输入代理端口..."
className="input input-bordered"
/>
</div>
</div>
{/* 其他基础配置 */}
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="form-control">
<label className="label">
<span className="label-text">识别前缀</span>
</label>
<input
type="text"
value={config.identifyPrefix}
onChange={(e) => setConfig({ ...config, identifyPrefix: e.target.value })}
placeholder="请输入识别前缀..."
className="input input-bordered"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">视频最大时长</span>
</label>
<input
type="number"
value={config.streamDuration}
onChange={(e) => setConfig({ ...config, streamDuration: parseInt(e.target.value) })}
className="input input-bordered"
/>
</div>
</div>
{/* 并发和定时配置 */}
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="form-control">
<label className="label">
<span className="label-text">队列并发数</span>
<span className="label-text-alt text-xs">仅影响B站下载</span>
</label>
<input
type="number"
value={config.queueConcurrency}
onChange={(e) => setConfig({ ...config, queueConcurrency: parseInt(e.target.value) })}
className="input input-bordered"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">视频下载并发数</span>
</label>
<input
type="number"
value={config.videoDownloadConcurrency}
onChange={(e) => setConfig({ ...config, videoDownloadConcurrency: parseInt(e.target.value) })}
className="input input-bordered"
/>
</div>
</div>
{/* DeepL API配置 */}
<div className="form-control w-full mb-6">
<label className="label">
<span className="label-text">DeepL API地址</span>
</label>
<textarea
value={config.deeplApiUrls}
onChange={(e) => setConfig({ ...config, deeplApiUrls: e.target.value })}
placeholder="请输入DeepL API地址多个地址用逗号分隔..."
className="textarea textarea-bordered h-24"
/>
</div>
{/* 开关配置 */}
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">兼容模式</span>
<input
type="checkbox"
checked={config.streamCompatibility}
onChange={(e) => setConfig({ ...config, streamCompatibility: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
<span className="text-xs text-base-content/70 ml-2">
NCQQ不用开启其他ICQQLLO需要开启
</span>
</div>
{/* 定时清理配置 */}
<div className="form-control w-full mt-6">
<label className="label">
<span className="label-text">自动清理时间</span>
<span className="label-text-alt text-xs">Cron表达式</span>
</label>
<input
type="text"
value={config.autoclearTrashtime}
onChange={(e) => setConfig({ ...config, autoclearTrashtime: e.target.value })}
placeholder="请输入Cron表达式..."
className="input input-bordered"
/>
</div>
</div>
</div>
{/* 保存按钮 */}
<div className="flex justify-end gap-4">
<button
className="btn btn-ghost"
onClick={handleReset}
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

@ -0,0 +1,14 @@
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

@ -0,0 +1,231 @@
import { useState, useEffect } from 'react';
import { NETEASECLOUD_QUALITY_LIST } from "../../../constants/constant.js";
import { readYamlConfig, updateYamlConfig } from '../../utils/yamlHelper';
export default function Ncm() {
const [config, setConfig] = useState({
useLocalNeteaseAPI: false,
useNeteaseSongRequest: false,
isSendVocal: true,
songRequestMaxList: 10,
neteaseCookie: '',
neteaseCloudAPIServer: '',
neteaseCloudAudioQuality: 'exhigh',
neteaseUserId: ''
});
const [loading, setLoading] = useState(false);
//
useEffect(() => {
const loadConfig = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
useLocalNeteaseAPI: yamlConfig.useLocalNeteaseAPI ?? false,
useNeteaseSongRequest: yamlConfig.useNeteaseSongRequest ?? false,
isSendVocal: yamlConfig.isSendVocal ?? true,
songRequestMaxList: yamlConfig.songRequestMaxList || 10,
neteaseCookie: yamlConfig.neteaseCookie || '',
neteaseCloudAPIServer: yamlConfig.neteaseCloudAPIServer || '',
neteaseCloudAudioQuality: yamlConfig.neteaseCloudAudioQuality || 'exhigh',
neteaseUserId: yamlConfig.neteaseUserId || ''
});
}
};
loadConfig();
}, []);
//
const handleSave = async () => {
setLoading(true);
try {
const success = await updateYamlConfig({
useLocalNeteaseAPI: config.useLocalNeteaseAPI,
useNeteaseSongRequest: config.useNeteaseSongRequest,
isSendVocal: config.isSendVocal,
songRequestMaxList: config.songRequestMaxList,
neteaseCookie: config.neteaseCookie,
neteaseCloudAPIServer: config.neteaseCloudAPIServer,
neteaseCloudAudioQuality: config.neteaseCloudAudioQuality,
neteaseUserId: config.neteaseUserId
});
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 handleReset = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
useLocalNeteaseAPI: yamlConfig.useLocalNeteaseAPI ?? false,
useNeteaseSongRequest: yamlConfig.useNeteaseSongRequest ?? false,
isSendVocal: yamlConfig.isSendVocal ?? true,
songRequestMaxList: yamlConfig.songRequestMaxList || 10,
neteaseCookie: yamlConfig.neteaseCookie || '',
neteaseCloudAPIServer: yamlConfig.neteaseCloudAPIServer || '',
neteaseCloudAudioQuality: yamlConfig.neteaseCloudAudioQuality || 'exhigh',
neteaseUserId: yamlConfig.neteaseUserId || ''
});
}
};
return (
<div className="p-6 mx-auto container">
{/* 成功提示 */}
<div id="ncm-toast-success" className="toast toast-top toast-end hidden">
<div className="alert alert-success">
<span>配置保存成功</span>
</div>
</div>
<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>
{/* 文本输入配置 */}
<div className="space-y-4 mb-6">
<div className="form-control w-full">
<label className="label">
<span className="label-text">Cookie</span>
</label>
<textarea
value={config.neteaseCookie}
onChange={(e) => setConfig({ ...config, neteaseCookie: e.target.value })}
placeholder="请输入网易云Cookie..."
className="textarea textarea-bordered h-24"
/>
</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text">自建API服务器地址</span>
</label>
<input
type="text"
value={config.neteaseCloudAPIServer}
onChange={(e) => setConfig({ ...config, neteaseCloudAPIServer: e.target.value })}
placeholder="请输入API服务器地址..."
className="input input-bordered w-full"
/>
</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text">用户ID</span>
<span className="label-text-alt text-xs text-warning">不要手动更改</span>
</label>
<input
type="text"
value={config.neteaseUserId}
onChange={(e) => setConfig({ ...config, neteaseUserId: e.target.value })}
placeholder="网易云用户ID"
className="input input-bordered w-full"
/>
</div>
</div>
{/* 开关配置部分 */}
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">使用自建API</span>
<input
type="checkbox"
checked={config.useLocalNeteaseAPI}
onChange={(e) => setConfig({ ...config, useLocalNeteaseAPI: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">开启点歌功能</span>
<input
type="checkbox"
checked={config.useNeteaseSongRequest}
onChange={(e) => setConfig({ ...config, useNeteaseSongRequest: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">发送群语音</span>
<input
type="checkbox"
checked={config.isSendVocal}
onChange={(e) => setConfig({ ...config, isSendVocal: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
</div>
{/* 其他配置 */}
<div className="grid md:grid-cols-2 gap-4">
<div className="form-control">
<label className="label">
<span className="label-text">点歌最大列表数</span>
</label>
<input
type="number"
value={config.songRequestMaxList}
onChange={(e) => setConfig({ ...config, songRequestMaxList: parseInt(e.target.value) })}
className="input input-bordered"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">音频质量</span>
</label>
<select
className="select select-bordered"
value={config.neteaseCloudAudioQuality}
onChange={(e) => setConfig({ ...config, neteaseCloudAudioQuality: e.target.value })}>
{NETEASECLOUD_QUALITY_LIST.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
</div>
</div>
{/* 保存按钮 */}
<div className="flex justify-end gap-4">
<button
className="btn btn-ghost"
onClick={handleReset}
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

@ -0,0 +1,151 @@
import { useState, useEffect } from 'react';
import { readYamlConfig, updateYamlConfig } from '../../utils/yamlHelper';
export default function Tiktok() {
const [config, setConfig] = useState({
douyinCookie: '',
douyinCompression: true,
douyinComments: false
});
const [loading, setLoading] = useState(false);
//
useEffect(() => {
const loadConfig = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
douyinCookie: yamlConfig.douyinCookie || '',
douyinCompression: yamlConfig.douyinCompression ?? true,
douyinComments: yamlConfig.douyinComments ?? false
});
}
};
loadConfig();
}, []);
//
const handleSave = async () => {
setLoading(true);
try {
const success = await updateYamlConfig({
douyinCookie: config.douyinCookie,
douyinCompression: config.douyinCompression,
douyinComments: config.douyinComments
});
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 handleReset = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
douyinCookie: yamlConfig.douyinCookie || '',
douyinCompression: yamlConfig.douyinCompression ?? true,
douyinComments: yamlConfig.douyinComments ?? false
});
}
};
return (
<div className="p-6 mx-auto container">
{/* 成功提示 */}
<div id="tiktok-toast-success" className="toast toast-top toast-end hidden">
<div className="alert alert-success">
<span>配置保存成功</span>
</div>
</div>
<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配置 */}
<div className="form-control w-full mb-6">
<label className="label">
<span className="label-text">Cookie</span>
<span className="label-text-alt text-xs text-base-content/70">
格式odin_tt=xxx;passport_fe_beating_status=xxx;...
</span>
</label>
<textarea
value={config.douyinCookie}
onChange={(e) => setConfig({ ...config, douyinCookie: e.target.value })}
placeholder="请输入抖音Cookie..."
className="textarea textarea-bordered h-24"
/>
</div>
{/* 开关配置部分 */}
<div className="grid md:grid-cols-2 gap-4">
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">视频压缩</span>
<input
type="checkbox"
checked={config.douyinCompression}
onChange={(e) => setConfig({ ...config, douyinCompression: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
<span className="text-xs text-base-content/70 ml-2">
开启后使用压缩格式加速视频发送
</span>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">显示评论</span>
<input
type="checkbox"
checked={config.douyinComments}
onChange={(e) => setConfig({ ...config, douyinComments: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
<span className="text-xs text-base-content/70 ml-2">
是否显示视频评论
</span>
</div>
</div>
</div>
</div>
{/* 保存按钮 */}
<div className="flex justify-end gap-4">
<button
className="btn btn-ghost"
onClick={handleReset}
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

@ -0,0 +1,11 @@
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

@ -0,0 +1,168 @@
import { useState, useEffect } from 'react';
import { BILI_CDN_SELECT_LIST, YOUTUBE_GRAPHICS_LIST } from "../../../constants/constant.js";
import { readYamlConfig, updateYamlConfig } from '../../utils/yamlHelper';
export default function Youtube() {
const [config, setConfig] = useState({
youtubeGraphicsOptions: 720,
youtubeClipTime: 0,
youtubeDuration: 480,
youtubeCookiePath: ''
});
const [loading, setLoading] = useState(false);
//
useEffect(() => {
const loadConfig = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
youtubeGraphicsOptions: yamlConfig.youtubeGraphicsOptions || 720,
youtubeClipTime: yamlConfig.youtubeClipTime || 0,
youtubeDuration: yamlConfig.youtubeDuration || 480,
youtubeCookiePath: yamlConfig.youtubeCookiePath || ''
});
}
};
loadConfig();
}, []);
//
const handleSave = async () => {
setLoading(true);
try {
const success = await updateYamlConfig({
youtubeGraphicsOptions: config.youtubeGraphicsOptions,
youtubeClipTime: config.youtubeClipTime,
youtubeDuration: config.youtubeDuration,
youtubeCookiePath: config.youtubeCookiePath
});
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 handleReset = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
youtubeGraphicsOptions: yamlConfig.youtubeGraphicsOptions || 720,
youtubeClipTime: yamlConfig.youtubeClipTime || 0,
youtubeDuration: yamlConfig.youtubeDuration || 480,
youtubeCookiePath: yamlConfig.youtubeCookiePath || ''
});
}
};
return (
<div className="p-6 mx-auto container">
{/* 成功提示 */}
<div id="youtube-toast-success" className="toast toast-top toast-end hidden">
<div className="alert alert-success">
<span>配置保存成功</span>
</div>
</div>
<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>
{/* Cookie路径配置 */}
<div className="form-control w-full mb-6">
<label className="label">
<span className="label-text">Cookie文件路径</span>
</label>
<input
type="text"
value={config.youtubeCookiePath}
onChange={(e) => setConfig({ ...config, youtubeCookiePath: e.target.value })}
placeholder="请输入Cookie.txt文件路径..."
className="input input-bordered w-full"
/>
</div>
{/* 数值配置部分 */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="form-control">
<label className="label">
<span className="label-text">下载画质</span>
<span className="label-text-alt text-xs">0为原画</span>
</label>
<select
className="select select-bordered"
value={config.youtubeGraphicsOptions}
onChange={(e) => setConfig({ ...config, youtubeGraphicsOptions: parseInt(e.target.value) })}>
{
YOUTUBE_GRAPHICS_LIST.map(item => {
return (
<option value={ item.value }>{ item.label }</option>
)
})
}
</select>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">最大截取时长</span>
<span className="label-text-alt text-xs">建议不超过5分钟</span>
</label>
<input
type="number"
value={config.youtubeClipTime}
onChange={(e) => setConfig({ ...config, youtubeClipTime: parseInt(e.target.value) })}
className="input input-bordered"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">视频时长限制</span>
<span className="label-text-alt text-xs">建议不超过30分钟</span>
</label>
<input
type="number"
value={config.youtubeDuration}
onChange={(e) => setConfig({ ...config, youtubeDuration: parseInt(e.target.value) })}
className="input input-bordered"
/>
</div>
</div>
</div>
</div>
{/* 保存按钮 */}
<div className="flex justify-end gap-4">
<button
className="btn btn-ghost"
onClick={handleReset}
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

@ -0,0 +1,67 @@
"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

@ -0,0 +1,46 @@
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);
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));
}, []);
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>R 插件的自动选择更新 / 强制更新</p>
</div>
<button className="btn btn-warning">🚧施工</button>
</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

@ -0,0 +1,17 @@
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

@ -0,0 +1,44 @@
import React, { useEffect, useState } from "react";
import { BOT_INFO_URL } from "../../constants/api.js";
export function BotItem() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(BOT_INFO_URL)
.then(response => {
return response.json();
})
.then(data => setUser(data))
}, []);
return (
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">🐔状态</h2>
<div className="flex flex-row pt-5 justify-between items-center">
<div className={ `avatar ${ 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` }/>
</div>
</div>
<div className="flex flex-col ml-12 space-y-2">
<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="space-x-1">
<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>
</div>
)
}

View File

@ -0,0 +1,105 @@
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

@ -0,0 +1,147 @@
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

@ -0,0 +1,116 @@
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

@ -0,0 +1,45 @@
"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>
);
}

11
server/constants/api.js Normal file
View File

@ -0,0 +1,11 @@
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`;

130
server/constants/sidebar.js Normal file
View File

@ -0,0 +1,130 @@
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

@ -0,0 +1,22 @@
"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);

5
server/next.config.js Normal file
View File

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

6
server/postcss.config.js Normal file
View File

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

41
server/styles/global.css Normal file
View File

@ -0,0 +1,41 @@
@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;
/* 滑块悬停时的颜色 */
}

23
server/tailwind.config.js Normal file
View File

@ -0,0 +1,23 @@
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;

17
server/utils/redis.js Normal file
View File

@ -0,0 +1,17 @@
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

@ -0,0 +1,29 @@
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;
}
};

60
start-nextjs.js Normal file
View File

@ -0,0 +1,60 @@
import { spawn } from 'child_process';
logger.info(`[R插件][Next.js监测], 父进程 PID: ${process.pid}`);
let nextjsProcess = null;
// 构建应用程序
export const buildNextJs = () => {
logger.info(logger.yellow('[R插件][Next.js监测],正在构建 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插件][Next.js监测],构建失败,退出码:${code}`);
reject(new Error('Build failed'));
}
});
});
};
// 启动子进程运行 Next.js
export const startNextJs = (mode = 'start') => {
const script = mode === 'start' ? 'start' : 'dev';
logger.info(logger.yellow(`[R插件][Next.js监测],启动 Next.js ${mode} 进程...`));
nextjsProcess = spawn('pnpm', ['run', script], {
cwd: './plugins/rconsole-plugin', // 指定工作目录
stdio: ['ignore', 'ignore', 'ignore', 'ipc'], // 继承父进程的标准输入输出
shell: true,
});
// 子进程异常退出时捕获信号
nextjsProcess.on('close', (code) => {
logger.error(`[R插件][Next.js监测]Next.js 进程发生异常 ${code}`);
nextjsProcess = null;
});
};
// 捕获父进程退出信号
const cleanup = () => {
logger.info(logger.yellow('[R插件][Next.js监测] 父进程退出,终止子进程...'));
if (nextjsProcess) {
nextjsProcess.kill(); // 终止子进程
}
process.exit();
};
// 绑定父进程的退出信号
process.on('SIGINT', cleanup); // Ctrl+C 信号
process.on('SIGTERM', cleanup); // kill 命令信号
process.on('exit', cleanup); // 正常退出

View File

@ -86,51 +86,3 @@ export async function redisExistAndUpdateObject(key, updateKey, updateObj) {
await redisSetKey(key, objs); await redisSetKey(key, objs);
} }
} }
/**
* 删除某个key
* @param key
* @returns {Promise<number>}
* @example
* const result = await redisDeleteKey('myKey');
* console.log(result); // 1 if key was deleted, 0 if key did not exist
*/
export async function redisDeleteKey(key) {
return redis.del(key);
}
/**
* 获取所有的key
* @returns {Promise<Array<string>>}
* @example
* const keys = await redisGetAllKeys();
* console.log(keys); // ['key1', 'key2', ...]
*/
export async function redisGetAllKeys() {
return redis.keys('*');
}
/**
* 设置某个key的过期时间
* @param key
* @param seconds
* @returns {Promise<boolean>}
* @example
* const result = await redisExpireKey('myKey', 3600);
* console.log(result); // true if timeout was set, false if key does not exist
*/
export async function redisExpireKey(key, seconds) {
return redis.expire(key, seconds);
}
/**
* 获取某个key的剩余生存时间
* @param key
* @returns {Promise<number>}
* @example
* const ttl = await redisTTLKey('myKey');
* console.log(ttl); // time to live in seconds, -1 if key does not have timeout, -2 if key does not exist
*/
export async function redisTTLKey(key) {
return redis.ttl(key);
}

View File

@ -124,4 +124,31 @@ export async function getReplyMsg(e) {
"message_id" : msgId "message_id" : msgId
}) })
return msg.data return msg.data
} }
/**
* 获取机器人信息
* @param e
* @returns {Promise<*>}
*/
export async function getBotLoginInfo(e) {
return await e.bot.sendApi("get_login_info");
}
/**
* 获取运行状态
* @param e
* @returns {Promise<*>}
*/
export async function getBotStatus(e) {
return await e.bot.sendApi("get_status");
}
/**
* 获取版本信息
* @param e
* @returns {Promise<*>}
*/
export async function getBotVersionInfo(e) {
return await e.bot.sendApi("get_version_info");
}