@ttmg/cli 0.4.6-vibe-beta.16 → 0.4.6-vibe-beta.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,12 @@
1
1
  ### @ttmg/cli
2
2
 
3
+ `0.4.6-vibe-beta.17` 支持上传后扫码预览:`ttmg upload --vibe` 上传完成后继续查询同一任务,收到 `preview_url` 后在原页面显示预览二维码。等待期间保持进程运行;超时或查询失败仍保留上传成功状态,请勿直接重传。详见 [Vibe 接入说明](vibe-upload.md)。
4
+
5
+ Version `0.4.6-vibe-beta.17`: `ttmg upload --vibe` keeps polling the same task after upload and shows a game preview QR on the original page when `preview_url` arrives. Keep the process running. A preview timeout or lookup failure preserves the upload receipt; do not upload again.
6
+
3
7
  Vibe 手机扫码授权与上传:`ttmg upload --vibe --dir ./game --title "My game" --icon ./avatar.png --format ndjson`。用户在手机 TikTok 扫码授权后,原命令自动上传。只收集标题和本地头像,并使用游戏目录或代码包;其他应用资料由 CLI 补齐。页面展示标题、头像、描述、类型和子类型。见 [Vibe 接入说明](vibe-upload.md)。
4
8
 
5
- 扫码上传使用 `0.4.6-vibe-beta.16`,安装后核对版本与能力声明。扫码链路已用 PPE 测试素材完成真实上传;正式环境未开放。
9
+ 扫码上传使用 `0.4.6-vibe-beta.17`,安装后核对版本与能力声明。Snow Crew 已在 PPE 完成真实上传并取得预览链接,用户确认扫码预览跑通;正式环境未开放。
6
10
 
7
11
  `ttmg` is a command-line tool designed for managing and developing mini-game projects. It supports initialization, development, debugging, and packaging for both H5 and native mini-games.
8
12
  `ttmg` 是一款专为小游戏项目管理与开发设计的命令行工具,支持 H5 小游戏和原生小游戏的初始化、开发调试及打包构建。
@@ -14,7 +18,7 @@ Vibe 版本支持手机扫码授权上传、可选 `--description`、分类查
14
18
  固定版本安装(发布标签为 `vibe-beta`):
15
19
 
