@baipiaodajun/mcbots
Version:
Minecraft bot and status dashboard for multi-server management
1,560 lines (1,396 loc) • 56 kB
JavaScript
const mineflayer = require('mineflayer');
const express = require('express');
const net = require('net');
const crypto = require('crypto');
const cookieParser = require('cookie-parser');
const path = require('path');
const fs = require('fs');
const PORT = process.env.SERVER_PORT || process.env.PORT || 3000 ;
const CHAT = process.env.CHAT || false ;
const MOVE = process.env.MOVE || false ;
// Minecraft服务器配置
const SERVERS = [
{
host: "127.0.0.1",
port: 25565,
minBots: 1,
maxBots: 3,
version: "1.20.1"
}
];
const CONFIG_FILE = path.resolve(process.cwd(), './servers.json'); // 根据你的实际路径调整
let loaded = false;
// 1. 优先尝试从 ./servers.json 读取
if (fs.existsSync(CONFIG_FILE)) {
try {
const fileContent = fs.readFileSync(CONFIG_FILE, 'utf-8').trim();
if (fileContent) {
const fileConfig = JSON.parse(fileContent);
if (Array.isArray(fileConfig)) {
SERVERS.length = 0;
SERVERS.push(...fileConfig);
console.log(`从 ${CONFIG_FILE} 加载服务器配置成功,数量: ${SERVERS.length} 项`);
loaded = true;
} else {
console.warn('配置文件内容不是数组,已忽略');
}
}
} catch (err) {
console.error(`读取或解析 ${CONFIG_FILE} 失败,将回落到环境变量`);
console.error('错误详情:', err.message);
}
}
// 2. 文件不存在或解析失败 → 回退到环境变量 SERVERS_JSON
if (!loaded && process.env.SERVERS_JSON) {
try {
const envConfig = JSON.parse(process.env.SERVERS_JSON);
if (Array.isArray(envConfig)) {
SERVERS.length = 0;
SERVERS.push(...envConfig);
console.log(`从环境变量 SERVERS_JSON 加载服务器配置,数量: ${SERVERS.length} 项`);
loaded = true;
} else {
throw new Error('环境变量 SERVERS_JSON 内容不是数组');
}
} catch (error) {
console.error('无法解析环境变量 SERVERS_JSON');
console.error('原因:', error.message);
console.error('原始内容预览:');
console.error(process.env.SERVERS_JSON?.slice(0, 500) + (process.env.SERVERS_JSON?.length > 500 ? '...' : ''));
console.error('请检查 JSON 格式是否正确(双引号、逗号、括号等)');
console.error('程序即将退出...');
process.exit(1);
}
}
// 3. 都没有 → 报错退出(防止空配置启动)
if (!loaded) {
console.error('未找到任何有效的服务器配置!');
console.error(`请提供以下任意一种方式:`);
console.error(` 1. 在项目根目录放置 ./config/servers.json 文件`);
console.error(` 2. 设置环境变量 SERVERS_JSON='[{"host":"...", "port":...}, ...]'`);
console.error('程序即将退出...');
process.exit(1);
}
/**
* 测试 IP 和端口是否可达
* @param {string} host - IP 或域名
* @param {number} port - 端口号
* @param {number} timeoutMs - 超时时间(毫秒)
* @returns {Promise<boolean>}
*/
function testConnection(host, port, timeoutMs = 5000) {
return new Promise((resolve) => {
const socket = new net.Socket();
// 成功连接
socket.once('connect', () => {
socket.destroy();
resolve(true);
});
// 出错或拒绝连接
socket.once('error', () => {
socket.destroy();
resolve(false);
});
// 超时
socket.setTimeout(timeoutMs, () => {
socket.destroy();
resolve(false);
});
// 尝试连接
socket.connect(port, host);
});
}
// 配置常量
const BOT_CONFIG = {
reconnectDelay: 5000,
maxReconnectAttempts: 5,
healthCheckInterval: 60000,
viewDistance: 4,
connectTimeout: 15000,
chat: CHAT,
move: MOVE
};
const SERVER_CONFIG = {
statusCheckInterval: 30000,
maxFailedAttempts: 3,
resetTimeout: 30000
};
// 全局状态存储
const globalServerStatus = {
servers: new Map()
};
function generateUsername() {
const adjectives = [
'Clever', 'Swift', 'Brave', 'Sneaky', 'Happy', 'Crazy', 'Silky',
'Fluffy', 'Shiny', 'Quick', 'Mighty', 'Tiny', 'Wise', 'Lazy'
];
const animals = [
'Fox', 'Wolf', 'Bear', 'Panda', 'Tiger', 'Eagle', 'Shark',
'Mole', 'Badger', 'Otter', 'Cat', 'Frog', 'Dog'
];
const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
const animal = animals[Math.floor(Math.random() * animals.length)];
return `${adjective}${animal}`;
}
class HotUpdateAuth {
constructor() {
this.hashedSecrets = new Set();
this.salt = process.env.HOTUPDATE_SALT || 'mc-bot-manager-2025-salt';
if (process.env.HOTUPDATE_SECRET) {
const hash = crypto.createHash('sha256')
.update(process.env.HOTUPDATE_SECRET)
.digest('hex');
this.hashedSecrets.add(hash);
console.log('[热更新认证] 已从明文环境变量加载并哈希密钥');
}
else{
this.hashedSecrets = new Set([
// 用下面命令生成(不要直接把明文写在这里):
// node -e "console.log(require('crypto').createHash('sha256').update('你的超级强密钥2025').digest('hex'))"
'1cc1e7dd16a37dda1837bb44fc559024fb89961d94548fe9db1700721c489366',
// 'a1b2c3d4...' // ← 可以再加一条旧密钥,方便轮换
]);
}
}
// 带 salt 的哈希(更防彩虹表)
_hashWithSalt(secret) {
return crypto.createHash('sha256')
.update(secret + this.salt)
.digest('hex');
}
// 主验证函数
verify(secret) {
if (!secret) return false;
const plainHash = crypto.createHash('sha256').update(secret).digest('hex');
const saltedHash = this._hashWithSalt(secret);
// 支持两种存储方式(兼容旧项目)
return [...this.hashedSecrets].some(h => h === plainHash || h === saltedHash);
}
// 方便你在控制台生成新哈希(部署时运行一次即可)
static generateHash(plain) {
const salt = process.env.HOTUPDATE_SALT || 'mc-bot-manager-2025-salt';
const hash = crypto.createHash('sha256').update(plain + salt).digest('hex');
console.log('明文密钥 :', plain);
console.log('加盐 SHA256:', hash);
return hash;
}
}
const hotUpdateAuth =new HotUpdateAuth();
// Minecraft机器人管理器
class MinecraftBotManager {
constructor(host, port, minBots, maxBots, version) {
this.host = host;
this.port = port;
this.minBots = minBots;
this.maxBots = maxBots;
this.version = version || "1.20.1";
this.currentBots = 0;
this.activeBots = new Map();
this.failedAttempts = 0;
this.lastUpdate = Date.now();
this.status = 'initializing';
this.monitoringInterval = null;
this.timeout= null;
this.reachable=false;
this.lastReachable = null;
this.resetTimer=null;
this.notice=true;
// 机器人名称池
this.botNames = Array.from({ length: 20 }, () => generateUsername());
// 注册到全局状态
globalServerStatus.servers.set(`${host}:${port}`, {
host: host,
port: port,
minBots: minBots,
maxBots: maxBots,
currentBots: 0,
activeBots: [],
lastUpdate: Date.now(),
status: 'initializing'
});
}
dispose() {
const key = `${this.host}:${this.port}`;
console.log(`[${key}] 正在释放 BotManager 所有资源...`);
// 1. 停止所有正在运行的机器人(最重要!)
this.activeBots.forEach((bot, botName) => {
try {
if (bot && typeof bot.end === 'function') {
bot.removeAllListeners(); // 关键!移除所有事件监听,防止回调残留
bot.end(); // 主动断开连接
}
console.log(`[${key}] 已断开机器人: ${botName}`);
} catch (err) {
console.warn(`[${key}] 断开 ${botName} 时出错:`, err.message);
}
});
this.activeBots.clear();
this.currentBots = 0;
// 2. 清理所有定时器(防止残留 setInterval/setTimeout)
if (this.monitoringInterval) {
clearInterval(this.monitoringInterval);
this.monitoringInterval = null;
console.log(`[${key}] 已清理 monitoringInterval`);
}
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
if (this.resetTimer) {
clearTimeout(this.resetTimer);
this.resetTimer = null;
console.log(`[${key}] 已清理 resetTimer`);
}
// 4. 从全局状态移除(前端立刻看不到)
globalServerStatus.servers.delete(key);
console.log(`[${key}] 已从 globalServerStatus 中移除`);
// 5. 清理自身引用,帮助 GC
this.botNames = null;
this.activeBots = null;
this.status = 'disposed';
this.reachable = false;
console.log(`[${key}] BotManager 资源释放完成`);
}
// 生成随机机器人名称
generateBotName() {
const baseName = this.botNames[Math.floor(Math.random() * this.botNames.length)];
const randomNum = Math.floor(Math.random() * 1000);
return `${baseName}${randomNum}`;
}
async testmc() {
const reachable = await testConnection(this.host, this.port, 3000);
this.reachable = reachable;
// 只有状态发生变化时才通知
if (this.reachable !== this.lastReachable) {
if (!this.reachable) {
console.log(`[${this.host}:${this.port}] MC服务器网络不可达`);
} else {
console.log(`[${this.host}:${this.port}] MC服务器恢复可达`);
}
this.lastReachable = this.reachable;
this.notice=true;
}
}
// 创建Minecraft机器人
async createBot(botName) {
if (this.currentBots >= this.maxBots) {
console.log(`[${this.host}:${this.port}] 已达到最大机器人限制: ${this.maxBots}`);
return null;
}
await this.testmc();
if (!this.reachable) {
return null;
}
if(!botName){
botName = this.generateBotName();
}
try {
console.log(`[${this.host}:${this.port}] 创建机器人: ${botName}`);
const bot = mineflayer.createBot({
host: this.host,
port: this.port,
username: botName,
version: this.version,
viewDistance: BOT_CONFIG.viewDistance,
auth: 'offline'
});
this.timeout = setTimeout(() => {
console.error(`[${this.host}:${this.port}] ${botName}连接超时,放弃等待`);
if (typeof bot.end === 'function') {
bot.end();
}
}, BOT_CONFIG.connectTimeout);
// 设置机器人事件处理
this.setupBotEvents(bot, botName);
this.activeBots.set(botName, bot);
this.currentBots++;
// this.failedAttempts = 0;
this.updateStatus();
return bot;
} catch (error) {
console.log(`[${this.host}:${this.port}] 创建机器人 ${botName} 失败:`, error.message);
this.handleBotFailure(botName);
return null;
}
}
// 设置机器人事件
setupBotEvents(bot, botName) {
bot.on('login', () => {
console.log(`[${this.host}:${this.port}] 机器人 ${botName} 登录成功`);
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
this.updateStatus();
});
bot.on('spawn', () => {
console.log(`[${this.host}:${this.port}] 机器人 ${botName} 生成在世界中`);
// 机器人基础行为
this.setupBotBehavior(bot, botName);
});
bot.on('message', (message) => {
const text = message.toString();
console.log(`[${this.host}:${this.port}] ${botName} 收到消息: ${text}`);
});
bot.on('error', (error) => {
console.log(`[${this.host}:${this.port}] 机器人 ${botName} 错误:`, error.message);
// if (this.activeBots.has(botName) && this.failedAttempts <= SERVER_CONFIG.maxFailedAttempts) {
this.handleBotDisconnect(botName);
// } else {
// this.handleBotFailure(botName);
// }
});
bot.on('end', (reason) => {
console.log(`[${this.host}:${this.port}] 机器人 ${botName} 断开连接:`, reason);
console.log(`[${this.host}:${this.port}] ${botName}:失败次数${this.failedAttempts}`);
this.handleBotDisconnect(botName);
});
bot.on('kicked', (reason) => {
console.log(`[${this.host}:${this.port}] 机器人 ${botName} 被踢出:`, reason);
this.handleBotDisconnect(botName);
});
}
// 设置机器人行为
setupBotBehavior(bot, botName) {
// 随机移动
if (BOT_CONFIG.move){
setInterval(() => {
if (bot.entity && Math.random() < 0.3) {
const yaw = Math.random() * Math.PI * 2;
const pitch = Math.random() * Math.PI - Math.PI / 2;
bot.look(yaw, pitch, false);
if (Math.random() < 0.2) {
bot.setControlState('forward', true);
setTimeout(() => {
bot.setControlState('forward', false);
}, 1000);
}
}
}, 5000);
}
// 随机聊天(如果启用)
if (BOT_CONFIG.chat && Math.random() < 0.1) {
setInterval(() => {
const messages = ['Hello!', 'Nice server!', 'What\'s up?', 'Good game!'];
const randomMessage = messages[Math.floor(Math.random() * messages.length)];
bot.chat(randomMessage);
}, 30000 + Math.random() * 60000);
}
}
// 处理机器人断开连接 - 增强版本
handleBotDisconnect(botName) {
if (this.activeBots.has(botName)) {
console.log(`[${this.host}:${this.port}] 从活跃列表中移除机器人: ${botName}`);
this.activeBots.delete(botName);
this.currentBots = Math.max(0, this.currentBots - 1);
this.updateStatus();
// 记录断开连接时间,用于调试
console.log(`[${this.host}:${this.port}] 当前活跃机器人数量: ${this.currentBots}, 目标: ${this.minBots}`);
if(this.failedAttempts > SERVER_CONFIG.maxFailedAttempts){
console.log(`[${this.host}:${this.port}] 机器人 ${botName} 重连次数太多,跳过`);
return;
}
// 延迟重连,避免频繁重连
setTimeout(() => {
this.reconnect(botName);
}, BOT_CONFIG.reconnectDelay);
} else {
console.log(`[${this.host}:${this.port}] 机器人 ${botName} 不在活跃列表中,无需处理`);
}
}
// 处理机器人失败
handleBotFailure(botName) {
this.failedAttempts++;
this.updateStatus();
if (this.failedAttempts >= SERVER_CONFIG.maxFailedAttempts) {
console.log(`[${this.host}:${this.port}] 失败次数过多,${SERVER_CONFIG.resetTimeout / 1000}秒后再次尝试`);
setTimeout(() => {
this.failedAttempts = 0;
this.reconnect(botName);
}, SERVER_CONFIG.resetTimeout);
return;
}
setTimeout(() => {
this.reconnect(botName);
}, BOT_CONFIG.reconnectDelay);
}
// 维护机器人数目 - 增强版本
maintainBots() {
const neededBots = this.minBots - this.currentBots;
// console.log(`[${this.host}:${this.port}] 当前机器人: ${this.currentBots}, 需要: ${neededBots}, 失败次数: ${this.failedAttempts}`);
if (neededBots > 0 && this.failedAttempts < SERVER_CONFIG.maxFailedAttempts) {
if (this.notice) {
console.log(`[${this.host}:${this.port}] 需要启动 ${neededBots} 个机器人`);
this.notice=false;
}
for (let i = 0; i < neededBots; i++) {
setTimeout(() => {
this.createBot();
}, i * 8000); // 每隔8秒启动一个
}
}
// else if (neededBots > 0) {
// console.log(`[${this.host}:${this.port}] 由于失败次数过多,暂停创建新机器人`);
// }
this.updateStatus();
}
reconnect(botName){
if (this.failedAttempts < SERVER_CONFIG.maxFailedAttempts) {
console.log(`[${this.host}:${this.port}] 第${this.failedAttempts}次重连 ${botName} 机器人`);
this.failedAttempts++;
setTimeout(() => {
this.createBot(botName);
}, 5000);
} else{
// 如果还没有设置过 resetTimer,就设置一次
if (!this.resetTimer) {
this.resetTimer = setTimeout(() => {
this.failedAttempts = 0;
this.resetTimer = null; // 清理标记,允许下次再设置
console.log(
`[${this.host}:${this.port}] 失败次数已重置,可以重新尝试创建机器人`
);
}, SERVER_CONFIG.resetTimeout);
}
console.log(`[${this.host}:${this.port}] 由于失败次数过多,暂停创建新机器人`);
}
this.updateStatus();
}
// 更新状态
updateStatus() {
const serverInfo = globalServerStatus.servers.get(`${this.host}:${this.port}`);
if (serverInfo) {
serverInfo.currentBots = this.currentBots;
serverInfo.activeBots = Array.from(this.activeBots.keys());
serverInfo.lastUpdate = Date.now();
serverInfo.status = this.currentBots >= this.minBots ? 'healthy' :
this.failedAttempts >= SERVER_CONFIG.maxFailedAttempts ? 'failed' : 'degraded';
this.status = serverInfo.status;
}
}
// 启动监控
startMonitoring() {
this.maintainBots();
this.monitoringInterval = setInterval(() => {
this.maintainBots();
}, SERVER_CONFIG.statusCheckInterval);
}
// 停止所有机器人
stopAllBots() {
this.activeBots.forEach((bot, botName) => {
try {
bot.quit();
console.log(`[${this.host}:${this.port}] 停止机器人: ${botName}`);
} catch (error) {
console.log(`[${this.host}:${this.port}] 停止机器人 ${botName} 失败:`, error.message);
}
});
this.activeBots.clear();
this.currentBots = 0;
this.updateStatus();
if (this.monitoringInterval) {
clearInterval(this.monitoringInterval);
this.monitoringInterval = null;
}
}
// 获取状态信息
getStatus() {
return {
host: this.host,
port: this.port,
version: this.version,
minBots: this.minBots,
maxBots: this.maxBots,
currentBots: this.currentBots,
activeBots: Array.from(this.activeBots.keys()),
failedAttempts: this.failedAttempts,
status: this.status,
lastUpdate: this.lastUpdate
};
}
}
// 系统监控
class SystemMonitor {
constructor() {
this.memoryUsage = '未知';
this.uptime = 0;
this.monitoringInterval = null;
}
async getMemoryUsage() {
return new Promise((resolve) => {
const used = process.memoryUsage();
this.memoryUsage = `RSS: ${Math.round(used.rss / 1024 / 1024)}MB, Heap: ${Math.round(used.heapUsed / 1024 / 1024)}MB`;
resolve(this.memoryUsage);
});
}
startMonitoring() {
this.getMemoryUsage();
this.monitoringInterval = setInterval(async () => {
await this.getMemoryUsage();
this.uptime = process.uptime();
}, BOT_CONFIG.healthCheckInterval);
}
stopMonitoring() {
if (this.monitoringInterval) {
clearInterval(this.monitoringInterval);
}
}
getSystemInfo() {
return {
memoryUsage: this.memoryUsage,
uptime: this.uptime,
nodeVersion: process.version,
platform: process.platform
};
}
}
// Web状态服务器
class StatusServer {
constructor(port = PORT) {
this.port = port;
this.app = express();
this.app.use(express.json({ limit: '10mb' }));
this.app.use(express.text({ type: 'text/plain' }));
this.app.use(express.urlencoded({ extended: true }));
this.app.use(cookieParser());
this.server = null;
this.setupRoutes();
}
setupRoutes() {
this.app.get('/', (req, res) => {
const serversStatus = Array.from(globalServerStatus.servers.values());
const systemInfo = systemMonitor.getSystemInfo();
const { version } = require('./package.json');
const html = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Minecraft 机器人监控系统</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 30px;
color: white;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
}
.dashboard {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 20px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.dashboard {
grid-template-columns: 1fr;
}
}
.system-card {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
backdrop-filter: blur(10px);
}
.system-card h3 {
color: #4a5568;
margin-bottom: 20px;
font-size: 1.3rem;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 10px;
}
.stats-grid {
display: grid;
gap: 15px;
}
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: #f7fafc;
border-radius: 10px;
border-left: 4px solid #4299e1;
}
.stat-label {
font-weight: 600;
color: #4a5568;
}
.stat-value {
font-weight: 700;
color: #2d3748;
}
.servers-section {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.servers-section h3 {
color: #4a5568;
margin-bottom: 20px;
font-size: 1.3rem;
}
.server-grid {
display: grid;
gap: 20px;
}
.server-card {
border-radius: 12px;
padding: 20px;
border: 1px solid #e2e8f0;
transition: all 0.3s ease;
background: white;
}
.server-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
}
.server-card.healthy {
border-left: 4px solid #48bb78;
}
.server-card.degraded {
border-left: 4px solid #ed8936;
}
.server-card.failed {
border-left: 4px solid #f56565;
}
.server-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.server-title {
font-size: 1.2rem;
font-weight: 600;
color: #2d3748;
}
.status-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
}
.status-healthy {
background: #c6f6d5;
color: #276749;
}
.status-degraded {
background: #fed7d7;
color: #c53030;
}
.status-failed {
background: #fed7d7;
color: #c53030;
}
.server-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
.info-item {
text-align: center;
padding: 10px;
background: #f7fafc;
border-radius: 8px;
}
.info-label {
font-size: 0.85rem;
color: #718096;
margin-bottom: 5px;
}
.info-value {
font-size: 1.1rem;
font-weight: 700;
color: #2d3748;
}
.bots-section {
margin-top: 15px;
}
.bots-title {
font-weight: 600;
margin-bottom: 10px;
color: #4a5568;
}
.bots-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.bot-tag {
background: #4299e1;
color: white;
padding: 4px 12px;
border-radius: 15px;
font-size: 0.8rem;
font-weight: 500;
}
.no-bots {
color: #a0aec0;
font-style: italic;
text-align: center;
padding: 10px;
}
.last-update {
text-align: right;
font-size: 0.8rem;
color: #a0aec0;
margin-top: 10px;
}
.refresh-info {
text-align: center;
color: white;
margin-top: 20px;
opacity: 0.8;
font-size: 0.9rem;
}
.refresh-info a {
color: #ffeb3b;
font-weight: bold;
text-decoration: none;
margin-left: 8px;
}
.refresh-info a:hover {
text-decoration: underline;
color: #fff176;
}
.refresh-info button {
margin-left: 8px;
background-color: #ffeb3b;
color: #6a0dad;
font-weight: bold;
border: none;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
}
.refresh-info button:hover {
background-color: #fff176;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e2e8f0;
border-radius: 4px;
overflow: hidden;
margin-top: 5px;
}
.progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.progress-healthy {
background: linear-gradient(90deg, #48bb78, #68d391);
}
.progress-degraded {
background: linear-gradient(90deg, #ed8936, #f6ad55);
}
.progress-failed {
background: linear-gradient(90deg, #f56565, #fc8181);
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎮 Minecraft 机器人监控系统</h1>
<p>实时监控服务器状态和机器人连接</p>
</div>
<div class="dashboard">
<div class="system-card">
<h3>📊 系统概览</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">运行时间</span>
<span class="stat-value">${Math.floor(systemInfo.uptime / 60)} 分钟</span>
</div>
<div class="stat-item">
<span class="stat-label">内存使用</span>
<span class="stat-value">${systemInfo.memoryUsage.split(',')[0].replace('RSS: ', '')}</span>
</div>
<div class="stat-item">
<span class="stat-label">Node.js 版本</span>
<span class="stat-value">${systemInfo.nodeVersion}</span>
</div>
<div class="stat-item">
<span class="stat-label">监控服务器</span>
<span class="stat-value">${serversStatus.length} 个</span>
</div>
</div>
</div>
<div class="servers-section">
<h3>🖥️ 服务器状态</h3>
<div class="server-grid">
${serversStatus.map(server => {
const progress = (server.currentBots / server.maxBots) * 100;
const statusClass = server.status === 'healthy' ? 'status-healthy' :
server.status === 'degraded' ? 'status-degraded' : 'status-failed';
const progressClass = server.status === 'healthy' ? 'progress-healthy' :
server.status === 'degraded' ? 'progress-degraded' : 'progress-failed';
const statusText = server.status === 'healthy' ? '正常' :
server.status === 'degraded' ? '降级' : '故障';
return `
<div class="server-card ${server.status}">
<div class="server-header">
<div class="server-title">${server.host}:${server.port}</div>
<div class="status-badge ${statusClass}">${statusText}</div>
</div>
<div class="server-info">
<div class="info-item">
<div class="info-label">机器人数量</div>
<div class="info-value">${server.currentBots} / ${server.maxBots}</div>
<div class="progress-bar">
<div class="progress-fill ${progressClass}" style="width: ${progress}%"></div>
</div>
</div>
<div class="info-item">
<div class="info-label">最小要求</div>
<div class="info-value">${server.minBots}</div>
</div>
<div class="info-item">
<div class="info-label">连接状态</div>
<div class="info-value" style="color: ${
server.status === 'healthy' ? '#48bb78' :
server.status === 'degraded' ? '#ed8936' : '#f56565'
}">${server.status}</div>
</div>
</div>
<div class="bots-section">
<div class="bots-title">🤖 活跃机器人</div>
<div class="bots-list">
${server.activeBots.length > 0 ?
server.activeBots.map(bot => `
<div class="bot-tag">${bot}</div>
`).join('') :
'<div class="no-bots">暂无活跃机器人</div>'
}
</div>
</div>
<div class="last-update">
最后更新: ${new Date(server.lastUpdate).toLocaleString('zh-CN')}
</div>
</div>
`;
}).join('')}
</div>
</div>
</div>
<div class="refresh-info">
页面每30秒自动刷新 • Minecraft 机器人监控系统 v${version}
· <a href="/hotupdateserver/dashboard" rel="noopener noreferrer">更新MC机器人配置</a>
· <a href="https://www.npmjs.com/package/@baipiaodajun/mcbots" target="_blank" rel="noopener noreferrer">NPM主頁</a>
· <a href="https://gbjs.hospedagem-gratis.com/mcbot.html" target="_blank" rel="noopener noreferrer">SERVER_JSON生成器</a>
· <a href="https://gbjs.hospedagem-gratis.com/mcbot2.html" target="_blank" rel="noopener noreferrer">SERVER_JSON修改器</a>
· <button id="copy-config">复制配置</button>
</div>
<!-- 隐藏元素存放 SERVERS_JSON -->
<div id="servers-json" style="display:none;">
${process.env.SERVERS_JSON}
</div>
</div>
<script>
document.getElementById('copy-config').addEventListener('click', () => {
// 从隐藏元素中获取内容
const serversJson = document.getElementById('servers-json').textContent.trim();
if (serversJson) {
navigator.clipboard.writeText(serversJson)
.then(() => alert('配置已复制到剪贴板'))
.catch(err => alert('复制失败: ' + err));
} else {
alert('未找到配置内容');
}
});
// 30秒自动刷新
setTimeout(() => {
location.reload();
}, 30000);
// 添加一些交互动画
document.addEventListener('DOMContentLoaded', function() {
const cards = document.querySelectorAll('.server-card');
cards.forEach(card => {
card.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-5px)';
});
card.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
});
});
});
</script>
</body>
</html>`;
res.send(html);
});
this.app.get('/api/status', (req, res) => {
const serversStatus = Array.from(globalServerStatus.servers.values());
const systemInfo = systemMonitor.getSystemInfo();
res.json({
system: systemInfo,
servers: serversStatus,
timestamp: Date.now()
});
});
// ==================== 1. 密钥登录页(修复:登录成功后只存密钥到 cookie,前端用它发 header)================
this.app.get('/hotupdateserver', (req, res) => {
const key = req.query.key || req.headers['x-auth-key'] || '';
if (key && hotUpdateAuth.verify(key)) {
// 直接跳转并种 cookie(仅供前端 JS 读取)
res.cookie('hotupdate_token', key, { maxAge: 3600000, httpOnly: false, path: '/', sameSite: 'lax' });
return res.redirect('/hotupdateserver/dashboard');
}
res.send(`<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>权限认证</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #667eea, #764ba2);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 20px;
}
.box {
max-width: 520px;
width: 95%;
background: white;
border-radius: 20px;
padding: 50px 45px;
box-shadow: 0 20px 50px rgba(0,0,0,0.35);
text-align: center;
backdrop-filter: blur(10px);
}
h1 {
font-size: 2.5rem;
margin-bottom: 12px;
background: linear-gradient(90deg, #4299e1, #805ad5);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
p { color: #718096; margin-bottom: 38px; font-size: 1.15rem; }
/* 核心容器:固定宽度,完美居中 */
.input-group {
width: 100%;
max-width: 420px;
margin: 0 auto 26px auto;
}
/* 关键:强制 input 和 button 完全一致,包括边框和内边距 */
.input-group input,
.input-group button {
box-sizing: border-box !important; /* 包含 border + padding */
width: 100% !important;
padding: 18px 20px;
font-size: 1.18rem;
border-radius: 14px;
display: block;
margin: 0;
}
.input-group input {
border: 3px solid #e2e8f0;
background: #f8fafc;
transition: all 0.3s;
}
.input-group input:focus {
outline: none;
border-color: #4299e1;
background: white;
box-shadow: 0 0 0 5px rgba(66,153,225,.25);
}
.input-group button {
background: #4299e1;
color: white;
border: 3px solid #4299e1; /* 加边框,让厚度一致! */
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 6px 20px rgba(66,153,225,.4);
}
.input-group button:hover {
background: #3182ce;
border-color: #3182ce;
transform: translateY(-2px);
}
.error {
color: #f56565;
margin-top: 15px;
font-weight: 600;
display: none;
}
</style>
</head>
<body>
<div class="box">
<h1>MC机器人管理中心</h1>
<p>请输入管理密钥</p>
<div class="input-group">
<input type="password" id="k" placeholder="输入密钥后按回车" autofocus>
</div>
<div class="input-group">
<button onclick="login()">立即验证</button>
</div>
<div class="error" id="msg"></div>
</div>
<script>
async function login() {
const key = document.getElementById('k').value.trim();
if (!key) return;
try {
const r = await fetch('/api/verify-hotupdate-key', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-auth-key': key // 直接发 header
}
});
const d = await r.json();
if (d.success) {
// 种 cookie 供后续 dashboard 使用
document.cookie = 'hotupdate_token=' + encodeURIComponent(key) + ';path=/;max-age=3600';
location.href = '/hotupdateserver/dashboard';
} else {
document.getElementById('msg').textContent = '密钥错误';
document.getElementById('msg').style.display = 'block';
}
} catch(e) {
document.getElementById('msg').textContent = '网络错误';
document.getElementById('msg').style.display = 'block';
}
}
document.getElementById('k').addEventListener('keyup', e => e.key==='Enter' && login());
</script>
</body></html>`);
});
// ==================== 2. 验证接口(只认 x-auth-key)================
this.app.post('/api/verify-hotupdate-key', (req, res) => {
const key = req.headers['x-auth-key'] || '';
if (hotUpdateAuth.verify(key)) {
res.json({ success: true });
} else {
res.status(401).json({ success: false, message: 'Invalid key' });
}
});
// ==================== 3. 仪表盘(从 cookie 读取密钥 → 发 x-auth-key)================
this.app.get('/hotupdateserver/dashboard', (req, res) => {
// 认证逻辑
const key = req.headers['x-auth-key'] || '';
const token = req.cookies?.hotupdate_token || '';
if (!hotUpdateAuth.verify(key) && !hotUpdateAuth.verify(token)) {
return res.redirect('/hotupdateserver');
}
const currentConfig = JSON.stringify(global.SERVERS || SERVERS, null, 2);
const { version } = require('./package.json');
res.send(`<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>热更新配置 - Minecraft 机器人系统</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
color: #333;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 35px;
box-shadow: 0 20px 50px rgba(0,0,0,0.3);
backdrop-filter: blur(12px);
}
h1 {
text-align: center;
font-size: 2.4rem;
margin-bottom: 12px;
background: linear-gradient(90deg, #4299e1, #805ad5);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
p.subtitle {
text-align: center;
color: #718096;
margin-bottom: 30px;
font-size: 1.1rem;
}
textarea {
width: 100%;
height: 520px;
padding: 18px;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 15px;
line-height: 1.6;
border: 3px solid #e2e8f0;
border-radius: 14px;
resize: vertical;
background: #f8fafc;
transition: all 0.3s;
}
textarea:focus {
outline: none;
border-color: #4299e1;
box-shadow: 0 0 0 4px rgba(66, 153, 225, 0.2);
background: white;
}
.actions {
text-align: center;
margin: 30px 0 20px;
}
.btn {
padding: 16px 40px;
margin: 0 12px;
font-size: 1.2rem;
font-weight: bold;
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
}
.btn-apply {
background: #48bb78;
color: white;
}
.btn-apply:hover { background: #38a169; transform: translateY(-3px); }
.btn-logout {
background: #f56565;
color: white;
}
.btn-logout:hover { background: #e53e3e; transform: translateY(-3px); }
.msg {
margin-top: 20px;
padding: 16px;
border-radius: 12px;
text-align: center;
font-weight: 600;
font-size: 1.1rem;
display: none;
}
.success { background: #c6f6d5; color: #276749; }
.error { background: #fed7d7; color: #c53030; }
.footer {
text-align: center;
margin-top: 40px;
color: #a0aec0;
font-size: 0.95rem;
}
.footer a { color: #4299e1; text-decoration: none; }
.footer a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<h1>热更新服务器配置</h1>
<p class="subtitle">直接修改下方 JSON 配置,点击“应用并热更新”立即生效</p>
<textarea id="config" spellcheck="false">${currentConfig.replace(/</g, '<')}</textarea>
<div class="actions">
<button class="btn btn-apply" id="apply">应用并热更新</button>
<button class="btn btn-logout" id="logout">退出登录</button>
</div>
<div id="msg" class="msg"></div>
<div class="footer">
Minecraft 机器人监控系统 v${version} •
<a href="/" target="_blank">返回监控主页</a>
</div>
</div>
<script>
function getToken() {
const m = document.cookie.match(/hotupdate_token=([^;]+)/);
return m ? decodeURIComponent(m[1]) : '';
}
document.getElementById('apply').onclick = async () => {
const btn = document.getElementById('apply');
const msg = document.getElementById('msg');
const text = document.getElementById('config').value;
btn.disabled = true;
msg.style.display = 'none';
let config;
try {
config = JSON.parse(text);
if (!Array.isArray(config)) throw new Error('配置必须是一个数组');
} catch (e) {
msg.textContent = 'JSON 解析失败:' + e.message;
msg.className = 'msg error';
msg.style.display = 'block';
btn.disabled = false;
return;
}
try {
const rawText = document.getElementById('config').value.trim();
const r = await fetch('/api/apply-new-servers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-auth-key': getToken()
},
body: rawText
});
const d = await r.json();
msg.textContent = d.success
? '配置已成功应用!所有机器人已热更新完成'
: '应用失败:' + (d.message || '未知错误');
msg.className = 'msg ' + (d.success ? 'success' : 'error');
} catch (e) {
msg.textContent = '提交失败:' + e.message;
msg.className = 'msg error';
}
msg.style.display = 'block';
btn.disabled = false;
};
document.getElementById('logout').onclick = () => {
document.cookie = 'hotupdate_token=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';
location.href = '/hotupdateserver';
};
</script>
</body>
</html>`);
});
// ==================== 4. 应用新配置接口(只认 x-auth-key)================
this.app.post('/api/apply-new-servers', async (req, res) => {
const key = req.headers['x-auth-key'] || '';
if (!hotUpdateAuth.verify(key)) {
return res.status(403).json({ success: false, message: '认证失败' });
}
let rawJsonText = '';
// 1. text/plain 直接发原始 JSON 文本(最推荐)
if (typeof req.body === 'string') {
rawJsonText = req.body.trim();
}
// 2. application/json + { servers: [...] } 对象
else if (req.body && Array.isArray(req.body.servers)) {
rawJsonText = JSON.stringify(req.body.servers, null, 2);
}
// 3. application/json + { config: "..." } 字符串
else if (req.body && typeof req.body.config === 'string') {
rawJsonText = req.body.config.trim();
}
// 4. 直接就是数组(极少见但也支持)
else if (Array.isArray(req.body)) {
rawJsonText = JSON.stringify(req.body, null, 2);
}
else {
return res.status(400).json({
success: false,
message: '不支持的提交格式(请发送 JSON 文本或 {servers: [...]})'
});
}
if (rawJsonText === '' || rawJsonText === '[]') {
return res.status(400).json({ success: false, message: '配置不能为空' });
}
// 解析并验证
let servers;
try {
servers = JSON.parse(rawJsonText);
if (!Array.isArray(servers)) throw new Error('根节点必须是数组');
} catch (err) {
return res.status(400).json({
success: false,
message: 'JSON 格式错误:' + err.message
});
}
try {
// 更新内存
SERVERS.length = 0;
SERVERS.push(...servers);
// 关键:同步更新环境变量原始字符串(重启后依然生效)
process.env.SERVERS_JSON = rawJsonText;
// 持久化到文件(双保险)
require('fs').writeFileSync('./servers.json', rawJsonText + '\n');
// 执行热更新
await hotUpdateServers();
console.log(`热更新成功!已加载 ${servers.length} 台服务器配置`);
res.json({ success: true, message: '热更新成功,配置已永久保存' });
} catch (err) {
console.error('热更新执行失败:', err);
res.status(500).json({ success: false, message: '服务器内部错误' });
}
});
}
start() {
this.server = this.app.listen(this.port, () => {
console.log(`状态监控页面: http://localhost:${this.port}`);
});
}
stop() {
if (this.server) {
this.server.close();
}
}
}
// 全局实例
const systemMonitor = new SystemMonitor();
const statusServer = new StatusServer();
let botManagers = [];
// 主初始化
async function initialize() {
console.log('初始化Minecraft机器人管理系统...');
systemMonitor.startMonitoring();
// 创建机器人管理器
botManagers = SERVERS.map(server =>
new MinecraftBotManager(
server.host,
server.port,
server.minBots,
server.maxBots,
server.version
)
);
// 启动所有管理器
botManagers.forEach(manager => {
manager.startMonitoring();
console.log(`启动服务器: ${manager.host}:${manager.port} (${manager.minBots}-${manager.maxBots} 机器人)`);
});
statusServer.start();
console.log('Minecraft机器人管理系统初始化完成');
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
}
async function hotUpdateServers() {
const newServers = SERVERS;
const newServerMap = new Map();
newServers.forEach(srv => {
const key = `${srv.host}:${srv.port}`;
newServerMap.set(key, { ...srv });
});
const currentManagers = new Map();
botManagers.forEach(manager => {
const key = `${manager.host}:${manager.port}`;
currentManagers.set(key, manager);
});
const toRemove = [];
const toAdd