@ttmg/cli 0.3.2-beta.4 → 0.3.2-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/dist/index.js +1616 -989
  2. package/dist/index.js.map +1 -1
  3. package/dist/openDataContext/template/open_context.html.hbs +29 -0
  4. package/dist/openDataContext/template/open_context_sdk.js.hbs +16 -0
  5. package/dist/package.json +4 -3
  6. package/dist/public/assets/index-0QsVohJs.js +1 -0
  7. package/dist/public/assets/index-1oO8mc2L.js +1 -0
  8. package/dist/public/assets/index-2LCyHbgv.js +1 -0
  9. package/dist/public/assets/index-5oEydzr8.js +1 -0
  10. package/dist/public/assets/index-7DkNQbVL.js +1 -0
  11. package/dist/public/assets/index-7DkNQbVL.js.br +0 -0
  12. package/dist/public/assets/index-7gXKDEMH.js +1 -0
  13. package/dist/public/assets/index-A5pSGmoW.js +1 -0
  14. package/dist/public/assets/index-A5pSGmoW.js.br +0 -0
  15. package/dist/public/assets/index-B-vfB9Ya.js +1 -0
  16. package/dist/public/assets/index-B0Cw26f2.js +1 -0
  17. package/dist/public/assets/index-B0lMLCEN.js +1 -0
  18. package/dist/public/assets/index-B0lMLCEN.js.br +0 -0
  19. package/dist/public/assets/index-B6HUBYPb.js +1 -0
  20. package/dist/public/assets/index-B7JMUpIM.js +23 -0
  21. package/dist/public/assets/index-B7JMUpIM.js.br +0 -0
  22. package/dist/public/assets/index-B7j-geVc.js +1 -0
  23. package/dist/public/assets/index-B8N8nU5k.js +1 -0
  24. package/dist/public/assets/index-B90t3m2Y.js +1 -0
  25. package/dist/public/assets/index-BA1vKWLl.js +1 -0
  26. package/dist/public/assets/index-BB5kLk47.css +1 -0
  27. package/dist/public/assets/index-BBiD3ZY0.js +1 -0
  28. package/dist/public/assets/index-BEkRt3ox.js +1 -0
  29. package/dist/public/assets/index-BH5OVuUr.js +1 -0
  30. package/dist/public/assets/index-BH5OVuUr.js.br +0 -0
  31. package/dist/public/assets/index-BIJQ2jN-.js +1 -0
  32. package/dist/public/assets/index-BK45hlql.js +1 -0
  33. package/dist/public/assets/index-BK45hlql.js.br +0 -0
  34. package/dist/public/assets/index-BNnfKu4_.js +1 -0
  35. package/dist/public/assets/index-BPf4cmgA.css +1 -0
  36. package/dist/public/assets/index-BQQoGoko.js +1 -0
  37. package/dist/public/assets/index-BR-m6RXi.js +1 -0
  38. package/dist/public/assets/index-BRZmkpre.js +1 -0
  39. package/dist/public/assets/index-BSH4idOV.js +1 -0
  40. package/dist/public/assets/index-BSH4idOV.js.br +0 -0
  41. package/dist/public/assets/index-BSb2BUk_.js +1 -0
  42. package/dist/public/assets/index-BTnlaKEm.js +1 -0
  43. package/dist/public/assets/index-BUT4SrUj.js +1 -0
  44. package/dist/public/assets/index-BUT4SrUj.js.br +0 -0
  45. package/dist/public/assets/index-BWO_zr5E.js +1 -0
  46. package/dist/public/assets/index-BWO_zr5E.js.br +0 -0
  47. package/dist/public/assets/index-Bb28RxEr.js +1 -0
  48. package/dist/public/assets/index-Bb28RxEr.js.br +0 -0
  49. package/dist/public/assets/index-BdGK9RSJ.js +1 -0
  50. package/dist/public/assets/index-Bf6aJOeV.css +1 -0
  51. package/dist/public/assets/index-BfX_lbWX.css +1 -0
  52. package/dist/public/assets/index-BfX_lbWX.css.br +0 -0
  53. package/dist/public/assets/index-BfnzurfX.js +1 -0
  54. package/dist/public/assets/index-BkiB8M4Y.css +1 -0
  55. package/dist/public/assets/index-BkiB8M4Y.css.br +0 -0
  56. package/dist/public/assets/index-BmT98gGj.js +1 -0
  57. package/dist/public/assets/index-BmT98gGj.js.br +0 -0
  58. package/dist/public/assets/index-Bpv7pLX2.js +1 -0
  59. package/dist/public/assets/index-Bpv7pLX2.js.br +0 -0
  60. package/dist/public/assets/index-Bq6YxKLX.css +1 -0
  61. package/dist/public/assets/index-BreEkwI6.js +1 -0
  62. package/dist/public/assets/index-BreEkwI6.js.br +0 -0
  63. package/dist/public/assets/index-Bs8SawoL.js +1 -0
  64. package/dist/public/assets/index-Bs8SawoL.js.br +0 -0
  65. package/dist/public/assets/index-Bue_fL5q.css +1 -0
  66. package/dist/public/assets/index-Bue_fL5q.css.br +0 -0
  67. package/dist/public/assets/index-BujwLBmz.js +1 -0
  68. package/dist/public/assets/index-BvC2EkcY.js +1 -0
  69. package/dist/public/assets/index-Bw3iU4wV.js +1 -0
  70. package/dist/public/assets/index-BwbPFgZF.css +1 -0
  71. package/dist/public/assets/index-ByAY7l0v.js +1 -0
  72. package/dist/public/assets/index-Byxv2SPa.js +1 -0
  73. package/dist/public/assets/index-C250u6X0.css +1 -0
  74. package/dist/public/assets/index-C250u6X0.css.br +0 -0
  75. package/dist/public/assets/index-C2qYQNvg.js +1 -0
  76. package/dist/public/assets/index-C2qYQNvg.js.br +0 -0
  77. package/dist/public/assets/index-C46fvTvh.js +23 -0
  78. package/dist/public/assets/index-C46fvTvh.js.br +0 -0
  79. package/dist/public/assets/index-C4FRfjag.css +1 -0
  80. package/dist/public/assets/index-C4cINkXW.js +1 -0
  81. package/dist/public/assets/index-C4cINkXW.js.br +0 -0
  82. package/dist/public/assets/index-C8f539tK.css +1 -0
  83. package/dist/public/assets/index-C9KcyJn2.js +1 -0
  84. package/dist/public/assets/index-C9_SXd2H.css +1 -0
  85. package/dist/public/assets/index-CAwhcizP.css +1 -0
  86. package/dist/public/assets/index-CAwhcizP.css.br +0 -0
  87. package/dist/public/assets/index-CFwZ2h-R.js +1 -0
  88. package/dist/public/assets/index-CHTG85h6.js +1 -0
  89. package/dist/public/assets/index-CL1kuXlO.js +1 -0
  90. package/dist/public/assets/index-CPibVFVC.js +1 -0
  91. package/dist/public/assets/index-CSEt7Wc0.js +25 -0
  92. package/dist/public/assets/index-CSEt7Wc0.js.br +0 -0
  93. package/dist/public/assets/index-CUlfC1eK.js +1 -0
  94. package/dist/public/assets/index-CUlfC1eK.js.br +0 -0
  95. package/dist/public/assets/index-CV0599Ro.js +1 -0
  96. package/dist/public/assets/index-CV0599Ro.js.br +0 -0
  97. package/dist/public/assets/index-CWMGWfEt.js +1 -0
  98. package/dist/public/assets/index-CWS-rk-J.js +23 -0
  99. package/dist/public/assets/index-CWS-rk-J.js.br +0 -0
  100. package/dist/public/assets/index-CXQMSlbc.js +1 -0
  101. package/dist/public/assets/index-CXQMSlbc.js.br +0 -0
  102. package/dist/public/assets/index-CYyj9uYU.js +1 -0
  103. package/dist/public/assets/index-CYyj9uYU.js.br +0 -0
  104. package/dist/public/assets/index-CZtKeBZm.js +1 -0
  105. package/dist/public/assets/index-Ca67110A.js +1 -0
  106. package/dist/public/assets/index-CdrbgBJX.js +1 -0
  107. package/dist/public/assets/index-CfmWGzK2.js +1 -0
  108. package/dist/public/assets/index-Cfp2WBke.js +1 -0
  109. package/dist/public/assets/index-Cfp2WBke.js.br +0 -0
  110. package/dist/public/assets/index-CgVq-50w.css +1 -0
  111. package/dist/public/assets/index-CgVq-50w.css.br +0 -0
  112. package/dist/public/assets/index-ChblnyYX.js +1 -0
  113. package/dist/public/assets/index-ChceQzYz.js +1 -0
  114. package/dist/public/assets/index-CjzSEyzv.js +1 -0
  115. package/dist/public/assets/index-CluLESuq.js +1 -0
  116. package/dist/public/assets/index-CluLESuq.js.br +0 -0
  117. package/dist/public/assets/index-CnBegIDJ.js +1 -0
  118. package/dist/public/assets/index-Cnuj31jy.js +1 -0
  119. package/dist/public/assets/index-CzYlYBW_.js +1 -0
  120. package/dist/public/assets/index-D-_HSgCe.js +1 -0
  121. package/dist/public/assets/index-D1_vcunJ.js +23 -0
  122. package/dist/public/assets/index-D1_vcunJ.js.br +0 -0
  123. package/dist/public/assets/index-D4s2xpeA.js +1 -0
  124. package/dist/public/assets/index-D5ydsq9J.js +24 -0
  125. package/dist/public/assets/index-D5ydsq9J.js.br +0 -0
  126. package/dist/public/assets/index-D67sKNrw.css +1 -0
  127. package/dist/public/assets/index-D67sKNrw.css.br +0 -0
  128. package/dist/public/assets/index-D6RKE8qm.js +1 -0
  129. package/dist/public/assets/index-D6rLeEiy.js +1 -0
  130. package/dist/public/assets/index-DDC2R7_m.js +1 -0
  131. package/dist/public/assets/index-DDqw8mHL.js +1 -0
  132. package/dist/public/assets/index-DE1GZvdF.js +1 -0
  133. package/dist/public/assets/index-DFD_qEAi.js +1 -0
  134. package/dist/public/assets/index-DFEMmQoM.js +23 -0
  135. package/dist/public/assets/index-DFEMmQoM.js.br +0 -0
  136. package/dist/public/assets/index-DGHL1pSq.js +1 -0
  137. package/dist/public/assets/index-DJ-xo_2v.js +1 -0
  138. package/dist/public/assets/index-DJIK_Un5.js +23 -0
  139. package/dist/public/assets/index-DJIK_Un5.js.br +0 -0
  140. package/dist/public/assets/index-DKK8dOYS.js +1 -0
  141. package/dist/public/assets/index-DNmLdBiH.js +1 -0
  142. package/dist/public/assets/index-DNmLdBiH.js.br +0 -0
  143. package/dist/public/assets/index-DP2DtK77.js +1 -0
  144. package/dist/public/assets/index-DPSts5Re.css +1 -0
  145. package/dist/public/assets/index-DPSts5Re.css.br +0 -0
  146. package/dist/public/assets/index-DT6IV9TH.js +1 -0
  147. package/dist/public/assets/index-DU0eX6vn.js +1 -0
  148. package/dist/public/assets/index-DU0eX6vn.js.br +0 -0
  149. package/dist/public/assets/index-DWVXwBku.js +1 -0
  150. package/dist/public/assets/index-DXFg6tGR.css +1 -0
  151. package/dist/public/assets/index-DaS-23NX.js +1 -0
  152. package/dist/public/assets/index-Db0ve94g.js +1 -0
  153. package/dist/public/assets/index-Db0ve94g.js.br +0 -0
  154. package/dist/public/assets/index-DcTZlirs.js +1 -0
  155. package/dist/public/assets/index-De1gwiJs.js +1 -0
  156. package/dist/public/assets/index-De1gwiJs.js.br +0 -0
  157. package/dist/public/assets/index-DfGgXJTb.js +1 -0
  158. package/dist/public/assets/index-DfGgXJTb.js.br +0 -0
  159. package/dist/public/assets/index-DfZFMeOV.js +1 -0
  160. package/dist/public/assets/index-DfZFMeOV.js.br +0 -0
  161. package/dist/public/assets/index-DjHdTW8H.js +1 -0
  162. package/dist/public/assets/index-DlnMPCtX.js +1 -0
  163. package/dist/public/assets/index-Do_Bcsn1.js +1 -0
  164. package/dist/public/assets/index-DpFldGAl.js +1 -0
  165. package/dist/public/assets/index-DpgkUWkv.css +1 -0
  166. package/dist/public/assets/index-DpgkUWkv.css.br +0 -0
  167. package/dist/public/assets/index-DrFLOJ8g.js +23 -0
  168. package/dist/public/assets/index-DrFLOJ8g.js.br +0 -0
  169. package/dist/public/assets/index-DuBOXL7A.js +23 -0
  170. package/dist/public/assets/index-DuBOXL7A.js.br +0 -0
  171. package/dist/public/assets/index-DxQ0R3Pw.js +1 -0
  172. package/dist/public/assets/index-DyMZ_Lkc.js +1 -0
  173. package/dist/public/assets/index-DzLOWSx9.js +1 -0
  174. package/dist/public/assets/index-DzLOWSx9.js.br +0 -0
  175. package/dist/public/assets/index-FEOLSRkl.js +1 -0
  176. package/dist/public/assets/index-Fi4Rrt8I.js +1 -0
  177. package/dist/public/assets/index-HY89kziJ.js +1 -0
  178. package/dist/public/assets/index-Ifzp5fEn.js +1 -0
  179. package/dist/public/assets/index-J11P4g5S.js +1 -0
  180. package/dist/public/assets/index-J11P4g5S.js.br +0 -0
  181. package/dist/public/assets/index-JSX8aCBJ.js +1 -0
  182. package/dist/public/assets/index-JSX8aCBJ.js.br +0 -0
  183. package/dist/public/assets/index-JvC1YVUI.js +1 -0
  184. package/dist/public/assets/index-MT2NggoZ.js +1 -0
  185. package/dist/public/assets/index-MT2NggoZ.js.br +0 -0
  186. package/dist/public/assets/index-NdyUV0z0.js +1 -0
  187. package/dist/public/assets/index-OVJGZ7cU.js +1 -0
  188. package/dist/public/assets/index-T0nyAjcT.js +24 -0
  189. package/dist/public/assets/index-T0nyAjcT.js.br +0 -0
  190. package/dist/public/assets/index-UfX9eOrO.js +1 -0
  191. package/dist/public/assets/index-UujKgP-l.js +1 -0
  192. package/dist/public/assets/index-V67-RbBt.js +1 -0
  193. package/dist/public/assets/index-V67-RbBt.js.br +0 -0
  194. package/dist/public/assets/index-VzuG_wgX.js +1 -0
  195. package/dist/public/assets/index-XZ5vQlJ4.js +1 -0
  196. package/dist/public/assets/index-XdW2ZDCl.js +1 -0
  197. package/dist/public/assets/index-_ZVXaqDY.js +1 -0
  198. package/dist/public/assets/index-aPvChUsm.js +1 -0
  199. package/dist/public/assets/index-b5aKh2UT.js +1 -0
  200. package/dist/public/assets/index-bgdF_Zeb.js +23 -0
  201. package/dist/public/assets/index-bgdF_Zeb.js.br +0 -0
  202. package/dist/public/assets/index-d9sXoj67.js +1 -0
  203. package/dist/public/assets/index-d9sXoj67.js.br +0 -0
  204. package/dist/public/assets/index-ea8VcG1l.js +1 -0
  205. package/dist/public/assets/index-eoaE1BZf.css +1 -0
  206. package/dist/public/assets/index-eoaE1BZf.css.br +0 -0
  207. package/dist/public/assets/index-gZxXPtMU.js +1 -0
  208. package/dist/public/assets/index-hGMU1JI_.js +1 -0
  209. package/dist/public/assets/index-khqRg3Tb.js +1 -0
  210. package/dist/public/assets/index-nCPCU1qS.js +1 -0
  211. package/dist/public/assets/index-umN-hrVX.js +1 -0
  212. package/dist/public/assets/index-yh4tKekV.css +1 -0
  213. package/dist/public/assets/index-z6YlZek6.js +1 -0
  214. package/dist/public/index.html +2 -2
  215. package/dist/scripts/resetLocalState.js +48 -0
  216. package/dist/scripts/worker.js +62 -12
  217. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -3,205 +3,59 @@
3
3
 
4
4
  var commander = require('commander');
5
5
  var inquirer = require('inquirer');
6
- var fs = require('fs');
7
- var jsdom = require('jsdom');
8
- var prettier = require('prettier');
9
- var chalk = require('chalk');
10
- var express = require('express');
11
6
  var path = require('path');
12
- var cheerio = require('cheerio');
13
7
  var os = require('os');
14
- var worker_threads = require('worker_threads');
15
8
  var axios = require('axios');
16
- var qs = require('qs');
9
+ var fs = require('fs');
17
10
  var handlebars = require('handlebars');
18
11
  var esbuild = require('esbuild');
12
+ var chalk = require('chalk');
13
+ var boxen = require('boxen');
14
+ var semver = require('semver');
15
+ var jsdom = require('jsdom');
16
+ var prettier = require('prettier');
17
+ var express = require('express');
18
+ var cheerio = require('cheerio');
19
19
  var archiver = require('archiver');
20
20
  var chokidar = require('chokidar');
21
21
  var WebSocket = require('ws');
22
+ var worker_threads = require('worker_threads');
22
23
  var glob = require('glob');
23
24
  var got = require('got');
24
25
  var FormData$1 = require('form-data');
25
26
  var stream = require('stream');
26
27
  var ttmgPack = require('ttmg-pack');
27
- var boxen = require('boxen');
28
28
  var http = require('http');
29
+ var expressStaticGzip = require('express-static-gzip');
30
+ var fileUpload = require('express-fileupload');
29
31
  var fs$1 = require('node:fs');
30
32
  var path$1 = require('node:path');
31
33
  var zlib = require('zlib');
32
34
  var promises = require('fs/promises');
33
- var expressStaticGzip = require('express-static-gzip');
34
- var fileUpload = require('express-fileupload');
35
+ var qs = require('qs');
35
36
 