16
20
  ```bash
17
- npm install -g @ttmg/cli@0.4.6-vibe-beta.16
21
+ npm install -g @ttmg/cli@0.4.6-vibe-beta.17
18
22
  ttmg --version
19
23
  ttmg capabilities --format json
20
24
  ttmg upload --help
@@ -89,16 +93,6 @@ ttmg login --implicit --email <email> --password <password>
89
93
  options when credentials are provided by the execution environment.
90
94
  执行环境也可以通过 `TTMG_LOGIN_EMAIL` 和 `TTMG_LOGIN_PASSWORD` 提供凭据。
91
95
 
92
- Inspect the reusable local session without exposing its Cookie. The status is
93
- local-only; it does not make a network request. `--require-auth` is useful for
94
- Agent preflight because it exits non-zero when login is missing.
95
- 可在不暴露 Cookie 的情况下读取本地会话是否可复用。该状态只检查本地配置,不发起网络请求;Agent 预检可增加 `--require-auth`,未登录时以非零状态退出。
96
-
97
- ```
98
- ttmg auth status --format json
99
- ttmg auth status --format json --require-auth
100
- ```
101
-
102
96
  Agents can discover the exact command and runtime capability surface before
103
97
  acting. Unsupported device and Runtime features are explicitly reported as
104
98
  `false`; commands that create a platform version are marked with
package/dist/index.js CHANGED
@@ -7263,6 +7263,8 @@ Vibe QR upload (real PPE trial):
7263
7263
  Keep this process running. At waiting_authorization, scan the QR on pageUrl with TikTok on your phone.
7264
7264
  pageUrl is the local QR/progress page on the CLI computer. Reopen it with authorizationOpenCommand.
7265
7265
  The CLI polls and uploads avatar, app_info and package automatically after authorization. Do not start a second upload.
7266
+ After upload, waiting_preview keeps polling the same task until previewUrl is available; pageUrl then shows the game preview QR.
7267
+ Preview ready: previewReady=true and qrPurpose=game_preview. A preview timeout preserves uploaded=true; check the task, do not reupload.
7266
7268
  Real success: terminal=true, status=uploaded, uploaded=true, simulation=false. On failure, read errorCode and nextAction.
7267
7269
  The local QR page opens for every output format; --no-open disables automatic opening.
7268
7270
  The page displays the submitted title, icon, description, category and subcategory.
@@ -7711,6 +7713,8 @@ Vibe 扫码上传(真实 PPE 试用):
7711
7713
  保持原进程运行。在 waiting_authorization 阶段,使用手机 TikTok 扫描 pageUrl 页面中的二维码。
7712
7714
  pageUrl 是 CLI 所在电脑的二维码/进度页;重新打开时,执行返回的 authorizationOpenCommand。
7713
7715
  CLI 负责轮询,授权后自动上传 avatar、app_info、package,不再启动第二次上传。
7716
+ 上传后进入 waiting_preview,继续查询同一任务;拿到 previewUrl 后,原 pageUrl 页面展示游戏预览二维码。
7717
+ previewReady=true 且 qrPurpose=game_preview 表示可扫码预览;等待超时仍保留 uploaded=true,请核对任务,不要重传。
7714
7718
  真实成功:terminal=true、status=uploaded、uploaded=true、simulation=false。失败时读取 errorCode 和 nextAction。
7715
7719
  所有输出格式都会打开本地二维码页;--no-open 可关闭自动打开。
7716
7720
  页面展示本次提交的标题、头像、描述、类型和子类型。
@@ -16378,7 +16382,7 @@ async function authStatus(options = {}) {
16378
16382
  return result;
16379
16383
  }
16380
16384
 
16381
- var version = "0.4.6-vibe-beta.16";
16385
+ var version = "0.4.6-vibe-beta.17";
16382
16386
  var engines = {
16383
16387
  node: ">=20.19.1 <21 || >=22"
16384
16388
  };
@@ -17264,7 +17268,18 @@ function getCapabilities() {
17264
17268
  provisionalHttpAdapter: 'loopback-only',
17265
17269
  httpContract: 'minis-import-task',
17266
17270
  authorizationQr: true,
17267
- previewQr: false,
17271
+ previewQr: true,
17272
+ preview: {
17273
+ taskEndpoint: 'get_minis_import_task_detail_for_agent',
17274
+ waitingStatus: 'waiting_preview',
17275
+ resultFields: ['previewUrl', 'previewReady', 'clientKey', 'appId', 'codeAssetId', 'minisCenterHistoryId'],
17276
+ readiness: 'nonempty-valid-preview-url',
17277
+ qrPurpose: 'game_preview',
17278
+ sameTask: true,
17279
+ timeout: 'shared-operation-timeout',
17280
+ failurePreservesUploaded: true,
17281
+ deviceRunVerified: false,
17282
+ },
17268
17283
  realAuthorizationVerified: true,
17269
17284
  realUploadVerified: true,
17270
17285
  verificationScope: { backend: 'ppe', authorizationMode: 'qr', assets: 'test-fixtures', date: '2026-09-16', production: false },
@@ -18618,7 +18633,7 @@ var authOpen = /*#__PURE__*/Object.freeze({
18618
18633
  const prefix = '/bff/app_info/minis_import/';
18619
18634
  const MINIS_IMPORT_PATHS = {
18620
18635
  create: `${prefix}create_minis_import_task_for_agent/`,
18621
- status: `${prefix}get_minis_import_task_status_for_agent/`,
18636
+ status: `${prefix}get_minis_import_task_detail_for_agent/`,
18622
18637
  icon: `${prefix}upload_minis_import_app_icon_for_agent/`,
18623
18638
  info: `${prefix}submit_minis_import_info_for_agent/`,
18624
18639
  package: `${prefix}upload_minis_import_package_for_agent/`,
@@ -18677,6 +18692,31 @@ function createBackend(mode, transport) {
18677
18692
  });
18678
18693
  return {
18679
18694
  mode,
18695
+ async getTaskDetail(task, signal) {
18696
+ const data = await call({ method: 'GET', path: MINIS_IMPORT_PATHS.status,
18697
+ data: credentials(task), signal });
18698
+ const raw = status(data.status);
18699
+ if (raw === 8)
18700
+ throw new VibeError('VIBE_AUTH_EXPIRED');
18701
+ if (raw === 7)
18702
+ throw new VibeError('VIBE_TASK_FAILED');
18703
+ if (data.ready_for_input !== undefined && typeof data.ready_for_input !== 'boolean')
18704
+ throw new VibeError('VIBE_INVALID_RESPONSE');
18705
+ const detail = { statusRaw: raw, readyForInput: data.ready_for_input === true };
18706
+ for (const [wire, field] of [['app_id', 'appId'], ['code_asset_id', 'codeAssetId'],
18707
+ ['minis_center_history_id', 'minisCenterHistoryId']]) {
18708
+ if (data[wire] !== undefined && data[wire] !== '')
18709
+ detail[field] = taskId(data[wire]);
18710
+ }
18711
+ if (data.client_key !== undefined && data.client_key !== '') {
18712
+ if (!nonempty(data.client_key))
18713
+ throw new VibeError('VIBE_INVALID_RESPONSE');
18714
+ detail.clientKey = data.client_key;
18715
+ }
18716
+ if (data.preview_url !== undefined && data.preview_url !== '')
18717
+ detail.previewUrl = safeWebUrl(data.preview_url);
18718
+ return detail;
18719
+ },
18680
18720
  async createTask(input, signal) {
18681
18721
  const data = await call({
18682
18722
  method: 'POST',
@@ -18807,6 +18847,7 @@ function createPpeMockBackend(lane, adapter, scenario = 'success') {
18807
18847
  };
18808
18848
  return {
18809
18849
  mode: 'ppe-mock',
18850
+ getTaskDetail: (task, signal) => real.getTaskDetail(task, signal),
18810
18851
  uploadAvailable: true,
18811
18852
  dispose: () => real.dispose?.(),
18812
18853
  async createTask(input, signal) {
@@ -19283,9 +19324,9 @@ const vibePageStyles = `
19283
19324
 
