diff --git a/apps/webUI.js b/apps/webUI.js
new file mode 100644
index 0000000..06c23d7
--- /dev/null
+++ b/apps/webUI.js
@@ -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;
+ }
+}
diff --git a/config/tools.yaml b/config/tools.yaml
index b86c1f6..96ed8ee 100644
--- a/config/tools.yaml
+++ b/config/tools.yaml
@@ -1,3 +1,4 @@
+isOpenWebUI: false # 是否开启webui
defaultPath: './data/rcmp4/' # 保存视频的位置
videoSizeLimit: 70 # 视频大小限制(单位MB),超过大小则转换成群文件上传
proxyAddr: '127.0.0.1' # 魔法地址
diff --git a/constants/constant.js b/constants/constant.js
index 0becdb5..930046f 100644
--- a/constants/constant.js
+++ b/constants/constant.js
@@ -93,10 +93,10 @@ export const REDIS_YUNZAI_CLOUDSONGLIST = "Yz:rconsole:tools:cloudsonglist";
export const REDIS_YUNZAI_WHITELIST = "Yz:rconsole:tools:whitelist";
/**
- * 番剧列表缓存
+ * WEBUI需要数据的缓存
* @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 = "";
diff --git a/index.js b/index.js
index d2f5c43..c80cc07 100644
--- a/index.js
+++ b/index.js
@@ -1,12 +1,15 @@
import fs from "node:fs";
import path from "path";
import config from "./model/config.js";
+import { buildNextJs, startNextJs } from "./start-nextjs.js";
if (!global.segment) {
global.segment = (await import("oicq")).segment
}
// 加载版本号
const versionData = config.getConfig("version");
+// 加载是否使用WebUI
+const isOpenWebUI = config.getConfig("tools").isOpenWebUI;
// 加载名称
const packageJsonPath = path.join('./plugins', 'rconsole-plugin', 'package.json');
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]];
}
+
+// 检查是否启动 webui
+if (isOpenWebUI) {
+ buildNextJs()
+ .then(() => startNextJs('start'))
+}
+
export { apps };
diff --git a/package.json b/package.json
index 8c73196..221716a 100644
--- a/package.json
+++ b/package.json
@@ -2,11 +2,31 @@
"name": "rconsole-plugin",
"description": "R-Plugin",
"type": "module",
+ "scripts": {
+ "dev": "cd server && next dev -p 4016",
+ "start": "cd server && next start -p 4016",
+ "build": "cd server && next build"
+ },
"dependencies": {
"axios": "^1.3.4",
+ "chart.js": "^4.4.6",
"form-data": "^4.0.1",
+ "ioredis": "^5.4.1",
+ "js-yaml": "^4.1.0",
+ "next": "^14.2.16",
"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",
- "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"
}
}
diff --git a/server/app/layout.jsx b/server/app/layout.jsx
new file mode 100644
index 0000000..e8acbcc
--- /dev/null
+++ b/server/app/layout.jsx
@@ -0,0 +1,9 @@
+import "../styles/global.css";
+
+export default function RootLayout({ children }) {
+ return (
+
+
{children}
+
+ )
+}
diff --git a/server/app/page.jsx b/server/app/page.jsx
new file mode 100644
index 0000000..63aed4c
--- /dev/null
+++ b/server/app/page.jsx
@@ -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 (
+
+
+
+
+ )
+}
diff --git a/server/app/r/api/bot/route.js b/server/app/r/api/bot/route.js
new file mode 100644
index 0000000..9066032
--- /dev/null
+++ b/server/app/r/api/bot/route.js
@@ -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' },
+ });
+}
diff --git a/server/app/r/api/commit/route.js b/server/app/r/api/commit/route.js
new file mode 100644
index 0000000..4860c3e
--- /dev/null
+++ b/server/app/r/api/commit/route.js
@@ -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' },
+ });
+}
diff --git a/server/app/r/api/config/route.js b/server/app/r/api/config/route.js
new file mode 100644
index 0000000..2e6f69e
--- /dev/null
+++ b/server/app/r/api/config/route.js
@@ -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' },
+ });
+ }
+}
diff --git a/server/app/r/api/network/route.js b/server/app/r/api/network/route.js
new file mode 100644
index 0000000..ba2c6af
--- /dev/null
+++ b/server/app/r/api/network/route.js
@@ -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' },
+ })
+ }
+}
diff --git a/server/app/r/api/network2/route.js b/server/app/r/api/network2/route.js
new file mode 100644
index 0000000..1b96685
--- /dev/null
+++ b/server/app/r/api/network2/route.js
@@ -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' },
+ });
+ }
+}
diff --git a/server/app/r/api/system/route.js b/server/app/r/api/system/route.js
new file mode 100644
index 0000000..af08533
--- /dev/null
+++ b/server/app/r/api/system/route.js
@@ -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 });
+ }
+}
diff --git a/server/app/r/api/version/route.js b/server/app/r/api/version/route.js
new file mode 100644
index 0000000..de810b7
--- /dev/null
+++ b/server/app/r/api/version/route.js
@@ -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' },
+ });
+}
diff --git a/server/components/ThemeToggle.jsx b/server/components/ThemeToggle.jsx
new file mode 100644
index 0000000..67ba3a8
--- /dev/null
+++ b/server/components/ThemeToggle.jsx
@@ -0,0 +1,23 @@
+import React, { useState } from 'react';
+
+function ThemeToggle() {
+ // 用于保存主题状态,默认为“light”主题
+ const [isDarkTheme, setIsDarkTheme] = useState(false);
+
+ // 切换主题时的处理函数
+ const handleThemeChange = () => {
+ setIsDarkTheme(!isDarkTheme);
+ };
+
+ return (
+
+ );
+}
+
+export default ThemeToggle;
diff --git a/server/components/content.jsx b/server/components/content.jsx
new file mode 100644
index 0000000..135534a
--- /dev/null
+++ b/server/components/content.jsx
@@ -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 (
+
+ {currentItem?.component || SIDEBAR_ITEMS[0].component}
+
+ );
+}
diff --git a/server/components/contents/bili.jsx b/server/components/contents/bili.jsx
new file mode 100644
index 0000000..61a0a46
--- /dev/null
+++ b/server/components/contents/bili.jsx
@@ -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 (
+
+ {/* 成功提示 */}
+
+
+
+
Bilibili 配置
+
+ {/* 基础配置部分 */}
+
+
+
基础配置
+
+ {/* SESSDATA配置 */}
+
+
+ setConfig({ ...config, biliSessData: e.target.value })}
+ placeholder="请输入Bilibili SESSDATA"
+ className="input input-bordered w-full"
+ />
+
+
+ {/* 数值配置部分 */}
+
+
+ {/* 开关配置部分 */}
+
+
+ {/* 下拉选择配置部分 */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* 保存按钮 */}
+
+
+
+
+
+
+ )
+}
diff --git a/server/components/contents/generic.jsx b/server/components/contents/generic.jsx
new file mode 100644
index 0000000..bf08997
--- /dev/null
+++ b/server/components/contents/generic.jsx
@@ -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 (
+
+ {/* 成功提示 */}
+
+
+
+
通用配置
+
+ {/* 基础配置部分 */}
+
+
+
基础配置
+
+ {/* 路径和大小限制配置 */}
+
+
+ {/* 代理配置 */}
+
+
+ {/* 其他基础配置 */}
+
+
+ {/* 并发和定时配置 */}
+
+
+ {/* DeepL API配置 */}
+
+
+
+
+ {/* 开关配置 */}
+
+
+
+ NCQQ不用开启,其他ICQQ、LLO需要开启
+
+
+
+ {/* 定时清理配置 */}
+
+
+ setConfig({ ...config, autoclearTrashtime: e.target.value })}
+ placeholder="请输入Cron表达式..."
+ className="input input-bordered"
+ />
+
+
+
+
+ {/* 保存按钮 */}
+
+
+
+
+
+
+ );
+}
diff --git a/server/components/contents/home.jsx b/server/components/contents/home.jsx
new file mode 100644
index 0000000..6d68f90
--- /dev/null
+++ b/server/components/contents/home.jsx
@@ -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 (
+
+
+
+
+
+ );
+}
diff --git a/server/components/contents/ncm.jsx b/server/components/contents/ncm.jsx
new file mode 100644
index 0000000..77b6b7b
--- /dev/null
+++ b/server/components/contents/ncm.jsx
@@ -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 (
+
+ {/* 成功提示 */}
+
+
+
+
网易云音乐配置
+
+ {/* 基础配置部分 */}
+
+
+
基础配置
+
+ {/* 文本输入配置 */}
+
+
+
+
+
+
+ setConfig({ ...config, neteaseCloudAPIServer: e.target.value })}
+ placeholder="请输入API服务器地址..."
+ className="input input-bordered w-full"
+ />
+
+
+
+ setConfig({ ...config, neteaseUserId: e.target.value })}
+ placeholder="网易云用户ID"
+ className="input input-bordered w-full"
+ />
+
+
+
+ {/* 开关配置部分 */}
+
+
+ {/* 其他配置 */}
+
+
+
+ setConfig({ ...config, songRequestMaxList: parseInt(e.target.value) })}
+ className="input input-bordered"
+ />
+
+
+
+
+
+
+
+
+
+ {/* 保存按钮 */}
+
+
+
+
+
+
+ );
+}
diff --git a/server/components/contents/tiktok.jsx b/server/components/contents/tiktok.jsx
new file mode 100644
index 0000000..38ea112
--- /dev/null
+++ b/server/components/contents/tiktok.jsx
@@ -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 (
+
+ {/* 成功提示 */}
+
+
+
+
抖音配置
+
+ {/* 基础配置部分 */}
+
+
+
基础配置
+
+ {/* Cookie配置 */}
+
+
+
+
+ {/* 开关配置部分 */}
+
+
+
+
+ {/* 保存按钮 */}
+
+
+
+
+
+
+ );
+}
diff --git a/server/components/contents/weekly.jsx b/server/components/contents/weekly.jsx
new file mode 100644
index 0000000..c3767d5
--- /dev/null
+++ b/server/components/contents/weekly.jsx
@@ -0,0 +1,11 @@
+export default function Weekly() {
+ return (
+
+
+
+ )
+}
diff --git a/server/components/contents/youtube.jsx b/server/components/contents/youtube.jsx
new file mode 100644
index 0000000..107a7ce
--- /dev/null
+++ b/server/components/contents/youtube.jsx
@@ -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 (
+
+ {/* 成功提示 */}
+
+
+
+
YouTube 配置
+
+ {/* 基础配置部分 */}
+
+
+
基础配置
+
+ {/* Cookie路径配置 */}
+
+
+ setConfig({ ...config, youtubeCookiePath: e.target.value })}
+ placeholder="请输入Cookie.txt文件路径..."
+ className="input input-bordered w-full"
+ />
+
+
+ {/* 数值配置部分 */}
+
+
+
+
+
+
+
+ setConfig({ ...config, youtubeClipTime: parseInt(e.target.value) })}
+ className="input input-bordered"
+ />
+
+
+
+ setConfig({ ...config, youtubeDuration: parseInt(e.target.value) })}
+ className="input input-bordered"
+ />
+
+
+
+
+
+ {/* 保存按钮 */}
+
+
+
+
+
+
+ );
+}
diff --git a/server/components/header.jsx b/server/components/header.jsx
new file mode 100644
index 0000000..8f2c03e
--- /dev/null
+++ b/server/components/header.jsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+

