@sfmc-bds/bds-tools 0.1.0 → 0.2.0-beta.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 (71) hide show
  1. package/dist/bds-manager.d.ts.map +1 -1
  2. package/dist/bds-manager.js +236 -263
  3. package/dist/bds-manager.js.map +7 -1
  4. package/dist/changelog.js +39 -50
  5. package/dist/changelog.js.map +7 -1
  6. package/dist/check-update.d.ts +2 -2
  7. package/dist/check-update.d.ts.map +1 -1
  8. package/dist/check-update.js +361 -401
  9. package/dist/check-update.js.map +7 -1
  10. package/dist/cli-pack-manager.d.ts +10 -6
  11. package/dist/cli-pack-manager.d.ts.map +1 -1
  12. package/dist/cli-pack-manager.js +226 -135
  13. package/dist/cli-pack-manager.js.map +7 -1
  14. package/dist/fsx.js +86 -100
  15. package/dist/fsx.js.map +7 -1
  16. package/dist/http.d.ts.map +1 -1
  17. package/dist/http.js +166 -185
  18. package/dist/http.js.map +7 -1
  19. package/dist/is-main.d.ts +17 -0
  20. package/dist/is-main.d.ts.map +1 -0
  21. package/dist/is-main.js +32 -0
  22. package/dist/is-main.js.map +7 -0
  23. package/dist/log.d.ts +2 -4
  24. package/dist/log.d.ts.map +1 -1
  25. package/dist/log.js +12 -19
  26. package/dist/log.js.map +7 -1
  27. package/dist/logger.js +6 -10
  28. package/dist/logger.js.map +7 -1
  29. package/dist/pack-manager.d.ts +88 -24
  30. package/dist/pack-manager.d.ts.map +1 -1
  31. package/dist/pack-manager.js +280 -245
  32. package/dist/pack-manager.js.map +7 -1
  33. package/dist/paths.d.ts +8 -3
  34. package/dist/paths.d.ts.map +1 -1
  35. package/dist/paths.js +53 -39
  36. package/dist/paths.js.map +7 -1
  37. package/dist/qqutil.d.ts.map +1 -1
  38. package/dist/qqutil.js +90 -87
  39. package/dist/qqutil.js.map +7 -1
  40. package/dist/recovery.js +26 -37
  41. package/dist/recovery.js.map +7 -1
  42. package/dist/rollback.d.ts +1 -1
  43. package/dist/rollback.js +55 -74
  44. package/dist/rollback.js.map +7 -1
  45. package/dist/server-properties.d.ts +12 -0
  46. package/dist/server-properties.d.ts.map +1 -0
  47. package/dist/server-properties.js +28 -0
  48. package/dist/server-properties.js.map +7 -0
  49. package/dist/taskbar.d.ts +2 -0
  50. package/dist/taskbar.d.ts.map +1 -1
  51. package/dist/taskbar.js +51 -82
  52. package/dist/taskbar.js.map +7 -1
  53. package/dist/types.js +1 -5
  54. package/dist/types.js.map +7 -1
  55. package/dist/update-result.d.ts +12 -0
  56. package/dist/update-result.d.ts.map +1 -0
  57. package/dist/update-result.js +15 -0
  58. package/dist/update-result.js.map +7 -0
  59. package/dist/upstream.js +94 -108
  60. package/dist/upstream.js.map +7 -1
  61. package/dist/version.js +63 -81
  62. package/dist/version.js.map +7 -1
  63. package/dist/world-packs.d.ts +112 -0
  64. package/dist/world-packs.d.ts.map +1 -0
  65. package/dist/world-packs.js +335 -0
  66. package/dist/world-packs.js.map +7 -0
  67. package/dist/zipx.d.ts +10 -0
  68. package/dist/zipx.d.ts.map +1 -0
  69. package/dist/zipx.js +48 -0
  70. package/dist/zipx.js.map +7 -0
  71. package/package.json +17 -6
@@ -1,16 +1,8 @@
1
- /**
2
- * check-update.ts — BDS 自动更新器 (主流程)
3
- *
4
- * 1. 临时 staging 目录 (mkdtempSync) - 避免污染 SCRIPT_DIR
5
- * 2. 下载/解压失败 → 自动从备份回滚 preserves
6
- * 3. 流式 SHA256 校验 - 不把 250MB 文件读入内存
7
- * 4. 兼容版本白名单 - 不匹配则跳过升级
8
- * 5. QQ 通知统一加 5s 超时 - 永远不阻塞主流程
9
- * 6. Rollback marker 落盘 - 跨进程 / 跨重启可恢复
10
- */
11
- import { createFileSink, createLogger, createStdoutSink } from "@sfmc-bds/sdk/logs";
12
- import cliProgress from "cli-progress";
13
- import JSZip from "jszip";
1
+ import {
2
+ bindByteProgressToBar,
3
+ createNodeServiceLogger,
4
+ createTerminalProgress
5
+ } from "@sfmc-bds/sdk/logs";
14
6
  import fs from "node:fs";
15
7
  import os from "node:os";
16
8
  import path from "node:path";
@@ -18,425 +10,393 @@ import { createBdsManager } from "./bds-manager.js";
18
10
  import { CHANGELOG_BASE, fetchChangelog } from "./changelog.js";
19
11
  import { copyDirSync, emptyDirSync, hashFileAsync, rmSafe } from "./fsx.js";
20
12
  import { httpDownload } from "./http.js";
13
+ import { isMainModule } from "./is-main.js";
21
14
  import { loadConfig, LOG_PATH, resolvePaths } from "./paths.js";
22
15
  import { sendText, sendWithImage } from "./qqutil.js";
