@baipiaodajun/mcbots 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +27 -0
  2. package/package.json +12 -11
  3. package/server.js +653 -10
package/README.md CHANGED
@@ -56,5 +56,32 @@ docker run -d \
56
56
  -p 3000:3000 \
57
57
  mingli2038/mcbot:latest
58
58
  ```
59
+
60
+ ### 新版本说明
61
+
62
+ - 新版本引入了 **热更新功能:`SERVER_JSON`**
63
+ - 目前该功能处于 **实验状态**,可能会导致一些 **意料之外的问题**
64
+ - 如果需要稳定使用,推荐继续使用 **1.1 版本**
65
+
66
+ ---
67
+
68
+ ### 页面更新
69
+
70
+ 在页面底部新增了修复入口:
71
+ **【更新 MC 机器人配置】**
72
+
73
+ 该页面涉及 **服务器配置更新**,因此所有操作均已加上认证。
74
+
75
+ ---
76
+
77
+ ### 密钥设置
78
+
79
+ - 默认密钥可从 [Telegram频道](https://t.me/boost/wanjulaji) 获取
80
+ - 也可以通过环境变量自行设置:
81
+ - `HOTUPDATE_SECRET`:用于设置密钥
82
+ - `HOTUPDATE_SALT`:用于加盐,防止彩虹表破解
83
+
84
+ ---
85
+
59
86
  ## 来源
60
87
  这个东西不是我的原创,思路来自Tweek白嫖群的[这种事可以花点钱]用户给的镜像[ghcr.io/oprmg/mcbot:latest](https://ghcr.io/oprmg/mcbot:latest),我通过AI重构其实现并加入自己的想法从而重新发布出来。
package/package.json CHANGED
@@ -1,25 +1,26 @@
1
1
  {
2
2
  "name": "@baipiaodajun/mcbots",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Minecraft bot and status dashboard for multi-server management",
5
5
  "main": "server.js",
6
6
  "scripts": {
7
- "start": "node server.js"
7
+ "start": "node server.js"
8
8
  },
9
9
  "keywords": [
10
- "minecraft",
11
- "bot",
12
- "mineflayer"
10
+ "minecraft",
11
+ "bot",
12
+ "mineflayer"
13
13
  ],
14
14
  "author": "mingli2038",
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
- "mineflayer": "^4.33.0",
18
- "node-fetch": "^2.7.0",
19
- "express": "^4.18.2"
17
+ "cookie-parser": "^1.4.7",
18
+ "express": "^4.18.2",
19
+ "mineflayer": "^4.33.0",
20
+ "node-fetch": "^2.7.0"
20
21
  },
21
22
  "files": [
22
- "server.js",
23
- "README.md"
23
+ "server.js",
24
+ "README.md"
24
25
  ]
25
- }
26
+ }
package/server.js CHANGED
@@ -1,6 +1,12 @@
1
1
  const mineflayer = require('mineflayer');
2
2
  const express = require('express');
3
3
  const net = require('net');
4
+
5
+ const crypto = require('crypto');
6
+ const cookieParser = require('cookie-parser');
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+
4
10
  const PORT = process.env.SERVER_PORT || process.env.PORT || 3000 ;
5
11
  const CHAT = process.env.CHAT || false ;
6
12
  const MOVE = process.env.MOVE || false ;
@@ -15,22 +21,63 @@ const SERVERS = [
15
21
  }
16
22
  ];
17
23
 
18
- // 从环境变量读取服务器配置
19
- if (process && process.env && process.env.SERVERS_JSON) {
24
+ const CONFIG_FILE = path.resolve(process.cwd(), './servers.json'); // 根据你的实际路径调整
25
+
26
+ let loaded = false;
27
+
28
+ // 1. 优先尝试从 ./servers.json 读取
29
+ if (fs.existsSync(CONFIG_FILE)) {
30
+ try {
31
+ const fileContent = fs.readFileSync(CONFIG_FILE, 'utf-8').trim();
32
+ if (fileContent) {
33
+ const fileConfig = JSON.parse(fileContent);
34
+ if (Array.isArray(fileConfig)) {
35
+ SERVERS.length = 0;
36
+ SERVERS.push(...fileConfig);
37
+ console.log(`从 ${CONFIG_FILE} 加载服务器配置成功,数量: ${SERVERS.length} 项`);
38
+ loaded = true;
39
+ } else {
40
+ console.warn('配置文件内容不是数组,已忽略');
41
+ }
42
+ }
43
+ } catch (err) {
44
+ console.error(`读取或解析 ${CONFIG_FILE} 失败,将回落到环境变量`);
45
+ console.error('错误详情:', err.message);
46
+ }
47
+ }
48
+
49
+ // 2. 文件不存在或解析失败 → 回退到环境变量 SERVERS_JSON
50
+ if (!loaded && process.env.SERVERS_JSON) {
20
51
  try {
21
52
  const envConfig = JSON.parse(process.env.SERVERS_JSON);
22
- SERVERS.length = 0;
23
- SERVERS.push(...envConfig);
24
- console.log('从环境变量加载服务器配置,数量:', SERVERS.length, '项');
53
+ if (Array.isArray(envConfig)) {
54
+ SERVERS.length = 0;
55
+ SERVERS.push(...envConfig);
56
+ console.log(`从环境变量 SERVERS_JSON 加载服务器配置,数量: ${SERVERS.length} 项`);
57
+ loaded = true;
58
+ } else {
59
+ throw new Error('环境变量 SERVERS_JSON 内容不是数组');
60
+ }
25
61
  } catch (error) {
26
- console.error('❌ [错误] 无法解析环境变量 SERVERS_JSON');
62
+ console.error('无法解析环境变量 SERVERS_JSON');
27
63
  console.error('原因:', error.message);
28
- console.error('原始内容:\n', process.env.SERVERS_JSON);
29
- console.error('请检查 JSON 格式是否正确,例如引号、逗号是否缺失');
30
- console.error('即将退出...');
64
+ console.error('原始内容预览:');
65
+ console.error(process.env.SERVERS_JSON?.slice(0, 500) + (process.env.SERVERS_JSON?.length > 500 ? '...' : ''));
66
+ console.error('请检查 JSON 格式是否正确(双引号、逗号、括号等)');
67
+ console.error('程序即将退出...');
31
68
  process.exit(1);
32
69
  }
33
70
  }
71
+
72
+ // 3. 都没有 → 报错退出(防止空配置启动)
73
+ if (!loaded) {
74
+ console.error('未找到任何有效的服务器配置!');
75
+ console.error(`请提供以下任意一种方式:`);
76
+ console.error(` 1. 在项目根目录放置 ./config/servers.json 文件`);
77
+ console.error(` 2. 设置环境变量 SERVERS_JSON='[{"host":"...", "port":...}, ...]'`);
78
+ console.error('程序即将退出...');
79
+ process.exit(1);
80
+ }
34
81
  /**
35
82
  * 测试 IP 和端口是否可达
36
83
  * @param {string} host - IP 或域名
@@ -100,6 +147,56 @@ function generateUsername() {
100
147
  const animal = animals[Math.floor(Math.random() * animals.length)];
101
148
  return `${adjective}${animal}`;
102
149
  }
150
+
151
+ class HotUpdateAuth {
152
+ constructor() {
153
+ this.hashedSecrets = new Set();
154
+ this.salt = process.env.HOTUPDATE_SALT || 'mc-bot-manager-2025-salt';
155
+ if (process.env.HOTUPDATE_SECRET) {
156
+ const hash = crypto.createHash('sha256')
157
+ .update(process.env.HOTUPDATE_SECRET)
158
+ .digest('hex');
159
+ this.hashedSecrets.add(hash);
160
+ console.log('[热更新认证] 已从明文环境变量加载并哈希密钥');
161
+ }
162
+ else{
163
+ this.hashedSecrets = new Set([
164
+ // 用下面命令生成(不要直接把明文写在这里):
165
+ // node -e "console.log(require('crypto').createHash('sha256').update('你的超级强密钥2025').digest('hex'))"
166
+ '1cc1e7dd16a37dda1837bb44fc559024fb89961d94548fe9db1700721c489366',
167
+ // 'a1b2c3d4...' // ← 可以再加一条旧密钥,方便轮换
168
+ ]);
169
+ }
170
+ }
171
+
172
+ // 带 salt 的哈希(更防彩虹表)
173
+ _hashWithSalt(secret) {
174
+ return crypto.createHash('sha256')
175
+ .update(secret + this.salt)
176
+ .digest('hex');
177
+ }
178
+
179
+ // 主验证函数
180
+ verify(secret) {
181
+ if (!secret) return false;
182
+
183
+ const plainHash = crypto.createHash('sha256').update(secret).digest('hex');
184
+ const saltedHash = this._hashWithSalt(secret);
185
+
186
+ // 支持两种存储方式(兼容旧项目)
187
+ return [...this.hashedSecrets].some(h => h === plainHash || h === saltedHash);
188
+ }
189
+
190
+ // 方便你在控制台生成新哈希(部署时运行一次即可)
191
+ static generateHash(plain) {
192
+ const salt = process.env.HOTUPDATE_SALT || 'mc-bot-manager-2025-salt';
193
+ const hash = crypto.createHash('sha256').update(plain + salt).digest('hex');
194
+ console.log('明文密钥 :', plain);
195
+ console.log('加盐 SHA256:', hash);
196
+ return hash;
197
+ }
198
+ }
199
+ const hotUpdateAuth =new HotUpdateAuth();
103
200
  // Minecraft机器人管理器
104
201
  class MinecraftBotManager {
105
202
  constructor(host, port, minBots, maxBots, version) {
@@ -134,7 +231,54 @@ class MinecraftBotManager {
134
231
  status: 'initializing'
135
232
  });
136
233
  }
137
-
234
+ dispose() {
235
+ const key = `${this.host}:${this.port}`;
236
+ console.log(`[${key}] 正在释放 BotManager 所有资源...`);
237
+
238
+ // 1. 停止所有正在运行的机器人(最重要!)
239
+ this.activeBots.forEach((bot, botName) => {
240
+ try {
241
+ if (bot && typeof bot.end === 'function') {
242
+ bot.removeAllListeners(); // 关键!移除所有事件监听,防止回调残留
243
+ bot.end(); // 主动断开连接
244
+ }
245
+ console.log(`[${key}] 已断开机器人: ${botName}`);
246
+ } catch (err) {
247
+ console.warn(`[${key}] 断开 ${botName} 时出错:`, err.message);
248
+ }
249
+ });
250
+ this.activeBots.clear();
251
+ this.currentBots = 0;
252
+
253
+ // 2. 清理所有定时器(防止残留 setInterval/setTimeout)
254
+ if (this.monitoringInterval) {
255
+ clearInterval(this.monitoringInterval);
256
+ this.monitoringInterval = null;
257
+ console.log(`[${key}] 已清理 monitoringInterval`);
258
+ }
259
+
260
+ if (this.timeout) {
261
+ clearTimeout(this.timeout);
262
+ this.timeout = null;
263
+ }
264
+
265
+ if (this.resetTimer) {
266
+ clearTimeout(this.resetTimer);
267
+ this.resetTimer = null;
268
+ console.log(`[${key}] 已清理 resetTimer`);
269
+ }
270
+ // 4. 从全局状态移除(前端立刻看不到)
271
+ globalServerStatus.servers.delete(key);
272
+ console.log(`[${key}] 已从 globalServerStatus 中移除`);
273
+
274
+ // 5. 清理自身引用,帮助 GC
275
+ this.botNames = null;
276
+ this.activeBots = null;
277
+ this.status = 'disposed';
278
+ this.reachable = false;
279
+
280
+ console.log(`[${key}] BotManager 资源释放完成`);
281
+ }
138
282
  // 生成随机机器人名称
139
283
  generateBotName() {
140
284
  const baseName = this.botNames[Math.floor(Math.random() * this.botNames.length)];
@@ -463,6 +607,10 @@ class StatusServer {
463
607
  constructor(port = PORT) {
464
608
  this.port = port;
465
609
  this.app = express();
610
+ this.app.use(express.json({ limit: '10mb' }));
611
+ this.app.use(express.text({ type: 'text/plain' }));
612
+ this.app.use(express.urlencoded({ extended: true }));
613
+ this.app.use(cookieParser());
466
614
  this.server = null;
467
615
  this.setupRoutes();
468
616
  }
@@ -869,6 +1017,7 @@ class StatusServer {
869
1017
 
870
1018
  <div class="refresh-info">
871
1019
  页面每30秒自动刷新 • Minecraft 机器人监控系统 v${version}
1020
+ · <a href="/hotupdateserver/dashboard" rel="noopener noreferrer">更新MC机器人配置</a>
872
1021
  · <a href="https://www.npmjs.com/package/@baipiaodajun/mcbots" target="_blank" rel="noopener noreferrer">NPM主頁</a>
873
1022
  · <a href="https://gbjs.hospedagem-gratis.com/mcbot.html" target="_blank" rel="noopener noreferrer">SERVER_JSON生成器</a>
874
1023
  · <a href="https://gbjs.hospedagem-gratis.com/mcbot2.html" target="_blank" rel="noopener noreferrer">SERVER_JSON修改器</a>
@@ -928,6 +1077,419 @@ class StatusServer {
928
1077
  timestamp: Date.now()
929
1078
  });
930
1079
  });
1080
+ // ==================== 1. 密钥登录页(修复:登录成功后只存密钥到 cookie,前端用它发 header)================
1081
+ this.app.get('/hotupdateserver', (req, res) => {
1082
+ const key = req.query.key || req.headers['x-auth-key'] || '';
1083
+ if (key && hotUpdateAuth.verify(key)) {
1084
+ // 直接跳转并种 cookie(仅供前端 JS 读取)
1085
+ res.cookie('hotupdate_token', key, { maxAge: 3600000, httpOnly: false, path: '/', sameSite: 'lax' });
1086
+ return res.redirect('/hotupdateserver/dashboard');
1087
+ }
1088
+
1089
+ res.send(`<!DOCTYPE html>
1090
+ <html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
1091
+ <title>权限认证</title>
1092
+ <style>
1093
+ body {
1094
+ font-family: 'Segoe UI', sans-serif;
1095
+ background: linear-gradient(135deg, #667eea, #764ba2);
1096
+ min-height: 100vh;
1097
+ display: flex;
1098
+ align-items: center;
1099
+ justify-content: center;
1100
+ margin: 0;
1101
+ padding: 20px;
1102
+ }
1103
+ .box {
1104
+ max-width: 520px;
1105
+ width: 95%;
1106
+ background: white;
1107
+ border-radius: 20px;
1108
+ padding: 50px 45px;
1109
+ box-shadow: 0 20px 50px rgba(0,0,0,0.35);
1110
+ text-align: center;
1111
+ backdrop-filter: blur(10px);
1112
+ }
1113
+ h1 {
1114
+ font-size: 2.5rem;
1115
+ margin-bottom: 12px;
1116
+ background: linear-gradient(90deg, #4299e1, #805ad5);
1117
+ -webkit-background-clip: text;
1118
+ -webkit-text-fill-color: transparent;
1119
+ }
1120
+ p { color: #718096; margin-bottom: 38px; font-size: 1.15rem; }
1121
+
1122
+ /* 核心容器:固定宽度,完美居中 */
1123
+ .input-group {
1124
+ width: 100%;
1125
+ max-width: 420px;
1126
+ margin: 0 auto 26px auto;
1127
+ }
1128
+
1129
+ /* 关键:强制 input 和 button 完全一致,包括边框和内边距 */
1130
+ .input-group input,
1131
+ .input-group button {
1132
+ box-sizing: border-box !important; /* 包含 border + padding */
1133
+ width: 100% !important;
1134
+ padding: 18px 20px;
1135
+ font-size: 1.18rem;
1136
+ border-radius: 14px;
1137
+ display: block;
1138
+ margin: 0;
1139
+ }
1140
+
1141
+ .input-group input {
1142
+ border: 3px solid #e2e8f0;
1143
+ background: #f8fafc;
1144
+ transition: all 0.3s;
1145
+ }
1146
+ .input-group input:focus {
1147
+ outline: none;
1148
+ border-color: #4299e1;
1149
+ background: white;
1150
+ box-shadow: 0 0 0 5px rgba(66,153,225,.25);
1151
+ }
1152
+
1153
+ .input-group button {
1154
+ background: #4299e1;
1155
+ color: white;
1156
+ border: 3px solid #4299e1; /* 加边框,让厚度一致! */
1157
+ font-weight: bold;
1158
+ cursor: pointer;
1159
+ transition: all 0.3s;
1160
+ box-shadow: 0 6px 20px rgba(66,153,225,.4);
1161
+ }
1162
+ .input-group button:hover {
1163
+ background: #3182ce;
1164
+ border-color: #3182ce;
1165
+ transform: translateY(-2px);
1166
+ }
1167
+
1168
+ .error {
1169
+ color: #f56565;
1170
+ margin-top: 15px;
1171
+ font-weight: 600;
1172
+ display: none;
1173
+ }
1174
+ </style>
1175
+ </head>
1176
+ <body>
1177
+ <div class="box">
1178
+ <h1>MC机器人管理中心</h1>
1179
+ <p>请输入管理密钥</p>
1180
+ <div class="input-group">
1181
+ <input type="password" id="k" placeholder="输入密钥后按回车" autofocus>
1182
+ </div>
1183
+ <div class="input-group">
1184
+ <button onclick="login()">立即验证</button>
1185
+ </div>
1186
+ <div class="error" id="msg"></div>
1187
+ </div>
1188
+ <script>
1189
+ async function login() {
1190
+ const key = document.getElementById('k').value.trim();
1191
+ if (!key) return;
1192
+
1193
+ try {
1194
+ const r = await fetch('/api/verify-hotupdate-key', {
1195
+ method: 'POST',
1196
+ headers: {
1197
+ 'Content-Type': 'application/json',
1198
+ 'x-auth-key': key // 直接发 header
1199
+ }
1200
+ });
1201
+ const d = await r.json();
1202
+ if (d.success) {
1203
+ // 种 cookie 供后续 dashboard 使用
1204
+ document.cookie = 'hotupdate_token=' + encodeURIComponent(key) + ';path=/;max-age=3600';
1205
+ location.href = '/hotupdateserver/dashboard';
1206
+ } else {
1207
+ document.getElementById('msg').textContent = '密钥错误';
1208
+ document.getElementById('msg').style.display = 'block';
1209
+ }
1210
+ } catch(e) {
1211
+ document.getElementById('msg').textContent = '网络错误';
1212
+ document.getElementById('msg').style.display = 'block';
1213
+ }
1214
+ }
1215
+ document.getElementById('k').addEventListener('keyup', e => e.key==='Enter' && login());
1216
+ </script>
1217
+ </body></html>`);
1218
+ });
1219
+
1220
+ // ==================== 2. 验证接口(只认 x-auth-key)================
1221
+ this.app.post('/api/verify-hotupdate-key', (req, res) => {
1222
+ const key = req.headers['x-auth-key'] || '';
1223
+ if (hotUpdateAuth.verify(key)) {
1224
+ res.json({ success: true });
1225
+ } else {
1226
+ res.status(401).json({ success: false, message: 'Invalid key' });
1227
+ }
1228
+ });
1229
+
1230
+ // ==================== 3. 仪表盘(从 cookie 读取密钥 → 发 x-auth-key)================
1231
+ this.app.get('/hotupdateserver/dashboard', (req, res) => {
1232
+ // 认证逻辑
1233
+ const key = req.headers['x-auth-key'] || '';
1234
+ const token = req.cookies?.hotupdate_token || '';
1235
+ if (!hotUpdateAuth.verify(key) && !hotUpdateAuth.verify(token)) {
1236
+ return res.redirect('/hotupdateserver');
1237
+ }
1238
+
1239
+ const currentConfig = JSON.stringify(global.SERVERS || SERVERS, null, 2);
1240
+ const { version } = require('./package.json');
1241
+
1242
+ res.send(`<!DOCTYPE html>
1243
+ <html lang="zh-CN">
1244
+ <head>
1245
+ <meta charset="UTF-8">
1246
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1247
+ <title>热更新配置 - Minecraft 机器人系统</title>
1248
+ <style>
1249
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1250
+ body {
1251
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1252
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1253
+ min-height: 100vh;
1254
+ padding: 20px;
1255
+ color: #333;
1256
+ }
1257
+ .container {
1258
+ max-width: 1000px;
1259
+ margin: 0 auto;
1260
+ background: rgba(255, 255, 255, 0.95);
1261
+ border-radius: 20px;
1262
+ padding: 35px;
1263
+ box-shadow: 0 20px 50px rgba(0,0,0,0.3);
1264
+ backdrop-filter: blur(12px);
1265
+ }
1266
+ h1 {
1267
+ text-align: center;
1268
+ font-size: 2.4rem;
1269
+ margin-bottom: 12px;
1270
+ background: linear-gradient(90deg, #4299e1, #805ad5);
1271
+ -webkit-background-clip: text;
1272
+ -webkit-text-fill-color: transparent;
1273
+ }
1274
+ p.subtitle {
1275
+ text-align: center;
1276
+ color: #718096;
1277
+ margin-bottom: 30px;
1278
+ font-size: 1.1rem;
1279
+ }
1280
+ textarea {
1281
+ width: 100%;
1282
+ height: 520px;
1283
+ padding: 18px;
1284
+ font-family: 'Consolas', 'Courier New', monospace;
1285
+ font-size: 15px;
1286
+ line-height: 1.6;
1287
+ border: 3px solid #e2e8f0;
1288
+ border-radius: 14px;
1289
+ resize: vertical;
1290
+ background: #f8fafc;
1291
+ transition: all 0.3s;
1292
+ }
1293
+ textarea:focus {
1294
+ outline: none;
1295
+ border-color: #4299e1;
1296
+ box-shadow: 0 0 0 4px rgba(66, 153, 225, 0.2);
1297
+ background: white;
1298
+ }
1299
+ .actions {
1300
+ text-align: center;
1301
+ margin: 30px 0 20px;
1302
+ }
1303
+ .btn {
1304
+ padding: 16px 40px;
1305
+ margin: 0 12px;
1306
+ font-size: 1.2rem;
1307
+ font-weight: bold;
1308
+ border: none;
1309
+ border-radius: 12px;
1310
+ cursor: pointer;
1311
+ transition: all 0.3s;
1312
+ box-shadow: 0 6px 20px rgba(0,0,0,0.15);
1313
+ }
1314
+ .btn-apply {
1315
+ background: #48bb78;
1316
+ color: white;
1317
+ }
1318
+ .btn-apply:hover { background: #38a169; transform: translateY(-3px); }
1319
+ .btn-logout {
1320
+ background: #f56565;
1321
+ color: white;
1322
+ }
1323
+ .btn-logout:hover { background: #e53e3e; transform: translateY(-3px); }
1324
+ .msg {
1325
+ margin-top: 20px;
1326
+ padding: 16px;
1327
+ border-radius: 12px;
1328
+ text-align: center;
1329
+ font-weight: 600;
1330
+ font-size: 1.1rem;
1331
+ display: none;
1332
+ }
1333
+ .success { background: #c6f6d5; color: #276749; }
1334
+ .error { background: #fed7d7; color: #c53030; }
1335
+ .footer {
1336
+ text-align: center;
1337
+ margin-top: 40px;
1338
+ color: #a0aec0;
1339
+ font-size: 0.95rem;
1340
+ }
1341
+ .footer a { color: #4299e1; text-decoration: none; }
1342
+ .footer a:hover { text-decoration: underline; }
1343
+ </style>
1344
+ </head>
1345
+ <body>
1346
+ <div class="container">
1347
+ <h1>热更新服务器配置</h1>
1348
+ <p class="subtitle">直接修改下方 JSON 配置,点击“应用并热更新”立即生效</p>
1349
+
1350
+ <textarea id="config" spellcheck="false">${currentConfig.replace(/</g, '&lt;')}</textarea>
1351
+
1352
+ <div class="actions">
1353
+ <button class="btn btn-apply" id="apply">应用并热更新</button>
1354
+ <button class="btn btn-logout" id="logout">退出登录</button>
1355
+ </div>
1356
+
1357
+ <div id="msg" class="msg"></div>
1358
+
1359
+ <div class="footer">
1360
+ Minecraft 机器人监控系统 v${version} •
1361
+ <a href="/" target="_blank">返回监控主页</a>
1362
+ </div>
1363
+ </div>
1364
+
1365
+ <script>
1366
+ function getToken() {
1367
+ const m = document.cookie.match(/hotupdate_token=([^;]+)/);
1368
+ return m ? decodeURIComponent(m[1]) : '';
1369
+ }
1370
+
1371
+ document.getElementById('apply').onclick = async () => {
1372
+ const btn = document.getElementById('apply');
1373
+ const msg = document.getElementById('msg');
1374
+ const text = document.getElementById('config').value;
1375
+
1376
+ btn.disabled = true;
1377
+ msg.style.display = 'none';
1378
+
1379
+ let config;
1380
+ try {
1381
+ config = JSON.parse(text);
1382
+ if (!Array.isArray(config)) throw new Error('配置必须是一个数组');
1383
+ } catch (e) {
1384
+ msg.textContent = 'JSON 解析失败:' + e.message;
1385
+ msg.className = 'msg error';
1386
+ msg.style.display = 'block';
1387
+ btn.disabled = false;
1388
+ return;
1389
+ }
1390
+
1391
+ try {
1392
+ const rawText = document.getElementById('config').value.trim();
1393
+ const r = await fetch('/api/apply-new-servers', {
1394
+ method: 'POST',
1395
+ headers: {
1396
+ 'Content-Type': 'application/json',
1397
+ 'x-auth-key': getToken()
1398
+ },
1399
+ body: rawText
1400
+ });
1401
+ const d = await r.json();
1402
+
1403
+ msg.textContent = d.success
1404
+ ? '配置已成功应用!所有机器人已热更新完成'
1405
+ : '应用失败:' + (d.message || '未知错误');
1406
+ msg.className = 'msg ' + (d.success ? 'success' : 'error');
1407
+ } catch (e) {
1408
+ msg.textContent = '提交失败:' + e.message;
1409
+ msg.className = 'msg error';
1410
+ }
1411
+
1412
+ msg.style.display = 'block';
1413
+ btn.disabled = false;
1414
+ };
1415
+
1416
+ document.getElementById('logout').onclick = () => {
1417
+ document.cookie = 'hotupdate_token=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';
1418
+ location.href = '/hotupdateserver';
1419
+ };
1420
+ </script>
1421
+ </body>
1422
+ </html>`);
1423
+ });
1424
+ // ==================== 4. 应用新配置接口(只认 x-auth-key)================
1425
+ this.app.post('/api/apply-new-servers', async (req, res) => {
1426
+ const key = req.headers['x-auth-key'] || '';
1427
+ if (!hotUpdateAuth.verify(key)) {
1428
+ return res.status(403).json({ success: false, message: '认证失败' });
1429
+ }
1430
+
1431
+ let rawJsonText = '';
1432
+
1433
+ // 1. text/plain 直接发原始 JSON 文本(最推荐)
1434
+ if (typeof req.body === 'string') {
1435
+ rawJsonText = req.body.trim();
1436
+ }
1437
+ // 2. application/json + { servers: [...] } 对象
1438
+ else if (req.body && Array.isArray(req.body.servers)) {
1439
+ rawJsonText = JSON.stringify(req.body.servers, null, 2);
1440
+ }
1441
+ // 3. application/json + { config: "..." } 字符串
1442
+ else if (req.body && typeof req.body.config === 'string') {
1443
+ rawJsonText = req.body.config.trim();
1444
+ }
1445
+ // 4. 直接就是数组(极少见但也支持)
1446
+ else if (Array.isArray(req.body)) {
1447
+ rawJsonText = JSON.stringify(req.body, null, 2);
1448
+ }
1449
+ else {
1450
+ return res.status(400).json({
1451
+ success: false,
1452
+ message: '不支持的提交格式(请发送 JSON 文本或 {servers: [...]})'
1453
+ });
1454
+ }
1455
+
1456
+ if (rawJsonText === '' || rawJsonText === '[]') {
1457
+ return res.status(400).json({ success: false, message: '配置不能为空' });
1458
+ }
1459
+
1460
+ // 解析并验证
1461
+ let servers;
1462
+ try {
1463
+ servers = JSON.parse(rawJsonText);
1464
+ if (!Array.isArray(servers)) throw new Error('根节点必须是数组');
1465
+ } catch (err) {
1466
+ return res.status(400).json({
1467
+ success: false,
1468
+ message: 'JSON 格式错误:' + err.message
1469
+ });
1470
+ }
1471
+
1472
+ try {
1473
+ // 更新内存
1474
+ SERVERS.length = 0;
1475
+ SERVERS.push(...servers);
1476
+
1477
+ // 关键:同步更新环境变量原始字符串(重启后依然生效)
1478
+ process.env.SERVERS_JSON = rawJsonText;
1479
+
1480
+ // 持久化到文件(双保险)
1481
+ require('fs').writeFileSync('./servers.json', rawJsonText + '\n');
1482
+
1483
+ // 执行热更新
1484
+ await hotUpdateServers();
1485
+
1486
+ console.log(`热更新成功!已加载 ${servers.length} 台服务器配置`);
1487
+ res.json({ success: true, message: '热更新成功,配置已永久保存' });
1488
+ } catch (err) {
1489
+ console.error('热更新执行失败:', err);
1490
+ res.status(500).json({ success: false, message: '服务器内部错误' });
1491
+ }
1492
+ });
931
1493
  }
932
1494
 
933
1495
  start() {
@@ -979,6 +1541,87 @@ async function initialize() {
979
1541
  process.on('SIGTERM', shutdown);
980
1542
  }
981
1543
 
1544
+ async function hotUpdateServers() {
1545
+ const newServers = SERVERS;
1546
+
1547
+ const newServerMap = new Map();
1548
+ newServers.forEach(srv => {
1549
+ const key = `${srv.host}:${srv.port}`;
1550
+ newServerMap.set(key, { ...srv });
1551
+ });
1552
+
1553
+ const currentManagers = new Map();
1554
+ botManagers.forEach(manager => {
1555
+ const key = `${manager.host}:${manager.port}`;
1556
+ currentManagers.set(key, manager);
1557
+ });
1558
+
1559
+ const toRemove = [];
1560
+ const toAdd = [];
1561
+
1562
+ // 1. 处理需要删除或重建的服务器
1563
+ for (const [key, manager] of currentManagers) {
1564
+ const newConfig = newServerMap.get(key);
1565
+
1566
+ if (!newConfig) {
1567
+ // 删除:从 globalServerStatus 中移除
1568
+ globalServerStatus.servers.delete(key);
1569
+ console.log(`热更新: 删除服务器 ${key}`);
1570
+ toRemove.push(manager);
1571
+ } else {
1572
+ const configSame =
1573
+ manager.minBots === newConfig.minBots &&
1574
+ manager.maxBots === newConfig.maxBots &&
1575
+ manager.version === newConfig.version;
1576
+
1577
+ if (configSame) {
1578
+ // 完全一致:保留,并从 newServerMap 删除(避免重复创建)
1579
+ newServerMap.delete(key);
1580
+ // 状态保持最新(可选:刷新 lastUpdate)
1581
+ const status = globalServerStatus.servers.get(key);
1582
+ if (status) status.lastUpdate = Date.now();
1583
+ } else {
1584
+ // 配置变更:先删除旧状态,再重建
1585
+ globalServerStatus.servers.delete(key);
1586
+ console.log(`热更新: 重建服务器 ${key} ` +
1587
+ `(${manager.minBots}-${manager.maxBots}@${manager.version} → ` +
1588
+ `${newConfig.minBots}-${newConfig.maxBots}@${newConfig.version})`);
1589
+ toRemove.push(manager);
1590
+ toAdd.push(newConfig);
1591
+ newServerMap.delete(key);
1592
+ }
1593
+ }
1594
+ }
1595
+
1596
+ // 2. 处理新增的服务器
1597
+ for (const config of newServerMap.values()) {
1598
+ const key = `${config.host}:${config.port}`;
1599
+ console.log(`热更新: 新增服务器 ${key} (${config.minBots}-${config.maxBots})`);
1600
+ toAdd.push(config);
1601
+ }
1602
+
1603
+ // 3. 先关闭 + 移除旧的管理器
1604
+ for (const manager of toRemove) {
1605
+ manager.dispose(); // 统一释放资源
1606
+ const idx = botManagers.indexOf(manager);
1607
+ if (idx !== -1) botManagers.splice(idx, 1);
1608
+ }
1609
+
1610
+ // 4. 创建新的管理器(构造函数里会自动注册到 globalServerStatus)
1611
+ for (const config of toAdd) {
1612
+ const newManager = new MinecraftBotManager(
1613
+ config.host,
1614
+ config.port,
1615
+ config.minBots,
1616
+ config.maxBots,
1617
+ config.version
1618
+ );
1619
+ newManager.startMonitoring();
1620
+ botManagers.push(newManager);
1621
+ }
1622
+ console.log(`热更新完成,当前运行服务器数量: ${botManagers.length},前端状态已同步`);
1623
+ }
1624
+
982
1625
  function shutdown() {
983
1626
  console.log('正在关闭系统...');
984
1627