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

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.18` 在上传成功后短暂显示成功提示,再平滑返回原扫码界面,保留最初的绑定二维码,终态 `qrPurpose=authorization_binding`。CLI 继续查询同一任务,并通过 `previewUrl` 返回后端预览链接;电脑页面不切换为试玩码。预览超时或查询失败仍保留上传回执,请勿直接重传。详见 [Vibe 接入说明](vibe-upload.md)。
4
+
5
+ Version `0.4.6-vibe-beta.18`: after upload, the page briefly confirms success, then smoothly returns to the original binding QR. The CLI keeps polling the same task and returns its preview link in `previewUrl`; the page QR remains for binding. 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.18`,安装后核对版本与能力声明。Snow Crew 已在 PPE 完成真实绑定上传,本次返回原绑定码的页面体验已由用户验证。预览链接的完整链路此前已在 beta.17 验证;正式环境未开放。
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.18
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, pageUrl returns to the original binding QR. waiting_preview keeps polling the same task until previewUrl is available.
7267
+ previewReady=true means the preview link is available; qrPurpose=authorization_binding describes the original QR retained on pageUrl. A preview timeout preserves uploaded=true; 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
+ 上传成功后 pageUrl 平滑返回原绑定二维码;waiting_preview 继续查询同一任务直到取得 previewUrl。
7717
+ previewReady=true 表示预览链接已返回;qrPurpose=authorization_binding 表示 pageUrl 保留原绑定码。等待超时仍保留 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.18";
16382
16386
  var engines = {
16383
16387
  node: ">=20.19.1 <21 || >=22"
16384
16388
  };
@@ -17265,6 +17269,18 @@ function getCapabilities() {
17265
17269
  httpContract: 'minis-import-task',
17266
17270
  authorizationQr: true,
17267
17271
  previewQr: false,
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: 'authorization_binding',
17278
+ pageQrSource: 'original-landing-url',
17279
+ sameTask: true,
17280
+ timeout: 'shared-operation-timeout',
17281
+ failurePreservesUploaded: true,
17282
+ deviceRunVerified: false,
17283
+ },
17268
17284
  realAuthorizationVerified: true,
17269
17285
  realUploadVerified: true,
17270
17286
  verificationScope: { backend: 'ppe', authorizationMode: 'qr', assets: 'test-fixtures', date: '2026-09-16', production: false },