23
- import { clearRollbackMarker, getDirSize, rollbackFromBackup, verifyBdsInstall, writeRollbackMarker, } from "./rollback.js";
16
+ import {
17
+ clearRollbackMarker,
18
+ getDirSize,
19
+ rollbackFromBackup,
20
+ verifyBdsInstall,
21
+ writeRollbackMarker
22
+ } from "./rollback.js";
23
+ import { ensureEmitServerTelemetry } from "./server-properties.js";
24
24
  import { clearTaskbarProgress, isTaskbarSupported, setTaskbarProgress } from "./taskbar.js";
25
- import { buildDownloadUrls, fetchVersionDetails, getVersionInfo, isVersionCompatible, verifyFileHash, } from "./upstream.js";
25
+ import { emitUpdateResult } from "./update-result.js";
26
+ import {
27
+ buildDownloadUrls,
28
+ fetchVersionDetails,
29
+ getVersionInfo,
30
+ isVersionCompatible,
31
+ verifyFileHash
32
+ } from "./upstream.js";
26
33
  import { compareVersions, getCurrentVersionAsync, getCurrentVersionSync, saveVersionCache } from "./version.js";
27
- // 独立入口:source = "updater",与 bds-manager "bds-tools" 区分
28
- const updaterFileSink = createFileSink(LOG_PATH);
29
- const log = createLogger({ source: "updater", sinks: [createStdoutSink({ bare: true }), updaterFileSink] });
30
- const closeLog = () => updaterFileSink.close();
34
+ import { extractZipFileToDir } from "./zipx.js";
35
+ const log = createNodeServiceLogger({ source: "updater", logPath: LOG_PATH });
36
+ const closeLog = () => log.close();
31
37
  function parseArgs(argv) {
32
- const out = {};
33
- for (const a of argv) {
34
- const m = a.match(/^--([\w-]+)(?:=(.+))?$/);
35
- if (m)
36
- out[m[1]] = m[2] || true;
37
- }
38
- return out;
38
+ const out = {};
39
+ for (const a of argv) {
40
+ const m = a.match(/^--([\w-]+)(?:=(.+))?$/);
41
+ if (m) out[m[1]] = m[2] || true;
42
+ }
43
+ return out;
39
44
  }
40
45
  function buildContext() {
41
- const cfg = loadConfig();
42
- const args = parseArgs(process.argv.slice(2));
43
- const { bds_path, backup_dir, preserve } = resolvePaths(cfg);
44
- const channel = String(args["channel"] || cfg.channel || "release");
45
- return {
46
- cfg,
47
- channel,
48
- checkOnly: !!args["check-only"],
49
- force: !!args["force"],
50
- bdsPath: bds_path,
51
- backupDir: backup_dir,
52
- preserve,
53
- autoRestart: !args["no-start"] && cfg.auto_restart !== false,
54
- qqNotify: !!cfg.qq_notify,
55
- };
46
+ const cfg = loadConfig();
47
+ const args = parseArgs(process.argv.slice(2));
48
+ const { bds_path, backup_dir, preserve } = resolvePaths(cfg);
49
+ const channel = String(args["channel"] || cfg.channel || "release");
50
+ return {
51
+ cfg,
52
+ channel,
53
+ checkOnly: !!args["check-only"],
54
+ force: !!args["force"],
55
+ bdsPath: bds_path,
56
+ backupDir: backup_dir,
57
+ preserve,
58
+ autoRestart: !args["no-start"] && cfg.auto_restart !== false,
59
+ qqNotify: !!cfg.qq_notify
60
+ };
56
61
  }
57
62
  async function doBackup(bdsPath, backupDir, preserve) {
58
- const dateStr = new Date().toISOString().slice(0, 10);
59
- const dest = path.join(backupDir, dateStr);
60
- fs.mkdirSync(dest, { recursive: true });
61
- let anyCopied = false;
62
- for (const item of preserve) {
63
- const src = path.join(bdsPath, item);
64
- const target = path.join(dest, item);
65
- if (!fs.existsSync(src))
66
- continue;
67
- fs.mkdirSync(path.dirname(target), { recursive: true });
68
- if (fs.statSync(src).isDirectory())
69
- copyDirSync(src, target);
70
- else
71
- fs.copyFileSync(src, target);
72
- anyCopied = true;
73
- log.info(`已备份: ${item}`);
74
- }
75
- // 即使全部 preserve 都跳过,也让目录存在以便记录时间戳
76
- if (!anyCopied) {
77
- log.info(`没有需要备份的文件 (preserve 列表为空或全部缺失)`);
78
- }
79
- return { path: dest, size: getDirSize(dest) };
63
+ const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
64
+ const dest = path.join(backupDir, dateStr);
65
+ fs.mkdirSync(dest, { recursive: true });
66
+ let anyCopied = false;
67
+ for (const item of preserve) {
68
+ const src = path.join(bdsPath, item);
69
+ const target = path.join(dest, item);
70
+ if (!fs.existsSync(src)) continue;
71
+ fs.mkdirSync(path.dirname(target), { recursive: true });
72
+ if (fs.statSync(src).isDirectory()) copyDirSync(src, target);
73
+ else fs.copyFileSync(src, target);
74
+ anyCopied = true;
75
+ log.info(`\u5DF2\u5907\u4EFD: ${item}`);
76
+ }
77
+ if (!anyCopied) {
78
+ log.info(`\u6CA1\u6709\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6 (preserve \u5217\u8868\u4E3A\u7A7A\u6216\u5168\u90E8\u7F3A\u5931)`);
79
+ }
80
+ return { path: dest, size: getDirSize(dest) };
80
81
  }
