@ttmg/cli 0.4.0 → 0.4.1

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 (84) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/index.js +544 -39
  3. package/dist/index.js.map +1 -1
  4. package/dist/package.json +1 -1
  5. package/dist/public/assets/{Card-BMss5cuV.js → Card-ASGGlJ8S.js} +1 -1
  6. package/dist/public/assets/Detail-DY__Zw0r.js +1 -0
  7. package/dist/public/assets/Detail-DY__Zw0r.js.br +0 -0
  8. package/dist/public/assets/MonetizationMode-sHN_4ybQ.js +1 -0
  9. package/dist/public/assets/MonetizationModeSummary-DLvyriLM.js +1 -0
  10. package/dist/public/assets/{SectionHeader-3b39hv3n.js → SectionHeader-B7dueBCf.js} +1 -1
  11. package/dist/public/assets/{Tag-B5gYnANu.js → Tag-BsX0d6UL.js} +1 -1
  12. package/dist/public/assets/{arrow-left-B3swWB64.js → arrow-left-uixm7Sd1.js} +1 -1
  13. package/dist/public/assets/{baseForm-Dsi8Ia0k.js → baseForm-C8XfTFyG.js} +1 -1
  14. package/dist/public/assets/baseForm-C8XfTFyG.js.br +0 -0
  15. package/dist/public/assets/boxes-C7RoIbId.js +1 -0
  16. package/dist/public/assets/{chevron-right-WZeuFw11.js → chevron-right-B1WBZrrD.js} +1 -1
  17. package/dist/public/assets/circle-check-wUxXeINd.js +1 -0
  18. package/dist/public/assets/{compass-CCsIev11.js → compass-VJNhHwqW.js} +1 -1
  19. package/dist/public/assets/index-BBR3CPkN.js +1 -0
  20. package/dist/public/assets/index-BBR3CPkN.js.br +0 -0
  21. package/dist/public/assets/{index-uquhwGkB.js → index-BH099q_U.js} +1 -1
  22. package/dist/public/assets/{index-ROKxx4f7.css → index-BJZ4gOGZ.css} +1 -1
  23. package/dist/public/assets/index-BJZ4gOGZ.css.br +0 -0
  24. package/dist/public/assets/{index-D0xEiy7C.js → index-BNc8JtET.js} +1 -1
  25. package/dist/public/assets/{index-JNUqDBWt.js → index-BO5MBsdm.js} +1 -1
  26. package/dist/public/assets/index-Bd-f8ERX.js +1 -0
  27. package/dist/public/assets/{index-DeL2bgxo.js → index-C1C34gtn.js} +1 -1
  28. package/dist/public/assets/index-C7Tmy9g3.js +1 -0
  29. package/dist/public/assets/index-CCKb9Y-j.js +14 -0
  30. package/dist/public/assets/index-CCKb9Y-j.js.br +0 -0
  31. package/dist/public/assets/index-CKhfKyA3.js +1 -0
  32. package/dist/public/assets/index-CKhfKyA3.js.br +0 -0
  33. package/dist/public/assets/{index-C9Un1hFP.js → index-C_npSvtQ.js} +1 -1
  34. package/dist/public/assets/{index-BOl1-Siv.css → index-DHXlsTU6.css} +1 -1
  35. package/dist/public/assets/index-DHXlsTU6.css.br +0 -0
  36. package/dist/public/assets/index-DMsjcIN8.js +1 -0
  37. package/dist/public/assets/index-DNd8ZTok.js +1 -0
  38. package/dist/public/assets/index-DNd8ZTok.js.br +0 -0
  39. package/dist/public/assets/{index-DjY3Igd6.js → index-DbxY65Om.js} +1 -1
  40. package/dist/public/assets/index-Dn5HHf-0.js +1 -0
  41. package/dist/public/assets/index-DpkktYot.css +1 -0
  42. package/dist/public/assets/index-DtoDrber.js +1 -0
  43. package/dist/public/assets/index-DtoDrber.js.br +0 -0
  44. package/dist/public/assets/index-Dx5VGEQm.css +1 -0
  45. package/dist/public/assets/{index-Cigxnpav.js → index-ZHPtAdJM.js} +1 -1
  46. package/dist/public/assets/index-ZHPtAdJM.js.br +0 -0
  47. package/dist/public/assets/{index-C9HXjiVu.js → index-ZtlCPCV9.js} +1 -1
  48. package/dist/public/assets/{index-B-AeuNlL.css → index-_6n0s04V.css} +1 -1
  49. package/dist/public/assets/index-_6n0s04V.css.br +0 -0
  50. package/dist/public/assets/{index-C2NWNiPX.js → index-a-rMUS2V.js} +1 -1
  51. package/dist/public/assets/index-bQUQ2Wmz.js +1 -0
  52. package/dist/public/assets/{index-HYSh4-Ri.js → index-qqnHwFjO.js} +1 -1
  53. package/dist/public/assets/{index-aqIfJUqW.js → index-r3aSeNL1.js} +1 -1
  54. package/dist/public/assets/{index-D1oGAPRa.js → index-s_30KWiA.js} +1 -1
  55. package/dist/public/assets/index-t4JeqZ4a.js +1 -0
  56. package/dist/public/assets/layers-Biv61qtE.js +1 -0
  57. package/dist/public/assets/{sparkles-D2QPhLAu.js → sparkles-Bv2GxrFr.js} +1 -1
  58. package/dist/public/assets/{zap-Cv4X8yRx.js → zap-CjkC_qWZ.js} +1 -1
  59. package/dist/public/index.html +2 -2
  60. package/package.json +1 -1
  61. package/dist/public/assets/Detail-Lyas-t6F.js +0 -1
  62. package/dist/public/assets/Detail-Lyas-t6F.js.br +0 -0
  63. package/dist/public/assets/MonetizationMode-DI4fy3U7.js +0 -1
  64. package/dist/public/assets/MonetizationModeSummary-B6yEB26H.js +0 -1
  65. package/dist/public/assets/baseForm-Dsi8Ia0k.js.br +0 -0
  66. package/dist/public/assets/index-B-AeuNlL.css.br +0 -0
  67. package/dist/public/assets/index-B3-gsm5-.js +0 -1
  68. package/dist/public/assets/index-B3L1GnMP.js +0 -1
  69. package/dist/public/assets/index-B3L1GnMP.js.br +0 -0
  70. package/dist/public/assets/index-B6NHbQwP.js +0 -1
  71. package/dist/public/assets/index-BJV8-tz8.js +0 -1
  72. package/dist/public/assets/index-BOl1-Siv.css.br +0 -0
  73. package/dist/public/assets/index-BqSAtEgq.js +0 -1
  74. package/dist/public/assets/index-CV2u_S0E.js +0 -1
  75. package/dist/public/assets/index-CV2u_S0E.js.br +0 -0
  76. package/dist/public/assets/index-Ci-lqt1V.js +0 -1
  77. package/dist/public/assets/index-Ci-lqt1V.js.br +0 -0
  78. package/dist/public/assets/index-Cigxnpav.js.br +0 -0
  79. package/dist/public/assets/index-DpWpqPKC.js +0 -1
  80. package/dist/public/assets/index-DpWpqPKC.js.br +0 -0
  81. package/dist/public/assets/index-Dvg_oNs7.css +0 -1
  82. package/dist/public/assets/index-DxISN0Xc.js +0 -14
  83. package/dist/public/assets/index-DxISN0Xc.js.br +0 -0
  84. package/dist/public/assets/index-ROKxx4f7.css.br +0 -0