@@ -18618,7 +18634,7 @@ var authOpen = /*#__PURE__*/Object.freeze({
18618
18634
  const prefix = '/bff/app_info/minis_import/';
18619
18635
  const MINIS_IMPORT_PATHS = {
18620
18636
  create: `${prefix}create_minis_import_task_for_agent/`,
18621
- status: `${prefix}get_minis_import_task_status_for_agent/`,
18637
+ status: `${prefix}get_minis_import_task_detail_for_agent/`,
18622
18638
  icon: `${prefix}upload_minis_import_app_icon_for_agent/`,
18623
18639
  info: `${prefix}submit_minis_import_info_for_agent/`,
18624
18640
  package: `${prefix}upload_minis_import_package_for_agent/`,
@@ -18677,6 +18693,31 @@ function createBackend(mode, transport) {
18677
18693
  });
18678
18694
  return {
18679
18695
  mode,
18696
+ async getTaskDetail(task, signal) {
18697
+ const data = await call({ method: 'GET', path: MINIS_IMPORT_PATHS.status,
18698
+ data: credentials(task), signal });
18699
+ const raw = status(data.status);
18700
+ if (raw === 8)
18701
+ throw new VibeError('VIBE_AUTH_EXPIRED');
18702
+ if (raw === 7)
18703
+ throw new VibeError('VIBE_TASK_FAILED');
18704
+ if (data.ready_for_input !== undefined && typeof data.ready_for_input !== 'boolean')
18705
+ throw new VibeError('VIBE_INVALID_RESPONSE');
18706
+ const detail = { statusRaw: raw, readyForInput: data.ready_for_input === true };
18707
+ for (const [wire, field] of [['app_id', 'appId'], ['code_asset_id', 'codeAssetId'],
18708
+ ['minis_center_history_id', 'minisCenterHistoryId']]) {
18709
+ if (data[wire] !== undefined && data[wire] !== '')
18710
+ detail[field] = taskId(data[wire]);
18711
+ }
18712
+ if (data.client_key !== undefined && data.client_key !== '') {
18713
+ if (!nonempty(data.client_key))
18714
+ throw new VibeError('VIBE_INVALID_RESPONSE');
18715
+ detail.clientKey = data.client_key;
18716
+ }
18717
+ if (data.preview_url !== undefined && data.preview_url !== '')
18718
+ detail.previewUrl = safeWebUrl(data.preview_url);
18719
+ return detail;
18720
+ },
18680
18721
  async createTask(input, signal) {
18681
18722
  const data = await call({
18682
18723
  method: 'POST',
@@ -18807,6 +18848,7 @@ function createPpeMockBackend(lane, adapter, scenario = 'success') {
18807
18848
  };
18808
18849
  return {
18809
18850
  mode: 'ppe-mock',
18851
+ getTaskDetail: (task, signal) => real.getTaskDetail(task, signal),
18810
18852
  uploadAvailable: true,
18811
18853
  dispose: () => real.dispose?.(),
18812
18854
  async createTask(input, signal) {
@@ -19283,9 +19325,9 @@ const vibePageStyles = `
19283
19325
 
19284
19326
  :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
19327
  *{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}
19328
+ 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}
19329
+ .shell{position:relative;width:100%;max-width:400px}
19330
+ .steps{display:flex;align-items:center;justify-content:center;gap:10px;margin:0 0 24px;padding:0;list-style:none}
19289
19331
  .step{display:flex;align-items:center;gap:6px;font-size:11px;color:#9298a2;white-space:nowrap}
19290
19332
  .step:after{content:"";height:1px;width:25px;margin-left:4px;background:#e9ebef}.step:last-child:after{display:none}
19291
19333
  .step-number{height:18px;min-width:18px;border-radius:50%;border:1px solid #e2e5e9;display:grid;place-items:center;font-size:10px}
@@ -19293,17 +19335,17 @@ body{margin:0;min-height:100vh;min-height:100svh;display:flex;align-items:center
19293
19335
  .step.done{color:#537565}.step.done .step-number{background:#edf6f0;color:#287c54;border-color:#edf6f0}
19294
19336
  .heading{text-align:center}h1{font-size:26px;font-weight:650;line-height:1.35;letter-spacing:-.6px;margin:0 0 10px}
19295
19337
  .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}
19338
+ .game{margin:24px 0 0;padding-top:20px;border-top:1px solid #eceef1;text-align:center}
19339
+ .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
19340
  .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}
19341
+ .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}
19342
+ .game-description{white-space:pre-wrap;line-height:1.65}.game-categories{display:grid;grid-template-columns:1fr;gap:16px;margin-top:16px}
19301
19343
  .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%}
19344
+ .game+.qr-area{margin-top:20px}.scan-hint{text-align:center;font-size:11px;color:#7a818c;line-height:1.7;margin:8px 0 0}
19302
19345
  .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
19346
  .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}}
19347
+ .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}
19348
+ @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
19349
  `;
19308
19350
 
19309
19351
  const VIBE_PAGE_WORKER = '__vibe-page-server';
@@ -19311,7 +19353,38 @@ const VIBE_PAGE_WORKER = '__vibe-page-server';
19311
19353
  const VIBE_PAGE_HTML_LIMIT = 16 * 1024 * 1024;
19312
19354
  const pageUpdates = `(() => {
19313
19355
  let etag;
19314
- const terminal = () => ['complete', 'failed'].includes(document.querySelector('main')?.dataset.phase);
19356
+ const terminal = () => ['complete', 'preview', 'failed'].includes(document.querySelector('main')?.dataset.phase);
19357
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
19358
+ async function replace(current, updated) {
19359
+ const animate = current.dataset.phase !== updated.dataset.phase && current.animate &&
19360
+ !window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
19361
+ const height = current.getBoundingClientRect().height;
19362
+ if (animate) await current.animate([{opacity: 1}, {opacity: 0}], {duration: 140, fill: 'forwards'}).finished.catch(() => {});
19363
+ current.replaceWith(updated);
19364
+ if (animate) await updated.animate([
19365
+ {opacity: 0, height: height + 'px', overflow: 'hidden'},
19366
+ {opacity: 1, height: updated.getBoundingClientRect().height + 'px', overflow: 'hidden'}
19367
+ ], {duration: 280, easing: 'ease-out'}).finished.catch(() => {});
19368
+ }
19369
+ async function show(updated) {
19370
+ let current = document.querySelector('main');
19371
+ if (!current || current.outerHTML === updated.outerHTML) return;
19372
+ if (updated.dataset.uploadedBinding === 'true' && current.dataset.uploadedBinding !== 'true') {
19373
+ // Return to the original binding QR after showing the upload receipt,
19374
+ // even when polling skips intermediate upload states.
19375
+ const success = updated.cloneNode(true);
19376
+ success.dataset.phase = 'upload_success';
19377
+ delete success.dataset.uploadedBinding;
19378
+ const heading = success.querySelector('.heading');
19379
+ success.querySelector('h1').textContent = heading.dataset.uploadTitle;
19380
+ success.querySelector('.subtitle').textContent = heading.dataset.uploadSubtitle;
19381
+ success.querySelectorAll('.qr-area, .scan-action, .scan-hint').forEach(node => node.remove());
19382
+ await replace(current, success);
19383
+ current = success;
19384
+ await delay(1200);
19385
+ }
19386
+ await replace(current, updated);
19387
+ }
19315
19388
  async function refresh() {
19316
19389
  if (terminal()) return;
19317
19390
  try {
@@ -19319,8 +19392,8 @@ const pageUpdates = `(() => {
19319
19392
  if (response.ok) {
19320
19393
  etag = response.headers.get('etag');
19321
19394
  const next = new DOMParser().parseFromString(await response.text(), 'text/html');
19322
- const current = document.querySelector('main'), updated = next.querySelector('main');
19323
- if (current && updated && current.outerHTML !== updated.outerHTML) current.replaceWith(updated);
19395
+ const updated = next.querySelector('main');
19396
+ if (updated) await show(updated);
19324
19397
  document.title = next.title;
19325
19398
  }
19326
19399
  } catch { /* Keep the current page readable during a temporary connection failure. */ }
@@ -19333,6 +19406,8 @@ async function renderVibePage(input) {
19333
19406
  const zh = input.language !== 'en-US';
19334
19407
  const phase = input.phase ||
19335
19408
  (input.purpose === 'authorization_binding' ? 'binding' : 'uploading');
19409
+ const uploadedBinding = !input.simulation && input.purpose === 'authorization_binding' &&
19410
+ (phase === 'waiting_preview' || phase === 'preview' || phase === 'complete');
19336
19411
  const copy = zh
19337
19412
  ? {
19338
19413
  steps: ['扫码授权', '上传游戏', '完成'],
@@ -19357,7 +19432,6 @@ async function renderVibePage(input) {
19357
19432
  bindingCode: '授权绑定二维码',
19358
19433
  accepted: input.simulation ? '模拟完成' : '已上传',
19359
19434
  mockNote: input.hybrid ? '真实创建 / 查询任务 · 授权 / 上传为模拟 · 不改变真实任务状态' : '模拟运行 · 无需扫码,不会真实上传',
19360
- footer: '临时页面 · 15 分钟后关闭',
19361
19435
  }
19362
19436
  : {
19363
19437
  steps: ['Authorize', 'Upload', 'Done'],
@@ -19386,7 +19460,6 @@ async function renderVibePage(input) {
19386
19460
  bindingCode: 'Authorization QR',
19387
19461
  accepted: input.simulation ? 'Simulation complete' : 'Uploaded',
19388
19462
  mockNote: input.hybrid ? 'Real task creation / polling · Mock authorization / upload · Real task status unchanged' : 'Mock only · no scan needed, nothing is uploaded',
19389
- footer: 'Temporary page · closes after 15 minutes',
19390
19463
  };
19391
19464
  if (input.web) {
19392
19465
  copy.steps[0] = zh ? '网页授权' : 'Web authorization';
@@ -19405,28 +19478,42 @@ async function renderVibePage(input) {
19405
19478
  : '<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
19479
  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
19480
  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
19481
+ const uploadSuccessTitle = zh ? '上传成功' : 'Upload complete';
19482
+ const uploadSuccessSubtitle = zh
19483
+ ? '游戏内容已上传,正在返回扫码页面。'
19484
+ : 'Your game has been uploaded. Returning to the scan page.';
19485
+ const title = uploadedBinding ? (zh ? '扫码打开绑定页面' : 'Scan to open the binding page')
19486
+ : phase === 'waiting_preview' ? (zh ? '正在准备预览' : 'Preparing preview')
19487
+ : phase === 'preview' ? (zh ? '扫码预览游戏' : 'Scan to preview')
19488
+ : phase === 'binding'
19489
+ ? copy.bindingTitle
19490
+ : phase === 'uploading'
19491
+ ? copy.uploadingTitle
19492
+ : phase === 'failed'
19493
+ ? copy.failedTitle
19494
+ : copy.completeTitle;
19495
+ const subtitle = uploadedBinding ? (phase === 'complete' ? escape(input.message) : zh
19496
+ ? '本次游戏代码包已上传完成,再次扫码即可查看预览版本并提交审核。'
19497
+ : 'Your game package has been uploaded. Scan again to view the preview version and submit it for review.')
19498
+ : phase === 'waiting_preview' ? (zh ? '游戏内容已上传,预览就绪后会自动显示二维码。' : 'Game content uploaded. The preview QR will appear when ready.')
19499
+ : phase === 'preview' ? (zh ? '上传完成,使用手机 TikTok 扫描下方二维码。' : 'Upload complete. Scan the QR below with TikTok on your phone.')
19500
+ : phase === 'complete' && !input.simulation ? escape(input.message)
19501
+ : phase === 'binding'
19502
+ ? copy.bindingSubtitle
19503
+ : phase === 'uploading'
19504
+ ? copy.uploadingSubtitle
19505
+ : phase === 'failed'
19506
+ ? escape(input.message)
19507
+ : copy.completeSubtitle;
19508
+ const qr = (phase === 'binding' || phase === 'preview' || uploadedBinding) && !input.web && input.qrUrl
19423
19509
  ? await QRCode.toDataURL(input.qrUrl, {
19424
19510
  width: 384,
19425
19511
  margin: 4,
19426
19512
  errorCorrectionLevel: 'M',
19427
19513
  })
19428
19514
  : '';
19429
- const purpose = copy.bindingCode;
19515
+ const purpose = phase === 'preview' && !uploadedBinding ? (zh ? '游戏预览二维码' : 'Game preview QR') : copy.bindingCode;
19516
+ const done = phase === 'complete' || phase === 'preview' || phase === 'waiting_preview';
19430
19517
  const progress = typeof input.progressPercent === 'number' &&
19431
19518
  Number.isFinite(input.progressPercent)
19432
19519
  ? Math.max(0, Math.min(100, Math.round(input.progressPercent)))
@@ -19437,11 +19524,11 @@ async function renderVibePage(input) {
19437
19524
  ? `<div class="upload-area"><div class="percentage">${progress}<span>%</span></div><div class="progress-wrap"><progress aria-label="${copy.uploadLabel}" max="100" value="${progress}"></progress><p class="transfer-label">${copy.uploadLabel}</p></div></div>`
19438
19525
  : '';
19439
19526
  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
- <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>`}
19442
- <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}
19527
+ <body><main class="shell" data-phase="${phase}"${uploadedBinding ? ' data-uploaded-binding="true"' : ''}>${input.simulation ? `<p class="mock-badge">${copy.mock}</p>` : ''}
19528
+ <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>`}
19529
+ <div class="heading"${uploadedBinding ? ` data-upload-title="${escape(uploadSuccessTitle)}" data-upload-subtitle="${escape(uploadSuccessSubtitle)}"` : ''}><h1 id="page-title">${title}</h1><p class="subtitle" role="status">${subtitle}</p></div>${game}${phase === 'binding' || uploadedBinding ? `<p class="scan-action">${copy.bindingAction}</p>` : ''}${content}${uploadedBinding ? `<p class="scan-hint">${zh ? '这是本次上传的原绑定二维码,无需重复上传。' : 'This is the original binding QR for this upload. No need to upload again.'}</p>` : ''}
19443
19530
  </section>
19444
- <p class="footer">${input.simulation ? `<span class="dot" aria-hidden="true"></span>${copy.mockNote}` : copy.footer}</p></main></body></html>`;
19531
+ ${input.simulation ? `<p class="footer"><span class="dot" aria-hidden="true"></span>${copy.mockNote}</p>` : ''}</main></body></html>`;
19445
19532
  }
19446
19533
  async function createVibePageServer() {
19447
19534
  const route = `/${crypto.randomBytes(24).toString('hex')}`;
@@ -19634,6 +19721,7 @@ const stageText = {
19634
19721
  ],
19635
19722
  uploading: ['授权完成,正在自动上传', 'Authorized; uploading automatically'],
19636
19723
  uploaded: ['上传完成', 'Upload complete'],
19724
+ waiting_preview: ['上传完成,正在准备预览', 'Upload complete; preparing preview'],
19637
19725
  failed: [
19638
19726
  '本次操作失败,请根据错误码处理',
19639
19727
  'Operation failed; check the error code',
@@ -19701,6 +19789,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19701
19789
  let timedOut = false;
19702
19790
  let page;
19703
19791
  let backend = deps.backend;
19792
+ let task;
19704
19793
  let detached = false;
19705
19794
  let progressUpdates = Promise.resolve();
19706
19795
  let ppeLane;
@@ -19844,7 +19933,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19844
19933
  };
19845
19934
  const sourceProjectId = deps.projectId(options);
19846
19935
  emit('creating_task', { platformWrite: backend.mode === 'ppe' || backend.mode === 'ppe-mock' });
19847
- const task = await backend.createTask({
19936
+ task = await backend.createTask({
19848
19937
  source_type: 1,
19849
19938
  source_project_id: sourceProjectId,
19850
19939
  task_idempotency_id: result.operationId,
@@ -19979,7 +20068,6 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19979
20068
  }
19980
20069
  }
19981
20070
  });
19982
- task.importToken = '';
19983
20071
  task.landingUrl = '';
19984
20072
  // A validated upload receipt is authoritative even if result-page rendering fails.
19985
20073
  result = { ...result, uploaded: true, ...(backend.mode === 'ppe-mock'
@@ -19988,20 +20076,49 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19988
20076
  await progressUpdates;
19989
20077
  if (progressPageError)
19990
20078
  throw progressPageError;
20079
+ if (!result.simulation) {
20080
+ emit('waiting_preview', { previewReady: false, terminal: false, nextAction: 'wait',
20081
+ qrPurpose: 'authorization_binding', authorizationOpenCommand: undefined });
20082
+ await page.update({ ...pageGame, language: getCurrentLanguage(), phase: 'waiting_preview',
20083
+ qrUrl: authorizationUrl, purpose: 'authorization_binding',
20084
+ simulation: false, message: text('waiting_preview') });
20085
+ while (true) {
20086
+ ensureActive();
20087
+ const detail = await backend.getTaskDetail(task, controller.signal);
20088
+ ensureActive();
20089
+ const fields = { taskStatusRaw: detail.statusRaw,
20090
+ ...(detail.clientKey ? { clientKey: detail.clientKey } : {}),
20091
+ ...(detail.appId ? { appId: detail.appId } : {}),
20092
+ ...(detail.codeAssetId ? { codeAssetId: detail.codeAssetId } : {}),
20093
+ ...(detail.minisCenterHistoryId ? { minisCenterHistoryId: detail.minisCenterHistoryId } : {}) };
20094
+ if (detail.previewUrl) {
20095
+ result = { ...result, ...fields, previewUrl: detail.previewUrl, previewReady: true };
20096
+ break;
20097
+ }
20098
+ if (Object.entries(fields).some(([key, value]) => result[key] !== value))
20099
+ emit('waiting_preview', fields);
20100
+ await deps.sleep(pollMs, controller.signal);
20101
+ }
20102
+ }
19991
20103
  await page.update({
19992
20104
  ...pageGame,
19993
20105
  language: getCurrentLanguage(),
19994
- phase: 'complete',
20106
+ phase: result.previewReady ? 'preview' : 'complete',
20107
+ qrUrl: result.simulation ? undefined : authorizationUrl,
20108
+ purpose: result.simulation ? undefined : 'authorization_binding',
19995
20109
  simulation: result.simulation,
19996
20110
  message: text('uploaded', result.simulation),
19997
20111
  });
19998
20112
  page.detach();
19999
20113
  detached = true;
20000
- return emit('uploaded', { terminal: true, nextAction: 'none' });
20114
+ return emit('uploaded', { terminal: true, nextAction: result.previewReady ? 'scan_qr' : 'none',
20115
+ qrPurpose: result.previewReady ? 'authorization_binding' : undefined,
20116
+ ...(result.previewReady ? { message: getCurrentLanguage() === 'zh-CN'
20117
+ ? '上传完成。原绑定二维码已保留,可使用 TikTok 再次扫码打开绑定页面。' : 'Upload complete. The original binding QR is available to reopen the binding page with TikTok.' } : {}) });
20001
20118
  }
20002
20119
  catch (error) {
20003
20120
  const code = timedOut
20004
- ? 'VIBE_TIMEOUT'
20121
+ ? result.uploaded ? 'VIBE_PREVIEW_TIMEOUT' : 'VIBE_TIMEOUT'
20005
20122
  : controller.signal.aborted
20006
20123
  ? 'VIBE_CANCELLED'
20007
20124
  : error instanceof VibeError
@@ -20090,14 +20207,14 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20090
20207
  };
20091
20208
  const message = result.uploaded
20092
20209
  ? getCurrentLanguage() === 'zh-CN'
20093
- ? '上传已完成,但结果页面显示失败,请勿重复上传。'
20094
- : 'Upload completed, but the result page failed. Do not upload again.'
20210
+ ? `游戏内容已上传,但${code === 'VIBE_PREVIEW_TIMEOUT' ? '等待预览超时' : code === 'VIBE_CANCELLED' ? '已停止等待预览' : '预览查询或页面显示未完成'}。请核对任务,请勿重复上传。`
20211
+ : `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
20212
  : messages[code]?.[getCurrentLanguage() === 'zh-CN' ? 0 : 1] ||
20096
20213
  text(status);
20097
20214
  controller.abort();
20098
20215
  await progressUpdates;
20099
20216
  // Remove the binding QR on failure too. Keep a readable terminal result page.
20100
- if (page && code !== 'VIBE_CANCELLED') {
20217
+ if (page && (code !== 'VIBE_CANCELLED' || result.uploaded)) {
20101
20218
  try {
20102
20219
  await page.update({
20103
20220
  ...pageMetadata,
@@ -20106,7 +20223,11 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20106
20223
  simulation: result.simulation,
20107
20224
  hybrid: result.backend === 'ppe-mock',
20108
20225
  phase: result.uploaded ? 'complete' : 'failed',
20109
- message,
20226
+ message: result.uploaded && !result.simulation
20227
+ ? getCurrentLanguage() === 'zh-CN'
20228
+ ? '本次游戏代码包已上传完成,可在手机端查看预览版本并提交审核。'
20229
+ : 'Your game package has been uploaded. You can view the preview version and submit it for review on your phone.'
20230
+ : message,
20110
20231
  });
20111
20232
  page.detach();
20112
20233
  detached = true;
@@ -20117,7 +20238,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20117
20238
  }
20118
20239
  return emit(status, {
20119
20240
  terminal: true,
20120
- nextAction: result.uploaded || code === 'VIBE_CANCELLED'
20241
+ nextAction: result.uploaded ? 'check_task' : code === 'VIBE_CANCELLED'
20121
20242
  ? 'none'
20122
20243
  : result.uploadAttempted ||
20123
20244
  (result.taskId &&
@@ -20141,6 +20262,10 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20141
20262
  });
20142
20263
  }
20143
20264
  finally {
20265
+ if (task) {
20266
+ task.importToken = '';
20267
+ task.landingUrl = '';
20268
+ }
20144
20269
  clearTimeout(timeout);
20145
20270
  externalSignal?.removeEventListener('abort', cancel);
20146
20271
  backend?.dispose?.();