81
- /** 异步恢复 preserves 从备份到 BDS 路径 */
82
82
  async function restorePreserves(bdsPath, backupDir, preserve) {
83
- for (const item of preserve) {
84
- const src = path.join(backupDir, item);
85
- const dest = path.join(bdsPath, item);
86
- if (!fs.existsSync(src))
87
- continue;
88
- try {
89
- if (fs.statSync(src).isDirectory()) {
90
- if (fs.existsSync(dest))
91
- emptyDirSync(dest);
92
- copyDirSync(src, dest);
93
- }
94
- else {
95
- fs.mkdirSync(path.dirname(dest), { recursive: true });
96
- fs.copyFileSync(src, dest);
97
- }
98
- log.info(`已恢复: ${item}`);
99
- }
100
- catch (e) {
101
- log.warn(`恢复失败 ${item}: ${e.message}`);
102
- }
83
+ for (const item of preserve) {
84
+ const src = path.join(backupDir, item);
85
+ const dest = path.join(bdsPath, item);
86
+ if (!fs.existsSync(src)) continue;
87
+ try {
88
+ if (fs.statSync(src).isDirectory()) {
89
+ if (fs.existsSync(dest)) emptyDirSync(dest);
90
+ copyDirSync(src, dest);
91
+ } else {
92
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
93
+ fs.copyFileSync(src, dest);
94
+ }
95
+ log.info(`\u5DF2\u6062\u590D: ${item}`);
96
+ } catch (e) {
97
+ log.warn(`\u6062\u590D\u5931\u8D25 ${item}: ${e.message}`);
103
98
  }
99
+ }
104
100
  }
105
- /** 把 srcDir 下的内容移动到 destDir (覆盖) */
106
101
  async function extractZipToBds(zipPath, destDir) {
107
- const data = await fs.promises.readFile(zipPath);
108
- const zip = await JSZip.loadAsync(data);
109
- // 抽出到临时目录,避免旧内容干扰
110
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bds-extract-"));
111
- try {
112
- const entries = Object.values(zip.files);
113
- for (const e of entries) {
114
- const out = path.join(tmpDir, e.name);
115
- if (e.dir) {
116
- fs.mkdirSync(out, { recursive: true });
117
- continue;
118
- }
119
- fs.mkdirSync(path.dirname(out), { recursive: true });
120
- const buf = await e.async("nodebuffer");
121
- fs.writeFileSync(out, buf);
122
- }
123
- // 把临时目录里的内容复制到 BDS 路径
124
- for (const entry of fs.readdirSync(tmpDir)) {
125
- const srcPath = path.join(tmpDir, entry);
126
- const destPath = path.join(destDir, entry);
127
- if (fs.statSync(srcPath).isDirectory()) {
128
- if (fs.existsSync(destPath))
129
- emptyDirSync(destPath);
130
- copyDirSync(srcPath, destPath);
131
- }
132
- else {
133
- fs.copyFileSync(srcPath, destPath);
134
- }
135
- }
136
- }
137
- finally {
138
- rmSafe(tmpDir);
102
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bds-extract-"));
103
+ try {
104
+ await extractZipFileToDir(zipPath, tmpDir);
105
+ for (const entry of fs.readdirSync(tmpDir)) {
106
+ const srcPath = path.join(tmpDir, entry);
107
+ const destPath = path.join(destDir, entry);
108
+ if (fs.statSync(srcPath).isDirectory()) {
109
+ if (fs.existsSync(destPath)) emptyDirSync(destPath);
110
+ copyDirSync(srcPath, destPath);
111
+ } else {
112
+ fs.copyFileSync(srcPath, destPath);
113
+ }
139
114
  }
115
+ } finally {
116
+ rmSafe(tmpDir);
117
+ }
140
118
  }
141
- export async function runUpdate() {
142
- const startTime = Date.now();
143
- const ctx = buildContext();
144
- const { cfg, channel, checkOnly, force, bdsPath, backupDir, preserve, autoRestart, qqNotify } = ctx;
145
- log.info(`===== 开始检查更新 (${channel}) =====`);
146
- fs.mkdirSync(bdsPath, { recursive: true });
147
- fs.mkdirSync(backupDir, { recursive: true });
148
- // 1. 当前版本
149
- const exePath = path.join(bdsPath, "bedrock_server.exe");
150
- const currentVer = await getCurrentVersionAsync(exePath).catch(() => getCurrentVersionSync(exePath));
151
- log.info(`当前版本: ${currentVer}`);
152
- // 2. 获取最新版本
153
- let latestVer;
154
- try {
155
- const info = await getVersionInfo(cfg, channel);
156
- latestVer = info.version;
157
- log.info(`最新版本: ${latestVer}`);
158
- const cmp = compareVersions(latestVer, currentVer);
159
- if (cmp <= 0 && !force) {
160
- log.info("已是最新版本,无需更新");
161
- if (qqNotify) {
162
- await sendText(`✅ BDS 已是最新版本\n\n当前: ${currentVer}\n最新: ${latestVer}`);
163
- }
164
- console.log(`CURRENT=${currentVer}`);
165
- console.log(`LATEST=${latestVer}`);
166
- return 0;
167
- }
119
+ async function runUpdate() {
120
+ const startTime = Date.now();
121
+ const ctx = buildContext();
122
+ const { cfg, channel, checkOnly, force, bdsPath, backupDir, preserve, autoRestart, qqNotify } = ctx;
123
+ log.info(`===== \u5F00\u59CB\u68C0\u67E5\u66F4\u65B0 (${channel}) =====`);
124
+ fs.mkdirSync(bdsPath, { recursive: true });
125
+ fs.mkdirSync(backupDir, { recursive: true });
126
+ const exePath = path.join(bdsPath, "bedrock_server.exe");
127
+ const currentVer = await getCurrentVersionAsync(exePath).catch(() => getCurrentVersionSync(exePath));
128
+ log.info(`\u5F53\u524D\u7248\u672C: ${currentVer}`);
129
+ let latestVer;
130
+ try {
131
+ const info = await getVersionInfo(cfg, channel);
132
+ latestVer = info.version;
133
+ log.info(`\u6700\u65B0\u7248\u672C: ${latestVer}`);
134
+ const cmp = compareVersions(latestVer, currentVer);
135
+ if (cmp <= 0 && !force) {
136
+ log.info("\u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF0C\u65E0\u9700\u66F4\u65B0");
137
+ if (qqNotify) {
138
+ await sendText(`\u2705 BDS \u5DF2\u662F\u6700\u65B0\u7248\u672C
139
+
140
+ \u5F53\u524D: ${currentVer}
141
+ \u6700\u65B0: ${latestVer}`);
142
+ }
143
+ console.log(`CURRENT=${currentVer}`);
144
+ console.log(`LATEST=${latestVer}`);
145
+ emitUpdateResult("uptodate");
146
+ return 0;
168
147
  }
169
- catch (e) {
170
- log.error(`获取版本信息失败: ${e.message}`);
171
- await sendText(`❌ BDS 更新失败\n\n无法获取版本信息: ${e.message}`);
172
- return 1;
173
- }
174
- // 兼容性检查
175
- if (!force && !isVersionCompatible(cfg, latestVer)) {
176
- log.warn(`${latestVer} 不在兼容版本白名单中,跳过升级`);
177
- if (qqNotify) {
178
- await sendText(`⚠️ BDS ${latestVer} 不在兼容性白名单,已跳过升级。请人工确认。`);
179
- }
180
- return 2;
181
- }
182
- // 3. 取下载链接
183
- let downloadUrls;
184
- let hash;
185
- try {
186
- const details = await fetchVersionDetails(cfg, channel, latestVer);
187
- downloadUrls = buildDownloadUrls(cfg, channel, latestVer, details);
188
- hash = { sha1: details.sha1, sha256: details.sha256 };
189
- }
190
- catch (e) {
191
- log.error(`获取下载详情失败: ${e.message}`);
192
- await sendText(`❌ BDS 更新失败\n\n下载详情获取失败: ${e.message}`);
193
- return 1;
194
- }
195
- if (checkOnly) {
196
- console.log(`CURRENT=${currentVer}`);
197
- console.log(`LATEST=${latestVer}`);
198
- console.log(`CHANNEL=${channel}`);
199
- console.log(`URLS=${downloadUrls.length}`);
200
- return 0;
201
- }
202
- log.info(`发现新版本: ${currentVer} → ${latestVer}`);
203
- // 4. QQ 预告
148
+ } catch (e) {
149
+ log.error(`\u83B7\u53D6\u7248\u672C\u4FE1\u606F\u5931\u8D25: ${e.message}`);
150
+ await sendText(`\u274C BDS \u66F4\u65B0\u5931\u8D25
151
+
152
+ \u65E0\u6CD5\u83B7\u53D6\u7248\u672C\u4FE1\u606F: ${e.message}`);
153
+ return 1;
154
+ }
155
+ if (!force && !isVersionCompatible(cfg, latestVer)) {
156
+ log.warn(`${latestVer} \u4E0D\u5728\u517C\u5BB9\u7248\u672C\u767D\u540D\u5355\u4E2D\uFF0C\u8DF3\u8FC7\u5347\u7EA7`);
204
157
  if (qqNotify) {
205
- await sendText(`𝐌𝐢𝐧𝐞𝐜𝐫𝐚𝐟𝐭 𝐁𝐃𝐒 更新通知\n\n` +
206
- `检测到新版本!\n\n` +
207
- `当前: ${currentVer}\n` +
208
- `最新: ${latestVer}\n` +
209
- `频道: ${channel === "preview" ? "预览版" : "正式版"}\n\n` +
210
- `服务器即将开始更新~ 请耐心等待^(* ̄(oo) ̄)^`);
211
- const cl = await fetchChangelog(channel);
212
- if (cl) {
213
- const text = `📋 更新内容概要\n\n${cl.text.slice(0, 1500)}\n\n完整日志: ${CHANGELOG_BASE}`;
214
- if (cl.imageBase64)
215
- await sendWithImage(text, cl.imageBase64);
216
- else
217
- await sendText(text);
218
- }
219
- else {
220
- await sendText(`📋 更新日志: ${CHANGELOG_BASE}`);
221
- }
222
- }
223
- // 5. 备份
224
- let backupInfo;
225
- try {
226
- backupInfo = await doBackup(bdsPath, backupDir, preserve);
227
- log.info(`备份完成: ${backupInfo.path} (${(backupInfo.size / 1024 / 1024).toFixed(1)} MB)`);
228
- if (qqNotify) {
229
- await sendText(`✅ 备份完成\n\n大小: ${(backupInfo.size / 1024 / 1024).toFixed(1)} MB\n位置: ${backupInfo.path}`);
230
- }
158
+ await sendText(`\u26A0\uFE0F BDS ${latestVer} \u4E0D\u5728\u517C\u5BB9\u6027\u767D\u540D\u5355\uFF0C\u5DF2\u8DF3\u8FC7\u5347\u7EA7\u3002\u8BF7\u4EBA\u5DE5\u786E\u8BA4\u3002`);
231
159
  }
232
- catch (e) {
233
- log.error(`备份失败: ${e.message}`);
234
- await sendText(`❌ BDS 更新失败\n\n备份失败: ${e.message}\n操作已中止`);
235
- return 1;
160
+ emitUpdateResult("skipped");
161
+ return 2;
162
+ }
163
+ let downloadUrls;
164
+ let hash;
165
+ try {
166
+ const details = await fetchVersionDetails(cfg, channel, latestVer);
167
+ downloadUrls = buildDownloadUrls(cfg, channel, latestVer, details);
168
+ hash = { sha1: details.sha1, sha256: details.sha256 };
169
+ } catch (e) {
170
+ log.error(`\u83B7\u53D6\u4E0B\u8F7D\u8BE6\u60C5\u5931\u8D25: ${e.message}`);
171
+ await sendText(`\u274C BDS \u66F4\u65B0\u5931\u8D25
172
+
173
+ \u4E0B\u8F7D\u8BE6\u60C5\u83B7\u53D6\u5931\u8D25: ${e.message}`);
174
+ return 1;
175
+ }
176
+ if (checkOnly) {
177
+ console.log(`CURRENT=${currentVer}`);
178
+ console.log(`LATEST=${latestVer}`);
179
+ console.log(`CHANNEL=${channel}`);
180
+ console.log(`URLS=${downloadUrls.length}`);
181
+ emitUpdateResult("check-only");
182
+ return 0;
183
+ }
184
+ log.info(`\u53D1\u73B0\u65B0\u7248\u672C: ${currentVer} \u2192 ${latestVer}`);
185
+ if (qqNotify) {
186
+ await sendText(
187
+ `\u{1D40C}\u{1D422}\u{1D427}\u{1D41E}\u{1D41C}\u{1D42B}\u{1D41A}\u{1D41F}\u{1D42D} \u{1D401}\u{1D403}\u{1D412} \u66F4\u65B0\u901A\u77E5
188
+
189
+ \u68C0\u6D4B\u5230\u65B0\u7248\u672C\uFF01
190
+
191
+ \u5F53\u524D: ${currentVer}
192
+ \u6700\u65B0: ${latestVer}
193
+ \u9891\u9053: ${channel === "preview" ? "\u9884\u89C8\u7248" : "\u6B63\u5F0F\u7248"}
194
+
195
+ \u670D\u52A1\u5668\u5373\u5C06\u5F00\u59CB\u66F4\u65B0~ \u8BF7\u8010\u5FC3\u7B49\u5F85^(*\uFFE3(oo)\uFFE3)^`
196
+ );
197
+ const cl = await fetchChangelog(channel);
198
+ if (cl) {
199
+ const text = `\u{1F4CB} \u66F4\u65B0\u5185\u5BB9\u6982\u8981
200
+
201
+ ${cl.text.slice(0, 1500)}
202
+
203
+ \u5B8C\u6574\u65E5\u5FD7: ${CHANGELOG_BASE}`;
204
+ if (cl.imageBase64) await sendWithImage(text, cl.imageBase64);
205
+ else await sendText(text);
206
+ } else {
207
+ await sendText(`\u{1F4CB} \u66F4\u65B0\u65E5\u5FD7: ${CHANGELOG_BASE}`);
236
208
  }
237
- // 6. 写回滚标记 (跨进程可恢复)
238
- writeRollbackMarker({
239
- timestamp: Date.now(),
240
- bds_path: bdsPath,
241
- backup_dir: backupInfo.path,
242
- preserve,
243
- previous_version: currentVer,
244
- });
245
- // 7. 停服
246
- const bds = createBdsManager({ detached: true });
247
- try {
248
- log.info("停止 BDS 服务...");
249
- await bds.stop();
250
- log.info("BDS 已停止");
209
+ }
210
+ let backupInfo;
211
+ try {
212
+ backupInfo = await doBackup(bdsPath, backupDir, preserve);
213
+ log.info(`\u5907\u4EFD\u5B8C\u6210: ${backupInfo.path} (${(backupInfo.size / 1024 / 1024).toFixed(1)} MB)`);
214
+ if (qqNotify) {
215
+ await sendText(`\u2705 \u5907\u4EFD\u5B8C\u6210
216
+
217
+ \u5927\u5C0F: ${(backupInfo.size / 1024 / 1024).toFixed(1)} MB
218
+ \u4F4D\u7F6E: ${backupInfo.path}`);
251
219
  }
252
- catch (e) {
253
- log.warn(`BDS 停止异常: ${e.message}`);
220
+ } catch (e) {
221
+ log.error(`\u5907\u4EFD\u5931\u8D25: ${e.message}`);
222
+ await sendText(`\u274C BDS \u66F4\u65B0\u5931\u8D25
223
+
224
+ \u5907\u4EFD\u5931\u8D25: ${e.message}
225
+ \u64CD\u4F5C\u5DF2\u4E2D\u6B62`);
226
+ return 1;
227
+ }
228
+ writeRollbackMarker({
229
+ timestamp: Date.now(),
230
+ bds_path: bdsPath,
231
+ backup_dir: backupInfo.path,
232
+ preserve,
233
+ previous_version: currentVer
234
+ });
235
+ const bds = createBdsManager({ detached: true });
236
+ try {
237
+ log.info("\u505C\u6B62 BDS \u670D\u52A1...");
238
+ await bds.stop();
239
+ log.info("BDS \u5DF2\u505C\u6B62");
240
+ } catch (e) {
241
+ log.warn(`BDS \u505C\u6B62\u5F02\u5E38: ${e.message}`);
242
+ }
243
+ const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), "bds-update-"));
244
+ const zipPath = path.join(stagingDir, `bedrock-server-${latestVer}.zip`);
245
+ let downloaded = false;
246
+ try {
247
+ log.info(`\u5F00\u59CB\u4E0B\u8F7D ${latestVer}...`);
248
+ if (qqNotify) await sendText(`\u{1F4E5} \u6B63\u5728\u4E0B\u8F7D BDS ${latestVer}...`);
249
+ let lastErr = null;
250
+ const downloadTimeoutMs = (cfg.download_timeout ?? 120) * 1e3;
251
+ if (isTaskbarSupported() && process.stdout.isTTY) {
252
+ log.info("\u68C0\u6D4B\u5230 Windows Terminal,\u4EFB\u52A1\u680F\u8FDB\u5EA6\u5DF2\u542F\u7528 (OSC 9;4)");
254
253
  }
255
- // 8. 下载 (全部失败则回滚)
256
- // mkdtempSync 创建临时 staging 目录 - 避免污染 SCRIPT_DIR
257
- const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), "bds-update-"));
258
- const zipPath = path.join(stagingDir, `bedrock-server-${latestVer}.zip`);
259
- let downloaded = false;
260
- try {
261
- log.info(`开始下载 ${latestVer}...`);
262
- if (qqNotify)
263
- await sendText(`📥 正在下载 BDS ${latestVer}...`);
264
- let lastErr = null;
265
- const downloadTimeoutMs = (cfg.download_timeout ?? 120) * 1000;
266
- const progressBar = new cliProgress.SingleBar({
267
- format: "下载进度 | {bar} | {percentage}% | {value}/{total} MB | 速度: {speed}",
268
- barCompleteChar: "\u2588",
269
- barIncompleteChar: "\u2591",
270
- hideCursor: true,
271
- clearOnComplete: false, // 保留进度条
272
- }, cliProgress.Presets.shades_classic);
273
- let lastTime = Date.now();
274
- let lastLoaded = 0;
275
- let barStarted = false; // 标记是否已启动
276
- if (isTaskbarSupported()) {
277
- log.info("检测到 Windows Terminal,任务栏进度已启用 (OSC 9;4)");
278
- }
279
- for (const url of downloadUrls) {
280
- try {
281
- await httpDownload(url, zipPath, {
282
- totalTimeoutMs: Math.max(downloadTimeoutMs, 600_000), // 不少于 10 分钟
283
- onProgress: (dl, total) => {
284
- // 第一次触发时启动进度条
285
- if (!barStarted) {
286
- progressBar.start(total, 0, { speed: "0 KB/s" });
287
- barStarted = true;
288
- setTaskbarProgress(0); // 启动任务栏进度(0%)
289
- }
290
- const now = Date.now();
291
- const timeDelta = (now - lastTime) / 1000; // 秒
292
- const bytesDelta = dl - lastLoaded;
293
- const speed = timeDelta > 0 ? bytesDelta / timeDelta : 0;
294
- const speedStr = speed > 1024 * 1024
295
- ? `${(speed / 1024 / 1024).toFixed(1)} MB/s`
296
- : speed > 1024
297
- ? `${(speed / 1024).toFixed(1)} KB/s`
298
- : `${speed.toFixed(0)} B/s`;
299
- progressBar.update(dl, { speed: speedStr });
300
- // 同步任务栏进度(0-100)。httpDownload 内部已做 100ms 节流,
301
- // 这里再 set 一次,getSnapshot 比较,相同值直接跳过
302
- setTaskbarProgress((dl / total) * 100);
303
- lastLoaded = dl;
304
- lastTime = now;
305
- },
306
- });
307
- progressBar.stop();
308
- // 下载完成,任务栏收到 100% 绿条后清掉
309
- setTaskbarProgress(100);
310
- clearTaskbarProgress();
311
- downloaded = true;
312
- lastErr = null;
313
- break;
314
- }
315
- catch (e) {
316
- if (barStarted)
317
- progressBar.stop();
318
- // 下载失败,任务栏亮红
319
- setTaskbarProgress(100, "error");
320
- clearTaskbarProgress();
321
- lastErr = e;
322
- log.warn(`${url} 不可用: ${lastErr.message},尝试备用地址...`);
323
- }
254
+ for (const url of downloadUrls) {
255
+ const progressBar = createTerminalProgress({
256
+ stream: process.stderr,
257
+ logger: (msg) => log.info(msg.startsWith("\u8FDB\u5EA6") ? `\u4E0B\u8F7D${msg.slice(2)}` : msg),
258
+ format: "\u4E0B\u8F7D\u8FDB\u5EA6 | {bar} | {percentage}% | {value}/{total} MB | \u901F\u5EA6: {speed}"
259
+ });
260
+ const onByteProgress = bindByteProgressToBar(progressBar, {
261
+ speedSampleMs: 0,
262
+ onProgress: (dl, total) => {
263
+ const pct = total > 0 ? dl / total * 100 : 0;
264
+ setTaskbarProgress(pct);
324
265
  }
325
- if (!downloaded && lastErr)
326
- throw lastErr;
327
- const zipSizeMB = (fs.statSync(zipPath).size / 1024 / 1024).toFixed(1);
328
- log.info(`下载完成 (${zipSizeMB} MB)`);
329
- // 流式哈希校验
330
- if (hash.sha1 || hash.sha256) {
331
- const algo = hash.sha256 ? "SHA256" : "SHA1";
332
- log.info(`校验文件完整性 (${algo})...`);
333
- const ok = await verifyFileHash(zipPath, hash.sha1, hash.sha256);
334
- if (!ok)
335
- throw new Error(`${algo} 校验不通过,文件可能损坏`);
336
- log.info(`${algo} 校验通过`);
337
- }
338
- }
339
- catch (e) {
340
- log.error(`下载失败: ${e.message}`);
341
- await sendText(`❌ BDS 更新失败\n\n下载失败: ${e.message}\nBDS 已停止,请手动恢复`);
342
- // 自动回滚
343
- rollbackFromBackup({
344
- timestamp: Date.now(),
345
- bds_path: bdsPath,
346
- backup_dir: backupInfo.path,
347
- preserve,
348
- previous_version: currentVer,
266
+ });
267
+ try {
268
+ setTaskbarProgress(0);
269
+ await httpDownload(url, zipPath, {
270
+ totalTimeoutMs: Math.max(downloadTimeoutMs, 6e5),
271
+ // 不少于 10 分钟
272
+ onProgress: onByteProgress
349
273
  });
350
- rmSafe(stagingDir);
351
- return 1;
352
- }
353
- // 9. 解压 + 部署
354
- try {
355
- log.info("解压中...");
356
- // 先清空 BDS 目录
357
- emptyDirSync(bdsPath);
358
- await extractZipToBds(zipPath, bdsPath);
359
- log.info("解压完成");
360
- }
361
- catch (e) {
362
- log.error(`解压失败: ${e.message}`);
363
- await sendText(`❌ BDS 更新失败\n\n解压失败: ${e.message}\nBDS 已停止,请手动恢复`);
364
- // 回滚: 清空 + 从备份恢复 preserves
365
- emptyDirSync(bdsPath);
366
- await restorePreserves(bdsPath, backupInfo.path, preserve);
367
- rmSafe(stagingDir);
368
- return 1;
369
- }
370
- finally {
371
- rmSafe(zipPath); // 清掉 staging zip
274
+ if (progressBar.active) progressBar.stop();
275
+ setTaskbarProgress(100);
276
+ clearTaskbarProgress();
277
+ downloaded = true;
278
+ lastErr = null;
279
+ break;
280
+ } catch (e) {
281
+ if (progressBar.active) progressBar.stop();
282
+ setTaskbarProgress(100, "error");
283
+ clearTaskbarProgress();
284
+ lastErr = e;
285
+ log.warn(`${url} \u4E0D\u53EF\u7528: ${lastErr.message}\uFF0C\u5C1D\u8BD5\u5907\u7528\u5730\u5740...`);
286
+ }
372
287
  }
373
- // 10. 完整性检查 (避免解压出空/缺文件的病态包)
374
- const verify = verifyBdsInstall(bdsPath, [
375
- "bedrock_server.exe",
376
- /*"bedrock_server_symbols.debug",*/ "permissions.json",
377
- ]);
378
- if (!verify.ok) {
379
- log.error(`解压后的 BDS 不完整,缺失文件: ${verify.missing.join(", ")}`);
380
- await sendText(`❌ BDS 更新失败\n\n部署后缺失关键文件: ${verify.missing.join(", ")}`);
381
- emptyDirSync(bdsPath);
382
- await restorePreserves(bdsPath, backupInfo.path, preserve);
383
- rmSafe(stagingDir);
384
- return 1;
288
+ if (!downloaded && lastErr) throw lastErr;
289
+ const zipSizeMB = (fs.statSync(zipPath).size / 1024 / 1024).toFixed(1);
290
+ log.info(`\u4E0B\u8F7D\u5B8C\u6210 (${zipSizeMB} MB)`);
291
+ if (hash.sha1 || hash.sha256) {
292
+ const algo = hash.sha256 ? "SHA256" : "SHA1";
293
+ log.info(`\u6821\u9A8C\u6587\u4EF6\u5B8C\u6574\u6027 (${algo})...`);
294
+ const ok = await verifyFileHash(zipPath, hash.sha1, hash.sha256);
295
+ if (!ok) throw new Error(`${algo} \u6821\u9A8C\u4E0D\u901A\u8FC7\uFF0C\u6587\u4EF6\u53EF\u80FD\u635F\u574F`);
296
+ log.info(`${algo} \u6821\u9A8C\u901A\u8FC7`);
385
297
  }
386
- // 11. 恢复 preserves (zip 解压时这些目录可能被覆盖)
298
+ } catch (e) {
299
+ log.error(`\u4E0B\u8F7D\u5931\u8D25: ${e.message}`);
300
+ await sendText(`\u274C BDS \u66F4\u65B0\u5931\u8D25
301
+
302
+ \u4E0B\u8F7D\u5931\u8D25: ${e.message}
303
+ BDS \u5DF2\u505C\u6B62\uFF0C\u8BF7\u624B\u52A8\u6062\u590D`);
304
+ rollbackFromBackup({
305
+ timestamp: Date.now(),
306
+ bds_path: bdsPath,
307
+ backup_dir: backupInfo.path,
308
+ preserve,
309
+ previous_version: currentVer
310
+ });
311
+ rmSafe(stagingDir);
312
+ return 1;
313
+ }
314
+ try {
315
+ log.info("\u89E3\u538B\u4E2D...");
316
+ emptyDirSync(bdsPath);
317
+ await extractZipToBds(zipPath, bdsPath);
318
+ log.info("\u89E3\u538B\u5B8C\u6210");
319
+ } catch (e) {
320
+ log.error(`\u89E3\u538B\u5931\u8D25: ${e.message}`);
321
+ await sendText(`\u274C BDS \u66F4\u65B0\u5931\u8D25
322
+
323
+ \u89E3\u538B\u5931\u8D25: ${e.message}
324
+ BDS \u5DF2\u505C\u6B62\uFF0C\u8BF7\u624B\u52A8\u6062\u590D`);
325
+ emptyDirSync(bdsPath);
387
326
  await restorePreserves(bdsPath, backupInfo.path, preserve);
388
- // 12. 写入版本缓存
389
- const newExeHash = await hashFileAsync(exePath, "sha256").catch(() => "");
390
- if (newExeHash) {
391
- saveVersionCache(latestVer, newExeHash);
392
- log.info(`已记录版本 ${latestVer} 的 SHA256`);
393
- }
394
- // 13. 启动 BDS
395
- if (autoRestart) {
396
- try {
397
- log.info("启动 BDS...");
398
- await bds.start();
399
- }
400
- catch (e) {
401
- log.error(`启动失败: ${e.message}`);
402
- await sendText(`❌ BDS 更新失败\n\n启动失败: ${e.message}\n请手动启动 BDS`);
403
- // 注意: 此时 BDS 已经部署完成,启动失败 → 启动问题,不做文件级回滚
404
- rmSafe(stagingDir);
405
- clearRollbackMarker();
406
- return 1;
407
- }
408
- }
409
327
  rmSafe(stagingDir);
410
- clearRollbackMarker();
411
- // 14. QQ 通知结果
412
- const durationMs = Date.now() - startTime;
413
- if (qqNotify) {
414
- await sendText(`✅ BDS 更新完成\n\n` +
415
- `${currentVer} → ${latestVer}\n` +
416
- `耗时: ${(durationMs / 1000).toFixed(1)}s\n` +
417
- `${autoRestart ? "服务器已重新启动" : "请手动重启服务器"}`);
328
+ return 1;
329
+ } finally {
330
+ rmSafe(zipPath);
331
+ }
332
+ const verify = verifyBdsInstall(bdsPath, [
333
+ "bedrock_server.exe",
334
+ /*"bedrock_server_symbols.debug",*/
335
+ "permissions.json"
336
+ ]);
337
+ if (!verify.ok) {
338
+ log.error(`\u89E3\u538B\u540E\u7684 BDS \u4E0D\u5B8C\u6574\uFF0C\u7F3A\u5931\u6587\u4EF6: ${verify.missing.join(", ")}`);
339
+ await sendText(`\u274C BDS \u66F4\u65B0\u5931\u8D25
340
+
341
+ \u90E8\u7F72\u540E\u7F3A\u5931\u5173\u952E\u6587\u4EF6: ${verify.missing.join(", ")}`);
342
+ emptyDirSync(bdsPath);
343
+ await restorePreserves(bdsPath, backupInfo.path, preserve);
344
+ rmSafe(stagingDir);
345
+ return 1;
346
+ }
347
+ await restorePreserves(bdsPath, backupInfo.path, preserve);
348
+ ensureEmitServerTelemetry(bdsPath, log);
349
+ const newExeHash = await hashFileAsync(exePath, "sha256").catch(() => "");
350
+ if (newExeHash) {
351
+ saveVersionCache(latestVer, newExeHash);
352
+ log.info(`\u5DF2\u8BB0\u5F55\u7248\u672C ${latestVer} \u7684 SHA256`);
353
+ }
354
+ if (autoRestart) {
355
+ try {
356
+ log.info("\u542F\u52A8 BDS...");
357
+ await bds.start();
358
+ } catch (e) {
359
+ log.error(`\u542F\u52A8\u5931\u8D25: ${e.message}`);
360
+ await sendText(`\u274C BDS \u66F4\u65B0\u5931\u8D25
361
+
362
+ \u542F\u52A8\u5931\u8D25: ${e.message}
363
+ \u8BF7\u624B\u52A8\u542F\u52A8 BDS`);
364
+ rmSafe(stagingDir);
365
+ clearRollbackMarker();
366
+ return 1;
418
367
  }
419
- log.info(`===== 更新完成 (${(durationMs / 1000).toFixed(1)}s) =====`);
420
- return 0;
368
+ }
369
+ rmSafe(stagingDir);
370
+ clearRollbackMarker();
371
+ const durationMs = Date.now() - startTime;
372
+ if (qqNotify) {
373
+ await sendText(
374
+ `\u2705 BDS \u66F4\u65B0\u5B8C\u6210
375
+
376
+ ${currentVer} \u2192 ${latestVer}
377
+ \u8017\u65F6: ${(durationMs / 1e3).toFixed(1)}s
378
+ ${autoRestart ? "\u670D\u52A1\u5668\u5DF2\u91CD\u65B0\u542F\u52A8" : "\u8BF7\u624B\u52A8\u91CD\u542F\u670D\u52A1\u5668"}`
379
+ );
380
+ }
381
+ log.info(`===== \u66F4\u65B0\u5B8C\u6210 (${(durationMs / 1e3).toFixed(1)}s) =====`);
382
+ emitUpdateResult("deployed");
383
+ return 0;
421
384
  }
422
385
  function isMain() {
423
- if (process.env.SFMC_SERVICE === "update")
424
- return true;
425
- if (require.main === module)
426
- return true;
427
- const entry = process.argv[1] ?? "";
428
- return entry.endsWith("check-update.js") || entry.endsWith("check-update.ts");
386
+ if (process.env.SFMC_SERVICE === "update") return true;
387
+ return isMainModule(import.meta.url);
429
388
  }
430
389
  if (isMain()) {
431
- runUpdate()
432
- .then((code) => {
433
- closeLog();
434
- process.exit(code);
435
- })
436
- .catch((e) => {
437
- log.error(`未捕获错误: ${e.message}`);
438
- closeLog();
439
- process.exit(1);
440
- });
390
+ runUpdate().then((code) => {
391
+ closeLog();
392
+ process.exit(code);
393
+ }).catch((e) => {
394
+ log.error(`\u672A\u6355\u83B7\u9519\u8BEF: ${e.message}`);
395
+ closeLog();
396
+ process.exit(1);
397
+ });
441
398
  }
442
- //# sourceMappingURL=check-update.js.map
399
+ export {
400
+ runUpdate
401
+ };
402
+ //# sourceMappingURL=check-update.js.map