36
37
  function _interopNamespaceDefault(e) {
37
- var n = Object.create(null);
38
- if (e) {
39
- Object.keys(e).forEach(function (k) {
40
- if (k !== 'default') {
41
- var d = Object.getOwnPropertyDescriptor(e, k);
42
- Object.defineProperty(n, k, d.get ? d : {
43
- enumerable: true,
44
- get: function () { return e[k]; }
38
+ var n = Object.create(null);
39
+ if (e) {
40
+ Object.keys(e).forEach(function (k) {
41
+ if (k !== 'default') {
42
+ var d = Object.getOwnPropertyDescriptor(e, k);
43
+ Object.defineProperty(n, k, d.get ? d : {
44
+ enumerable: true,
45
+ get: function () { return e[k]; }
46
+ });
47
+ }
45
48
  });
46
- }
47
- });
48
- }
49
- n.default = e;
50
- return Object.freeze(n);
49
+ }
50
+ n.default = e;
51
+ return Object.freeze(n);
51
52
  }
52
53
 
53
- var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
54
54
  var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
55
55
  var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
56
+ var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
56
57
  var glob__namespace = /*#__PURE__*/_interopNamespaceDefault(glob);
57
58
 
58
- const CONFIG_FILE_NAME = 'minigame.config.json';
59
- const SDK_URL = 'https://connect.tiktok-minis.com/game/sdk.js';
60
- const VCONSOLE_URL = 'https://connect.tiktok-minis.com/libs/vConsole.js';
61
- const VCONSOLE_INIT = `
62
- if(typeof VConsole === 'function') {
63
- window.vConsole = new VConsole();
64
- }
65
- `;
66
- const MINIS_MANIFEST_FILE_NAME = 'minis.manifest.json';
67
- const MINIS_RUNTIME_URL = 'https://www.tiktok.com/minigames/runtime';
68
-
69
- const { JSDOM } = jsdom;
70
- const CONFIG_PATH$1 = `${process.cwd()}/${CONFIG_FILE_NAME}`;
71
- const INDEX_HTML_PATH = `${process.cwd()}/index.html`;
72
- function isSandbox(clientKey) {
73
- /**
74
- * sb 开头的 clientKey 都是 sandbox 环境
75
- */
76
- return clientKey.startsWith('sb');
77
- }
78
- // 判断是否是 TTMinis.game.init 的初始化脚本
79
- function isTTMinisInitScript(script, clientKey) {
80
- if (!script.innerHTML) {
81
- return false;
82
- }
83
- return (script.innerHTML.includes('TTMinis.game.init') &&
84
- script.innerHTML.includes(clientKey));
85
- }
86
- async function injectScripts({ config, clientKey }) {
87
- // 1. 检查 config 文件是否已存在
88
- fs.writeFileSync(CONFIG_PATH$1, JSON.stringify(config, null, 2));
89
- // 2. 读取 index.html
90
- if (!fs.existsSync(INDEX_HTML_PATH)) {
91
- console.error('index.html does not exist');
92
- return;
93
- }
94
- const indexHtml = fs.readFileSync(INDEX_HTML_PATH, 'utf8');
95
- const dom = new JSDOM(indexHtml);
96
- const { document } = dom.window;
97
- // 3. head 标签
98
- let head = document.querySelector('head');
99
- if (!head) {
100
- head = document.createElement('head');
101
- document.documentElement.insertBefore(head, document.body);
102
- }
103
- // 4. 检查是否已注入 SDK
104
- const scriptList = Array.from(document.querySelectorAll('script'));
105
- const hasSDK = scriptList.some(script => script.src && script.src.includes(SDK_URL));
106
- // 5. 检查是否已注入 vConsole
107
- const hasVConsole = scriptList.some(script => script.src && script.src.includes('vConsole.js'));
108
- let lastInitScript = null;
109
- if (!hasSDK) {
110
- // 插入 SDK 脚本
111
- const sdkScript = document.createElement('script');
112
- sdkScript.src = SDK_URL;
113
- head.insertBefore(sdkScript, head.firstChild);
114
- // 插入 SDK init 脚本
115
- const initScript = document.createElement('script');
116
- initScript.innerHTML = `
117
- window.TTMinis = TTMinis;
118
- TTMinis.game.init({
119
- clientKey: "${clientKey}",
120
- });
121
- `;
122
- head.insertBefore(initScript, sdkScript.nextSibling);
123
- lastInitScript = initScript;
124
- }
125
- else {
126
- // 已经有 SDK,查找 TTMinis.game.init 脚本
127
- const headScripts = Array.from(head.querySelectorAll('script'));
128
- lastInitScript = headScripts.find(script => isTTMinisInitScript(script, clientKey));
129
- if (lastInitScript) {
130
- console.log('JS SDK 已接入,跳过 SDK 相关脚本注入');
131
- }
132
- else {
133
- // 没有 TTMinis.game.init,则查找最后一个 SDK 脚本
134
- const sdkScripts = headScripts.filter(script => script.src && script.src.includes(SDK_URL));
135
- if (sdkScripts.length > 0) {
136
- lastInitScript = sdkScripts[sdkScripts.length - 1];
137
- }
138
- }
139
- }
140
- /**
141
- * 只有 Sandbox 环境才需要注入 vConsole
142
- */
143
- if (isSandbox(clientKey)) {
144
- console.log('Sandbox 环境,跳过 vConsole 相关脚本注入');
145
- // 8. 插入 vConsole 相关脚本(如果需要)
146
- if (!hasVConsole) {
147
- // vConsole 相关脚本的插入点
148
- let insertAfterNode = lastInitScript;
149
- if (insertAfterNode) {
150
- insertAfterNode = insertAfterNode.nextSibling;
151
- }
152
- else {
153
- insertAfterNode = head.firstChild;
154
- }
155
- // vConsole 源码
156
- const vconsoleSourceScript = document.createElement('script');
157
- vconsoleSourceScript.src = VCONSOLE_URL;
158
- // vConsole 初始化
159
- const vconsoleInitScript = document.createElement('script');
160
- vconsoleInitScript.innerHTML = VCONSOLE_INIT;
161
- head.insertBefore(vconsoleSourceScript, insertAfterNode);
162
- head.insertBefore(vconsoleInitScript, insertAfterNode);
163
- }
164
- }
165
- // 9. 格式化并写回 index.html
166
- const formattedHtml = await prettier.format(dom.serialize(), {
167
- parser: 'html',
168
- });
169
- fs.writeFileSync(INDEX_HTML_PATH, formattedHtml);
170
- console.log(chalk.green.bold('TikTok H5 Mini Game initialization has been completed...'));
171
- }
172
-
173
- function init$1() {
174
- const promptModule = inquirer.createPromptModule();
175
- promptModule([
176
- {
177
- type: 'input',
178
- name: 'clientKey',
179
- message: 'Please input client key',
180
- },
181
- {
182
- type: 'input',
183
- name: 'devPort',
184
- message: 'Please input dev port',
185
- default: 9527,
186
- },
187
- ])
188
- .then(async (answers) => {
189
- const { clientKey, devPort } = answers;
190
- const config = {
191
- _comment: 'orientation is the orientation of the game. It can be either \'VERTICAL\' or \'HORIZONTAL\'.our game default is VERTICAL; minigame.config.json dev is a configuration file for minigame development. You can use it to configure the minigame.',
192
- orientation: 'VERTICAL',
193
- dev: {
194
- port: devPort,
195
- },
196
- };
197
- await injectScripts({ clientKey, config });
198
- process.exit(0);
199
- })
200
- .catch(() => {
201
- process.exit(1);
202
- });
203
- }
204
-
205
59
  async function openUrl(url) {
206
60
  const { launch } = await import('chrome-launcher');
207
61
  try {
@@ -209,7 +63,7 @@ async function openUrl(url) {
209
63
  await launch({
210
64
  startingUrl: url,
211
65
  chromeFlags: [
212
- '--auto-open-devtools-for-tabs', // 自动打开 DevTools
66
+ '--auto-open-devtools-for-tabs',
213
67
  '--no-default-browser-check',
214
68
  '--allow-insecure-localhost',
215
69
  '--allow-running-insecure-content',
@@ -217,6 +71,7 @@ async function openUrl(url) {
217
71
  `--user-data-dir=${userDataDir}`,
218
72
  '--disk-cache-size=0',
219
73
  '--media-cache-size=0',
74
+ '--disable-web-security',
220
75
  ],
221
76
  });
222
77
  await new Promise(resolve => { });
@@ -224,46 +79,18 @@ async function openUrl(url) {
224
79
  }
225
80
  catch (e) {
226
81
  return false;
227
- // return Promise.reject(e);
228
- }
229
- }
230
-
231
- function getLocalIP() {
232
- const networkInterfaces = os.networkInterfaces();
233
- for (const interfaceName in networkInterfaces) {
234
- const interfaceInfo = networkInterfaces[interfaceName];
235
- if (!interfaceInfo)
236
- continue;
237
- for (const addressInfo of interfaceInfo) {
238
- if (addressInfo.family === 'IPv4' && !addressInfo.internal) {
239
- return addressInfo.address;
240
- }
241
- }
242
82
  }
243
83
  }
244
84
 
245
- async function checkUpdate() {
246
- const worker = new worker_threads.Worker(path.resolve(__dirname, './scripts/worker.js'));
247
- worker.on('message', msg => {
248
- // console.log(msg);
249
- });
250
- worker.on('error', err => {
251
- console.error(err);
252
- });
253
- worker.postMessage({
254
- type: 'checkUpdate',
255
- });
256
- }
257
-
258
- const CONFIG_PATH = process.env.TTMGRC_PATH || path.join(os.homedir(), '.ttmgrc');
85
+ const CONFIG_PATH$1 = process.env.TTMGRC_PATH || path.join(os.homedir(), '.ttmgrc');
259
86
  const getTTMGRC = () => {
260
87
  // only check one time
261
- if (!fs.existsSync(CONFIG_PATH)) {
88
+ if (!fs.existsSync(CONFIG_PATH$1)) {
262
89
  return null;
263
90
  }
264
91
  else {
265
92
  // safe parse
266
- let res = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
93
+ let res = JSON.parse(fs.readFileSync(CONFIG_PATH$1, 'utf8'));
267
94
  return res;
268
95
  }
269
96
  };
@@ -271,198 +98,36 @@ const setTTMGRC = (config) => {
271
98
  // updata cache config
272
99
  config = config;
273
100
  const originConfig = getTTMGRC() || {};
274
- fs.writeFileSync(CONFIG_PATH, JSON.stringify({ ...originConfig, ...config }));
101
+ fs.writeFileSync(CONFIG_PATH$1, JSON.stringify({ ...originConfig, ...config }));
275
102
  };
276
-
277
- function printMessage(type, message) {
278
- const prefix = type === 'Error' ? chalk.red('Error: ') : chalk.yellow('Warning: ');
279
- const log = type === 'Error' ? console.error : console.warn;
280
- log(prefix + message);
281
- }
282
- let spinner$1;
283
- const LOGIN_TT4D = 'https://developers.tiktok.com/passport/web/email/login';
284
- const params = {
285
- aid: '2471',
286
- account_sdk_source: 'web',
287
- sdk_version: '2.1.6-tiktok',
103
+ const resetTTMGRC = (config = {}) => {
104
+ fs.writeFileSync(CONFIG_PATH$1, JSON.stringify(config));
288
105
  };
289
- const prompt = inquirer.createPromptModule();
290
- async function login(options) {
291
- const verbose = options?.verbose === true;
292
- const log = (msg, data) => {
293
- if (!verbose)
294
- return;
295
- console.log(chalk.gray('[ttmg login]'), msg, data !== undefined ? data : '');
296
- };
297
- if (verbose)
298
- log('Verbose logging enabled');
299
- console.log(chalk.yellowBright('⚠️ Note: Please login with your TikTok Developer Platform account.'));
300
- // 增加对邮箱密码的校验
301
- const { email, password } = await prompt([
302
- {
303
- type: 'input',
304
- name: 'email',
305
- message: 'Email:',
306
- validate: input => {
307
- if (!input) {
308
- return 'email is required, please input email';
309
- }
310
- else {
311
- /**
312
- * 邮箱格式校验
313
- */
314
- if (!/^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/.test(input)) {
315
- return 'email format is invalid';
316
- }
317
- }
318
- return true;
319
- },
320
- },
321
- {
322
- type: 'password',
323
- name: 'password',
324
- message: 'Password:',
325
- mask: '*',
326
- validate: input => {
327
- if (!input) {
328
- return 'password is required, please input password';
329
- }
330
- return true;
331
- },
332
- },
333
- ]);
334
- const url = LOGIN_TT4D + '?' + new URLSearchParams(params);
335
- log('Request URL', url);
336
- log('Request params', { ...params, email: email.replace(/(.{2}).*(@.*)/, '$1***$2') });
337
- const headers = {
338
- 'Content-Type': 'application/x-www-form-urlencoded',
339
- Accept: '*/*',
340
- 'Accept-Encoding': 'gzip, deflate, br',
341
- Origin: 'https://developers.tiktok.com',
342
- Referer: 'https://developers.tiktok.com/passport/web/email/login',
343
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0 Safari/537.36',
344
- };
345
- const ora = await import('ora');
346
- spinner$1 = ora.default({
347
- text: chalk.bold.cyan('Logging in...'),
348
- spinner: 'dots',
349
- });
350
- spinner$1.start();
351
- const data = qs.stringify({
352
- email,
353
- password,
354
- mix_mode: '1',
355
- fixed_mix_mode: '1',
356
- });
357
- try {
358
- log('Sending POST request...');
359
- const response = await axios.post(url, data, {
360
- headers,
361
- maxRedirects: 20,
362
- timeout: 30000,
363
- });
364
- log('Response status', response.status);
365
- log('Response data', response?.data);
366
- if (!response?.data?.data?.user_id) {
367
- const errCode = response.data?.data?.error_code;
368
- const errMsg = response.data?.data?.description;
369
- const statusText = response?.statusText ?? '';
370
- spinner$1.fail(chalk.red('login failed'));
371
- if (verbose) {
372
- console.log(chalk.gray('Response status:'), response?.status);
373
- console.log(chalk.gray('Response statusText:'), statusText || '(empty)');
374
- console.log(chalk.gray('Response data:'), response?.data ?? '(empty)');
375
- console.log('');
376
- }
377
- if (errCode || errMsg) {
378
- log('Login failed (api)', { errCode, errMsg, fullData: response?.data });
379
- printMessage('Error', `Login failed: ${errMsg}${errCode ? `, error_code: ${errCode}` : ''}`);
380
- }
381
- else {
382
- log('Login failed (no user_id)', { responseBody: response?.data });
383
- if (verbose)
384
- log('Full response (for debugging)', response);
385
- const looksLikeProxyResponse = statusText === 'Connection established' ||
386
- (response?.status === 200 &&
387
- (response?.data == null ||
388
- typeof response.data !== 'object' ||
389
- !('data' in response.data)));
390
- if (looksLikeProxyResponse) {
391
- printMessage('Warning', [
392
- 'The response does not look like the login API (e.g. proxy returned "Connection established" instead of forwarding the real response).',
393
- '',
394
- 'The API is not reachable from mainland without proxy. Please ensure your proxy correctly forwards HTTPS to developers.tiktok.com: try another proxy node, or check that the proxy is in global/tunnel mode and not intercepting HTTPS.',
395
- '',
396
- 'Proxy troubleshooting doc:',
397
- 'https://bytedance.larkoffice.com/wiki/ZblJwT0ZNil9jJkS8EgcFlcQnFc',
398
- ].join('\n'));
399
- }
400
- else {
401
- printMessage('Error', 'Login failed. No user_id in response.');
402
- }
403
- }
404
- spinner$1.stop();
405
- process.exit(1);
406
- }
407
- else {
408
- const data = {
409
- email,
410
- user_id: response?.data?.data?.user_id,
411
- cookie: response?.headers['set-cookie'].join('; '),
412
- };
413
- log('Login success', { user_id: data.user_id, email: data.email });
414
- setTTMGRC(data);
415
- spinner$1.succeed(chalk.bold.green('login successfully!'));
416
- process.exit(0);
417
- }
418
- }
419
- catch (error) {
420
- log('Request error', error instanceof Error ? { message: error.message, name: error.name, stack: error.stack } : error);
421
- spinner$1.fail(chalk.red.bold('Failed to connect to login service'));
422
- printMessage('Error', [
423
- "Detected that the current terminal's network proxy settings are preventing external network access.",
424
- '',
425
- 'Please check your local terminal proxy configuration. You can follow this doc to modify your terminal network settings:',
426
- 'https://bytedance.larkoffice.com/wiki/ZblJwT0ZNil9jJkS8EgcFlcQnFc',
427
- ].join('\n'));
428
- process.exit(1);
429
- }
430
- }
431
-
432
- async function setup(options) {
433
- const inputLang = options?.lang;
434
- const normalizedLang = inputLang === 'en-US' || inputLang === 'zh-CN' ? inputLang : null;
435
- const lang = normalizedLang ??
436
- (await inquirer.createPromptModule()([
437
- {
438
- type: 'list',
439
- name: 'lang',
440
- message: 'Select language your prefer',
441
- choices: [
442
- { name: 'English (en-US)', value: 'en-US' },
443
- { name: '简体中文 (zh-CN)', value: 'zh-CN' },
444
- ],
445
- default: 'en-US',
446
- },
447
- ])).lang;
448
- setTTMGRC({ lang });
449
- }
450
-
451
- // ppe_dev_tool
452
- async function request({ url, method, data, headers, params, }) {
453
- const config = getTTMGRC();
454
- const cookie = config?.cookie;
455
- try {
456
- const res = await axios({
457
- url,
458
- method,
459
- data,
460
- params,
461
- headers: {
462
- Cookie: cookie,
463
- 'x-use-ppe': '1',
464
- 'x-ppe-env': 'ppe_upgrade_script',
465
- ...(headers || {}),
106
+ const getCurrentUser = () => {
107
+ try {
108
+ const config = getTTMGRC();
109
+ return config;
110
+ }
111
+ catch (err) {
112
+ return null;
113
+ }
114
+ };
115
+
116
+ // ppe_dev_tool
117
+ async function request({ url, method, data, headers, params, }) {
118
+ const config = getTTMGRC();
119
+ const cookie = config?.cookie;
120
+ try {
121
+ const res = await axios({
122
+ url,
123
+ method,
124
+ data,
125
+ params,
126
+ headers: {
127
+ Cookie: cookie,
128
+ // 'x-use-ppe': '1',
129
+ // 'x-tt-env': 'ppe_upgrade_script',
130
+ ...(headers || {}),
466
131
  },
467
132
  });
468
133
  // @ts-ignore
@@ -635,22 +300,6 @@ async function uploadGameToPlatform({ data, name, clientKey, note = '--', appId,
635
300
  }
636
301
  }
637
302
 
638
- function getCurrentUser() {
639
- try {
640
- const config = getTTMGRC();
641
- return config;
642
- }
643
- catch (err) {
644
- return null;
645
- }
646
- }
647
-
648
- function ensureDirSync(dirPath) {
649
- if (!fs.existsSync(dirPath)) {
650
- fs.mkdirSync(dirPath, { recursive: true });
651
- }
652
- }
653
-
654
303
  const bareToRelativePlugin = {
655
304
  name: 'bare-to-relative',
656
305
  setup(build) {
@@ -685,8 +334,8 @@ function buildOpenContext(sourcePath) {
685
334
  }
686
335
 
687
336
  function generateOpenContextHtml(openContextPath, appId) {
688
- const templatePath = path.join(__dirname, 'template/open_context.html.hbs');
689
- const sdkPath = path.join(__dirname, 'template/open_context_sdk.js.hbs');
337
+ const templatePath = resolveTemplatePath('open_context.html.hbs');
338
+ const sdkPath = resolveTemplatePath('open_context_sdk.js.hbs');
690
339
  const templateSource = fs.readFileSync(templatePath).toString();
691
340
  const sdkSource = fs.readFileSync(sdkPath).toString();
692
341
  const template = handlebars.compile(templateSource);
@@ -699,127 +348,649 @@ function generateOpenContextHtml(openContextPath, appId) {
699
348
  });
700
349
  fs.writeFileSync('./open_context.html', openContextRes);
701
350
  }
351
+ function resolveTemplatePath(fileName) {
352
+ const runtimePath = path.join(__dirname, 'openDataContext/template', fileName);
353
+ if (fs.existsSync(runtimePath)) {
354
+ return runtimePath;
355
+ }
356
+ return path.join(process.cwd(), 'statics/openDataContext/template', fileName);
357
+ }
358
+
359
+ const messages = {
360
+ 'en-US': {
361
+ 'cli.description': 'TikTok Mini Games Command Line Tool',
362
+ 'cli.version.desc': 'Show version',
363
+ 'cli.option.dev.client': 'Debug TikTok Mini Games for Client',
364
+ 'cli.option.dev.h5': 'Debug TikTok Mini Games for Web',
365
+ 'cli.command.login.desc': 'Login with developer account',
366
+ 'cli.command.login.verbose': 'Print verbose logs for debugging',
367
+ 'cli.command.setup.desc': 'Initialize ttmg environment',
368
+ 'cli.command.setup.lang': 'Language (only supports): en-US | zh-CN',
369
+ 'cli.command.reset.desc': 'Reset local CLI state',
370
+ 'cli.command.init.desc': 'Initialize project',
371
+ 'cli.command.dev.desc': 'Open browser dev environment',
372
+ 'cli.command.build.desc': 'Bundle project',
373
+ 'cli.option.h5': 'H5 Mini Game',
374
+ 'cli.native.init.placeholder': 'Native Mini Game initialize',
375
+ 'cli.native.build.placeholder': 'Native Mini Game bundle',
376
+ 'setup.prompt.selectLanguage': 'Select your preferred language',
377
+ 'setup.choice.en': 'English (en-US)',
378
+ 'setup.choice.zh': '简体中文 (zh-CN)',
379
+ 'notice.versionUpdated': 'TTMG has been updated to v{version}.',
380
+ 'notice.setup.recommend': 'Tip: run setup once to configure your preferred CLI language.',
381
+ 'notice.setup.alreadyConfigured': 'Current language is {lang}. You can update it anytime.',
382
+ 'notice.setup.howTo': 'How to configure language:',
383
+ 'notice.setup.commandInteractive': '• Interactive: ttmg setup',
384
+ 'notice.setup.commandExplicit': '• Explicit: ttmg setup --lang en-US | ttmg setup --lang zh-CN',
385
+ 'notice.setup.commandHint': 'Run: ttmg setup (interactive) or ttmg setup --lang en-US | ttmg setup --lang zh-CN',
386
+ 'setup.error.unsupportedLang': 'Unsupported language: {lang}.',
387
+ 'setup.error.availableLangs': 'Supported languages: en-US, zh-CN.',
388
+ 'setup.error.chooseHint': 'Run `ttmg setup` to choose language interactively.',
389
+ 'setup.success': 'Setup completed. CLI language is now {lang}.',
390
+ 'reset.warning.title': 'Reset will perform these changes:',
391
+ 'reset.warning.lang': 'Language setting will be cleared.',
392
+ 'reset.warning.login': 'Login state will be cleared.',
393
+ 'reset.warning.clientKey': 'Historical debug client keys will be removed.',
394
+ 'reset.confirm.prompt': 'Do you want to continue reset?',
395
+ 'reset.confirm.yes': 'Yes, reset now',
396
+ 'reset.confirm.no': 'No, keep current local state',
397
+ 'reset.cancelled': 'Reset cancelled.',
398
+ 'reset.success': 'Local state has been reset.',
399
+ 'login.error.prefix': 'Error: ',
400
+ 'login.warning.prefix': 'Warning: ',
401
+ 'login.verbose.enabled': 'Verbose logging enabled',
402
+ 'login.note': '⚠️ Note: Please login with your TikTok Developer Platform account.',
403
+ 'login.email.prompt': 'Email:',
404
+ 'login.email.required': 'email is required, please input email',
405
+ 'login.email.invalid': 'email format is invalid',
406
+ 'login.password.prompt': 'Password:',
407
+ 'login.password.required': 'password is required, please input password',
408
+ 'login.spinner.loggingIn': 'Logging in...',
409
+ 'login.failed': 'login failed',
410
+ 'login.success': 'login successfully!',
411
+ 'login.error.withCode': 'Login failed: {message}, error_code: {code}',
412
+ 'login.error.withMessage': 'Login failed: {message}',
413
+ 'login.error.noUserId': 'Login failed. No user_id in response.',
414
+ 'login.warning.proxyIssue': 'The response does not look like the login API (e.g. proxy returned "Connection established" instead of forwarding the real response).\n\nThe API is not reachable from mainland without proxy. Please ensure your proxy correctly forwards HTTPS to developers.tiktok.com: try another proxy node, or check that the proxy is in global/tunnel mode and not intercepting HTTPS.\n\nProxy troubleshooting doc:\nhttps://bytedance.larkoffice.com/wiki/ZblJwT0ZNil9jJkS8EgcFlcQnFc',
415
+ 'login.error.connectService': 'Failed to connect to login service',
416
+ 'login.error.networkBlocked': "Detected that the current terminal's network proxy settings are preventing external network access.\n\nPlease check your local terminal proxy configuration. You can follow this doc to modify your terminal network settings:\nhttps://bytedance.larkoffice.com/wiki/ZblJwT0ZNil9jJkS8EgcFlcQnFc",
417
+ 'h5.dev.configMissing': '{file} is not exist, please run minis game init first',
418
+ 'h5.dev.precheckTips': '⚠️ Before dev, please ensure:\n 1. The account used to login www.tiktok.com is in the sandbox target user range of Minis developer platform, otherwise login authorization will throw an error.\n 2. The browser allows www.tiktok.com <popup and redirect>, because the authorization login linkage needs to open a new tab popup for operation, otherwise the authorization login linkage will not be able to debug normally.',
419
+ 'h5.dev.startingBanner': '\n \n============== start dev your game, it will take a few seconds ============ \n \n',
420
+ 'h5.dev.cspFailed': 'Failed to set CSP header:',
421
+ 'h5.dev.accessUrl': 'you can access {url} to debug your game in browser...',
422
+ 'h5.dev.openBrowserFailed': 'Failed to open browser, you can access it manually',
423
+ 'h5.bundle.configMissing': '{file} is not exist, please run minis game init first',
424
+ 'h5.bundle.prompt.zipName': 'Please input zip name',
425
+ 'h5.bundle.start': 'start build your game, it will take a few minutes...',
426
+ 'h5.bundle.success': 'build {zipName}.zip success, you can find it in desktop, use time {cost} ms',
427
+ 'h5.bundle.fail': 'auto build {zipName}.zip failed: {error}, you should zip it manually',
428
+ 'h5.bundle.desktopFallback': 'Desktop folder not found, using default path: {path}',
429
+ 'h5.init.prompt.clientKey': 'Please input client key',
430
+ 'h5.init.prompt.devPort': 'Please input dev port',
431
+ 'h5.init.indexMissing': 'index.html does not exist',
432
+ 'h5.init.sdkAlreadyInjected': 'JS SDK is already injected, skip SDK script injection',
433
+ 'h5.init.sandboxSkipVconsole': 'Sandbox environment detected, skip vConsole script injection',
434
+ 'h5.init.done': 'TikTok H5 Mini Game initialization has been completed...',
435
+ 'native.server.readyIn': 'ready in',
436
+ 'native.server.portInUse': 'Port {port} is already in use, trying {next}...',
437
+ 'native.server.failedAfterRetries': 'Failed to start server after trying {retries} ports.',
438
+ 'native.check.notWorkspace': 'Current directory is not a Mini Game project entry directory, please enter the game project root directory for debugging',
439
+ 'native.check.subpackagesCaseError': "Error: 'subPackages' (camelCase) is found in {file}. Please use 'subpackages' (all lowercase) instead.",
440
+ 'native.compile.watching': '🔔 Watching game assets for local debugging...',
441
+ 'native.compile.compiling': '🚀 Compiling game assets for local debugging...',
442
+ 'native.compile.success': '✔ Game resources compiled successfully!',
443
+ 'native.upload.spinner.start': 'Uploading game assets to client...',
444
+ 'native.upload.spinner.size': 'Start upload Game assets to client, size: {size} MB',
445
+ 'native.upload.spinner.progress': 'Uploading game assets to client... {percent}%',
446
+ 'native.upload.success': 'Upload game assets to client success! Cost: {cost}ms',
447
+ 'native.upload.fail': 'Upload failed with error: {error}, please check current debug env and try to scan qrcode to reupload again.',
448
+ 'native.upload.serverFail': '✖ Upload failed with server error, please scan qrcode to reupload',
449
+ 'native.upload.compressError': 'Error during compression:',
450
+ 'native.server.upload.packingDir': 'Packing current directory: {cwd} ...',
451
+ 'native.server.upload.packed': 'Pack completed, size: {size} MB',
452
+ 'native.server.upload.failed': 'Upload failed: {error}',
453
+ 'common.unknownError': 'An unknown error occurred.',
454
+ 'native.init.selectClientKey': 'Select game client key for debugging:',
455
+ 'native.init.lastUsed': '{clientKey} (last used)',
456
+ 'native.init.addNew': 'Add a new client key',
457
+ 'native.init.inputNew': 'Input new client key:',
458
+ 'native.init.inputYour': 'Input your Client Key:',
459
+ 'native.init.clientKeyRequired': 'Client key is required, please input client key',
460
+ 'native.tips.1': ' The QR code page will be opened automatically in Chrome.',
461
+ 'native.tips.1.sub': ' If it fails, please open the following link manually:',
462
+ 'native.tips.2': ' The debugging service will automatically compile your game assets.',
463
+ 'native.tips.2.sub1': ' Any changes in your game directory will trigger recompilation.',
464
+ 'native.tips.2.sub2': ' You can debug the updated content when upload is completed.',
465
+ 'native.tips.3': ' After scanning the QR code with your phone for Test User authentication,',
466
+ 'native.tips.3.sub1': ' the compiled code package will be uploaded to the client automatically.',
467
+ 'native.tips.3.sub2': ' Game debugging will start right away.',
468
+ 'native.tips.4': ' Before scanning with TikTok, make sure your phone and computer are on the same Wi-Fi.',
469
+ 'native.tips.4.sub1': ' This is required for stable local connection and debugging.',
470
+ },
471
+ 'zh-CN': {
472
+ 'cli.description': 'TikTok 小游戏命令行工具',
473
+ 'cli.version.desc': '显示版本号',
474
+ 'cli.option.dev.client': '客户端调试 TikTok 小游戏',
475
+ 'cli.option.dev.h5': 'Web 调试 TikTok 小游戏',
476
+ 'cli.command.login.desc': '使用开发者账号登录',
477
+ 'cli.command.login.verbose': '输出调试用详细日志',
478
+ 'cli.command.setup.desc': '初始化 ttmg 环境',
479
+ 'cli.command.setup.lang': '语言(仅支持):en-US | zh-CN',
480
+ 'cli.command.reset.desc': '重置本地 CLI 状态',
481
+ 'cli.command.init.desc': '初始化项目',
482
+ 'cli.command.dev.desc': '打开浏览器调试环境',
483
+ 'cli.command.build.desc': '打包项目',
484
+ 'cli.option.h5': 'H5 小游戏',
485
+ 'cli.native.init.placeholder': 'Native 小游戏初始化',
486
+ 'cli.native.build.placeholder': 'Native 小游戏打包',
487
+ 'setup.prompt.selectLanguage': '请选择偏好语言',
488
+ 'setup.choice.en': '英语 (en-US)',
489
+ 'setup.choice.zh': '简体中文 (zh-CN)',
490
+ 'notice.versionUpdated': 'TTMG 已升级到 v{version}。',
491
+ 'notice.setup.recommend': '建议执行一次 setup,配置 CLI 输出语言。',
492
+ 'notice.setup.alreadyConfigured': '当前语言为 {lang},你可以随时调整。',
493
+ 'notice.setup.howTo': '语言配置方式:',
494
+ 'notice.setup.commandInteractive': '• 交互式:ttmg setup',
495
+ 'notice.setup.commandExplicit': '• 显式指定:ttmg setup --lang en-US | ttmg setup --lang zh-CN',
496
+ 'notice.setup.commandHint': '执行:ttmg setup(交互选择)或 ttmg setup --lang en-US | ttmg setup --lang zh-CN',
497
+ 'setup.error.unsupportedLang': '不支持的语言:{lang}。',
498
+ 'setup.error.availableLangs': '可选语言:en-US、zh-CN。',
499
+ 'setup.error.chooseHint': '可执行 `ttmg setup` 进行交互选择。',
500
+ 'setup.success': '设置完成,CLI 语言已切换为 {lang}。',
501
+ 'reset.warning.title': '重置将执行以下操作:',
502
+ 'reset.warning.lang': '语言设置会被清空。',
503
+ 'reset.warning.login': '登录状态会被清空。',
504
+ 'reset.warning.clientKey': '历史调试的 client key 会被清理。',
505
+ 'reset.confirm.prompt': '是否继续重置?',
506
+ 'reset.confirm.yes': '是,立即重置',
507
+ 'reset.confirm.no': '否,保留当前本地状态',
508
+ 'reset.cancelled': '已取消重置。',
509
+ 'reset.success': '本地状态已重置。',
510
+ 'login.error.prefix': '错误:',
511
+ 'login.warning.prefix': '警告:',
512
+ 'login.verbose.enabled': '已开启详细日志',
513
+ 'login.note': '⚠️ 提示:请使用 TikTok 开发者平台账号登录。',
514
+ 'login.email.prompt': '邮箱:',
515
+ 'login.email.required': '邮箱必填,请输入邮箱',
516
+ 'login.email.invalid': '邮箱格式不正确',
517
+ 'login.password.prompt': '密码:',
518
+ 'login.password.required': '密码必填,请输入密码',
519
+ 'login.spinner.loggingIn': '登录中...',
520
+ 'login.failed': '登录失败',
521
+ 'login.success': '登录成功!',
522
+ 'login.error.withCode': '登录失败:{message},错误码:{code}',
523
+ 'login.error.withMessage': '登录失败:{message}',
524
+ 'login.error.noUserId': '登录失败,响应中未返回 user_id。',
525
+ 'login.warning.proxyIssue': '当前响应看起来不是登录接口返回(例如代理返回了 "Connection established",而不是转发真实响应)。\n\n该接口在大陆网络通常需要代理。请确认代理可正确转发 developers.tiktok.com 的 HTTPS 请求:尝试切换节点,或检查代理是否为全局/隧道模式且未拦截 HTTPS。\n\n代理排查文档:\nhttps://bytedance.larkoffice.com/wiki/ZblJwT0ZNil9jJkS8EgcFlcQnFc',
526
+ 'login.error.connectService': '连接登录服务失败',
527
+ 'login.error.networkBlocked': '检测到当前终端代理设置导致无法访问外网。\n\n请检查本地终端代理配置,可参考以下文档修改:\nhttps://bytedance.larkoffice.com/wiki/ZblJwT0ZNil9jJkS8EgcFlcQnFc',
528
+ 'h5.dev.configMissing': '{file} 不存在,请先执行 minis game init',
529
+ 'h5.dev.precheckTips': '⚠️ 开始调试前请确认:\n 1. 当前登录 www.tiktok.com 的账号在小程序开发者平台沙盒目标用户范围内,否则登录授权会报错。\n 2. 浏览器允许 www.tiktok.com 弹窗与重定向,授权登录链路需要新开标签页进行操作,否则无法正常调试。',
530
+ 'h5.dev.startingBanner': '\n \n============== 已开始调试你的游戏,通常需要几秒钟 ============ \n \n',
531
+ 'h5.dev.cspFailed': '设置 CSP 头失败:',
532
+ 'h5.dev.accessUrl': '可访问 {url} 在浏览器中调试游戏...',
533
+ 'h5.dev.openBrowserFailed': '自动打开浏览器失败,请手动访问链接',
534
+ 'h5.bundle.configMissing': '{file} 不存在,请先执行 minis game init',
535
+ 'h5.bundle.prompt.zipName': '请输入压缩包名称',
536
+ 'h5.bundle.start': '开始打包游戏,预计需要几分钟...',
537
+ 'h5.bundle.success': '打包 {zipName}.zip 成功,可在桌面找到,耗时 {cost} ms',
538
+ 'h5.bundle.fail': '自动打包 {zipName}.zip 失败:{error},请手动压缩',
539
+ 'h5.bundle.desktopFallback': '未找到桌面目录,使用默认路径:{path}',
540
+ 'h5.init.prompt.clientKey': '请输入 client key',
541
+ 'h5.init.prompt.devPort': '请输入调试端口',
542
+ 'h5.init.indexMissing': 'index.html 不存在',
543
+ 'h5.init.sdkAlreadyInjected': '检测到 JS SDK 已接入,跳过 SDK 相关脚本注入',
544
+ 'h5.init.sandboxSkipVconsole': '检测到 Sandbox 环境,跳过 vConsole 相关脚本注入',
545
+ 'h5.init.done': 'TikTok H5 小游戏初始化已完成...',
546
+ 'native.server.readyIn': '启动耗时',
547
+ 'native.server.portInUse': '端口 {port} 已被占用,尝试 {next}...',
548
+ 'native.server.failedAfterRetries': '连续尝试 {retries} 个端口后,服务启动失败。',
549
+ 'native.check.notWorkspace': '当前目录不是小游戏工程入口目录,请进入游戏项目根目录后再进行调试',
550
+ 'native.check.subpackagesCaseError': "错误:在 {file} 中发现 'subPackages'(驼峰),请改为全小写 'subpackages'。",
551
+ 'native.compile.watching': '🔔 正在监听游戏资源变更(本地调试)...',
552
+ 'native.compile.compiling': '🚀 正在编译游戏资源(本地调试)...',
553
+ 'native.compile.success': '✔ 游戏资源编译成功!',
554
+ 'native.upload.spinner.start': '正在上传游戏资源到客户端...',
555
+ 'native.upload.spinner.size': '开始上传游戏资源到客户端,大小:{size} MB',
556
+ 'native.upload.spinner.progress': '正在上传游戏资源到客户端... {percent}%',
557
+ 'native.upload.success': '上传游戏资源到客户端成功!耗时:{cost}ms',
558
+ 'native.upload.fail': '上传失败:{error},请检查当前调试环境并重新扫码上传。',
559
+ 'native.upload.serverFail': '✖ 上传失败(服务端错误),请重新扫码上传',
560
+ 'native.upload.compressError': '压缩过程中发生错误:',
561
+ 'native.server.upload.packingDir': '正在打包当前目录:{cwd} ...',
562
+ 'native.server.upload.packed': '打包完成,大小:{size} MB',
563
+ 'native.server.upload.failed': '上传失败:{error}',
564
+ 'common.unknownError': '发生未知错误。',
565
+ 'native.init.selectClientKey': '请选择用于调试的游戏 client key:',
566
+ 'native.init.lastUsed': '{clientKey}(上次使用)',
567
+ 'native.init.addNew': '新增一个 client key',
568
+ 'native.init.inputNew': '输入新的 client key:',
569
+ 'native.init.inputYour': '输入你的 Client Key:',
570
+ 'native.init.clientKeyRequired': 'client key 必填,请输入 client key',
571
+ 'native.tips.1': ' 将自动在 Chrome 打开二维码页面。',
572
+ 'native.tips.1.sub': ' 若失败,请手动打开以下链接:',
573
+ 'native.tips.2': ' 调试服务会自动编译你的游戏资源。',
574
+ 'native.tips.2.sub1': ' 游戏目录内的任何改动都会触发重新编译。',
575
+ 'native.tips.2.sub2': ' 上传完成后即可调试最新内容。',
576
+ 'native.tips.3': ' 手机扫码通过 Test User 认证后,',
577
+ 'native.tips.3.sub1': ' 编译后的代码包会自动上传到客户端。',
578
+ 'native.tips.3.sub2': ' 随即开始游戏调试。',
579
+ 'native.tips.4': ' 在使用 TikTok 扫码前,请确保手机和电脑处于同一个 Wi-Fi。',
580
+ 'native.tips.4.sub1': ' 这是本地连接与正常调试的前提条件。',
581
+ },
582
+ };
702
583
 
703
- const app = express();
704
- // 1. 检查配置文件是否存在
705
- function dev$1() {
706
- const configPath = path.join(process.cwd(), CONFIG_FILE_NAME);
707
- if (!fs.existsSync(configPath)) {
708
- console.log(chalk.red.bold(`${CONFIG_FILE_NAME} is not exist, please run minis game init first`));
709
- return;
584
+ function resolveSupportedLanguage(lang) {
585
+ if (!lang) {
586
+ return undefined;
710
587
  }
711
- // 2. 读取配置
712
- const gameConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
713
- const devPort = gameConfig.dev?.port || 9527;
714
- const hasOpenContext = !!gameConfig.openDataContext;
715
- if (hasOpenContext) {
716
- generateOpenContextHtml(gameConfig.openDataContext, gameConfig.app_id);
588
+ if (lang in messages) {
589
+ return lang;
717
590
  }
718
- // 3. 打印开发前提示
719
- console.log(chalk.yellow.bold('⚠️ Before dev, please ensure:\n 1. The account used to login www.tiktok.com is in the sandbox target user range of Minis developer platform, otherwise login authorization will throw an error.\n 2. The browser allows www.tiktok.com <popup and redirect>, because the authorization login linkage needs to open a new tab popup for operation, otherwise the authorization login linkage will not be able to debug normally.'));
720
- console.log(chalk.bold.blue('\n \n============== start dev your game, it will take a few seconds ============ \n \n'));
721
- /**
722
- * 支持 .br 文件, 支持 gzip
723
- */
724
- app.use((req, res, next) => {
725
- if (req.url.endsWith('.br')) {
726
- res.setHeader('Content-Encoding', 'br');
727
- }
728
- else if (req.url.endsWith('.gz')) {
729
- res.setHeader('Content-Encoding', 'gzip');
730
- }
731
- next();
591
+ const mainLang = lang.split('-')[0];
592
+ if (mainLang === 'zh') {
593
+ return 'zh-CN';
594
+ }
595
+ if (mainLang === 'en') {
596
+ return 'en-US';
597
+ }
598
+ return undefined;
599
+ }
600
+ function getConfiguredLanguage() {
601
+ return resolveSupportedLanguage(getTTMGRC()?.lang);
602
+ }
603
+ function getLanguage() {
604
+ return getConfiguredLanguage() ?? 'en-US';
605
+ }
606
+ function format(template, params) {
607
+ if (!params)
608
+ return template;
609
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
610
+ const value = params[key];
611
+ return value === undefined ? `{${key}}` : String(value);
732
612
  });
733
- /**
734
- * 给所有的请求返回设置 CSP
735
- */
736
- app.use((req, res, next) => {
737
- /**
738
- * 计算 HTML 中的内联脚本生成 hash 插入 CSP 中
739
- */
740
- try {
741
- // 1. 读取 HTML 文件内容
742
- const htmlPath = path.join(process.cwd(), 'index.html');
743
- const html = fs.readFileSync(htmlPath, 'utf8');
744
- // 2. cheerio 解析 HTML
745
- const $ = cheerio.load(html);
746
- // 3. 提取所有无 src 属性的内联 <script> 内容
747
- const scripts = [];
748
- $('script:not([src])').each((i, elem) => {
749
- const content = $(elem).html();
750
- if (content && content.trim()) {
751
- scripts.push(content);
752
- }
753
- });
754
- // 4. 计算每段脚本的 SHA-256 hash 并 base64 编码
755
- // const hashes = scripts.map(script => {
756
- // const hash = crypto
757
- // .createHash('sha256')
758
- // .update(script, 'utf8')
759
- // .digest('base64');
760
- // return `'sha256-${hash}'`;
761
- // });
762
- // 开发者本地调试,信任的域名默认为 * 便于调试
763
- const devTrustedDomain = '*';
764
- res.setHeader('Content-Security-Policy', `default-src 'self';script-src 'self' data: blob: 'unsafe-eval' 'unsafe-inline' connect.tiktok-minis.com sf-connect.tiktokminis.us;img-src 'self' ${devTrustedDomain} data: blob: *; connect-src 'self' ${devTrustedDomain} data: blob: ; style-src 'self' ${devTrustedDomain} 'unsafe-inline' fonts.googleapis.com data: blob: *; font-src 'self' fonts.gstatic.com blob: data: *; media-src 'self' ${devTrustedDomain} data: blob: *; frame-src 'none'; base-uri 'self'; worker-src 'self' blob: data: ;`);
613
+ }
614
+ function t(key, params, langOverride) {
615
+ const lang = langOverride ?? getLanguage();
616
+ const template = messages[lang][key] ?? messages['en-US'][key] ?? key;
617
+ return format(template, params);
618
+ }
619
+ function getCurrentLanguage() {
620
+ return getLanguage();
621
+ }
622
+
623
+ const SETUP_NOTICE_ID = 'setup-language-onboarding';
624
+ const versionNoticeConfigs = [
625
+ {
626
+ id: SETUP_NOTICE_ID,
627
+ sinceVersion: '0.3.2-beta.5',
628
+ headlineKey: 'notice.versionUpdated',
629
+ bodyConfiguredKey: 'notice.setup.alreadyConfigured',
630
+ bodyUnconfiguredKey: 'notice.setup.recommend',
631
+ guideTitleKey: 'notice.setup.howTo',
632
+ guideItemKeys: ['notice.setup.commandInteractive', 'notice.setup.commandExplicit'],
633
+ dismissOnCommands: ['setup', 'reset'],
634
+ },
635
+ ];
636
+
637
+ const boxenFn = boxen.default ??
638
+ boxen;
639
+ function maybeShowPostInstallNotice(currentVersion) {
640
+ if (!process.stdout.isTTY)
641
+ return;
642
+ const parsedCurrentVersion = semver.valid(currentVersion);
643
+ if (!parsedCurrentVersion)
644
+ return;
645
+ const argv = process.argv.slice(2);
646
+ const isHelpOrVersion = argv.includes('-h') ||
647
+ argv.includes('--help') ||
648
+ argv.includes('-v') ||
649
+ argv.includes('--version');
650
+ if (isHelpOrVersion)
651
+ return;
652
+ const config = getTTMGRC() || {};
653
+ const seenNoticeIds = new Set(config.seenNoticeIds || []);
654
+ let shouldPersistState = false;
655
+ // Backward compatibility for old single-key notice status.
656
+ const legacyVersion = config.lastNotifiedCliVersion;
657
+ if (legacyVersion &&
658
+ semver.valid(legacyVersion) &&
659
+ semver.gte(legacyVersion, '0.3.2-beta.5') &&
660
+ !seenNoticeIds.has(SETUP_NOTICE_ID)) {
661
+ seenNoticeIds.add(SETUP_NOTICE_ID);
662
+ shouldPersistState = true;
663
+ }
664
+ const commandName = argv.find(arg => !arg.startsWith('-'));
665
+ const currentLang = config.lang;
666
+ const hasConfiguredLang = currentLang === 'en-US' || currentLang === 'zh-CN';
667
+ const pendingNotices = versionNoticeConfigs.filter(notice => {
668
+ if (seenNoticeIds.has(notice.id))
669
+ return false;
670
+ return semver.gte(parsedCurrentVersion, notice.sinceVersion);
671
+ });
672
+ for (const notice of pendingNotices) {
673
+ if (commandName && notice.dismissOnCommands?.includes(commandName)) {
674
+ seenNoticeIds.add(notice.id);
675
+ shouldPersistState = true;
676
+ continue;
765
677
  }
766
- catch (e) {
767
- // 如果 index.html 不存在或有异常,CSP 头就不设置
768
- console.warn(chalk.red('Failed to set CSP header:'), e.message);
678
+ const lines = [
679
+ chalk.green.bold(t(notice.headlineKey, { version: currentVersion })),
680
+ ];
681
+ if (hasConfiguredLang && notice.bodyConfiguredKey) {
682
+ lines.push(t(notice.bodyConfiguredKey, { lang: currentLang }));
769
683
  }
770
- next();
771
- });
772
- // 4. 静态资源服务
773
- app.use(express.static(path.join(process.cwd())));
774
- // 5. 启动服务并自动打开浏览器
775
- app.listen(devPort, () => {
776
- const gameUrl = `http://localhost:${devPort}`;
777
- const openContextUrl = `http://localhost:${devPort}/open_context.html`;
778
- let devUrl = `${MINIS_RUNTIME_URL}?minis_url=${gameUrl}&enable_log=1`;
779
- if (hasOpenContext) {
780
- devUrl += `&open_context_url=${openContextUrl}`;
684
+ else if (!hasConfiguredLang && notice.bodyUnconfiguredKey) {
685
+ lines.push(chalk.yellow(t(notice.bodyUnconfiguredKey)));
781
686
  }
782
- console.log(`you can access ${chalk.green.underline.bold(devUrl)} to debug your game in browser...`);
783
- try {
784
- // 自动打开浏览器,跨平台
785
- openUrl(devUrl);
687
+ if (notice.guideTitleKey) {
688
+ lines.push('');
689
+ lines.push(chalk.bold(t(notice.guideTitleKey)));
786
690
  }
787
- catch (e) {
788
- console.warn(chalk.red('Failed to open browser, you can access it manually'), e.message);
691
+ for (const itemKey of notice.guideItemKeys || []) {
692
+ lines.push(chalk.cyan(t(itemKey)));
789
693
  }
790
- });
791
- }
792
-
793
- async function buildMinisManifest() {
794
- try {
795
- const buildPath = path.join(process.cwd());
796
- const resourceList = [];
797
- const allFiles = collectAllFiles(buildPath);
798
- Object.keys(allFiles)
799
- .filter(file => !file.endsWith('.map'))
800
- .forEach(file => {
801
- const relativeFilePath = allFiles[file];
802
- const filePathArr = relativeFilePath.split('/').filter(Boolean); // Split filename
803
- const fileName = filePathArr.pop() || ''; // Get filename
804
- if (filePathArr.length === 0) {
805
- resourceList.push({ type: 'file', name: fileName });
806
- }
807
- else {
808
- const folder = findOrCreateFolder(filePathArr, resourceList);
809
- folder.children.push({ type: 'file', name: fileName });
810
- }
694
+ const content = lines.join('\n');
695
+ console.log(boxenFn(content, {
696
+ title: 'Notice',
697
+ titleAlignment: 'left',
698
+ borderStyle: 'round',
699
+ borderColor: 'cyan',
700
+ padding: {
701
+ top: 0,
702
+ right: 1,
703
+ bottom: 0,
704
+ left: 1,
705
+ },
706
+ margin: 1,
707
+ }));
708
+ seenNoticeIds.add(notice.id);
709
+ shouldPersistState = true;
710
+ }
711
+ if (shouldPersistState || legacyVersion) {
712
+ setTTMGRC({
713
+ seenNoticeIds: Array.from(seenNoticeIds),
714
+ lastNotifiedCliVersion: undefined,
811
715
  });
812
- fs.writeFileSync(path.join(buildPath, MINIS_MANIFEST_FILE_NAME), JSON.stringify({ name: MINIS_MANIFEST_FILE_NAME, resource_list: resourceList }, null, 2));
813
716
  }
814
- catch (error) {
815
- console.error(chalk.red(`Error during debug process: ${error.message}`));
816
- if (error instanceof Error && error.stack) {
817
- console.error(chalk.red(`Stack trace: ${error.stack}`));
818
- }
819
- process.exit(1);
717
+ }
718
+
719
+ function ensureDirSync(dirPath) {
720
+ if (!fs.existsSync(dirPath)) {
721
+ fs.mkdirSync(dirPath, { recursive: true });
820
722
  }
821
723
  }
822
- function findOrCreateFolder(pathArray, currentFolder) {
724
+
725
+ const CONFIG_FILE_NAME = 'minigame.config.json';
726
+ const SDK_URL = 'https://connect.tiktok-minis.com/game/sdk.js';
727
+ const VCONSOLE_URL = 'https://connect.tiktok-minis.com/libs/vConsole.js';
728
+ const VCONSOLE_INIT = `
729
+ if(typeof VConsole === 'function') {
730
+ window.vConsole = new VConsole();
731
+ }
732
+ `;
733
+ const MINIS_MANIFEST_FILE_NAME = 'minis.manifest.json';
734
+ const MINIS_RUNTIME_URL = 'https://www.tiktok.com/minigames/runtime';
735
+
736
+ const { JSDOM } = jsdom;
737
+ const CONFIG_PATH = `${process.cwd()}/${CONFIG_FILE_NAME}`;
738
+ const INDEX_HTML_PATH = `${process.cwd()}/index.html`;
739
+ function isSandbox(clientKey) {
740
+ /**
741
+ * sb 开头的 clientKey 都是 sandbox 环境
742
+ */
743
+ return clientKey.startsWith('sb');
744
+ }
745
+ // 判断是否是 TTMinis.game.init 的初始化脚本
746
+ function isTTMinisInitScript(script, clientKey) {
747
+ if (!script.innerHTML) {
748
+ return false;
749
+ }
750
+ return (script.innerHTML.includes('TTMinis.game.init') &&
751
+ script.innerHTML.includes(clientKey));
752
+ }
753
+ async function injectScripts({ config, clientKey }) {
754
+ // 1. 检查 config 文件是否已存在
755
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
756
+ // 2. 读取 index.html
757
+ if (!fs.existsSync(INDEX_HTML_PATH)) {
758
+ console.error(t('h5.init.indexMissing'));
759
+ return;
760
+ }
761
+ const indexHtml = fs.readFileSync(INDEX_HTML_PATH, 'utf8');
762
+ const dom = new JSDOM(indexHtml);
763
+ const { document } = dom.window;
764
+ // 3. head 标签
765
+ let head = document.querySelector('head');
766
+ if (!head) {
767
+ head = document.createElement('head');
768
+ document.documentElement.insertBefore(head, document.body);
769
+ }
770
+ // 4. 检查是否已注入 SDK
771
+ const scriptList = Array.from(document.querySelectorAll('script'));
772
+ const hasSDK = scriptList.some(script => script.src && script.src.includes(SDK_URL));
773
+ // 5. 检查是否已注入 vConsole
774
+ const hasVConsole = scriptList.some(script => script.src && script.src.includes('vConsole.js'));
775
+ let lastInitScript = null;
776
+ if (!hasSDK) {
777
+ // 插入 SDK 脚本
778
+ const sdkScript = document.createElement('script');
779
+ sdkScript.src = SDK_URL;
780
+ head.insertBefore(sdkScript, head.firstChild);
781
+ // 插入 SDK init 脚本
782
+ const initScript = document.createElement('script');
783
+ initScript.innerHTML = `
784
+ window.TTMinis = TTMinis;
785
+ TTMinis.game.init({
786
+ clientKey: "${clientKey}",
787
+ });
788
+ `;
789
+ head.insertBefore(initScript, sdkScript.nextSibling);
790
+ lastInitScript = initScript;
791
+ }
792
+ else {
793
+ // 已经有 SDK,查找 TTMinis.game.init 脚本
794
+ const headScripts = Array.from(head.querySelectorAll('script'));
795
+ lastInitScript = headScripts.find(script => isTTMinisInitScript(script, clientKey));
796
+ if (lastInitScript) {
797
+ console.log(t('h5.init.sdkAlreadyInjected'));
798
+ }
799
+ else {
800
+ // 没有 TTMinis.game.init,则查找最后一个 SDK 脚本
801
+ const sdkScripts = headScripts.filter(script => script.src && script.src.includes(SDK_URL));
802
+ if (sdkScripts.length > 0) {
803
+ lastInitScript = sdkScripts[sdkScripts.length - 1];
804
+ }
805
+ }
806
+ }
807
+ /**
808
+ * 只有 Sandbox 环境才需要注入 vConsole
809
+ */
810
+ if (isSandbox(clientKey)) {
811
+ console.log(t('h5.init.sandboxSkipVconsole'));
812
+ // 8. 插入 vConsole 相关脚本(如果需要)
813
+ if (!hasVConsole) {
814
+ // vConsole 相关脚本的插入点
815
+ let insertAfterNode = lastInitScript;
816
+ if (insertAfterNode) {
817
+ insertAfterNode = insertAfterNode.nextSibling;
818
+ }
819
+ else {
820
+ insertAfterNode = head.firstChild;
821
+ }
822
+ // vConsole 源码
823
+ const vconsoleSourceScript = document.createElement('script');
824
+ vconsoleSourceScript.src = VCONSOLE_URL;
825
+ // vConsole 初始化
826
+ const vconsoleInitScript = document.createElement('script');
827
+ vconsoleInitScript.innerHTML = VCONSOLE_INIT;
828
+ head.insertBefore(vconsoleSourceScript, insertAfterNode);
829
+ head.insertBefore(vconsoleInitScript, insertAfterNode);
830
+ }
831
+ }
832
+ // 9. 格式化并写回 index.html
833
+ const formattedHtml = await prettier.format(dom.serialize(), {
834
+ parser: 'html',
835
+ });
836
+ fs.writeFileSync(INDEX_HTML_PATH, formattedHtml);
837
+ console.log(chalk.green.bold(t('h5.init.done')));
838
+ }
839
+
840
+ function init$1() {
841
+ const promptModule = inquirer.createPromptModule();
842
+ promptModule([
843
+ {
844
+ type: 'input',
845
+ name: 'clientKey',
846
+ message: t('h5.init.prompt.clientKey'),
847
+ },
848
+ {
849
+ type: 'input',
850
+ name: 'devPort',
851
+ message: t('h5.init.prompt.devPort'),
852
+ default: 9527,
853
+ },
854
+ ])
855
+ .then(async (answers) => {
856
+ const { clientKey, devPort } = answers;
857
+ const config = {
858
+ _comment: 'orientation is the orientation of the game. It can be either \'VERTICAL\' or \'HORIZONTAL\'.our game default is VERTICAL; minigame.config.json dev is a configuration file for minigame development. You can use it to configure the minigame.',
859
+ orientation: 'VERTICAL',
860
+ dev: {
861
+ port: devPort,
862
+ },
863
+ };
864
+ await injectScripts({ clientKey, config });
865
+ process.exit(0);
866
+ })
867
+ .catch(() => {
868
+ process.exit(1);
869
+ });
870
+ }
871
+
872
+ const app = express();
873
+ // 1. 检查配置文件是否存在
874
+ function dev$1() {
875
+ const configPath = path.join(process.cwd(), CONFIG_FILE_NAME);
876
+ if (!fs.existsSync(configPath)) {
877
+ console.log(chalk.red.bold(t('h5.dev.configMissing', { file: CONFIG_FILE_NAME })));
878
+ return;
879
+ }
880
+ // 2. 读取配置
881
+ const gameConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
882
+ const devPort = gameConfig.dev?.port || 9527;
883
+ const hasOpenContext = !!gameConfig.openDataContext;
884
+ if (hasOpenContext) {
885
+ generateOpenContextHtml(gameConfig.openDataContext, gameConfig.app_id);
886
+ }
887
+ // 3. 打印开发前提示
888
+ console.log(chalk.yellow.bold(t('h5.dev.precheckTips')));
889
+ console.log(chalk.bold.blue(t('h5.dev.startingBanner')));
890
+ /**
891
+ * 支持 .br 文件, 支持 gzip
892
+ */
893
+ app.use((req, res, next) => {
894
+ if (req.url.endsWith('.br')) {
895
+ res.setHeader('Content-Encoding', 'br');
896
+ }
897
+ else if (req.url.endsWith('.gz')) {
898
+ res.setHeader('Content-Encoding', 'gzip');
899
+ }
900
+ next();
901
+ });
902
+ /**
903
+ * 给所有的请求返回设置 CSP
904
+ */
905
+ app.use((req, res, next) => {
906
+ /**
907
+ * 计算 HTML 中的内联脚本生成 hash 插入 CSP 中
908
+ */
909
+ try {
910
+ // 1. 读取 HTML 文件内容
911
+ const htmlPath = path.join(process.cwd(), 'index.html');
912
+ const html = fs.readFileSync(htmlPath, 'utf8');
913
+ // 2. 用 cheerio 解析 HTML
914
+ const $ = cheerio.load(html);
915
+ // 3. 提取所有无 src 属性的内联 <script> 内容
916
+ const scripts = [];
917
+ $('script:not([src])').each((i, elem) => {
918
+ const content = $(elem).html();
919
+ if (content && content.trim()) {
920
+ scripts.push(content);
921
+ }
922
+ });
923
+ // 4. 计算每段脚本的 SHA-256 hash 并 base64 编码
924
+ // const hashes = scripts.map(script => {
925
+ // const hash = crypto
926
+ // .createHash('sha256')
927
+ // .update(script, 'utf8')
928
+ // .digest('base64');
929
+ // return `'sha256-${hash}'`;
930
+ // });
931
+ // 开发者本地调试,信任的域名默认为 * 便于调试
932
+ const devTrustedDomain = '*';
933
+ res.setHeader('Content-Security-Policy', `default-src 'self';script-src 'self' data: blob: 'unsafe-eval' 'unsafe-inline' connect.tiktok-minis.com sf-connect.tiktokminis.us;img-src 'self' ${devTrustedDomain} data: blob: *; connect-src 'self' ${devTrustedDomain} data: blob: ; style-src 'self' ${devTrustedDomain} 'unsafe-inline' fonts.googleapis.com data: blob: *; font-src 'self' fonts.gstatic.com blob: data: *; media-src 'self' ${devTrustedDomain} data: blob: *; frame-src 'none'; base-uri 'self'; worker-src 'self' blob: data: ;`);
934
+ }
935
+ catch (e) {
936
+ // 如果 index.html 不存在或有异常,CSP 头就不设置
937
+ console.warn(chalk.red(t('h5.dev.cspFailed')), e.message);
938
+ }
939
+ next();
940
+ });
941
+ // 4. 静态资源服务
942
+ app.use(express.static(path.join(process.cwd())));
943
+ // 5. 启动服务并自动打开浏览器
944
+ app.listen(devPort, () => {
945
+ const gameUrl = `http://localhost:${devPort}`;
946
+ const openContextUrl = `http://localhost:${devPort}/open_context.html`;
947
+ let devUrl = `${MINIS_RUNTIME_URL}?minis_url=${gameUrl}&enable_log=1`;
948
+ if (hasOpenContext) {
949
+ devUrl += `&open_context_url=${openContextUrl}`;
950
+ }
951
+ console.log(t('h5.dev.accessUrl', {
952
+ url: String(chalk.green.underline.bold(devUrl)),
953
+ }));
954
+ try {
955
+ // 自动打开浏览器,跨平台
956
+ openUrl(devUrl);
957
+ }
958
+ catch (e) {
959
+ console.warn(chalk.red(t('h5.dev.openBrowserFailed')), e.message);
960
+ }
961
+ });
962
+ }
963
+
964
+ async function buildMinisManifest() {
965
+ try {
966
+ const buildPath = path.join(process.cwd());
967
+ const resourceList = [];
968
+ const allFiles = collectAllFiles(buildPath);
969
+ Object.keys(allFiles)
970
+ .filter(file => !file.endsWith('.map'))
971
+ .forEach(file => {
972
+ const relativeFilePath = allFiles[file];
973
+ const filePathArr = relativeFilePath.split('/').filter(Boolean); // Split filename
974
+ const fileName = filePathArr.pop() || ''; // Get filename
975
+ if (filePathArr.length === 0) {
976
+ resourceList.push({ type: 'file', name: fileName });
977
+ }
978
+ else {
979
+ const folder = findOrCreateFolder(filePathArr, resourceList);
980
+ folder.children.push({ type: 'file', name: fileName });
981
+ }
982
+ });
983
+ fs.writeFileSync(path.join(buildPath, MINIS_MANIFEST_FILE_NAME), JSON.stringify({ name: MINIS_MANIFEST_FILE_NAME, resource_list: resourceList }, null, 2));
984
+ }
985
+ catch (error) {
986
+ console.error(chalk.red(`Error during debug process: ${error.message}`));
987
+ if (error instanceof Error && error.stack) {
988
+ console.error(chalk.red(`Stack trace: ${error.stack}`));
989
+ }
990
+ process.exit(1);
991
+ }
992
+ }
993
+ function findOrCreateFolder(pathArray, currentFolder) {
823
994
  let folder = currentFolder.find(item => item.type === 'folder' && item.name === pathArray[0]);
824
995
  if (!folder) {
825
996
  folder = { type: 'folder', name: pathArray[0], children: [] };
@@ -854,7 +1025,7 @@ async function build() {
854
1025
  try {
855
1026
  const configPath = path.join(process.cwd(), CONFIG_FILE_NAME);
856
1027
  if (!fs.existsSync(configPath)) {
857
- console.log(chalk.red.bold(`${CONFIG_FILE_NAME} is not exist, please run minis game init first`));
1028
+ console.log(chalk.red.bold(t('h5.bundle.configMissing', { file: CONFIG_FILE_NAME })));
858
1029
  return;
859
1030
  }
860
1031
  const gameConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
@@ -869,11 +1040,11 @@ async function build() {
869
1040
  type: 'input',
870
1041
  name: 'zipName',
871
1042
  default: 'game',
872
- message: 'Please input zip name',
1043
+ message: t('h5.bundle.prompt.zipName'),
873
1044
  });
874
1045
  zName = zipNameInput;
875
1046
  const startTime = Date.now();
876
- console.log(chalk.bold.blue('start build your game, it will take a few minutes...'));
1047
+ console.log(chalk.bold.blue(t('h5.bundle.start')));
877
1048
  /**
878
1049
  * 移除掉当前根目录下的文件包
879
1050
  */
@@ -898,11 +1069,17 @@ async function build() {
898
1069
  await archive.pipe(fs.createWriteStream(zipPath));
899
1070
  await archive.directory(path.resolve(process.cwd()), false);
900
1071
  await archive.finalize();
901
- console.log(chalk.yellow.bold(`build ${zipNameInput}.zip success, you can find it in desktop, use time ${Date.now() - startTime} ms`));
1072
+ console.log(chalk.yellow.bold(t('h5.bundle.success', {
1073
+ zipName: zipNameInput,
1074
+ cost: Date.now() - startTime,
1075
+ })));
902
1076
  process.exit(0);
903
1077
  }
904
1078
  catch (error) {
905
- console.log(chalk.red(`auto build ${zName}.zip failed: ${error.message}, you should zip it manually`));
1079
+ console.log(chalk.red(t('h5.bundle.fail', {
1080
+ zipName: zName,
1081
+ error: error.message,
1082
+ })));
906
1083
  process.exit(1);
907
1084
  }
908
1085
  }
@@ -935,7 +1112,7 @@ function getDesktopPath() {
935
1112
  // 这是一种安全的默认行为,因为即使文件夹不存在,程序后续创建文件时
936
1113
  // 也可以选择自动创建这个目录。
937
1114
  const defaultPath = path.join(homeDir, 'Desktop');
938
- console.log(`未找到特定桌面文件夹,使用默认路径: ${defaultPath}`);
1115
+ console.log(t('h5.bundle.desktopFallback', { path: defaultPath }));
939
1116
  return defaultPath;
940
1117
  }
941
1118
 
@@ -1076,6 +1253,7 @@ const store = Store.getInstance({
1076
1253
  nodeWsPort: DEV_WS_PORT,
1077
1254
  packages: {},
1078
1255
  isUnderCompiling: false,
1256
+ isUnderUploading: false,
1079
1257
  isWaitingForUpload: false,
1080
1258
  projectInfo: {
1081
1259
  projectSize: 0,
@@ -1276,10 +1454,6 @@ function getOutputDir() {
1276
1454
  }
1277
1455
 
1278
1456
  async function compile(context) {
1279
- // const { openDataContext } = getOpenContextConfig();
1280
- // if (!!openDataContext) {
1281
- // buildOpenContextToFile(openDataContext);
1282
- // }
1283
1457
  const entryDir = process.cwd();
1284
1458
  const outputDir = getOutputDir();
1285
1459
  const { clientKey, msg } = getClientKey();
@@ -1291,8 +1465,8 @@ async function compile(context) {
1291
1465
  console.log(chalk.red.bold(msg));
1292
1466
  }
1293
1467
  const startTip = context?.mode === 'watch'
1294
- ? '🔔 Watching game assets for local debugging...'
1295
- : '🚀 Compiling game assets for local debugging...';
1468
+ ? t('native.compile.watching')
1469
+ : t('native.compile.compiling');
1296
1470
  console.log(chalk.bold.cyan(startTip));
1297
1471
  wsServer.sendCompilationStatus('start');
1298
1472
  store.setState({
@@ -1329,7 +1503,7 @@ async function compile(context) {
1329
1503
  wsServer.sendCompilationStatus('end', {
1330
1504
  isSuccess: true,
1331
1505
  });
1332
- console.log(chalk.green.bold('✔ Game resources compiled successfully!'));
1506
+ console.log(chalk.green.bold(t('native.compile.success')));
1333
1507
  resolve(msg); // 编译结束,返回结果
1334
1508
  }
1335
1509
  worker.terminate();
@@ -1351,6 +1525,7 @@ async function compile(context) {
1351
1525
  outputDir,
1352
1526
  devPort: store.getState().nodeServerPort,
1353
1527
  entryDir,
1528
+ lang: getCurrentLanguage(),
1354
1529
  },
1355
1530
  });
1356
1531
  });
@@ -1390,22 +1565,35 @@ async function watch() {
1390
1565
  });
1391
1566
  }
1392
1567
 
1393
- let spinner;
1568
+ let spinner$1;
1394
1569
  async function uploadGame(callback) {
1395
1570
  const ora = await import('ora');
1396
- spinner = ora.default({
1397
- text: chalk.cyan.bold('Uploading game assets to client...'),
1571
+ spinner$1 = ora.default({
1572
+ text: chalk.cyan.bold(t('native.upload.spinner.start')),
1398
1573
  spinner: 'dots',
1399
1574
  });
1400
- spinner.start();
1575
+ spinner$1.start();
1401
1576
  const outputDir = getOutputDir();
1402
1577
  callback({
1403
1578
  status: 'start',
1404
1579
  percent: 0,
1405
1580
  });
1406
1581
  const zipPath = path.join(os.homedir(), '__TTMG__', 'upload.zip');
1407
- await zipDirectory(outputDir, zipPath);
1408
- await uploadZip(zipPath, callback);
1582
+ try {
1583
+ await zipDirectory(outputDir, zipPath);
1584
+ await uploadZip(zipPath, callback);
1585
+ }
1586
+ catch (err) {
1587
+ const errorMsg = err instanceof Error ? err.message : String(err);
1588
+ if (spinner$1?.isSpinning) {
1589
+ spinner$1.fail(chalk.red.bold(t('native.upload.fail', { error: errorMsg })));
1590
+ }
1591
+ callback({
1592
+ status: 'error',
1593
+ percent: 0,
1594
+ msg: errorMsg,
1595
+ });
1596
+ }
1409
1597
  }
1410
1598
  /**
1411
1599
  * 复制源目录内容到临时目录,然后根据 glob 模式过滤并压缩文件。
@@ -1449,7 +1637,7 @@ async function zipDirectory(sourceDir, outPath) {
1449
1637
  await archivePromise;
1450
1638
  }
1451
1639
  catch (err) {
1452
- console.error('压缩过程中发生错误:', err);
1640
+ console.error(t('native.upload.compressError'), err);
1453
1641
  throw err;
1454
1642
  }
1455
1643
  finally {
@@ -1466,63 +1654,65 @@ async function uploadZip(zipPath, callback) {
1466
1654
  });
1467
1655
  // 帮我计算下文件大小,变成 MB 为单位
1468
1656
  const fileSize = fs.statSync(zipPath).size / 1024 / 1024;
1469
- spinner.text = chalk.cyan.bold(`Start upload Game assets to client, size: ${fileSize.toFixed(2)} MB`);
1657
+ spinner$1.text = chalk.cyan.bold(t('native.upload.spinner.size', { size: fileSize.toFixed(2) }));
1470
1658
  const { clientHttpPort, clientHost } = store.getState();
1471
1659
  const url = `http://${clientHost}:${clientHttpPort}/game/upload`;
1472
1660
  try {
1473
1661
  // 1. 创建请求流
1474
- const stream = got.stream.post(url, {
1475
- body: form,
1476
- });
1477
- const handleProgress = progress => {
1478
- const percent = progress.percent;
1479
- // const transferred = progress.transferred;
1480
- // const total = progress.total;
1481
- spinner.text = chalk.cyan.bold(`Uploading game assets to client... ${(percent * 100).toFixed(0)}%`);
1482
- callback({
1483
- status: 'process',
1484
- percent,
1662
+ await new Promise(resolve => {
1663
+ const stream = got.stream.post(url, {
1664
+ body: form,
1485
1665
  });
1486
- };
1487
- stream.on('uploadProgress', handleProgress);
1488
- const chunks = [];
1489
- // 当流传输数据时,收集数据块
1490
- stream.on('data', chunk => {
1491
- chunks.push(chunk);
1492
- });
1493
- // 当流成功结束时
1494
- stream.on('end', () => {
1495
- spinner.succeed(chalk.green.bold(`Upload game assets to client success! Cost: ${Date.now() - startTime}ms`));
1496
- callback({
1497
- status: 'success',
1498
- percent: 1,
1666
+ const handleProgress = progress => {
1667
+ const percent = progress.percent;
1668
+ spinner$1.text = chalk.cyan.bold(t('native.upload.spinner.progress', {
1669
+ percent: (percent * 100).toFixed(0),
1670
+ }));
1671
+ callback({
1672
+ status: 'process',
1673
+ percent,
1674
+ });
1675
+ };
1676
+ const cleanup = () => {
1677
+ stream.off('uploadProgress', handleProgress);
1678
+ };
1679
+ stream.on('uploadProgress', handleProgress);
1680
+ // Consume response body to ensure 'end' is emitted.
1681
+ stream.on('data', () => { });
1682
+ // 当流成功结束时
1683
+ stream.on('end', () => {
1684
+ cleanup();
1685
+ spinner$1.succeed(chalk.green.bold(t('native.upload.success', { cost: Date.now() - startTime })));
1686
+ callback({
1687
+ status: 'success',
1688
+ percent: 1,
1689
+ });
1690
+ resolve();
1499
1691
  });
1500
- });
1501
- // 当流发生错误时
1502
- stream.on('error', err => {
1503
- stream.off('uploadProgress', handleProgress);
1504
- spinner.fail(chalk.red.bold(`Upload failed with error: ${err.message}, please check current debug env and try to scan qrcode to reupload again.`));
1505
- callback({
1506
- status: 'error',
1507
- percent: 0,
1508
- msg: err.message,
1692
+ // 当流发生错误时
1693
+ stream.on('error', err => {
1694
+ cleanup();
1695
+ spinner$1.fail(chalk.red.bold(t('native.upload.fail', { error: err.message })));
1696
+ callback({
1697
+ status: 'error',
1698
+ percent: 0,
1699
+ msg: err.message,
1700
+ });
1701
+ resolve();
1509
1702
  });
1510
1703
  });
1511
1704
  }
1512
1705
  catch (err) {
1513
- callback({
1514
- status: 'error',
1515
- percent: 0,
1516
- msg: err?.message,
1517
- });
1518
- process.stdout.write('\n');
1519
- console.log('\n');
1520
- console.error(chalk.red.bold('✖ Upload failed with server error, please scan qrcode to reupload'));
1706
+ throw err;
1521
1707
  }
1522
1708
  }
1523
1709
 
1524
- function listen() {
1525
- eventEmitter.on('startUpload', () => {
1710
+ let hasBoundUploadListeners = false;
1711
+ function listen$1() {
1712
+ if (hasBoundUploadListeners)
1713
+ return;
1714
+ hasBoundUploadListeners = true;
1715
+ eventEmitter.on('startUpload', async () => {
1526
1716
  /**
1527
1717
  * 如果还在编译中,需要等到编译结束再上传
1528
1718
  */
@@ -1532,30 +1722,53 @@ function listen() {
1532
1722
  });
1533
1723
  return;
1534
1724
  }
1535
- wsServer.sendUploadStatus('start');
1536
- uploadGame(({ status, percent, msg }) => {
1537
- if (status === 'process') {
1538
- wsServer.sendUploadStatus('process', {
1539
- status: 'process',
1540
- progress: percent,
1541
- });
1542
- }
1543
- else if (status === 'error') {
1544
- wsServer.sendUploadStatus('error', {
1545
- status: 'error',
1546
- errMsg: msg,
1547
- isSuccess: false,
1548
- });
1549
- }
1550
- else if (status === 'success') {
1551
- wsServer.sendUploadStatus('success', {
1552
- status: 'success',
1553
- packages: store.getState().packages,
1554
- clientKey: getClientKey().clientKey,
1555
- isSuccess: true,
1556
- });
1557
- }
1725
+ /**
1726
+ * 避免重复触发导致并发上传(会触发 ora spinner 警告)
1727
+ */
1728
+ if (store.getState().isUnderUploading) {
1729
+ return;
1730
+ }
1731
+ store.setState({
1732
+ isUnderUploading: true,
1558
1733
  });
1734
+ wsServer.sendUploadStatus('start');
1735
+ try {
1736
+ await uploadGame(({ status, percent, msg }) => {
1737
+ if (status === 'process') {
1738
+ wsServer.sendUploadStatus('process', {
1739
+ status: 'process',
1740
+ progress: percent,
1741
+ });
1742
+ }
1743
+ else if (status === 'error') {
1744
+ wsServer.sendUploadStatus('error', {
1745
+ status: 'error',
1746
+ errMsg: msg,
1747
+ isSuccess: false,
1748
+ });
1749
+ }
1750
+ else if (status === 'success') {
1751
+ wsServer.sendUploadStatus('success', {
1752
+ status: 'success',
1753
+ packages: store.getState().packages,
1754
+ clientKey: getClientKey().clientKey,
1755
+ isSuccess: true,
1756
+ });
1757
+ }
1758
+ });
1759
+ }
1760
+ catch (error) {
1761
+ wsServer.sendUploadStatus('error', {
1762
+ status: 'error',
1763
+ errMsg: error instanceof Error ? error.message : String(error),
1764
+ isSuccess: false,
1765
+ });
1766
+ }
1767
+ finally {
1768
+ store.setState({
1769
+ isUnderUploading: false,
1770
+ });
1771
+ }
1559
1772
  });
1560
1773
  eventEmitter.on('compileSuccess', () => {
1561
1774
  /**
@@ -1712,6 +1925,20 @@ function getLocalIPs() {
1712
1925
  }
1713
1926
  }
1714
1927
 
1928
+ function getLocalIP() {
1929
+ const networkInterfaces = os.networkInterfaces();
1930
+ for (const interfaceName in networkInterfaces) {
1931
+ const interfaceInfo = networkInterfaces[interfaceName];
1932
+ if (!interfaceInfo)
1933
+ continue;
1934
+ for (const addressInfo of interfaceInfo) {
1935
+ if (addressInfo.family === 'IPv4' && !addressInfo.internal) {
1936
+ return addressInfo.address;
1937
+ }
1938
+ }
1939
+ }
1940
+ }
1941
+
1715
1942
  async function init() {
1716
1943
  const promptModule = inquirer.createPromptModule();
1717
1944
  const { clientKey: lastUsedClientKey } = getTTMGRC() || {};
@@ -1720,12 +1947,12 @@ async function init() {
1720
1947
  {
1721
1948
  type: 'list',
1722
1949
  name: 'selectedClientKey',
1723
- message: 'Select game client key for debugging:',
1950
+ message: t('native.init.selectClientKey'),
1724
1951
  choices: [{
1725
- name: `${lastUsedClientKey} (last used)`,
1952
+ name: t('native.init.lastUsed', { clientKey: lastUsedClientKey }),
1726
1953
  value: lastUsedClientKey,
1727
1954
  }, {
1728
- name: 'Add a new client key',
1955
+ name: t('native.init.addNew'),
1729
1956
  value: 'new',
1730
1957
  }],
1731
1958
  },
@@ -1738,10 +1965,10 @@ async function init() {
1738
1965
  {
1739
1966
  type: 'input',
1740
1967
  name: 'clientKey',
1741
- message: 'Input new client key:',
1968
+ message: t('native.init.inputNew'),
1742
1969
  validate: input => {
1743
1970
  if (!input) {
1744
- return 'Client key is required, please input client key';
1971
+ return t('native.init.clientKeyRequired');
1745
1972
  }
1746
1973
  return true;
1747
1974
  },
@@ -1764,10 +1991,10 @@ async function init() {
1764
1991
  {
1765
1992
  type: 'input',
1766
1993
  name: 'clientKey',
1767
- message: 'Input your Client Key:',
1994
+ message: t('native.init.inputYour'),
1768
1995
  validate: input => {
1769
1996
  if (!input) {
1770
- return 'Client key is required, please input client key';
1997
+ return t('native.init.clientKeyRequired');
1771
1998
  }
1772
1999
  return true;
1773
2000
  },
@@ -1791,19 +2018,23 @@ function getDevToolVersion() {
1791
2018
  function showTips(context) {
1792
2019
  console.log(chalk.gray('─────────────────────────────────────────────'));
1793
2020
  console.log(chalk.yellow.bold('1.') +
1794
- ' The QR code page will be opened automatically in Chrome.');
1795
- console.log(' If it fails, please open the following link manually:');
2021
+ t('native.tips.1'));
2022
+ console.log(t('native.tips.1.sub'));
1796
2023
  console.log(' ' + chalk.cyan.underline(context.server));
1797
2024
  console.log('');
1798
2025
  console.log(chalk.yellow.bold('2.') +
1799
- ' The debugging service will automatically compile your game assets.');
1800
- console.log(' Any changes in your game directory will trigger recompilation.');
1801
- console.log(' You can debug the updated content when upload is completed.');
2026
+ t('native.tips.2'));
2027
+ console.log(t('native.tips.2.sub1'));
2028
+ console.log(t('native.tips.2.sub2'));
1802
2029
  console.log('');
1803
2030
  console.log(chalk.yellow.bold('3.') +
1804
- ' After scanning the QR code with your phone for Test User authentication,');
1805
- console.log(' the compiled code package will be uploaded to the client automatically.');
1806
- console.log(' Game debugging will start right away.');
2031
+ t('native.tips.3'));
2032
+ console.log(t('native.tips.3.sub1'));
2033
+ console.log(t('native.tips.3.sub2'));
2034
+ console.log('');
2035
+ console.log(chalk.red.bold('4. ⚠️') +
2036
+ t('native.tips.4'));
2037
+ console.log(t('native.tips.4.sub1'));
1807
2038
  console.log(chalk.gray('─────────────────────────────────────────────'));
1808
2039
  }
1809
2040
 
@@ -1887,7 +2118,7 @@ async function check() {
1887
2118
  // process.exit(1);
1888
2119
  // }
1889
2120
  if (!isWorkspace()) {
1890
- printBox('Error', `Current directory is not a Mini Game project entry directory, please enter the game project root directory for debugging`);
2121
+ printBox('Error', t('native.check.notWorkspace'));
1891
2122
  process.exit(1);
1892
2123
  }
1893
2124
  // const checkResult = await checkPkgs({
@@ -1937,6 +2168,236 @@ async function check() {
1937
2168
  // }
1938
2169
  }
1939
2170
 
2171
+ const devToolVersion$1 = getDevToolVersion();
2172
+ async function listen(app, options) {
2173
+ const maxRetries = options?.maxRetries;
2174
+ const version = devToolVersion$1;
2175
+ const server = http.createServer(app);
2176
+ function tryListen(port) {
2177
+ return new Promise(resolve => {
2178
+ const onError = err => {
2179
+ server.removeListener('listening', onListening);
2180
+ if (err.code === 'EADDRINUSE') {
2181
+ console.log(chalk(t('native.server.portInUse', {
2182
+ port,
2183
+ next: port + 1,
2184
+ })));
2185
+ resolve(false);
2186
+ }
2187
+ else {
2188
+ console.log(chalk.red.bold(err.message));
2189
+ process.exit(1);
2190
+ }
2191
+ };
2192
+ const onListening = () => {
2193
+ server.removeListener('error', onError);
2194
+ resolve(true);
2195
+ };
2196
+ server.once('error', onError);
2197
+ server.once('listening', onListening);
2198
+ server.listen(port);
2199
+ });
2200
+ }
2201
+ let isListening = false;
2202
+ for (let i = 0; i < maxRetries; i++) {
2203
+ const currentPort = store.getState().nodeServerPort;
2204
+ isListening = await tryListen(currentPort);
2205
+ if (isListening) {
2206
+ break;
2207
+ }
2208
+ store.setState({ nodeServerPort: currentPort + 1 });
2209
+ }
2210
+ if (!isListening) {
2211
+ console.log(chalk.red.bold(t('native.server.failedAfterRetries', { retries: maxRetries })));
2212
+ process.exit(1);
2213
+ }
2214
+ // @ts-ignore
2215
+ const port = server.address().port;
2216
+ const url = `http://localhost:${port}?v=${encodeURIComponent(version)}`;
2217
+ return { server, port, url, version };
2218
+ }
2219
+
2220
+ function setupMiddlewares(app, options) {
2221
+ const { publicPath, outputDir } = options;
2222
+ app.use(fileUpload());
2223
+ app.use(expressStaticGzip(publicPath, {
2224
+ enableBrotli: true,
2225
+ orderPreference: ['br'],
2226
+ }));
2227
+ app.use((req, res, next) => {
2228
+ res.header('Access-Control-Allow-Origin', '*');
2229
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
2230
+ res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
2231
+ res.header('Pragma', 'no-cache');
2232
+ res.header('Expires', '0');
2233
+ res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
2234
+ next();
2235
+ });
2236
+ app.use(express.json());
2237
+ app.use(express.urlencoded({ extended: true }));
2238
+ app.use('/game/files', express.static(outputDir));
2239
+ }
2240
+
2241
+ const successCode = 0;
2242
+ const errorCode = -1;
2243
+
2244
+ function buildCheckPkgsOptions(outputDir) {
2245
+ return {
2246
+ entry: process.cwd(),
2247
+ output: outputDir,
2248
+ dev: {
2249
+ enable: true,
2250
+ port: store.getState().nodeServerPort,
2251
+ host: 'localhost',
2252
+ enableSourcemap: false,
2253
+ enableLog: false,
2254
+ },
2255
+ build: {
2256
+ enableOdr: false,
2257
+ enableAPICheck: true,
2258
+ ...PKG_SIZE_LIMIT,
2259
+ },
2260
+ };
2261
+ }
2262
+
2263
+ const outputDir$2 = getOutputDir();
2264
+ const devToolVersion = getDevToolVersion();
2265
+ const gameConfigRoute = {
2266
+ method: 'get',
2267
+ path: '/game/config',
2268
+ handler: async (req, res) => {
2269
+ const [basic, checkResult] = await Promise.all([
2270
+ ttmgPack.getPkgs({ entryDir: process.cwd() }),
2271
+ ttmgPack.checkPkgs(buildCheckPkgsOptions(outputDir$2), {
2272
+ lang: getCurrentLanguage(),
2273
+ }),
2274
+ ]);
2275
+ const user = getCurrentUser();
2276
+ const { clientKey } = getClientKey();
2277
+ const localLang = getConfiguredLanguage();
2278
+ res.send({
2279
+ error: null,
2280
+ data: {
2281
+ user,
2282
+ code: successCode,
2283
+ nodeWsPort: store.getState().nodeWsPort,
2284
+ clientKey,
2285
+ schema: `https://www.tiktok.com/ttmg/dev/${clientKey}?host=${getLocalIP()}&port=${store.getState().nodeWsPort}&host_list=${encodeURIComponent(JSON.stringify(getLocalIPs()))}`,
2286
+ ...basic,
2287
+ devToolVersion,
2288
+ checkResult,
2289
+ lang: localLang,
2290
+ },
2291
+ });
2292
+ },
2293
+ };
2294
+
2295
+ const gameConfigFillbackRoute = {
2296
+ method: 'post',
2297
+ path: '/game/config-fillback',
2298
+ handler: async (req, res) => {
2299
+ const configuredLang = getConfiguredLanguage();
2300
+ if (configuredLang) {
2301
+ res.send({
2302
+ code: successCode,
2303
+ data: {
2304
+ lang: configuredLang,
2305
+ updated: false,
2306
+ },
2307
+ });
2308
+ return;
2309
+ }
2310
+ const incomingLang = req.body?.lang;
2311
+ const fallbackLang = resolveSupportedLanguage(incomingLang);
2312
+ if (!fallbackLang) {
2313
+ res.send({
2314
+ code: successCode,
2315
+ data: {
2316
+ lang: null,
2317
+ updated: false,
2318
+ },
2319
+ });
2320
+ return;
2321
+ }
2322
+ setTTMGRC({ lang: fallbackLang });
2323
+ res.send({
2324
+ code: successCode,
2325
+ data: {
2326
+ lang: fallbackLang,
2327
+ updated: true,
2328
+ },
2329
+ });
2330
+ },
2331
+ };
2332
+
2333
+ const outputDir$1 = getOutputDir();
2334
+ const gameCheckRoute = {
2335
+ method: 'get',
2336
+ path: '/game/check',
2337
+ handler: async (req, res) => {
2338
+ const checkResult = await ttmgPack.checkPkgs(buildCheckPkgsOptions(outputDir$1), {
2339
+ lang: getCurrentLanguage(),
2340
+ });
2341
+ res.send({ code: successCode, data: checkResult });
2342
+ },
2343
+ };
2344
+
2345
+ const gameDetailRoute = {
2346
+ method: 'get',
2347
+ path: '/game/detail',
2348
+ handler: async (req, res) => {
2349
+ const basic = await ttmgPack.getPkgs({ entryDir: process.cwd() });
2350
+ const { clientKey } = getClientKey();
2351
+ const { error, data: gameInfo } = await fetchGameInfo(clientKey);
2352
+ store.setState({
2353
+ appId: gameInfo?.app_id,
2354
+ });
2355
+ if (error) {
2356
+ res.send({ error, data: null });
2357
+ return;
2358
+ }
2359
+ res.send({ error: null, data: { ...basic, ...gameInfo } });
2360
+ },
2361
+ };
2362
+
2363
+ const gameUploadRoute = {
2364
+ method: 'post',
2365
+ path: '/game/upload',
2366
+ handler: async (req, res) => {
2367
+ try {
2368
+ console.log(t('native.server.upload.packingDir', { cwd: process.cwd() }));
2369
+ const gameZipBuffer = await zipCwdToBuffer();
2370
+ console.log(t('native.server.upload.packed', {
2371
+ size: (gameZipBuffer.length / 1024 / 1024).toFixed(2),
2372
+ }));
2373
+ const desc = req.headers['ttmg-game-desc'];
2374
+ const decodedDesc = decodeURIComponent(desc || '--');
2375
+ const { data, error } = await uploadGameToPlatform({
2376
+ // @ts-ignore
2377
+ data: gameZipBuffer,
2378
+ name: 'game.zip',
2379
+ clientKey: getClientKey().clientKey,
2380
+ note: decodedDesc,
2381
+ appId: store.getState().appId,
2382
+ });
2383
+ if (error) {
2384
+ res.send({ code: errorCode, error });
2385
+ }
2386
+ else {
2387
+ res.send({ code: successCode, data });
2388
+ }
2389
+ }
2390
+ catch (error) {
2391
+ let errorMessage = t('common.unknownError');
2392
+ if (error instanceof Error) {
2393
+ errorMessage = error.message;
2394
+ }
2395
+ console.error(t('native.server.upload.failed', { error: errorMessage }));
2396
+ res.status(500).send({ code: errorCode, data: errorMessage });
2397
+ }
2398
+ },
2399
+ };
2400
+
1940
2401
  const BASE_URL = 'https://developers.tiktok.com';
1941
2402
  const DEV_HEADERS = {
1942
2403
  // 'x-use-ppe': '1',
@@ -2825,212 +3286,56 @@ const getTaskInfo = async (params) => {
2825
3286
  });
2826
3287
  };
2827
3288
 
2828
- const successCode = 0;
2829
- const errorCode = -1;
2830
- const outputDir = getOutputDir();
2831
- const publicPath = path.join(__dirname, 'public');
2832
- const devToolVersion = getDevToolVersion();
2833
- async function start() {
2834
- const startTime = Date.now();
2835
- const app = express();
2836
- app.use(fileUpload()); // 启用 express-fileupload 中间件
2837
- // --- 中间件和路由设置 ---
2838
- app.use(expressStaticGzip(publicPath, {
2839
- enableBrotli: true,
2840
- orderPreference: ['br'],
2841
- }));
2842
- app.use((req, res, next) => {
2843
- res.header('Access-Control-Allow-Origin', '*');
2844
- res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
2845
- res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
2846
- res.header('Pragma', 'no-cache');
2847
- res.header('Expires', '0');
2848
- res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
2849
- next();
2850
- });
2851
- app.use(express.json());
2852
- app.use(express.urlencoded({ extended: true }));
2853
- app.use('/game/files', express.static(outputDir));
2854
- app.get('/game/config', async (req, res) => {
2855
- const [basic, checkResult] = await Promise.all([
2856
- ttmgPack.getPkgs({ entryDir: process.cwd() }),
2857
- ttmgPack.checkPkgs({
2858
- entry: process.cwd(),
2859
- output: outputDir,
2860
- dev: {
2861
- enable: true,
2862
- port: store.getState().nodeServerPort,
2863
- host: 'localhost',
2864
- enableSourcemap: false,
2865
- enableLog: false,
2866
- },
2867
- build: {
2868
- enableOdr: false,
2869
- enableAPICheck: true,
2870
- ...PKG_SIZE_LIMIT,
2871
- },
2872
- }),
2873
- ]);
2874
- const user = getCurrentUser();
2875
- const { clientKey } = getClientKey();
2876
- res.send({
2877
- error: null,
2878
- data: {
2879
- user,
2880
- code: successCode,
2881
- nodeWsPort: store.getState().nodeWsPort,
2882
- clientKey: clientKey,
2883
- schema: `https://www.tiktok.com/ttmg/dev/${clientKey}?host=${getLocalIP()}&port=${store.getState().nodeWsPort}&host_list=${encodeURIComponent(JSON.stringify(getLocalIPs()))}`,
2884
- ...basic,
2885
- devToolVersion,
2886
- checkResult,
2887
- },
2888
- });
2889
- });
2890
- app.get('/game/detail', async (req, res) => {
2891
- const basic = await ttmgPack.getPkgs({ entryDir: process.cwd() });
2892
- const { clientKey } = getClientKey();
2893
- const { error, data: gameInfo } = await fetchGameInfo(clientKey);
2894
- store.setState({
2895
- appId: gameInfo?.app_id,
2896
- });
2897
- if (error) {
2898
- res.send({ error, data: null });
2899
- return;
2900
- }
2901
- else {
2902
- res.send({ error: null, data: { ...basic, ...gameInfo } });
2903
- }
2904
- });
2905
- app.get('/game/check', async (req, res) => {
2906
- const checkResult = await ttmgPack.checkPkgs({
2907
- entry: process.cwd(),
2908
- output: outputDir,
2909
- dev: {
2910
- enable: true,
2911
- port: store.getState().nodeServerPort,
2912
- host: 'localhost',
2913
- enableSourcemap: false,
2914
- enableLog: false,
2915
- },
2916
- build: {
2917
- enableOdr: false,
2918
- enableAPICheck: true,
2919
- ...PKG_SIZE_LIMIT,
2920
- },
3289
+ const gameWasmCancelRoute = {
3290
+ method: 'post',
3291
+ path: '/game/wasm-cancel',
3292
+ handler: async (req, res) => {
3293
+ const { codePath } = req.body;
3294
+ console.log('wasm-cancel', req.body);
3295
+ await cancelSplit({
3296
+ wasmCodePath: codePath,
2921
3297
  });
2922
- res.send({ code: successCode, data: checkResult });
2923
- });
2924
- app.post('/game/upload', async (req, res) => {
2925
- try {
2926
- console.log(`正在打包当前目录: ${process.cwd()} ...`);
2927
- const gameZipBuffer = await zipCwdToBuffer();
2928
- // 变成 MB
2929
- console.log(`打包完成,大小: ${(gameZipBuffer.length / 1024 / 1024).toFixed(2)} MB`);
2930
- const desc = req.headers['ttmg-game-desc'];
2931
- const decodedDesc = decodeURIComponent(desc || '--');
2932
- const { data, error } = await uploadGameToPlatform({
2933
- // @ts-ignore
2934
- data: gameZipBuffer,
2935
- name: 'game.zip',
2936
- clientKey: getClientKey().clientKey,
2937
- note: decodedDesc,
2938
- appId: store.getState().appId,
2939
- });
2940
- if (error) {
2941
- res.send({ code: errorCode, error });
2942
- }
2943
- else {
2944
- res.send({ code: successCode, data });
2945
- }
2946
- }
2947
- catch (error) {
2948
- let errorMessage = 'An unknown error occurred.';
2949
- if (error instanceof Error) {
2950
- errorMessage = error.message;
2951
- }
2952
- console.error('Upload failed:', errorMessage);
2953
- res.status(500).send({ code: errorCode, data: errorMessage });
2954
- }
2955
- });
2956
- app.get('/game/wasm-split-config', (req, res) => {
2957
- const config = getSplitConfig();
2958
- if (!config) {
2959
- res.send({ code: errorCode, data: 'Failed to parse split config' });
2960
- }
2961
- else {
2962
- res.send({ code: successCode, data: config });
2963
- }
2964
- });
2965
- app.get('/game/wasm-split-options', (req, res) => {
2966
3298
  res.send({
2967
3299
  code: successCode,
2968
- data: {
2969
- options: [
2970
- {
2971
- md5: '123',
2972
- desc: 'test',
2973
- version: '1.0.0',
2974
- time: '2023-01-01',
2975
- funcCounts: 100,
2976
- },
2977
- {
2978
- md5: '456',
2979
- desc: 'test2',
2980
- version: '1.0.1',
2981
- time: '2023-01-02',
2982
- funcCounts: 200,
2983
- },
2984
- ],
2985
- },
3300
+ msg: 'cancel success',
2986
3301
  });
2987
- });
2988
- /**
2989
- * 分包预处理
2990
- */
2991
- app.post('/game/wasm-prepare', async (req, res) => {
2992
- const { codePath, desc, codeMd5, clientKey } = req.body;
2993
- console.log('wasm-prepare-start', req.body);
2994
- const result = await startPrepare({
3302
+ },
3303
+ };
3304
+
3305
+ const gameWasmCollectFuncidsRoute = {
3306
+ method: 'get',
3307
+ path: '/game/wasm-collect-funcids',
3308
+ handler: async (req, res) => {
3309
+ const { clientKey, codeMd5 } = req.query;
3310
+ console.log('wasm-collect-funcids', req.query);
3311
+ const response = await getCollectedFuncIds({
2995
3312
  client_key: clientKey,
2996
- desc,
2997
3313
  wasm_md5: codeMd5,
2998
- wasm_file_path: codePath,
2999
3314
  });
3000
- console.log('wasm-prepare-end', result);
3001
- if (result.error) {
3315
+ if (response.error) {
3002
3316
  res.send({
3003
3317
  code: errorCode,
3004
- error: result.error,
3005
- ctx: result.ctx,
3318
+ error: response.error,
3319
+ ctx: response.ctx,
3006
3320
  });
3007
3321
  }
3008
3322
  else {
3009
- const { md5 } = result.data?.result || {};
3010
- if (!md5) {
3011
- res.send({
3012
- code: errorCode,
3013
- error: result.data,
3014
- ctx: result.ctx,
3015
- });
3016
- }
3017
- else {
3018
- res.send({
3019
- code: successCode,
3020
- data: result.data?.result || {},
3021
- ctx: result.ctx,
3022
- });
3023
- }
3323
+ res.send({
3324
+ code: successCode,
3325
+ data: response.data?.result || {},
3326
+ ctx: response.ctx,
3327
+ });
3024
3328
  }
3025
- });
3026
- /**
3027
- * @description 分包预处理查询,根据 taskId 查询预处理状态
3028
- * 前十次返回 process,第11次返回 success
3029
- */
3030
- app.post('/game/wasm-prepare-result', async (req, res) => {
3031
- console.log('wasm-prepare-result-request', req.body);
3032
- const { clientKey, codeMd5 } = req.body;
3033
- const response = await getTaskStatus({
3329
+ },
3330
+ };
3331
+
3332
+ const gameWasmCollectInfoRoute = {
3333
+ method: 'get',
3334
+ path: '/game/wasm-collect-info',
3335
+ handler: async (req, res) => {
3336
+ const { clientKey, codeMd5 } = req.query;
3337
+ console.log('wasm-collect-info', req.query);
3338
+ const response = await getCollecttingInfo({
3034
3339
  client_key: clientKey,
3035
3340
  wasm_md5: codeMd5,
3036
3341
  });
@@ -3048,14 +3353,13 @@ async function start() {
3048
3353
  ctx: response.ctx,
3049
3354
  });
3050
3355
  }
3051
- });
3052
- /**
3053
- * @description 下载预处理后的 wasm 包
3054
- */
3055
- app.post('/game/wasm-prepare-download', async (req, res) => {
3056
- /**
3057
- * 下载完成后需要进行 br 并替换 codePath 对应的文件后再返回成功
3058
- */
3356
+ },
3357
+ };
3358
+
3359
+ const gameWasmPrepareDownloadRoute = {
3360
+ method: 'post',
3361
+ path: '/game/wasm-prepare-download',
3362
+ handler: async (req, res) => {
3059
3363
  const { clientKey, codeMd5, codePath } = req.body;
3060
3364
  console.log('wasm-prepare-download-start', req.body);
3061
3365
  const response = await downloadPrepared({
@@ -3077,14 +3381,16 @@ async function start() {
3077
3381
  ctx: response.ctx,
3078
3382
  });
3079
3383
  }
3080
- });
3081
- /**
3082
- * 开始代码分包
3083
- */
3084
- app.post('/game/wasm-split', async (req, res) => {
3384
+ },
3385
+ };
3386
+
3387
+ const gameWasmPrepareResultRoute = {
3388
+ method: 'post',
3389
+ path: '/game/wasm-prepare-result',
3390
+ handler: async (req, res) => {
3391
+ console.log('wasm-prepare-result-request', req.body);
3085
3392
  const { clientKey, codeMd5 } = req.body;
3086
- console.log('wasm-split-start', req.body);
3087
- const response = await startSplit({
3393
+ const response = await getTaskStatus({
3088
3394
  client_key: clientKey,
3089
3395
  wasm_md5: codeMd5,
3090
3396
  });
@@ -3098,34 +3404,57 @@ async function start() {
3098
3404
  else {
3099
3405
  res.send({
3100
3406
  code: successCode,
3101
- data: response.data,
3407
+ data: response.data?.result || {},
3102
3408
  ctx: response.ctx,
3103
3409
  });
3104
3410
  }
3105
- });
3106
- app.get('/game/wasm-taskinfo', async (req, res) => {
3107
- const { clientKey, codeMd5 } = req.query;
3108
- console.log('wasm-taskinfo', req.query);
3109
- const response = await getTaskInfo({
3411
+ },
3412
+ };
3413
+
3414
+ const gameWasmPrepareRoute = {
3415
+ method: 'post',
3416
+ path: '/game/wasm-prepare',
3417
+ handler: async (req, res) => {
3418
+ const { codePath, desc, codeMd5, clientKey } = req.body;
3419
+ console.log('wasm-prepare-start', req.body);
3420
+ const result = await startPrepare({
3110
3421
  client_key: clientKey,
3422
+ desc,
3111
3423
  wasm_md5: codeMd5,
3424
+ wasm_file_path: codePath,
3112
3425
  });
3113
- if (response.error) {
3426
+ console.log('wasm-prepare-end', result);
3427
+ if (result.error) {
3114
3428
  res.send({
3115
3429
  code: errorCode,
3116
- error: response.error,
3117
- ctx: response.ctx,
3430
+ error: result.error,
3431
+ ctx: result.ctx,
3118
3432
  });
3119
3433
  }
3120
3434
  else {
3121
- res.send({
3122
- code: successCode,
3123
- data: response.data?.result || {},
3124
- ctx: response.ctx,
3125
- });
3435
+ const { md5 } = result.data?.result || {};
3436
+ if (!md5) {
3437
+ res.send({
3438
+ code: errorCode,
3439
+ error: result.data,
3440
+ ctx: result.ctx,
3441
+ });
3442
+ }
3443
+ else {
3444
+ res.send({
3445
+ code: successCode,
3446
+ data: result.data?.result || {},
3447
+ ctx: result.ctx,
3448
+ });
3449
+ }
3126
3450
  }
3127
- });
3128
- app.post('/game/wasm-set-collect', async (req, res) => {
3451
+ },
3452
+ };
3453
+
3454
+ const gameWasmSetCollectRoute = {
3455
+ method: 'post',
3456
+ path: '/game/wasm-set-collect',
3457
+ handler: async (req, res) => {
3129
3458
  const { clientKey, codeMd5 } = req.body;
3130
3459
  console.log('wasm-set-collect', req.body);
3131
3460
  const response = await setCollect({
@@ -3146,13 +3475,19 @@ async function start() {
3146
3475
  ctx: response.ctx,
3147
3476
  });
3148
3477
  }
3149
- });
3150
- app.get('/game/wasm-collect-funcids', async (req, res) => {
3151
- const { clientKey, codeMd5 } = req.query;
3152
- console.log('wasm-collect-funcids', req.query);
3153
- const response = await getCollectedFuncIds({
3478
+ },
3479
+ };
3480
+
3481
+ const gameWasmSplitDownloadResultRoute = {
3482
+ method: 'post',
3483
+ path: '/game/wasm-split-download-result',
3484
+ handler: async (req, res) => {
3485
+ const { clientKey, codeMd5, codePath } = req.body;
3486
+ console.log('game/wasm-split-download-result-start', req.body);
3487
+ const response = await getSplitResult({
3154
3488
  client_key: clientKey,
3155
3489
  wasm_md5: codeMd5,
3490
+ wasm_path: codePath,
3156
3491
  });
3157
3492
  if (response.error) {
3158
3493
  res.send({
@@ -3162,20 +3497,47 @@ async function start() {
3162
3497
  });
3163
3498
  }
3164
3499
  else {
3500
+ const splitResult = (response.data?.result || {});
3501
+ const requiredDownloadFields = [
3502
+ 'main_wasm_download_url',
3503
+ 'main_wasm_h5_download_url',
3504
+ // 'sub_wasm_download_url',
3505
+ // 'sub_js_download_url',
3506
+ // 'sub_js_data_download_url',
3507
+ // 'sub_js_range_download_url',
3508
+ ];
3509
+ const missingFields = requiredDownloadFields.filter(field => {
3510
+ const value = splitResult[field];
3511
+ return typeof value !== 'string' || value.trim() === '';
3512
+ });
3513
+ if (missingFields.length > 0) {
3514
+ res.send({
3515
+ code: errorCode,
3516
+ error: {
3517
+ message: `Missing required wasm split fields: ${missingFields.join(', ')}`,
3518
+ },
3519
+ data: response.data || {},
3520
+ ctx: response.ctx,
3521
+ });
3522
+ return;
3523
+ }
3165
3524
  res.send({
3166
3525
  code: successCode,
3167
- data: response.data?.result || {},
3526
+ data: response.data || {},
3527
+ msg: 'download success',
3168
3528
  ctx: response.ctx,
3169
3529
  });
3170
3530
  }
3171
- });
3172
- app.get('/game/wasm-collect-info', async (req, res) => {
3173
- const { clientKey, codeMd5 } = req.query;
3174
- console.log('wasm-collect-info', req.query);
3175
- const response = await getCollecttingInfo({
3176
- client_key: clientKey,
3177
- wasm_md5: codeMd5,
3178
- });
3531
+ },
3532
+ };
3533
+
3534
+ const gameWasmSplitDownloadRoute = {
3535
+ method: 'post',
3536
+ path: '/game/wasm-split-download',
3537
+ handler: async (req, res) => {
3538
+ console.log('game/wasm-split-download-start', req.body);
3539
+ const response = await downloadSplited(req.body);
3540
+ console.log('game/wasm-split-download-end', response);
3179
3541
  if (response.error) {
3180
3542
  res.send({
3181
3543
  code: errorCode,
@@ -3186,21 +3548,51 @@ async function start() {
3186
3548
  else {
3187
3549
  res.send({
3188
3550
  code: successCode,
3189
- data: response.data?.result || {},
3551
+ data: response.data || {},
3552
+ msg: 'download success',
3190
3553
  ctx: response.ctx,
3191
3554
  });
3192
3555
  }
3193
- });
3194
- /**
3195
- * 获取代码分包结果
3196
- */
3197
- app.post('/game/wasm-split-result', async (req, res) => {
3198
- const { codeMd5, clientKey } = req.body;
3199
- console.log('wasm-split-result', req.body);
3200
- const response = await getTaskStatus({
3201
- client_key: clientKey,
3202
- wasm_md5: codeMd5,
3556
+ },
3557
+ };
3558
+
3559
+ const gameWasmSplitOptionsRoute = {
3560
+ method: 'get',
3561
+ path: '/game/wasm-split-options',
3562
+ handler: (req, res) => {
3563
+ res.send({
3564
+ code: successCode,
3565
+ data: {
3566
+ options: [
3567
+ {
3568
+ md5: '123',
3569
+ desc: 'test',
3570
+ version: '1.0.0',
3571
+ time: '2023-01-01',
3572
+ funcCounts: 100,
3573
+ },
3574
+ {
3575
+ md5: '456',
3576
+ desc: 'test2',
3577
+ version: '1.0.1',
3578
+ time: '2023-01-02',
3579
+ funcCounts: 200,
3580
+ },
3581
+ ],
3582
+ },
3203
3583
  });
3584
+ },
3585
+ };
3586
+
3587
+ const gameWasmSplitResetRoute = {
3588
+ method: 'post',
3589
+ path: '/game/wasm-split-reset',
3590
+ handler: async (req, res) => {
3591
+ const { clientKey, codeMd5, codePath } = req.body;
3592
+ console.log('wasm-split-reset', req.body);
3593
+ const response = await resetWasmSplit({
3594
+ clientkey: clientKey,
3595
+ wasmMd5: codeMd5});
3204
3596
  if (response.error) {
3205
3597
  res.send({
3206
3598
  code: errorCode,
@@ -3211,21 +3603,22 @@ async function start() {
3211
3603
  else {
3212
3604
  res.send({
3213
3605
  code: successCode,
3214
- data: response.data?.result || {},
3606
+ data: response.data || {},
3215
3607
  ctx: response.ctx,
3216
3608
  });
3217
3609
  }
3218
- });
3219
- /**
3220
- * @description 下载分包产物后,查询并返回下载结果
3221
- */
3222
- app.post('/game/wasm-split-download-result', async (req, res) => {
3223
- const { clientKey, codeMd5, codePath } = req.body;
3224
- console.log('game/wasm-split-download-result-start', req.body);
3225
- const response = await getSplitResult({
3610
+ },
3611
+ };
3612
+
3613
+ const gameWasmSplitResultRoute = {
3614
+ method: 'post',
3615
+ path: '/game/wasm-split-result',
3616
+ handler: async (req, res) => {
3617
+ const { codeMd5, clientKey } = req.body;
3618
+ console.log('wasm-split-result', req.body);
3619
+ const response = await getTaskStatus({
3226
3620
  client_key: clientKey,
3227
3621
  wasm_md5: codeMd5,
3228
- wasm_path: codePath,
3229
3622
  });
3230
3623
  if (response.error) {
3231
3624
  res.send({
@@ -3237,16 +3630,37 @@ async function start() {
3237
3630
  else {
3238
3631
  res.send({
3239
3632
  code: successCode,
3240
- data: response.data || {},
3241
- msg: 'download success',
3633
+ data: response.data?.result || {},
3242
3634
  ctx: response.ctx,
3243
3635
  });
3244
3636
  }
3245
- });
3246
- app.post('/game/wasm-split-download', async (req, res) => {
3247
- console.log('game/wasm-split-download-start', req.body);
3248
- const response = await downloadSplited(req.body);
3249
- console.log('game/wasm-split-download-end', response);
3637
+ },
3638
+ };
3639
+
3640
+ const gameWasmSplitConfigRoute = {
3641
+ method: 'get',
3642
+ path: '/game/wasm-split-config',
3643
+ handler: (req, res) => {
3644
+ const config = getSplitConfig();
3645
+ if (!config) {
3646
+ res.send({ code: errorCode, data: 'Failed to parse split config' });
3647
+ }
3648
+ else {
3649
+ res.send({ code: successCode, data: config });
3650
+ }
3651
+ },
3652
+ };
3653
+
3654
+ const gameWasmSplitRoute = {
3655
+ method: 'post',
3656
+ path: '/game/wasm-split',
3657
+ handler: async (req, res) => {
3658
+ const { clientKey, codeMd5 } = req.body;
3659
+ console.log('wasm-split-start', req.body);
3660
+ const response = await startSplit({
3661
+ client_key: clientKey,
3662
+ wasm_md5: codeMd5,
3663
+ });
3250
3664
  if (response.error) {
3251
3665
  res.send({
3252
3666
  code: errorCode,
@@ -3257,29 +3671,23 @@ async function start() {
3257
3671
  else {
3258
3672
  res.send({
3259
3673
  code: successCode,
3260
- data: response.data || {},
3261
- msg: 'download success',
3674
+ data: response.data,
3262
3675
  ctx: response.ctx,
3263
3676
  });
3264
3677
  }
3265
- });
3266
- app.post('/game/wasm-cancel', async (req, res) => {
3267
- const { clientKey, codePath } = req.body;
3268
- console.log('wasm-cancel', req.body);
3269
- await cancelSplit({
3270
- wasmCodePath: codePath,
3271
- });
3272
- res.send({
3273
- code: successCode,
3274
- msg: 'cancel success',
3678
+ },
3679
+ };
3680
+
3681
+ const gameWasmTaskinfoRoute = {
3682
+ method: 'get',
3683
+ path: '/game/wasm-taskinfo',
3684
+ handler: async (req, res) => {
3685
+ const { clientKey, codeMd5 } = req.query;
3686
+ console.log('wasm-taskinfo', req.query);
3687
+ const response = await getTaskInfo({
3688
+ client_key: clientKey,
3689
+ wasm_md5: codeMd5,
3275
3690
  });
3276
- });
3277
- app.post('/game/wasm-split-reset', async (req, res) => {
3278
- const { clientKey, codeMd5, codePath } = req.body;
3279
- console.log('wasm-split-reset', req.body);
3280
- const response = await resetWasmSplit({
3281
- clientkey: clientKey,
3282
- wasmMd5: codeMd5});
3283
3691
  if (response.error) {
3284
3692
  res.send({
3285
3693
  code: errorCode,
@@ -3290,133 +3698,352 @@ async function start() {
3290
3698
  else {
3291
3699
  res.send({
3292
3700
  code: successCode,
3293
- data: response.data || {},
3701
+ data: response.data?.result || {},
3294
3702
  ctx: response.ctx,
3295
3703
  });
3296
3704
  }
3705
+ },
3706
+ };
3707
+
3708
+ function getGameFallbackRoute(publicPath) {
3709
+ return {
3710
+ method: 'get',
3711
+ path: '*',
3712
+ handler: (req, res) => {
3713
+ res.sendFile(path.join(publicPath, 'index.html'));
3714
+ },
3715
+ };
3716
+ }
3717
+
3718
+ const routes = [
3719
+ gameConfigRoute,
3720
+ gameConfigFillbackRoute,
3721
+ gameDetailRoute,
3722
+ gameCheckRoute,
3723
+ gameUploadRoute,
3724
+ gameWasmSplitConfigRoute,
3725
+ gameWasmSplitOptionsRoute,
3726
+ gameWasmPrepareRoute,
3727
+ gameWasmPrepareResultRoute,
3728
+ gameWasmPrepareDownloadRoute,
3729
+ gameWasmSplitRoute,
3730
+ gameWasmTaskinfoRoute,
3731
+ gameWasmSetCollectRoute,
3732
+ gameWasmCollectFuncidsRoute,
3733
+ gameWasmCollectInfoRoute,
3734
+ gameWasmSplitResultRoute,
3735
+ gameWasmSplitDownloadResultRoute,
3736
+ gameWasmSplitDownloadRoute,
3737
+ gameWasmCancelRoute,
3738
+ gameWasmSplitResetRoute,
3739
+ ];
3740
+ function registerRoutes(app, options) {
3741
+ const allRoutes = [...routes, getGameFallbackRoute(options.publicPath)];
3742
+ for (const route of allRoutes) {
3743
+ if (route.method === 'get') {
3744
+ app.get(route.path, route.handler);
3745
+ }
3746
+ else if (route.method === 'post') {
3747
+ app.post(route.path, route.handler);
3748
+ }
3749
+ }
3750
+ }
3751
+
3752
+ const outputDir = getOutputDir();
3753
+ const publicPath = path.join(__dirname, 'public');
3754
+ async function start() {
3755
+ const startTime = Date.now();
3756
+ const app = express();
3757
+ setupMiddlewares(app, { publicPath, outputDir });
3758
+ registerRoutes(app, { publicPath });
3759
+ const { url, version } = await listen(app, { maxRetries: 20 });
3760
+ console.log(chalk.green.bold(`TTMG`), chalk.green(`v${version}`), chalk.gray(t('native.server.readyIn')), chalk.bold(`${Date.now() - startTime}ms`));
3761
+ showTips({ server: url });
3762
+ openUrl(url);
3763
+ }
3764
+
3765
+ async function dev() {
3766
+ await check();
3767
+ await init();
3768
+ await start();
3769
+ await compile();
3770
+ listen$1();
3771
+ watch();
3772
+ }
3773
+
3774
+ function printMessage(type, message) {
3775
+ const prefix = type === 'Error'
3776
+ ? chalk.red(t('login.error.prefix'))
3777
+ : chalk.yellow(t('login.warning.prefix'));
3778
+ const log = type === 'Error' ? console.error : console.warn;
3779
+ log(prefix + message);
3780
+ }
3781
+ let spinner;
3782
+ const LOGIN_TT4D = 'https://developers.tiktok.com/passport/web/email/login';
3783
+ const params = {
3784
+ aid: '2471',
3785
+ account_sdk_source: 'web',
3786
+ sdk_version: '2.1.6-tiktok',
3787
+ };
3788
+ const prompt = inquirer.createPromptModule();
3789
+ async function login(options) {
3790
+ const verbose = options?.verbose === true;
3791
+ const log = (msg, data) => {
3792
+ if (!verbose)
3793
+ return;
3794
+ console.log(chalk.gray('[ttmg login]'), msg, data !== undefined ? data : '');
3795
+ };
3796
+ if (verbose)
3797
+ log(t('login.verbose.enabled'));
3798
+ console.log(chalk.yellowBright(t('login.note')));
3799
+ const { email, password } = await prompt([
3800
+ {
3801
+ type: 'input',
3802
+ name: 'email',
3803
+ message: t('login.email.prompt'),
3804
+ validate: input => {
3805
+ if (!input) {
3806
+ return t('login.email.required');
3807
+ }
3808
+ else {
3809
+ if (!/^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/.test(input)) {
3810
+ return t('login.email.invalid');
3811
+ }
3812
+ }
3813
+ return true;
3814
+ },
3815
+ },
3816
+ {
3817
+ type: 'password',
3818
+ name: 'password',
3819
+ message: t('login.password.prompt'),
3820
+ mask: '*',
3821
+ validate: input => {
3822
+ if (!input) {
3823
+ return t('login.password.required');
3824
+ }
3825
+ return true;
3826
+ },
3827
+ },
3828
+ ]);
3829
+ const url = LOGIN_TT4D + '?' + new URLSearchParams(params);
3830
+ log('Request URL', url);
3831
+ log('Request params', { ...params, email: email.replace(/(.{2}).*(@.*)/, '$1***$2') });
3832
+ const headers = {
3833
+ 'Content-Type': 'application/x-www-form-urlencoded',
3834
+ Accept: '*/*',
3835
+ 'Accept-Encoding': 'gzip, deflate, br',
3836
+ Origin: 'https://developers.tiktok.com',
3837
+ Referer: 'https://developers.tiktok.com/passport/web/email/login',
3838
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0 Safari/537.36',
3839
+ };
3840
+ const ora = await import('ora');
3841
+ spinner = ora.default({
3842
+ text: chalk.bold.cyan(t('login.spinner.loggingIn')),
3843
+ spinner: 'dots',
3297
3844
  });
3298
- app.get('*', (req, res) => {
3299
- res.sendFile(path.join(publicPath, `index.html`));
3845
+ spinner.start();
3846
+ const data = qs.stringify({
3847
+ email,
3848
+ password,
3849
+ mix_mode: '1',
3850
+ fixed_mix_mode: '1',
3300
3851
  });
3301
- // --- 中间件和路由设置结束 ---
3302
- // 步骤 2: 用配置好的 app 实例创建一个 http.Server。我们只创建这一次!
3303
- const server = http.createServer(app);
3304
- /**
3305
- * @description 尝试在指定端口启动服务。这是个纯粹的辅助函数。
3306
- * @param {number} port - 要尝试的端口号。
3307
- * @returns {Promise<boolean>} 成功返回 true,因端口占用失败则返回 false。
3308
- */
3309
- function tryListen(port) {
3310
- return new Promise(resolve => {
3311
- // 定义错误处理函数
3312
- const onError = err => {
3313
- // 清理掉另一个事件的监听器,防止内存泄漏
3314
- server.removeListener('listening', onListening);
3315
- if (err.code === 'EADDRINUSE') {
3316
- console.log(chalk(`Port ${port} is already in use, trying ${port + 1}...`));
3317
- resolve(false); // 明确表示因端口占用而失败
3852
+ try {
3853
+ log('Sending POST request...');
3854
+ const response = await axios.post(url, data, {
3855
+ headers,
3856
+ maxRedirects: 20,
3857
+ timeout: 30000,
3858
+ });
3859
+ log('Response status', response.status);
3860
+ log('Response data', response?.data);
3861
+ if (!response?.data?.data?.user_id) {
3862
+ const errCode = response.data?.data?.error_code;
3863
+ const errMsg = response.data?.data?.description;
3864
+ const statusText = response?.statusText ?? '';
3865
+ spinner.fail(chalk.red(t('login.failed')));
3866
+ if (verbose) {
3867
+ console.log(chalk.gray('Response status:'), response?.status);
3868
+ console.log(chalk.gray('Response statusText:'), statusText || '(empty)');
3869
+ console.log(chalk.gray('Response data:'), response?.data ?? '(empty)');
3870
+ console.log('');
3871
+ }
3872
+ if (errCode || errMsg) {
3873
+ log('Login failed (api)', { errCode, errMsg, fullData: response?.data });
3874
+ printMessage('Error', errCode
3875
+ ? t('login.error.withCode', {
3876
+ message: String(errMsg ?? ''),
3877
+ code: String(errCode),
3878
+ })
3879
+ : errMsg
3880
+ ? t('login.error.withMessage', { message: String(errMsg) })
3881
+ : t('login.error.noUserId'));
3882
+ }
3883
+ else {
3884
+ log('Login failed (no user_id)', { responseBody: response?.data });
3885
+ if (verbose)
3886
+ log('Full response (for debugging)', response);
3887
+ const looksLikeProxyResponse = statusText === 'Connection established' ||
3888
+ (response?.status === 200 &&
3889
+ (response?.data == null ||
3890
+ typeof response.data !== 'object' ||
3891
+ !('data' in response.data)));
3892
+ if (looksLikeProxyResponse) {
3893
+ printMessage('Warning', t('login.warning.proxyIssue'));
3318
3894
  }
3319
3895
  else {
3320
- // 对于其他致命错误,直接退出进程
3321
- console.log(chalk.red.bold(err.message));
3322
- process.exit(1);
3896
+ printMessage('Error', t('login.error.noUserId'));
3323
3897
  }
3324
- };
3325
- // 定义成功处理函数
3326
- const onListening = () => {
3327
- // 清理掉另一个事件的监听器
3328
- server.removeListener('error', onError);
3329
- resolve(true); // 明确表示成功
3330
- };
3331
- // 使用 .once() 来确保监听器只执行一次然后自动移除
3332
- server.once('error', onError);
3333
- server.once('listening', onListening);
3334
- // 执行监听动作
3335
- server.listen(port);
3336
- });
3337
- }
3338
- // 步骤 3: 使用循环来线性、串行地尝试启动服务
3339
- let isListening = false;
3340
- const maxRetries = 20; // 设置一个最大重试次数,以防万一
3341
- for (let i = 0; i < maxRetries; i++) {
3342
- const currentPort = store.getState().nodeServerPort;
3343
- isListening = await tryListen(currentPort);
3344
- if (isListening) {
3345
- break; // 成功,跳出循环
3898
+ }
3899
+ spinner.stop();
3900
+ process.exit(1);
3346
3901
  }
3347
3902
  else {
3348
- // 失败(端口占用),更新端口号,准备下一次循环
3349
- store.setState({ nodeServerPort: currentPort + 1 });
3903
+ const loginData = {
3904
+ email,
3905
+ user_id: response?.data?.data?.user_id,
3906
+ cookie: response?.headers['set-cookie'].join('; '),
3907
+ };
3908
+ log('Login success', { user_id: loginData.user_id, email: loginData.email });
3909
+ setTTMGRC(loginData);
3910
+ spinner.succeed(chalk.bold.green(t('login.success')));
3911
+ process.exit(0);
3350
3912
  }
3351
3913
  }
3352
- // 步骤 4: 检查最终结果,如果所有尝试都失败了,则退出
3353
- if (!isListening) {
3354
- console.log(chalk.red.bold(`Failed to start server after trying ${maxRetries} ports.`));
3914
+ catch (error) {
3915
+ log('Request error', error instanceof Error ? { message: error.message, name: error.name, stack: error.stack } : error);
3916
+ spinner.fail(chalk.red.bold(t('login.error.connectService')));
3917
+ printMessage('Error', t('login.error.networkBlocked'));
3355
3918
  process.exit(1);
3356
3919
  }
3357
- // --- 服务启动成功后的逻辑 ---
3358
- // @ts-ignore
3359
- const finalPort = server.address().port; // 从成功的 server 实例安全地获取最终端口
3360
- console.log(chalk.green.bold(`TTMG`), chalk.green(`v${devToolVersion}`), chalk.gray(`ready in`), chalk.bold(`${Date.now() - startTime}ms`));
3361
- const baseUrl = `http://localhost:${finalPort}?v=${devToolVersion}`;
3362
- showTips({ server: baseUrl });
3363
- openUrl(baseUrl);
3364
3920
  }
3365
3921
 
3366
- async function dev() {
3367
- await check();
3368
- await init();
3369
- await start();
3370
- await compile();
3371
- listen();
3372
- watch();
3922
+ const supportedLangs = ['en-US', 'zh-CN'];
3923
+ function isSupportedLang(lang) {
3924
+ return supportedLangs.includes(lang);
3925
+ }
3926
+ async function setup(options) {
3927
+ const inputLang = options?.lang;
3928
+ if (inputLang !== undefined && !isSupportedLang(inputLang)) {
3929
+ console.error(chalk.red(t('setup.error.unsupportedLang', { lang: inputLang })));
3930
+ console.error(chalk.yellow(t('setup.error.availableLangs')));
3931
+ console.error(chalk.cyan(t('setup.error.chooseHint')));
3932
+ process.exit(1);
3933
+ }
3934
+ const lang = inputLang ??
3935
+ (await inquirer.createPromptModule()([
3936
+ {
3937
+ type: 'list',
3938
+ name: 'lang',
3939
+ message: t('setup.prompt.selectLanguage'),
3940
+ choices: [
3941
+ { name: t('setup.choice.en'), value: 'en-US' },
3942
+ { name: t('setup.choice.zh'), value: 'zh-CN' },
3943
+ ],
3944
+ default: 'en-US',
3945
+ },
3946
+ ])).lang;
3947
+ setTTMGRC({ lang });
3948
+ console.log(chalk.green.bold(t('setup.success', { lang }, lang)));
3949
+ process.exit(0);
3373
3950
  }
3374
3951
 
3375
- var version = "0.3.2-beta.4";
3952
+ async function checkUpdate() {
3953
+ const worker = new worker_threads.Worker(path.resolve(__dirname, './scripts/worker.js'));
3954
+ worker.on('message', msg => {
3955
+ // console.log(msg);
3956
+ });
3957
+ worker.on('error', err => {
3958
+ console.error(err);
3959
+ });
3960
+ worker.postMessage({
3961
+ type: 'checkUpdate',
3962
+ lang: getCurrentLanguage(),
3963
+ });
3964
+ }
3965
+
3966
+ async function reset() {
3967
+ console.log(chalk.yellow.bold(t('reset.warning.title')));
3968
+ console.log(chalk.yellow(`1. ${t('reset.warning.lang')}`));
3969
+ console.log(chalk.yellow(`2. ${t('reset.warning.login')}`));
3970
+ console.log(chalk.yellow(`3. ${t('reset.warning.clientKey')}`));
3971
+ console.log('');
3972
+ const prompt = inquirer.createPromptModule();
3973
+ const { confirmed } = await prompt([
3974
+ {
3975
+ type: 'list',
3976
+ name: 'confirmed',
3977
+ message: t('reset.confirm.prompt'),
3978
+ choices: [
3979
+ { name: t('reset.confirm.yes'), value: true },
3980
+ { name: t('reset.confirm.no'), value: false },
3981
+ ],
3982
+ default: false,
3983
+ },
3984
+ ]);
3985
+ if (!confirmed) {
3986
+ console.log(chalk.gray(t('reset.cancelled')));
3987
+ process.exit(0);
3988
+ }
3989
+ // Reset to an empty local state.
3990
+ resetTTMGRC({});
3991
+ console.log(chalk.green.bold(t('reset.success')));
3992
+ process.exit(0);
3993
+ }
3994
+
3995
+ var version = "0.3.2-beta.6";
3376
3996
  var pkg = {
3377
3997
  version: version};
3378
3998
 
3379
3999
  const program = new commander.Command();
4000
+ maybeShowPostInstallNotice(pkg.version);
3380
4001
  program
3381
4002
  .name('ttmg')
3382
- .description('TikTok Mini Games Command Line Tool')
3383
- .version(pkg.version, '-v, --version', '显示版本号')
3384
- .option('dev', 'Debug TikTok Mini Games for Client')
3385
- .option('dev --h5', 'Debug TikTok Mini Games for Web');
4003
+ .description(t('cli.description'))
4004
+ .version(pkg.version, '-v, --version', t('cli.version.desc'))
4005
+ .option('dev', t('cli.option.dev.client'))
4006
+ .option('dev --h5', t('cli.option.dev.h5'));
3386
4007
  program
3387
4008
  .command('login')
3388
- .description('User Dev Portal Account to Login')
3389
- .option('--verbose', 'Print verbose logs for debugging')
4009
+ .description(t('cli.command.login.desc'))
4010
+ .option('--verbose', t('cli.command.login.verbose'))
3390
4011
  .action(async (cmd) => {
3391
4012
  await login({ verbose: cmd.verbose });
3392
4013
  });
3393
4014
  program
3394
4015
  .command('setup')
3395
- .description('Initialize ttmg environment')
3396
- .option('--lang <lang>', 'Language: en-US | zh-CN')
4016
+ .description(t('cli.command.setup.desc'))
4017
+ .option('--lang <lang>', t('cli.command.setup.lang'))
3397
4018
  .action(async (cmd) => {
3398
4019
  await setup({ lang: cmd.lang });
3399
4020
  });
3400
4021
  program
3401
- .option('--h5', 'H5 Mini Game')
4022
+ .command('reset')
4023
+ .description(t('cli.command.reset.desc'))
4024
+ .action(async () => {
4025
+ await reset();
4026
+ });
4027
+ program
4028
+ .option('--h5', t('cli.option.h5'))
3402
4029
  .command('init')
3403
- .description('Initialize project')
4030
+ .description(t('cli.command.init.desc'))
3404
4031
  .action(() => {
3405
4032
  const options = program.opts(); // 获取 options
3406
4033
  if (options.h5) {
3407
4034
  init$1();
3408
4035
  }
3409
4036
  else {
3410
- console.log('Native Mini Game initialize');
4037
+ console.log(t('cli.native.init.placeholder'));
3411
4038
  }
3412
4039
  });
3413
4040
  /**
3414
4041
  * ttmg dev 命令
3415
4042
  */
3416
4043
  program
3417
- .option('--h5', 'H5 Mini Game')
4044
+ .option('--h5', t('cli.option.h5'))
3418
4045
  .command('dev')
3419
- .description('Open browser dev environment')
4046
+ .description(t('cli.command.dev.desc'))
3420
4047
  .action(async () => {
3421
4048
  const options = program.opts();
3422
4049
  if (options.h5) {
@@ -3431,16 +4058,16 @@ program
3431
4058
  * ttmg build 命令
3432
4059
  */
3433
4060
  program
3434
- .option('--h5', 'H5 Mini Game')
4061
+ .option('--h5', t('cli.option.h5'))
3435
4062
  .command('build')
3436
- .description('Bundle project')
4063
+ .description(t('cli.command.build.desc'))
3437
4064
  .action(() => {
3438
4065
  const options = program.opts(); // 获取 options
3439
4066
  if (options.h5) {
3440
4067
  build();
3441
4068
  }
3442
4069
  else {
3443
- console.log('Native Mini Game bundle');
4070
+ console.log(t('cli.native.build.placeholder'));
3444
4071
  }
3445
4072
  });
3446
4073
  program.parse(process.argv);