package/dist/index.js CHANGED
@@ -6924,10 +6924,8 @@ function isAxiosError(e) {
6924
6924
  }
6925
6925
 
6926
6926
  const CREATE_DIRECT_FEED_CARD_URL = 'https://developers.tiktok.com/tiktok/v4/devportal/mini_game/fyf_card/create_direct_feed_card';
6927
- const DIRECT_FEED_CARD_HEADERS$2 = {
6927
+ const DIRECT_FEED_CARD_HEADERS$3 = {
6928
6928
  'Content-Type': 'application/json',
6929
- 'x-use-ppe': '1',
6930
- 'x-tt-env': 'ppe_feed_play',
6931
6929
  };
6932
6930
  async function createDirectFeedCard({ appId, clientKey, directFeedCard, }) {
6933
6931
  const payload = {
@@ -6938,22 +6936,20 @@ async function createDirectFeedCard({ appId, clientKey, directFeedCard, }) {
6938
6936
  return request({
6939
6937
  url: CREATE_DIRECT_FEED_CARD_URL,
6940
6938
  method: 'POST',
6941
- headers: DIRECT_FEED_CARD_HEADERS$2,
6939
+ headers: DIRECT_FEED_CARD_HEADERS$3,
6942
6940
  data: payload,
6943
6941
  });
6944
6942
  }
6945
6943
 
6946
6944
  const GET_DIRECT_FEED_SCENARIOS_URL = 'https://developers.tiktok.com/tiktok/v4/devportal/mini_game/fyf_card/get_direct_feed_scenario';
6947
- const DIRECT_FEED_CARD_HEADERS$1 = {
6945
+ const DIRECT_FEED_CARD_HEADERS$2 = {
6948
6946
  'Content-Type': 'application/json',
6949
- 'x-use-ppe': '1',
6950
- 'x-tt-env': 'ppe_feed_play',
6951
6947
  };
6952
6948
  async function fetchDirectFeedScenarios({ appId, clientKey, }) {
6953
6949
  return request({
6954
6950
  url: GET_DIRECT_FEED_SCENARIOS_URL,
6955
6951
  method: 'POST',
6956
- headers: DIRECT_FEED_CARD_HEADERS$1,
6952
+ headers: DIRECT_FEED_CARD_HEADERS$2,
6957
6953
  data: {
6958
6954
  app_id: appId,
6959
6955
  client_key: clientKey,
@@ -6973,16 +6969,14 @@ async function fetchGameAssetPreviewUrl({ assetId, }) {
6973
6969
  }
6974
6970
 
6975
6971
  const GET_DIRECT_FEED_CARD_ASSET_PREVIEW_URL = 'https://developers.tiktok.com/tiktok/v4/devportal/mini_game/fyf_card/get_direct_feed_card_asset_preview_url';
6976
- const DIRECT_FEED_CARD_HEADERS = {
6972
+ const DIRECT_FEED_CARD_HEADERS$1 = {
6977
6973
  'Content-Type': 'application/json',
6978
- 'x-use-ppe': '1',
6979
- 'x-tt-env': 'ppe_feed_play',
6980
6974
  };
6981
6975
  async function fetchDirectFeedCardAssetPreviewUrl({ assetId, contentId, }) {
6982
6976
  return request({
6983
6977
  url: GET_DIRECT_FEED_CARD_ASSET_PREVIEW_URL,
6984
6978
  method: 'POST',
6985
- headers: DIRECT_FEED_CARD_HEADERS,
6979
+ headers: DIRECT_FEED_CARD_HEADERS$1,
6986
6980
  data: {
6987
6981
  asset_id: assetId,
6988
6982
  content_id: contentId,
@@ -7040,8 +7034,6 @@ async function fetchGameInfo(clientKey) {
7040
7034
  const LIST_DIRECT_FEED_CARD_URL = 'https://developers.tiktok.com/tiktok/v4/devportal/mini_game/fyf_card/list_direct_feed_card';
7041
7035
  const LIST_DIRECT_FEED_CARD_HEADERS = {
7042
7036
  'Content-Type': 'application/json',
7043
- 'x-use-ppe': '1',
7044
- 'x-tt-env': 'ppe_feed_play',
7045
7037
  };
7046
7038
  async function listDirectFeedCards({ appId, clientKey, }) {
7047
7039
  return request({
@@ -7055,6 +7047,23 @@ async function listDirectFeedCards({ appId, clientKey, }) {
7055
7047
  });
7056
7048
  }
7057
7049
 
7050
+ const CHANGE_DIRECT_FEED_CARD_STATUS_URL = 'https://developers.tiktok.com/tiktok/v4/devportal/mini_game/fyf_card/change_direct_feed_card_status';
7051
+ const DIRECT_FEED_CARD_HEADERS = {
7052
+ 'Content-Type': 'application/json',
7053
+ };
7054
+ async function changeDirectFeedCardStatus({ appId, clientKey, contentStatus, }) {
7055
+ return request({
7056
+ url: CHANGE_DIRECT_FEED_CARD_STATUS_URL,
7057
+ method: 'POST',
7058
+ headers: DIRECT_FEED_CARD_HEADERS,
7059
+ data: {
7060
+ app_id: appId,
7061
+ client_key: clientKey,
7062
+ content_status: contentStatus,
7063
+ },
7064
+ });
7065
+ }
7066
+
7058
7067
  async function uploadGameToPlatform({ data, name, clientKey, note = '--', appId, }) {
7059
7068
  if (!appId) {
7060
7069
  return {
@@ -9431,6 +9440,49 @@ const gameCheckRoute = {
9431
9440
  };
9432
9441
 
9433
9442
  const changelog = [
9443
+ {
9444
+ title: '0.4.1',
9445
+ target: {
9446
+ iOS: '>=43.1',
9447
+ Android: '>=43.1',
9448
+ },
9449
+ changes: {
9450
+ optimize: [
9451
+ {
9452
+ desc: {
9453
+ 'zh-CN': '优化上传代码包流程:上传后的平台处理提示改为更易理解的文案,不再向开发者暴露平台内部状态码及 asset / ready 等技术字段,进度与失败原因表达更清晰',
9454
+ 'en-US': 'Improve the code package upload flow: post-upload platform status messages are now easier to understand and no longer expose internal status codes or technical fields like asset / ready, making progress and failure reasons clearer.',
9455
+ },
9456
+ module: 'upload',
9457
+ },
9458
+ {
9459
+ desc: {
9460
+ 'zh-CN': '重构上传成功卡片布局:二维码与版本信息分区展示并以分隔线区隔,新增成功状态标识,排版更紧凑协调',
9461
+ 'en-US': 'Rework the upload success card layout: the QR code and version info are separated into clear sections with a divider, a success indicator is added, and the layout is more compact and balanced.',
9462
+ },
9463
+ module: 'upload',
9464
+ },
9465
+ ],
9466
+ },
9467
+ },
9468
+ {
9469
+ title: '0.4.0',
9470
+ target: {
9471
+ iOS: '>=43.1',
9472
+ Android: '>=43.1',
9473
+ },
9474
+ changes: {
9475
+ bugfix: [
9476
+ {
9477
+ desc: {
9478
+ 'zh-CN': '修复游戏无开发权限时提示信息展示异常的问题,引导更准确',
9479
+ 'en-US': 'Fix incorrect prompt messaging when the developer has no permission to develop the game, providing more accurate guidance.',
9480
+ },
9481
+ module: 'detail',
9482
+ },
9483
+ ],
9484
+ },
9485
+ },
9434
9486
  {
9435
9487
  title: '0.3.9',
9436
9488
  target: {
@@ -9912,7 +9964,7 @@ const gameDirectFeedCardAssetPreviewRoute = {
9912
9964
  },
9913
9965
  };
9914
9966
 
9915
- async function resolveAppIdentity$2(body) {
9967
+ async function resolveAppIdentity$3(body) {
9916
9968
  const clientKey = body?.clientKey?.trim() ||
9917
9969
  body?.client_key?.trim() ||
9918
9970
  getClientKey().clientKey?.trim();
@@ -9979,7 +10031,7 @@ const gameDirectFeedCardCreateRoute = {
9979
10031
  method: 'post',
9980
10032
  path: '/game/direct-feed-card/create',
9981
10033
  handler: async (req, res) => {
9982
- const identity = await resolveAppIdentity$2(req.body);
10034
+ const identity = await resolveAppIdentity$3(req.body);
9983
10035
  if (identity.error) {
9984
10036
  res.send({
9985
10037
  code: errorCode,
@@ -10035,7 +10087,7 @@ function isUsableDirectFeedCard(card) {
10035
10087
  }
10036
10088
  return true;
10037
10089
  }
10038
- async function resolveAppIdentity$1(body) {
10090
+ async function resolveAppIdentity$2(body) {
10039
10091
  const clientKey = body?.clientKey?.trim() ||
10040
10092
  body?.client_key?.trim() ||
10041
10093
  getClientKey().clientKey?.trim();
@@ -10080,7 +10132,7 @@ const gameDirectFeedCardListRoute = {
10080
10132
  method: 'post',
10081
10133
  path: '/game/direct-feed-card/list',
10082
10134
  handler: async (req, res) => {
10083
- const identity = await resolveAppIdentity$1(req.body);
10135
+ const identity = await resolveAppIdentity$2(req.body);
10084
10136
  if (identity.error) {
10085
10137
  res.send({
10086
10138
  code: errorCode,
@@ -10185,7 +10237,7 @@ function extractScenarioOptions(data) {
10185
10237
  }
10186
10238
  return [];
10187
10239
  }
10188
- async function resolveAppIdentity(body) {
10240
+ async function resolveAppIdentity$1(body) {
10189
10241
  const clientKey = body?.clientKey?.trim() ||
10190
10242
  body?.client_key?.trim() ||
10191
10243
  getClientKey().clientKey?.trim();
@@ -10230,7 +10282,7 @@ const gameDirectFeedCardScenariosRoute = {
10230
10282
  method: 'post',
10231
10283
  path: '/game/direct-feed-card/scenarios',
10232
10284
  handler: async (req, res) => {
10233
- const identity = await resolveAppIdentity(req.body);
10285
+ const identity = await resolveAppIdentity$1(req.body);
10234
10286
  if (identity.error) {
10235
10287
  res.send({
10236
10288
  code: errorCode,
@@ -10270,6 +10322,112 @@ const gameDirectFeedCardScenariosRoute = {
10270
10322
  },
10271
10323
  };
10272
10324
 
10325
+ async function resolveAppIdentity(body) {
10326
+ const clientKey = body?.clientKey?.trim() ||
10327
+ body?.client_key?.trim() ||
10328
+ getClientKey().clientKey?.trim();
10329
+ let appId = body?.appId?.trim() ||
10330
+ body?.app_id?.trim() ||
10331
+ store.getState().appId?.trim();
10332
+ if (!clientKey) {
10333
+ return {
10334
+ clientKey: '',
10335
+ appId,
10336
+ error: 'Missing client key. Please run `ttmg init` or pass `clientKey` from IDE.',
10337
+ };
10338
+ }
10339
+ if (!appId) {
10340
+ const gameInfoResponse = await fetchGameInfo(clientKey);
10341
+ if (gameInfoResponse.error) {
10342
+ return {
10343
+ clientKey,
10344
+ appId: '',
10345
+ error: gameInfoResponse.error,
10346
+ };
10347
+ }
10348
+ appId = gameInfoResponse.data?.app_id?.trim() || '';
10349
+ if (appId) {
10350
+ store.setState({ appId });
10351
+ }
10352
+ }
10353
+ if (!appId) {
10354
+ return {
10355
+ clientKey,
10356
+ appId: '',
10357
+ error: 'Missing app id. Please open project detail first or pass `appId` from IDE.',
10358
+ };
10359
+ }
10360
+ return {
10361
+ clientKey,
10362
+ appId,
10363
+ error: null,
10364
+ };
10365
+ }
10366
+ function normalizeContentStatus(body) {
10367
+ const source = body?.contentStatus || body?.content_status || {};
10368
+ const contentStatus = Object.entries(source).reduce((result, [contentId, status]) => {
10369
+ const normalizedContentId = String(contentId || '').trim();
10370
+ const normalizedStatus = Number(status);
10371
+ if (normalizedContentId && Number.isFinite(normalizedStatus)) {
10372
+ result[normalizedContentId] = normalizedStatus;
10373
+ }
10374
+ return result;
10375
+ }, {});
10376
+ if (Object.keys(contentStatus).length === 0) {
10377
+ return {
10378
+ contentStatus,
10379
+ error: 'Missing direct-play card status. Please pass a non-empty `contentStatus` from IDE.',
10380
+ };
10381
+ }
10382
+ return {
10383
+ contentStatus,
10384
+ error: null,
10385
+ };
10386
+ }
10387
+ const gameDirectFeedCardStatusRoute = {
10388
+ method: 'post',
10389
+ path: '/game/direct-feed-card/status',
10390
+ handler: async (req, res) => {
10391
+ const body = req.body;
10392
+ const identity = await resolveAppIdentity(body);
10393
+ if (identity.error) {
10394
+ res.send({
10395
+ code: errorCode,
10396
+ error: identity.error,
10397
+ data: null,
10398
+ });
10399
+ return;
10400
+ }
10401
+ const normalized = normalizeContentStatus(body);
10402
+ if (normalized.error) {
10403
+ res.send({
10404
+ code: errorCode,
10405
+ error: normalized.error,
10406
+ data: null,
10407
+ });
10408
+ return;
10409
+ }
10410
+ const response = await changeDirectFeedCardStatus({
10411
+ appId: identity.appId,
10412
+ clientKey: identity.clientKey,
10413
+ contentStatus: normalized.contentStatus,
10414
+ });
10415
+ if (response.error) {
10416
+ res.send({
10417
+ code: errorCode,
10418
+ error: response.error,
10419
+ ctx: response.ctx,
10420
+ });
10421
+ return;
10422
+ }
10423
+ res.send({
10424
+ code: successCode,
10425
+ data: response.data,
10426
+ ctx: response.ctx,
10427
+ });
10428
+ },
10429
+ };
10430
+
10273
10431
  const gameDetailRoute = {
10274
10432
  method: 'get',
10275
10433
  path: '/game/detail',
@@ -11203,36 +11361,79 @@ async function startSplit$1({ client_key, wasm_md5, }) {
11203
11361
  ctx: { logid: 'local', httpStatusCode: 400 },
11204
11362
  };
11205
11363
  }
11364
+ // ── Local boot-miss feedback (no server change) ──────────────────────
11365
+ // The runtime logs every sub-package function it had to pull in BEFORE the
11366
+ // first frame as `[wasmcode2] ... firstFrame=BEFORE` — each one is a
11367
+ // first-screen hitch. Drop those func ids into
11368
+ // `__TTMG_TEMP__/boot_extra_func_ids.txt` (one per line) and they get
11369
+ // merged into `alwaysInclude` so the next split pins them into the main
11370
+ // package, closing the gap WITHOUT re-enabling the indirect-closure flood.
11371
+ // No-op when the file is absent.
11372
+ const bootExtraPath = path$1.join(tempDir, 'boot_extra_func_ids.txt');
11373
+ let bootExtra = [];
11374
+ if (fs$1.existsSync(bootExtraPath)) {
11375
+ bootExtra = fs$1
11376
+ .readFileSync(bootExtraPath, 'utf-8')
11377
+ .split('\n')
11378
+ .map(l => parseInt(l.trim(), 10))
11379
+ .filter(n => Number.isInteger(n) && n > 0);
11380
+ }
11381
+ const alwaysIncludeIds = Array.from(new Set([...bootFuncIds, ...bootExtra]));
11382
+ // ── Collect coverage signal ──────────────────────────────────────────
11383
+ // With the indirect-call closure off, the main package ≈ the collected
11384
+ // set, so collect coverage now directly determines first-screen safety.
11385
+ // Surface it, and warn (never block) when it looks too thin to cover boot.
11386
+ const totalFuncs = Number(getGameJson()?.wasmFuncCount) || getLocalState().totalWasmFuncCount || 0;
11387
+ const coveragePct = totalFuncs ? (funcIds.length / totalFuncs) * 100 : 0;
11206
11388
  verboseLog(`[wasmtool] splitting with ${funcIds.length} func IDs` +
11389
+ ` (coverage ${coveragePct.toFixed(1)}% of ${totalFuncs})` +
11207
11390
  (bootFuncIds.length > 0
11208
- ? `, ${bootFuncIds.length} boot-phase func IDs (→ alwaysInclude)`
11209
- : ', no boot-phase info (legacy server, falling back to callClosure only)') +
11210
- `, archive=${archive}`);
11391
+ ? `, ${bootFuncIds.length} boot-phase func IDs`
11392
+ : ', no boot-phase info (legacy server, relying on collect + callClosure)') +
11393
+ (bootExtra.length > 0 ? `, +${bootExtra.length} boot-extra(local)` : '') +
11394
+ `, ${alwaysIncludeIds.length} → alwaysInclude, archive=${archive}`);
11395
+ // THIN_COLLECT_FLOOR is a heuristic, not a hard rule — tune from real
11396
+ // first-screen sizes. Warn-only: a thin collect with no boot hints means
11397
+ // first-screen functions may be served from the sub package before the
11398
+ // first frame (a visible hitch).
11399
+ const THIN_COLLECT_FLOOR = 8000;
11400
+ if (alwaysIncludeIds.length === 0 && funcIds.length < THIN_COLLECT_FLOOR) {
11401
+ verboseWarn(`[wasmtool] collect looks thin (${funcIds.length} funcs, no boot hints). ` +
11402
+ `First package may miss first-screen functions → pre-first-frame sub-package loads. ` +
11403
+ `Collect more scenarios (resume) before splitting, or add ids to ${bootExtraPath}.`);
11404
+ }
11211
11405
  try {
11212
11406
  const result = ttmgWasmtool.split({
11213
11407
  input: rawWasmPath,
11214
11408
  funcIds,
11215
- // Boot-phase func ids `alwaysInclude`. They are a subset of
11216
- // `funcIds` so this doesn't grow `collect_count`, but it DOES seed
11217
- // the direct-call closure BFS with the exact set needed for first
11218
- // frame, and the split tool's `alwaysIncludeAdded` counter is the
11219
- // observability signal when zero (= server didn't return boot info).
11220
- alwaysInclude: bootFuncIds.length > 0 ? bootFuncIds : undefined,
11409
+ // Boot-phase func ids (server) local boot-extra feedback
11410
+ // `alwaysInclude`. These seed the direct-call closure BFS with the
11411
+ // exact set needed for the first frame and force first-screen
11412
+ // functions into main; the split tool's `alwaysIncludeAdded` counter
11413
+ // is the observability signal (0 = no boot hints from any source).
11414
+ alwaysInclude: alwaysIncludeIds.length > 0 ? alwaysIncludeIds : undefined,
11221
11415
  // Always-on direct-call closure over (collect ∪ alwaysInclude ∪
11222
11416
  // start_func). Folds in func ids that collect missed (untaken
11223
11417
  // branches, race conditions during collect) so first-screen code
11224
11418
  // paths don't trap on archive trampolines. See the split tool's
11225
11419
  // `closure_added` counter for the per-build size impact.
11226
11420
  callClosure: true,
11227
- // Always-on indirect-call type-closure scoped to the boot subset.
11228
- // Catches IL2CPP virtual / interface / delegate dispatch which is
11229
- // the dominant source of remaining `firstFrame=BEFORE` archive
11230
- // trampoline hits after the runtime collect + direct closure
11231
- // passes (see `indirectClosureAdded` for the per-build size
11232
- // impact). Defaults to `true` in the wasmtool but we set it
11233
- // explicitly so a future tool default change can't silently turn
11234
- // it off in our pipeline.
11235
- callIndirectClosure: true,
11421
+ // OFF: the indirect-call type-closure is a static over-approximation
11422
+ // that pulls EVERY table function whose signature matches one used by
11423
+ // a boot root. In IL2CPP builds ~90% of functions live in the indirect
11424
+ // table and share only a few hundred signatures, so any non-trivial
11425
+ // boot set touches them all and this broadcasts almost the entire
11426
+ // module into main measured ~135k/136k funcs (~50MB) main, defeating
11427
+ // the split. First-screen indirect-call targets are instead covered
11428
+ // precisely by the runtime collect (logCall is injected into every
11429
+ // function body, so any function that actually runs before first frame
11430
+ // — including call_indirect targets — is already in `funcIds`) plus the
11431
+ // direct-call closure and `alwaysInclude`. Any residual first-frame
11432
+ // indirect hit is served on-demand by the archive loader at runtime;
11433
+ // keep those rare by improving collect-session coverage, not by
11434
+ // re-enabling this flood. Set explicitly so the wasmtool default
11435
+ // (currently `true`) can't silently turn it back on.
11436
+ callIndirectClosure: false,
11236
11437
  outputDir: splitOutputDir,
11237
11438
  archive,
11238
11439
  compress: true,
@@ -12826,6 +13027,306 @@ const gamePipelineModeGetRoute = {
12826
13027
  },
12827
13028
  };
12828
13029
 
13030
+ /**
13031
+ * 启动链路子包收集 —— DevTool CLI 侧契约类型。
13032
+ *
13033
+ * 协议字段与设计方案保持一致(见
13034
+ * topics/performance/startup-subpackage-collection-technical-design.md)。
13035
+ * `entry` 在协议层用单字段 name 表示:主包入口为内部约定 `__GAME__`,
13036
+ * 独立分包入口为该独立分包在源码 `subpackages[]` 中的 name。
13037
+ */
13038
+ /** 平台内部主包标识,只活在 DevTool / 客户端 / 编译服务等平台组件之间。 */
13039
+ const GAME_ENTRY = '__GAME__';
13040
+ /** 启动子包预下载配置字段名(源码侧 + 编译产物侧同名)。 */
13041
+ const PRELOAD_FIELD_NAME = 'parallelPreloadSubpackages';
13042
+
13043
+ /**
13044
+ * 从磁盘**新鲜读取**源码 game.json(不走 getGameJson 缓存,因为本流程要写)。
13045
+ * game.json 不存在或解析失败时抛错,由上层转成 failed 状态。
13046
+ */
13047
+ function readRawGameJson() {
13048
+ const filePath = path__namespace.join(process.cwd(), SUBPACKAGE_CONFIG_FILE_NAME);
13049
+ if (!fs__namespace.existsSync(filePath)) {
13050
+ throw new Error('game.json 不存在');
13051
+ }
13052
+ const raw = fs__namespace.readFileSync(filePath, 'utf-8');
13053
+ let json;
13054
+ try {
13055
+ json = JSON.parse(raw);
13056
+ }
13057
+ catch {
13058
+ throw new Error('game.json 解析失败');
13059
+ }
13060
+ const subpackagesField = SUBPACKAGE_FIELD_NAMES.find(k => Array.isArray(json[k])) ??
13061
+ SUBPACKAGE_FIELD_NAMES[0];
13062
+ return { filePath, raw, json, subpackagesField };
13063
+ }
13064
+ /** 只读读取一份 game.json;不存在 / 解析失败时返回 null,不抛错。 */
13065
+ function tryReadGameJson() {
13066
+ try {
13067
+ return readRawGameJson();
13068
+ }
13069
+ catch {
13070
+ return null;
13071
+ }
13072
+ }
13073
+ /** 取 subpackages 数组(保证返回数组)。 */
13074
+ function getSubpackages(game) {
13075
+ const list = game.json[game.subpackagesField];
13076
+ return Array.isArray(list) ? list : [];
13077
+ }
13078
+ /** 按 name 在 subpackages[] 中定位 entry。 */
13079
+ function findSubpackage(game, name) {
13080
+ return getSubpackages(game).find(s => s?.name === name);
13081
+ }
13082
+ /**
13083
+ * 把对象就地写回磁盘,沿用仓库 JSON 缩进 / 换行约定。
13084
+ */
13085
+ function writeGameJson(game) {
13086
+ fs__namespace.writeFileSync(game.filePath, JSON.stringify(game.json, null, JSON_INDENT) + JSON_EOL);
13087
+ }
13088
+ /** 失败回滚:把原始文本原样写回。 */
13089
+ function restoreGameJson(game) {
13090
+ fs__namespace.writeFileSync(game.filePath, game.raw);
13091
+ }
13092
+ /** entry 是否为主包入口。 */
13093
+ function isGameEntry(entry) {
13094
+ return entry === GAME_ENTRY;
13095
+ }
13096
+
13097
+ /**
13098
+ * 写入前校验:通用规则 + 独立分包入口增量规则。
13099
+ * 任意一条不满足直接抛 Error(上层转 failed,不写 game.json)。
13100
+ * 返回过滤掉防御性条目后的待写入列表(含 root)。
13101
+ */
13102
+ function validateCollect(game, entry, reported) {
13103
+ const subpackages = getSubpackages(game);
13104
+ if (!Array.isArray(game.json[game.subpackagesField])) {
13105
+ throw new Error('game.json.subpackages 缺失或不是数组');
13106
+ }
13107
+ // 独立分包入口增量校验
13108
+ if (!isGameEntry(entry)) {
13109
+ const entryPkg = findSubpackage(game, entry);
13110
+ if (!entryPkg) {
13111
+ throw new Error(`独立分包入口 ${entry} 不存在于 game.json.subpackages`);
13112
+ }
13113
+ if (entryPkg.independent !== true) {
13114
+ throw new Error(`入口 ${entry} 不是独立分包(independent !== true),不可作为启动入口采集`);
13115
+ }
13116
+ }
13117
+ const nameToPkg = new Map(subpackages.map(s => [s?.name, s]));
13118
+ const result = [];
13119
+ const seen = new Set();
13120
+ for (const item of reported) {
13121
+ const subPkgName = item?.subPkgName;
13122
+ // 客户端不应上报 __GAME__ 作为子包名:防御性忽略,不入库、不阻塞
13123
+ if (!subPkgName || subPkgName === GAME_ENTRY) {
13124
+ continue;
13125
+ }
13126
+ // 独立分包不预下载自己
13127
+ if (!isGameEntry(entry) && subPkgName === entry) {
13128
+ throw new Error(`独立分包入口 ${entry} 不能预下载自身`);
13129
+ }
13130
+ if (seen.has(subPkgName)) {
13131
+ continue;
13132
+ }
13133
+ seen.add(subPkgName);
13134
+ const pkg = nameToPkg.get(subPkgName);
13135
+ if (!pkg) {
13136
+ throw new Error(`子包 ${subPkgName} 未定义在 game.json.subpackages`);
13137
+ }
13138
+ if (!pkg.root || typeof pkg.root !== 'string') {
13139
+ throw new Error(`子包 ${subPkgName} 缺少 root 字段`);
13140
+ }
13141
+ const absDir = path__namespace.isAbsolute(pkg.root)
13142
+ ? pkg.root
13143
+ : path__namespace.join(process.cwd(), pkg.root);
13144
+ if (!fs__namespace.existsSync(absDir) || !fs__namespace.statSync(absDir).isDirectory()) {
13145
+ throw new Error(`子包 ${subPkgName} 的目录不存在:${pkg.root}`);
13146
+ }
13147
+ result.push({
13148
+ subPkgName,
13149
+ isStartup: !!item.isStartup,
13150
+ root: pkg.root,
13151
+ });
13152
+ }
13153
+ return result;
13154
+ }
13155
+
13156
+ /** 读出某个 entry 当前的 parallelPreloadSubpackages(保证返回数组)。 */
13157
+ function readEntryConfig(game, entry) {
13158
+ const container = isGameEntry(entry)
13159
+ ? game.json
13160
+ : findSubpackage(game, entry);
13161
+ const list = container?.[PRELOAD_FIELD_NAME];
13162
+ return Array.isArray(list) ? list : [];
13163
+ }
13164
+ /**
13165
+ * 按 entry 只读读取源码 game.json 中已有的 parallelPreloadSubpackages,
13166
+ * 用于 UI 初始化配置预览。game.json 不存在 / 字段缺失时返回空数组,不阻塞。
13167
+ */
13168
+ function readPreloadSubpackagesConfig(entry) {
13169
+ const game = tryReadGameJson();
13170
+ if (!game) {
13171
+ return { entry, parallelPreloadSubpackages: [] };
13172
+ }
13173
+ return { entry, parallelPreloadSubpackages: readEntryConfig(game, entry) };
13174
+ }
13175
+ /**
13176
+ * 列出某个 entry 下可被采集(可预下载)的候选子包,供 UI 基于真实可用包选择。
13177
+ *
13178
+ * 候选 = game.json.subpackages 中 root 目录真实存在、且 name 不等于当前 entry
13179
+ * 的子包(独立分包入口不可预下载自身)。仅供 mock 上报基于真实可用包选择,
13180
+ * 不含 size —— size 在编译期由 ttmg-compile 按压缩加密后体积回填。
13181
+ * game.json 不存在 / 字段缺失时返回空数组,不阻塞。
13182
+ */
13183
+ function listPreloadSubpackageCandidates(entry) {
13184
+ const game = tryReadGameJson();
13185
+ if (!game) {
13186
+ return { entry, candidates: [], totalSubpackages: 0 };
13187
+ }
13188
+ const subpackages = getSubpackages(game);
13189
+ const candidates = [];
13190
+ for (const sub of subpackages) {
13191
+ const subPkgName = sub?.name;
13192
+ const root = sub?.root;
13193
+ if (typeof subPkgName !== 'string' || !subPkgName)
13194
+ continue;
13195
+ if (subPkgName === entry)
13196
+ continue;
13197
+ if (typeof root !== 'string' || !root)
13198
+ continue;
13199
+ const absDir = path__namespace.isAbsolute(root)
13200
+ ? root
13201
+ : path__namespace.join(process.cwd(), root);
13202
+ if (!fs__namespace.existsSync(absDir) || !fs__namespace.statSync(absDir).isDirectory())
13203
+ continue;
13204
+ candidates.push({
13205
+ subPkgName,
13206
+ root,
13207
+ independent: sub.independent === true,
13208
+ });
13209
+ }
13210
+ return { entry, candidates, totalSubpackages: subpackages.length };
13211
+ }
13212
+ /**
13213
+ * 收集快照写入:校验 → 按 entry 覆盖写入对应位置 → 失败回滚。
13214
+ *
13215
+ * 只写入 `subPkgName` + `isStartup`,**不写 size**:size 是压缩+加密后的真实
13216
+ * 下发体积,由编译服务(ttmg-compile)在产出 STTPKG 时回填到编译产物,收集期
13217
+ * 拿不到也不写(见技术方案 3.4 / 3.5)。
13218
+ *
13219
+ * - 写入前校验失败:抛错,不修改 game.json。
13220
+ * - 进入写入阶段后异常:回滚原始 game.json 再抛错。
13221
+ * 返回本次 entry 下最终写入的 parallelPreloadSubpackages。
13222
+ */
13223
+ function collectPreloadSubpackages(entry, reported) {
13224
+ // readRawGameJson 抛错即「game.json 不存在 / 解析失败」,写入前阶段,不动文件
13225
+ const game = readRawGameJson();
13226
+ const validated = validateCollect(game, entry, reported);
13227
+ const parallelPreloadSubpackages = validated.map(item => ({
13228
+ subPkgName: item.subPkgName,
13229
+ isStartup: item.isStartup,
13230
+ }));
13231
+ let started = false;
13232
+ try {
13233
+ const container = isGameEntry(entry)
13234
+ ? game.json
13235
+ : findSubpackage(game, entry);
13236
+ if (!container) {
13237
+ // 独立分包入口已在 validate 阶段确认存在,这里是防御
13238
+ throw new Error(`入口 ${entry} 不存在`);
13239
+ }
13240
+ started = true;
13241
+ container[PRELOAD_FIELD_NAME] = parallelPreloadSubpackages;
13242
+ writeGameJson(game);
13243
+ }
13244
+ catch (err) {
13245
+ if (started) {
13246
+ restoreGameJson(game);
13247
+ }
13248
+ throw err;
13249
+ }
13250
+ return { entry, parallelPreloadSubpackages };
13251
+ }
13252
+
13253
+ /**
13254
+ * 读取某个启动入口已有的 parallelPreloadSubpackages(只读),用于 UI 初始化
13255
+ * 配置预览。对应设计方案的 `getPreloadSubpackagesConfig`。
13256
+ *
13257
+ * query: `entry`(缺省按主包 `__GAME__` 处理)。
13258
+ */
13259
+ const gamePreloadSubpackagesConfigRoute = {
13260
+ method: 'get',
13261
+ path: '/game/preload-subpackages-config',
13262
+ handler: async (req, res) => {
13263
+ const entry = req.query.entry || GAME_ENTRY;
13264
+ try {
13265
+ const data = readPreloadSubpackagesConfig(entry);
13266
+ res.send({ code: successCode, data });
13267
+ }
13268
+ catch (err) {
13269
+ res.send({
13270
+ code: errorCode,
13271
+ error: { message: err.message },
13272
+ });
13273
+ }
13274
+ },
13275
+ };
13276
+
13277
+ /**
13278
+ * 列出某个启动入口下可被采集(可预下载)的候选子包,供 UI mock 上报时
13279
+ * 基于真实可用包选择,而不是手填名字。候选直接来自 game.json.subpackages
13280
+ * 中 root 真实存在、name 不等于当前 entry 的子包,并附带压缩前体积。
13281
+ *
13282
+ * query: `entry`(缺省按主包 `__GAME__` 处理)。
13283
+ */
13284
+ const gamePreloadSubpackageCandidatesRoute = {
13285
+ method: 'get',
13286
+ path: '/game/preload-subpackage-candidates',
13287
+ handler: async (req, res) => {
13288
+ const entry = req.query.entry || GAME_ENTRY;
13289
+ try {
13290
+ const data = listPreloadSubpackageCandidates(entry);
13291
+ res.send({ code: successCode, data });
13292
+ }
13293
+ catch (err) {
13294
+ res.send({
13295
+ code: errorCode,
13296
+ error: { message: err.message },
13297
+ });
13298
+ }
13299
+ },
13300
+ };
13301
+
13302
+ /**
13303
+ * 收集快照写入:按 entry 校验 + 补体积 + 覆盖写入源码 game.json 对应位置,
13304
+ * 失败回滚。对应设计方案的 `collectPreloadSubpackages` /
13305
+ * `updatePreloadSubpackagesConfig`(HTTP 请求-响应实现,start 态由 UI 自身的
13306
+ * 处理中状态承载,CLI 只回 success / failed)。
13307
+ *
13308
+ * body: `{ entry, subpackages: [{ subPkgName, isStartup }] }`。
13309
+ */
13310
+ const gameCollectPreloadSubpackagesRoute = {
13311
+ method: 'post',
13312
+ path: '/game/collect-preload-subpackages',
13313
+ handler: async (req, res) => {
13314
+ const entry = req.body?.entry || GAME_ENTRY;
13315
+ const subpackages = req.body?.subpackages || [];
13316
+ console.log('collect-preload-subpackages', { entry, count: subpackages.length });
13317
+ try {
13318
+ const data = collectPreloadSubpackages(entry, subpackages);
13319
+ res.send({ code: successCode, data });
13320
+ }
13321
+ catch (err) {
13322
+ res.send({
13323
+ code: errorCode,
13324
+ error: { message: err.message },
13325
+ });
13326
+ }
13327
+ },
13328
+ };
13329
+
12829
13330
  const routes = [
12830
13331
  gameAssetPreviewUrlRoute,
12831
13332
  gameAssetsRoute,
@@ -12838,6 +13339,7 @@ const routes = [
12838
13339
  gameDirectFeedCardCreateRoute,
12839
13340
  gameDirectFeedCardListRoute,
12840
13341
  gameDirectFeedCardScenariosRoute,
13342
+ gameDirectFeedCardStatusRoute,
12841
13343
  gameUploadRoute,
12842
13344
  gameWasmSplitConfigRoute,
12843
13345
  gameWasmSplitOptionsRoute,
@@ -12858,6 +13360,9 @@ const routes = [
12858
13360
  gamePipelineModeRoute,
12859
13361
  gamePipelineModeGetRoute,
12860
13362
  gameLanguageRoute,
13363
+ gamePreloadSubpackagesConfigRoute,
13364
+ gamePreloadSubpackageCandidatesRoute,
13365
+ gameCollectPreloadSubpackagesRoute,
12861
13366
  ];
12862
13367
  /**
12863
13368
  * Express 4 does not catch rejections from async route handlers — an
@@ -13358,7 +13863,7 @@ async function upload({ clientKey, note = '--', dir, }) {
13358
13863
  }
13359
13864
  }
13360
13865
 
13361
- var version = "0.4.0";
13866
+ var version = "0.4.1";
13362
13867
  var pkg = {
13363
13868
  version: version};
13364
13869