@ttmg/cli 0.3.6-beta.3 → 0.3.6-beta.wasmcode.split

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 (77) hide show
  1. package/CHANGELOG.md +4 -1
  2. package/dist/index.js +1494 -462
  3. package/dist/index.js.map +1 -1
  4. package/dist/package.json +3 -2
  5. package/dist/public/assets/{baseForm-Dnb99UJB.js → baseForm-CH4B0fQb.js} +4 -4
  6. package/dist/public/assets/baseForm-CH4B0fQb.js.br +0 -0
  7. package/dist/public/assets/{index-yh4tKekV.css → index-BLfeu5YF.css} +1 -1
  8. package/dist/public/assets/{index-CPEErQsP.js → index-BM71H_Ze.js} +1 -1
  9. package/dist/public/assets/index-BRFS2ZhY.css +1 -0
  10. package/dist/public/assets/index-BRFS2ZhY.css.br +0 -0
  11. package/dist/public/assets/index-BSpAncbU.css +1 -0
  12. package/dist/public/assets/{index-C5pU_dUf.js → index-BUFQDiZR.js} +1 -1
  13. package/dist/public/assets/index-BUFQDiZR.js.br +0 -0
  14. package/dist/public/assets/index-BZ2ZRGvx.js +1 -0
  15. package/dist/public/assets/index-Bbp-fMXD.js +1 -0
  16. package/dist/public/assets/index-BqGAmMye.css +1 -0
  17. package/dist/public/assets/{index-CJtCELuI.js → index-Br-Gyi7B.js} +1 -1
  18. package/dist/public/assets/{index-vPoeqDW1.js → index-C0jTkN7v.js} +1 -1
  19. package/dist/public/assets/index-C0jTkN7v.js.br +0 -0
  20. package/dist/public/assets/index-C4KyRN0a.js +1 -0
  21. package/dist/public/assets/index-C4KyRN0a.js.br +0 -0
  22. package/dist/public/assets/{index-DHyyb3Gw.js → index-CA2yi7fQ.js} +1 -1
  23. package/dist/public/assets/index-CA2yi7fQ.js.br +0 -0
  24. package/dist/public/assets/index-CFcABxBe.css +1 -0
  25. package/dist/public/assets/index-CY576z43.js +14 -0
  26. package/dist/public/assets/index-CY576z43.js.br +0 -0
  27. package/dist/public/assets/index-C_GvGYUB.js +1 -0
  28. package/dist/public/assets/index-CtaSaIq7.css +1 -0
  29. package/dist/public/assets/index-D4uugaca.js +1 -0
  30. package/dist/public/assets/index-D6mgKJkP.js +1 -0
  31. package/dist/public/assets/{index-UihZn1LL.css → index-DEA7T0Eb.css} +1 -1
  32. package/dist/public/assets/index-DEA7T0Eb.css.br +0 -0
  33. package/dist/public/assets/{index-BaXjsvp9.js → index-DP50otHK.js} +1 -1
  34. package/dist/public/assets/index-DUdfX7io.js +1 -0
  35. package/dist/public/assets/{index-Bovw-Ai8.js → index-DVqEvrQd.js} +1 -1
  36. package/dist/public/assets/index-DVqEvrQd.js.br +0 -0
  37. package/dist/public/assets/index-Dcdc0Lzb.css +1 -0
  38. package/dist/public/assets/index-DeQT3JAU.css +1 -0
  39. package/dist/public/assets/index-DqFmR7Qk.css +1 -0
  40. package/dist/public/assets/index-DsIb6ZUl.css +1 -0
  41. package/dist/public/assets/index-DuzTbOuu.js +1 -0
  42. package/dist/public/assets/{index-DKKMC8UM.js → index-DwBUxhm4.js} +1 -1
  43. package/dist/public/assets/index-FqyT3gyn.js +1 -0
  44. package/dist/public/assets/{index-CqK4Ymjd.js → index-JoKaDNMa.js} +1 -1
  45. package/dist/public/assets/{index-BkiB8M4Y.css → index-XY8Gg_WJ.css} +1 -1
  46. package/dist/public/assets/index-XY8Gg_WJ.css.br +0 -0
  47. package/dist/public/assets/index-gJcsWhoa.css +1 -0
  48. package/dist/public/assets/index-tReoEVof.css +1 -0
  49. package/dist/public/assets/isPlainObject-DMoTnwgw.js +1 -0
  50. package/dist/public/assets/{times-BsXqTKq4.js → times-DWwvCyLh.js} +1 -1
  51. package/dist/public/index.html +39 -2
  52. package/package.json +3 -2
  53. package/dist/public/assets/baseForm-Dnb99UJB.js.br +0 -0
  54. package/dist/public/assets/index-B8aFXzhN.js +0 -1
  55. package/dist/public/assets/index-BB5kLk47.css +0 -1
  56. package/dist/public/assets/index-BGkIaNEY.js +0 -1
  57. package/dist/public/assets/index-BedicqfR.css +0 -1
  58. package/dist/public/assets/index-Bf6aJOeV.css +0 -1
  59. package/dist/public/assets/index-BkiB8M4Y.css.br +0 -0
  60. package/dist/public/assets/index-BmSg7a2H.js +0 -14
  61. package/dist/public/assets/index-BmSg7a2H.js.br +0 -0
  62. package/dist/public/assets/index-Bovw-Ai8.js.br +0 -0
  63. package/dist/public/assets/index-Bq6YxKLX.css +0 -1
  64. package/dist/public/assets/index-BwbPFgZF.css +0 -1
  65. package/dist/public/assets/index-C5pU_dUf.js.br +0 -0
  66. package/dist/public/assets/index-C8f539tK.css +0 -1
  67. package/dist/public/assets/index-CkcOz6VJ.js +0 -1
  68. package/dist/public/assets/index-D0piMtcL.js +0 -1
  69. package/dist/public/assets/index-DNZgHncv.js +0 -1
  70. package/dist/public/assets/index-DPSts5Re.css +0 -1
  71. package/dist/public/assets/index-DPSts5Re.css.br +0 -0
  72. package/dist/public/assets/index-DhmPFuxl.css +0 -1
  73. package/dist/public/assets/index-OPfpZTmc.js +0 -1
  74. package/dist/public/assets/index-OPfpZTmc.js.br +0 -0
  75. package/dist/public/assets/index-UihZn1LL.css.br +0 -0
  76. package/dist/public/assets/index-vPoeqDW1.js.br +0 -0
  77. package/dist/public/assets/isPlainObject-CKjwQ9bg.js +0 -1
package/dist/index.js CHANGED
@@ -38,9 +38,12 @@ var FormData$1 = require('form-data');
38
38
  var ttmgPack = require('ttmg-pack');
39
39
  var expressStaticGzip = require('express-static-gzip');
40
40
  var fileUpload = require('express-fileupload');
41
+ var ttmgWasmtool = require('@anrans001/ttmg-wasmtool');
42
+ var crypto$1 = require('node:crypto');
41
43
  var fs$1 = require('node:fs');
42
44
  var path$1 = require('node:path');
43
45
  var zlib = require('zlib');
46
+ var crypto = require('crypto');
44
47
  var promises = require('fs/promises');
45
48
  var qs = require('qs');
46
49
 
@@ -69,6 +72,8 @@ var http__namespace = /*#__PURE__*/_interopNamespaceDefault(http);
69
72
  var dns__namespace = /*#__PURE__*/_interopNamespaceDefault(dns);
70
73
  var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
71
74
  var glob__namespace = /*#__PURE__*/_interopNamespaceDefault(glob);
75
+ var fs__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(fs$1);
76
+ var path__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(path$1);
72
77
 
73
78
  async function openUrl(url) {
74
79
  const { launch } = await import('chrome-launcher');
@@ -6504,7 +6509,19 @@ const getCurrentUser = () => {
6504
6509
  }
6505
6510
  };
6506
6511
 
