@ttmg/cli 0.3.1-login.5 → 0.3.1-requirePlugin.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/dist/index.js +2314 -892
- package/dist/index.js.map +1 -1
- package/dist/package.json +3 -2
- package/dist/public/assets/{index-sCifET6h.css → index-9wLHKdTg.css} +1 -1
- package/dist/public/assets/index-9wLHKdTg.css.br +0 -0
- package/dist/public/assets/index-BUxLwNCK.js +22 -0
- package/dist/public/assets/index-BUxLwNCK.js.br +0 -0
- package/dist/public/assets/index-BjQHzL55.js +67 -0
- package/dist/public/assets/index-BjQHzL55.js.br +0 -0
- package/dist/public/assets/index-BlM8bGYa.js +22 -0
- package/dist/public/assets/index-BlM8bGYa.js.br +0 -0
- package/dist/public/assets/index-C7atXw2q.css +1 -0
- package/dist/public/assets/index-C7atXw2q.css.br +0 -0
- package/dist/public/assets/index-C9MQNqRT.js +22 -0
- package/dist/public/assets/index-C9MQNqRT.js.br +0 -0
- package/dist/public/assets/index-CkX8so1Z.css +1 -0
- package/dist/public/assets/index-CkX8so1Z.css.br +0 -0
- package/dist/public/assets/index-Cuov3ysP.js +67 -0
- package/dist/public/assets/index-Cuov3ysP.js.br +0 -0
- package/dist/public/assets/{index-YhxvkywD.js → index-D5if59dO.js} +1 -1
- package/dist/public/assets/index-D5if59dO.js.br +0 -0
- package/dist/public/assets/index-DBb1T-qz.js +22 -0
- package/dist/public/assets/index-DBb1T-qz.js.br +0 -0
- package/dist/public/assets/index-DER-DYLd.css +1 -0
- package/dist/public/assets/index-DER-DYLd.css.br +0 -0
- package/dist/public/assets/index-DuaknC25.js +67 -0
- package/dist/public/assets/index-DuaknC25.js.br +0 -0
- package/dist/public/assets/{index-D51pZ7N1.css → index-XHqcH2l3.css} +1 -1
- package/dist/public/assets/index-XHqcH2l3.css.br +0 -0
- package/dist/public/index.html +2 -2
- package/dist/scripts/worker.js +13 -6
- package/package.json +3 -2
- package/CHANGELOG.md +0 -176
- package/dist/public/assets/index-B_lMekya.js +0 -67
- package/dist/public/assets/index-B_lMekya.js.br +0 -0
- package/dist/public/assets/index-D51pZ7N1.css.br +0 -0
- package/dist/public/assets/index-YhxvkywD.js.br +0 -0
- package/dist/public/assets/index-sCifET6h.css.br +0 -0
package/dist/index.js
CHANGED
|
@@ -17,15 +17,20 @@ var qs = require('qs');
|
|
|
17
17
|
var handlebars = require('handlebars');
|
|
18
18
|
var esbuild = require('esbuild');
|
|
19
19
|
var archiver = require('archiver');
|
|
20
|
-
var http = require('http');
|
|
21
|
-
var ttmgPack = require('ttmg-pack');
|
|
22
|
-
var expressStaticGzip = require('express-static-gzip');
|
|
23
|
-
var fileUpload = require('express-fileupload');
|
|
24
20
|
var chokidar = require('chokidar');
|
|
25
21
|
var WebSocket = require('ws');
|
|
26
22
|
var glob = require('glob');
|
|
27
23
|
var got = require('got');
|
|
28
24
|
var FormData$1 = require('form-data');
|
|
25
|
+
var stream = require('stream');
|
|
26
|
+
var ttmgPack = require('ttmg-pack');
|
|
27
|
+
var http = require('http');
|
|
28
|
+
var fs$1 = require('node:fs');
|
|
29
|
+
var path$1 = require('node:path');
|
|
30
|
+
var zlib = require('zlib');
|
|
31
|
+
var promises = require('fs/promises');
|
|
32
|
+
var expressStaticGzip = require('express-static-gzip');
|
|
33
|
+
var fileUpload = require('express-fileupload');
|
|
29
34
|
|
|
30
35
|
function _interopNamespaceDefault(e) {
|
|
31
36
|
var n = Object.create(null);
|
|
@@ -44,6 +49,7 @@ function _interopNamespaceDefault(e) {
|
|
|
44
49
|
return Object.freeze(n);
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
|
|
47
53
|
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
48
54
|
var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
|
|
49
55
|
var glob__namespace = /*#__PURE__*/_interopNamespaceDefault(glob);
|
|
@@ -363,8 +369,13 @@ async function login() {
|
|
|
363
369
|
}
|
|
364
370
|
}
|
|
365
371
|
catch (error) {
|
|
366
|
-
|
|
367
|
-
|
|
372
|
+
// 1. Error Title: Red and bold
|
|
373
|
+
spinner$1.fail(chalk.red.bold('Failed to connect to login service'));
|
|
374
|
+
// 2. Description: Normal text with bold keywords
|
|
375
|
+
console.log(" Detected that the current terminal's " +
|
|
376
|
+
chalk.bold('network proxy settings') +
|
|
377
|
+
' are preventing external network access.', chalk.yellow('Please check your local terminal proxy configuration.'));
|
|
378
|
+
console.log(chalk.yellow('You can follow this doc to modify your terminal network settings:'), chalk.underline.yellow('https://bytedance.larkoffice.com/wiki/ZblJwT0ZNil9jJkS8EgcFlcQnFc'));
|
|
368
379
|
process.exit(1);
|
|
369
380
|
}
|
|
370
381
|
}
|
|
@@ -380,24 +391,88 @@ async function request({ url, method, data, headers, params, }) {
|
|
|
380
391
|
data,
|
|
381
392
|
params,
|
|
382
393
|
headers: {
|
|
383
|
-
...(headers || {}),
|
|
384
394
|
Cookie: cookie,
|
|
395
|
+
...(headers || {}),
|
|
385
396
|
},
|
|
386
397
|
});
|
|
387
398
|
// @ts-ignore
|
|
388
399
|
return {
|
|
389
400
|
data: res.data,
|
|
390
401
|
error: null,
|
|
402
|
+
ctx: {
|
|
403
|
+
logid: res?.headers['x-tt-logid'],
|
|
404
|
+
httpStatusCode: res?.status,
|
|
405
|
+
},
|
|
391
406
|
};
|
|
392
407
|
}
|
|
393
408
|
catch (err) {
|
|
394
409
|
// @ts-ignore
|
|
395
410
|
return {
|
|
396
411
|
data: null,
|
|
412
|
+
ctx: {
|
|
413
|
+
logid: err.response?.headers['x-tt-logid'],
|
|
414
|
+
httpStatusCode: err.response?.status,
|
|
415
|
+
},
|
|
397
416
|
error: err.response?.data,
|
|
398
417
|
};
|
|
399
418
|
}
|
|
400
419
|
}
|
|
420
|
+
async function download(url, filePath) {
|
|
421
|
+
// 清理旧文件
|
|
422
|
+
if (fs.existsSync(filePath)) {
|
|
423
|
+
try {
|
|
424
|
+
fs.unlinkSync(filePath);
|
|
425
|
+
}
|
|
426
|
+
catch { }
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
const res = await axios.get(url, {
|
|
430
|
+
responseType: 'stream',
|
|
431
|
+
// 让非 2xx 进入 catch
|
|
432
|
+
validateStatus: s => s >= 200 && s < 300,
|
|
433
|
+
});
|
|
434
|
+
// 关键:把“流事件”封装为 Promise,并 await 它
|
|
435
|
+
await new Promise((resolve, reject) => {
|
|
436
|
+
const writer = fs.createWriteStream(filePath);
|
|
437
|
+
const onError = (e) => {
|
|
438
|
+
cleanup();
|
|
439
|
+
try {
|
|
440
|
+
if (fs.existsSync(filePath))
|
|
441
|
+
fs.unlinkSync(filePath);
|
|
442
|
+
}
|
|
443
|
+
catch { }
|
|
444
|
+
reject(e);
|
|
445
|
+
};
|
|
446
|
+
const onClose = () => {
|
|
447
|
+
cleanup();
|
|
448
|
+
resolve();
|
|
449
|
+
};
|
|
450
|
+
const cleanup = () => {
|
|
451
|
+
writer.off('error', onError);
|
|
452
|
+
writer.off('close', onClose);
|
|
453
|
+
res.data.off('error', onError);
|
|
454
|
+
};
|
|
455
|
+
res.data.on('error', onError);
|
|
456
|
+
writer.on('error', onError);
|
|
457
|
+
writer.on('close', onClose);
|
|
458
|
+
res.data.pipe(writer);
|
|
459
|
+
});
|
|
460
|
+
// 成功
|
|
461
|
+
return { ok: true };
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
// 403 等受控处理
|
|
465
|
+
if (isAxiosError(err) && err.response?.status === 403) {
|
|
466
|
+
// 不抛出,让上层自行决定
|
|
467
|
+
throw new Error('下载链接已过期,请重新进行分包后重试');
|
|
468
|
+
}
|
|
469
|
+
// 其他错误抛出或返回
|
|
470
|
+
throw err;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function isAxiosError(e) {
|
|
474
|
+
return !!e?.isAxiosError;
|
|
475
|
+
}
|
|
401
476
|
|
|
402
477
|
async function fetchGameInfo(clientKey) {
|
|
403
478
|
if (clientKey?.startsWith('sb')) {
|
|
@@ -414,7 +489,7 @@ async function fetchGameInfo(clientKey) {
|
|
|
414
489
|
params: {
|
|
415
490
|
client_key: clientKey,
|
|
416
491
|
// version_type: 1,
|
|
417
|
-
}
|
|
492
|
+
},
|
|
418
493
|
});
|
|
419
494
|
const gameInfo = response?.data?.mini_game;
|
|
420
495
|
if (!gameInfo) {
|
|
@@ -445,7 +520,7 @@ async function fetchGameInfo(clientKey) {
|
|
|
445
520
|
params: {
|
|
446
521
|
client_key: clientKey,
|
|
447
522
|
// version_type: 1,
|
|
448
|
-
}
|
|
523
|
+
}
|
|
449
524
|
// headers: {
|
|
450
525
|
// 'x-use-ppe': '1',
|
|
451
526
|
// 'x-tt-env': 'ppe_mg_revamp',
|
|
@@ -583,6 +658,12 @@ function getCurrentUser() {
|
|
|
583
658
|
}
|
|
584
659
|
}
|
|
585
660
|
|
|
661
|
+
function ensureDirSync(dirPath) {
|
|
662
|
+
if (!fs.existsSync(dirPath)) {
|
|
663
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
586
667
|
function buildOpenContext(sourcePath) {
|
|
587
668
|
const result = esbuild.buildSync({
|
|
588
669
|
entryPoints: [sourcePath],
|
|
@@ -859,14 +940,80 @@ function getDesktopPath() {
|
|
|
859
940
|
return defaultPath;
|
|
860
941
|
}
|
|
861
942
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
943
|
+
/**
|
|
944
|
+
* 一个类型安全的、通用的事件发射器类。
|
|
945
|
+
* 它允许你注册、注销和触发具有严格类型检查的事件。
|
|
946
|
+
*/
|
|
947
|
+
class TypedEventEmitter {
|
|
948
|
+
constructor() {
|
|
949
|
+
// 使用 Map 存储事件监听器。
|
|
950
|
+
// Key 是事件名,Value 是一个 Set,其中包含该事件的所有监听器函数。
|
|
951
|
+
// 使用 Set 可以自动防止同一个监听器被重复注册。
|
|
952
|
+
this.listeners = new Map();
|
|
865
953
|
}
|
|
866
|
-
|
|
867
|
-
|
|
954
|
+
/**
|
|
955
|
+
* 注册一个事件监听器。
|
|
956
|
+
* @param eventName 要监听的事件名称。
|
|
957
|
+
* @param listener 事件触发时执行的回调函数。
|
|
958
|
+
* @returns 返回一个函数,调用该函数即可注销此监听器,方便使用。
|
|
959
|
+
*/
|
|
960
|
+
on(eventName, listener) {
|
|
961
|
+
if (!this.listeners.has(eventName)) {
|
|
962
|
+
this.listeners.set(eventName, new Set());
|
|
963
|
+
}
|
|
964
|
+
this.listeners.get(eventName).add(listener);
|
|
965
|
+
// 返回一个便捷的取消订阅函数
|
|
966
|
+
return () => this.off(eventName, listener);
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* 注销一个事件监听器。
|
|
970
|
+
* @param eventName 要注销的事件名称。
|
|
971
|
+
* @param listener 之前通过 on() 方法注册的回调函数实例。
|
|
972
|
+
*/
|
|
973
|
+
off(eventName, listener) {
|
|
974
|
+
const eventListeners = this.listeners.get(eventName);
|
|
975
|
+
if (eventListeners) {
|
|
976
|
+
eventListeners.delete(listener);
|
|
977
|
+
if (eventListeners.size === 0) {
|
|
978
|
+
this.listeners.delete(eventName);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* 触发一个事件,并同步调用所有相关的监听器。
|
|
984
|
+
* @param eventName 要触发的事件名称。
|
|
985
|
+
* @param payload 传递给所有监听器的数据。类型必须与事件定义匹配。
|
|
986
|
+
*/
|
|
987
|
+
emit(eventName, payload) {
|
|
988
|
+
const eventListeners = this.listeners.get(eventName);
|
|
989
|
+
if (eventListeners) {
|
|
990
|
+
// 遍历 Set 并执行每一个监听器
|
|
991
|
+
eventListeners.forEach(listener => {
|
|
992
|
+
try {
|
|
993
|
+
// 在独立的 try...catch 中调用,防止一个监听器的错误影响其他监听器
|
|
994
|
+
listener(payload);
|
|
995
|
+
}
|
|
996
|
+
catch (error) {
|
|
997
|
+
console.error(`Error in listener for event "${String(eventName)}":`, error);
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* 注销指定事件的所有监听器。
|
|
1004
|
+
* @param eventName 要清除监听器的事件名称。
|
|
1005
|
+
*/
|
|
1006
|
+
removeAllListeners(eventName) {
|
|
1007
|
+
this.listeners.delete(eventName);
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* 清空所有事件的所有监听器。
|
|
1011
|
+
*/
|
|
1012
|
+
clearAll() {
|
|
1013
|
+
this.listeners.clear();
|
|
868
1014
|
}
|
|
869
1015
|
}
|
|
1016
|
+
const eventEmitter = new TypedEventEmitter();
|
|
870
1017
|
|
|
871
1018
|
const DEV_PORT = 3000;
|
|
872
1019
|
const DEV_WS_PORT = 4000;
|
|
@@ -881,20 +1028,8 @@ const NATIVE_GAME_ENTRY_FILES = [
|
|
|
881
1028
|
'game.json',
|
|
882
1029
|
'project.config.json',
|
|
883
1030
|
];
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
* 获取 Client Key(选择或输入)
|
|
887
|
-
*/
|
|
888
|
-
function getClientKey() {
|
|
889
|
-
return {
|
|
890
|
-
clientKey: getTTMGRC()?.clientKey || '',
|
|
891
|
-
};
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
function getOutputDir() {
|
|
895
|
-
const { clientKey } = getClientKey();
|
|
896
|
-
return path.join(os.homedir(), '__TTMG__', clientKey);
|
|
897
|
-
}
|
|
1031
|
+
const SUBAPCKAGE_FILED_NAMES = ['subpackages', 'subPackages'];
|
|
1032
|
+
const SUBPACKAGE_CONFIG_FILE_NAME = 'game.json';
|
|
898
1033
|
|
|
899
1034
|
// store.ts
|
|
900
1035
|
class Store {
|
|
@@ -955,967 +1090,2254 @@ const store = Store.getInstance({
|
|
|
955
1090
|
checkResults: [],
|
|
956
1091
|
});
|
|
957
1092
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
' The QR code page will be opened automatically in Chrome.');
|
|
962
|
-
console.log(' If it fails, please open the following link manually:');
|
|
963
|
-
console.log(' ' + chalk.cyan.underline(context.server));
|
|
964
|
-
console.log('');
|
|
965
|
-
console.log(chalk.yellow.bold('2.') +
|
|
966
|
-
' The debugging service will automatically compile your game assets.');
|
|
967
|
-
console.log(' Any changes in your game directory will trigger recompilation.');
|
|
968
|
-
console.log(' You can debug the updated content when upload is completed.');
|
|
969
|
-
console.log('');
|
|
970
|
-
console.log(chalk.yellow.bold('3.') +
|
|
971
|
-
' After scanning the QR code with your phone for Test User authentication,');
|
|
972
|
-
console.log(' the compiled code package will be uploaded to the client automatically.');
|
|
973
|
-
console.log(' Game debugging will start right away.');
|
|
974
|
-
console.log(chalk.gray('─────────────────────────────────────────────'));
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
function getLocalIPs() {
|
|
978
|
-
// 有线网卡关键词(Windows/macOS 常见 + 一些 Linux 命名)
|
|
979
|
-
const WIRED_KEYWORDS = [
|
|
980
|
-
'ethernet',
|
|
981
|
-
'以太网',
|
|
982
|
-
'本地连接',
|
|
983
|
-
'en',
|
|
984
|
-
'lan',
|
|
985
|
-
'enp',
|
|
986
|
-
'eno', // en0/en1, enp0s3...
|
|
987
|
-
];
|
|
988
|
-
// 无线网卡关键词(Windows/macOS 常见 + 一些 Linux 命名)
|
|
989
|
-
const WIRELESS_KEYWORDS = [
|
|
990
|
-
'wi-fi',
|
|
991
|
-
'wifi',
|
|
992
|
-
'无线',
|
|
993
|
-
'无线网络连接',
|
|
994
|
-
'wlan',
|
|
995
|
-
'wlp',
|
|
996
|
-
'wl',
|
|
997
|
-
'airport', // 老旧 macOS/工具里偶见
|
|
998
|
-
];
|
|
999
|
-
// 虚拟/隧道/容器/桥接等常见关键词
|
|
1000
|
-
const VIRTUAL_INTERFACE_KEYWORDS = [
|
|
1001
|
-
'vmware',
|
|
1002
|
-
'virtual',
|
|
1003
|
-
'docker',
|
|
1004
|
-
'vbox',
|
|
1005
|
-
'br-',
|
|
1006
|
-
'bridge',
|
|
1007
|
-
'tun',
|
|
1008
|
-
'tap',
|
|
1009
|
-
'hamachi',
|
|
1010
|
-
'vEthernet',
|
|
1011
|
-
'vnic',
|
|
1012
|
-
'utun',
|
|
1013
|
-
'hyper-v',
|
|
1014
|
-
'loopback',
|
|
1015
|
-
'lo',
|
|
1016
|
-
'ppp',
|
|
1017
|
-
'tailscale',
|
|
1018
|
-
'zerotier',
|
|
1019
|
-
'npcap',
|
|
1020
|
-
'npf',
|
|
1021
|
-
'wg',
|
|
1022
|
-
'wireguard',
|
|
1023
|
-
'anyconnect',
|
|
1024
|
-
'teleport',
|
|
1025
|
-
];
|
|
1026
|
-
// 判断是否为物理网卡(基于名称的启发式)
|
|
1027
|
-
function isPhysicalInterface(name) {
|
|
1028
|
-
const lower = name.toLowerCase();
|
|
1029
|
-
return (WIRED_KEYWORDS.some(k => lower.includes(k)) ||
|
|
1030
|
-
WIRELESS_KEYWORDS.some(k => lower.includes(k)));
|
|
1031
|
-
}
|
|
1032
|
-
// 判断是否为虚拟/隧道接口
|
|
1033
|
-
function isVirtualInterface(name) {
|
|
1034
|
-
const lower = name.toLowerCase();
|
|
1035
|
-
return VIRTUAL_INTERFACE_KEYWORDS.some(k => lower.includes(k));
|
|
1036
|
-
}
|
|
1037
|
-
// 判断局域网私有 IPv4
|
|
1038
|
-
function isPrivateIPv4(ip) {
|
|
1039
|
-
return (ip.startsWith('10.') ||
|
|
1040
|
-
ip.startsWith('192.168.') ||
|
|
1041
|
-
/^172\.(1[6-9]|2\d|3[01])\./.test(ip));
|
|
1042
|
-
}
|
|
1043
|
-
// 兼容不同 Node 版本中 family 的类型('IPv4' | 4)
|
|
1044
|
-
function isIPv4Family(f) {
|
|
1045
|
-
return f === 4 || f === 'IPv4';
|
|
1093
|
+
class WsServer {
|
|
1094
|
+
constructor() {
|
|
1095
|
+
this.startWsServer(store.getState().nodeWsPort);
|
|
1046
1096
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
if (net.internal)
|
|
1061
|
-
continue;
|
|
1062
|
-
if (!net.address || !isPrivateIPv4(net.address))
|
|
1063
|
-
continue;
|
|
1064
|
-
// 若名称判断不通过,但地址是私有网段、且非回环/非内网,考虑接纳
|
|
1065
|
-
// 但排除明显虚拟(已经在上面过滤 name)
|
|
1066
|
-
if (matchedPhysicalName || shouldAcceptByHeuristic(name, net.address)) {
|
|
1067
|
-
results.push({ name, address: net.address });
|
|
1097
|
+
startWsServer(port) {
|
|
1098
|
+
this.ws = new WebSocket.Server({ port });
|
|
1099
|
+
this.ws.on('connection', ws => {
|
|
1100
|
+
const { clientHttpPort, clientHost, clientWsPort } = store.getState();
|
|
1101
|
+
if (clientHost) {
|
|
1102
|
+
this.send({
|
|
1103
|
+
method: 'clientDebugInfo',
|
|
1104
|
+
payload: {
|
|
1105
|
+
clientHttpPort,
|
|
1106
|
+
clientHost,
|
|
1107
|
+
clientWsPort,
|
|
1108
|
+
},
|
|
1109
|
+
});
|
|
1068
1110
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1111
|
+
ws.on('message', message => {
|
|
1112
|
+
/** 客户端发送的消息 */
|
|
1113
|
+
const clientMessage = JSON.parse(message.toString());
|
|
1114
|
+
const from = clientMessage.from;
|
|
1115
|
+
if (from === 'browser') {
|
|
1116
|
+
// console.log(chalk.yellow.bold('Browser message'), clientMessage);
|
|
1117
|
+
const method = clientMessage.method;
|
|
1118
|
+
switch (method) {
|
|
1119
|
+
case 'manualConnect': {
|
|
1120
|
+
const { clientHttpPort, clientHost, clientWsPort } = clientMessage.payload;
|
|
1121
|
+
store.setState({
|
|
1122
|
+
clientHttpPort,
|
|
1123
|
+
clientHost,
|
|
1124
|
+
clientWsPort,
|
|
1125
|
+
clientWsHost: clientHost,
|
|
1126
|
+
});
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
case 'startUpload':
|
|
1130
|
+
eventEmitter.emit('startUpload', {});
|
|
1131
|
+
break;
|
|
1132
|
+
case 'closeLocalDebug':
|
|
1133
|
+
console.log('closeLocalDebug');
|
|
1134
|
+
/**
|
|
1135
|
+
* 关闭调试服务
|
|
1136
|
+
*/
|
|
1137
|
+
this.ws.close();
|
|
1138
|
+
break;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
else {
|
|
1142
|
+
const method = clientMessage.method;
|
|
1143
|
+
switch (method) {
|
|
1144
|
+
/**
|
|
1145
|
+
* 客户端完成扫码成功,返回客户端的 host 和 port
|
|
1146
|
+
*/
|
|
1147
|
+
case 'startScanQRcode': {
|
|
1148
|
+
const payload = clientMessage.payload;
|
|
1149
|
+
console.log('startQRcode', payload);
|
|
1150
|
+
this.send({
|
|
1151
|
+
method: 'startScanQRcode',
|
|
1152
|
+
});
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
case 'scanQRCodeResult': {
|
|
1156
|
+
const payload = clientMessage.payload || {};
|
|
1157
|
+
const { host, port, wsPort, errMsg, isSuccess } = payload;
|
|
1158
|
+
console.log('scanQRCodeResult', payload);
|
|
1159
|
+
if (isSuccess) {
|
|
1160
|
+
store.setState({
|
|
1161
|
+
clientHttpPort: port,
|
|
1162
|
+
clientHost: host,
|
|
1163
|
+
clientWsPort: wsPort,
|
|
1164
|
+
});
|
|
1165
|
+
this.send({
|
|
1166
|
+
method: 'scanQRCodeSuccess',
|
|
1167
|
+
payload: {
|
|
1168
|
+
clientHttpPort: port,
|
|
1169
|
+
clientHost: host,
|
|
1170
|
+
clientWsPort: wsPort,
|
|
1171
|
+
},
|
|
1172
|
+
});
|
|
1173
|
+
console.log('scanQRcodeSuccess');
|
|
1174
|
+
}
|
|
1175
|
+
else {
|
|
1176
|
+
this.send({
|
|
1177
|
+
method: 'scanQRCodeFailed',
|
|
1178
|
+
payload: {
|
|
1179
|
+
errMsg,
|
|
1180
|
+
},
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
break;
|
|
1184
|
+
}
|
|
1185
|
+
// 手动绑定客户端调试服务
|
|
1186
|
+
// 待废弃
|
|
1187
|
+
case 'shareDevParams':
|
|
1188
|
+
const payload = clientMessage.payload;
|
|
1189
|
+
const { host, port, wsPort } = payload;
|
|
1190
|
+
store.setState({
|
|
1191
|
+
clientHttpPort: port,
|
|
1192
|
+
clientHost: host,
|
|
1193
|
+
clientWsPort: wsPort,
|
|
1194
|
+
clientWsHost: host,
|
|
1195
|
+
});
|
|
1196
|
+
this.send({
|
|
1197
|
+
method: 'scanQRCodeSuccess',
|
|
1198
|
+
payload: {
|
|
1199
|
+
clientHttpPort: port,
|
|
1200
|
+
clientHost: host,
|
|
1201
|
+
clientWsPort: wsPort,
|
|
1202
|
+
},
|
|
1203
|
+
});
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
this.ws.on('error', err => {
|
|
1210
|
+
if (err.code === 'EADDRINUSE') {
|
|
1211
|
+
store.setState({
|
|
1212
|
+
nodeWsPort: store.getState().nodeWsPort + 1,
|
|
1213
|
+
});
|
|
1214
|
+
this.startWsServer(store.getState().nodeWsPort);
|
|
1215
|
+
}
|
|
1216
|
+
else {
|
|
1217
|
+
console.log(chalk.red.bold(err.message));
|
|
1218
|
+
process.exit(1);
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
send(params) {
|
|
1223
|
+
this.ws.clients.forEach(client => {
|
|
1224
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1225
|
+
const data = {
|
|
1226
|
+
...params,
|
|
1227
|
+
from: 'nodeServer',
|
|
1228
|
+
};
|
|
1229
|
+
client.send(JSON.stringify(data));
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
sendResourceChange() {
|
|
1234
|
+
this.send({
|
|
1235
|
+
method: 'resourceChange',
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
sendCompilationStatus(status, payload) {
|
|
1239
|
+
this.send({
|
|
1240
|
+
method: 'compilationStatus',
|
|
1241
|
+
payload: {
|
|
1242
|
+
status,
|
|
1243
|
+
...payload,
|
|
1244
|
+
},
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
close() {
|
|
1248
|
+
this.ws.close();
|
|
1249
|
+
}
|
|
1250
|
+
sendUnitySplitStatus(payload) {
|
|
1251
|
+
this.send({
|
|
1252
|
+
method: 'unitySplitStatus',
|
|
1253
|
+
payload,
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
sendUploadStatus(status, payload) {
|
|
1257
|
+
this.send({
|
|
1258
|
+
method: 'uploadStatus',
|
|
1259
|
+
status,
|
|
1260
|
+
payload,
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
const wsServer = new WsServer();
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* 获取 Client Key(选择或输入)
|
|
1268
|
+
*/
|
|
1269
|
+
function getClientKey() {
|
|
1270
|
+
return {
|
|
1271
|
+
clientKey: getTTMGRC()?.clientKey || '',
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function getOutputDir() {
|
|
1276
|
+
const { clientKey } = getClientKey();
|
|
1277
|
+
return path.join(os.homedir(), '__TTMG__', clientKey);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
async function compile(context) {
|
|
1281
|
+
// const { openDataContext } = getOpenContextConfig();
|
|
1282
|
+
// if (!!openDataContext) {
|
|
1283
|
+
// buildOpenContextToFile(openDataContext);
|
|
1284
|
+
// }
|
|
1285
|
+
const entryDir = process.cwd();
|
|
1286
|
+
const outputDir = getOutputDir();
|
|
1287
|
+
const { clientKey, msg } = getClientKey();
|
|
1288
|
+
if (!clientKey) {
|
|
1289
|
+
if (context?.mode !== 'watch') {
|
|
1290
|
+
console.log(chalk.red.bold(msg));
|
|
1291
|
+
process.exit(1);
|
|
1292
|
+
}
|
|
1293
|
+
console.log(chalk.red.bold(msg));
|
|
1294
|
+
}
|
|
1295
|
+
const startTip = context?.mode === 'watch'
|
|
1296
|
+
? '🔔 Watching game assets for local debugging...'
|
|
1297
|
+
: '🚀 Compiling game assets for local debugging...';
|
|
1298
|
+
console.log(chalk.bold.cyan(startTip));
|
|
1299
|
+
wsServer.sendCompilationStatus('start');
|
|
1300
|
+
store.setState({
|
|
1301
|
+
isUnderCompiling: true,
|
|
1089
1302
|
});
|
|
1090
|
-
return
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1303
|
+
return new Promise((resolve, reject) => {
|
|
1304
|
+
const worker = new worker_threads.Worker(path.resolve(__dirname, './scripts/worker.js'));
|
|
1305
|
+
worker.on('message', msg => {
|
|
1306
|
+
if (msg.type === 'status' && msg.status === 'end') {
|
|
1307
|
+
const { isSuccess, errorMsg, packages } = msg;
|
|
1308
|
+
if (!isSuccess) {
|
|
1309
|
+
store.setState({
|
|
1310
|
+
isUnderCompiling: false,
|
|
1311
|
+
});
|
|
1312
|
+
wsServer.sendCompilationStatus('end', {
|
|
1313
|
+
errorMsg,
|
|
1314
|
+
isSuccess: false,
|
|
1315
|
+
});
|
|
1316
|
+
console.log(chalk.red.bold(errorMsg));
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
store.setState({
|
|
1320
|
+
isUnderCompiling: false,
|
|
1321
|
+
packages,
|
|
1322
|
+
});
|
|
1323
|
+
/**
|
|
1324
|
+
* 编译成功后,需要触发上传
|
|
1325
|
+
*/
|
|
1326
|
+
eventEmitter.emit('compileSuccess', {});
|
|
1327
|
+
wsServer.sendCompilationStatus('end', {
|
|
1328
|
+
isSuccess: true,
|
|
1329
|
+
});
|
|
1330
|
+
console.log(chalk.green.bold('✔ Game resources compiled successfully!'));
|
|
1331
|
+
resolve(msg); // 编译结束,返回结果
|
|
1332
|
+
}
|
|
1333
|
+
worker.terminate();
|
|
1334
|
+
}
|
|
1335
|
+
else if (msg.type === 'error') {
|
|
1336
|
+
reject(msg.error);
|
|
1337
|
+
worker.terminate();
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
worker.on('error', err => {
|
|
1341
|
+
console.log(chalk.red.bold(err.message));
|
|
1342
|
+
reject(err);
|
|
1343
|
+
worker.terminate();
|
|
1344
|
+
});
|
|
1345
|
+
worker.postMessage({
|
|
1346
|
+
type: 'compile',
|
|
1347
|
+
context: {
|
|
1348
|
+
clientKey,
|
|
1349
|
+
outputDir,
|
|
1350
|
+
devPort: store.getState().nodeServerPort,
|
|
1351
|
+
entryDir,
|
|
1352
|
+
},
|
|
1353
|
+
});
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// import { uploadGame } from './uploadGame';
|
|
1358
|
+
async function watch() {
|
|
1359
|
+
let debounceTimer = null;
|
|
1360
|
+
// 监听当前工作目录,排除 node_modules 和 .git
|
|
1361
|
+
const watcher = chokidar.watch(process.cwd(), {
|
|
1362
|
+
/**
|
|
1363
|
+
* 忽略 wasmcode/cache 目录
|
|
1364
|
+
*/
|
|
1365
|
+
ignored: /(^|[\/\\])(\.git|node_modules|__TTMG_TEMP__)/, // 忽略 .git 和 node_modules
|
|
1366
|
+
ignoreInitial: true, // 忽略初始添加事件
|
|
1367
|
+
persistent: true,
|
|
1368
|
+
awaitWriteFinish: {
|
|
1369
|
+
stabilityThreshold: 1000,
|
|
1370
|
+
},
|
|
1371
|
+
});
|
|
1372
|
+
// 任意文件变化都触发
|
|
1373
|
+
watcher.on('all', (event, path) => {
|
|
1374
|
+
// 清除之前的定时器
|
|
1375
|
+
if (debounceTimer)
|
|
1376
|
+
clearTimeout(debounceTimer);
|
|
1377
|
+
// 重新设置定时器
|
|
1378
|
+
debounceTimer = setTimeout(async () => {
|
|
1379
|
+
(await compile({
|
|
1380
|
+
mode: 'watch',
|
|
1381
|
+
}),
|
|
1382
|
+
wsServer.sendResourceChange());
|
|
1383
|
+
debounceTimer = null;
|
|
1384
|
+
}, 2000);
|
|
1385
|
+
});
|
|
1386
|
+
watcher.on('error', error => {
|
|
1387
|
+
// console.error(chalk.red('[watch] 监听发生错误:'), error);
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
let spinner;
|
|
1392
|
+
async function uploadGame(callback) {
|
|
1393
|
+
const ora = await import('ora');
|
|
1394
|
+
spinner = ora.default({
|
|
1395
|
+
text: chalk.cyan.bold('Uploading game assets to client...'),
|
|
1396
|
+
spinner: 'dots',
|
|
1397
|
+
});
|
|
1398
|
+
spinner.start();
|
|
1399
|
+
const outputDir = getOutputDir();
|
|
1400
|
+
callback({
|
|
1401
|
+
status: 'start',
|
|
1402
|
+
percent: 0,
|
|
1403
|
+
});
|
|
1404
|
+
const zipPath = path.join(os.homedir(), '__TTMG__', 'upload.zip');
|
|
1405
|
+
await zipDirectory(outputDir, zipPath);
|
|
1406
|
+
await uploadZip(zipPath, callback);
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* 复制源目录内容到临时目录,然后根据 glob 模式过滤并压缩文件。
|
|
1410
|
+
* 原始目录保持不变。
|
|
1411
|
+
*
|
|
1412
|
+
* @param sourceDir - 要压缩的源文件夹路径。
|
|
1413
|
+
* @param outPath - 输出的 zip 文件路径。
|
|
1414
|
+
*/
|
|
1415
|
+
async function zipDirectory(sourceDir, outPath) {
|
|
1416
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'zip-temp-'));
|
|
1417
|
+
try {
|
|
1418
|
+
// 2. 将源目录的所有内容复制到临时目录
|
|
1419
|
+
await fs.promises.cp(sourceDir, tempDir, { recursive: true });
|
|
1420
|
+
// 3. 对临时目录进行压缩
|
|
1421
|
+
const output = fs.createWriteStream(outPath);
|
|
1422
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
1423
|
+
// 使用 Promise 包装流操作
|
|
1424
|
+
const archivePromise = new Promise((resolve, reject) => {
|
|
1425
|
+
output.on('close', () => {
|
|
1426
|
+
resolve();
|
|
1427
|
+
});
|
|
1428
|
+
archive.on('warning', err => console.warn('Archiver warning:', err));
|
|
1429
|
+
output.on('error', err => reject(err));
|
|
1430
|
+
archive.on('error', err => reject(err));
|
|
1431
|
+
});
|
|
1432
|
+
archive.pipe(output);
|
|
1433
|
+
// 4. 使用 glob 在临时目录中查找文件,并应用过滤规则
|
|
1434
|
+
const files = await glob__namespace.glob('**/*', {
|
|
1435
|
+
cwd: tempDir,
|
|
1436
|
+
nodir: true,
|
|
1437
|
+
ignore: ['**/*.js.map', '**/*.d.ts', '__TTMG_TEMP__/**'],
|
|
1438
|
+
});
|
|
1439
|
+
// 5. 将过滤后的文件逐个添加到压缩包
|
|
1440
|
+
for (const file of files) {
|
|
1441
|
+
const filePath = path.join(tempDir, file);
|
|
1442
|
+
archive.file(filePath, { name: file });
|
|
1443
|
+
}
|
|
1444
|
+
// 6. 完成压缩
|
|
1445
|
+
await archive.finalize();
|
|
1446
|
+
// 等待文件流关闭
|
|
1447
|
+
await archivePromise;
|
|
1448
|
+
}
|
|
1449
|
+
catch (err) {
|
|
1450
|
+
console.error('压缩过程中发生错误:', err);
|
|
1451
|
+
throw err;
|
|
1452
|
+
}
|
|
1453
|
+
finally {
|
|
1454
|
+
// 7. 无论成功还是失败,都清理临时目录
|
|
1455
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
async function uploadZip(zipPath, callback) {
|
|
1459
|
+
const startTime = Date.now();
|
|
1460
|
+
const form = new FormData$1();
|
|
1461
|
+
form.append('file', fs.createReadStream(zipPath), {
|
|
1462
|
+
filename: 'upload.zip',
|
|
1463
|
+
contentType: 'application/zip',
|
|
1464
|
+
});
|
|
1465
|
+
// 帮我计算下文件大小,变成 MB 为单位
|
|
1466
|
+
const fileSize = fs.statSync(zipPath).size / 1024 / 1024;
|
|
1467
|
+
spinner.text = chalk.cyan.bold(`Start upload Game assets to client, size: ${fileSize.toFixed(2)} MB`);
|
|
1468
|
+
const { clientHttpPort, clientHost } = store.getState();
|
|
1469
|
+
const url = `http://${clientHost}:${clientHttpPort}/game/upload`;
|
|
1470
|
+
try {
|
|
1471
|
+
// 1. 创建请求流
|
|
1472
|
+
const stream = got.stream.post(url, {
|
|
1473
|
+
body: form,
|
|
1474
|
+
});
|
|
1475
|
+
const handleProgress = progress => {
|
|
1476
|
+
const percent = progress.percent;
|
|
1477
|
+
// const transferred = progress.transferred;
|
|
1478
|
+
// const total = progress.total;
|
|
1479
|
+
spinner.text = chalk.cyan.bold(`Uploading game assets to client... ${(percent * 100).toFixed(0)}%`);
|
|
1480
|
+
callback({
|
|
1481
|
+
status: 'process',
|
|
1482
|
+
percent,
|
|
1483
|
+
});
|
|
1484
|
+
};
|
|
1485
|
+
stream.on('uploadProgress', handleProgress);
|
|
1486
|
+
const chunks = [];
|
|
1487
|
+
// 当流传输数据时,收集数据块
|
|
1488
|
+
stream.on('data', chunk => {
|
|
1489
|
+
chunks.push(chunk);
|
|
1490
|
+
});
|
|
1491
|
+
// 当流成功结束时
|
|
1492
|
+
stream.on('end', () => {
|
|
1493
|
+
spinner.succeed(chalk.green.bold(`Upload game assets to client success! Cost: ${Date.now() - startTime}ms`));
|
|
1494
|
+
callback({
|
|
1495
|
+
status: 'success',
|
|
1496
|
+
percent: 1,
|
|
1497
|
+
});
|
|
1498
|
+
});
|
|
1499
|
+
// 当流发生错误时
|
|
1500
|
+
stream.on('error', err => {
|
|
1501
|
+
stream.off('uploadProgress', handleProgress);
|
|
1502
|
+
spinner.fail(chalk.red.bold(`Upload failed with error: ${err.message}, please check current debug env and try to scan qrcode to reupload again.`));
|
|
1503
|
+
callback({
|
|
1504
|
+
status: 'error',
|
|
1505
|
+
percent: 0,
|
|
1506
|
+
msg: err.message,
|
|
1507
|
+
});
|
|
1508
|
+
});
|
|
1104
1509
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
}
|
|
1115
|
-
return out;
|
|
1510
|
+
catch (err) {
|
|
1511
|
+
callback({
|
|
1512
|
+
status: 'error',
|
|
1513
|
+
percent: 0,
|
|
1514
|
+
msg: err?.message,
|
|
1515
|
+
});
|
|
1516
|
+
process.stdout.write('\n');
|
|
1517
|
+
console.log('\n');
|
|
1518
|
+
console.error(chalk.red.bold('✖ Upload failed with server error, please scan qrcode to reupload'));
|
|
1116
1519
|
}
|
|
1117
1520
|
}
|
|
1118
1521
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
// --- 中间件和路由设置 ---
|
|
1129
|
-
app.use(expressStaticGzip(publicPath, {
|
|
1130
|
-
enableBrotli: true,
|
|
1131
|
-
orderPreference: ['br'],
|
|
1132
|
-
}));
|
|
1133
|
-
app.use((req, res, next) => {
|
|
1134
|
-
res.header('Access-Control-Allow-Origin', '*');
|
|
1135
|
-
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
1136
|
-
res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
1137
|
-
res.header('Pragma', 'no-cache');
|
|
1138
|
-
res.header('Expires', '0');
|
|
1139
|
-
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
1140
|
-
next();
|
|
1141
|
-
});
|
|
1142
|
-
app.use(express.json());
|
|
1143
|
-
app.use(express.urlencoded({ extended: true }));
|
|
1144
|
-
app.use('/game/files', express.static(outputDir));
|
|
1145
|
-
app.get('/game/config', async (req, res) => {
|
|
1146
|
-
const basic = await ttmgPack.getPkgs({ entryDir: process.cwd() });
|
|
1147
|
-
const user = getCurrentUser();
|
|
1148
|
-
const { clientKey } = getClientKey();
|
|
1149
|
-
res.send({
|
|
1150
|
-
error: null,
|
|
1151
|
-
data: {
|
|
1152
|
-
user,
|
|
1153
|
-
code: successCode,
|
|
1154
|
-
nodeWsPort: store.getState().nodeWsPort,
|
|
1155
|
-
clientKey: clientKey,
|
|
1156
|
-
schema: `https://www.tiktok.com/ttmg/dev/${clientKey}?host=${getLocalIP()}&port=${store.getState().nodeWsPort}&host_list=${encodeURIComponent(JSON.stringify(getLocalIPs()))}`,
|
|
1157
|
-
...basic,
|
|
1158
|
-
devToolVersion,
|
|
1159
|
-
},
|
|
1160
|
-
});
|
|
1161
|
-
});
|
|
1162
|
-
app.get('/game/detail', async (req, res) => {
|
|
1163
|
-
const basic = await ttmgPack.getPkgs({ entryDir: process.cwd() });
|
|
1164
|
-
const { clientKey } = getClientKey();
|
|
1165
|
-
const { error, data: gameInfo } = await fetchGameInfo(clientKey);
|
|
1166
|
-
store.setState({
|
|
1167
|
-
appId: gameInfo?.app_id,
|
|
1168
|
-
sandboxId: gameInfo?.sandbox_info?.sandbox_id,
|
|
1169
|
-
});
|
|
1170
|
-
if (error) {
|
|
1171
|
-
res.send({ error, data: null });
|
|
1522
|
+
function listen() {
|
|
1523
|
+
eventEmitter.on('startUpload', () => {
|
|
1524
|
+
/**
|
|
1525
|
+
* 如果还在编译中,需要等到编译结束再上传
|
|
1526
|
+
*/
|
|
1527
|
+
if (store.getState().isUnderCompiling) {
|
|
1528
|
+
store.setState({
|
|
1529
|
+
isWaitingForUpload: true,
|
|
1530
|
+
});
|
|
1172
1531
|
return;
|
|
1173
1532
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1533
|
+
wsServer.sendUploadStatus('start');
|
|
1534
|
+
uploadGame(({ status, percent, msg }) => {
|
|
1535
|
+
if (status === 'process') {
|
|
1536
|
+
wsServer.sendUploadStatus('process', {
|
|
1537
|
+
status: 'process',
|
|
1538
|
+
progress: percent,
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
else if (status === 'error') {
|
|
1542
|
+
wsServer.sendUploadStatus('error', {
|
|
1543
|
+
status: 'error',
|
|
1544
|
+
errMsg: msg,
|
|
1545
|
+
isSuccess: false,
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
else if (status === 'success') {
|
|
1549
|
+
wsServer.sendUploadStatus('success', {
|
|
1550
|
+
status: 'success',
|
|
1551
|
+
packages: store.getState().packages,
|
|
1552
|
+
clientKey: getClientKey().clientKey,
|
|
1553
|
+
isSuccess: true,
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1197
1556
|
});
|
|
1198
|
-
res.send({ code: successCode, data: checkResult });
|
|
1199
1557
|
});
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
return;
|
|
1209
|
-
}
|
|
1210
|
-
try {
|
|
1211
|
-
// 通过 header 获取 desc
|
|
1212
|
-
const desc = req.headers['ttmg-game-desc'];
|
|
1213
|
-
// 需要做 decodeURIComponent 处理
|
|
1214
|
-
const decodedDesc = decodeURIComponent(desc || '--');
|
|
1215
|
-
// 直接传递需要的信息
|
|
1216
|
-
const { data, error } = await uploadGameToPlatform({
|
|
1217
|
-
data: uploadedFile.data, // 这是 Buffer
|
|
1218
|
-
name: uploadedFile.name, // 这是文件名
|
|
1219
|
-
clientKey: getClientKey().clientKey,
|
|
1220
|
-
note: decodedDesc,
|
|
1221
|
-
appId: store.getState().appId,
|
|
1222
|
-
sandboxId: store.getState().sandboxId,
|
|
1558
|
+
eventEmitter.on('compileSuccess', () => {
|
|
1559
|
+
/**
|
|
1560
|
+
* 如果有等待上传的任务,需要触发上传
|
|
1561
|
+
*/
|
|
1562
|
+
if (store.getState().isWaitingForUpload) {
|
|
1563
|
+
eventEmitter.emit('startUpload', {});
|
|
1564
|
+
store.setState({
|
|
1565
|
+
isWaitingForUpload: false,
|
|
1223
1566
|
});
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function getLocalIPs() {
|
|
1572
|
+
// 有线网卡关键词(Windows/macOS 常见 + 一些 Linux 命名)
|
|
1573
|
+
const WIRED_KEYWORDS = [
|
|
1574
|
+
'ethernet',
|
|
1575
|
+
'以太网',
|
|
1576
|
+
'本地连接',
|
|
1577
|
+
'en',
|
|
1578
|
+
'lan',
|
|
1579
|
+
'enp',
|
|
1580
|
+
'eno', // en0/en1, enp0s3...
|
|
1581
|
+
];
|
|
1582
|
+
// 无线网卡关键词(Windows/macOS 常见 + 一些 Linux 命名)
|
|
1583
|
+
const WIRELESS_KEYWORDS = [
|
|
1584
|
+
'wi-fi',
|
|
1585
|
+
'wifi',
|
|
1586
|
+
'无线',
|
|
1587
|
+
'无线网络连接',
|
|
1588
|
+
'wlan',
|
|
1589
|
+
'wlp',
|
|
1590
|
+
'wl',
|
|
1591
|
+
'airport', // 老旧 macOS/工具里偶见
|
|
1592
|
+
];
|
|
1593
|
+
// 虚拟/隧道/容器/桥接等常见关键词
|
|
1594
|
+
const VIRTUAL_INTERFACE_KEYWORDS = [
|
|
1595
|
+
'vmware',
|
|
1596
|
+
'virtual',
|
|
1597
|
+
'docker',
|
|
1598
|
+
'vbox',
|
|
1599
|
+
'br-',
|
|
1600
|
+
'bridge',
|
|
1601
|
+
'tun',
|
|
1602
|
+
'tap',
|
|
1603
|
+
'hamachi',
|
|
1604
|
+
'vEthernet',
|
|
1605
|
+
'vnic',
|
|
1606
|
+
'utun',
|
|
1607
|
+
'hyper-v',
|
|
1608
|
+
'loopback',
|
|
1609
|
+
'lo',
|
|
1610
|
+
'ppp',
|
|
1611
|
+
'tailscale',
|
|
1612
|
+
'zerotier',
|
|
1613
|
+
'npcap',
|
|
1614
|
+
'npf',
|
|
1615
|
+
'wg',
|
|
1616
|
+
'wireguard',
|
|
1617
|
+
'anyconnect',
|
|
1618
|
+
'teleport',
|
|
1619
|
+
];
|
|
1620
|
+
// 判断是否为物理网卡(基于名称的启发式)
|
|
1621
|
+
function isPhysicalInterface(name) {
|
|
1622
|
+
const lower = name.toLowerCase();
|
|
1623
|
+
return (WIRED_KEYWORDS.some(k => lower.includes(k)) ||
|
|
1624
|
+
WIRELESS_KEYWORDS.some(k => lower.includes(k)));
|
|
1625
|
+
}
|
|
1626
|
+
// 判断是否为虚拟/隧道接口
|
|
1627
|
+
function isVirtualInterface(name) {
|
|
1628
|
+
const lower = name.toLowerCase();
|
|
1629
|
+
return VIRTUAL_INTERFACE_KEYWORDS.some(k => lower.includes(k));
|
|
1630
|
+
}
|
|
1631
|
+
// 判断局域网私有 IPv4
|
|
1632
|
+
function isPrivateIPv4(ip) {
|
|
1633
|
+
return (ip.startsWith('10.') ||
|
|
1634
|
+
ip.startsWith('192.168.') ||
|
|
1635
|
+
/^172\.(1[6-9]|2\d|3[01])\./.test(ip));
|
|
1636
|
+
}
|
|
1637
|
+
// 兼容不同 Node 版本中 family 的类型('IPv4' | 4)
|
|
1638
|
+
function isIPv4Family(f) {
|
|
1639
|
+
return f === 4 || f === 'IPv4';
|
|
1640
|
+
}
|
|
1641
|
+
const nets = os.networkInterfaces();
|
|
1642
|
+
const results = [];
|
|
1643
|
+
for (const name of Object.keys(nets)) {
|
|
1644
|
+
const addrs = nets[name] || [];
|
|
1645
|
+
// 先粗略排除明显虚拟接口
|
|
1646
|
+
if (isVirtualInterface(name))
|
|
1647
|
+
continue;
|
|
1648
|
+
// 需要看地址再判断是否物理
|
|
1649
|
+
// 有些系统的命名不标准,允许在地址层面兜底
|
|
1650
|
+
let matchedPhysicalName = isPhysicalInterface(name);
|
|
1651
|
+
for (const net of addrs) {
|
|
1652
|
+
if (!isIPv4Family(net.family))
|
|
1653
|
+
continue;
|
|
1654
|
+
if (net.internal)
|
|
1655
|
+
continue;
|
|
1656
|
+
if (!net.address || !isPrivateIPv4(net.address))
|
|
1657
|
+
continue;
|
|
1658
|
+
// 若名称判断不通过,但地址是私有网段、且非回环/非内网,考虑接纳
|
|
1659
|
+
// 但排除明显虚拟(已经在上面过滤 name)
|
|
1660
|
+
if (matchedPhysicalName || shouldAcceptByHeuristic(name, net.address)) {
|
|
1661
|
+
results.push({ name, address: net.address });
|
|
1229
1662
|
}
|
|
1230
1663
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1664
|
+
}
|
|
1665
|
+
// 去重(同名+同地址)
|
|
1666
|
+
const unique = dedupe(results);
|
|
1667
|
+
// 稳定排序:无线优先 or 有线优先按需定。这里有线优先,再无线,再其他。
|
|
1668
|
+
unique.sort((a, b) => {
|
|
1669
|
+
const rank = (n) => {
|
|
1670
|
+
const lower = n.toLowerCase();
|
|
1671
|
+
if (WIRED_KEYWORDS.some(k => lower.includes(k)))
|
|
1672
|
+
return 0;
|
|
1673
|
+
if (WIRELESS_KEYWORDS.some(k => lower.includes(k)))
|
|
1674
|
+
return 1;
|
|
1675
|
+
return 2;
|
|
1676
|
+
};
|
|
1677
|
+
const ra = rank(a.name);
|
|
1678
|
+
const rb = rank(b.name);
|
|
1679
|
+
if (ra !== rb)
|
|
1680
|
+
return ra - rb;
|
|
1681
|
+
// 同级按名称自然排序
|
|
1682
|
+
return a.name.localeCompare(b.name, undefined, { numeric: true });
|
|
1683
|
+
});
|
|
1684
|
+
return unique.map(item => item.address);
|
|
1685
|
+
// 兜底启发式:如果名字不明显,但常见物理命名模式且 IP 为私网,接受
|
|
1686
|
+
function shouldAcceptByHeuristic(name, ip) {
|
|
1687
|
+
name.toLowerCase();
|
|
1688
|
+
// macOS: en0/en1
|
|
1689
|
+
if (/^en\d+$/i.test(name))
|
|
1690
|
+
return true;
|
|
1691
|
+
// Linux: enpXsY / enoX / wlpXsY
|
|
1692
|
+
if (/^(enp|eno|wlp)\w+$/i.test(name))
|
|
1693
|
+
return true;
|
|
1694
|
+
// Windows: 以太网 / Wi-Fi(非虚拟已过滤)
|
|
1695
|
+
if (/(以太网|本地连接|wi-?fi|无线)/.test(name))
|
|
1696
|
+
return true;
|
|
1697
|
+
return false;
|
|
1698
|
+
}
|
|
1699
|
+
function dedupe(arr) {
|
|
1700
|
+
const seen = new Set();
|
|
1701
|
+
const out = [];
|
|
1702
|
+
for (const item of arr) {
|
|
1703
|
+
const key = `${item.name}|${item.address}`;
|
|
1704
|
+
if (!seen.has(key)) {
|
|
1705
|
+
seen.add(key);
|
|
1706
|
+
out.push(item);
|
|
1236
1707
|
}
|
|
1237
|
-
// 打印详细错误到服务器日志,方便排查
|
|
1238
|
-
res.status(500).send({ code: errorCode, data: errorMessage }); // 使用正确的 HTTP 状态码
|
|
1239
1708
|
}
|
|
1240
|
-
|
|
1241
|
-
app.get('*', (req, res) => {
|
|
1242
|
-
res.sendFile(path.join(publicPath, `index.html`));
|
|
1243
|
-
});
|
|
1244
|
-
// --- 中间件和路由设置结束 ---
|
|
1245
|
-
// 步骤 2: 用配置好的 app 实例创建一个 http.Server。我们只创建这一次!
|
|
1246
|
-
const server = http.createServer(app);
|
|
1247
|
-
/**
|
|
1248
|
-
* @description 尝试在指定端口启动服务。这是个纯粹的辅助函数。
|
|
1249
|
-
* @param {number} port - 要尝试的端口号。
|
|
1250
|
-
* @returns {Promise<boolean>} 成功返回 true,因端口占用失败则返回 false。
|
|
1251
|
-
*/
|
|
1252
|
-
function tryListen(port) {
|
|
1253
|
-
return new Promise(resolve => {
|
|
1254
|
-
// 定义错误处理函数
|
|
1255
|
-
const onError = err => {
|
|
1256
|
-
// 清理掉另一个事件的监听器,防止内存泄漏
|
|
1257
|
-
server.removeListener('listening', onListening);
|
|
1258
|
-
if (err.code === 'EADDRINUSE') {
|
|
1259
|
-
console.log(chalk(`Port ${port} is already in use, trying ${port + 1}...`));
|
|
1260
|
-
resolve(false); // 明确表示因端口占用而失败
|
|
1261
|
-
}
|
|
1262
|
-
else {
|
|
1263
|
-
// 对于其他致命错误,直接退出进程
|
|
1264
|
-
console.log(chalk.red.bold(err.message));
|
|
1265
|
-
process.exit(1);
|
|
1266
|
-
}
|
|
1267
|
-
};
|
|
1268
|
-
// 定义成功处理函数
|
|
1269
|
-
const onListening = () => {
|
|
1270
|
-
// 清理掉另一个事件的监听器
|
|
1271
|
-
server.removeListener('error', onError);
|
|
1272
|
-
resolve(true); // 明确表示成功
|
|
1273
|
-
};
|
|
1274
|
-
// 使用 .once() 来确保监听器只执行一次然后自动移除
|
|
1275
|
-
server.once('error', onError);
|
|
1276
|
-
server.once('listening', onListening);
|
|
1277
|
-
// 执行监听动作
|
|
1278
|
-
server.listen(port);
|
|
1279
|
-
});
|
|
1709
|
+
return out;
|
|
1280
1710
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
async function init() {
|
|
1714
|
+
const promptModule = inquirer.createPromptModule();
|
|
1715
|
+
const { clientKey: lastUsedClientKey } = getTTMGRC() || {};
|
|
1716
|
+
if (lastUsedClientKey) {
|
|
1717
|
+
const { selectedClientKey } = await promptModule([
|
|
1718
|
+
{
|
|
1719
|
+
type: 'list',
|
|
1720
|
+
name: 'selectedClientKey',
|
|
1721
|
+
message: 'Select game client key for debugging:',
|
|
1722
|
+
choices: [{
|
|
1723
|
+
name: `${lastUsedClientKey} (last used)`,
|
|
1724
|
+
value: lastUsedClientKey,
|
|
1725
|
+
}, {
|
|
1726
|
+
name: 'Add a new client key',
|
|
1727
|
+
value: 'new',
|
|
1728
|
+
}],
|
|
1729
|
+
},
|
|
1730
|
+
]);
|
|
1731
|
+
if (selectedClientKey === 'new') {
|
|
1732
|
+
/**
|
|
1733
|
+
* 输入新的 clientKey
|
|
1734
|
+
*/
|
|
1735
|
+
const { clientKey } = await promptModule([
|
|
1736
|
+
{
|
|
1737
|
+
type: 'input',
|
|
1738
|
+
name: 'clientKey',
|
|
1739
|
+
message: 'Input new client key:',
|
|
1740
|
+
validate: input => {
|
|
1741
|
+
if (!input) {
|
|
1742
|
+
return 'Client key is required, please input client key';
|
|
1743
|
+
}
|
|
1744
|
+
return true;
|
|
1745
|
+
},
|
|
1746
|
+
},
|
|
1747
|
+
]);
|
|
1748
|
+
setTTMGRC({
|
|
1749
|
+
clientKey,
|
|
1750
|
+
});
|
|
1289
1751
|
}
|
|
1290
1752
|
else {
|
|
1291
|
-
//
|
|
1292
|
-
|
|
1753
|
+
// 并将 selectedClientKey 放到最前面
|
|
1754
|
+
setTTMGRC({
|
|
1755
|
+
clientKey: selectedClientKey,
|
|
1756
|
+
});
|
|
1293
1757
|
}
|
|
1294
1758
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1759
|
+
else {
|
|
1760
|
+
// 没有历史 clientKey,直接输入新的 clientKey
|
|
1761
|
+
const { clientKey } = await promptModule([
|
|
1762
|
+
{
|
|
1763
|
+
type: 'input',
|
|
1764
|
+
name: 'clientKey',
|
|
1765
|
+
message: 'Input your Client Key:',
|
|
1766
|
+
validate: input => {
|
|
1767
|
+
if (!input) {
|
|
1768
|
+
return 'Client key is required, please input client key';
|
|
1769
|
+
}
|
|
1770
|
+
return true;
|
|
1771
|
+
},
|
|
1772
|
+
},
|
|
1773
|
+
]);
|
|
1774
|
+
setTTMGRC({
|
|
1775
|
+
clientKey,
|
|
1776
|
+
});
|
|
1299
1777
|
}
|
|
1300
|
-
// --- 服务启动成功后的逻辑 ---
|
|
1301
|
-
// @ts-ignore
|
|
1302
|
-
const finalPort = server.address().port; // 从成功的 server 实例安全地获取最终端口
|
|
1303
|
-
console.log(chalk.green.bold(`TTMG`), chalk.green(`v${devToolVersion}`), chalk.gray(`ready in`), chalk.bold(`${Date.now() - startTime}ms`));
|
|
1304
|
-
const baseUrl = `http://localhost:${finalPort}?v=${devToolVersion}`;
|
|
1305
|
-
showTips({ server: baseUrl });
|
|
1306
|
-
openUrl(baseUrl);
|
|
1307
1778
|
}
|
|
1308
1779
|
|
|
1309
1780
|
/**
|
|
1310
|
-
*
|
|
1311
|
-
* 它允许你注册、注销和触发具有严格类型检查的事件。
|
|
1781
|
+
* 检查当前目录是否为 Mini Game 项目的入口目录
|
|
1312
1782
|
*/
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1783
|
+
function checkEntry() {
|
|
1784
|
+
const entryFiles = NATIVE_GAME_ENTRY_FILES.map(file => path.join(process.cwd(), file));
|
|
1785
|
+
const foundEntryFile = entryFiles.find(file => fs.existsSync(file));
|
|
1786
|
+
if (!foundEntryFile) {
|
|
1787
|
+
/**
|
|
1788
|
+
* 如果当前目录下没有任何一个入口文件,提示用户检查当前目录是否为 Mini Game 项目的入口目录,需要提醒开发者进入到游戏项目根目录进行调试
|
|
1789
|
+
*/
|
|
1790
|
+
console.error(chalk.red.bold(`Current directory is not a Mini Game project entry directory, please enter the game project root directory for debugging`));
|
|
1791
|
+
process.exit(1);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function getDevToolVersion() {
|
|
1796
|
+
try {
|
|
1797
|
+
return JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8')).version;
|
|
1319
1798
|
}
|
|
1799
|
+
catch (error) {
|
|
1800
|
+
return 'unknown';
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
function showTips(context) {
|
|
1805
|
+
console.log(chalk.gray('─────────────────────────────────────────────'));
|
|
1806
|
+
console.log(chalk.yellow.bold('1.') +
|
|
1807
|
+
' The QR code page will be opened automatically in Chrome.');
|
|
1808
|
+
console.log(' If it fails, please open the following link manually:');
|
|
1809
|
+
console.log(' ' + chalk.cyan.underline(context.server));
|
|
1810
|
+
console.log('');
|
|
1811
|
+
console.log(chalk.yellow.bold('2.') +
|
|
1812
|
+
' The debugging service will automatically compile your game assets.');
|
|
1813
|
+
console.log(' Any changes in your game directory will trigger recompilation.');
|
|
1814
|
+
console.log(' You can debug the updated content when upload is completed.');
|
|
1815
|
+
console.log('');
|
|
1816
|
+
console.log(chalk.yellow.bold('3.') +
|
|
1817
|
+
' After scanning the QR code with your phone for Test User authentication,');
|
|
1818
|
+
console.log(' the compiled code package will be uploaded to the client automatically.');
|
|
1819
|
+
console.log(' Game debugging will start right away.');
|
|
1820
|
+
console.log(chalk.gray('─────────────────────────────────────────────'));
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
/**
|
|
1824
|
+
* 辅助函数:将当前工作目录打包为 Buffer
|
|
1825
|
+
* @param customIgnores - 可选的自定义忽略规则数组 (支持 glob 模式,如 ['dist/**', '*.log'])
|
|
1826
|
+
*/
|
|
1827
|
+
const zipCwdToBuffer = (customIgnores = []) => {
|
|
1828
|
+
return new Promise((resolve, reject) => {
|
|
1829
|
+
const chunks = [];
|
|
1830
|
+
const output = new stream.Writable({
|
|
1831
|
+
write(chunk, encoding, next) {
|
|
1832
|
+
chunks.push(chunk);
|
|
1833
|
+
next();
|
|
1834
|
+
},
|
|
1835
|
+
});
|
|
1836
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
1837
|
+
output.on('finish', () => {
|
|
1838
|
+
resolve(Buffer.concat(chunks));
|
|
1839
|
+
});
|
|
1840
|
+
archive.on('error', err => {
|
|
1841
|
+
reject(err);
|
|
1842
|
+
});
|
|
1843
|
+
archive.pipe(output);
|
|
1844
|
+
const cwd = process.cwd();
|
|
1845
|
+
// 1. 基础忽略列表 (建议保留这些基础规则,防止包过大)
|
|
1846
|
+
const defaultIgnores = [
|
|
1847
|
+
'node_modules/**',
|
|
1848
|
+
'.git/**',
|
|
1849
|
+
'.DS_Store',
|
|
1850
|
+
ttmgPack.TTMG_TEMP_DIR,
|
|
1851
|
+
'*.zip', // 忽略自身生成的 zip
|
|
1852
|
+
];
|
|
1853
|
+
// 2. 合并自定义规则
|
|
1854
|
+
// Set 去重,防止重复添加
|
|
1855
|
+
const finalIgnores = Array.from(new Set([...defaultIgnores, ...customIgnores]));
|
|
1856
|
+
// 3. 执行打包
|
|
1857
|
+
// 注意:ignore 规则必须使用正斜杠 /,即使在 Windows 下
|
|
1858
|
+
archive.glob('**/*', {
|
|
1859
|
+
cwd: cwd,
|
|
1860
|
+
ignore: finalIgnores,
|
|
1861
|
+
dot: true, // 包含 .env 等点文件
|
|
1862
|
+
});
|
|
1863
|
+
archive.finalize();
|
|
1864
|
+
});
|
|
1865
|
+
};
|
|
1866
|
+
|
|
1867
|
+
const BASE_URL = 'https://developers.tiktok.com';
|
|
1868
|
+
const DEV_HEADERS = {
|
|
1869
|
+
// 'x-use-ppe': '1',
|
|
1870
|
+
// 'x-tt-env': UNITY_PPE_ENV,
|
|
1871
|
+
// 'x-tt-target-idc': 'sg',
|
|
1872
|
+
};
|
|
1873
|
+
const TTMG_TEMP_DIR = '__TTMG_TEMP__';
|
|
1874
|
+
const WASM_SPLIT_CACHE_DIR = `${TTMG_TEMP_DIR}/wasmcode/`;
|
|
1875
|
+
const WASM_SYMBOL_FILE_NAME = 'webgl.symbols.json';
|
|
1876
|
+
const UNITY_WASM_SPLIT_CONFIG_FIELD_SCHEME = {
|
|
1877
|
+
ENABLEWASMCOLLECT: `"$ENABLEWASMCOLLECT"`,
|
|
1878
|
+
enableWasmSplit: `"$ENABLEWASMSPLIT"`,
|
|
1879
|
+
ORIGINALWASMMD5: `$ORIGINALWASMMD5`,
|
|
1880
|
+
GLOBALVARLIST: `["$GLOBALVARLIST"]`,
|
|
1881
|
+
SUBJSURL: `$SUBJSURL`,
|
|
1882
|
+
WASMTABLESIZE: `"$WASMTABLESIZE"`,
|
|
1883
|
+
USINGWASMH5: `"$USINGWASMH5"`,
|
|
1884
|
+
IOS_CODE_FILE_MD5: `$IOS_CODE_FILE_MD5`,
|
|
1885
|
+
ANDROID_CODE_FILE_MD5: `$ANDROID_CODE_FILE_MD5`,
|
|
1886
|
+
ANDROID_SUB_CODE_FILE_MD5: `$SUB_CODE_FILE_MD5`,
|
|
1887
|
+
WASMSPLITVERSION: `"$WASMSPLITVERSION"`,
|
|
1888
|
+
ENABLEWASMSPLIT: `"$ENABLEWASMSPLIT"`,
|
|
1889
|
+
IOS_SUB_JS_FILE_CONFIG: `"$IOS_SUB_JS_FILE_CONFIG"`,
|
|
1890
|
+
};
|
|
1891
|
+
|
|
1892
|
+
const DIR_SPLIT = 'split';
|
|
1893
|
+
const WASM_SPLIT_SUBPACKAGE_CONFIG = {
|
|
1894
|
+
origin: {
|
|
1895
|
+
name: 'wasmcode',
|
|
1896
|
+
root: 'wasmcode/',
|
|
1897
|
+
},
|
|
1898
|
+
androidMain: {
|
|
1899
|
+
name: 'wasmcode-android',
|
|
1900
|
+
root: 'wasmcode-android/',
|
|
1901
|
+
},
|
|
1902
|
+
androidSub: {
|
|
1903
|
+
name: 'wasmcode1-android',
|
|
1904
|
+
root: 'wasmcode1-android/',
|
|
1905
|
+
},
|
|
1906
|
+
ios: {
|
|
1907
|
+
root: 'wasmcode-ios/',
|
|
1908
|
+
},
|
|
1909
|
+
iosMain: {
|
|
1910
|
+
name: 'wasmcode-ios',
|
|
1911
|
+
root: 'wasmcode-ios/',
|
|
1912
|
+
},
|
|
1913
|
+
iosSub: {
|
|
1914
|
+
name: 'wasmcode1-ios',
|
|
1915
|
+
root: 'wasmcode1-ios/',
|
|
1916
|
+
},
|
|
1917
|
+
};
|
|
1918
|
+
const WASM_FILENAME_SUFFIX = '.webgl.wasm.code.unityweb.wasm';
|
|
1919
|
+
const BR_SUFFIX = '.br';
|
|
1920
|
+
// 输出 JSON 格式
|
|
1921
|
+
const JSON_INDENT = 2;
|
|
1922
|
+
const JSON_EOL = '\n';
|
|
1923
|
+
// 并发与重试
|
|
1924
|
+
const CONCURRENCY_LIMIT = 2;
|
|
1925
|
+
const DOWNLOAD_RETRY = 3;
|
|
1926
|
+
const WASM_SPLIT_CONFIG_FILE_NAME = 'webgl-wasm-split.js';
|
|
1927
|
+
|
|
1928
|
+
// prepare.ts
|
|
1929
|
+
// 若你的 request 是 axios:你可以添加 maxBodyLength/ maxContentLength 等参数
|
|
1930
|
+
// 若是 got:可直接传 form 实例
|
|
1931
|
+
async function startPrepare(params) {
|
|
1932
|
+
const form = new FormData$1();
|
|
1933
|
+
form.append('desc', params.desc);
|
|
1934
|
+
form.append('wasm_md5', params.wasm_md5);
|
|
1935
|
+
form.append('with_ios', 'true');
|
|
1936
|
+
// 二进制字段:用 ReadStream(推荐)或 Buffer
|
|
1937
|
+
form.append('wasm_file', fs$1.createReadStream(path$1.join(process.cwd(), params.wasm_file_path)), {
|
|
1938
|
+
filename: path$1.basename(params.wasm_file_path),
|
|
1939
|
+
// 部分后端会依赖 content-type;如果不确定就用 application/octet-stream
|
|
1940
|
+
contentType: 'application/wasm',
|
|
1941
|
+
});
|
|
1320
1942
|
/**
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
1324
|
-
*
|
|
1943
|
+
* 兼容 WASM_SYMBOL_FILE_NAME
|
|
1944
|
+
* case 1:项目根目录有 webgl.symbols.json 文件
|
|
1945
|
+
* case 2:项目根目录没有 webgl.symbols.json 文件,但 TTMG_TEMP_DIR 有
|
|
1946
|
+
* 优先读 2
|
|
1325
1947
|
*/
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
}
|
|
1330
|
-
this.listeners.get(eventName).add(listener);
|
|
1331
|
-
// 返回一个便捷的取消订阅函数
|
|
1332
|
-
return () => this.off(eventName, listener);
|
|
1948
|
+
let symbolFilePath = path$1.join(process.cwd(), TTMG_TEMP_DIR, WASM_SYMBOL_FILE_NAME);
|
|
1949
|
+
if (!fs$1.existsSync(symbolFilePath)) {
|
|
1950
|
+
symbolFilePath = path$1.join(process.cwd(), WASM_SYMBOL_FILE_NAME);
|
|
1333
1951
|
}
|
|
1334
1952
|
/**
|
|
1335
|
-
*
|
|
1336
|
-
* @param eventName 要注销的事件名称。
|
|
1337
|
-
* @param listener 之前通过 on() 方法注册的回调函数实例。
|
|
1953
|
+
* 判断是否有 symbol 文件,有则上传,没有直接接口报错
|
|
1338
1954
|
*/
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1955
|
+
if (!fs$1.existsSync(symbolFilePath)) {
|
|
1956
|
+
return {
|
|
1957
|
+
error: {
|
|
1958
|
+
code: 400,
|
|
1959
|
+
message: `${WASM_SYMBOL_FILE_NAME} not found at ${path$1.join(process.cwd())},use unity plugin to rebuild`,
|
|
1960
|
+
client_key: params.client_key,
|
|
1961
|
+
},
|
|
1962
|
+
data: null,
|
|
1963
|
+
ctx: {
|
|
1964
|
+
logid: '',
|
|
1965
|
+
httpStatusCode: 400,
|
|
1966
|
+
},
|
|
1967
|
+
};
|
|
1347
1968
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1969
|
+
form.append('wasm_symbol_file', fs$1.createReadStream(symbolFilePath), {
|
|
1970
|
+
filename: WASM_SYMBOL_FILE_NAME,
|
|
1971
|
+
contentType: 'application/octet-stream',
|
|
1972
|
+
});
|
|
1973
|
+
// 关键:用 form.getHeaders() 获取带 boundary 的 Content-Type
|
|
1974
|
+
const formHeaders = form.getHeaders();
|
|
1975
|
+
return request({
|
|
1976
|
+
url: `${BASE_URL}/api/stark_wasm/v4/post/prepare`,
|
|
1977
|
+
method: 'POST',
|
|
1978
|
+
headers: {
|
|
1979
|
+
...DEV_HEADERS,
|
|
1980
|
+
...formHeaders, // 包含正确的 multipart/form-data; boundary=...
|
|
1981
|
+
},
|
|
1982
|
+
params: {
|
|
1983
|
+
client_key: params.client_key,
|
|
1984
|
+
with_ios: true,
|
|
1985
|
+
},
|
|
1986
|
+
data: form,
|
|
1987
|
+
// 若 request 基于 axios,建议加上以下两项以支持大文件:
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
async function withRetry(fn, retries = 3) {
|
|
1992
|
+
let lastErr;
|
|
1993
|
+
for (let i = 0; i < retries; i++) {
|
|
1994
|
+
try {
|
|
1995
|
+
return await fn();
|
|
1996
|
+
}
|
|
1997
|
+
catch (e) {
|
|
1998
|
+
lastErr = e;
|
|
1999
|
+
const delay = 2 ** i * 300;
|
|
2000
|
+
await new Promise(r => setTimeout(r, delay));
|
|
1366
2001
|
}
|
|
1367
2002
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
2003
|
+
throw lastErr;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
function updateWasmSplitConfig(fields) {
|
|
2007
|
+
for (const field in fields) {
|
|
2008
|
+
const value = fields[field];
|
|
2009
|
+
const isString = typeof value === 'string';
|
|
2010
|
+
const valueStr = isString ? value : String(value);
|
|
2011
|
+
const configFilePath = path.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME);
|
|
2012
|
+
const placeholder = UNITY_WASM_SPLIT_CONFIG_FIELD_SCHEME[field];
|
|
2013
|
+
const config = fs.readFileSync(configFilePath, 'utf-8');
|
|
2014
|
+
// 将占位符替换为 true/false 字面量
|
|
2015
|
+
// 用正则?因为 placeholder 是一个字符串,可能包含特殊字符
|
|
2016
|
+
const updated = config.replace(placeholder, valueStr);
|
|
2017
|
+
fs.writeFileSync(configFilePath, updated, 'utf-8');
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
async function compressWasmFile(wasmFilePath, compressedFilePath) {
|
|
2022
|
+
const arrayBuffer = await fs.promises.readFile(wasmFilePath);
|
|
2023
|
+
const compressedArrayBuffer = (await compressArrayBuffer(arrayBuffer));
|
|
2024
|
+
fs.writeFileSync(compressedFilePath, Buffer.from(compressedArrayBuffer));
|
|
2025
|
+
// 删除原始文件
|
|
2026
|
+
fs.unlinkSync(wasmFilePath);
|
|
2027
|
+
}
|
|
2028
|
+
function compressArrayBuffer(arrayBuffer) {
|
|
2029
|
+
return new Promise((resolve, reject) => {
|
|
2030
|
+
const compressStream = zlib.createBrotliCompress();
|
|
2031
|
+
compressStream.write(Buffer.from(arrayBuffer));
|
|
2032
|
+
compressStream.end();
|
|
2033
|
+
const compressedChunks = [];
|
|
2034
|
+
compressStream.on('data', chunk => compressedChunks.push(chunk));
|
|
2035
|
+
compressStream.on('end', () => resolve(Buffer.concat(compressedChunks)));
|
|
2036
|
+
compressStream.on('error', reject);
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
function keepCacheSync({ entryDir, originalWasmPath, originalSplitConfigPath, }) {
|
|
2041
|
+
const cacheDir = path__namespace.join(entryDir, WASM_SPLIT_CACHE_DIR);
|
|
2042
|
+
if (!fs__namespace.existsSync(cacheDir)) {
|
|
2043
|
+
fs__namespace.mkdirSync(cacheDir, { recursive: true });
|
|
2044
|
+
}
|
|
2045
|
+
const wasmCachePath = path__namespace.join(cacheDir, path__namespace.basename(originalWasmPath));
|
|
2046
|
+
if (!fs__namespace.existsSync(wasmCachePath)) {
|
|
2047
|
+
fs__namespace.copyFileSync(path__namespace.join(entryDir, originalWasmPath), wasmCachePath);
|
|
2048
|
+
}
|
|
2049
|
+
const splitConfigCachePath = path__namespace.join(cacheDir, path__namespace.basename(originalSplitConfigPath));
|
|
2050
|
+
if (!fs__namespace.existsSync(splitConfigCachePath)) {
|
|
2051
|
+
fs__namespace.copyFileSync(path__namespace.join(entryDir, originalSplitConfigPath), splitConfigCachePath);
|
|
1374
2052
|
}
|
|
1375
2053
|
/**
|
|
1376
|
-
*
|
|
2054
|
+
* 保存 game.json
|
|
1377
2055
|
*/
|
|
1378
|
-
|
|
1379
|
-
|
|
2056
|
+
const gameJsonPath = path__namespace.join(entryDir, 'game.json');
|
|
2057
|
+
const gameJsonCachePath = path__namespace.join(cacheDir, 'game.json');
|
|
2058
|
+
if (!fs__namespace.existsSync(gameJsonCachePath)) {
|
|
2059
|
+
fs__namespace.copyFileSync(gameJsonPath, gameJsonCachePath);
|
|
1380
2060
|
}
|
|
2061
|
+
return {
|
|
2062
|
+
cacheDir,
|
|
2063
|
+
};
|
|
1381
2064
|
}
|
|
1382
|
-
const eventEmitter = new TypedEventEmitter();
|
|
1383
2065
|
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
2066
|
+
async function downloadPrepared(data) {
|
|
2067
|
+
wsServer.sendUnitySplitStatus({
|
|
2068
|
+
status: 'star_fetch_prepared_wasm_url',
|
|
2069
|
+
});
|
|
2070
|
+
const res = await request({
|
|
2071
|
+
url: `${BASE_URL}/api/stark_wasm/v4/post/download_prepared`,
|
|
2072
|
+
method: 'POST',
|
|
2073
|
+
headers: DEV_HEADERS,
|
|
2074
|
+
data,
|
|
2075
|
+
});
|
|
2076
|
+
wsServer.sendUnitySplitStatus({
|
|
2077
|
+
status: 'fetch_prepared_wasm_url_done',
|
|
2078
|
+
});
|
|
2079
|
+
try {
|
|
2080
|
+
const downloadUrl = res?.data?.result?.download_url;
|
|
2081
|
+
const willReplaceWasmPath = path.join(process.cwd(), data.wasm_path);
|
|
2082
|
+
if (downloadUrl) {
|
|
2083
|
+
const { cacheDir } = keepCacheSync({
|
|
2084
|
+
entryDir: process.cwd(),
|
|
2085
|
+
originalWasmPath: data.wasm_path,
|
|
2086
|
+
originalSplitConfigPath: WASM_SPLIT_CONFIG_FILE_NAME,
|
|
2087
|
+
});
|
|
2088
|
+
if (downloadUrl.includes('.br')) {
|
|
2089
|
+
const tempWasmPath = path.join(cacheDir, '__temp__.wasm.br');
|
|
2090
|
+
wsServer.sendUnitySplitStatus({
|
|
2091
|
+
status: 'start_download_prepared_wasm',
|
|
2092
|
+
url: downloadUrl,
|
|
2093
|
+
});
|
|
2094
|
+
await download(downloadUrl, tempWasmPath);
|
|
2095
|
+
/**
|
|
2096
|
+
* 下载完成后需要进行 br 并替换 codePath 对应的文件后再返回成功
|
|
2097
|
+
*/
|
|
2098
|
+
fs$1.copyFileSync(tempWasmPath, willReplaceWasmPath);
|
|
2099
|
+
wsServer.sendUnitySplitStatus({
|
|
2100
|
+
status: 'download_prepared_wasm_done',
|
|
2101
|
+
url: downloadUrl,
|
|
1400
2102
|
});
|
|
1401
2103
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
* 客户端完成扫码成功,返回客户端的 host 和 port
|
|
1437
|
-
*/
|
|
1438
|
-
case 'startScanQRcode': {
|
|
1439
|
-
const payload = clientMessage.payload;
|
|
1440
|
-
console.log('startQRcode', payload);
|
|
1441
|
-
this.send({
|
|
1442
|
-
method: 'startScanQRcode',
|
|
1443
|
-
});
|
|
1444
|
-
break;
|
|
1445
|
-
}
|
|
1446
|
-
case 'scanQRCodeResult': {
|
|
1447
|
-
const payload = clientMessage.payload || {};
|
|
1448
|
-
const { host, port, wsPort, errMsg, isSuccess } = payload;
|
|
1449
|
-
console.log('scanQRCodeResult', payload);
|
|
1450
|
-
if (isSuccess) {
|
|
1451
|
-
store.setState({
|
|
1452
|
-
clientHttpPort: port,
|
|
1453
|
-
clientHost: host,
|
|
1454
|
-
clientWsPort: wsPort,
|
|
1455
|
-
});
|
|
1456
|
-
this.send({
|
|
1457
|
-
method: 'scanQRCodeSuccess',
|
|
1458
|
-
payload: {
|
|
1459
|
-
clientHttpPort: port,
|
|
1460
|
-
clientHost: host,
|
|
1461
|
-
clientWsPort: wsPort,
|
|
1462
|
-
},
|
|
1463
|
-
});
|
|
1464
|
-
console.log('scanQRcodeSuccess');
|
|
1465
|
-
}
|
|
1466
|
-
else {
|
|
1467
|
-
this.send({
|
|
1468
|
-
method: 'scanQRCodeFailed',
|
|
1469
|
-
payload: {
|
|
1470
|
-
errMsg,
|
|
1471
|
-
},
|
|
1472
|
-
});
|
|
1473
|
-
}
|
|
1474
|
-
break;
|
|
1475
|
-
}
|
|
1476
|
-
// 手动绑定客户端调试服务
|
|
1477
|
-
// 待废弃
|
|
1478
|
-
case 'shareDevParams':
|
|
1479
|
-
const payload = clientMessage.payload;
|
|
1480
|
-
const { host, port, wsPort } = payload;
|
|
1481
|
-
store.setState({
|
|
1482
|
-
clientHttpPort: port,
|
|
1483
|
-
clientHost: host,
|
|
1484
|
-
clientWsPort: wsPort,
|
|
1485
|
-
clientWsHost: host,
|
|
1486
|
-
});
|
|
1487
|
-
this.send({
|
|
1488
|
-
method: 'scanQRCodeSuccess',
|
|
1489
|
-
payload: {
|
|
1490
|
-
clientHttpPort: port,
|
|
1491
|
-
clientHost: host,
|
|
1492
|
-
clientWsPort: wsPort,
|
|
1493
|
-
},
|
|
1494
|
-
});
|
|
1495
|
-
break;
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
2104
|
+
else {
|
|
2105
|
+
const tempWasmPath = path.join(cacheDir, '__temp__.wasm');
|
|
2106
|
+
wsServer.sendUnitySplitStatus({
|
|
2107
|
+
status: 'start_download_prepared_wasm',
|
|
2108
|
+
url: downloadUrl,
|
|
2109
|
+
});
|
|
2110
|
+
await download(downloadUrl, tempWasmPath);
|
|
2111
|
+
wsServer.sendUnitySplitStatus({
|
|
2112
|
+
status: 'download_prepared_wasm_done',
|
|
2113
|
+
url: downloadUrl,
|
|
2114
|
+
});
|
|
2115
|
+
/**
|
|
2116
|
+
* 下载完成后需要进行 br 并替换 codePath 对应的文件后再返回成功
|
|
2117
|
+
*/
|
|
2118
|
+
wsServer.sendUnitySplitStatus({
|
|
2119
|
+
status: 'start_compress_prepared_wasm',
|
|
2120
|
+
});
|
|
2121
|
+
await compressWasmFile(tempWasmPath, willReplaceWasmPath);
|
|
2122
|
+
wsServer.sendUnitySplitStatus({
|
|
2123
|
+
status: 'compress_prepared_wasm_done',
|
|
2124
|
+
url: downloadUrl,
|
|
2125
|
+
});
|
|
2126
|
+
wsServer.sendUnitySplitStatus({
|
|
2127
|
+
status: 'write_compress_prepared_wasm_done',
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
wsServer.sendUnitySplitStatus({
|
|
2131
|
+
status: 'start_update_wasm_split_config',
|
|
2132
|
+
});
|
|
2133
|
+
/**
|
|
2134
|
+
* 读取 webgl-wasm-split.js内容,将 enableWasmCollect 设为 true
|
|
2135
|
+
*/
|
|
2136
|
+
updateWasmSplitConfig({
|
|
2137
|
+
ENABLEWASMCOLLECT: true,
|
|
1498
2138
|
});
|
|
2139
|
+
wsServer.sendUnitySplitStatus({
|
|
2140
|
+
status: 'update_wasm_split_config_done',
|
|
2141
|
+
});
|
|
2142
|
+
return {
|
|
2143
|
+
isSuccess: true,
|
|
2144
|
+
ctx: res?.ctx,
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
else {
|
|
2148
|
+
return {
|
|
2149
|
+
isSuccess: false,
|
|
2150
|
+
error: {
|
|
2151
|
+
code: res.data?.code,
|
|
2152
|
+
message: res.data?.message,
|
|
2153
|
+
},
|
|
2154
|
+
ctx: res?.ctx,
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
catch (error) {
|
|
2159
|
+
return {
|
|
2160
|
+
isSuccess: false,
|
|
2161
|
+
error: {
|
|
2162
|
+
code: res.data?.code,
|
|
2163
|
+
message: error.message,
|
|
2164
|
+
},
|
|
2165
|
+
ctx: res?.ctx,
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
async function getCollectedFuncIds({ client_key, wasm_md5, }) {
|
|
2171
|
+
return request({
|
|
2172
|
+
url: `${BASE_URL}/api/stark_wasm/v4/get/collectedfuncids`,
|
|
2173
|
+
method: 'GET',
|
|
2174
|
+
headers: DEV_HEADERS,
|
|
2175
|
+
params: {
|
|
2176
|
+
client_key,
|
|
2177
|
+
wasm_md5,
|
|
2178
|
+
},
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
async function setCollect({ client_key, wasm_md5, }) {
|
|
2183
|
+
return request({
|
|
2184
|
+
url: `${BASE_URL}/api/stark_wasm/v4/post/set_collecting`,
|
|
2185
|
+
method: 'POST',
|
|
2186
|
+
data: {
|
|
2187
|
+
client_key,
|
|
2188
|
+
wasm_md5,
|
|
2189
|
+
},
|
|
2190
|
+
headers: DEV_HEADERS,
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
async function getCollecttingInfo({ client_key, wasm_md5, }) {
|
|
2195
|
+
return request({
|
|
2196
|
+
url: `${BASE_URL}/api/stark_wasm/v4/get/funccollect`,
|
|
2197
|
+
method: 'GET',
|
|
2198
|
+
headers: DEV_HEADERS,
|
|
2199
|
+
params: {
|
|
2200
|
+
client_key,
|
|
2201
|
+
wasm_md5,
|
|
2202
|
+
},
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// /api/stark_wasm/v4/post/split
|
|
2207
|
+
async function startSplit({ client_key, wasm_md5, }) {
|
|
2208
|
+
return request({
|
|
2209
|
+
url: `${BASE_URL}/api/stark_wasm/v4/post/split`,
|
|
2210
|
+
method: 'POST',
|
|
2211
|
+
headers: {
|
|
2212
|
+
...DEV_HEADERS,
|
|
2213
|
+
},
|
|
2214
|
+
data: {
|
|
2215
|
+
client_key,
|
|
2216
|
+
wasm_md5,
|
|
2217
|
+
},
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
/*
|
|
2222
|
+
How it works:
|
|
2223
|
+
`this.#head` is an instance of `Node` which keeps track of its current value and nests another instance of `Node` that keeps the value that comes after it. When a value is provided to `.enqueue()`, the code needs to iterate through `this.#head`, going deeper and deeper to find the last value. However, iterating through every single item is slow. This problem is solved by saving a reference to the last value as `this.#tail` so that it can reference it to add a new value.
|
|
2224
|
+
*/
|
|
2225
|
+
|
|
2226
|
+
class Node {
|
|
2227
|
+
value;
|
|
2228
|
+
next;
|
|
2229
|
+
|
|
2230
|
+
constructor(value) {
|
|
2231
|
+
this.value = value;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
class Queue {
|
|
2236
|
+
#head;
|
|
2237
|
+
#tail;
|
|
2238
|
+
#size;
|
|
2239
|
+
|
|
2240
|
+
constructor() {
|
|
2241
|
+
this.clear();
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
enqueue(value) {
|
|
2245
|
+
const node = new Node(value);
|
|
2246
|
+
|
|
2247
|
+
if (this.#head) {
|
|
2248
|
+
this.#tail.next = node;
|
|
2249
|
+
this.#tail = node;
|
|
2250
|
+
} else {
|
|
2251
|
+
this.#head = node;
|
|
2252
|
+
this.#tail = node;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
this.#size++;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
dequeue() {
|
|
2259
|
+
const current = this.#head;
|
|
2260
|
+
if (!current) {
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
this.#head = this.#head.next;
|
|
2265
|
+
this.#size--;
|
|
2266
|
+
|
|
2267
|
+
// Clean up tail reference when queue becomes empty
|
|
2268
|
+
if (!this.#head) {
|
|
2269
|
+
this.#tail = undefined;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
return current.value;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
peek() {
|
|
2276
|
+
if (!this.#head) {
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
return this.#head.value;
|
|
2281
|
+
|
|
2282
|
+
// TODO: Node.js 18.
|
|
2283
|
+
// return this.#head?.value;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
clear() {
|
|
2287
|
+
this.#head = undefined;
|
|
2288
|
+
this.#tail = undefined;
|
|
2289
|
+
this.#size = 0;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
get size() {
|
|
2293
|
+
return this.#size;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
* [Symbol.iterator]() {
|
|
2297
|
+
let current = this.#head;
|
|
2298
|
+
|
|
2299
|
+
while (current) {
|
|
2300
|
+
yield current.value;
|
|
2301
|
+
current = current.next;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
* drain() {
|
|
2306
|
+
while (this.#head) {
|
|
2307
|
+
yield this.dequeue();
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
const AsyncResource = {
|
|
2313
|
+
bind(fn, _type, thisArg) {
|
|
2314
|
+
return fn.bind(thisArg);
|
|
2315
|
+
},
|
|
2316
|
+
};
|
|
2317
|
+
|
|
2318
|
+
function pLimit(concurrency) {
|
|
2319
|
+
if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
|
|
2320
|
+
throw new TypeError('Expected `concurrency` to be a number from 1 and up');
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
const queue = new Queue();
|
|
2324
|
+
let activeCount = 0;
|
|
2325
|
+
|
|
2326
|
+
const next = () => {
|
|
2327
|
+
activeCount--;
|
|
2328
|
+
|
|
2329
|
+
if (queue.size > 0) {
|
|
2330
|
+
queue.dequeue()();
|
|
2331
|
+
}
|
|
2332
|
+
};
|
|
2333
|
+
|
|
2334
|
+
const run = async (function_, resolve, arguments_) => {
|
|
2335
|
+
activeCount++;
|
|
2336
|
+
|
|
2337
|
+
const result = (async () => function_(...arguments_))();
|
|
2338
|
+
|
|
2339
|
+
resolve(result);
|
|
2340
|
+
|
|
2341
|
+
try {
|
|
2342
|
+
await result;
|
|
2343
|
+
} catch {}
|
|
2344
|
+
|
|
2345
|
+
next();
|
|
2346
|
+
};
|
|
2347
|
+
|
|
2348
|
+
const enqueue = (function_, resolve, arguments_) => {
|
|
2349
|
+
queue.enqueue(
|
|
2350
|
+
AsyncResource.bind(run.bind(undefined, function_, resolve, arguments_)),
|
|
2351
|
+
);
|
|
2352
|
+
|
|
2353
|
+
(async () => {
|
|
2354
|
+
// This function needs to wait until the next microtask before comparing
|
|
2355
|
+
// `activeCount` to `concurrency`, because `activeCount` is updated asynchronously
|
|
2356
|
+
// when the run function is dequeued and called. The comparison in the if-statement
|
|
2357
|
+
// needs to happen asynchronously as well to get an up-to-date value for `activeCount`.
|
|
2358
|
+
await Promise.resolve();
|
|
2359
|
+
|
|
2360
|
+
if (activeCount < concurrency && queue.size > 0) {
|
|
2361
|
+
queue.dequeue()();
|
|
2362
|
+
}
|
|
2363
|
+
})();
|
|
2364
|
+
};
|
|
2365
|
+
|
|
2366
|
+
const generator = (function_, ...arguments_) => new Promise(resolve => {
|
|
2367
|
+
enqueue(function_, resolve, arguments_);
|
|
2368
|
+
});
|
|
2369
|
+
|
|
2370
|
+
Object.defineProperties(generator, {
|
|
2371
|
+
activeCount: {
|
|
2372
|
+
get: () => activeCount,
|
|
2373
|
+
},
|
|
2374
|
+
pendingCount: {
|
|
2375
|
+
get: () => queue.size,
|
|
2376
|
+
},
|
|
2377
|
+
clearQueue: {
|
|
2378
|
+
value() {
|
|
2379
|
+
queue.clear();
|
|
2380
|
+
},
|
|
2381
|
+
},
|
|
2382
|
+
});
|
|
2383
|
+
|
|
2384
|
+
return generator;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
function updateSubpackageConfigSync() {
|
|
2388
|
+
const gameJsonPath = path__namespace.join(process.cwd(), SUBPACKAGE_CONFIG_FILE_NAME);
|
|
2389
|
+
const raw = fs__namespace.readFileSync(gameJsonPath, 'utf-8');
|
|
2390
|
+
const gameJson = JSON.parse(raw);
|
|
2391
|
+
const fieldName = SUBAPCKAGE_FILED_NAMES.find(k => k in gameJson) ??
|
|
2392
|
+
SUBAPCKAGE_FILED_NAMES[0];
|
|
2393
|
+
if (!gameJson[fieldName])
|
|
2394
|
+
gameJson[fieldName] = [];
|
|
2395
|
+
const subpackages = gameJson[fieldName];
|
|
2396
|
+
// 删除老的 'wasmcode'
|
|
2397
|
+
const filtered = subpackages.filter(s => s.name !== WASM_SPLIT_SUBPACKAGE_CONFIG.origin.name);
|
|
2398
|
+
/**
|
|
2399
|
+
* 基于 SUBPACKAGE_CONFIG_FILE_NAME 更新 subpackages
|
|
2400
|
+
*/
|
|
2401
|
+
filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain);
|
|
2402
|
+
filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub);
|
|
2403
|
+
filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.iosMain);
|
|
2404
|
+
filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.iosSub);
|
|
2405
|
+
// 合并去重:存在则更新 root,不存在则新增
|
|
2406
|
+
const map = new Map(filtered.map(s => [s.name, s]));
|
|
2407
|
+
gameJson[fieldName] = Array.from(map.values());
|
|
2408
|
+
fs__namespace.writeFileSync(gameJsonPath, JSON.stringify(gameJson, null, JSON_INDENT) + JSON_EOL);
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
async function downloadAndCompress(opts) {
|
|
2412
|
+
const { startDownloadStatus, downloadDoneStatus, startCompressStatus, compressDoneStatus, url, out, enableCompress = false, } = opts;
|
|
2413
|
+
if (!url)
|
|
2414
|
+
return;
|
|
2415
|
+
const willDownloadedFileIsBr = url.includes(BR_SUFFIX);
|
|
2416
|
+
const wasmBrOutName = willDownloadedFileIsBr ? out + BR_SUFFIX : out;
|
|
2417
|
+
// 下载
|
|
2418
|
+
wsServer.sendUnitySplitStatus({ status: startDownloadStatus });
|
|
2419
|
+
console.log(`download url: ${url}`);
|
|
2420
|
+
const t0 = Date.now();
|
|
2421
|
+
await withRetry(() => download(url, wasmBrOutName), DOWNLOAD_RETRY);
|
|
2422
|
+
try {
|
|
2423
|
+
const st = await promises.stat(wasmBrOutName);
|
|
2424
|
+
if (!st.size)
|
|
2425
|
+
throw new Error(`Empty download: ${wasmBrOutName}`);
|
|
2426
|
+
wsServer.sendUnitySplitStatus({ status: downloadDoneStatus, url });
|
|
2427
|
+
console.log(`download done: ${path.basename(wasmBrOutName)} size=${st.size}B time=${Date.now() - t0}ms`);
|
|
2428
|
+
}
|
|
2429
|
+
catch (e) {
|
|
2430
|
+
await promises.rm(wasmBrOutName);
|
|
2431
|
+
throw e;
|
|
2432
|
+
}
|
|
2433
|
+
if (enableCompress) {
|
|
2434
|
+
console.log(`compress start: ${path.basename(out)}${BR_SUFFIX}`);
|
|
2435
|
+
// 压缩
|
|
2436
|
+
wsServer.sendUnitySplitStatus({ status: startCompressStatus });
|
|
2437
|
+
const t1 = Date.now();
|
|
2438
|
+
await compressWasmFile(out, wasmBrOutName);
|
|
2439
|
+
wsServer.sendUnitySplitStatus({ status: compressDoneStatus });
|
|
2440
|
+
console.log(`compress done: ${path.basename(wasmBrOutName)} time=${Date.now() - t1}ms`);
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* 在当前文件所在目录下写入一个空的 game.js
|
|
2444
|
+
*/
|
|
2445
|
+
fs__namespace.writeFileSync(path.join(path.dirname(out), 'game.js'), '', {
|
|
2446
|
+
encoding: 'utf-8',
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
async function downloadSplited(context) {
|
|
2451
|
+
const cwd = process.cwd();
|
|
2452
|
+
const splitTempDir = path.join(cwd, WASM_SPLIT_CACHE_DIR, DIR_SPLIT);
|
|
2453
|
+
ensureDirSync(splitTempDir);
|
|
2454
|
+
const mainAndroidDir = path.join(splitTempDir, WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain.root);
|
|
2455
|
+
const subAndroidDir = path.join(splitTempDir, WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub.root);
|
|
2456
|
+
const mainIosDir = path.join(splitTempDir, WASM_SPLIT_SUBPACKAGE_CONFIG.iosMain.root);
|
|
2457
|
+
const subIosDir = path.join(splitTempDir, WASM_SPLIT_SUBPACKAGE_CONFIG.iosSub.root);
|
|
2458
|
+
[mainAndroidDir, subAndroidDir, mainIosDir, subIosDir].forEach(ensureDirSync);
|
|
2459
|
+
const mainAndroidWasmCodeTempPath = path.join(mainAndroidDir, `${context.main_wasm_md5}${WASM_FILENAME_SUFFIX}`);
|
|
2460
|
+
const subAndroidWasmCodeTempPath = path.join(subAndroidDir, `${context.sub_wasm_md5}${WASM_FILENAME_SUFFIX}`);
|
|
2461
|
+
const mainIosWasmCodeTempPath = path.join(mainIosDir, `${context.main_wasm_h5_md5}${WASM_FILENAME_SUFFIX}`);
|
|
2462
|
+
const limit = pLimit(CONCURRENCY_LIMIT);
|
|
2463
|
+
try {
|
|
2464
|
+
console.log('downloadWasmSplit', context);
|
|
2465
|
+
// 原有状态文案,按你之前的写法
|
|
2466
|
+
wsServer.sendUnitySplitStatus({
|
|
2467
|
+
status: 'start_download_android_main_wasm',
|
|
1499
2468
|
});
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
store.setState({
|
|
1503
|
-
nodeWsPort: store.getState().nodeWsPort + 1,
|
|
1504
|
-
});
|
|
1505
|
-
this.startWsServer(store.getState().nodeWsPort);
|
|
1506
|
-
}
|
|
1507
|
-
else {
|
|
1508
|
-
console.log(chalk.red.bold(err.message));
|
|
1509
|
-
process.exit(1);
|
|
1510
|
-
}
|
|
2469
|
+
wsServer.sendUnitySplitStatus({
|
|
2470
|
+
status: 'start_download_android_sub_wasm_code',
|
|
1511
2471
|
});
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
2472
|
+
wsServer.sendUnitySplitStatus({ status: 'start_download_ios_main_wasm' });
|
|
2473
|
+
/**
|
|
2474
|
+
* 需要做个保护,只有 有 URL 时才下载
|
|
2475
|
+
*/
|
|
2476
|
+
// 并发下载 + 压缩(带重试)
|
|
2477
|
+
await Promise.all([
|
|
2478
|
+
limit(() => downloadAndCompress({
|
|
2479
|
+
startDownloadStatus: 'start_download_android_main_wasm',
|
|
2480
|
+
downloadDoneStatus: 'download_android_main_wasm_done',
|
|
2481
|
+
startCompressStatus: 'start_compress_android_main_wasm',
|
|
2482
|
+
compressDoneStatus: 'compress_android_main_wasm_done',
|
|
2483
|
+
url: context.main_wasm_download_url,
|
|
2484
|
+
out: mainAndroidWasmCodeTempPath,
|
|
2485
|
+
})),
|
|
2486
|
+
limit(() => downloadAndCompress({
|
|
2487
|
+
startDownloadStatus: 'start_download_android_sub_wasm_code',
|
|
2488
|
+
downloadDoneStatus: 'download_android_sub_wasm_code_done',
|
|
2489
|
+
startCompressStatus: 'start_compress_android_sub_wasm_code',
|
|
2490
|
+
compressDoneStatus: 'compress_android_sub_wasm_code_done',
|
|
2491
|
+
url: context.sub_wasm_download_url,
|
|
2492
|
+
out: subAndroidWasmCodeTempPath,
|
|
2493
|
+
})),
|
|
2494
|
+
limit(() => downloadAndCompress({
|
|
2495
|
+
startDownloadStatus: 'start_download_ios_main_wasm',
|
|
2496
|
+
downloadDoneStatus: 'download_ios_main_wasm_done',
|
|
2497
|
+
startCompressStatus: 'start_compress_ios_main_wasm',
|
|
2498
|
+
compressDoneStatus: 'compress_ios_main_wasm_done',
|
|
2499
|
+
url: context.main_wasm_h5_download_url,
|
|
2500
|
+
out: mainIosWasmCodeTempPath,
|
|
2501
|
+
})),
|
|
2502
|
+
// 下载 ios sub js range json
|
|
2503
|
+
limit(() => downloadAndCompress({
|
|
2504
|
+
startDownloadStatus: 'start_download_ios_range_json',
|
|
2505
|
+
downloadDoneStatus: 'download_ios_range_json_done',
|
|
2506
|
+
url: context.sub_js_range_download_url,
|
|
2507
|
+
out: path.join(subIosDir, 'func_bytes_range.json'),
|
|
2508
|
+
})),
|
|
2509
|
+
// 下载 ios sub js data br
|
|
2510
|
+
limit(() => downloadAndCompress({
|
|
2511
|
+
startDownloadStatus: 'start_download_ios_js_data_br',
|
|
2512
|
+
downloadDoneStatus: 'download_ios_js_data_br_done',
|
|
2513
|
+
url: context.sub_js_data_download_url,
|
|
2514
|
+
out: path.join(subIosDir, 'subjs.data'),
|
|
2515
|
+
})),
|
|
2516
|
+
]);
|
|
2517
|
+
// 复制 split/* 到项目根目录(递归、覆盖)——避免 EISDIR
|
|
2518
|
+
console.log('copy splitTempDir to root start');
|
|
2519
|
+
wsServer.sendUnitySplitStatus({ status: 'start_write_splited_wasm_br' });
|
|
2520
|
+
for (const file of fs.readdirSync(splitTempDir)) {
|
|
2521
|
+
const srcPath = path.join(splitTempDir, file);
|
|
2522
|
+
const destPath = path.join(cwd, file);
|
|
2523
|
+
// 如果目标路径有文件或目录,先删除
|
|
2524
|
+
if (fs.existsSync(destPath)) {
|
|
2525
|
+
await promises.rm(destPath, { recursive: true, force: true });
|
|
1521
2526
|
}
|
|
2527
|
+
await promises.cp(srcPath, destPath, { recursive: true, force: true });
|
|
2528
|
+
}
|
|
2529
|
+
wsServer.sendUnitySplitStatus({ status: 'write_splited_wasm_done' });
|
|
2530
|
+
console.log('copy splitTempDir to root end');
|
|
2531
|
+
// 更新分包配置(幂等)
|
|
2532
|
+
console.log('updateSubpackageConfigSync start');
|
|
2533
|
+
updateSubpackageConfigSync();
|
|
2534
|
+
console.log('updateSubpackageConfigSync end');
|
|
2535
|
+
// 更新 wasm split 配置(保持原始状态文案)
|
|
2536
|
+
console.log('updateWasmSplitConfig start');
|
|
2537
|
+
wsServer.sendUnitySplitStatus({ status: 'start_update_wasm_split_config' });
|
|
2538
|
+
updateWasmSplitConfig({
|
|
2539
|
+
ENABLEWASMCOLLECT: true,
|
|
2540
|
+
ORIGINALWASMMD5: `${context.original_wasm_md5}`,
|
|
2541
|
+
WASMTABLESIZE: context.table_size,
|
|
2542
|
+
GLOBALVARLIST: JSON.stringify(context.global_var_list ?? []),
|
|
2543
|
+
SUBJSURL: `${context.sub_js_download_url}`,
|
|
2544
|
+
IOS_CODE_FILE_MD5: `${context.main_wasm_h5_md5}`,
|
|
2545
|
+
ANDROID_CODE_FILE_MD5: `${context.main_wasm_md5}`,
|
|
2546
|
+
ANDROID_SUB_CODE_FILE_MD5: `${context.sub_wasm_md5}`,
|
|
2547
|
+
WASMSPLITVERSION: `${context.version}`,
|
|
2548
|
+
USINGWASMH5: Boolean(context.main_wasm_h5_md5),
|
|
2549
|
+
ENABLEWASMSPLIT: true,
|
|
2550
|
+
// IOS_SUB_JS_FILE_CONFIG: JSON.stringify(context.merged_js ?? {}),
|
|
1522
2551
|
});
|
|
2552
|
+
wsServer.sendUnitySplitStatus({ status: 'update_wasm_split_config_done' });
|
|
2553
|
+
console.log('updateWasmSplitConfig end');
|
|
2554
|
+
return {
|
|
2555
|
+
data: {
|
|
2556
|
+
isSuccess: true,
|
|
2557
|
+
},
|
|
2558
|
+
ctx: context,
|
|
2559
|
+
};
|
|
1523
2560
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
2561
|
+
catch (err) {
|
|
2562
|
+
wsServer.sendUnitySplitStatus({
|
|
2563
|
+
status: 'wasm_split_failed',
|
|
2564
|
+
errorMsg: err instanceof Error ? err.message : String(err),
|
|
1527
2565
|
});
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
...payload,
|
|
2566
|
+
return {
|
|
2567
|
+
data: {
|
|
2568
|
+
isSuccess: false,
|
|
2569
|
+
},
|
|
2570
|
+
error: {
|
|
2571
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1535
2572
|
},
|
|
2573
|
+
ctx: context,
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
finally {
|
|
2577
|
+
// 清理临时目录与旧 wasmcode 目录
|
|
2578
|
+
console.log('delete splitTempDir start');
|
|
2579
|
+
await promises.rm(splitTempDir, { recursive: true, force: true });
|
|
2580
|
+
console.log('delete splitTempDir end');
|
|
2581
|
+
console.log('delete wasmcode start');
|
|
2582
|
+
await promises.rm(path.join(cwd, WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root), {
|
|
2583
|
+
recursive: true,
|
|
2584
|
+
force: true,
|
|
1536
2585
|
});
|
|
2586
|
+
console.log('delete wasmcode end');
|
|
1537
2587
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
async function getSplitResult({ client_key, wasm_md5, wasm_path, }) {
|
|
2591
|
+
return request({
|
|
2592
|
+
url: `${BASE_URL}/api/stark_wasm/v4/post/download`,
|
|
2593
|
+
method: 'POST',
|
|
2594
|
+
headers: { ...DEV_HEADERS },
|
|
2595
|
+
data: { client_key, wasm_md5, wasm_path },
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
function cancelSplit(params) {
|
|
2600
|
+
/**
|
|
2601
|
+
* 把— __TTMG_TEMP__/wasmcode/ 目录下的所有文件恢复到原本的位置,进行重置
|
|
2602
|
+
*/
|
|
2603
|
+
const { wasmCodePath } = params;
|
|
2604
|
+
const cacheDir = path__namespace.join(process.cwd(), WASM_SPLIT_CACHE_DIR);
|
|
2605
|
+
/**
|
|
2606
|
+
* 恢复 br 文件
|
|
2607
|
+
*/
|
|
2608
|
+
if (fs__namespace.existsSync(cacheDir)) {
|
|
2609
|
+
/**
|
|
2610
|
+
* 判断是否有缓存的 br 文件
|
|
2611
|
+
*/
|
|
2612
|
+
const targetWasmBrPath = path__namespace.join(cacheDir, path__namespace.basename(wasmCodePath));
|
|
2613
|
+
if (fs__namespace.existsSync(targetWasmBrPath)) {
|
|
2614
|
+
const destWasmBrPath = path__namespace.join(process.cwd(), wasmCodePath);
|
|
2615
|
+
// 规避没有文件夹的情况
|
|
2616
|
+
ensureDirSync(path__namespace.dirname(destWasmBrPath));
|
|
2617
|
+
fs__namespace.copyFileSync(targetWasmBrPath, destWasmBrPath);
|
|
2618
|
+
}
|
|
1540
2619
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
2620
|
+
/**
|
|
2621
|
+
* 恢复 webgl-wasm-split.js 文件
|
|
2622
|
+
*/
|
|
2623
|
+
const splitConfigCachePath = path__namespace.join(cacheDir, WASM_SPLIT_CONFIG_FILE_NAME);
|
|
2624
|
+
if (fs__namespace.existsSync(splitConfigCachePath)) {
|
|
2625
|
+
fs__namespace.copyFileSync(splitConfigCachePath, path__namespace.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME));
|
|
2626
|
+
}
|
|
2627
|
+
/**
|
|
2628
|
+
* 恢复 game.json 文件
|
|
2629
|
+
*/
|
|
2630
|
+
const gameJsonCachePath = path__namespace.join(cacheDir, 'game.json');
|
|
2631
|
+
if (fs__namespace.existsSync(gameJsonCachePath)) {
|
|
2632
|
+
fs__namespace.copyFileSync(gameJsonCachePath, path__namespace.join(process.cwd(), 'game.json'));
|
|
1547
2633
|
}
|
|
1548
2634
|
}
|
|
1549
|
-
const wsServer = new WsServer();
|
|
1550
2635
|
|
|
1551
|
-
async function
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
2636
|
+
async function resetWasmSplit(data) {
|
|
2637
|
+
const res = await request({
|
|
2638
|
+
url: `${BASE_URL}/api/stark_wasm/v4/post/reset`,
|
|
2639
|
+
method: 'POST',
|
|
2640
|
+
headers: {
|
|
2641
|
+
...DEV_HEADERS,
|
|
2642
|
+
},
|
|
2643
|
+
data: {
|
|
2644
|
+
client_key: data.clientkey,
|
|
2645
|
+
wasm_md5: data.wasmMd5,
|
|
2646
|
+
},
|
|
2647
|
+
});
|
|
2648
|
+
/**
|
|
2649
|
+
* 把— __TTMG_TEMP__/wasmcode/ 目录下的所有文件恢复到原本的位置,进行重置
|
|
2650
|
+
*/
|
|
2651
|
+
const cacheDir = path.join(process.cwd(), WASM_SPLIT_CACHE_DIR);
|
|
2652
|
+
/**
|
|
2653
|
+
* 恢复 br 文件
|
|
2654
|
+
*/
|
|
2655
|
+
if (fs.existsSync(cacheDir)) {
|
|
2656
|
+
/**
|
|
2657
|
+
* 判断是否有缓存的 br 文件
|
|
2658
|
+
*/
|
|
2659
|
+
/**
|
|
2660
|
+
* 判断 cache 文件夹下有没有 .br 文件
|
|
2661
|
+
*
|
|
2662
|
+
*/
|
|
2663
|
+
const targetWasmBrPath = fs
|
|
2664
|
+
.readdirSync(cacheDir)
|
|
2665
|
+
.find(item => item.endsWith('.br'));
|
|
2666
|
+
if (targetWasmBrPath) {
|
|
2667
|
+
const destWasmBrPath = path.join(process.cwd(), WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root, path.basename(targetWasmBrPath));
|
|
2668
|
+
// 规避没有文件夹的情况
|
|
2669
|
+
ensureDirSync(path.dirname(destWasmBrPath));
|
|
2670
|
+
fs.copyFileSync(path.join(cacheDir, targetWasmBrPath), destWasmBrPath);
|
|
1563
2671
|
}
|
|
1564
|
-
console.log(chalk.red.bold(msg));
|
|
1565
2672
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
2673
|
+
/**
|
|
2674
|
+
* 恢复 webgl-wasm-split.js 文件
|
|
2675
|
+
*/
|
|
2676
|
+
const splitConfigCachePath = path.join(cacheDir, WASM_SPLIT_CONFIG_FILE_NAME);
|
|
2677
|
+
if (fs.existsSync(splitConfigCachePath)) {
|
|
2678
|
+
fs.copyFileSync(splitConfigCachePath, path.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME));
|
|
2679
|
+
}
|
|
2680
|
+
/**
|
|
2681
|
+
* 恢复 game.json 文件
|
|
2682
|
+
*/
|
|
2683
|
+
const gameJsonCachePath = path.join(cacheDir, 'game.json');
|
|
2684
|
+
if (fs.existsSync(gameJsonCachePath)) {
|
|
2685
|
+
fs.copyFileSync(gameJsonCachePath, path.join(process.cwd(), 'game.json'));
|
|
2686
|
+
}
|
|
2687
|
+
/**
|
|
2688
|
+
* 删除历史分包产物
|
|
2689
|
+
*/
|
|
2690
|
+
const androidSubpackageDir = path.join(process.cwd(), WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain.root);
|
|
2691
|
+
if (fs.existsSync(androidSubpackageDir)) {
|
|
2692
|
+
fs.rmSync(androidSubpackageDir, { recursive: true });
|
|
2693
|
+
}
|
|
2694
|
+
const androidSubpackageSubDir = path.join(process.cwd(), WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub.root);
|
|
2695
|
+
if (fs.existsSync(androidSubpackageSubDir)) {
|
|
2696
|
+
fs.rmSync(androidSubpackageSubDir, { recursive: true });
|
|
2697
|
+
}
|
|
2698
|
+
const iosSubpackageDir = path.join(process.cwd(), WASM_SPLIT_SUBPACKAGE_CONFIG.ios.root);
|
|
2699
|
+
if (fs.existsSync(iosSubpackageDir)) {
|
|
2700
|
+
fs.rmSync(iosSubpackageDir, { recursive: true });
|
|
2701
|
+
}
|
|
2702
|
+
return res;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
function getSplitConfig() {
|
|
2706
|
+
const configFilePath = path__namespace.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME);
|
|
2707
|
+
try {
|
|
2708
|
+
// 1. 检查文件是否存在
|
|
2709
|
+
if (!fs__namespace.existsSync(configFilePath)) {
|
|
2710
|
+
console.error(`Config file not found at: ${configFilePath}`);
|
|
2711
|
+
return null;
|
|
2712
|
+
}
|
|
2713
|
+
// 2. 同步读取文件内容为字符串
|
|
2714
|
+
const fileContent = fs__namespace.readFileSync(configFilePath, 'utf-8');
|
|
2715
|
+
// 3. 构造一个函数来执行并返回 module.exports 的内容
|
|
2716
|
+
// 这是一种比直接用 eval() 更安全的方式,因为它限制了代码的执行作用域
|
|
2717
|
+
const evaluateModule = new Function('module', `${fileContent}; return module.exports;`);
|
|
2718
|
+
// 准备一个临时的 module 对象
|
|
2719
|
+
const tempModule = { exports: {} };
|
|
2720
|
+
// 4. 执行函数,并将配置赋值给 config 变量
|
|
2721
|
+
const config = evaluateModule(tempModule);
|
|
2722
|
+
// 5. 检查是否成功获取了配置
|
|
2723
|
+
if (typeof config === 'object' && config !== null) {
|
|
2724
|
+
return config;
|
|
2725
|
+
}
|
|
2726
|
+
else {
|
|
2727
|
+
console.error('Failed to extract a valid config object from the file.');
|
|
2728
|
+
return null;
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
catch (error) {
|
|
2732
|
+
console.error('Error reading or evaluating split config file:', error);
|
|
2733
|
+
return null;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
const getTaskStatus = (params) => {
|
|
2738
|
+
return request({
|
|
2739
|
+
url: `${BASE_URL}/api/stark_wasm/v4/get/status`,
|
|
2740
|
+
method: 'GET',
|
|
2741
|
+
headers: DEV_HEADERS,
|
|
2742
|
+
params,
|
|
1573
2743
|
});
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
2744
|
+
};
|
|
2745
|
+
|
|
2746
|
+
const getTaskInfo = async (params) => {
|
|
2747
|
+
return request({
|
|
2748
|
+
url: `${BASE_URL}/api/stark_wasm/v4/get/taskinfo`,
|
|
2749
|
+
method: 'GET',
|
|
2750
|
+
headers: DEV_HEADERS,
|
|
2751
|
+
params,
|
|
2752
|
+
});
|
|
2753
|
+
};
|
|
2754
|
+
|
|
2755
|
+
const successCode = 0;
|
|
2756
|
+
const errorCode = -1;
|
|
2757
|
+
const outputDir = getOutputDir();
|
|
2758
|
+
const publicPath = path.join(__dirname, 'public');
|
|
2759
|
+
const devToolVersion = getDevToolVersion();
|
|
2760
|
+
async function start() {
|
|
2761
|
+
const startTime = Date.now();
|
|
2762
|
+
const app = express();
|
|
2763
|
+
app.use(fileUpload()); // 启用 express-fileupload 中间件
|
|
2764
|
+
// --- 中间件和路由设置 ---
|
|
2765
|
+
app.use(expressStaticGzip(publicPath, {
|
|
2766
|
+
enableBrotli: true,
|
|
2767
|
+
orderPreference: ['br'],
|
|
2768
|
+
}));
|
|
2769
|
+
app.use((req, res, next) => {
|
|
2770
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
2771
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
2772
|
+
res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
2773
|
+
res.header('Pragma', 'no-cache');
|
|
2774
|
+
res.header('Expires', '0');
|
|
2775
|
+
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
2776
|
+
next();
|
|
2777
|
+
});
|
|
2778
|
+
app.use(express.json());
|
|
2779
|
+
app.use(express.urlencoded({ extended: true }));
|
|
2780
|
+
app.use('/game/files', express.static(outputDir));
|
|
2781
|
+
app.get('/game/config', async (req, res) => {
|
|
2782
|
+
const basic = await ttmgPack.getPkgs({ entryDir: process.cwd() });
|
|
2783
|
+
const user = getCurrentUser();
|
|
2784
|
+
const { clientKey } = getClientKey();
|
|
2785
|
+
res.send({
|
|
2786
|
+
error: null,
|
|
2787
|
+
data: {
|
|
2788
|
+
user,
|
|
2789
|
+
code: successCode,
|
|
2790
|
+
nodeWsPort: store.getState().nodeWsPort,
|
|
2791
|
+
clientKey: clientKey,
|
|
2792
|
+
schema: `https://www.tiktok.com/ttmg/dev/${clientKey}?host=${getLocalIP()}&port=${store.getState().nodeWsPort}&host_list=${encodeURIComponent(JSON.stringify(getLocalIPs()))}`,
|
|
2793
|
+
...basic,
|
|
2794
|
+
devToolVersion,
|
|
2795
|
+
},
|
|
1610
2796
|
});
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
2797
|
+
});
|
|
2798
|
+
app.get('/game/detail', async (req, res) => {
|
|
2799
|
+
const basic = await ttmgPack.getPkgs({ entryDir: process.cwd() });
|
|
2800
|
+
const { clientKey } = getClientKey();
|
|
2801
|
+
const { error, data: gameInfo } = await fetchGameInfo(clientKey);
|
|
2802
|
+
store.setState({
|
|
2803
|
+
appId: gameInfo?.app_id,
|
|
2804
|
+
sandboxId: gameInfo?.sandbox_info?.sandbox_id,
|
|
1615
2805
|
});
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
2806
|
+
if (error) {
|
|
2807
|
+
res.send({ error, data: null });
|
|
2808
|
+
return;
|
|
2809
|
+
}
|
|
2810
|
+
else {
|
|
2811
|
+
res.send({ error: null, data: { ...basic, ...gameInfo } });
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
2814
|
+
app.get('/game/check', async (req, res) => {
|
|
2815
|
+
const checkResult = await ttmgPack.checkPkgs({
|
|
2816
|
+
entryDir: process.cwd(),
|
|
2817
|
+
config: {
|
|
2818
|
+
entry: process.cwd(),
|
|
2819
|
+
output: outputDir,
|
|
2820
|
+
dev: {
|
|
2821
|
+
enable: true,
|
|
2822
|
+
port: store.getState().nodeServerPort,
|
|
2823
|
+
host: 'localhost',
|
|
2824
|
+
enableSourcemap: false,
|
|
2825
|
+
enableLog: false,
|
|
2826
|
+
},
|
|
2827
|
+
build: {
|
|
2828
|
+
enableOdr: false,
|
|
2829
|
+
enableAPICheck: true,
|
|
2830
|
+
...PKG_SIZE_LIMIT,
|
|
2831
|
+
},
|
|
1623
2832
|
},
|
|
1624
2833
|
});
|
|
2834
|
+
res.send({ code: successCode, data: checkResult });
|
|
1625
2835
|
});
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
async
|
|
1630
|
-
|
|
1631
|
-
//
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
//
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
2836
|
+
/**
|
|
2837
|
+
* @description 上传游戏代码到服务器
|
|
2838
|
+
*/
|
|
2839
|
+
// app.post('/game/upload', async (req, res) => {
|
|
2840
|
+
// /**
|
|
2841
|
+
// * 我要新版本不做开发者选择,直接当前 cwd 作为 entryDir,压缩成 game.zip 进行上传
|
|
2842
|
+
// */
|
|
2843
|
+
// const fileKeys = Object.keys(req.files);
|
|
2844
|
+
// const uploadedFile = req.files[fileKeys[0]];
|
|
2845
|
+
// if (!uploadedFile) {
|
|
2846
|
+
// res.status(400).send({ code: errorCode, data: 'No file uploaded' }); // 使用正确的 HTTP 状态码
|
|
2847
|
+
// return;
|
|
2848
|
+
// }
|
|
2849
|
+
// try {
|
|
2850
|
+
// // 通过 header 获取 desc
|
|
2851
|
+
// const desc = req.headers['ttmg-game-desc'];
|
|
2852
|
+
// // 需要做 decodeURIComponent 处理
|
|
2853
|
+
// const decodedDesc = decodeURIComponent(desc || '--');
|
|
2854
|
+
// // 直接传递需要的信息
|
|
2855
|
+
// const { data, error } = await uploadGameToPlatform({
|
|
2856
|
+
// data: uploadedFile.data, // 这是 Buffer
|
|
2857
|
+
// name: uploadedFile.name, // 这是文件名
|
|
2858
|
+
// clientKey: getClientKey().clientKey,
|
|
2859
|
+
// note: decodedDesc,
|
|
2860
|
+
// appId: store.getState().appId,
|
|
2861
|
+
// sandboxId: store.getState().sandboxId,
|
|
2862
|
+
// });
|
|
2863
|
+
// if (error) {
|
|
2864
|
+
// res.send({ code: errorCode, error });
|
|
2865
|
+
// } else {
|
|
2866
|
+
// res.send({ code: successCode, data });
|
|
2867
|
+
// }
|
|
2868
|
+
// } catch (error) {
|
|
2869
|
+
// // 错误处理可以更具体
|
|
2870
|
+
// let errorMessage = 'An unknown error occurred.';
|
|
2871
|
+
// if (error instanceof Error) {
|
|
2872
|
+
// errorMessage = error.message;
|
|
2873
|
+
// }
|
|
2874
|
+
// // 打印详细错误到服务器日志,方便排查
|
|
2875
|
+
// res.status(500).send({ code: errorCode, data: errorMessage }); // 使用正确的 HTTP 状态码
|
|
2876
|
+
// }
|
|
2877
|
+
// });
|
|
2878
|
+
app.post('/game/upload', async (req, res) => {
|
|
2879
|
+
try {
|
|
2880
|
+
console.log(`正在打包当前目录: ${process.cwd()} ...`);
|
|
2881
|
+
const gameZipBuffer = await zipCwdToBuffer();
|
|
2882
|
+
// 变成 MB
|
|
2883
|
+
console.log(`打包完成,大小: ${(gameZipBuffer.length / 1024 / 1024).toFixed(2)} MB`);
|
|
2884
|
+
const desc = req.headers['ttmg-game-desc'];
|
|
2885
|
+
const decodedDesc = decodeURIComponent(desc || '--');
|
|
2886
|
+
const { data, error } = await uploadGameToPlatform({
|
|
2887
|
+
// @ts-ignore
|
|
2888
|
+
data: gameZipBuffer,
|
|
2889
|
+
name: 'game.zip',
|
|
2890
|
+
clientKey: getClientKey().clientKey,
|
|
2891
|
+
note: decodedDesc,
|
|
2892
|
+
appId: store.getState().appId,
|
|
2893
|
+
sandboxId: store.getState().sandboxId,
|
|
2894
|
+
});
|
|
2895
|
+
if (error) {
|
|
2896
|
+
res.send({ code: errorCode, error });
|
|
2897
|
+
}
|
|
2898
|
+
else {
|
|
2899
|
+
res.send({ code: successCode, data });
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
catch (error) {
|
|
2903
|
+
let errorMessage = 'An unknown error occurred.';
|
|
2904
|
+
if (error instanceof Error) {
|
|
2905
|
+
errorMessage = error.message;
|
|
2906
|
+
}
|
|
2907
|
+
console.error('Upload failed:', errorMessage);
|
|
2908
|
+
res.status(500).send({ code: errorCode, data: errorMessage });
|
|
2909
|
+
}
|
|
1653
2910
|
});
|
|
1654
|
-
|
|
1655
|
-
|
|
2911
|
+
app.get('/game/wasm-split-config', (req, res) => {
|
|
2912
|
+
const config = getSplitConfig();
|
|
2913
|
+
if (!config) {
|
|
2914
|
+
res.send({ code: errorCode, data: 'Failed to parse split config' });
|
|
2915
|
+
}
|
|
2916
|
+
else {
|
|
2917
|
+
res.send({ code: successCode, data: config });
|
|
2918
|
+
}
|
|
1656
2919
|
});
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
2920
|
+
app.get('/game/wasm-split-options', (req, res) => {
|
|
2921
|
+
res.send({
|
|
2922
|
+
code: successCode,
|
|
2923
|
+
data: {
|
|
2924
|
+
options: [
|
|
2925
|
+
{
|
|
2926
|
+
md5: '123',
|
|
2927
|
+
desc: 'test',
|
|
2928
|
+
version: '1.0.0',
|
|
2929
|
+
time: '2023-01-01',
|
|
2930
|
+
funcCounts: 100,
|
|
2931
|
+
},
|
|
2932
|
+
{
|
|
2933
|
+
md5: '456',
|
|
2934
|
+
desc: 'test2',
|
|
2935
|
+
version: '1.0.1',
|
|
2936
|
+
time: '2023-01-02',
|
|
2937
|
+
funcCounts: 200,
|
|
2938
|
+
},
|
|
2939
|
+
],
|
|
2940
|
+
},
|
|
2941
|
+
});
|
|
1665
2942
|
});
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
2943
|
+
/**
|
|
2944
|
+
* 分包预处理
|
|
2945
|
+
*/
|
|
2946
|
+
app.post('/game/wasm-prepare', async (req, res) => {
|
|
2947
|
+
const { codePath, desc, codeMd5, clientKey } = req.body;
|
|
2948
|
+
console.log('wasm-prepare-start', req.body);
|
|
2949
|
+
const result = await startPrepare({
|
|
2950
|
+
client_key: clientKey,
|
|
2951
|
+
desc,
|
|
2952
|
+
wasm_md5: codeMd5,
|
|
2953
|
+
wasm_file_path: codePath,
|
|
2954
|
+
});
|
|
2955
|
+
console.log('wasm-prepare-end', result);
|
|
2956
|
+
if (result.error) {
|
|
2957
|
+
res.send({
|
|
2958
|
+
code: errorCode,
|
|
2959
|
+
error: result.error,
|
|
2960
|
+
ctx: result.ctx,
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
else {
|
|
2964
|
+
const { md5 } = result.data?.result || {};
|
|
2965
|
+
if (!md5) {
|
|
2966
|
+
res.send({
|
|
2967
|
+
code: errorCode,
|
|
2968
|
+
error: result.data,
|
|
2969
|
+
ctx: result.ctx,
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
else {
|
|
2973
|
+
res.send({
|
|
2974
|
+
code: successCode,
|
|
2975
|
+
data: result.data?.result || {},
|
|
2976
|
+
ctx: result.ctx,
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
1671
2980
|
});
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
// 3. 对临时目录进行压缩
|
|
1689
|
-
const output = fs.createWriteStream(outPath);
|
|
1690
|
-
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
1691
|
-
// 使用 Promise 包装流操作
|
|
1692
|
-
const archivePromise = new Promise((resolve, reject) => {
|
|
1693
|
-
output.on('close', () => {
|
|
1694
|
-
resolve();
|
|
2981
|
+
/**
|
|
2982
|
+
* @description 分包预处理查询,根据 taskId 查询预处理状态
|
|
2983
|
+
* 前十次返回 process,第11次返回 success
|
|
2984
|
+
*/
|
|
2985
|
+
app.post('/game/wasm-prepare-result', async (req, res) => {
|
|
2986
|
+
console.log('wasm-prepare-result-request', req.body);
|
|
2987
|
+
const { clientKey, codeMd5 } = req.body;
|
|
2988
|
+
const response = await getTaskStatus({
|
|
2989
|
+
client_key: clientKey,
|
|
2990
|
+
wasm_md5: codeMd5,
|
|
2991
|
+
});
|
|
2992
|
+
if (response.error) {
|
|
2993
|
+
res.send({
|
|
2994
|
+
code: errorCode,
|
|
2995
|
+
error: response.error,
|
|
2996
|
+
ctx: response.ctx,
|
|
1695
2997
|
});
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
2998
|
+
}
|
|
2999
|
+
else {
|
|
3000
|
+
res.send({
|
|
3001
|
+
code: successCode,
|
|
3002
|
+
data: response.data?.result || {},
|
|
3003
|
+
ctx: response.ctx,
|
|
3004
|
+
});
|
|
3005
|
+
}
|
|
3006
|
+
});
|
|
3007
|
+
/**
|
|
3008
|
+
* @description 下载预处理后的 wasm 包
|
|
3009
|
+
*/
|
|
3010
|
+
app.post('/game/wasm-prepare-download', async (req, res) => {
|
|
3011
|
+
/**
|
|
3012
|
+
* 下载完成后需要进行 br 并替换 codePath 对应的文件后再返回成功
|
|
3013
|
+
*/
|
|
3014
|
+
const { clientKey, codeMd5, codePath } = req.body;
|
|
3015
|
+
console.log('wasm-prepare-download-start', req.body);
|
|
3016
|
+
const response = await downloadPrepared({
|
|
3017
|
+
client_key: clientKey,
|
|
3018
|
+
wasm_md5: codeMd5,
|
|
3019
|
+
wasm_path: codePath,
|
|
1699
3020
|
});
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
3021
|
+
if (response.isSuccess) {
|
|
3022
|
+
res.send({
|
|
3023
|
+
code: successCode,
|
|
3024
|
+
msg: 'download success',
|
|
3025
|
+
ctx: response.ctx,
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
3028
|
+
else {
|
|
3029
|
+
res.send({
|
|
3030
|
+
code: errorCode,
|
|
3031
|
+
error: response.error,
|
|
3032
|
+
ctx: response.ctx,
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
});
|
|
3036
|
+
/**
|
|
3037
|
+
* 开始代码分包
|
|
3038
|
+
*/
|
|
3039
|
+
app.post('/game/wasm-split', async (req, res) => {
|
|
3040
|
+
const { clientKey, codeMd5 } = req.body;
|
|
3041
|
+
console.log('wasm-split-start', req.body);
|
|
3042
|
+
const response = await startSplit({
|
|
3043
|
+
client_key: clientKey,
|
|
3044
|
+
wasm_md5: codeMd5,
|
|
1706
3045
|
});
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
3046
|
+
if (response.error) {
|
|
3047
|
+
res.send({
|
|
3048
|
+
code: errorCode,
|
|
3049
|
+
error: response.error,
|
|
3050
|
+
ctx: response.ctx,
|
|
3051
|
+
});
|
|
3052
|
+
}
|
|
3053
|
+
else {
|
|
3054
|
+
res.send({
|
|
3055
|
+
code: successCode,
|
|
3056
|
+
data: response.data,
|
|
3057
|
+
ctx: response.ctx,
|
|
3058
|
+
});
|
|
1711
3059
|
}
|
|
1712
|
-
// 6. 完成压缩
|
|
1713
|
-
await archive.finalize();
|
|
1714
|
-
// 等待文件流关闭
|
|
1715
|
-
await archivePromise;
|
|
1716
|
-
}
|
|
1717
|
-
catch (err) {
|
|
1718
|
-
console.error('压缩过程中发生错误:', err);
|
|
1719
|
-
throw err;
|
|
1720
|
-
}
|
|
1721
|
-
finally {
|
|
1722
|
-
// 7. 无论成功还是失败,都清理临时目录
|
|
1723
|
-
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
async function uploadZip(zipPath, callback) {
|
|
1727
|
-
const startTime = Date.now();
|
|
1728
|
-
const form = new FormData$1();
|
|
1729
|
-
form.append('file', fs.createReadStream(zipPath), {
|
|
1730
|
-
filename: 'upload.zip',
|
|
1731
|
-
contentType: 'application/zip',
|
|
1732
3060
|
});
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
// 1. 创建请求流
|
|
1740
|
-
const stream = got.stream.post(url, {
|
|
1741
|
-
body: form,
|
|
3061
|
+
app.get('/game/wasm-taskinfo', async (req, res) => {
|
|
3062
|
+
const { clientKey, codeMd5 } = req.query;
|
|
3063
|
+
console.log('wasm-taskinfo', req.query);
|
|
3064
|
+
const response = await getTaskInfo({
|
|
3065
|
+
client_key: clientKey,
|
|
3066
|
+
wasm_md5: codeMd5,
|
|
1742
3067
|
});
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
callback({
|
|
1749
|
-
status: 'process',
|
|
1750
|
-
percent,
|
|
3068
|
+
if (response.error) {
|
|
3069
|
+
res.send({
|
|
3070
|
+
code: errorCode,
|
|
3071
|
+
error: response.error,
|
|
3072
|
+
ctx: response.ctx,
|
|
1751
3073
|
});
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
3074
|
+
}
|
|
3075
|
+
else {
|
|
3076
|
+
res.send({
|
|
3077
|
+
code: successCode,
|
|
3078
|
+
data: response.data?.result || {},
|
|
3079
|
+
ctx: response.ctx,
|
|
3080
|
+
});
|
|
3081
|
+
}
|
|
3082
|
+
});
|
|
3083
|
+
app.post('/game/wasm-set-collect', async (req, res) => {
|
|
3084
|
+
const { clientKey, codeMd5 } = req.body;
|
|
3085
|
+
console.log('wasm-set-collect', req.body);
|
|
3086
|
+
const response = await setCollect({
|
|
3087
|
+
client_key: clientKey,
|
|
3088
|
+
wasm_md5: codeMd5,
|
|
1758
3089
|
});
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
3090
|
+
if (response.error) {
|
|
3091
|
+
res.send({
|
|
3092
|
+
code: errorCode,
|
|
3093
|
+
error: response.error,
|
|
3094
|
+
ctx: response.ctx,
|
|
3095
|
+
});
|
|
3096
|
+
}
|
|
3097
|
+
else {
|
|
3098
|
+
res.send({
|
|
3099
|
+
code: successCode,
|
|
3100
|
+
data: response.data || {},
|
|
3101
|
+
ctx: response.ctx,
|
|
1765
3102
|
});
|
|
3103
|
+
}
|
|
3104
|
+
});
|
|
3105
|
+
app.get('/game/wasm-collect-funcids', async (req, res) => {
|
|
3106
|
+
const { clientKey, codeMd5 } = req.query;
|
|
3107
|
+
console.log('wasm-collect-funcids', req.query);
|
|
3108
|
+
const response = await getCollectedFuncIds({
|
|
3109
|
+
client_key: clientKey,
|
|
3110
|
+
wasm_md5: codeMd5,
|
|
1766
3111
|
});
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
3112
|
+
if (response.error) {
|
|
3113
|
+
res.send({
|
|
3114
|
+
code: errorCode,
|
|
3115
|
+
error: response.error,
|
|
3116
|
+
ctx: response.ctx,
|
|
3117
|
+
});
|
|
3118
|
+
}
|
|
3119
|
+
else {
|
|
3120
|
+
res.send({
|
|
3121
|
+
code: successCode,
|
|
3122
|
+
data: response.data?.result || {},
|
|
3123
|
+
ctx: response.ctx,
|
|
1775
3124
|
});
|
|
3125
|
+
}
|
|
3126
|
+
});
|
|
3127
|
+
app.get('/game/wasm-collect-info', async (req, res) => {
|
|
3128
|
+
const { clientKey, codeMd5 } = req.query;
|
|
3129
|
+
console.log('wasm-collect-info', req.query);
|
|
3130
|
+
const response = await getCollecttingInfo({
|
|
3131
|
+
client_key: clientKey,
|
|
3132
|
+
wasm_md5: codeMd5,
|
|
1776
3133
|
});
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
3134
|
+
if (response.error) {
|
|
3135
|
+
res.send({
|
|
3136
|
+
code: errorCode,
|
|
3137
|
+
error: response.error,
|
|
3138
|
+
ctx: response.ctx,
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
else {
|
|
3142
|
+
res.send({
|
|
3143
|
+
code: successCode,
|
|
3144
|
+
data: response.data?.result || {},
|
|
3145
|
+
ctx: response.ctx,
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
});
|
|
3149
|
+
/**
|
|
3150
|
+
* 获取代码分包结果
|
|
3151
|
+
*/
|
|
3152
|
+
app.post('/game/wasm-split-result', async (req, res) => {
|
|
3153
|
+
const { codeMd5, clientKey } = req.body;
|
|
3154
|
+
console.log('wasm-split-result', req.body);
|
|
3155
|
+
const response = await getTaskStatus({
|
|
3156
|
+
client_key: clientKey,
|
|
3157
|
+
wasm_md5: codeMd5,
|
|
1783
3158
|
});
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
function listen() {
|
|
1791
|
-
eventEmitter.on('startUpload', () => {
|
|
1792
|
-
/**
|
|
1793
|
-
* 如果还在编译中,需要等到编译结束再上传
|
|
1794
|
-
*/
|
|
1795
|
-
if (store.getState().isUnderCompiling) {
|
|
1796
|
-
store.setState({
|
|
1797
|
-
isWaitingForUpload: true,
|
|
3159
|
+
if (response.error) {
|
|
3160
|
+
res.send({
|
|
3161
|
+
code: errorCode,
|
|
3162
|
+
error: response.error,
|
|
3163
|
+
ctx: response.ctx,
|
|
1798
3164
|
});
|
|
1799
|
-
return;
|
|
1800
3165
|
}
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
3166
|
+
else {
|
|
3167
|
+
res.send({
|
|
3168
|
+
code: successCode,
|
|
3169
|
+
data: response.data?.result || {},
|
|
3170
|
+
ctx: response.ctx,
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
});
|
|
3174
|
+
/**
|
|
3175
|
+
*
|
|
3176
|
+
高鹏
|
|
3177
|
+
抄一下码下面的话
|
|
3178
|
+
我试试这个咋样看[看]闵行的是好吃的
|
|
3179
|
+
可以
|
|
3180
|
+
下周大哥来 让他别整一群的 就咱们几个吃个串挺好
|
|
3181
|
+
哪个unity筹备群拉你了哈
|
|
3182
|
+
好
|
|
3183
|
+
新知识 30% 等于部分用户
|
|
3184
|
+
0.3.1-unity.6
|
|
3185
|
+
{
|
|
3186
|
+
"code": 0,
|
|
3187
|
+
"data": {
|
|
3188
|
+
"isSuccess": true
|
|
3189
|
+
},
|
|
3190
|
+
"msg": "download success",
|
|
3191
|
+
"ctx":
|
|
3192
|
+
}
|
|
3193
|
+
发现个小问题
|
|
3194
|
+
wasmcode1-ios 下面下载缺了一个func_bytes_range.json 以及subjs.data.br 多了一个 br 后缀
|
|
3195
|
+
|
|
3196
|
+
Shift + Enter 换行
|
|
3197
|
+
|
|
3198
|
+
*/
|
|
3199
|
+
app.post('/game/wasm-split-download-result', async (req, res) => {
|
|
3200
|
+
const { clientKey, codeMd5, codePath } = req.body;
|
|
3201
|
+
console.log('game/wasm-split-download-result-start', req.body);
|
|
3202
|
+
const response = await getSplitResult({
|
|
3203
|
+
client_key: clientKey,
|
|
3204
|
+
wasm_md5: codeMd5,
|
|
3205
|
+
wasm_path: codePath,
|
|
1824
3206
|
});
|
|
3207
|
+
if (response.error) {
|
|
3208
|
+
res.send({
|
|
3209
|
+
code: errorCode,
|
|
3210
|
+
error: response.error,
|
|
3211
|
+
ctx: response.ctx,
|
|
3212
|
+
});
|
|
3213
|
+
}
|
|
3214
|
+
else {
|
|
3215
|
+
res.send({
|
|
3216
|
+
code: successCode,
|
|
3217
|
+
data: response.data || {},
|
|
3218
|
+
msg: 'download success',
|
|
3219
|
+
ctx: response.ctx,
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
1825
3222
|
});
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
if (
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
3223
|
+
app.post('/game/wasm-split-download', async (req, res) => {
|
|
3224
|
+
console.log('game/wasm-split-download-start', req.body);
|
|
3225
|
+
const response = await downloadSplited(req.body);
|
|
3226
|
+
console.log('game/wasm-split-download-end', response);
|
|
3227
|
+
if (response.error) {
|
|
3228
|
+
res.send({
|
|
3229
|
+
code: errorCode,
|
|
3230
|
+
error: response.error,
|
|
3231
|
+
ctx: response.ctx,
|
|
3232
|
+
});
|
|
3233
|
+
}
|
|
3234
|
+
else {
|
|
3235
|
+
res.send({
|
|
3236
|
+
code: successCode,
|
|
3237
|
+
data: response.data || {},
|
|
3238
|
+
msg: 'download success',
|
|
3239
|
+
ctx: response.ctx,
|
|
1834
3240
|
});
|
|
1835
3241
|
}
|
|
1836
3242
|
});
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
* 输入新的 clientKey
|
|
1860
|
-
*/
|
|
1861
|
-
const { clientKey } = await promptModule([
|
|
1862
|
-
{
|
|
1863
|
-
type: 'input',
|
|
1864
|
-
name: 'clientKey',
|
|
1865
|
-
message: 'Input new client key:',
|
|
1866
|
-
validate: input => {
|
|
1867
|
-
if (!input) {
|
|
1868
|
-
return 'Client key is required, please input client key';
|
|
1869
|
-
}
|
|
1870
|
-
return true;
|
|
1871
|
-
},
|
|
1872
|
-
},
|
|
1873
|
-
]);
|
|
1874
|
-
setTTMGRC({
|
|
1875
|
-
clientKey,
|
|
3243
|
+
app.post('/game/wasm-cancel', async (req, res) => {
|
|
3244
|
+
const { clientKey, codePath } = req.body;
|
|
3245
|
+
console.log('wasm-cancel', req.body);
|
|
3246
|
+
await cancelSplit({
|
|
3247
|
+
wasmCodePath: codePath,
|
|
3248
|
+
});
|
|
3249
|
+
res.send({
|
|
3250
|
+
code: successCode,
|
|
3251
|
+
msg: 'cancel success',
|
|
3252
|
+
});
|
|
3253
|
+
});
|
|
3254
|
+
app.post('/game/wasm-split-reset', async (req, res) => {
|
|
3255
|
+
const { clientKey, codeMd5, codePath } = req.body;
|
|
3256
|
+
console.log('wasm-split-reset', req.body);
|
|
3257
|
+
const response = await resetWasmSplit({
|
|
3258
|
+
clientkey: clientKey,
|
|
3259
|
+
wasmMd5: codeMd5});
|
|
3260
|
+
if (response.error) {
|
|
3261
|
+
res.send({
|
|
3262
|
+
code: errorCode,
|
|
3263
|
+
error: response.error,
|
|
3264
|
+
ctx: response.ctx,
|
|
1876
3265
|
});
|
|
1877
3266
|
}
|
|
1878
3267
|
else {
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
3268
|
+
res.send({
|
|
3269
|
+
code: successCode,
|
|
3270
|
+
data: response.data || {},
|
|
3271
|
+
ctx: response.ctx,
|
|
1882
3272
|
});
|
|
1883
3273
|
}
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
3274
|
+
});
|
|
3275
|
+
app.get('*', (req, res) => {
|
|
3276
|
+
res.sendFile(path.join(publicPath, `index.html`));
|
|
3277
|
+
});
|
|
3278
|
+
// --- 中间件和路由设置结束 ---
|
|
3279
|
+
// 步骤 2: 用配置好的 app 实例创建一个 http.Server。我们只创建这一次!
|
|
3280
|
+
const server = http.createServer(app);
|
|
3281
|
+
/**
|
|
3282
|
+
* @description 尝试在指定端口启动服务。这是个纯粹的辅助函数。
|
|
3283
|
+
* @param {number} port - 要尝试的端口号。
|
|
3284
|
+
* @returns {Promise<boolean>} 成功返回 true,因端口占用失败则返回 false。
|
|
3285
|
+
*/
|
|
3286
|
+
function tryListen(port) {
|
|
3287
|
+
return new Promise(resolve => {
|
|
3288
|
+
// 定义错误处理函数
|
|
3289
|
+
const onError = err => {
|
|
3290
|
+
// 清理掉另一个事件的监听器,防止内存泄漏
|
|
3291
|
+
server.removeListener('listening', onListening);
|
|
3292
|
+
if (err.code === 'EADDRINUSE') {
|
|
3293
|
+
console.log(chalk(`Port ${port} is already in use, trying ${port + 1}...`));
|
|
3294
|
+
resolve(false); // 明确表示因端口占用而失败
|
|
3295
|
+
}
|
|
3296
|
+
else {
|
|
3297
|
+
// 对于其他致命错误,直接退出进程
|
|
3298
|
+
console.log(chalk.red.bold(err.message));
|
|
3299
|
+
process.exit(1);
|
|
3300
|
+
}
|
|
3301
|
+
};
|
|
3302
|
+
// 定义成功处理函数
|
|
3303
|
+
const onListening = () => {
|
|
3304
|
+
// 清理掉另一个事件的监听器
|
|
3305
|
+
server.removeListener('error', onError);
|
|
3306
|
+
resolve(true); // 明确表示成功
|
|
3307
|
+
};
|
|
3308
|
+
// 使用 .once() 来确保监听器只执行一次然后自动移除
|
|
3309
|
+
server.once('error', onError);
|
|
3310
|
+
server.once('listening', onListening);
|
|
3311
|
+
// 执行监听动作
|
|
3312
|
+
server.listen(port);
|
|
1902
3313
|
});
|
|
1903
3314
|
}
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
3315
|
+
// 步骤 3: 使用循环来线性、串行地尝试启动服务
|
|
3316
|
+
let isListening = false;
|
|
3317
|
+
const maxRetries = 20; // 设置一个最大重试次数,以防万一
|
|
3318
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
3319
|
+
const currentPort = store.getState().nodeServerPort;
|
|
3320
|
+
isListening = await tryListen(currentPort);
|
|
3321
|
+
if (isListening) {
|
|
3322
|
+
break; // 成功,跳出循环
|
|
3323
|
+
}
|
|
3324
|
+
else {
|
|
3325
|
+
// 失败(端口占用),更新端口号,准备下一次循环
|
|
3326
|
+
store.setState({ nodeServerPort: currentPort + 1 });
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
// 步骤 4: 检查最终结果,如果所有尝试都失败了,则退出
|
|
3330
|
+
if (!isListening) {
|
|
3331
|
+
console.log(chalk.red.bold(`Failed to start server after trying ${maxRetries} ports.`));
|
|
1917
3332
|
process.exit(1);
|
|
1918
3333
|
}
|
|
3334
|
+
// --- 服务启动成功后的逻辑 ---
|
|
3335
|
+
// @ts-ignore
|
|
3336
|
+
const finalPort = server.address().port; // 从成功的 server 实例安全地获取最终端口
|
|
3337
|
+
console.log(chalk.green.bold(`TTMG`), chalk.green(`v${devToolVersion}`), chalk.gray(`ready in`), chalk.bold(`${Date.now() - startTime}ms`));
|
|
3338
|
+
const baseUrl = `http://localhost:${finalPort}?v=${devToolVersion}`;
|
|
3339
|
+
showTips({ server: baseUrl });
|
|
3340
|
+
openUrl(baseUrl);
|
|
1919
3341
|
}
|
|
1920
3342
|
|
|
1921
3343
|
async function dev() {
|
|
@@ -1927,7 +3349,7 @@ async function dev() {
|
|
|
1927
3349
|
watch();
|
|
1928
3350
|
}
|
|
1929
3351
|
|
|
1930
|
-
var version = "0.3.1-
|
|
3352
|
+
var version = "0.3.1-requirePlugin.0";
|
|
1931
3353
|
var pkg = {
|
|
1932
3354
|
version: version};
|
|
1933
3355
|
|