@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.
- package/README.md +27 -0
- package/package.json +12 -11
- 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
|
|
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
|
-
|
|
7
|
+
"start": "node server.js"
|
|
8
8
|
},
|
|
9
9
|
"keywords": [
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
"minecraft",
|
|
11
|
+
"bot",
|
|
12
|
+
"mineflayer"
|
|
13
13
|
],
|
|
14
14
|
"author": "mingli2038",
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"dependencies": {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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('
|
|
62
|
+
console.error('无法解析环境变量 SERVERS_JSON');
|
|
27
63
|
console.error('原因:', error.message);
|
|
28
|
-
console.error('
|
|
29
|
-
console.error('
|
|
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
|
-
|
|
847
|
-
|
|
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, '<')}</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
|
|