@ttmg/cli 0.3.1-login.2 → 0.3.1-unity.10

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