+
+
+
+
{user?.nickname || "未获取"}
+
{user?.user_id || "NaN"}
+
+
+
+
+ )
+};
diff --git a/server/components/home/bot-config.jsx b/server/components/home/bot-config.jsx
new file mode 100644
index 0000000..a8a8058
--- /dev/null
+++ b/server/components/home/bot-config.jsx
@@ -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 (
+
+
+
🔥更新看板
+
+
+
+
最新版本
+
当前最新版本为:{ version }
+
+
+
+
+
+
手动更新
+
R 插件的自动选择更新 / 强制更新
+
+
+
+
+
+
+
+ );
+}
diff --git a/server/components/home/bot-info.jsx b/server/components/home/bot-info.jsx
new file mode 100644
index 0000000..ded27b8
--- /dev/null
+++ b/server/components/home/bot-info.jsx
@@ -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 (
+
+
+ {/* 机器人信息卡片 */ }
+
+
+
+
+
+ )
+}
diff --git a/server/components/home/bot-item.jsx b/server/components/home/bot-item.jsx
new file mode 100644
index 0000000..50a4298
--- /dev/null
+++ b/server/components/home/bot-item.jsx
@@ -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 (
+
+
+
🐔状态
+
+
+
+

+
+
+
+
+
昵称:{ user?.nickname || "未获取" }
+
QQ号:{ user?.user_id || "NaN" }
+
+
+
协议信息:
+
+
{ user?.app_name }
+
{ user?.app_version }
+
{ user?.protocol_version }
+
+
+
+
+
+
+ )
+}
diff --git a/server/components/home/bot-network.jsx b/server/components/home/bot-network.jsx
new file mode 100644
index 0000000..5d07ae0
--- /dev/null
+++ b/server/components/home/bot-network.jsx
@@ -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: (
+
+ )
+ },
+ {
+ name: "Github",
+ url: "https://github.com/",
+ icon: (
+
+ )
+ },
+ {
+ name: "YouTube",
+ url: "https://youtube.com/",
+ icon: (
+
+ )
+ },
+ {
+ name: "Tiktok",
+ url: "https://tiktok.com/",
+ icon: (
+
+ )
+ },
+ // ... 其他链接配置类似
+];
+
+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 (
+
+
+
+
🌐网速
+
+
+
+ { TESTING_LINKS.map((link, index) => (
+
+ { link.icon }
+ { linksTime[index] }
+
+ )) }
+
+
+
+
+
+
+
+ );
+}
diff --git a/server/components/home/network.jsx b/server/components/home/network.jsx
new file mode 100644
index 0000000..4e632d6
--- /dev/null
+++ b/server/components/home/network.jsx
@@ -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 (
+
+
+
+
+
网络监控
+
+
+
上传: {networkData.uploadSpeed} KB/s
+
下载: {networkData.downloadSpeed} KB/s
+
+
+
总发送: {networkData.totalSent} GB
+
总接收: {networkData.totalReceived} GB
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/server/components/home/system.jsx b/server/components/home/system.jsx
new file mode 100644
index 0000000..0754bbb
--- /dev/null
+++ b/server/components/home/system.jsx
@@ -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 (
+
+
+ {/* 状态卡片 */ }
+
+
+
状态
+
+
+
+
+
+
CPU
+
{ systemInfo ? `( ${ systemInfo.cpuCoresUsed } / ${ systemInfo.totalCpuCores } ) 核` : "" }
+
+
+
+
+
+
内存
+
{systemInfo ? `${systemInfo.usedMemory} / ${systemInfo.totalMemory}` : ""}
+
+
+
+
+
+
磁盘使用
+
{systemInfo ? `${systemInfo.usedDisk} / ${systemInfo.totalDisk}` : ""}
+
+
+
+
+
+
+ {/* 系统信息卡片 */ }
+
+
+
系统信息
+
主机名称: {systemInfo ? systemInfo.hostname : ""}
+
发行版本: {systemInfo ? systemInfo.distro : ""}
+
内核版本: {systemInfo ? systemInfo.kernelVersion : ""}
+
系统类型: {systemInfo ? systemInfo.arch : ""}
+
主机地址: {systemInfo ? systemInfo.ipAddress : ""}
+
运行时间: {systemInfo ? systemInfo.uptime : ""}
+
+
+
+
+ );
+}
diff --git a/server/components/sidebar.jsx b/server/components/sidebar.jsx
new file mode 100644
index 0000000..a1287d5
--- /dev/null
+++ b/server/components/sidebar.jsx
@@ -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 (
+
+ );
+}
diff --git a/server/constants/api.js b/server/constants/api.js
new file mode 100644
index 0000000..be7f14a
--- /dev/null
+++ b/server/constants/api.js
@@ -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`;
diff --git a/server/constants/sidebar.js b/server/constants/sidebar.js
new file mode 100644
index 0000000..57bf14b
--- /dev/null
+++ b/server/constants/sidebar.js
@@ -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: ,
+ theme: "light",
+ component:
+ },
+ {
+ name: "通用及杂项",
+ icon: ,
+ theme: "cupcake",
+ component:
+ },
+ {
+ name: "哔哩哔哩控制台",
+ icon: ,
+ theme: "valentine",
+ component:
+ },
+ {
+ name: "抖音控制台",
+ icon: ,
+ theme: "dark",
+ component:
+ },
+ {
+ name: "油管控制台",
+ icon: ,
+ theme: "dracula",
+ component:
+ },
+ {
+ name: "网易云控制台",
+ icon: ,
+ theme: "lofi",
+ component:
+ },
+ {
+ name: "周刊预览",
+ icon: ,
+ theme: "retro",
+ component:
+ }
+];
diff --git a/server/contexts/drawer-context.js b/server/contexts/drawer-context.js
new file mode 100644
index 0000000..ced0fed
--- /dev/null
+++ b/server/contexts/drawer-context.js
@@ -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 (
+
+ {children}
+
+ );
+};
+
+export const useDrawer = () => useContext(DrawerContext);
+
+
diff --git a/server/next.config.js b/server/next.config.js
new file mode 100644
index 0000000..2333c75
--- /dev/null
+++ b/server/next.config.js
@@ -0,0 +1,5 @@
+export default {
+ eslint: {
+ ignoreDuringBuilds: true, // 构建时忽略 ESLint 错误
+ },
+};
diff --git a/server/postcss.config.js b/server/postcss.config.js
new file mode 100644
index 0000000..28c1b6d
--- /dev/null
+++ b/server/postcss.config.js
@@ -0,0 +1,6 @@
+// postcss.config.js
+export default {
+ plugins: {
+ tailwindcss: {},
+ },
+};
diff --git a/server/styles/global.css b/server/styles/global.css
new file mode 100644
index 0000000..1f09233
--- /dev/null
+++ b/server/styles/global.css
@@ -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;
+ /* 滑块悬停时的颜色 */
+}
diff --git a/server/tailwind.config.js b/server/tailwind.config.js
new file mode 100644
index 0000000..34e338e
--- /dev/null
+++ b/server/tailwind.config.js
@@ -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;
diff --git a/server/utils/redis.js b/server/utils/redis.js
new file mode 100644
index 0000000..3744815
--- /dev/null
+++ b/server/utils/redis.js
@@ -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,
+})
diff --git a/server/utils/yamlHelper.js b/server/utils/yamlHelper.js
new file mode 100644
index 0000000..c9abffe
--- /dev/null
+++ b/server/utils/yamlHelper.js
@@ -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;
+ }
+};
\ No newline at end of file
diff --git a/start-nextjs.js b/start-nextjs.js
new file mode 100644
index 0000000..94181bf
--- /dev/null
+++ b/start-nextjs.js
@@ -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); // 正常退出
diff --git a/utils/redis-util.js b/utils/redis-util.js
index cd1ddf4..1d15eea 100644
--- a/utils/redis-util.js
+++ b/utils/redis-util.js
@@ -86,51 +86,3 @@ export async function redisExistAndUpdateObject(key, updateKey, updateObj) {
await redisSetKey(key, objs);
}
}
-
-/**
- * 删除某个key
- * @param key
- * @returns {Promise}
- * @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>}
- * @example
- * const keys = await redisGetAllKeys();
- * console.log(keys); // ['key1', 'key2', ...]
- */
-export async function redisGetAllKeys() {
- return redis.keys('*');
-}
-
-/**
- * 设置某个key的过期时间
- * @param key
- * @param seconds
- * @returns {Promise}
- * @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}
- * @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);
-}
\ No newline at end of file
diff --git a/utils/yunzai-util.js b/utils/yunzai-util.js
index f0799ea..fa343c1 100644
--- a/utils/yunzai-util.js
+++ b/utils/yunzai-util.js
@@ -124,4 +124,31 @@ export async function getReplyMsg(e) {
"message_id" : msgId
})
return msg.data
-}
\ No newline at end of file
+}
+
+/**
+ * 获取机器人信息
+ * @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");
+}