@baipiaodajun/mcbots 1.0.10 → 1.2.0

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 +671 -13
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.0.10",
3
+ "version": "1.2.0",
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) {
@@ -463,6 +560,10 @@ class StatusServer {
463
560
  constructor(port = PORT) {
464
561
  this.port = port;
465
562
  this.app = express();
563
+ this.app.use(express.json({ limit: '10mb' }));
564
+ this.app.use(express.text({ type: 'text/plain' }));
565
+ this.app.use(express.urlencoded({ extended: true }));
566
+ this.app.use(cookieParser());
466
567
  this.server = null;
467
568
  this.setupRoutes();
468
569
  }
@@ -471,7 +572,7 @@ class StatusServer {
471
572
  this.app.get('/', (req, res) => {
472
573
  const serversStatus = Array.from(globalServerStatus.servers.values());
473
574
  const systemInfo = systemMonitor.getSystemInfo();
474
-
575
+ const { version } = require('./package.json');
475
576
  const html = `
476
577
  <!DOCTYPE html>
477
578
  <html lang="zh-CN">
@@ -721,7 +822,31 @@ class StatusServer {
721
822
  opacity: 0.8;
722
823
  font-size: 0.9rem;
723
824
  }
724
-
825
+ .refresh-info a {
826
+ color: #ffeb3b;
827
+ font-weight: bold;
828
+ text-decoration: none;
829
+ margin-left: 8px;
830
+ }
831
+
832
+ .refresh-info a:hover {
833
+ text-decoration: underline;
834
+ color: #fff176;
835
+ }
836
+ .refresh-info button {
837
+ margin-left: 8px;
838
+ background-color: #ffeb3b;
839
+ color: #6a0dad;
840
+ font-weight: bold;
841
+ border: none;
842
+ border-radius: 4px;
843
+ padding: 4px 8px;
844
+ cursor: pointer;
845
+ }
846
+
847
+ .refresh-info button:hover {
848
+ background-color: #fff176;
849
+ }
725
850
  .progress-bar {
726
851
  width: 100%;
727
852
  height: 8px;
@@ -777,6 +902,7 @@ class StatusServer {
777
902
  <span class="stat-label">监控服务器</span>
778
903
  <span class="stat-value">${serversStatus.length} 个</span>
779
904
  </div>
905
+
780
906
  </div>
781
907
  </div>
782
908
 
@@ -843,11 +969,33 @@ class StatusServer {
843
969
  </div>
844
970
 
845
971
  <div class="refresh-info">
846
- 页面每30秒自动刷新 • Minecraft 机器人监控系统 v1.0
847
- </div>
972
+ 页面每30秒自动刷新 • Minecraft 机器人监控系统 v${version}
973
+ · <a href="/hotupdateserver/dashboard" rel="noopener noreferrer">更新MC机器人配置</a>
974
+ · <a href="https://www.npmjs.com/package/@baipiaodajun/mcbots" target="_blank" rel="noopener noreferrer">NPM主頁</a>
975
+ · <a href="https://gbjs.hospedagem-gratis.com/mcbot.html" target="_blank" rel="noopener noreferrer">SERVER_JSON生成器</a>
976
+ · <a href="https://gbjs.hospedagem-gratis.com/mcbot2.html" target="_blank" rel="noopener noreferrer">SERVER_JSON修改器</a>
977
+ · <button id="copy-config">复制配置</button>
978
+ </div>
979
+ <!-- 隐藏元素存放 SERVERS_JSON -->
980
+ <div id="servers-json" style="display:none;">
981
+ ${process.env.SERVERS_JSON}
982
+ </div>
848
983
  </div>
849
984
 
850
985
  <script>
986
+ document.getElementById('copy-config').addEventListener('click', () => {
987
+ // 从隐藏元素中获取内容
988
+ const serversJson = document.getElementById('servers-json').textContent.trim();
989
+
990
+ if (serversJson) {
991
+ navigator.clipboard.writeText(serversJson)
992
+ .then(() => alert('配置已复制到剪贴板'))
993
+ .catch(err => alert('复制失败: ' + err));
994
+ } else {
995
+ alert('未找到配置内容');
996
+ }
997
+ });
998
+
851
999
  // 30秒自动刷新
852
1000
  setTimeout(() => {
853
1001
  location.reload();
@@ -882,6 +1030,419 @@ class StatusServer {
882
1030
  timestamp: Date.now()
883
1031
  });
884
1032
  });
1033
+ // ==================== 1. 密钥登录页(修复:登录成功后只存密钥到 cookie,前端用它发 header)================
1034
+ this.app.get('/hotupdateserver', (req, res) => {
1035
+ const key = req.query.key || req.headers['x-auth-key'] || '';
1036
+ if (key && hotUpdateAuth.verify(key)) {
1037
+ // 直接跳转并种 cookie(仅供前端 JS 读取)
1038
+ res.cookie('hotupdate_token', key, { maxAge: 3600000, httpOnly: false, path: '/', sameSite: 'lax' });
1039
+ return res.redirect('/hotupdateserver/dashboard');
1040
+ }
1041
+
1042
+ res.send(`<!DOCTYPE html>
1043
+ <html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
1044
+ <title>权限认证</title>
1045
+ <style>
1046
+ body {
1047
+ font-family: 'Segoe UI', sans-serif;
1048
+ background: linear-gradient(135deg, #667eea, #764ba2);
1049
+ min-height: 100vh;
1050
+ display: flex;
1051
+ align-items: center;
1052
+ justify-content: center;
1053
+ margin: 0;
1054
+ padding: 20px;
1055
+ }
1056
+ .box {
1057
+ max-width: 520px;
1058
+ width: 95%;
1059
+ background: white;
1060
+ border-radius: 20px;
1061
+ padding: 50px 45px;
1062
+ box-shadow: 0 20px 50px rgba(0,0,0,0.35);
1063
+ text-align: center;
1064
+ backdrop-filter: blur(10px);
1065
+ }
1066
+ h1 {
1067
+ font-size: 2.5rem;
1068
+ margin-bottom: 12px;
1069
+ background: linear-gradient(90deg, #4299e1, #805ad5);
1070
+ -webkit-background-clip: text;
1071
+ -webkit-text-fill-color: transparent;
1072
+ }
1073
+ p { color: #718096; margin-bottom: 38px; font-size: 1.15rem; }
1074
+
1075
+ /* 核心容器:固定宽度,完美居中 */
1076
+ .input-group {
1077
+ width: 100%;
1078
+ max-width: 420px;
1079
+ margin: 0 auto 26px auto;
1080
+ }
1081
+
1082
+ /* 关键:强制 input 和 button 完全一致,包括边框和内边距 */
1083
+ .input-group input,
1084
+ .input-group button {
1085
+ box-sizing: border-box !important; /* 包含 border + padding */
1086
+ width: 100% !important;
1087
+ padding: 18px 20px;
1088
+ font-size: 1.18rem;
1089
+ border-radius: 14px;
1090
+ display: block;
1091
+ margin: 0;
1092
+ }
1093
+
1094
+ .input-group input {
1095
+ border: 3px solid #e2e8f0;
1096
+ background: #f8fafc;
1097
+ transition: all 0.3s;
1098
+ }
1099
+ .input-group input:focus {
1100
+ outline: none;
1101
+ border-color: #4299e1;
1102
+ background: white;
1103
+ box-shadow: 0 0 0 5px rgba(66,153,225,.25);
1104
+ }
1105
+
1106
+ .input-group button {
1107
+ background: #4299e1;
1108
+ color: white;
1109
+ border: 3px solid #4299e1; /* 加边框,让厚度一致! */
1110
+ font-weight: bold;
1111
+ cursor: pointer;
1112
+ transition: all 0.3s;
1113
+ box-shadow: 0 6px 20px rgba(66,153,225,.4);
1114
+ }
1115
+ .input-group button:hover {
1116
+ background: #3182ce;
1117
+ border-color: #3182ce;
1118
+ transform: translateY(-2px);
1119
+ }
1120
+
1121
+ .error {
1122
+ color: #f56565;
1123
+ margin-top: 15px;
1124
+ font-weight: 600;
1125
+ display: none;
1126
+ }
1127
+ </style>
1128
+ </head>
1129
+ <body>
1130
+ <div class="box">
1131
+ <h1>MC机器人管理中心</h1>
1132
+ <p>请输入管理密钥</p>
1133
+ <div class="input-group">
1134
+ <input type="password" id="k" placeholder="输入密钥后按回车" autofocus>
1135
+ </div>
1136
+ <div class="input-group">
1137
+ <button onclick="login()">立即验证</button>
1138
+ </div>
1139
+ <div class="error" id="msg"></div>
1140
+ </div>
1141
+ <script>
1142
+ async function login() {
1143
+ const key = document.getElementById('k').value.trim();
1144
+ if (!key) return;
1145
+
1146
+ try {
1147
+ const r = await fetch('/api/verify-hotupdate-key', {
1148
+ method: 'POST',
1149
+ headers: {
1150
+ 'Content-Type': 'application/json',
1151
+ 'x-auth-key': key // 直接发 header
1152
+ }
1153
+ });
1154
+ const d = await r.json();
1155
+ if (d.success) {
1156
+ // 种 cookie 供后续 dashboard 使用
1157
+ document.cookie = 'hotupdate_token=' + encodeURIComponent(key) + ';path=/;max-age=3600';
1158
+ location.href = '/hotupdateserver/dashboard';
1159
+ } else {
1160
+ document.getElementById('msg').textContent = '密钥错误';
1161
+ document.getElementById('msg').style.display = 'block';
1162
+ }
1163
+ } catch(e) {
1164
+ document.getElementById('msg').textContent = '网络错误';
1165
+ document.getElementById('msg').style.display = 'block';
1166
+ }
1167
+ }
1168
+ document.getElementById('k').addEventListener('keyup', e => e.key==='Enter' && login());
1169
+ </script>
1170
+ </body></html>`);
1171
+ });
1172
+
1173
+ // ==================== 2. 验证接口(只认 x-auth-key)================
1174
+ this.app.post('/api/verify-hotupdate-key', (req, res) => {
1175
+ const key = req.headers['x-auth-key'] || '';
1176
+ if (hotUpdateAuth.verify(key)) {
1177
+ res.json({ success: true });
1178
+ } else {
1179
+ res.status(401).json({ success: false, message: 'Invalid key' });
1180
+ }
1181
+ });
1182
+
1183
+ // ==================== 3. 仪表盘(从 cookie 读取密钥 → 发 x-auth-key)================
1184
+ this.app.get('/hotupdateserver/dashboard', (req, res) => {
1185
+ // 认证逻辑
1186
+ const key = req.headers['x-auth-key'] || '';
1187
+ const token = req.cookies?.hotupdate_token || '';
1188
+ if (!hotUpdateAuth.verify(key) && !hotUpdateAuth.verify(token)) {
1189
+ return res.redirect('/hotupdateserver');
1190
+ }
1191
+
1192
+ const currentConfig = JSON.stringify(global.SERVERS || SERVERS, null, 2);
1193
+ const { version } = require('./package.json');
1194
+
1195
+ res.send(`<!DOCTYPE html>
1196
+ <html lang="zh-CN">
1197
+ <head>
1198
+ <meta charset="UTF-8">
1199
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1200
+ <title>热更新配置 - Minecraft 机器人系统</title>
1201
+ <style>
1202
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1203
+ body {
1204
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1205
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1206
+ min-height: 100vh;
1207
+ padding: 20px;
1208
+ color: #333;
1209
+ }
1210
+ .container {
1211
+ max-width: 1000px;
1212
+ margin: 0 auto;
1213
+ background: rgba(255, 255, 255, 0.95);
1214
+ border-radius: 20px;
1215
+ padding: 35px;
1216
+ box-shadow: 0 20px 50px rgba(0,0,0,0.3);
1217
+ backdrop-filter: blur(12px);
1218
+ }
1219
+ h1 {
1220
+ text-align: center;
1221
+ font-size: 2.4rem;
1222
+ margin-bottom: 12px;
1223
+ background: linear-gradient(90deg, #4299e1, #805ad5);
1224
+ -webkit-background-clip: text;
1225
+ -webkit-text-fill-color: transparent;
1226
+ }
1227
+ p.subtitle {
1228
+ text-align: center;
1229
+ color: #718096;
1230
+ margin-bottom: 30px;
1231
+ font-size: 1.1rem;
1232
+ }
1233
+ textarea {
1234
+ width: 100%;
1235
+ height: 520px;
1236
+ padding: 18px;
1237
+ font-family: 'Consolas', 'Courier New', monospace;
1238
+ font-size: 15px;
1239
+ line-height: 1.6;
1240
+ border: 3px solid #e2e8f0;
1241
+ border-radius: 14px;
1242
+ resize: vertical;
1243
+ background: #f8fafc;
1244
+ transition: all 0.3s;
1245
+ }
1246
+ textarea:focus {
1247
+ outline: none;
1248
+ border-color: #4299e1;
1249
+ box-shadow: 0 0 0 4px rgba(66, 153, 225, 0.2);
1250
+ background: white;
1251
+ }
1252
+ .actions {
1253
+ text-align: center;
1254
+ margin: 30px 0 20px;
1255
+ }
1256
+ .btn {
1257
+ padding: 16px 40px;
1258
+ margin: 0 12px;
1259
+ font-size: 1.2rem;
1260
+ font-weight: bold;
1261
+ border: none;
1262
+ border-radius: 12px;
1263
+ cursor: pointer;
1264
+ transition: all 0.3s;
1265
+ box-shadow: 0 6px 20px rgba(0,0,0,0.15);
1266
+ }
1267
+ .btn-apply {
1268
+ background: #48bb78;
1269
+ color: white;
1270
+ }
1271
+ .btn-apply:hover { background: #38a169; transform: translateY(-3px); }
1272
+ .btn-logout {
1273
+ background: #f56565;
1274
+ color: white;
1275
+ }
1276
+ .btn-logout:hover { background: #e53e3e; transform: translateY(-3px); }
1277
+ .msg {
1278
+ margin-top: 20px;
1279
+ padding: 16px;
1280
+ border-radius: 12px;
1281
+ text-align: center;
1282
+ font-weight: 600;
1283
+ font-size: 1.1rem;
1284
+ display: none;
1285
+ }
1286
+ .success { background: #c6f6d5; color: #276749; }
1287
+ .error { background: #fed7d7; color: #c53030; }
1288
+ .footer {
1289
+ text-align: center;
1290
+ margin-top: 40px;
1291
+ color: #a0aec0;
1292
+ font-size: 0.95rem;
1293
+ }
1294
+ .footer a { color: #4299e1; text-decoration: none; }
1295
+ .footer a:hover { text-decoration: underline; }
1296
+ </style>
1297
+ </head>
1298
+ <body>
1299
+ <div class="container">
1300
+ <h1>热更新服务器配置</h1>
1301
+ <p class="subtitle">直接修改下方 JSON 配置,点击“应用并热更新”立即生效</p>
1302
+
1303
+ <textarea id="config" spellcheck="false">${currentConfig.replace(/</g, '&lt;')}</textarea>
1304
+
1305
+ <div class="actions">
1306
+ <button class="btn btn-apply" id="apply">应用并热更新</button>
1307
+ <button class="btn btn-logout" id="logout">退出登录</button>
1308
+ </div>
1309
+
1310
+ <div id="msg" class="msg"></div>
1311
+
1312
+ <div class="footer">
1313
+ Minecraft 机器人监控系统 v${version} •
1314
+ <a href="/" target="_blank">返回监控主页</a>
1315
+ </div>
1316
+ </div>
1317
+
1318
+ <script>
1319
+ function getToken() {
1320
+ const m = document.cookie.match(/hotupdate_token=([^;]+)/);
1321
+ return m ? decodeURIComponent(m[1]) : '';
1322
+ }
1323
+
1324
+ document.getElementById('apply').onclick = async () => {
1325
+ const btn = document.getElementById('apply');
1326
+ const msg = document.getElementById('msg');
1327
+ const text = document.getElementById('config').value;
1328
+
1329
+ btn.disabled = true;
1330
+ msg.style.display = 'none';
1331
+
1332
+ let config;
1333
+ try {
1334
+ config = JSON.parse(text);
1335
+ if (!Array.isArray(config)) throw new Error('配置必须是一个数组');
1336
+ } catch (e) {
1337
+ msg.textContent = 'JSON 解析失败:' + e.message;
1338
+ msg.className = 'msg error';
1339
+ msg.style.display = 'block';
1340
+ btn.disabled = false;
1341
+ return;
1342
+ }
1343
+
1344
+ try {
1345
+ const rawText = document.getElementById('config').value.trim();
1346
+ const r = await fetch('/api/apply-new-servers', {
1347
+ method: 'POST',
1348
+ headers: {
1349
+ 'Content-Type': 'application/json',
1350
+ 'x-auth-key': getToken()
1351
+ },
1352
+ body: rawText
1353
+ });
1354
+ const d = await r.json();
1355
+
1356
+ msg.textContent = d.success
1357
+ ? '配置已成功应用!所有机器人已热更新完成'
1358
+ : '应用失败:' + (d.message || '未知错误');
1359
+ msg.className = 'msg ' + (d.success ? 'success' : 'error');
1360
+ } catch (e) {
1361
+ msg.textContent = '提交失败:' + e.message;
1362
+ msg.className = 'msg error';
1363
+ }
1364
+
1365
+ msg.style.display = 'block';
1366
+ btn.disabled = false;
1367
+ };
1368
+
1369
+ document.getElementById('logout').onclick = () => {
1370
+ document.cookie = 'hotupdate_token=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';
1371
+ location.href = '/hotupdateserver';
1372
+ };
1373
+ </script>
1374
+ </body>
1375
+ </html>`);
1376
+ });
1377
+ // ==================== 4. 应用新配置接口(只认 x-auth-key)================
1378
+ this.app.post('/api/apply-new-servers', async (req, res) => {
1379
+ const key = req.headers['x-auth-key'] || '';
1380
+ if (!hotUpdateAuth.verify(key)) {
1381
+ return res.status(403).json({ success: false, message: '认证失败' });
1382
+ }
1383
+
1384
+ let rawJsonText = '';
1385
+
1386
+ // 1. text/plain 直接发原始 JSON 文本(最推荐)
1387
+ if (typeof req.body === 'string') {
1388
+ rawJsonText = req.body.trim();
1389
+ }
1390
+ // 2. application/json + { servers: [...] } 对象
1391
+ else if (req.body && Array.isArray(req.body.servers)) {
1392
+ rawJsonText = JSON.stringify(req.body.servers, null, 2);
1393
+ }
1394
+ // 3. application/json + { config: "..." } 字符串
1395
+ else if (req.body && typeof req.body.config === 'string') {
1396
+ rawJsonText = req.body.config.trim();
1397
+ }
1398
+ // 4. 直接就是数组(极少见但也支持)
1399
+ else if (Array.isArray(req.body)) {
1400
+ rawJsonText = JSON.stringify(req.body, null, 2);
1401
+ }
1402
+ else {
1403
+ return res.status(400).json({
1404
+ success: false,
1405
+ message: '不支持的提交格式(请发送 JSON 文本或 {servers: [...]})'
1406
+ });
1407
+ }
1408
+
1409
+ if (rawJsonText === '' || rawJsonText === '[]') {
1410
+ return res.status(400).json({ success: false, message: '配置不能为空' });
1411
+ }
1412
+
1413
+ // 解析并验证
1414
+ let servers;
1415
+ try {
1416
+ servers = JSON.parse(rawJsonText);
1417
+ if (!Array.isArray(servers)) throw new Error('根节点必须是数组');
1418
+ } catch (err) {
1419
+ return res.status(400).json({
1420
+ success: false,
1421
+ message: 'JSON 格式错误:' + err.message
1422
+ });
1423
+ }
1424
+
1425
+ try {
1426
+ // 更新内存
1427
+ SERVERS.length = 0;
1428
+ SERVERS.push(...servers);
1429
+
1430
+ // 关键:同步更新环境变量原始字符串(重启后依然生效)
1431
+ process.env.SERVERS_JSON = rawJsonText;
1432
+
1433
+ // 持久化到文件(双保险)
1434
+ require('fs').writeFileSync('./servers.json', rawJsonText + '\n');
1435
+
1436
+ // 执行热更新
1437
+ await hotUpdateServers();
1438
+
1439
+ console.log(`热更新成功!已加载 ${servers.length} 台服务器配置`);
1440
+ res.json({ success: true, message: '热更新成功,配置已永久保存' });
1441
+ } catch (err) {
1442
+ console.error('热更新执行失败:', err);
1443
+ res.status(500).json({ success: false, message: '服务器内部错误' });
1444
+ }
1445
+ });
885
1446
  }
886
1447
 
887
1448
  start() {
@@ -933,6 +1494,103 @@ async function initialize() {
933
1494
  process.on('SIGTERM', shutdown);
934
1495
  }
935
1496
 
1497
+ async function hotUpdateServers() {
1498
+ const newServers = SERVERS;
1499
+
1500
+ const newServerMap = new Map();
1501
+ newServers.forEach(srv => {
1502
+ const key = `${srv.host}:${srv.port}`;
1503
+ newServerMap.set(key, { ...srv });
1504
+ });
1505
+
1506
+ const currentManagers = new Map();
1507
+ botManagers.forEach(manager => {
1508
+ const key = `${manager.host}:${manager.port}`;
1509
+ currentManagers.set(key, manager);
1510
+ });
1511
+
1512
+ const toRemove = [];
1513
+ const toAdd = [];
1514
+
1515
+ // 1. 处理需要删除或重建的服务器
1516
+ for (const [key, manager] of currentManagers) {
1517
+ const newConfig = newServerMap.get(key);
1518
+
1519
+ if (!newConfig) {
1520
+ // 删除:从 globalServerStatus 中移除
1521
+ globalServerStatus.servers.delete(key);
1522
+ console.log(`热更新: 删除服务器 ${key}`);
1523
+ toRemove.push(manager);
1524
+ } else {
1525
+ const configSame =
1526
+ manager.minBots === newConfig.minBots &&
1527
+ manager.maxBots === newConfig.maxBots &&
1528
+ manager.version === newConfig.version;
1529
+
1530
+ if (configSame) {
1531
+ // 完全一致:保留,并从 newServerMap 删除(避免重复创建)
1532
+ newServerMap.delete(key);
1533
+ // 状态保持最新(可选:刷新 lastUpdate)
1534
+ const status = globalServerStatus.servers.get(key);
1535
+ if (status) status.lastUpdate = Date.now();
1536
+ } else {
1537
+ // 配置变更:先删除旧状态,再重建
1538
+ globalServerStatus.servers.delete(key);
1539
+ console.log(`热更新: 重建服务器 ${key} ` +
1540
+ `(${manager.minBots}-${manager.maxBots}@${manager.version} → ` +
1541
+ `${newConfig.minBots}-${newConfig.maxBots}@${newConfig.version})`);
1542
+ toRemove.push(manager);
1543
+ toAdd.push(newConfig);
1544
+ newServerMap.delete(key);
1545
+ }
1546
+ }
1547
+ }
1548
+
1549
+ // 2. 处理新增的服务器
1550
+ for (const config of newServerMap.values()) {
1551
+ const key = `${config.host}:${config.port}`;
1552
+ console.log(`热更新: 新增服务器 ${key} (${config.minBots}-${config.maxBots})`);
1553
+ toAdd.push(config);
1554
+
1555
+ // 立即在前台状态中注册(前端马上能看到 initializing)
1556
+ globalServerStatus.servers.set(key, {
1557
+ host: config.host,
1558
+ port: config.port,
1559
+ minBots: config.minBots,
1560
+ maxBots: config.maxBots,
1561
+ currentBots: 0,
1562
+ activeBots: [],
1563
+ lastUpdate: Date.now(),
1564
+ status: 'initializing'
1565
+ });
1566
+ }
1567
+
1568
+ // 3. 先关闭 + 移除旧的管理器
1569
+ for (const manager of toRemove) {
1570
+ manager.stopAllBots();
1571
+ if (typeof manager.stopMonitoring === 'function') {
1572
+ manager.stopMonitoring();
1573
+ }
1574
+ const idx = botManagers.indexOf(manager);
1575
+ if (idx !== -1) botManagers.splice(idx, 1);
1576
+ }
1577
+
1578
+ // 4. 创建新的管理器(构造函数里会自动注册到 globalServerStatus)
1579
+ for (const config of toAdd) {
1580
+ const newManager = new MinecraftBotManager(
1581
+ config.host,
1582
+ config.port,
1583
+ config.minBots,
1584
+ config.maxBots,
1585
+ config.version
1586
+ );
1587
+ newManager.startMonitoring();
1588
+ botManagers.push(newManager);
1589
+ }
1590
+
1591
+ console.log(`热更新完成,当前运行服务器数量: ${botManagers.length},前端状态已同步`);
1592
+ }
1593
+
936
1594
  function shutdown() {
937
1595
  console.log('正在关闭系统...');
938
1596