UNPKG

@baipiaodajun/mcbots

Version:

Minecraft bot and status dashboard for multi-server management

1,560 lines (1,396 loc) 56 kB
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, '&lt;')}</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