19284
19325
  :root{font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;color:#24282f;background:#fff;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased}
19285
19326
  *{box-sizing:border-box}
19286
- body{margin:0;min-height:100vh;min-height:100svh;display:flex;align-items:center;justify-content:center;padding:36px 24px;font-size:14px;line-height:1.5}
19287
- .shell{width:100%;max-width:400px}
19288
- .steps{display:flex;align-items:center;justify-content:center;gap:10px;margin:0 0 32px;padding:0;list-style:none}
19327
+ body{margin:0;min-height:100vh;min-height:100svh;display:flex;align-items:center;justify-content:center;padding:64px 24px;font-size:14px;line-height:1.5}
19328
+ .shell{position:relative;width:100%;max-width:400px}
19329
+ .steps{display:flex;align-items:center;justify-content:center;gap:10px;margin:0 0 24px;padding:0;list-style:none}
19289
19330
  .step{display:flex;align-items:center;gap:6px;font-size:11px;color:#9298a2;white-space:nowrap}
19290
19331
  .step:after{content:"";height:1px;width:25px;margin-left:4px;background:#e9ebef}.step:last-child:after{display:none}
19291
19332
  .step-number{height:18px;min-width:18px;border-radius:50%;border:1px solid #e2e5e9;display:grid;place-items:center;font-size:10px}
@@ -19293,17 +19334,16 @@ body{margin:0;min-height:100vh;min-height:100svh;display:flex;align-items:center
19293
19334
  .step.done{color:#537565}.step.done .step-number{background:#edf6f0;color:#287c54;border-color:#edf6f0}
19294
19335
  .heading{text-align:center}h1{font-size:26px;font-weight:650;line-height:1.35;letter-spacing:-.6px;margin:0 0 10px}
19295
19336
  .subtitle{color:#7a818c;font-size:12px;line-height:1.7;margin:0;overflow-wrap:anywhere}
19296
- .game{margin:28px 0 0;padding-top:24px;border-top:1px solid #eceef1}
19297
- .game-summary{display:flex;align-items:center;gap:12px}.game-title{margin:0;font-size:15px;font-weight:600;overflow-wrap:anywhere;min-width:0}
19337
+ .game{margin:24px 0 0;padding-top:20px;border-top:1px solid #eceef1;text-align:center}
19338
+ .game-summary{display:flex;align-items:center;justify-content:center;gap:12px}.game-title{max-width:100%;margin:0;font-size:15px;font-weight:600;overflow-wrap:anywhere;min-width:0}
19298
19339
  .game-icon,.game-avatar{width:44px;height:44px;border-radius:10px;flex-shrink:0;background:#f0f2f5}.game-avatar{object-fit:cover}.game-icon{display:grid;place-items:center;color:#707b8b}
19299
- .game-details{margin:20px 0 0;font-size:13px}.game-details dt{font-size:11px;color:#9298a2;margin-bottom:4px}.game-details dd{margin:0;overflow-wrap:anywhere}
19300
- .game-description{white-space:pre-wrap;line-height:1.65}.game-categories{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:18px}
19340
+ .game-details{margin:16px 0 0;font-size:13px}.game-details>div:first-child,.game-categories>div{display:flex;align-items:baseline;justify-content:center;gap:8px}.game-details dt{flex-shrink:0;font-size:11px;color:#9298a2;margin:0}.game-details dd{min-width:0;margin:0;text-align:left;overflow-wrap:anywhere}
19341
+ .game-description{white-space:pre-wrap;line-height:1.65}.game-categories{display:grid;grid-template-columns:1fr;gap:16px;margin-top:16px}
19301
19342
  .scan-action{text-align:center;font-size:13px;font-weight:500;margin:26px 0 10px}.qr-area{display:flex;justify-content:center}.qr-frame{padding:8px;width:208px;height:208px}.qr-frame img{display:block;width:192px;height:192px;max-width:100%}
19302
19343
  .upload-area{display:flex;align-items:center;flex-direction:column;gap:16px;padding-top:26px}.percentage{font-size:34px;line-height:1;font-variant-numeric:tabular-nums;font-weight:550}.percentage span{font-size:16px;color:#858e9d;padding-left:3px}
19303
19344
  .progress-wrap{width:100%}progress{display:block;width:100%;height:4px;border:none;border-radius:20px;overflow:hidden;background:#edf0f4;accent-color:#4c6378}progress::-webkit-progress-bar{background:#edf0f4;border-radius:20px}progress::-webkit-progress-value{background:#4c6378;border-radius:20px}progress::-moz-progress-bar{background:#4c6378;border-radius:20px}.transfer-label{text-align:center;font-size:11px;color:#8b939f;margin:8px 0 0}
19304
- .footer{text-align:center;font-size:10px;line-height:1.7;color:#a0a5ae;margin:28px 0 0}.footer .dot{display:inline-block;height:5px;width:5px;border-radius:50%;background:#c1995e;vertical-align:middle;margin-right:5px}.mock-badge{width:fit-content;margin:0 0 10px auto;font-size:10px;padding:2px 7px;border:1px solid #dfe2e7;border-radius:5px;color:#707783}
19305
- @media(max-width:380px){body{padding:28px 20px}.steps{gap:7px}.step:after{width:16px;margin-left:1px}h1{font-size:24px}}
19306
- @media(max-height:640px){body{align-items:flex-start;padding-top:28px;padding-bottom:28px}}
19345
+ .footer{text-align:center;font-size:10px;line-height:1.7;color:#a0a5ae;margin:24px 0 0}.footer .dot{display:inline-block;height:5px;width:5px;border-radius:50%;background:#c1995e;vertical-align:middle;margin-right:5px}.mock-badge{width:fit-content;margin:0 0 10px auto;font-size:10px;padding:2px 7px;border:1px solid #dfe2e7;border-radius:5px;color:#707783}
19346
+ @media(max-width:380px){body{padding-left:20px;padding-right:20px}.steps{gap:7px}.step:after{width:16px;margin-left:1px}h1{font-size:24px}}
19307
19347
  `;
19308
19348
 
19309
19349
  const VIBE_PAGE_WORKER = '__vibe-page-server';
@@ -19311,7 +19351,7 @@ const VIBE_PAGE_WORKER = '__vibe-page-server';
19311
19351
  const VIBE_PAGE_HTML_LIMIT = 16 * 1024 * 1024;
19312
19352
  const pageUpdates = `(() => {
19313
19353
  let etag;
19314
- const terminal = () => ['complete', 'failed'].includes(document.querySelector('main')?.dataset.phase);
19354
+ const terminal = () => ['complete', 'preview', 'failed'].includes(document.querySelector('main')?.dataset.phase);
19315
19355
  async function refresh() {
19316
19356
  if (terminal()) return;
19317
19357
  try {
@@ -19405,28 +19445,34 @@ async function renderVibePage(input) {
19405
19445
  : '<span class="game-icon" aria-hidden="true"><svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M7 7h10c2 0 3 2 3 4l1 5c.4 2-1 3-2.5 2l-3-2h-7l-3 2C4 19 2.6 18 3 16l1-5c0-2 1-4 3-4Z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M8 10v4m-2-2h4m6-1h.01M18 13h.01" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></span>';
19406
19446
  const game = `<div class="game"><div class="game-summary">${icon}<p class="game-title">${escape(input.title)}</p></div><dl class="game-details"><div><dt>${zh ? '描述' : 'Description'}</dt><dd class="game-description">${escape(input.description ?? VIBE_PPE_DEFAULT_APP_INFO.description)}</dd></div><div class="game-categories"><div><dt>${zh ? '类型' : 'Category'}</dt><dd>${escape(input.category ?? VIBE_PPE_DEFAULT_APP_INFO.category)}</dd></div><div><dt>${zh ? '子类型' : 'Subcategory'}</dt><dd>${escape(input.subcategory ?? VIBE_PPE_DEFAULT_APP_INFO.subcategory)}</dd></div></div></dl></div>`;
19407
19447
  const checkIcon = '<svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="m3 8 3 3 7-7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
19408
- const title = phase === 'binding'
19409
- ? copy.bindingTitle
19410
- : phase === 'uploading'
19411
- ? copy.uploadingTitle
19412
- : phase === 'failed'
19413
- ? copy.failedTitle
19414
- : copy.completeTitle;
19415
- const subtitle = phase === 'binding'
19416
- ? copy.bindingSubtitle
19417
- : phase === 'uploading'
19418
- ? copy.uploadingSubtitle
19419
- : phase === 'failed'
19420
- ? escape(input.message)
19421
- : copy.completeSubtitle;
19422
- const qr = phase === 'binding' && !input.web && input.qrUrl
19448
+ const title = phase === 'waiting_preview' ? (zh ? '正在准备预览' : 'Preparing preview')
19449
+ : phase === 'preview' ? (zh ? '扫码预览游戏' : 'Scan to preview')
19450
+ : phase === 'binding'
19451
+ ? copy.bindingTitle
19452
+ : phase === 'uploading'
19453
+ ? copy.uploadingTitle
19454
+ : phase === 'failed'
19455
+ ? copy.failedTitle
19456
+ : copy.completeTitle;
19457
+ const subtitle = phase === 'waiting_preview' ? (zh ? '游戏内容已上传,预览就绪后会自动显示二维码。' : 'Game content uploaded. The preview QR will appear when ready.')
19458
+ : phase === 'preview' ? (zh ? '上传完成,使用手机 TikTok 扫描下方二维码。' : 'Upload complete. Scan the QR below with TikTok on your phone.')
19459
+ : phase === 'complete' && !input.simulation ? escape(input.message)
19460
+ : phase === 'binding'
19461
+ ? copy.bindingSubtitle
19462
+ : phase === 'uploading'
19463
+ ? copy.uploadingSubtitle
19464
+ : phase === 'failed'
19465
+ ? escape(input.message)
19466
+ : copy.completeSubtitle;
19467
+ const qr = (phase === 'binding' || phase === 'preview') && !input.web && input.qrUrl
19423
19468
  ? await QRCode.toDataURL(input.qrUrl, {
19424
19469
  width: 384,
19425
19470
  margin: 4,
19426
19471
  errorCorrectionLevel: 'M',
19427
19472
  })
19428
19473
  : '';
19429
- const purpose = copy.bindingCode;
19474
+ const purpose = phase === 'preview' ? (zh ? '游戏预览二维码' : 'Game preview QR') : copy.bindingCode;
19475
+ const done = phase === 'complete' || phase === 'preview';
19430
19476
  const progress = typeof input.progressPercent === 'number' &&
19431
19477
  Number.isFinite(input.progressPercent)
19432
19478
  ? Math.max(0, Math.min(100, Math.round(input.progressPercent)))
@@ -19438,7 +19484,7 @@ async function renderVibePage(input) {
19438
19484
  : '';
19439
19485
  return `<!doctype html><html lang="${zh ? 'zh-CN' : 'en'}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${escape(title)} · ${escape(input.title)}</title><style>${vibePageStyles}</style><script src="?updates" defer></script></head>
19440
19486
  <body><main class="shell" data-phase="${phase}">${input.simulation ? `<p class="mock-badge">${copy.mock}</p>` : ''}
19441
- <section class="card" aria-labelledby="page-title">${phase === 'failed' ? '' : `<ol class="steps" aria-label="${zh ? '上传步骤' : 'Upload steps'}">${copy.steps.map((label, i) => `<li class="step ${phase === 'complete' || i < step ? 'done' : i === step ? 'active' : ''}"${phase !== 'complete' && i === step ? ' aria-current="step"' : ''}><span class="step-number">${phase === 'complete' || i < step ? checkIcon : i + 1}</span>${label}</li>`).join('')}</ol>`}
19487
+ <section class="card" aria-labelledby="page-title">${phase === 'failed' ? '' : `<ol class="steps" aria-label="${zh ? '上传步骤' : 'Upload steps'}">${copy.steps.map((label, i) => `<li class="step ${done || i < step ? 'done' : i === step ? 'active' : ''}"${!done && i === step ? ' aria-current="step"' : ''}><span class="step-number">${done || i < step ? checkIcon : i + 1}</span>${label}</li>`).join('')}</ol>`}
19442
19488
  <div class="heading"><h1 id="page-title">${title}</h1><p class="subtitle" role="status">${subtitle}</p></div>${game}${phase === 'binding' ? `<p class="scan-action">${copy.bindingAction}</p>` : ''}${content}
19443
19489
  </section>
19444
19490
  <p class="footer">${input.simulation ? `<span class="dot" aria-hidden="true"></span>${copy.mockNote}` : copy.footer}</p></main></body></html>`;
@@ -19634,6 +19680,7 @@ const stageText = {
19634
19680
  ],
19635
19681
  uploading: ['授权完成,正在自动上传', 'Authorized; uploading automatically'],
19636
19682
  uploaded: ['上传完成', 'Upload complete'],
19683
+ waiting_preview: ['上传完成,正在准备预览', 'Upload complete; preparing preview'],
19637
19684
  failed: [
19638
19685
  '本次操作失败,请根据错误码处理',
19639
19686
  'Operation failed; check the error code',
@@ -19701,6 +19748,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19701
19748
  let timedOut = false;
19702
19749
  let page;
19703
19750
  let backend = deps.backend;
19751
+ let task;
19704
19752
  let detached = false;
19705
19753
  let progressUpdates = Promise.resolve();
19706
19754
  let ppeLane;
@@ -19844,7 +19892,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19844
19892
  };
19845
19893
  const sourceProjectId = deps.projectId(options);
19846
19894
  emit('creating_task', { platformWrite: backend.mode === 'ppe' || backend.mode === 'ppe-mock' });
19847
- const task = await backend.createTask({
19895
+ task = await backend.createTask({
19848
19896
  source_type: 1,
19849
19897
  source_project_id: sourceProjectId,
19850
19898
  task_idempotency_id: result.operationId,
@@ -19979,7 +20027,6 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19979
20027
  }
19980
20028
  }
19981
20029
  });
19982
- task.importToken = '';
19983
20030
  task.landingUrl = '';
19984
20031
  // A validated upload receipt is authoritative even if result-page rendering fails.
19985
20032
  result = { ...result, uploaded: true, ...(backend.mode === 'ppe-mock'
@@ -19988,20 +20035,48 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19988
20035
  await progressUpdates;
19989
20036
  if (progressPageError)
19990
20037
  throw progressPageError;
20038
+ if (!result.simulation) {
20039
+ emit('waiting_preview', { previewReady: false, terminal: false, nextAction: 'wait',
20040
+ qrPurpose: undefined, authorizationOpenCommand: undefined });
20041
+ await page.update({ ...pageGame, language: getCurrentLanguage(), phase: 'waiting_preview',
20042
+ simulation: false, message: text('waiting_preview') });
20043
+ while (true) {
20044
+ ensureActive();
20045
+ const detail = await backend.getTaskDetail(task, controller.signal);
20046
+ ensureActive();
20047
+ const fields = { taskStatusRaw: detail.statusRaw,
20048
+ ...(detail.clientKey ? { clientKey: detail.clientKey } : {}),
20049
+ ...(detail.appId ? { appId: detail.appId } : {}),
20050
+ ...(detail.codeAssetId ? { codeAssetId: detail.codeAssetId } : {}),
20051
+ ...(detail.minisCenterHistoryId ? { minisCenterHistoryId: detail.minisCenterHistoryId } : {}) };
20052
+ if (detail.previewUrl) {
20053
+ result = { ...result, ...fields, previewUrl: detail.previewUrl, previewReady: true };
20054
+ break;
20055
+ }
20056
+ if (Object.entries(fields).some(([key, value]) => result[key] !== value))
20057
+ emit('waiting_preview', fields);
20058
+ await deps.sleep(pollMs, controller.signal);
20059
+ }
20060
+ }
19991
20061
  await page.update({
19992
20062
  ...pageGame,
19993
20063
  language: getCurrentLanguage(),
19994
- phase: 'complete',
20064
+ phase: result.previewReady ? 'preview' : 'complete',
20065
+ qrUrl: result.previewUrl,
20066
+ purpose: result.previewReady ? 'game_preview' : undefined,
19995
20067
  simulation: result.simulation,
19996
20068
  message: text('uploaded', result.simulation),
19997
20069
  });
19998
20070
  page.detach();
19999
20071
  detached = true;
20000
- return emit('uploaded', { terminal: true, nextAction: 'none' });
20072
+ return emit('uploaded', { terminal: true, nextAction: result.previewReady ? 'scan_qr' : 'none',
20073
+ qrPurpose: result.previewReady ? 'game_preview' : undefined,
20074
+ ...(result.previewReady ? { message: getCurrentLanguage() === 'zh-CN'
20075
+ ? '上传完成,请使用 TikTok 扫码预览游戏。' : 'Upload complete. Scan with TikTok to preview your game.' } : {}) });
20001
20076
  }
20002
20077
  catch (error) {
20003
20078
  const code = timedOut
20004
- ? 'VIBE_TIMEOUT'
20079
+ ? result.uploaded ? 'VIBE_PREVIEW_TIMEOUT' : 'VIBE_TIMEOUT'
20005
20080
  : controller.signal.aborted
20006
20081
  ? 'VIBE_CANCELLED'
20007
20082
  : error instanceof VibeError
@@ -20090,14 +20165,14 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20090
20165
  };
20091
20166
  const message = result.uploaded
20092
20167
  ? getCurrentLanguage() === 'zh-CN'
20093
- ? '上传已完成,但结果页面显示失败,请勿重复上传。'
20094
- : 'Upload completed, but the result page failed. Do not upload again.'
20168
+ ? `游戏内容已上传,但${code === 'VIBE_PREVIEW_TIMEOUT' ? '等待预览超时' : code === 'VIBE_CANCELLED' ? '已停止等待预览' : '预览查询或页面显示未完成'}。请核对任务,请勿重复上传。`
20169
+ : `Game content was uploaded, but ${code === 'VIBE_PREVIEW_TIMEOUT' ? 'preview polling timed out' : code === 'VIBE_CANCELLED' ? 'preview polling was cancelled' : 'preview lookup or display did not complete'}. Check the task; do not upload again.`
20095
20170
  : messages[code]?.[getCurrentLanguage() === 'zh-CN' ? 0 : 1] ||
20096
20171
  text(status);
20097
20172
  controller.abort();
20098
20173
  await progressUpdates;
20099
20174
  // Remove the binding QR on failure too. Keep a readable terminal result page.
20100
- if (page && code !== 'VIBE_CANCELLED') {
20175
+ if (page && (code !== 'VIBE_CANCELLED' || result.uploaded)) {
20101
20176
  try {
20102
20177
  await page.update({
20103
20178
  ...pageMetadata,
@@ -20117,7 +20192,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20117
20192
  }
20118
20193
  return emit(status, {
20119
20194
  terminal: true,
20120
- nextAction: result.uploaded || code === 'VIBE_CANCELLED'
20195
+ nextAction: result.uploaded ? 'check_task' : code === 'VIBE_CANCELLED'
20121
20196
  ? 'none'
20122
20197
  : result.uploadAttempted ||
20123
20198
  (result.taskId &&
@@ -20141,6 +20216,10 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20141
20216
  });
20142
20217
  }
20143
20218
  finally {
20219
+ if (task) {
20220
+ task.importToken = '';
20221
+ task.landingUrl = '';
20222
+ }
20144
20223
  clearTimeout(timeout);
20145
20224
  externalSignal?.removeEventListener('abort', cancel);
20146
20225
  backend?.dispose?.();