6507
- // ppe_dev_tool
6512
+ /**
6513
+ * PPE / 测试环境开关。
6514
+ *
6515
+ * 所有 CLI 接口(`stark_wasm/v4/*`, `wasm-collect/v1/*`, portal 鉴权等)
6516
+ * 都共用本文件的 `request()`,所以在这里统一注入 PPE header 就能覆盖全量。
6517
+ * 不在 `DEV_HEADERS` 里改是因为那份常量只被 `remotePipeline.ts` 里的老
6518
+ * 远程分包接口 spread 用;新加的 `startSession.ts` / `finishSession.ts` /
6519
+ * `getCollectedFuncIds.ts` 都没 spread,漏一处就会绕开 PPE。
6520
+ *
6521
+ * 要切回线上环境,直接把 `CLI_PPE_ENV` 设成空串即可(下方条件 spread 会
6522
+ * 自动不带这两个 header)。
6523
+ */
6524
+ const CLI_PPE_ENV = 'ppe_wasm_test';
6508
6525
  function getAxiosProxyConfig() {
6509
6526
  const config = getTTMGRC();
6510
6527
  // 优先级: http-proxy > socks-proxy > proxy (老字段兼容)
@@ -6542,6 +6559,8 @@ async function request({ url, method, data, headers, params, }) {
6542
6559
  console.log('Method:', method);
6543
6560
  console.log('Headers:', JSON.stringify({
6544
6561
  Cookie: cookie,
6562
+ ...({ 'x-use-ppe': '1', 'x-tt-env': CLI_PPE_ENV }
6563
+ ),
6545
6564
  ...(headers || {}),
6546
6565
  }, null, 2));
6547
6566
  if (params) {
@@ -6559,8 +6578,11 @@ async function request({ url, method, data, headers, params, }) {
6559
6578
  params,
6560
6579
  headers: {
6561
6580
  Cookie: cookie,
6562
- // 'x-use-ppe': '1',
6563
- // 'x-tt-env': 'ppe_upgrade_script',
6581
+ // 注入 PPE header — 放在 caller headers 之前,允许单个调用点通过
6582
+ // 显式传 `x-tt-env` 来覆盖本次请求(例如某个接口还没在 PPE 上发布)。
6583
+ ...(CLI_PPE_ENV
6584
+ ? { 'x-use-ppe': '1', 'x-tt-env': CLI_PPE_ENV }
6585
+ : {}),
6564
6586
  ...(headers || {}),
6565
6587
  },
6566
6588
  ...proxyConfig,
@@ -6600,7 +6622,6 @@ async function request({ url, method, data, headers, params, }) {
6600
6622
  }
6601
6623
  }
6602
6624
  async function download(url, filePath) {
6603
- // 清理旧文件
6604
6625
  if (fs.existsSync(filePath)) {
6605
6626
  try {
6606
6627
  fs.unlinkSync(filePath);
@@ -6608,16 +6629,31 @@ async function download(url, filePath) {
6608
6629
  catch { }
6609
6630
  }
6610
6631
  const proxyConfig = getAxiosProxyConfig();
6632
+ console.log('[download] start', { url: url.slice(0, 120), filePath, hasProxy: !!proxyConfig.httpsAgent });
6611
6633
  try {
6612
6634
  const res = await axios.get(url, {
6613
6635
  responseType: 'stream',
6614
- // 让非 2xx 进入 catch
6615
6636
  validateStatus: s => s >= 200 && s < 300,
6637
+ // Bail out if the server doesn't start responding within 30s instead of
6638
+ // hanging forever (e.g. proxy misrouting a CDN signed URL).
6639
+ timeout: 30000,
6616
6640
  ...proxyConfig,
6617
6641
  });
6618
- // 关键:把“流事件”封装为 Promise,并 await
6642
+ const total = Number(res.headers['content-length'] || 0);
6643
+ let received = 0;
6644
+ let lastLoggedPct = -1;
6645
+ const startedAt = Date.now();
6619
6646
  await new Promise((resolve, reject) => {
6620
6647
  const writer = fs.createWriteStream(filePath);
6648
+ // Inactivity watchdog: if no bytes arrive for 60s mid-stream, abort.
6649
+ let inactivityTimer = null;
6650
+ const resetInactivity = () => {
6651
+ if (inactivityTimer)
6652
+ clearTimeout(inactivityTimer);
6653
+ inactivityTimer = setTimeout(() => {
6654
+ onError(new Error('download stalled: no data for 60s'));
6655
+ }, 60000);
6656
+ };
6621
6657
  const onError = (e) => {
6622
6658
  cleanup();
6623
6659
  try {
@@ -6629,28 +6665,42 @@ async function download(url, filePath) {
6629
6665
  };
6630
6666
  const onClose = () => {
6631
6667
  cleanup();
6668
+ console.log(`[download] done: ${received} bytes in ${Date.now() - startedAt}ms`);
6632
6669
  resolve();
6633
6670
  };
6634
6671
  const cleanup = () => {
6672
+ if (inactivityTimer)
6673
+ clearTimeout(inactivityTimer);
6635
6674
  writer.off('error', onError);
6636
6675
  writer.off('close', onClose);
6637
6676
  res.data.off('error', onError);
6677
+ res.data.off('data', onData);
6678
+ };
6679
+ const onData = (chunk) => {
6680
+ received += chunk.length;
6681
+ resetInactivity();
6682
+ if (total > 0) {
6683
+ const pct = Math.floor((received / total) * 10) * 10;
6684
+ if (pct !== lastLoggedPct) {
6685
+ lastLoggedPct = pct;
6686
+ console.log(`[download] ${pct}% (${received}/${total})`);
6687
+ }
6688
+ }
6638
6689
  };
6639
6690
  res.data.on('error', onError);
6691
+ res.data.on('data', onData);
6640
6692
  writer.on('error', onError);
6641
6693
  writer.on('close', onClose);
6694
+ resetInactivity();
6642
6695
  res.data.pipe(writer);
6643
6696
  });
6644
- // 成功
6645
6697
  return { ok: true };
6646
6698
  }
6647
6699
  catch (err) {
6648
- // 403 等受控处理
6700
+ console.log('[download] failed:', err?.message);
6649
6701
  if (isAxiosError(err) && err.response?.status === 403) {
6650
- // 不抛出,让上层自行决定
6651
6702
  throw new Error('下载链接已过期,请重新进行分包后重试');
6652
6703
  }
6653
- // 其他错误抛出或返回
6654
6704
  throw err;
6655
6705
  }
6656
6706
  }
@@ -8899,7 +8949,12 @@ const gameUploadRoute = {
8899
8949
  },
8900
8950
  };
8901
8951
 
8952
+ // 历史遗留:`remotePipeline.ts` 里的老远程分包接口 spread 了 `DEV_HEADERS`。
8953
+ // 目前全局 PPE 走的是 `libs/api/request.ts` 里的 `CLI_PPE_ENV`,这里保持
8954
+ // 相同的值是为了:一旦以后需要按接口粒度覆盖 PPE(例如远程走 PPE、本地
8955
+ // 走线上),只需在这里填回 header、两处值天然一致。
8902
8956
  const BASE_URL = 'https://developers.tiktok.com';
8957
+ const WASM_COLLECT_BASE_URL = `${BASE_URL}/api/wasm-collect/v1`;
8903
8958
  const DEV_HEADERS = {
8904
8959
  // 'x-use-ppe': '1',
8905
8960
  // 'x-tt-env': UNITY_PPE_ENV,
@@ -8922,6 +8977,8 @@ const UNITY_WASM_SPLIT_CONFIG_FIELD_SCHEME = {
8922
8977
  WASMSPLITVERSION: `"$WASMSPLITVERSION"`,
8923
8978
  ENABLEWASMSPLIT: `"$ENABLEWASMSPLIT"`,
8924
8979
  IOS_SUB_JS_FILE_CONFIG: `"$IOS_SUB_JS_FILE_CONFIG"`,
8980
+ ENABLEARCHIVEMODE: `"$ENABLEARCHIVEMODE"`,
8981
+ ARCHIVE_CODE_FILE_MD5: `$ARCHIVE_CODE_FILE_MD5`,
8925
8982
  };
8926
8983
 
8927
8984
  const DIR_SPLIT = 'split';
@@ -8960,69 +9017,6 @@ const CONCURRENCY_LIMIT = 2;
8960
9017
  const DOWNLOAD_RETRY = 3;
8961
9018
  const WASM_SPLIT_CONFIG_FILE_NAME = 'webgl-wasm-split.js';
8962
9019
 
8963
- // prepare.ts
8964
- // 若你的 request 是 axios:你可以添加 maxBodyLength/ maxContentLength 等参数
8965
- // 若是 got:可直接传 form 实例
8966
- async function startPrepare(params) {
8967
- const form = new FormData$1();
8968
- form.append('desc', params.desc);
8969
- form.append('wasm_md5', params.wasm_md5);
8970
- form.append('with_ios', 'true');
8971
- // 二进制字段:用 ReadStream(推荐)或 Buffer
8972
- form.append('wasm_file', fs$1.createReadStream(path$1.join(process.cwd(), params.wasm_file_path)), {
8973
- filename: path$1.basename(params.wasm_file_path),
8974
- // 部分后端会依赖 content-type;如果不确定就用 application/octet-stream
8975
- contentType: 'application/wasm',
8976
- });
8977
- /**
8978
- * 兼容 WASM_SYMBOL_FILE_NAME
8979
- * case 1:项目根目录有 webgl.symbols.json 文件
8980
- * case 2:项目根目录没有 webgl.symbols.json 文件,但 TTMG_TEMP_DIR 有
8981
- * 优先读 2
8982
- */
8983
- let symbolFilePath = path$1.join(process.cwd(), TTMG_TEMP_DIR, WASM_SYMBOL_FILE_NAME);
8984
- if (!fs$1.existsSync(symbolFilePath)) {
8985
- symbolFilePath = path$1.join(process.cwd(), WASM_SYMBOL_FILE_NAME);
8986
- }
8987
- /**
8988
- * 判断是否有 symbol 文件,有则上传,没有直接接口报错
8989
- */
8990
- if (!fs$1.existsSync(symbolFilePath)) {
8991
- return {
8992
- error: {
8993
- code: 400,
8994
- message: `${WASM_SYMBOL_FILE_NAME} not found at ${path$1.join(process.cwd())},use unity plugin to rebuild`,
8995
- client_key: params.client_key,
8996
- },
8997
- data: null,
8998
- ctx: {
8999
- logid: '',
9000
- httpStatusCode: 400,
9001
- },
9002
- };
9003
- }
9004
- form.append('wasm_symbol_file', fs$1.createReadStream(symbolFilePath), {
9005
- filename: WASM_SYMBOL_FILE_NAME,
9006
- contentType: 'application/octet-stream',
9007
- });
9008
- // 关键:用 form.getHeaders() 获取带 boundary 的 Content-Type
9009
- const formHeaders = form.getHeaders();
9010
- return request({
9011
- url: `${BASE_URL}/api/stark_wasm/v4/post/prepare`,
9012
- method: 'POST',
9013
- headers: {
9014
- ...DEV_HEADERS,
9015
- ...formHeaders, // 包含正确的 multipart/form-data; boundary=...
9016
- },
9017
- params: {
9018
- client_key: params.client_key,
9019
- with_ios: true,
9020
- },
9021
- data: form,
9022
- // 若 request 基于 axios,建议加上以下两项以支持大文件:
9023
- });
9024
- }
9025
-
9026
9020
  async function withRetry(fn, retries = 3) {
9027
9021
  let lastErr;
9028
9022
  for (let i = 0; i < retries; i++) {
@@ -9039,18 +9033,26 @@ async function withRetry(fn, retries = 3) {
9039
9033
  }
9040
9034
 
9041
9035
  function updateWasmSplitConfig(fields) {
9036
+ const configFilePath = path.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME);
9037
+ let config = fs.readFileSync(configFilePath, 'utf-8');
9042
9038
  for (const field in fields) {
9043
9039
  const value = fields[field];
9044
- const isString = typeof value === 'string';
9045
- const valueStr = isString ? value : String(value);
9046
- const configFilePath = path.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME);
9047
9040
  const placeholder = UNITY_WASM_SPLIT_CONFIG_FIELD_SCHEME[field];
9048
- const config = fs.readFileSync(configFilePath, 'utf-8');
9049
- // 将占位符替换为 true/false 字面量
9050
- // 用正则?因为 placeholder 是一个字符串,可能包含特殊字符
9051
- const updated = config.replace(placeholder, valueStr);
9052
- fs.writeFileSync(configFilePath, updated, 'utf-8');
9041
+ if (!placeholder)
9042
+ continue;
9043
+ let replacement;
9044
+ if (typeof value === 'boolean' || typeof value === 'number') {
9045
+ replacement = String(value);
9046
+ }
9047
+ else if (typeof value === 'string') {
9048
+ replacement = value;
9049
+ }
9050
+ else {
9051
+ replacement = String(value);
9052
+ }
9053
+ config = config.replace(placeholder, replacement);
9053
9054
  }
9055
+ fs.writeFileSync(configFilePath, config, 'utf-8');
9054
9056
  }
9055
9057
 
9056
9058
  async function compressWasmFile(wasmFilePath, compressedFilePath) {
@@ -9062,7 +9064,11 @@ async function compressWasmFile(wasmFilePath, compressedFilePath) {
9062
9064
  }
9063
9065
  function compressArrayBuffer(arrayBuffer) {
9064
9066
  return new Promise((resolve, reject) => {
9065
- const compressStream = zlib.createBrotliCompress();
9067
+ const compressStream = zlib.createBrotliCompress({
9068
+ params: {
9069
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 9,
9070
+ },
9071
+ });
9066
9072
  compressStream.write(Buffer.from(arrayBuffer));
9067
9073
  compressStream.end();
9068
9074
  const compressedChunks = [];
@@ -9098,176 +9104,899 @@ function keepCacheSync({ entryDir, originalWasmPath, originalSplitConfigPath, })
9098
9104
  };
9099
9105
  }
9100
9106
 
9101
- async function downloadPrepared(data) {
9102
- wsServer.sendUnitySplitStatus({
9103
- status: 'star_fetch_prepared_wasm_url',
9104
- });
9105
- const res = await request({
9106
- url: `${BASE_URL}/api/stark_wasm/v4/post/download_prepared`,
9107
- method: 'POST',
9108
- headers: DEV_HEADERS,
9109
- data,
9110
- });
9111
- wsServer.sendUnitySplitStatus({
9112
- status: 'fetch_prepared_wasm_url_done',
9107
+ /**
9108
+ * Restore webgl-wasm-split.js from the cached original (with placeholders).
9109
+ * Called at the start of each prepare so that pipeline-specific values can be
9110
+ * applied deterministically, regardless of previous runs.
9111
+ * No-op if the cache does not yet exist (first run).
9112
+ */
9113
+ function restoreSplitConfigFromCache(entryDir = process.cwd()) {
9114
+ const cachedConfigPath = path.join(entryDir, WASM_SPLIT_CACHE_DIR, path.basename(WASM_SPLIT_CONFIG_FILE_NAME));
9115
+ const targetConfigPath = path.join(entryDir, WASM_SPLIT_CONFIG_FILE_NAME);
9116
+ if (fs.existsSync(cachedConfigPath)) {
9117
+ fs.copyFileSync(cachedConfigPath, targetConfigPath);
9118
+ }
9119
+ }
9120
+
9121
+ /**
9122
+ * Restore the project from the backup cache:
9123
+ * - original (unmodified) wasm file back into its `wasmcode/<file>.br` location
9124
+ * - webgl-wasm-split.js back to its template (with placeholders)
9125
+ * - game.json back to its pre-split version
9126
+ * - remove generated sub-package directories (wasmcode-android, wasmcode1-android, wasmcode-ios, etc.)
9127
+ *
9128
+ * Shared by both local and remote reset/rollback flows.
9129
+ */
9130
+ function restoreFromCache(entryDir = process.cwd()) {
9131
+ const cacheDir = path.join(entryDir, WASM_SPLIT_CACHE_DIR);
9132
+ if (fs.existsSync(cacheDir)) {
9133
+ const targetWasmBrPath = fs
9134
+ .readdirSync(cacheDir)
9135
+ .find(item => item.endsWith('.br'));
9136
+ if (targetWasmBrPath) {
9137
+ const destWasmBrPath = path.join(entryDir, WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root, path.basename(targetWasmBrPath));
9138
+ ensureDirSync(path.dirname(destWasmBrPath));
9139
+ fs.copyFileSync(path.join(cacheDir, targetWasmBrPath), destWasmBrPath);
9140
+ }
9141
+ }
9142
+ const splitConfigCachePath = path.join(cacheDir, WASM_SPLIT_CONFIG_FILE_NAME);
9143
+ if (fs.existsSync(splitConfigCachePath)) {
9144
+ fs.copyFileSync(splitConfigCachePath, path.join(entryDir, WASM_SPLIT_CONFIG_FILE_NAME));
9145
+ }
9146
+ const gameJsonCachePath = path.join(cacheDir, 'game.json');
9147
+ if (fs.existsSync(gameJsonCachePath)) {
9148
+ fs.copyFileSync(gameJsonCachePath, path.join(entryDir, 'game.json'));
9149
+ }
9150
+ const subDirs = [
9151
+ WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain.root,
9152
+ WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub.root,
9153
+ WASM_SPLIT_SUBPACKAGE_CONFIG.ios.root,
9154
+ ];
9155
+ for (const subDir of subDirs) {
9156
+ const full = path.join(entryDir, subDir);
9157
+ if (fs.existsSync(full)) {
9158
+ fs.rmSync(full, { recursive: true });
9159
+ }
9160
+ }
9161
+ }
9162
+
9163
+ async function decompressWasmFile(inputPath, outputPath) {
9164
+ const compressed = await fs.promises.readFile(inputPath);
9165
+ const decompressed = await new Promise((resolve, reject) => {
9166
+ zlib.brotliDecompress(compressed, (err, result) => {
9167
+ if (err)
9168
+ reject(err);
9169
+ else
9170
+ resolve(result);
9171
+ });
9113
9172
  });
9173
+ await fs.promises.writeFile(outputPath, decompressed);
9174
+ }
9175
+
9176
+ function computeFileMd5Sync(filePath) {
9177
+ const content = fs.readFileSync(filePath);
9178
+ return crypto.createHash('md5').update(content).digest('hex');
9179
+ }
9180
+
9181
+ let cached = null;
9182
+ function getGameJson() {
9183
+ if (cached)
9184
+ return cached;
9185
+ const filePath = path$1.join(process.cwd(), 'game.json');
9186
+ if (fs$1.existsSync(filePath)) {
9187
+ try {
9188
+ cached = JSON.parse(fs$1.readFileSync(filePath, 'utf-8'));
9189
+ }
9190
+ catch {
9191
+ cached = {};
9192
+ }
9193
+ }
9194
+ else {
9195
+ cached = {};
9196
+ }
9197
+ return cached;
9198
+ }
9199
+
9200
+ function metaFilePath(entryDir = process.cwd()) {
9201
+ return path__namespace$1.join(entryDir, TTMG_TEMP_DIR, 'prepared-meta.json');
9202
+ }
9203
+ function writePreparedMeta(meta, entryDir = process.cwd()) {
9204
+ const target = metaFilePath(entryDir);
9205
+ fs__namespace$1.mkdirSync(path__namespace$1.dirname(target), { recursive: true });
9206
+ const payload = {
9207
+ ...meta,
9208
+ preparedAt: new Date().toISOString(),
9209
+ };
9210
+ fs__namespace$1.writeFileSync(target, JSON.stringify(payload, null, 2), 'utf-8');
9211
+ }
9212
+ function readPreparedMeta(entryDir = process.cwd()) {
9213
+ const target = metaFilePath(entryDir);
9214
+ if (!fs__namespace$1.existsSync(target))
9215
+ return null;
9114
9216
  try {
9115
- const downloadUrl = res?.data?.result?.download_url;
9116
- const willReplaceWasmPath = path.join(process.cwd(), data.wasm_path);
9117
- if (downloadUrl) {
9118
- const { cacheDir } = keepCacheSync({
9119
- entryDir: process.cwd(),
9120
- originalWasmPath: data.wasm_path,
9121
- originalSplitConfigPath: WASM_SPLIT_CONFIG_FILE_NAME,
9122
- });
9123
- if (downloadUrl.includes('.br')) {
9124
- const tempWasmPath = path.join(cacheDir, '__temp__.wasm.br');
9125
- wsServer.sendUnitySplitStatus({
9126
- status: 'start_download_prepared_wasm',
9127
- url: downloadUrl,
9128
- });
9129
- await download(downloadUrl, tempWasmPath);
9130
- /**
9131
- * 下载完成后需要进行 br 并替换 codePath 对应的文件后再返回成功
9132
- */
9133
- fs$1.copyFileSync(tempWasmPath, willReplaceWasmPath);
9134
- wsServer.sendUnitySplitStatus({
9135
- status: 'download_prepared_wasm_done',
9136
- url: downloadUrl,
9137
- });
9138
- }
9139
- else {
9140
- const tempWasmPath = path.join(cacheDir, '__temp__.wasm');
9141
- wsServer.sendUnitySplitStatus({
9142
- status: 'start_download_prepared_wasm',
9143
- url: downloadUrl,
9144
- });
9145
- await download(downloadUrl, tempWasmPath);
9146
- wsServer.sendUnitySplitStatus({
9147
- status: 'download_prepared_wasm_done',
9148
- url: downloadUrl,
9149
- });
9150
- /**
9151
- * 下载完成后需要进行 br 并替换 codePath 对应的文件后再返回成功
9152
- */
9153
- wsServer.sendUnitySplitStatus({
9154
- status: 'start_compress_prepared_wasm',
9155
- });
9156
- await compressWasmFile(tempWasmPath, willReplaceWasmPath);
9157
- wsServer.sendUnitySplitStatus({
9158
- status: 'compress_prepared_wasm_done',
9159
- url: downloadUrl,
9160
- });
9161
- wsServer.sendUnitySplitStatus({
9162
- status: 'write_compress_prepared_wasm_done',
9163
- });
9164
- }
9165
- wsServer.sendUnitySplitStatus({
9166
- status: 'start_update_wasm_split_config',
9167
- });
9168
- /**
9169
- * 读取 webgl-wasm-split.js内容,将 enableWasmCollect 设为 true
9170
- */
9171
- updateWasmSplitConfig({
9172
- ENABLEWASMCOLLECT: true,
9173
- });
9174
- wsServer.sendUnitySplitStatus({
9175
- status: 'update_wasm_split_config_done',
9176
- });
9177
- return {
9178
- isSuccess: true,
9179
- ctx: res?.ctx,
9180
- };
9217
+ const raw = fs__namespace$1.readFileSync(target, 'utf-8');
9218
+ const parsed = JSON.parse(raw);
9219
+ if (typeof parsed?.preparedWasmMd5 === 'string' &&
9220
+ typeof parsed?.codePath === 'string' &&
9221
+ parsed.preparedWasmMd5.length === 32) {
9222
+ return parsed;
9223
+ }
9224
+ return null;
9225
+ }
9226
+ catch {
9227
+ return null;
9228
+ }
9229
+ }
9230
+ /**
9231
+ * Return the current md5 of the wasm file referenced by `prepared-meta.json`
9232
+ * or null if the file is missing / meta isn't present. Caller compares the
9233
+ * result to `meta.preparedWasmMd5` mismatch means the project's wasm
9234
+ * has drifted from the prepared output (Unity re-build etc.) and the
9235
+ * project should be walked back through the prepare step before collect
9236
+ * can produce useful data.
9237
+ */
9238
+ function computeCurrentProjectWasmMd5(entryDir = process.cwd()) {
9239
+ const meta = readPreparedMeta(entryDir);
9240
+ if (!meta)
9241
+ return null;
9242
+ const absolutePath = path__namespace$1.join(entryDir, meta.codePath);
9243
+ if (!fs__namespace$1.existsSync(absolutePath))
9244
+ return null;
9245
+ const currentMd5 = crypto$1
9246
+ .createHash('md5')
9247
+ .update(fs__namespace$1.readFileSync(absolutePath))
9248
+ .digest('hex');
9249
+ return { meta, currentMd5 };
9250
+ }
9251
+
9252
+ const state = {
9253
+ pipelineMode: 'local',
9254
+ originalWasmPath: '',
9255
+ preparedWasmPath: '',
9256
+ codePath: '',
9257
+ splitOutputDir: '',
9258
+ splitMeta: null,
9259
+ totalWasmFuncCount: 0,
9260
+ wasmSize: 0,
9261
+ isArchiveMode: true,
9262
+ };
9263
+ function getLocalState() {
9264
+ return state;
9265
+ }
9266
+ function setLocalState(partial) {
9267
+ Object.assign(state, partial);
9268
+ }
9269
+
9270
+ async function startPrepare$1(params) {
9271
+ const tempDir = path$1.join(process.cwd(), TTMG_TEMP_DIR);
9272
+ ensureDirSync(tempDir);
9273
+ const inputPath = path$1.join(process.cwd(), params.wasm_file_path);
9274
+ let rawWasmPath = path$1.join(tempDir, 'original.wasm');
9275
+ if (inputPath.endsWith('.br')) {
9276
+ await decompressWasmFile(inputPath, rawWasmPath);
9277
+ }
9278
+ else {
9279
+ fs$1.copyFileSync(inputPath, rawWasmPath);
9280
+ }
9281
+ const preparedWasmPath = path$1.join(tempDir, 'prepared.wasm');
9282
+ try {
9283
+ const result = ttmgWasmtool.prepare(rawWasmPath, preparedWasmPath);
9284
+ console.log(`[wasmtool] prepare done: ${result.outputSize} bytes, ${result.timeCost}s`);
9285
+ const gameJson = getGameJson();
9286
+ const totalWasmFuncCount = gameJson.wasmFuncCount ?? 0;
9287
+ const wasmSize = fs$1.existsSync(inputPath)
9288
+ ? fs$1.statSync(inputPath).size
9289
+ : 0;
9290
+ setLocalState({
9291
+ originalWasmPath: rawWasmPath,
9292
+ preparedWasmPath,
9293
+ codePath: params.wasm_file_path,
9294
+ totalWasmFuncCount,
9295
+ wasmSize,
9296
+ });
9297
+ keepCacheSync({
9298
+ entryDir: process.cwd(),
9299
+ originalWasmPath: params.wasm_file_path,
9300
+ originalSplitConfigPath: WASM_SPLIT_CONFIG_FILE_NAME,
9301
+ });
9302
+ // Start from cached (placeholder) config so pipeline switching is deterministic
9303
+ restoreSplitConfigFromCache();
9304
+ const willReplaceWasmPath = path$1.join(process.cwd(), params.wasm_file_path);
9305
+ // Diagnostic: prove prepare actually produced a different binary
9306
+ // (size should grow noticeably because every function body is prefixed
9307
+ // with a scwebgl.logCall(funcIndex) call).
9308
+ const rawSize = fs$1.existsSync(rawWasmPath) ? fs$1.statSync(rawWasmPath).size : 0;
9309
+ const preparedSize = fs$1.existsSync(preparedWasmPath)
9310
+ ? fs$1.statSync(preparedWasmPath).size
9311
+ : 0;
9312
+ const rawMd5 = fs$1.existsSync(rawWasmPath)
9313
+ ? crypto$1.createHash('md5').update(fs$1.readFileSync(rawWasmPath)).digest('hex')
9314
+ : '<missing>';
9315
+ const preparedMd5 = fs$1.existsSync(preparedWasmPath)
9316
+ ? crypto$1.createHash('md5').update(fs$1.readFileSync(preparedWasmPath)).digest('hex')
9317
+ : '<missing>';
9318
+ console.log(`[wasmtool] prepare sanity: raw(size=${rawSize} md5=${rawMd5}) -> prepared(size=${preparedSize} md5=${preparedMd5}) delta=${preparedSize - rawSize}`);
9319
+ if (preparedSize <= rawSize || preparedMd5 === rawMd5) {
9320
+ console.warn('[wasmtool] WARNING: prepared wasm is not larger / md5 is unchanged vs raw wasm. Instrumentation likely did not happen.');
9321
+ }
9322
+ console.log('[wasmtool] compressing prepared wasm (quality=9)...');
9323
+ await compressWasmFile(preparedWasmPath, willReplaceWasmPath);
9324
+ console.log('[wasmtool] compressed and written to project');
9325
+ // Diagnostic: confirm the file the client actually fetches was overwritten,
9326
+ // and compare to the cached original brotli so we can prove on-disk replacement.
9327
+ const replacedSize = fs$1.existsSync(willReplaceWasmPath)
9328
+ ? fs$1.statSync(willReplaceWasmPath).size
9329
+ : 0;
9330
+ const replacedMd5 = fs$1.existsSync(willReplaceWasmPath)
9331
+ ? crypto$1.createHash('md5').update(fs$1.readFileSync(willReplaceWasmPath)).digest('hex')
9332
+ : '<missing>';
9333
+ const cachedOriginalBr = path$1.join(process.cwd(), TTMG_TEMP_DIR, 'wasmcode', path$1.basename(params.wasm_file_path));
9334
+ const cachedOriginalSize = fs$1.existsSync(cachedOriginalBr)
9335
+ ? fs$1.statSync(cachedOriginalBr).size
9336
+ : 0;
9337
+ const cachedOriginalMd5 = fs$1.existsSync(cachedOriginalBr)
9338
+ ? crypto$1
9339
+ .createHash('md5')
9340
+ .update(fs$1.readFileSync(cachedOriginalBr))
9341
+ .digest('hex')
9342
+ : '<missing>';
9343
+ console.log(`[wasmtool] on-disk replace check: project=${params.wasm_file_path} size=${replacedSize} md5=${replacedMd5} | cached-original size=${cachedOriginalSize} md5=${cachedOriginalMd5}`);
9344
+ if (replacedMd5 === cachedOriginalMd5) {
9345
+ console.warn('[wasmtool] WARNING: project wasm md5 matches cached-original md5. The file was not actually replaced with the instrumented build.');
9181
9346
  }
9182
9347
  else {
9183
- return {
9184
- isSuccess: false,
9185
- error: {
9186
- code: res.data?.code,
9187
- message: res.data?.message,
9188
- },
9189
- ctx: res?.ctx,
9190
- };
9348
+ console.log('[wasmtool] OK: project wasm differs from cached-original — instrumented wasm is on disk.');
9191
9349
  }
9350
+ // Local pipeline uses the new wasm-collect/v1/report API + archive sub-wasm.
9351
+ // ORIGINALWASMMD5 must be set now (not only at split time) so the plugin
9352
+ // reports the correct wasm_md5 during the collect phase.
9353
+ updateWasmSplitConfig({
9354
+ ENABLEWASMCOLLECT: true,
9355
+ ENABLEARCHIVEMODE: true,
9356
+ ORIGINALWASMMD5: params.wasm_md5,
9357
+ });
9358
+ console.log('[wasmtool] wasm split config updated (local pipeline: archive=true)');
9359
+ // Disk-persisted anchor for "wasm drift" detection in
9360
+ // `game-wasm-split-config` route. Stores the md5 that prepare just
9361
+ // wrote into the project alongside the project-relative path. The
9362
+ // route reads this back on every Modal open, recomputes the md5 of
9363
+ // the file on disk, and if they differ (Unity re-build, git
9364
+ // checkout, etc.) suppresses `enableWasmCollect=true` in the
9365
+ // response so the IDE goes back through prepare instead of dropping
9366
+ // the user straight into Collect with an un-instrumented wasm on
9367
+ // the device. See `preparedMeta.ts` for full rationale.
9368
+ writePreparedMeta({
9369
+ preparedWasmMd5: replacedMd5,
9370
+ codePath: params.wasm_file_path,
9371
+ });
9372
+ console.log(`[wasmtool] prepared-meta written: md5=${replacedMd5} codePath=${params.wasm_file_path}`);
9373
+ return {
9374
+ data: {
9375
+ code: 0,
9376
+ message: 'success',
9377
+ result: { md5: params.wasm_md5 },
9378
+ },
9379
+ error: null,
9380
+ ctx: { logid: 'local', httpStatusCode: 200 },
9381
+ };
9192
9382
  }
9193
- catch (error) {
9383
+ catch (err) {
9194
9384
  return {
9195
- isSuccess: false,
9385
+ data: null,
9196
9386
  error: {
9197
- code: res.data?.code,
9198
- message: error.message,
9387
+ code: 500,
9388
+ message: err instanceof Error ? err.message : String(err),
9199
9389
  },
9200
- ctx: res?.ctx,
9390
+ ctx: { logid: 'local', httpStatusCode: 500 },
9201
9391
  };
9202
9392
  }
9203
9393
  }
9204
9394
 
9205
- async function getCollectedFuncIds({ client_key, wasm_md5, }) {
9206
- return request({
9207
- url: `${BASE_URL}/api/stark_wasm/v4/get/collectedfuncids`,
9395
+ /**
9396
+ * Local pipeline: startPrepareLocal already compressed/replaced the wasm and
9397
+ * updated webgl-wasm-split.js, so this step is a no-op that just emits UI
9398
+ * status events for parity with the remote flow.
9399
+ */
9400
+ async function downloadPrepared$1(_data) {
9401
+ const { preparedWasmPath } = getLocalState();
9402
+ if (!preparedWasmPath) {
9403
+ return {
9404
+ isSuccess: false,
9405
+ error: { code: 404, message: 'Prepared wasm not found. Run prepare first.' },
9406
+ };
9407
+ }
9408
+ wsServer.sendUnitySplitStatus({ status: 'update_wasm_split_config_done' });
9409
+ return { isSuccess: true, ctx: { logid: 'local' } };
9410
+ }
9411
+
9412
+ async function getCollectedFuncIds$1({ client_key, wasm_md5, }) {
9413
+ const res = await request({
9414
+ url: `${WASM_COLLECT_BASE_URL}/progress`,
9208
9415
  method: 'GET',
9209
- headers: DEV_HEADERS,
9210
9416
  params: {
9211
- client_key,
9417
+ app_id: client_key,
9212
9418
  wasm_md5,
9213
9419
  },
9214
9420
  });
9421
+ const funcCount = res?.data?.func_count ?? 0;
9422
+ return {
9423
+ data: {
9424
+ code: res?.data?.code ?? 0,
9425
+ message: 'success',
9426
+ result: {
9427
+ collected_func_count: funcCount,
9428
+ data_size: funcCount,
9429
+ real_data_size: funcCount,
9430
+ collect_state: res?.data?.collect_state,
9431
+ },
9432
+ },
9433
+ error: res.error,
9434
+ ctx: res.ctx,
9435
+ };
9215
9436
  }
9216
9437
 
9217
- async function setCollect({ client_key, wasm_md5, }) {
9218
- return request({
9219
- url: `${BASE_URL}/api/stark_wasm/v4/post/set_collecting`,
9438
+ /**
9439
+ * POST /start — opens a collect session (Portal-authenticated).
9440
+ *
9441
+ * Idempotent on the server: re-opening an already-open session just refreshes
9442
+ * `started_at`; only `reset: true` wipes history.
9443
+ *
9444
+ * Default `reset` is `false` to mirror the server-side default documented in
9445
+ * `wasm_api.md` §5.1 — "页面刷新 / 恢复" must NOT silently destroy data. The
9446
+ * "fresh run" semantic (e.g. user clicks "重新开始分包") is the responsibility
9447
+ * of the caller, which must explicitly pass `reset: true`. See `setCollect`
9448
+ * for the CLI-level wiring of those two paths.
9449
+ *
9450
+ * NOTE on naming: the server route is flat (`/start`, not `/session/start`).
9451
+ * Our local symbol stays `startWasmSession` because it's the "start collect
9452
+ * session" lifecycle primitive from the IDE's perspective.
9453
+ */
9454
+ async function startWasmSession({ client_key, wasm_md5, reset, }) {
9455
+ const res = await request({
9456
+ url: `${WASM_COLLECT_BASE_URL}/start`,
9220
9457
  method: 'POST',
9221
9458
  data: {
9222
- client_key,
9459
+ app_id: client_key,
9223
9460
  wasm_md5,
9461
+ reset: reset ?? false,
9224
9462
  },
9225
- headers: DEV_HEADERS,
9226
9463
  });
9464
+ return {
9465
+ data: res.data
9466
+ ? {
9467
+ code: res.data.code ?? 0,
9468
+ message: res.data.message || 'success',
9469
+ result: {
9470
+ collect_state: res.data.collect_state,
9471
+ started_at: res.data.started_at,
9472
+ },
9473
+ }
9474
+ : null,
9475
+ error: res.error,
9476
+ ctx: res.ctx,
9477
+ };
9227
9478
  }
9228
9479
 
9229
- async function getCollecttingInfo({ client_key, wasm_md5, }) {
9230
- return request({
9231
- url: `${BASE_URL}/api/stark_wasm/v4/get/funccollect`,
9480
+ /**
9481
+ * "开始收集" 的本地 pipeline 实现。语义上等价于老远程流程的
9482
+ * `stark_wasm/v4/post/set_collecting`:**打开 server 端的 collect 窗口**,
9483
+ * 让 plugin 之后的 `/report` 请求能落库。
9484
+ *
9485
+ * 做三件事(顺序敏感):
9486
+ * 1. `POST /start` 打开 session —— 失败必须立即返回给 IDE,
9487
+ * 否则 UI 会让用户进"正在收集"但实际 plugin 所有上报都会被 fail-close
9488
+ * 丢弃,场面非常悲伤。
9489
+ * 2. 成功后上传符号表(`/symbols`)。这一步故意不 await、错误仅 warn —
9490
+ * 符号表只是给 server 端后续调试用的 debug 信息,丢了也不影响分包主链路。
9491
+ * 3. 返回 `{code: 0}`。
9492
+ *
9493
+ * 两种调用语义(与 `wasm_api.md` §5.1 对齐):
9494
+ * - 默认(`resume` 缺省 / false)—— 用户点"开始收集 / 重新开始分包",
9495
+ * 发 `reset: true`,服务端清空历史。这是历史行为,对应 IDE 上的
9496
+ * "willCollect → startCollect" 主入口。
9497
+ * - `resume: true` —— 页面刷新 / 恢复继续,发 `reset: false`,幂等
9498
+ * 打开 session、保留已有 func_ids。需要这条路径的 caller(如 IDE
9499
+ * 重新挂载组件检测到 server `collect_state: "open"` 想接续)必须
9500
+ * 显式传,避免误清。
9501
+ *
9502
+ * Session 生命周期对前端透明——IDE 只知道"开始收集 / 完成收集"两个动作,
9503
+ * `/start` 和 `/finish` 都被封在本地 dispatcher 内。这样远程 pipeline(没有
9504
+ * session 概念)和本地 pipeline(有 session)在 IDE 层看起来是对称的。
9505
+ */
9506
+ async function setCollect$1({ client_key, wasm_md5, resume, }) {
9507
+ const startRes = await startWasmSession({
9508
+ client_key,
9509
+ wasm_md5,
9510
+ reset: !resume,
9511
+ });
9512
+ if (startRes.error || !startRes.data || startRes.data.code !== 0) {
9513
+ // /start is invoked internally by setCollect now; IDE only sees this
9514
+ // bubbled up as a generic "开始收集失败" toast, so dump a structured
9515
+ // one-liner here with logid — the single most useful field when
9516
+ // asking backend to look up what happened on their side.
9517
+ const code = startRes.error?.code ?? startRes.data?.code ?? -1;
9518
+ const message = startRes.error?.message ||
9519
+ startRes.data?.message ||
9520
+ 'Open collect session failed';
9521
+ const logid = startRes.ctx?.logid || 'n/a';
9522
+ console.error(`[wasm-collect] /start failed: code=${code} message=${message} logid=${logid}`);
9523
+ return {
9524
+ data: startRes.data ?? null,
9525
+ error: startRes.error ?? { code, message },
9526
+ ctx: startRes.ctx,
9527
+ };
9528
+ }
9529
+ let symbolPath = path$1.join(process.cwd(), WASM_SYMBOL_FILE_NAME);
9530
+ if (!fs$1.existsSync(symbolPath)) {
9531
+ symbolPath = path$1.join(process.cwd(), TTMG_TEMP_DIR, WASM_SYMBOL_FILE_NAME);
9532
+ }
9533
+ if (fs$1.existsSync(symbolPath)) {
9534
+ const symbols = fs$1.readFileSync(symbolPath, 'utf-8');
9535
+ request({
9536
+ url: `${WASM_COLLECT_BASE_URL}/symbols`,
9537
+ method: 'POST',
9538
+ data: {
9539
+ app_id: client_key,
9540
+ wasm_md5,
9541
+ symbols,
9542
+ },
9543
+ }).catch(err => {
9544
+ console.warn('[wasmtool] Failed to upload symbols:', err);
9545
+ });
9546
+ }
9547
+ return {
9548
+ data: { code: 0, message: 'success', result: {} },
9549
+ error: null,
9550
+ ctx: { logid: 'local', httpStatusCode: 200 },
9551
+ };
9552
+ }
9553
+
9554
+ async function getCollecttingInfo$1({ client_key, wasm_md5, }) {
9555
+ const res = await request({
9556
+ url: `${WASM_COLLECT_BASE_URL}/progress`,
9232
9557
  method: 'GET',
9233
- headers: DEV_HEADERS,
9234
9558
  params: {
9235
- client_key,
9559
+ app_id: client_key,
9236
9560
  wasm_md5,
9237
9561
  },
9238
9562
  });
9563
+ const { totalWasmFuncCount } = getLocalState();
9564
+ // Fall back to game.json.wasmFuncCount so the total survives CLI restarts.
9565
+ const gameJsonFuncCount = Number(getGameJson()?.wasmFuncCount) || 0;
9566
+ return {
9567
+ data: {
9568
+ code: res?.data?.code ?? 0,
9569
+ message: 'success',
9570
+ result: {
9571
+ app_id: client_key,
9572
+ wasm_md5,
9573
+ collected_func_count: res?.data?.func_count ?? 0,
9574
+ total_wasm_func_count: gameJsonFuncCount || totalWasmFuncCount || 0,
9575
+ collect_state: res?.data?.collect_state,
9576
+ },
9577
+ },
9578
+ error: res.error,
9579
+ ctx: res.ctx,
9580
+ };
9239
9581
  }
9240
9582
 
9241
- // /api/stark_wasm/v4/post/split
9242
- async function startSplit({ client_key, wasm_md5, }) {
9243
- return request({
9244
- url: `${BASE_URL}/api/stark_wasm/v4/post/split`,
9583
+ /**
9584
+ * POST /finish closes a collect session and returns the final `func_count`
9585
+ * so the IDE can surface "本次共收集 N 个函数" in the success dialog.
9586
+ * Idempotent on the server.
9587
+ *
9588
+ * NOTE on naming: the server route is flat (`/finish`, not `/session/finish`).
9589
+ * The local symbol keeps `finishWasmSession` for symmetry with `startWasmSession`.
9590
+ */
9591
+ async function finishWasmSession({ client_key, wasm_md5, }) {
9592
+ const res = await request({
9593
+ url: `${WASM_COLLECT_BASE_URL}/finish`,
9245
9594
  method: 'POST',
9246
- headers: {
9247
- ...DEV_HEADERS,
9248
- },
9249
9595
  data: {
9250
- client_key,
9596
+ app_id: client_key,
9251
9597
  wasm_md5,
9252
9598
  },
9253
9599
  });
9600
+ return {
9601
+ data: res.data
9602
+ ? {
9603
+ code: res.data.code ?? 0,
9604
+ message: res.data.message || 'success',
9605
+ result: {
9606
+ collect_state: res.data.collect_state,
9607
+ func_count: res.data.func_count ?? 0,
9608
+ finished_at: res.data.finished_at,
9609
+ },
9610
+ }
9611
+ : null,
9612
+ error: res.error,
9613
+ ctx: res.ctx,
9614
+ };
9254
9615
  }
9255
9616
 
9256
- /*
9257
- How it works:
9258
- `this.#head` is an instance of `Node` which keeps track of its current value and nests another instance of `Node` that keeps the value that comes after it. When a value is provided to `.enqueue()`, the code needs to iterate through `this.#head`, going deeper and deeper to find the last value. However, iterating through every single item is slow. This problem is solved by saving a reference to the last value as `this.#tail` so that it can reference it to add a new value.
9259
- */
9260
-
9261
- class Node {
9262
- value;
9263
- next;
9264
-
9265
- constructor(value) {
9266
- this.value = value;
9267
- }
9268
- }
9269
-
9270
- class Queue {
9617
+ async function startSplit$1({ client_key, wasm_md5, }) {
9618
+ const tempDir = path$1.join(process.cwd(), TTMG_TEMP_DIR);
9619
+ const splitOutputDir = path$1.join(tempDir, 'split-output');
9620
+ if (fs$1.existsSync(splitOutputDir)) {
9621
+ fs$1.rmSync(splitOutputDir, { recursive: true, force: true });
9622
+ }
9623
+ ensureDirSync(splitOutputDir);
9624
+ const { originalWasmPath, isArchiveMode: archive } = getLocalState();
9625
+ const rawWasmPath = originalWasmPath || path$1.join(tempDir, 'original.wasm');
9626
+ if (!fs$1.existsSync(rawWasmPath)) {
9627
+ return {
9628
+ data: null,
9629
+ error: {
9630
+ code: 404,
9631
+ message: 'Original wasm not found. Run prepare first.',
9632
+ },
9633
+ ctx: { logid: 'local', httpStatusCode: 404 },
9634
+ };
9635
+ }
9636
+ const exportRes = await request({
9637
+ url: `${WASM_COLLECT_BASE_URL}/export`,
9638
+ method: 'GET',
9639
+ params: {
9640
+ app_id: client_key,
9641
+ wasm_md5,
9642
+ strategy: 'union',
9643
+ },
9644
+ });
9645
+ const funcIds = exportRes?.data?.func_ids;
9646
+ const bootFuncIds = exportRes?.data?.boot_func_ids ?? [];
9647
+ if (!funcIds?.length) {
9648
+ return {
9649
+ data: null,
9650
+ error: {
9651
+ code: 400,
9652
+ message: 'No collected func IDs found.',
9653
+ },
9654
+ ctx: { logid: 'local', httpStatusCode: 400 },
9655
+ };
9656
+ }
9657
+ console.log(`[wasmtool] splitting with ${funcIds.length} func IDs` +
9658
+ (bootFuncIds.length > 0
9659
+ ? `, ${bootFuncIds.length} boot-phase func IDs (→ alwaysInclude)`
9660
+ : ', no boot-phase info (legacy server, falling back to callClosure only)') +
9661
+ `, archive=${archive}`);
9662
+ try {
9663
+ const result = ttmgWasmtool.split({
9664
+ input: rawWasmPath,
9665
+ funcIds,
9666
+ // Boot-phase func ids → `alwaysInclude`. They are a subset of
9667
+ // `funcIds` so this doesn't grow `collect_count`, but it DOES seed
9668
+ // the direct-call closure BFS with the exact set needed for first
9669
+ // frame, and the split tool's `alwaysIncludeAdded` counter is the
9670
+ // observability signal when zero (= server didn't return boot info).
9671
+ alwaysInclude: bootFuncIds.length > 0 ? bootFuncIds : undefined,
9672
+ // Always-on direct-call closure over (collect ∪ alwaysInclude ∪
9673
+ // start_func). Folds in func ids that collect missed (untaken
9674
+ // branches, race conditions during collect) so first-screen code
9675
+ // paths don't trap on archive trampolines. See the split tool's
9676
+ // `closure_added` counter for the per-build size impact.
9677
+ callClosure: true,
9678
+ // Always-on indirect-call type-closure scoped to the boot subset.
9679
+ // Catches IL2CPP virtual / interface / delegate dispatch which is
9680
+ // the dominant source of remaining `firstFrame=BEFORE` archive
9681
+ // trampoline hits after the runtime collect + direct closure
9682
+ // passes (see `indirectClosureAdded` for the per-build size
9683
+ // impact). Defaults to `true` in the wasmtool but we set it
9684
+ // explicitly so a future tool default change can't silently turn
9685
+ // it off in our pipeline.
9686
+ callIndirectClosure: true,
9687
+ outputDir: splitOutputDir,
9688
+ archive,
9689
+ compress: true,
9690
+ quality: 9,
9691
+ });
9692
+ if (result.code !== 0) {
9693
+ return {
9694
+ data: null,
9695
+ error: { code: result.code, message: result.errMsg },
9696
+ ctx: { logid: 'local', httpStatusCode: 500 },
9697
+ };
9698
+ }
9699
+ const mainBrPath = result.mainWasmPath + '.br';
9700
+ const actualMainPath = fs$1.existsSync(mainBrPath)
9701
+ ? mainBrPath
9702
+ : result.mainWasmPath;
9703
+ const mainWasmMd5 = computeFileMd5Sync(actualMainPath);
9704
+ const subBrPath = result.subWasmPath ? result.subWasmPath + '.br' : '';
9705
+ const actualSubPath = subBrPath && fs$1.existsSync(subBrPath)
9706
+ ? subBrPath
9707
+ : result.subWasmPath;
9708
+ const subWasmMd5 = actualSubPath
9709
+ ? computeFileMd5Sync(actualSubPath)
9710
+ : '';
9711
+ let archiveMd5 = '';
9712
+ if (archive && result.archivePath) {
9713
+ const archiveBrPath = result.archivePath + '.br';
9714
+ const actualArchivePath = fs$1.existsSync(archiveBrPath)
9715
+ ? archiveBrPath
9716
+ : result.archivePath;
9717
+ console.log(`[wasmtool] archivePath=${result.archivePath}, brExists=${fs$1.existsSync(archiveBrPath)}, actualExists=${fs$1.existsSync(actualArchivePath)}`);
9718
+ if (fs$1.existsSync(actualArchivePath)) {
9719
+ archiveMd5 = computeFileMd5Sync(actualArchivePath);
9720
+ console.log(`[wasmtool] archive_md5=${archiveMd5}`);
9721
+ }
9722
+ }
9723
+ else {
9724
+ console.log(`[wasmtool] skip archive md5: archive=${archive}, archivePath=${result.archivePath}`);
9725
+ }
9726
+ const globalVarList = result.globalVarList
9727
+ .split(';')
9728
+ .filter(Boolean)
9729
+ .map((entry) => {
9730
+ const [name, type, mutable] = entry.trim().split(',');
9731
+ return { name, type, mutable: mutable === '1' };
9732
+ });
9733
+ const splitMeta = {
9734
+ original_wasm_md5: wasm_md5,
9735
+ main_wasm_md5: mainWasmMd5,
9736
+ main_wasm_h5_md5: mainWasmMd5,
9737
+ sub_wasm_md5: subWasmMd5,
9738
+ archive_md5: archiveMd5,
9739
+ table_size: result.tableSize,
9740
+ global_var_list: globalVarList,
9741
+ version: Date.now(),
9742
+ total_wasm_count: result.totalWasmCount,
9743
+ main_wasm_count: result.mainWasmCount,
9744
+ time_cost: result.timeCost,
9745
+ archive,
9746
+ local_main_wasm_path: result.mainWasmPath,
9747
+ local_sub_wasm_path: result.subWasmPath,
9748
+ local_func_meta_path: result.funcMetaPath,
9749
+ local_archive_path: result.archivePath,
9750
+ // Composition breakdown of main_funcs — the single most useful
9751
+ // piece of information when triaging "why is my main package X MB"
9752
+ // (or, conversely, "why are first-screen sub-package batches still
9753
+ // loading"). collect = runtime-observed, always_include =
9754
+ // boot_func_ids, closure = BFS direct callees, indirect_closure =
9755
+ // type-matching pass scoped to boot funcs (covers IL2CPP virtual
9756
+ // dispatch), export = wasm exports. These sum with imports to
9757
+ // main_wasm_count.
9758
+ collect_func_count: result.collectFuncCount,
9759
+ always_include_added: result.alwaysIncludeAdded,
9760
+ closure_added: result.closureAdded,
9761
+ indirect_closure_added: result.indirectClosureAdded,
9762
+ indirect_closure_types: result.indirectClosureTypes,
9763
+ export_added: result.exportAdded,
9764
+ };
9765
+ setLocalState({ splitOutputDir, splitMeta });
9766
+ console.log(`[wasmtool] split done: total=${result.totalWasmCount}, main=${result.mainWasmCount} ` +
9767
+ `(collect=${result.collectFuncCount}, +alwaysInclude=${result.alwaysIncludeAdded}, ` +
9768
+ `+closure=${result.closureAdded}, +indirectClosure=${result.indirectClosureAdded}` +
9769
+ `[types=${result.indirectClosureTypes}], +exports=${result.exportAdded}), ` +
9770
+ `time=${result.timeCost}s`);
9771
+ // Split landed — close the collect session so the plugin stops reporting.
9772
+ // Awaited (not fire-and-forget) so IDE can rely on "wasm-split returned
9773
+ // success" meaning "session definitively closed". If /finish itself
9774
+ // fails (e.g. portal cookie expired mid-run) we still return split
9775
+ // success to the IDE — the plugin already has the MD5-bound session
9776
+ // state from the earlier /report responses and will time out on TTL
9777
+ // anyway; failing split for a finalizer hiccup would be worse UX.
9778
+ let funcCount;
9779
+ try {
9780
+ const finishRes = await finishWasmSession({ client_key, wasm_md5 });
9781
+ if (finishRes.error || !finishRes.data || finishRes.data.code !== 0) {
9782
+ // Soft failure: split already succeeded from the user's POV, but
9783
+ // this is the main diagnostic breadcrumb if someone later reports
9784
+ // "plugin kept uploading after 分包完成". Always include logid so
9785
+ // backend can cross-reference without having to know our build.
9786
+ const code = finishRes.error?.code ?? finishRes.data?.code ?? -1;
9787
+ const message = finishRes.error?.message ||
9788
+ finishRes.data?.message ||
9789
+ 'finish session non-success';
9790
+ const logid = finishRes.ctx?.logid || 'n/a';
9791
+ console.error(`[wasm-split] /finish failed (split still succeeded): code=${code} message=${message} logid=${logid}`);
9792
+ }
9793
+ else {
9794
+ funcCount = finishRes.data.result?.func_count;
9795
+ }
9796
+ }
9797
+ catch (e) {
9798
+ const msg = e instanceof Error ? e.message : String(e);
9799
+ console.error(`[wasm-split] /finish threw (split still succeeded): ${msg}`);
9800
+ }
9801
+ return {
9802
+ data: { code: 0, message: 'success', func_count: funcCount },
9803
+ error: null,
9804
+ ctx: { logid: 'local', httpStatusCode: 200 },
9805
+ };
9806
+ }
9807
+ catch (err) {
9808
+ return {
9809
+ data: null,
9810
+ error: {
9811
+ code: 500,
9812
+ message: err instanceof Error ? err.message : String(err),
9813
+ },
9814
+ ctx: { logid: 'local', httpStatusCode: 500 },
9815
+ };
9816
+ }
9817
+ }
9818
+
9819
+ const ARCHIVE_SUBPACKAGE_CONFIG = [
9820
+ { name: 'wasmcode', root: 'wasmcode/' },
9821
+ { name: 'wasmcode1', root: 'wasmcode1/' },
9822
+ { name: 'wasmcode-archive', root: 'wasmcode-archive/' },
9823
+ ];
9824
+ function updateSubpackageConfigSync(archive = false) {
9825
+ const gameJsonPath = path__namespace.join(process.cwd(), SUBPACKAGE_CONFIG_FILE_NAME);
9826
+ const raw = fs__namespace.readFileSync(gameJsonPath, 'utf-8');
9827
+ const gameJson = JSON.parse(raw);
9828
+ delete gameJson.wasmFuncCount;
9829
+ const fieldName = SUBPACKAGE_FIELD_NAMES.find(k => k in gameJson) ??
9830
+ SUBPACKAGE_FIELD_NAMES[0];
9831
+ if (!gameJson[fieldName])
9832
+ gameJson[fieldName] = [];
9833
+ const subpackages = gameJson[fieldName];
9834
+ const filtered = subpackages.filter(s => s.name !== WASM_SPLIT_SUBPACKAGE_CONFIG.origin.name);
9835
+ if (archive) {
9836
+ ARCHIVE_SUBPACKAGE_CONFIG.forEach(pkg => filtered.push(pkg));
9837
+ }
9838
+ else {
9839
+ filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain);
9840
+ filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub);
9841
+ filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.iosMain);
9842
+ filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.iosSub);
9843
+ }
9844
+ const map = new Map(filtered.map(s => [s.name, s]));
9845
+ gameJson[fieldName] = Array.from(map.values());
9846
+ fs__namespace.writeFileSync(gameJsonPath, JSON.stringify(gameJson, null, JSON_INDENT) + JSON_EOL);
9847
+ }
9848
+
9849
+ async function downloadSplited$1(_context) {
9850
+ const cwd = process.cwd();
9851
+ const { splitMeta } = getLocalState();
9852
+ if (!splitMeta) {
9853
+ return {
9854
+ data: { isSuccess: false },
9855
+ error: { message: 'No local split result found. Run split first.' },
9856
+ ctx: _context,
9857
+ };
9858
+ }
9859
+ const splitTempDir = path.join(cwd, WASM_SPLIT_CACHE_DIR, DIR_SPLIT);
9860
+ ensureDirSync(splitTempDir);
9861
+ const isArchive = splitMeta.archive;
9862
+ const mainAndroidDir = path.join(splitTempDir, isArchive ? 'wasmcode' : WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain.root);
9863
+ const subAndroidDir = path.join(splitTempDir, isArchive ? 'wasmcode1' : WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub.root);
9864
+ const mainIosDir = isArchive
9865
+ ? mainAndroidDir
9866
+ : path.join(splitTempDir, WASM_SPLIT_SUBPACKAGE_CONFIG.iosMain.root);
9867
+ const subIosDir = path.join(splitTempDir, isArchive ? 'wasmcode-archive' : WASM_SPLIT_SUBPACKAGE_CONFIG.iosSub.root);
9868
+ const dirs = [...new Set([mainAndroidDir, subAndroidDir, mainIosDir, subIosDir])];
9869
+ dirs.forEach(ensureDirSync);
9870
+ try {
9871
+ console.log('[wasmtool] organizing split output...');
9872
+ const mainWasmMd5 = splitMeta.main_wasm_md5;
9873
+ const subWasmMd5 = splitMeta.sub_wasm_md5;
9874
+ const mainWasmH5Md5 = splitMeta.main_wasm_h5_md5;
9875
+ const localMainPath = splitMeta.local_main_wasm_path;
9876
+ const mainBrPath = localMainPath + BR_SUFFIX;
9877
+ const actualMainPath = fs.existsSync(mainBrPath) ? mainBrPath : localMainPath;
9878
+ if (actualMainPath && fs.existsSync(actualMainPath)) {
9879
+ const isBr = actualMainPath.endsWith(BR_SUFFIX);
9880
+ const ext = isBr
9881
+ ? `${WASM_FILENAME_SUFFIX}${BR_SUFFIX}`
9882
+ : WASM_FILENAME_SUFFIX;
9883
+ const mainAndroidDest = path.join(mainAndroidDir, `${mainWasmMd5}${ext}`);
9884
+ fs.copyFileSync(actualMainPath, mainAndroidDest);
9885
+ wsServer.sendUnitySplitStatus({
9886
+ status: 'download_android_main_wasm_done',
9887
+ });
9888
+ if (mainIosDir !== mainAndroidDir) {
9889
+ const mainIosDest = path.join(mainIosDir, `${mainWasmH5Md5}${ext}`);
9890
+ fs.copyFileSync(actualMainPath, mainIosDest);
9891
+ }
9892
+ wsServer.sendUnitySplitStatus({
9893
+ status: 'download_ios_main_wasm_done',
9894
+ });
9895
+ }
9896
+ const localSubPath = splitMeta.local_sub_wasm_path;
9897
+ const subBrPath = localSubPath + BR_SUFFIX;
9898
+ const actualSubPath = fs.existsSync(subBrPath) ? subBrPath : localSubPath;
9899
+ if (actualSubPath && fs.existsSync(actualSubPath)) {
9900
+ const isBr = actualSubPath.endsWith(BR_SUFFIX);
9901
+ const ext = isBr
9902
+ ? `${WASM_FILENAME_SUFFIX}${BR_SUFFIX}`
9903
+ : WASM_FILENAME_SUFFIX;
9904
+ const subAndroidDest = path.join(subAndroidDir, `${subWasmMd5}${ext}`);
9905
+ fs.copyFileSync(actualSubPath, subAndroidDest);
9906
+ wsServer.sendUnitySplitStatus({
9907
+ status: 'download_android_sub_wasm_code_done',
9908
+ });
9909
+ }
9910
+ const localArchivePath = splitMeta.local_archive_path;
9911
+ if (isArchive && localArchivePath) {
9912
+ const archiveBrPath = localArchivePath + BR_SUFFIX;
9913
+ const actualArchivePath = fs.existsSync(archiveBrPath) ? archiveBrPath : localArchivePath;
9914
+ console.log(`[wasmtool] archive copy: archive_md5=${splitMeta.archive_md5}, localPath=${localArchivePath}, brExists=${fs.existsSync(archiveBrPath)}, actual=${actualArchivePath}`);
9915
+ if (fs.existsSync(actualArchivePath)) {
9916
+ const archiveMd5 = splitMeta.archive_md5 || '';
9917
+ const archiveBaseName = path.basename(actualArchivePath);
9918
+ const destName = archiveMd5 ? `${archiveMd5}.${archiveBaseName}` : archiveBaseName;
9919
+ const archiveDest = path.join(subIosDir, destName);
9920
+ console.log(`[wasmtool] archive dest: ${archiveDest}`);
9921
+ fs.copyFileSync(actualArchivePath, archiveDest);
9922
+ }
9923
+ }
9924
+ dirs.forEach((dir) => {
9925
+ fs.writeFileSync(path.join(dir, 'game.js'), '', { encoding: 'utf-8' });
9926
+ });
9927
+ console.log('[wasmtool] copy split output to root...');
9928
+ wsServer.sendUnitySplitStatus({ status: 'start_write_splited_wasm_br' });
9929
+ for (const file of fs.readdirSync(splitTempDir)) {
9930
+ const srcPath = path.join(splitTempDir, file);
9931
+ const destPath = path.join(cwd, file);
9932
+ if (fs.existsSync(destPath)) {
9933
+ await promises.rm(destPath, { recursive: true, force: true });
9934
+ }
9935
+ await promises.cp(srcPath, destPath, { recursive: true, force: true });
9936
+ }
9937
+ wsServer.sendUnitySplitStatus({ status: 'write_splited_wasm_done' });
9938
+ console.log('[wasmtool] updating subpackage config...');
9939
+ updateSubpackageConfigSync(isArchive);
9940
+ console.log('[wasmtool] updating wasm split config...');
9941
+ wsServer.sendUnitySplitStatus({ status: 'start_update_wasm_split_config' });
9942
+ updateWasmSplitConfig({
9943
+ ENABLEWASMCOLLECT: true,
9944
+ ORIGINALWASMMD5: `${splitMeta.original_wasm_md5}`,
9945
+ WASMTABLESIZE: splitMeta.table_size,
9946
+ GLOBALVARLIST: JSON.stringify(splitMeta.global_var_list ?? []),
9947
+ SUBJSURL: '',
9948
+ IOS_CODE_FILE_MD5: `${splitMeta.main_wasm_h5_md5}`,
9949
+ ANDROID_CODE_FILE_MD5: `${splitMeta.main_wasm_md5}`,
9950
+ ANDROID_SUB_CODE_FILE_MD5: `${splitMeta.sub_wasm_md5}`,
9951
+ ARCHIVE_CODE_FILE_MD5: `${splitMeta.archive_md5 || ''}`,
9952
+ WASMSPLITVERSION: `${splitMeta.version}`,
9953
+ USINGWASMH5: Boolean(splitMeta.main_wasm_h5_md5),
9954
+ ENABLEWASMSPLIT: true,
9955
+ ENABLEARCHIVEMODE: isArchive,
9956
+ });
9957
+ wsServer.sendUnitySplitStatus({ status: 'update_wasm_split_config_done' });
9958
+ return {
9959
+ data: { isSuccess: true },
9960
+ ctx: splitMeta,
9961
+ };
9962
+ }
9963
+ catch (err) {
9964
+ wsServer.sendUnitySplitStatus({
9965
+ status: 'wasm_split_failed',
9966
+ errorMsg: err instanceof Error ? err.message : String(err),
9967
+ });
9968
+ return {
9969
+ data: { isSuccess: false },
9970
+ error: { message: err instanceof Error ? err.message : String(err) },
9971
+ ctx: splitMeta,
9972
+ };
9973
+ }
9974
+ finally {
9975
+ await promises.rm(splitTempDir, { recursive: true, force: true });
9976
+ if (!isArchive) {
9977
+ await promises.rm(path.join(cwd, WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root), {
9978
+ recursive: true,
9979
+ force: true,
9980
+ });
9981
+ }
9982
+ }
9983
+ }
9984
+
9985
+ /*
9986
+ How it works:
9987
+ `this.#head` is an instance of `Node` which keeps track of its current value and nests another instance of `Node` that keeps the value that comes after it. When a value is provided to `.enqueue()`, the code needs to iterate through `this.#head`, going deeper and deeper to find the last value. However, iterating through every single item is slow. This problem is solved by saving a reference to the last value as `this.#tail` so that it can reference it to add a new value.
9988
+ */
9989
+
9990
+ class Node {
9991
+ value;
9992
+ next;
9993
+
9994
+ constructor(value) {
9995
+ this.value = value;
9996
+ }
9997
+ }
9998
+
9999
+ class Queue {
9271
10000
  #head;
9272
10001
  #tail;
9273
10002
  #size;
@@ -9419,70 +10148,28 @@ function pLimit(concurrency) {
9419
10148
  return generator;
9420
10149
  }
9421
10150
 
9422
- function updateSubpackageConfigSync() {
9423
- const gameJsonPath = path__namespace.join(process.cwd(), SUBPACKAGE_CONFIG_FILE_NAME);
9424
- const raw = fs__namespace.readFileSync(gameJsonPath, 'utf-8');
9425
- const gameJson = JSON.parse(raw);
9426
- const fieldName = SUBPACKAGE_FIELD_NAMES.find(k => k in gameJson) ??
9427
- SUBPACKAGE_FIELD_NAMES[0];
9428
- if (!gameJson[fieldName])
9429
- gameJson[fieldName] = [];
9430
- const subpackages = gameJson[fieldName];
9431
- // 删除老的 'wasmcode'
9432
- const filtered = subpackages.filter(s => s.name !== WASM_SPLIT_SUBPACKAGE_CONFIG.origin.name);
9433
- /**
9434
- * 基于 SUBPACKAGE_CONFIG_FILE_NAME 更新 subpackages
9435
- */
9436
- filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain);
9437
- filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.androidSub);
9438
- filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.iosMain);
9439
- filtered.push(WASM_SPLIT_SUBPACKAGE_CONFIG.iosSub);
9440
- // 合并去重:存在则更新 root,不存在则新增
9441
- const map = new Map(filtered.map(s => [s.name, s]));
9442
- gameJson[fieldName] = Array.from(map.values());
9443
- fs__namespace.writeFileSync(gameJsonPath, JSON.stringify(gameJson, null, JSON_INDENT) + JSON_EOL);
9444
- }
9445
-
9446
- async function downloadAndCompress(opts) {
9447
- const { startDownloadStatus, downloadDoneStatus, startCompressStatus, compressDoneStatus, url, out, enableCompress = false, } = opts;
10151
+ async function downloadOne(opts) {
10152
+ const { startStatus, doneStatus, url, out } = opts;
9448
10153
  if (!url)
9449
10154
  return;
9450
10155
  const willDownloadedFileIsBr = url.includes(BR_SUFFIX);
9451
- const wasmBrOutName = willDownloadedFileIsBr ? out + BR_SUFFIX : out;
9452
- // 下载
9453
- wsServer.sendUnitySplitStatus({ status: startDownloadStatus });
9454
- console.log(`download url: ${url}`);
10156
+ const finalOut = willDownloadedFileIsBr && !out.endsWith(BR_SUFFIX) ? out + BR_SUFFIX : out;
10157
+ wsServer.sendUnitySplitStatus({ status: startStatus });
10158
+ console.log(`[remote-split-download] fetching -> ${finalOut}`);
9455
10159
  const t0 = Date.now();
9456
- await withRetry(() => download(url, wasmBrOutName), DOWNLOAD_RETRY);
9457
- try {
9458
- const st = await promises.stat(wasmBrOutName);
9459
- if (!st.size)
9460
- throw new Error(`Empty download: ${wasmBrOutName}`);
9461
- wsServer.sendUnitySplitStatus({ status: downloadDoneStatus, url });
9462
- console.log(`download done: ${path.basename(wasmBrOutName)} size=${st.size}B time=${Date.now() - t0}ms`);
9463
- }
9464
- catch (e) {
9465
- await promises.rm(wasmBrOutName);
9466
- throw e;
9467
- }
9468
- if (enableCompress) {
9469
- console.log(`compress start: ${path.basename(out)}${BR_SUFFIX}`);
9470
- // 压缩
9471
- wsServer.sendUnitySplitStatus({ status: startCompressStatus });
9472
- const t1 = Date.now();
9473
- await compressWasmFile(out, wasmBrOutName);
9474
- wsServer.sendUnitySplitStatus({ status: compressDoneStatus });
9475
- console.log(`compress done: ${path.basename(wasmBrOutName)} time=${Date.now() - t1}ms`);
9476
- }
9477
- /**
9478
- * 在当前文件所在目录下写入一个空的 game.js
9479
- */
9480
- fs__namespace.writeFileSync(path.join(path.dirname(out), 'game.js'), '', {
9481
- encoding: 'utf-8',
9482
- });
10160
+ await withRetry(() => download(url, finalOut), DOWNLOAD_RETRY);
10161
+ const st = await promises.stat(finalOut);
10162
+ if (!st.size) {
10163
+ await promises.rm(finalOut, { force: true });
10164
+ throw new Error(`Empty download: ${finalOut}`);
10165
+ }
10166
+ console.log(`[remote-split-download] done: ${path.basename(finalOut)} size=${st.size}B time=${Date.now() - t0}ms`);
10167
+ wsServer.sendUnitySplitStatus({ status: doneStatus, url });
10168
+ // Legacy behaviour: write an empty game.js next to each downloaded artifact
10169
+ // so the subpackage loader doesn't complain about missing js entries.
10170
+ fs.writeFileSync(path.join(path.dirname(out), 'game.js'), '', 'utf-8');
9483
10171
  }
9484
-
9485
- async function downloadSplited(context) {
10172
+ async function downloadSplitedRemote(context) {
9486
10173
  const cwd = process.cwd();
9487
10174
  const splitTempDir = path.join(cwd, WASM_SPLIT_CACHE_DIR, DIR_SPLIT);
9488
10175
  ensureDirSync(splitTempDir);
@@ -9491,144 +10178,124 @@ async function downloadSplited(context) {
9491
10178
  const mainIosDir = path.join(splitTempDir, WASM_SPLIT_SUBPACKAGE_CONFIG.iosMain.root);
9492
10179
  const subIosDir = path.join(splitTempDir, WASM_SPLIT_SUBPACKAGE_CONFIG.iosSub.root);
9493
10180
  [mainAndroidDir, subAndroidDir, mainIosDir, subIosDir].forEach(ensureDirSync);
9494
- const mainAndroidWasmCodeTempPath = path.join(mainAndroidDir, `${context.main_wasm_md5}${WASM_FILENAME_SUFFIX}`);
9495
- const subAndroidWasmCodeTempPath = path.join(subAndroidDir, `${context.sub_wasm_md5}${WASM_FILENAME_SUFFIX}`);
9496
- const mainIosWasmCodeTempPath = path.join(mainIosDir, `${context.main_wasm_h5_md5}${WASM_FILENAME_SUFFIX}`);
10181
+ const mainAndroidOut = path.join(mainAndroidDir, `${context.main_wasm_md5}${WASM_FILENAME_SUFFIX}`);
10182
+ const subAndroidOut = path.join(subAndroidDir, `${context.sub_wasm_md5}${WASM_FILENAME_SUFFIX}`);
10183
+ const mainIosOut = path.join(mainIosDir, `${context.main_wasm_h5_md5}${WASM_FILENAME_SUFFIX}`);
9497
10184
  const limit = pLimit(CONCURRENCY_LIMIT);
9498
10185
  try {
9499
- console.log('downloadWasmSplit', context);
9500
- // 原有状态文案,按你之前的写法
9501
- wsServer.sendUnitySplitStatus({
9502
- status: 'start_download_android_main_wasm',
9503
- });
9504
- wsServer.sendUnitySplitStatus({
9505
- status: 'start_download_android_sub_wasm_code',
10186
+ console.log('[remote-split-download] start', {
10187
+ original_wasm_md5: context.original_wasm_md5,
10188
+ main_wasm_md5: context.main_wasm_md5,
10189
+ sub_wasm_md5: context.sub_wasm_md5,
10190
+ main_wasm_h5_md5: context.main_wasm_h5_md5,
9506
10191
  });
9507
- wsServer.sendUnitySplitStatus({ status: 'start_download_ios_main_wasm' });
9508
- /**
9509
- * 需要做个保护,只有 有 URL 时才下载
9510
- */
9511
- // 并发下载 + 压缩(带重试)
9512
10192
  await Promise.all([
9513
- limit(() => downloadAndCompress({
9514
- startDownloadStatus: 'start_download_android_main_wasm',
9515
- downloadDoneStatus: 'download_android_main_wasm_done',
9516
- startCompressStatus: 'start_compress_android_main_wasm',
9517
- compressDoneStatus: 'compress_android_main_wasm_done',
10193
+ limit(() => downloadOne({
10194
+ startStatus: 'start_download_android_main_wasm',
10195
+ doneStatus: 'download_android_main_wasm_done',
9518
10196
  url: context.main_wasm_download_url,
9519
- out: mainAndroidWasmCodeTempPath,
10197
+ out: mainAndroidOut,
9520
10198
  })),
9521
- limit(() => downloadAndCompress({
9522
- startDownloadStatus: 'start_download_android_sub_wasm_code',
9523
- downloadDoneStatus: 'download_android_sub_wasm_code_done',
9524
- startCompressStatus: 'start_compress_android_sub_wasm_code',
9525
- compressDoneStatus: 'compress_android_sub_wasm_code_done',
10199
+ limit(() => downloadOne({
10200
+ startStatus: 'start_download_android_sub_wasm_code',
10201
+ doneStatus: 'download_android_sub_wasm_code_done',
9526
10202
  url: context.sub_wasm_download_url,
9527
- out: subAndroidWasmCodeTempPath,
10203
+ out: subAndroidOut,
9528
10204
  })),
9529
- limit(() => downloadAndCompress({
9530
- startDownloadStatus: 'start_download_ios_main_wasm',
9531
- downloadDoneStatus: 'download_ios_main_wasm_done',
9532
- startCompressStatus: 'start_compress_ios_main_wasm',
9533
- compressDoneStatus: 'compress_ios_main_wasm_done',
10205
+ limit(() => downloadOne({
10206
+ startStatus: 'start_download_ios_main_wasm',
10207
+ doneStatus: 'download_ios_main_wasm_done',
9534
10208
  url: context.main_wasm_h5_download_url,
9535
- out: mainIosWasmCodeTempPath,
10209
+ out: mainIosOut,
9536
10210
  })),
9537
- // 下载 ios sub js range json
9538
- limit(() => downloadAndCompress({
9539
- startDownloadStatus: 'start_download_ios_range_json',
9540
- downloadDoneStatus: 'download_ios_range_json_done',
10211
+ limit(() => downloadOne({
10212
+ startStatus: 'start_download_ios_range_json',
10213
+ doneStatus: 'download_ios_range_json_done',
9541
10214
  url: context.sub_js_range_download_url,
9542
10215
  out: path.join(subIosDir, 'func_bytes_range.json'),
9543
10216
  })),
9544
- // 下载 ios sub js data br
9545
- limit(() => downloadAndCompress({
9546
- startDownloadStatus: 'start_download_ios_js_data_br',
9547
- downloadDoneStatus: 'download_ios_js_data_br_done',
10217
+ limit(() => downloadOne({
10218
+ startStatus: 'start_download_ios_js_data_br',
10219
+ doneStatus: 'download_ios_js_data_br_done',
9548
10220
  url: context.sub_js_data_download_url,
9549
10221
  out: path.join(subIosDir, 'subjs.data'),
9550
10222
  })),
9551
10223
  ]);
9552
- // 复制 split/* 到项目根目录(递归、覆盖)——避免 EISDIR
9553
- console.log('copy splitTempDir to root start');
10224
+ console.log('[remote-split-download] copying split output to project root...');
9554
10225
  wsServer.sendUnitySplitStatus({ status: 'start_write_splited_wasm_br' });
9555
10226
  for (const file of fs.readdirSync(splitTempDir)) {
9556
10227
  const srcPath = path.join(splitTempDir, file);
9557
10228
  const destPath = path.join(cwd, file);
9558
- // 如果目标路径有文件或目录,先删除
9559
10229
  if (fs.existsSync(destPath)) {
9560
10230
  await promises.rm(destPath, { recursive: true, force: true });
9561
10231
  }
9562
10232
  await promises.cp(srcPath, destPath, { recursive: true, force: true });
9563
10233
  }
9564
10234
  wsServer.sendUnitySplitStatus({ status: 'write_splited_wasm_done' });
9565
- console.log('copy splitTempDir to root end');
9566
- // 更新分包配置(幂等)
9567
- console.log('updateSubpackageConfigSync start');
9568
- updateSubpackageConfigSync();
9569
- console.log('updateSubpackageConfigSync end');
9570
- // 更新 wasm split 配置(保持原始状态文案)
9571
- console.log('updateWasmSplitConfig start');
10235
+ console.log('[remote-split-download] updating subpackage config...');
10236
+ updateSubpackageConfigSync(false);
10237
+ console.log('[remote-split-download] updating webgl-wasm-split.js...');
9572
10238
  wsServer.sendUnitySplitStatus({ status: 'start_update_wasm_split_config' });
9573
10239
  updateWasmSplitConfig({
9574
10240
  ENABLEWASMCOLLECT: true,
9575
- ORIGINALWASMMD5: `${context.original_wasm_md5}`,
10241
+ ENABLEWASMSPLIT: true,
10242
+ ENABLEARCHIVEMODE: false,
10243
+ ORIGINALWASMMD5: `${context.original_wasm_md5 ?? ''}`,
9576
10244
  WASMTABLESIZE: context.table_size,
9577
10245
  GLOBALVARLIST: JSON.stringify(context.global_var_list ?? []),
9578
- SUBJSURL: `${context.sub_js_download_url}`,
9579
- IOS_CODE_FILE_MD5: `${context.main_wasm_h5_md5}`,
9580
- ANDROID_CODE_FILE_MD5: `${context.main_wasm_md5}`,
9581
- ANDROID_SUB_CODE_FILE_MD5: `${context.sub_wasm_md5}`,
9582
- WASMSPLITVERSION: `${context.version}`,
10246
+ SUBJSURL: `${context.sub_js_download_url ?? ''}`,
10247
+ IOS_CODE_FILE_MD5: `${context.main_wasm_h5_md5 ?? ''}`,
10248
+ ANDROID_CODE_FILE_MD5: `${context.main_wasm_md5 ?? ''}`,
10249
+ ANDROID_SUB_CODE_FILE_MD5: `${context.sub_wasm_md5 ?? ''}`,
10250
+ WASMSPLITVERSION: `${context.version ?? ''}`,
9583
10251
  USINGWASMH5: Boolean(context.main_wasm_h5_md5),
9584
- ENABLEWASMSPLIT: true,
9585
- // IOS_SUB_JS_FILE_CONFIG: JSON.stringify(context.merged_js ?? {}),
9586
10252
  });
9587
10253
  wsServer.sendUnitySplitStatus({ status: 'update_wasm_split_config_done' });
9588
- console.log('updateWasmSplitConfig end');
9589
- return {
9590
- data: {
9591
- isSuccess: true,
9592
- },
9593
- ctx: context,
9594
- };
10254
+ console.log('[remote-split-download] all done');
10255
+ return { data: { isSuccess: true }, ctx: context };
9595
10256
  }
9596
10257
  catch (err) {
9597
- wsServer.sendUnitySplitStatus({
9598
- status: 'wasm_split_failed',
9599
- errorMsg: err instanceof Error ? err.message : String(err),
9600
- });
10258
+ const message = err instanceof Error ? err.message : String(err);
10259
+ console.log('[remote-split-download] failed:', message);
10260
+ wsServer.sendUnitySplitStatus({ status: 'wasm_split_failed', errorMsg: message });
9601
10261
  return {
9602
- data: {
9603
- isSuccess: false,
9604
- },
9605
- error: {
9606
- message: err instanceof Error ? err.message : String(err),
9607
- },
10262
+ data: { isSuccess: false },
10263
+ error: { message },
9608
10264
  ctx: context,
9609
10265
  };
9610
10266
  }
9611
10267
  finally {
9612
- // 清理临时目录与旧 wasmcode 目录
9613
- console.log('delete splitTempDir start');
9614
10268
  await promises.rm(splitTempDir, { recursive: true, force: true });
9615
- console.log('delete splitTempDir end');
9616
- console.log('delete wasmcode start');
10269
+ // Legacy flow: the server-produced `wasmcode/` placeholder at the project
10270
+ // root is no longer needed once we've laid down the 4 platform dirs.
9617
10271
  await promises.rm(path.join(cwd, WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root), {
9618
10272
  recursive: true,
9619
10273
  force: true,
9620
10274
  });
9621
- console.log('delete wasmcode end');
9622
10275
  }
9623
10276
  }
9624
10277
 
9625
- async function getSplitResult({ client_key, wasm_md5, wasm_path, }) {
9626
- return request({
9627
- url: `${BASE_URL}/api/stark_wasm/v4/post/download`,
9628
- method: 'POST',
9629
- headers: { ...DEV_HEADERS },
9630
- data: { client_key, wasm_md5, wasm_path },
9631
- });
10278
+ async function getSplitResult$1(_params) {
10279
+ const { splitMeta } = getLocalState();
10280
+ if (!splitMeta) {
10281
+ return {
10282
+ data: null,
10283
+ error: {
10284
+ code: 404,
10285
+ message: 'No local split result found. Run split first.',
10286
+ },
10287
+ ctx: { logid: 'local', httpStatusCode: 404 },
10288
+ };
10289
+ }
10290
+ return {
10291
+ data: {
10292
+ code: 0,
10293
+ message: 'success',
10294
+ result: splitMeta,
10295
+ },
10296
+ error: null,
10297
+ ctx: { logid: 'local', httpStatusCode: 200 },
10298
+ };
9632
10299
  }
9633
10300
 
9634
10301
  function cancelSplit(params) {
@@ -9668,60 +10335,34 @@ function cancelSplit(params) {
9668
10335
  }
9669
10336
  }
9670
10337
 
9671
- async function resetWasmSplit(data) {
10338
+ async function resetWasmSplit$1(data) {
9672
10339
  const res = await request({
9673
- url: `${BASE_URL}/api/stark_wasm/v4/post/reset`,
10340
+ url: `${WASM_COLLECT_BASE_URL}/reset`,
9674
10341
  method: 'POST',
9675
- headers: {
9676
- ...DEV_HEADERS,
9677
- },
9678
10342
  data: {
9679
- client_key: data.clientkey,
10343
+ app_id: data.clientkey,
9680
10344
  wasm_md5: data.wasmMd5,
9681
10345
  },
9682
10346
  });
9683
- /**
9684
- * 把— __TTMG_TEMP__/wasmcode/ 目录下的所有文件恢复到原本的位置,进行重置
9685
- */
9686
10347
  const cacheDir = path.join(process.cwd(), WASM_SPLIT_CACHE_DIR);
9687
- /**
9688
- * 恢复 br 文件
9689
- */
9690
10348
  if (fs.existsSync(cacheDir)) {
9691
- /**
9692
- * 判断是否有缓存的 br 文件
9693
- */
9694
- /**
9695
- * 判断 cache 文件夹下有没有 .br 文件
9696
- *
9697
- */
9698
10349
  const targetWasmBrPath = fs
9699
10350
  .readdirSync(cacheDir)
9700
10351
  .find(item => item.endsWith('.br'));
9701
10352
  if (targetWasmBrPath) {
9702
10353
  const destWasmBrPath = path.join(process.cwd(), WASM_SPLIT_SUBPACKAGE_CONFIG.origin.root, path.basename(targetWasmBrPath));
9703
- // 规避没有文件夹的情况
9704
10354
  ensureDirSync(path.dirname(destWasmBrPath));
9705
10355
  fs.copyFileSync(path.join(cacheDir, targetWasmBrPath), destWasmBrPath);
9706
10356
  }
9707
10357
  }
9708
- /**
9709
- * 恢复 webgl-wasm-split.js 文件
9710
- */
9711
10358
  const splitConfigCachePath = path.join(cacheDir, WASM_SPLIT_CONFIG_FILE_NAME);
9712
10359
  if (fs.existsSync(splitConfigCachePath)) {
9713
10360
  fs.copyFileSync(splitConfigCachePath, path.join(process.cwd(), WASM_SPLIT_CONFIG_FILE_NAME));
9714
10361
  }
9715
- /**
9716
- * 恢复 game.json 文件
9717
- */
9718
10362
  const gameJsonCachePath = path.join(cacheDir, 'game.json');
9719
10363
  if (fs.existsSync(gameJsonCachePath)) {
9720
10364
  fs.copyFileSync(gameJsonCachePath, path.join(process.cwd(), 'game.json'));
9721
10365
  }
9722
- /**
9723
- * 删除历史分包产物
9724
- */
9725
10366
  const androidSubpackageDir = path.join(process.cwd(), WASM_SPLIT_SUBPACKAGE_CONFIG.androidMain.root);
9726
10367
  if (fs.existsSync(androidSubpackageDir)) {
9727
10368
  fs.rmSync(androidSubpackageDir, { recursive: true });
@@ -9734,6 +10375,15 @@ async function resetWasmSplit(data) {
9734
10375
  if (fs.existsSync(iosSubpackageDir)) {
9735
10376
  fs.rmSync(iosSubpackageDir, { recursive: true });
9736
10377
  }
10378
+ // Drop the prepared-meta anchor as well — rollback restores the
10379
+ // original wasm into the project, so any md5 we previously recorded
10380
+ // for the prepared build is no longer valid. Leaving it behind would
10381
+ // make the split-config drift guard fire on the very next Modal open
10382
+ // and force the user through a redundant prepare cycle.
10383
+ const preparedMetaPath = path.join(process.cwd(), TTMG_TEMP_DIR, 'prepared-meta.json');
10384
+ if (fs.existsSync(preparedMetaPath)) {
10385
+ fs.rmSync(preparedMetaPath, { force: true });
10386
+ }
9737
10387
  return res;
9738
10388
  }
9739
10389
 
@@ -9769,23 +10419,291 @@ function getSplitConfig() {
9769
10419
  }
9770
10420
  }
9771
10421
 
9772
- const getTaskStatus = (params) => {
9773
- return request({
9774
- url: `${BASE_URL}/api/stark_wasm/v4/get/status`,
10422
+ var WasmStatus;
10423
+ (function (WasmStatus) {
10424
+ WasmStatus[WasmStatus["IdleStatus"] = 0] = "IdleStatus";
10425
+ WasmStatus[WasmStatus["WasmPreparingStatus"] = 1] = "WasmPreparingStatus";
10426
+ WasmStatus[WasmStatus["WasmPreparedStatus"] = 2] = "WasmPreparedStatus";
10427
+ WasmStatus[WasmStatus["WasmSplitStatus"] = 3] = "WasmSplitStatus";
10428
+ WasmStatus[WasmStatus["WasmSplittingStatus"] = 4] = "WasmSplittingStatus";
10429
+ WasmStatus[WasmStatus["WasmSplitDoneStatus"] = 5] = "WasmSplitDoneStatus";
10430
+ WasmStatus[WasmStatus["WasmSplitReadyToPrepareStatus"] = 6] = "WasmSplitReadyToPrepareStatus";
10431
+ WasmStatus[WasmStatus["WasmSplitPreparingStatus"] = 7] = "WasmSplitPreparingStatus";
10432
+ WasmStatus[WasmStatus["WasmSplitPreparedStatus"] = 8] = "WasmSplitPreparedStatus";
10433
+ WasmStatus[WasmStatus["WasmCollectingStatus"] = 9] = "WasmCollectingStatus";
10434
+ WasmStatus[WasmStatus["WasmUploadFailStatus"] = -1] = "WasmUploadFailStatus";
10435
+ WasmStatus[WasmStatus["WasmDownloadFailStatus"] = -2] = "WasmDownloadFailStatus";
10436
+ WasmStatus[WasmStatus["WasmFileNotExistStatus"] = -3] = "WasmFileNotExistStatus";
10437
+ WasmStatus[WasmStatus["WasmSplitFailStatus"] = -4] = "WasmSplitFailStatus";
10438
+ WasmStatus[WasmStatus["WasmSplitUpdateDBFailedStatus"] = -5] = "WasmSplitUpdateDBFailedStatus";
10439
+ WasmStatus[WasmStatus["WasmSplitPrepareFailedStatus"] = -6] = "WasmSplitPrepareFailedStatus";
10440
+ })(WasmStatus || (WasmStatus = {}));
10441
+
10442
+ const getTaskStatus$1 = async (params) => {
10443
+ const { preparedWasmPath, splitMeta } = getLocalState();
10444
+ let status = WasmStatus.IdleStatus;
10445
+ if (splitMeta) {
10446
+ status = WasmStatus.WasmSplitDoneStatus;
10447
+ }
10448
+ else if (preparedWasmPath) {
10449
+ status = WasmStatus.WasmSplitPreparedStatus;
10450
+ }
10451
+ return {
10452
+ data: {
10453
+ code: 0,
10454
+ message: 'success',
10455
+ result: {
10456
+ status,
10457
+ wasm_md5: params.wasm_md5,
10458
+ },
10459
+ },
10460
+ error: null,
10461
+ ctx: { logid: 'local', httpStatusCode: 200 },
10462
+ };
10463
+ };
10464
+
10465
+ const getTaskInfo$1 = async (params) => {
10466
+ const res = await request({
10467
+ url: `${WASM_COLLECT_BASE_URL}/progress`,
9775
10468
  method: 'GET',
9776
- headers: DEV_HEADERS,
9777
- params,
10469
+ params: {
10470
+ app_id: params.client_key,
10471
+ wasm_md5: params.wasm_md5,
10472
+ },
9778
10473
  });
10474
+ const { totalWasmFuncCount, preparedWasmPath, wasmSize } = getLocalState();
10475
+ // Prefer game.json as the source of truth so wasm_size / total_wasm_func_count
10476
+ // survive CLI restarts. localState values are only populated during the
10477
+ // prepare step of the current session; after a restart they default to 0.
10478
+ // game.json carries wasmCodeSize/wasmFuncCount emitted at build time, so
10479
+ // re-entering the collect step still shows the correct totals.
10480
+ const gameJson = getGameJson();
10481
+ const gameJsonWasmSize = Number(gameJson?.wasmCodeSize) || 0;
10482
+ const gameJsonFuncCount = Number(gameJson?.wasmFuncCount) || 0;
10483
+ return {
10484
+ data: {
10485
+ code: res?.data?.code ?? 0,
10486
+ message: 'success',
10487
+ result: {
10488
+ app_id: params.client_key,
10489
+ wasm_md5: params.wasm_md5,
10490
+ is_prepared: Boolean(preparedWasmPath),
10491
+ collected_func_count: res?.data?.func_count ?? 0,
10492
+ total_wasm_func_count: gameJsonFuncCount || totalWasmFuncCount || 0,
10493
+ wasm_size: gameJsonWasmSize || wasmSize || 0,
10494
+ },
10495
+ },
10496
+ error: res.error,
10497
+ ctx: res.ctx,
10498
+ };
9779
10499
  };
9780
10500
 
9781
- const getTaskInfo = async (params) => {
10501
+ async function startPrepareRemote(params) {
10502
+ // Back up the original wasm + split config on the first run so cancel/rollback
10503
+ // works even if the user aborts before the server-side prepare finishes.
10504
+ keepCacheSync({
10505
+ entryDir: process.cwd(),
10506
+ originalWasmPath: params.wasm_file_path,
10507
+ originalSplitConfigPath: WASM_SPLIT_CONFIG_FILE_NAME,
10508
+ });
10509
+ const form = new FormData$1();
10510
+ form.append('desc', params.desc);
10511
+ form.append('wasm_md5', params.wasm_md5);
10512
+ form.append('with_ios', 'true');
10513
+ form.append('wasm_file', fs$1.createReadStream(path$1.join(process.cwd(), params.wasm_file_path)), { filename: path$1.basename(params.wasm_file_path), contentType: 'application/wasm' });
10514
+ let symbolFilePath = path$1.join(process.cwd(), TTMG_TEMP_DIR, WASM_SYMBOL_FILE_NAME);
10515
+ if (!fs$1.existsSync(symbolFilePath)) {
10516
+ symbolFilePath = path$1.join(process.cwd(), WASM_SYMBOL_FILE_NAME);
10517
+ }
10518
+ if (!fs$1.existsSync(symbolFilePath)) {
10519
+ return {
10520
+ error: { code: 400, message: `${WASM_SYMBOL_FILE_NAME} not found`, client_key: params.client_key },
10521
+ data: null,
10522
+ ctx: { logid: '', httpStatusCode: 400 },
10523
+ };
10524
+ }
10525
+ form.append('wasm_symbol_file', fs$1.createReadStream(symbolFilePath), {
10526
+ filename: WASM_SYMBOL_FILE_NAME,
10527
+ contentType: 'application/octet-stream',
10528
+ });
10529
+ const formHeaders = form.getHeaders();
10530
+ return request({
10531
+ url: `${BASE_URL}/api/stark_wasm/v4/post/prepare`,
10532
+ method: 'POST',
10533
+ headers: { ...DEV_HEADERS, ...formHeaders },
10534
+ params: { client_key: params.client_key, with_ios: true },
10535
+ data: form,
10536
+ });
10537
+ }
10538
+ async function setCollectRemote({ client_key, wasm_md5 }) {
10539
+ return request({
10540
+ url: `${BASE_URL}/api/stark_wasm/v4/post/set_collecting`,
10541
+ method: 'POST',
10542
+ data: { client_key, wasm_md5 },
10543
+ headers: DEV_HEADERS,
10544
+ });
10545
+ }
10546
+ async function getCollectedFuncIdsRemote({ client_key, wasm_md5 }) {
10547
+ return request({
10548
+ url: `${BASE_URL}/api/stark_wasm/v4/get/collectedfuncids`,
10549
+ method: 'GET',
10550
+ headers: DEV_HEADERS,
10551
+ params: { client_key, wasm_md5 },
10552
+ });
10553
+ }
10554
+ async function getCollecttingInfoRemote({ client_key, wasm_md5 }) {
10555
+ return request({
10556
+ url: `${BASE_URL}/api/stark_wasm/v4/get/funccollect`,
10557
+ method: 'GET',
10558
+ headers: DEV_HEADERS,
10559
+ params: { client_key, wasm_md5 },
10560
+ });
10561
+ }
10562
+ async function startSplitRemote({ client_key, wasm_md5 }) {
10563
+ return request({
10564
+ url: `${BASE_URL}/api/stark_wasm/v4/post/split`,
10565
+ method: 'POST',
10566
+ headers: { ...DEV_HEADERS },
10567
+ data: { client_key, wasm_md5 },
10568
+ });
10569
+ }
10570
+ async function getTaskInfoRemote(params) {
9782
10571
  return request({
9783
10572
  url: `${BASE_URL}/api/stark_wasm/v4/get/taskinfo`,
9784
10573
  method: 'GET',
9785
10574
  headers: DEV_HEADERS,
9786
10575
  params,
9787
10576
  });
9788
- };
10577
+ }
10578
+ async function getTaskStatusRemote(params) {
10579
+ return request({
10580
+ url: `${BASE_URL}/api/stark_wasm/v4/get/status`,
10581
+ method: 'GET',
10582
+ headers: DEV_HEADERS,
10583
+ params,
10584
+ });
10585
+ }
10586
+ async function resetWasmSplitRemote(data) {
10587
+ const res = await request({
10588
+ url: `${BASE_URL}/api/stark_wasm/v4/post/reset`,
10589
+ method: 'POST',
10590
+ headers: { ...DEV_HEADERS },
10591
+ data: { client_key: data.clientkey, wasm_md5: data.wasmMd5 },
10592
+ });
10593
+ // Restore project files (wasm / webgl-wasm-split.js / game.json) so the
10594
+ // next prepare starts from the original placeholders.
10595
+ restoreFromCache();
10596
+ return res;
10597
+ }
10598
+ async function getSplitResultRemote({ client_key, wasm_md5, wasm_path }) {
10599
+ return request({
10600
+ url: `${BASE_URL}/api/stark_wasm/v4/post/download`,
10601
+ method: 'POST',
10602
+ headers: { ...DEV_HEADERS },
10603
+ data: { client_key, wasm_md5, wasm_path },
10604
+ });
10605
+ }
10606
+ /**
10607
+ * Remote pipeline: after the server finishes preparing (instrumenting) the wasm,
10608
+ * fetch the download URL, download the prepared wasm, replace the project file,
10609
+ * and update webgl-wasm-split.js for the LEGACY reporting flow.
10610
+ */
10611
+ async function downloadPreparedRemote(data) {
10612
+ wsServer.sendUnitySplitStatus({ status: 'star_fetch_prepared_wasm_url' });
10613
+ const res = await request({
10614
+ url: `${BASE_URL}/api/stark_wasm/v4/post/download_prepared`,
10615
+ method: 'POST',
10616
+ headers: DEV_HEADERS,
10617
+ data,
10618
+ });
10619
+ wsServer.sendUnitySplitStatus({ status: 'fetch_prepared_wasm_url_done' });
10620
+ try {
10621
+ const downloadUrl = res?.data?.result?.download_url;
10622
+ if (!downloadUrl) {
10623
+ console.log('[remote-download-prepared] no download_url in response');
10624
+ return {
10625
+ isSuccess: false,
10626
+ error: { code: res.data?.code, message: res.data?.message || 'No download_url returned' },
10627
+ ctx: res?.ctx,
10628
+ };
10629
+ }
10630
+ const willReplaceWasmPath = path$1.join(process.cwd(), data.wasm_path);
10631
+ const { cacheDir } = keepCacheSync({
10632
+ entryDir: process.cwd(),
10633
+ originalWasmPath: data.wasm_path,
10634
+ originalSplitConfigPath: WASM_SPLIT_CONFIG_FILE_NAME,
10635
+ });
10636
+ console.log(`[remote-download-prepared] target=${willReplaceWasmPath}`);
10637
+ if (downloadUrl.includes('.br')) {
10638
+ const tempWasmPath = path$1.join(cacheDir, '__temp__.wasm.br');
10639
+ console.log('[remote-download-prepared] downloading (br) ->', tempWasmPath);
10640
+ wsServer.sendUnitySplitStatus({ status: 'start_download_prepared_wasm', url: downloadUrl });
10641
+ const startedAt = Date.now();
10642
+ await download(downloadUrl, tempWasmPath);
10643
+ console.log(`[remote-download-prepared] download done in ${Date.now() - startedAt}ms, size=${fs$1.statSync(tempWasmPath).size}`);
10644
+ fs$1.copyFileSync(tempWasmPath, willReplaceWasmPath);
10645
+ wsServer.sendUnitySplitStatus({ status: 'download_prepared_wasm_done', url: downloadUrl });
10646
+ }
10647
+ else {
10648
+ const tempWasmPath = path$1.join(cacheDir, '__temp__.wasm');
10649
+ console.log('[remote-download-prepared] downloading (raw) ->', tempWasmPath);
10650
+ wsServer.sendUnitySplitStatus({ status: 'start_download_prepared_wasm', url: downloadUrl });
10651
+ const startedAt = Date.now();
10652
+ await download(downloadUrl, tempWasmPath);
10653
+ console.log(`[remote-download-prepared] download done in ${Date.now() - startedAt}ms, size=${fs$1.statSync(tempWasmPath).size}`);
10654
+ wsServer.sendUnitySplitStatus({ status: 'download_prepared_wasm_done', url: downloadUrl });
10655
+ wsServer.sendUnitySplitStatus({ status: 'start_compress_prepared_wasm' });
10656
+ await compressWasmFile(tempWasmPath, willReplaceWasmPath);
10657
+ console.log('[remote-download-prepared] compressed and written to project');
10658
+ wsServer.sendUnitySplitStatus({ status: 'compress_prepared_wasm_done', url: downloadUrl });
10659
+ wsServer.sendUnitySplitStatus({ status: 'write_compress_prepared_wasm_done' });
10660
+ }
10661
+ wsServer.sendUnitySplitStatus({ status: 'start_update_wasm_split_config' });
10662
+ // Remote (legacy) pipeline: enable collect but disable archive mode so the
10663
+ // plugin reports to the legacy stark_wasm/v4 collect API.
10664
+ // ORIGINALWASMMD5 must be set now (not only at split time) so the plugin
10665
+ // sends the correct wasm_md5 in every collect report.
10666
+ restoreSplitConfigFromCache();
10667
+ updateWasmSplitConfig({
10668
+ ENABLEWASMCOLLECT: true,
10669
+ ENABLEARCHIVEMODE: false,
10670
+ ORIGINALWASMMD5: res?.data?.result?.original_wasm_md5 ||
10671
+ res?.data?.result?.md5 ||
10672
+ data.wasm_md5,
10673
+ });
10674
+ wsServer.sendUnitySplitStatus({ status: 'update_wasm_split_config_done' });
10675
+ console.log('[remote-download-prepared] split config updated, returning success');
10676
+ return { isSuccess: true, ctx: res?.ctx };
10677
+ }
10678
+ catch (error) {
10679
+ console.log('[remote-download-prepared] error:', error);
10680
+ return {
10681
+ isSuccess: false,
10682
+ error: { code: res.data?.code, message: error instanceof Error ? error.message : String(error) },
10683
+ ctx: res?.ctx,
10684
+ };
10685
+ }
10686
+ }
10687
+
10688
+ function isLocal() {
10689
+ return getLocalState().pipelineMode === 'local';
10690
+ }
10691
+ const startPrepare = (params) => isLocal() ? startPrepare$1(params) : startPrepareRemote(params);
10692
+ const downloadPrepared = (params) => isLocal() ? downloadPrepared$1() : downloadPreparedRemote(params);
10693
+ const setCollect = (params) => isLocal() ? setCollect$1(params) : setCollectRemote(params);
10694
+ const getCollectedFuncIds = (params) => isLocal() ? getCollectedFuncIds$1(params) : getCollectedFuncIdsRemote(params);
10695
+ const getCollecttingInfo = (params) => isLocal() ? getCollecttingInfo$1(params) : getCollecttingInfoRemote(params);
10696
+ const startSplit = (params) => isLocal() ? startSplit$1(params) : startSplitRemote(params);
10697
+ const downloadSplited = (context) => isLocal() ? downloadSplited$1(context) : downloadSplitedRemote(context);
10698
+ const getSplitResult = (params) => isLocal() ? getSplitResult$1() : getSplitResultRemote(params);
10699
+ const getTaskInfo = (params) => isLocal() ? getTaskInfo$1(params) : getTaskInfoRemote(params);
10700
+ const getTaskStatus = (params) => isLocal() ? getTaskStatus$1(params) : getTaskStatusRemote(params);
10701
+ const resetWasmSplit = (data) => isLocal() ? resetWasmSplit$1(data) : resetWasmSplitRemote(data);
10702
+ // Collect session (`/start` / `/finish`) is an implementation detail of the
10703
+ // local `wasm-collect/v1` pipeline — it's invoked inside `setCollectLocal`
10704
+ // and `startSplitLocal` respectively. The remote `stark_wasm/v4` pipeline
10705
+ // has no session concept. IDE never calls these directly, so there is no
10706
+ // dispatcher exposed here.
9789
10707
 
9790
10708
  const gameWasmCancelRoute = {
9791
10709
  method: 'post',
@@ -9889,26 +10807,35 @@ const gameWasmPrepareResultRoute = {
9889
10807
  method: 'post',
9890
10808
  path: '/game/wasm-prepare-result',
9891
10809
  handler: async (req, res) => {
9892
- console.log('wasm-prepare-result-request', req.body);
9893
- const { clientKey, codeMd5 } = req.body;
9894
- const response = await getTaskStatus({
9895
- client_key: clientKey,
9896
- wasm_md5: codeMd5,
9897
- });
9898
- if (response.error) {
9899
- res.send({
9900
- code: errorCode,
9901
- error: response.error,
9902
- ctx: response.ctx,
9903
- });
9904
- }
9905
- else {
10810
+ const { pipelineMode } = getLocalState();
10811
+ if (pipelineMode === 'local') {
9906
10812
  res.send({
9907
10813
  code: successCode,
9908
- data: response.data?.result || {},
9909
- ctx: response.ctx,
10814
+ data: { status: WasmStatus.WasmSplitPreparedStatus },
10815
+ ctx: { logid: 'local' },
9910
10816
  });
10817
+ return;
10818
+ }
10819
+ const { codeMd5, clientKey } = req.body;
10820
+ const result = await getTaskStatus({
10821
+ client_key: clientKey,
10822
+ wasm_md5: codeMd5,
10823
+ });
10824
+ // For the remote pipeline, forward the full `result` payload so the IDE
10825
+ // gets both `status` and the accompanying `package` info, matching the
10826
+ // legacy behaviour that the UI was written against.
10827
+ if (result?.error) {
10828
+ console.log('[wasm-prepare-result] remote error', result.error);
10829
+ res.send({ code: errorCode, error: result.error, ctx: result?.ctx });
10830
+ return;
9911
10831
  }
10832
+ const data = result?.data?.result ?? { status: WasmStatus.IdleStatus };
10833
+ console.log(`[wasm-prepare-result] remote status=${data?.status}`);
10834
+ res.send({
10835
+ code: successCode,
10836
+ data,
10837
+ ctx: result?.ctx ?? { logid: 'remote' },
10838
+ });
9912
10839
  },
9913
10840
  };
9914
10841
 
@@ -9917,7 +10844,8 @@ const gameWasmPrepareRoute = {
9917
10844
  path: '/game/wasm-prepare',
9918
10845
  handler: async (req, res) => {
9919
10846
  const { codePath, desc, codeMd5, clientKey } = req.body;
9920
- console.log('wasm-prepare-start', req.body);
10847
+ const { pipelineMode } = getLocalState();
10848
+ console.log(`wasm-prepare-start [mode=${pipelineMode}]`, req.body);
9921
10849
  const result = await startPrepare({
9922
10850
  client_key: clientKey,
9923
10851
  desc,
@@ -9956,11 +10884,19 @@ const gameWasmSetCollectRoute = {
9956
10884
  method: 'post',
9957
10885
  path: '/game/wasm-set-collect',
9958
10886
  handler: async (req, res) => {
9959
- const { clientKey, codeMd5 } = req.body;
10887
+ // `resume` is optional and only meaningful for the local pipeline:
10888
+ // when the IDE detects an existing open session (e.g. user refreshed
10889
+ // the page mid-collect) and wants to "继续收集" without nuking the
10890
+ // already-uploaded func_ids, it POSTs `{ resume: true }`. Default
10891
+ // (omitted / false) keeps the historical "fresh run" behaviour on
10892
+ // /start (server gets `reset: true`). See `setCollect` jsdoc for the
10893
+ // two-path contract; remote pipeline ignores the field outright.
10894
+ const { clientKey, codeMd5, resume } = req.body;
9960
10895
  console.log('wasm-set-collect', req.body);
9961
10896
  const response = await setCollect({
9962
10897
  client_key: clientKey,
9963
10898
  wasm_md5: codeMd5,
10899
+ resume,
9964
10900
  });
9965
10901
  if (response.error) {
9966
10902
  res.send({
@@ -9983,50 +10919,42 @@ const gameWasmSplitDownloadResultRoute = {
9983
10919
  method: 'post',
9984
10920
  path: '/game/wasm-split-download-result',
9985
10921
  handler: async (req, res) => {
10922
+ const { pipelineMode, splitMeta } = getLocalState();
10923
+ if (pipelineMode === 'local') {
10924
+ if (!splitMeta) {
10925
+ res.send({
10926
+ code: errorCode,
10927
+ error: { message: 'No local split result found. Run split first.' },
10928
+ });
10929
+ return;
10930
+ }
10931
+ res.send({
10932
+ code: successCode,
10933
+ data: { result: splitMeta },
10934
+ msg: 'download success',
10935
+ ctx: { logid: 'local' },
10936
+ });
10937
+ return;
10938
+ }
9986
10939
  const { clientKey, codeMd5, codePath } = req.body;
9987
- console.log('game/wasm-split-download-result-start', req.body);
9988
- const response = await getSplitResult({
10940
+ const result = await getSplitResult({
9989
10941
  client_key: clientKey,
9990
10942
  wasm_md5: codeMd5,
9991
10943
  wasm_path: codePath,
9992
10944
  });
9993
- if (response.error) {
10945
+ if (result.error) {
9994
10946
  res.send({
9995
10947
  code: errorCode,
9996
- error: response.error,
9997
- ctx: response.ctx,
10948
+ error: result.error,
10949
+ ctx: result.ctx,
9998
10950
  });
9999
10951
  }
10000
10952
  else {
10001
- const splitResult = (response.data?.result || {});
10002
- const requiredDownloadFields = [
10003
- 'main_wasm_download_url',
10004
- 'main_wasm_h5_download_url',
10005
- // 'sub_wasm_download_url',
10006
- // 'sub_js_download_url',
10007
- // 'sub_js_data_download_url',
10008
- // 'sub_js_range_download_url',
10009
- ];
10010
- const missingFields = requiredDownloadFields.filter(field => {
10011
- const value = splitResult[field];
10012
- return typeof value !== 'string' || value.trim() === '';
10013
- });
10014
- if (missingFields.length > 0) {
10015
- res.send({
10016
- code: errorCode,
10017
- error: {
10018
- message: `Missing required wasm split fields: ${missingFields.join(', ')}`,
10019
- },
10020
- data: response.data || {},
10021
- ctx: response.ctx,
10022
- });
10023
- return;
10024
- }
10025
10953
  res.send({
10026
10954
  code: successCode,
10027
- data: response.data || {},
10955
+ data: result.data || {},
10028
10956
  msg: 'download success',
10029
- ctx: response.ctx,
10957
+ ctx: result.ctx,
10030
10958
  });
10031
10959
  }
10032
10960
  },
@@ -10115,26 +11043,32 @@ const gameWasmSplitResultRoute = {
10115
11043
  method: 'post',
10116
11044
  path: '/game/wasm-split-result',
10117
11045
  handler: async (req, res) => {
10118
- const { codeMd5, clientKey } = req.body;
10119
- console.log('wasm-split-result', req.body);
10120
- const response = await getTaskStatus({
10121
- client_key: clientKey,
10122
- wasm_md5: codeMd5,
10123
- });
10124
- if (response.error) {
10125
- res.send({
10126
- code: errorCode,
10127
- error: response.error,
10128
- ctx: response.ctx,
10129
- });
10130
- }
10131
- else {
11046
+ const { pipelineMode } = getLocalState();
11047
+ if (pipelineMode === 'local') {
10132
11048
  res.send({
10133
11049
  code: successCode,
10134
- data: response.data?.result || {},
10135
- ctx: response.ctx,
11050
+ data: { status: WasmStatus.WasmSplitDoneStatus },
11051
+ ctx: { logid: 'local' },
10136
11052
  });
11053
+ return;
10137
11054
  }
11055
+ const { clientKey, codeMd5 } = req.body;
11056
+ const result = await getTaskStatus({
11057
+ client_key: clientKey,
11058
+ wasm_md5: codeMd5,
11059
+ });
11060
+ if (result?.error) {
11061
+ console.log('[wasm-split-result] remote error', result.error);
11062
+ res.send({ code: errorCode, error: result.error, ctx: result?.ctx });
11063
+ return;
11064
+ }
11065
+ const data = result?.data?.result ?? { status: WasmStatus.IdleStatus };
11066
+ console.log(`[wasm-split-result] remote status=${data?.status}`);
11067
+ res.send({
11068
+ code: successCode,
11069
+ data,
11070
+ ctx: result?.ctx ?? { logid: 'remote' },
11071
+ });
10138
11072
  },
10139
11073
  };
10140
11074
 
@@ -10145,10 +11079,52 @@ const gameWasmSplitConfigRoute = {
10145
11079
  const config = getSplitConfig();
10146
11080
  if (!config) {
10147
11081
  res.send({ code: errorCode, data: 'Failed to parse split config' });
11082
+ return;
10148
11083
  }
10149
- else {
10150
- res.send({ code: successCode, data: config });
11084
+ // When the CLI is restarted mid-session, localState.pipelineMode resets to
11085
+ // 'local' even though the project on disk may have been prepared in remote
11086
+ // mode. Re-infer it from the persisted split config so subsequent
11087
+ // dispatches (taskinfo / collect / split download) pick the right backend.
11088
+ // Heuristic: the local pipeline always writes enableArchiveMode=true, the
11089
+ // legacy remote pipeline always writes enableArchiveMode=false.
11090
+ if (config.enableWasmCollect) {
11091
+ const inferredMode = config.enableArchiveMode === true ? 'local' : 'remote';
11092
+ const current = getLocalState().pipelineMode;
11093
+ if (current !== inferredMode) {
11094
+ setLocalState({ pipelineMode: inferredMode });
11095
+ console.log(`[pipeline] inferred mode=${inferredMode} from webgl-wasm-split.js (was ${current})`);
11096
+ }
10151
11097
  }
11098
+ // ── wasm drift guard ─────────────────────────────────────────────
11099
+ //
11100
+ // `webgl-wasm-split.js` is persisted state about "which stage was
11101
+ // last completed", but it can desync from reality: the user's Unity
11102
+ // build re-emits `wasmcode/<file>.br` with a fresh, un-instrumented
11103
+ // binary while the config still claims `enableWasmCollect=true`. The
11104
+ // IDE's `canCollect()` then returns true, the prepare step gets
11105
+ // skipped, and the device loads a wasm that has no `scwebgl.logCall`
11106
+ // import — the `[wasmcollect] FATAL: no scwebgl.logCall import`
11107
+ // failure.
11108
+ //
11109
+ // To guard: compare the wasm file currently on disk to the md5 that
11110
+ // startPrepare wrote into `.ttmg-temp/prepared-meta.json`. If they
11111
+ // differ, demote `enableWasmCollect` back to its placeholder string
11112
+ // in the response so `canCollect()` → false and the IDE walks the
11113
+ // user through prepare again. We never touch the real config file
11114
+ // on disk — this is a transient correction at the read boundary, so
11115
+ // the next successful prepare seamlessly re-aligns everything.
11116
+ if (config.enableWasmCollect === true) {
11117
+ const wasmMeta = computeCurrentProjectWasmMd5();
11118
+ if (wasmMeta && wasmMeta.currentMd5 !== wasmMeta.meta.preparedWasmMd5) {
11119
+ console.warn(`[wasmtool] wasm drift detected: project wasm md5=${wasmMeta.currentMd5} but prepared meta expected ${wasmMeta.meta.preparedWasmMd5} (path=${wasmMeta.meta.codePath}). Forcing IDE back to prepare.`);
11120
+ // Mirror the string-placeholder shape the template uses before
11121
+ // prepare writes a real boolean — matches what `canCollect`
11122
+ // expects and is indistinguishable from "never prepared" from
11123
+ // the IDE's perspective.
11124
+ config.enableWasmCollect = '$ENABLEWASMCOLLECT';
11125
+ }
11126
+ }
11127
+ res.send({ code: successCode, data: config });
10152
11128
  },
10153
11129
  };
10154
11130
 
@@ -10216,6 +11192,59 @@ function getGameFallbackRoute(publicPath) {
10216
11192
  };
10217
11193
  }
10218
11194
 
11195
+ /**
11196
+ * Explicit "user clicked a lang toggle in the IDE" endpoint. Unlike
11197
+ * `/game/config-fillback` — which intentionally no-ops when the CLI
11198
+ * already has a lang configured — this route always writes the incoming
11199
+ * lang to TTMGRC so the next IDE bootstrap's `setCurrentLang(cliLang)`
11200
+ * call won't stomp the user's fresh choice.
11201
+ */
11202
+ const gameLanguageRoute = {
11203
+ method: 'post',
11204
+ path: '/game/language',
11205
+ handler: async (req, res) => {
11206
+ const incomingLang = req.body?.lang;
11207
+ const nextLang = resolveSupportedLanguage(incomingLang);
11208
+ if (!nextLang) {
11209
+ res.send({
11210
+ code: successCode,
11211
+ data: {
11212
+ lang: null,
11213
+ updated: false,
11214
+ },
11215
+ });
11216
+ return;
11217
+ }
11218
+ setTTMGRC({ lang: nextLang });
11219
+ res.send({
11220
+ code: successCode,
11221
+ data: {
11222
+ lang: nextLang,
11223
+ updated: true,
11224
+ },
11225
+ });
11226
+ },
11227
+ };
11228
+
11229
+ const gamePipelineModeRoute = {
11230
+ method: 'post',
11231
+ path: '/game/pipeline-mode',
11232
+ handler: (req, res) => {
11233
+ const { mode } = req.body;
11234
+ setLocalState({ pipelineMode: mode });
11235
+ console.log(`[pipeline] mode set to: ${mode}`);
11236
+ res.send({ code: successCode, data: { mode } });
11237
+ },
11238
+ };
11239
+ const gamePipelineModeGetRoute = {
11240
+ method: 'get',
11241
+ path: '/game/pipeline-mode',
11242
+ handler: (_req, res) => {
11243
+ const { pipelineMode } = getLocalState();
11244
+ res.send({ code: successCode, data: { mode: pipelineMode } });
11245
+ },
11246
+ };
11247
+
10219
11248
  const routes = [
10220
11249
  gameConfigRoute,
10221
11250
  gameConfigFillbackRoute,
@@ -10237,6 +11266,9 @@ const routes = [
10237
11266
  gameWasmSplitDownloadRoute,
10238
11267
  gameWasmCancelRoute,
10239
11268
  gameWasmSplitResetRoute,
11269
+ gamePipelineModeRoute,
11270
+ gamePipelineModeGetRoute,
11271
+ gameLanguageRoute,
10240
11272
  ];
10241
11273
  function registerRoutes(app, options) {
10242
11274
  const allRoutes = [...routes, getGameFallbackRoute(options.publicPath)];
@@ -10679,7 +11711,7 @@ async function upload({ clientKey, note = '--', dir, }) {
10679
11711
  }
10680
11712
  }
10681
11713
 
10682
- var version = "0.3.6-beta.3";
11714
+ var version = "0.3.6-beta.wasmcode.split";
10683
11715
  var pkg = {
10684
11716
  version: version};
10685
11717