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

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/dist/index.js CHANGED
@@ -7248,6 +7248,7 @@ const messages = {
7248
7248
  'cli.command.game.categories.category': 'Filter by an exact category value, e.g. Puzzle',
7249
7249
  'cli.command.game.categories.unknown': 'Unknown category. Run ttmg game categories --format json for valid values.',
7250
7250
  'cli.command.game.categories.invalidFormat': 'Use text, json, or ndjson output.',
7251
+ 'cli.command.upload.minisType': 'MINI_GAME (default) or MINI_TOOL (local PPE trial; Native ZIP, Efficiency & Office)',
7251
7252
  'cli.command.upload.vibeCategory': 'Optional game category; provide with --subcategory (--vibe). Query ttmg game categories --format json',
7252
7253
  'cli.command.upload.vibeDescription': 'Optional game description (--vibe); default: Discover the fun of TikTok Mini Games.',
7253
7254
  'cli.command.upload.vibeSubcategory': 'Optional subcategory belonging to --category; omit both to keep Other/Other (--vibe)',
@@ -7263,8 +7264,9 @@ Vibe QR upload (real PPE trial):
7263
7264
  Keep this process running. At waiting_authorization, scan the QR on pageUrl with TikTok on your phone.
7264
7265
  pageUrl is the local QR/progress page on the CLI computer. Reopen it with authorizationOpenCommand.
7265
7266
  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.
7267
+ After upload, waiting_preview keeps polling the same task until previewUrl is available, including status 9 (waiting for review submission).
7268
+ When previewReady=true, pageUrl keeps the original QR on the left for checking progress and shows the preview QR on the right. qrPurpose is game_preview or tool_preview. A preview timeout preserves uploaded=true; do not reupload.
7269
+ The operation timeout defaults to 1800 seconds (maximum 3600). The page stays available during the operation and for 30 minutes after the CLI finishes.
7268
7270
  Real success: terminal=true, status=uploaded, uploaded=true, simulation=false. On failure, read errorCode and nextAction.
7269
7271
  The local QR page opens for every output format; --no-open disables automatic opening.
7270
7272
  The page displays the submitted title, icon, description, category and subcategory.
@@ -7483,7 +7485,7 @@ Vibe QR upload (real PPE trial):
7483
7485
  'cli.command.upload.vibeApi': 'Minis Import API origin on a loopback IP only (--vibe)',
7484
7486
  'cli.command.upload.vibeAuthorizeUrl': 'Obsolete: remove this option; task creation returns landing_url (--vibe)',
7485
7487
  'cli.command.upload.vibePollInterval': 'Override task polling interval; otherwise use poll_interval_seconds (--vibe)',
7486
- 'cli.command.upload.vibeTimeout': 'Operation timeout in seconds (--vibe; default 300)',
7488
+ 'cli.command.upload.vibeTimeout': 'Operation timeout in seconds (--vibe; default 1800, maximum 3600)',
7487
7489
  'cli.command.upload.vibeRequired': 'These options require --vibe; the existing platform upload has not been executed.',
7488
7490
  'cli.command.testAccount.desc': 'Manage Mini Game test-account access',
7489
7491
  'cli.command.testAccount.add.desc': 'Generate the authorization entry for adding a TikTok test account',
@@ -7698,6 +7700,7 @@ Vibe QR upload (real PPE trial):
7698
7700
  'cli.command.game.categories.category': '按类型的精确值筛选,例如 Puzzle',
7699
7701
  'cli.command.game.categories.unknown': '类型不存在。使用 ttmg game categories --format json 查询有效值。',
7700
7702
  'cli.command.game.categories.invalidFormat': '输出格式请使用 text、json 或 ndjson。',
7703
+ 'cli.command.upload.minisType': 'MINI_GAME(默认)或 MINI_TOOL(本地 PPE 试用;Native ZIP、效率办公类)',
7701
7704
  'cli.command.upload.vibeCategory': '可选游戏类型,须与 --subcategory 同传(--vibe);用 ttmg game categories --format json 查询',
7702
7705
  'cli.command.upload.vibeDescription': '可选游戏描述(--vibe);默认值:Discover the fun of TikTok Mini Games.',
7703
7706
  'cli.command.upload.vibeSubcategory': '所属类型的可选子类型;两者均不传保留 Other/Other(--vibe)',
@@ -7713,8 +7716,9 @@ Vibe 扫码上传(真实 PPE 试用):
7713
7716
  保持原进程运行。在 waiting_authorization 阶段,使用手机 TikTok 扫描 pageUrl 页面中的二维码。
7714
7717
  pageUrl 是 CLI 所在电脑的二维码/进度页;重新打开时,执行返回的 authorizationOpenCommand。
7715
7718
  CLI 负责轮询,授权后自动上传 avatar、app_info、package,不再启动第二次上传。
7716
- 上传成功后 pageUrl 平滑返回原绑定二维码;waiting_preview 继续查询同一任务直到取得 previewUrl。
7717
- previewReady=true 表示预览链接已返回;qrPurpose=authorization_binding 表示 pageUrl 保留原绑定码。等待超时仍保留 uploaded=true,请核对任务,不要重传。
7719
+ 上传成功后 waiting_preview 继续查询同一任务直到取得 previewUrl,包括状态 9(等待提交代码审核)。
7720
+ previewReady=true 时,pageUrl 左侧保留原码用于查看进度,右侧显示预览码;qrPurpose 为 game_preview 或 tool_preview。等待超时仍保留 uploaded=true,请核对任务,不要重传。
7721
+ 整体超时默认 1800 秒,最大 3600 秒;页面在任务进行中保持可用,CLI 结束后再保留 30 分钟。
7718
7722
  真实成功:terminal=true、status=uploaded、uploaded=true、simulation=false。失败时读取 errorCode 和 nextAction。
7719
7723
  所有输出格式都会打开本地二维码页;--no-open 可关闭自动打开。
7720
7724
  页面展示本次提交的标题、头像、描述、类型和子类型。
@@ -7933,7 +7937,7 @@ Vibe 扫码上传(真实 PPE 试用):
7933
7937
  'cli.command.upload.vibeApi': 'Minis Import 接口地址,仅接受回环 IP(--vibe)',
7934
7938
  'cli.command.upload.vibeAuthorizeUrl': '已废弃,请移除此参数;授权地址由创建任务返回(--vibe)',
7935
7939
  'cli.command.upload.vibePollInterval': '覆盖 task 轮询间隔;默认使用接口 poll_interval_seconds(--vibe)',
7936
- 'cli.command.upload.vibeTimeout': '操作超时秒数(--vibe,默认 300)',
7940
+ 'cli.command.upload.vibeTimeout': '操作超时秒数(--vibe,默认 1800,最大 3600)',
7937
7941
  'cli.command.upload.vibeRequired': '这些参数需要 --vibe;没有执行原有平台上传。',
7938
7942
  'cli.command.testAccount.desc': '管理小游戏测试账号访问',
7939
7943
  'cli.command.testAccount.add.desc': '生成添加 TikTok 测试账号的授权入口',
@@ -16382,7 +16386,7 @@ async function authStatus(options = {}) {
16382
16386
  return result;
16383
16387
  }
16384
16388
 
16385
- var version = "0.4.6-vibe-beta.18";
16389
+ var version = "0.4.6-vibe-beta.20";
16386
16390
  var engines = {
16387
16391
  node: ">=20.19.1 <21 || >=22"
16388
16392
  };
@@ -16405,6 +16409,23 @@ const VIBE_INPUT_REQUIREMENTS = {
16405
16409
  categoryPairRequiredWhenProvided: true,
16406
16410
  defaultCategory: { category: 'Other', subcategory: 'Other' },
16407
16411
  };
16412
+ const VIBE_TOOL_INPUT_REQUIREMENTS = {
16413
+ ...VIBE_INPUT_REQUIREMENTS,
16414
+ defaultMetadataProfile: 'ppe-tool-trial',
16415
+ optionalSkillInputs: ['description', 'minisType'],
16416
+ defaultDescription: 'A simple productivity tool for everyday tasks.',
16417
+ categoryQuery: undefined,
16418
+ categoryPairRequiredWhenProvided: false,
16419
+ defaultCategory: { category: 'EFFICIENCY_AND_OFFICE', subcategory: '' },
16420
+ region: 'US', minSDKVersion: '1.0.0',
16421
+ };
16422
+ function resolveMinisType(value) {
16423
+ if (value === undefined)
16424
+ return 'MINI_GAME';
16425
+ if (value !== 'MINI_GAME' && value !== 'MINI_TOOL')
16426
+ throw new VibeError('VIBE_INVALID_MINIS_TYPE');
16427
+ return value;
16428
+ }
16408
16429
  class VibeError extends Error {
16409
16430
  constructor(code) {
16410
16431
  super(code);
@@ -17142,6 +17163,10 @@ function getCapabilities() {
17142
17163
  networkRequired: false,
17143
17164
  },
17144
17165
  vibeUpload: {
17166
+ minisType: {
17167
+ option: '--minis-type', default: 'MINI_GAME', values: ['MINI_GAME', 'MINI_TOOL'],
17168
+ tool: { readiness: 'local-ppe-trial', deviceVerified: false, category: 'EFFICIENCY_AND_OFFICE', region: 'US', minSDKVersion: '1.0.0', assetType: 'NativeRuntime', metadataOverrides: false },
17169
+ },
17145
17170
  command: 'ttmg upload --vibe --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
17146
17171
  mockCommand: 'ttmg upload --vibe --mock --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
17147
17172
  guide: 'vibe-upload.md',
@@ -17268,14 +17293,16 @@ function getCapabilities() {
17268
17293
  provisionalHttpAdapter: 'loopback-only',
17269
17294
  httpContract: 'minis-import-task',
17270
17295
  authorizationQr: true,
17271
- previewQr: false,
17296
+ previewQr: true,
17272
17297
  preview: {
17273
17298
  taskEndpoint: 'get_minis_import_task_detail_for_agent',
17274
17299
  waitingStatus: 'waiting_preview',
17275
17300
  resultFields: ['previewUrl', 'previewReady', 'clientKey', 'appId', 'codeAssetId', 'minisCenterHistoryId'],
17276
17301
  readiness: 'nonempty-valid-preview-url',
17277
- qrPurpose: 'authorization_binding',
17278
- pageQrSource: 'original-landing-url',
17302
+ qrPurpose: 'game_preview',
17303
+ toolQrPurpose: 'tool_preview',
17304
+ pageQrSource: 'preview-url',
17305
+ retainsBindingQr: true,
17279
17306
  sameTask: true,
17280
17307
  timeout: 'shared-operation-timeout',
17281
17308
  failurePreservesUploaded: true,
@@ -18039,6 +18066,7 @@ withVerboseOption(program
18039
18066
  .option('--serve-preview-page', t('cli.command.upload.servePreviewPage'))
18040
18067
  .option('--no-open', t('cli.command.upload.noOpen'))
18041
18068
  .option('--vibe', t('cli.command.upload.vibe'))
18069
+ .option('--minis-type <type>', t('cli.command.upload.minisType'))
18042
18070
  .addOption(new commander.Option('--web', t('cli.command.upload.vibeWeb')).hideHelp())
18043
18071
  // Vibe v2 backend selection (including opt-in ppe-mock) stays outside the Skill workflow.
18044
18072
  .option('--title <title>', t('cli.command.upload.vibeTitle'))
@@ -18064,6 +18092,7 @@ withVerboseOption(program
18064
18092
  }
18065
18093
  if ([
18066
18094
  cmd.title,
18095
+ cmd.minisType,
18067
18096
  cmd.description,
18068
18097
  cmd.icon,
18069
18098
  cmd.appInfo,
@@ -18664,7 +18693,10 @@ function taskId(value) {
18664
18693
  return value;
18665
18694
  }
18666
18695
  function status(value) {
18667
- if (!Number.isInteger(value) || Number(value) < 1 || Number(value) > 8)
18696
+ // IDL status 9 waits for code review submission; preview may already be ready.
18697
+ // Keep polling it just like other non-failure states and use preview_url to
18698
+ // determine preview readiness, rather than treating review as a prerequisite.
18699
+ if (!Number.isInteger(value) || Number(value) < 1 || Number(value) > 9)
18668
18700
  throw new VibeError('VIBE_INVALID_RESPONSE');
18669
18701
  return Number(value);
18670
18702
  }
@@ -18775,7 +18807,7 @@ function createBackend(mode, transport) {
18775
18807
  path: MINIS_IMPORT_PATHS.info,
18776
18808
  data: {
18777
18809
  ...credentials(task),
18778
- app_info: { ...(appInfo || VIBE_PPE_DEFAULT_APP_INFO), app_name: title, icon_uri: iconUri },
18810
+ app_info: { ...Object.fromEntries(Object.entries(appInfo || VIBE_PPE_DEFAULT_APP_INFO).filter(([, value]) => value !== '')), app_name: title, icon_uri: iconUri },
18779
18811
  },
18780
18812
  signal,
18781
18813
  });
@@ -18967,7 +18999,7 @@ function createMockBackend(scenario = 'success') {
18967
18999
  if (path === MINIS_IMPORT_PATHS.create) {
18968
19000
  if (created ||
18969
19001
  data.source_type !== 1 ||
18970
- data.minis_type !== 'MINI_GAME' ||
19002
+ !['MINI_GAME', 'MINI_TOOL'].includes(data.minis_type) ||
18971
19003
  !nonempty(data.source_project_id) ||
18972
19004
  !nonempty(data.task_idempotency_id))
18973
19005
  throw new VibeError('VIBE_INVALID_RESPONSE');
@@ -19271,9 +19303,19 @@ function readFile(file, limit, code) {
19271
19303
  }
19272
19304
  async function prepareVibeInput(options, onPackaging, signal, deps = { check: executeCheck, zip: zipCwdToBuffer }) {
19273
19305
  const title = options.title?.trim();
19306
+ const minisType = resolveMinisType(options.minisType);
19274
19307
  if (!title)
19275
19308
  throw new VibeError('VIBE_TITLE_REQUIRED');
19276
- const appInfo = applyVibeCategory(options, applyVibeDescription(options.description, readVibeAppInfo(options.appInfo)));
19309
+ if (minisType === 'MINI_TOOL' && (options.appInfo || options.category || options.subcategory))
19310
+ throw new VibeError('VIBE_TOOL_TRIAL_METADATA');
19311
+ const appInfo = minisType === 'MINI_TOOL'
19312
+ ? applyVibeDescription(options.description, {
19313
+ description: 'A simple productivity tool for everyday tasks.',
19314
+ category: 'EFFICIENCY_AND_OFFICE', subcategory: '',
19315
+ terms_of_service: 'https://www.tiktok.com', privacy_policy: 'https://www.tiktok.com',
19316
+ copyright_confirmation: true, region: 'US', min_sdk_version: '1.0.0',
19317
+ })
19318
+ : applyVibeCategory(options, applyVibeDescription(options.description, readVibeAppInfo(options.appInfo)));
19277
19319
  if (options.package && options.dir)
19278
19320
  throw new VibeError('VIBE_INPUT_CONFLICT');
19279
19321
  const image = readFile(options.icon, VIBE_ICON_LIMIT, 'VIBE_INVALID_ICON');
@@ -19307,6 +19349,7 @@ async function prepareVibeInput(options, onPackaging, signal, deps = { check: ex
19307
19349
  if (signal.aborted)
19308
19350
  throw new VibeError('VIBE_CANCELLED');
19309
19351
  const input = {
19352
+ minisType,
19310
19353
  title,
19311
19354
  ...(appInfo ? { appInfo } : {}),
19312
19355
  icon: {
@@ -19326,28 +19369,254 @@ const vibePageStyles = `
19326
19369
  :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}
19327
19370
  *{box-sizing:border-box}
19328
19371
  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}
19372
+ .shell{position:relative;width:480px;max-width:100%}
19373
+ .steps{display:flex;align-items:center;justify-content:center;gap:10px;margin:24px 0 20px;padding:0;list-style:none}
19331
19374
  .step{display:flex;align-items:center;gap:6px;font-size:11px;color:#9298a2;white-space:nowrap}
19332
19375
  .step:after{content:"";height:1px;width:25px;margin-left:4px;background:#e9ebef}.step:last-child:after{display:none}
19333
19376
  .step-number{height:18px;min-width:18px;border-radius:50%;border:1px solid #e2e5e9;display:grid;place-items:center;font-size:10px}
19334
- .step.active{color:#272c34;font-weight:600}.step.active .step-number{background:#272c34;color:#fff;border-color:#272c34}
19335
- .step.done{color:#537565}.step.done .step-number{background:#edf6f0;color:#287c54;border-color:#edf6f0}
19336
- .heading{text-align:center}h1{font-size:26px;font-weight:650;line-height:1.35;letter-spacing:-.6px;margin:0 0 10px}
19337
- .subtitle{color:#7a818c;font-size:12px;line-height:1.7;margin:0;overflow-wrap:anywhere}
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}
19377
+ .step.active{color:#272c34;font-weight:600}.step.active .step-number{background:none;color:#272c34;border-color:transparent}
19378
+ .step.done{color:#7a818c}.step.done .step-number{background:#a3e635;color:#24282f;border-color:#a3e635}
19379
+ .swirl{display:block;width:18px;height:18px;border-radius:50%;background:conic-gradient(transparent 0 .1turn,currentColor 1turn);-webkit-mask:radial-gradient(farthest-side,#0000 calc(100% - 2.5px),#000 0);mask:radial-gradient(farthest-side,#0000 calc(100% - 2.5px),#000 0);animation:swirl-spin .9s linear infinite}
19380
+ @keyframes swirl-spin{to{transform:rotate(1turn)}}
19381
+ .heading{text-align:center}h1{font-size:20px;font-weight:600;line-height:1.6;letter-spacing:-.3px;margin:0 0 12px}
19382
+ .subtitle{color:#7a818c;font-size:14px;line-height:1.6;margin:0;min-height:22px;overflow-wrap:anywhere}
19383
+ .preview-loading{width:min(240px,100%);height:4px;margin:18px auto 0;border-radius:4px;overflow:hidden;background:#eceef1}
19384
+ .preview-loading span{display:block;width:40%;height:100%;border-radius:inherit;background:#858c96;animation:preview-loading-slide 1.8s ease-in-out infinite}
19385
+ @keyframes preview-loading-slide{from{transform:translateX(-100%)}to{transform:translateX(250%)}}
19386
+ @media(prefers-reduced-motion:reduce){.preview-loading span{animation:none;transform:translateX(75%)}}
19387
+ .game{display:grid;grid-template-columns:44px minmax(0,1fr);column-gap:14px;align-items:start;margin:24px 22px 0;padding:16px 18px;border-radius:16px;background:#f7f8fa;text-align:left}
19388
+ .game-summary{display:contents}.game-title{grid-column:2;max-width:100%;margin:0;font-size:15px;font-weight:600;overflow-wrap:anywhere;min-width:0}
19340
19389
  .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}
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}
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}
19390
+ .game-icon,.game-avatar{grid-column:1;grid-row:1 / 3;align-self:center}
19391
+ .game-details{grid-column:2;margin:4px 0 0;font-size:12px;color:#7a818c}.game-details>div:first-child,.game-categories>div{display:flex;align-items:baseline;gap:6px}.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}
19392
+ .game-description{white-space:pre-wrap;line-height:1.65}.game-categories{display:flex;flex-wrap:wrap;gap:6px 16px;margin-top:6px}
19393
+ /* 32px preserves at least four quiet modules even for a 21-module QR at 160px. */
19394
+ .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:32px;width:224px;height:224px;background:#fff}.qr-frame img{display:block;width:160px;height:160px;max-width:100%}
19395
+ .game+.qr-area{margin-top:20px}.scan-hint{text-align:center;font-size:11px;color:#7a818c;line-height:1.7;margin:8px 0 0;min-height:19px}
19396
+ .qr-area{flex-wrap:wrap;gap:20px;min-height:288px;align-content:flex-start}.qr-code{margin:0;width:224px;text-align:center}.qr-code figcaption{font-size:13px;font-weight:600;margin-top:4px;min-height:60px}.qr-code figcaption span{display:block;font-size:11px;font-weight:400;color:#7a818c;margin-top:4px;min-height:35px}
19397
+ .snake{display:grid;grid-template-columns:repeat(var(--cells),var(--module));color:#24282f;width:160px;height:160px}
19398
+ .snake i{width:var(--module);height:var(--module)}.snake i.lit,.snake i.on{background:currentColor}.snake i.fresh{animation:snake-settle .5s ease-out}.snake i.apple{background:#a3e635;animation:snake-apple 1.2s ease-in-out infinite}
19399
+ @keyframes snake-settle{from{background:#a3e635}to{background:currentColor}}@keyframes snake-apple{50%{opacity:.4}}
19400
+ @media(prefers-reduced-motion:reduce){.swirl,.snake i.fresh,.snake i.apple{animation:none}}
19345
19401
  .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}
19346
19402
  .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}
19347
19403
  .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}}
19404
+ @media(max-width:380px){body{padding-left:20px;padding-right:20px}.steps{gap:7px}.step:after{width:16px;margin-left:1px}}
19405
+ @media(max-width:520px){.game{margin-left:0;margin-right:0}.scan-hint{min-height:38px}}
19349
19406
  `;
19350
19407
 
19408
+ // Adapted from tiktok-minis-skills publish_tool.js at 132cd91.
19409
+ // Decorative waiting animation only; real preview QRs always use previewUrl.
19410
+ const qrPreviewLoader = `(() => {
19411
+ const neighbours = (p, w) => {
19412
+ const row = (p / w) | 0, col = p % w, out = [];
19413
+ if (row > 0) out.push(p - w);
19414
+ if (row < w - 1) out.push(p + w);
19415
+ if (col > 0) out.push(p - 1);
19416
+ if (col < w - 1) out.push(p + 1);
19417
+ return out;
19418
+ };
19419
+
19420
+ /** First step of a shortest path from 'from' to 'to', body cells excluded. */
19421
+ function stepToward(from, to, blocked, w) {
19422
+ if (from === to) return from;
19423
+ const prev = new Map([[from, from]]);
19424
+ let front = [from];
19425
+ while (front.length) {
19426
+ const next = [];
19427
+ for (const cell of front)
19428
+ for (const nb of neighbours(cell, w)) {
19429
+ if (prev.has(nb) || (blocked.has(nb) && nb !== to)) continue;
19430
+ prev.set(nb, cell);
19431
+ if (nb === to) {
19432
+ let node = nb;
19433
+ while (prev.get(node) !== from) node = prev.get(node);
19434
+ return node;
19435
+ }
19436
+ next.push(nb);
19437
+ }
19438
+ front = next;
19439
+ }
19440
+ return -1;
19441
+ }
19442
+
19443
+ /** Nearest still-unlit module, by plain grid distance from the head. */
19444
+ function nearestTarget(head, pending, w) {
19445
+ const hr = (head / w) | 0, hc = head % w;
19446
+ let best = -1, bestD = Infinity;
19447
+ for (const cell of pending) {
19448
+ const d = Math.abs(((cell / w) | 0) - hr) + Math.abs((cell % w) - hc);
19449
+ if (d < bestD) { bestD = d; best = cell; }
19450
+ }
19451
+ return best;
19452
+ }
19453
+
19454
+ function play(matrix, w, maxLength) {
19455
+ const pending = new Set();
19456
+ for (let i = 0; i < w * w; i++) if (matrix[i]) pending.add(i);
19457
+
19458
+ // The snake starts on a dark module, and only moving lights one, so the
19459
+ // starting square has to be lit here or the finished code is a module
19460
+ // short -- a hole the error correction hides rather than a code that is
19461
+ // actually right.
19462
+ const start = [...pending][0];
19463
+ pending.delete(start);
19464
+ const snake = [start];
19465
+ const body = new Set(snake);
19466
+ const lit = new Set([start]);
19467
+ const frames = [];
19468
+ let budget = w * w * 6;
19469
+
19470
+ while (pending.size && --budget > 0) {
19471
+ const head = snake[snake.length - 1];
19472
+ const target = nearestTarget(head, pending, w);
19473
+ if (target < 0) break;
19474
+ const move = stepToward(head, target, body, w);
19475
+ if (move < 0) break;
19476
+
19477
+ snake.push(move);
19478
+ body.add(move);
19479
+ while (snake.length > maxLength) body.delete(snake.shift());
19480
+ if (matrix[move]) { lit.add(move); pending.delete(move); }
19481
+
19482
+ frames.push({ snake: [...snake], lit: [...lit], apple: target });
19483
+ }
19484
+ return frames;
19485
+ }
19486
+
19487
+ function mount(host) {
19488
+ if (host.dataset.mounted) return;
19489
+ host.dataset.mounted = '1';
19490
+ const matrix = JSON.parse(host.dataset.matrix);
19491
+ const w = Number(host.dataset.size);
19492
+ const SPEED = Number(host.dataset.speed) || 20; // ms a step
19493
+ host.style.setProperty('--cells', w);
19494
+ host.replaceChildren(...Array.from({ length: w * w }, () => document.createElement('i')));
19495
+ const dots = Array.from(host.children);
19496
+
19497
+ const frames = play(matrix, w, 14);
19498
+ const last = frames.length - 1;
19499
+ if (last < 1) return;
19500
+ const all = frames[last].lit;
19501
+
19502
+ // A lap stops short of the last modules and fades, rather than holding a
19503
+ // finished code that would read as the preview having arrived.
19504
+ const DRAW = Math.round(0.95 * last);
19505
+ const FADE = 20;
19506
+ const lap = DRAW + FADE;
19507
+ // Below this a lap is too unfinished for a sprint to be worth watching.
19508
+ const SPRINTABLE = 0.6;
19509
+
19510
+ // One clock for the whole wait. <main> is replaced as the upload advances,
19511
+ // so a board may be a later stand-in for an earlier one; it has to carry
19512
+ // on from where that one stopped instead of starting the code over.
19513
+ window.__snakeStart = window.__snakeStart || Date.now();
19514
+
19515
+ let sprint = null;
19516
+
19517
+ /**
19518
+ * Only the cells that changed. Repainting all 841 every frame meant ~2500
19519
+ * class writes 50 times a second to move a 14-cell snake; this touches
19520
+ * about 30. The lit list only ever grows within a pass, so the new
19521
+ * modules are its tail, and the body and the apple are small.
19522
+ *
19523
+ * The settle flag is off while the snake is only wandering: the flash
19524
+ * means a module was just revealed, and nothing is revealed then.
19525
+ */
19526
+ const litSet = new Set();
19527
+ let litCount = 0;
19528
+ let prevBody = [];
19529
+ let prevApple = -1;
19530
+
19531
+ const cell = (k, className, opacity) => {
19532
+ const dot = dots[k];
19533
+ if (dot.className !== className) dot.className = className;
19534
+ if (dot.style.opacity !== opacity) dot.style.opacity = opacity;
19535
+ };
19536
+ const restingClass = (k) => (litSet.has(k) ? 'lit' : '');
19537
+
19538
+ const paint = ({ snake, lit, apple }, settle = true) => {
19539
+ if (lit.length < litCount) { // a fresh pass: start from an empty board
19540
+ litSet.clear(); litCount = 0; prevBody = []; prevApple = -1;
19541
+ for (const dot of dots) { dot.className = ''; dot.style.opacity = ''; }
19542
+ }
19543
+ // Paint every new module here rather than relying on the body to
19544
+ // uncover it: frames are skipped whenever the board moves faster than
19545
+ // one step a tick -- the sprint skips dozens -- and a module the body
19546
+ // never sat on would otherwise stay dark in a finished code.
19547
+ for (let i = litCount; i < lit.length; i++) { litSet.add(lit[i]); cell(lit[i], 'lit', ''); }
19548
+ litCount = lit.length;
19549
+
19550
+ // The module the snake lit is under its head; it shows when the tail
19551
+ // clears it, so the flash belongs to the cell the body just left.
19552
+ for (const k of prevBody)
19553
+ if (!snake.includes(k))
19554
+ cell(k, litSet.has(k) && settle ? 'lit fresh' : restingClass(k), '');
19555
+
19556
+ const length = snake.length;
19557
+ snake.forEach((k, i) => cell(k, 'on', String(0.2 + 0.55 * ((i + 1) / length))));
19558
+ prevBody = snake;
19559
+
19560
+ if (prevApple !== apple) {
19561
+ if (prevApple >= 0 && !snake.includes(prevApple))
19562
+ cell(prevApple, restingClass(prevApple), '');
19563
+ if (apple >= 0) cell(apple, 'apple', '');
19564
+ prevApple = apple;
19565
+ }
19566
+ };
19567
+
19568
+ const step = () => Math.floor((Date.now() - window.__snakeStart) / SPEED) % lap;
19569
+ /** How far into its draw this lap is, 0 to 1. */
19570
+ const lapProgress = () => Math.min(1, step() / DRAW);
19571
+
19572
+ if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
19573
+ paint(frames[Math.floor(DRAW / 3)], false);
19574
+ host.finish = () => Promise.resolve();
19575
+ return;
19576
+ }
19577
+
19578
+ const timer = setInterval(() => {
19579
+ // The page swaps <main> as the upload advances; stop with the old board.
19580
+ if (!host.isConnected) { clearInterval(timer); return; }
19581
+ if (sprint) {
19582
+ const k = Math.min(1, (Date.now() - sprint.at) / sprint.ms);
19583
+ const p = sprint.from + (1 - sprint.from) * k;
19584
+ if (p >= 1) {
19585
+ paint({ snake: [], lit: all, apple: -1 });
19586
+ clearInterval(timer);
19587
+ sprint.done();
19588
+ return;
19589
+ }
19590
+ paint(frames[Math.round(p * last)]);
19591
+ return;
19592
+ }
19593
+ const at = step();
19594
+ if (at > DRAW) { // fading out before the next lap
19595
+ host.style.opacity = String(1 - (at - DRAW) / FADE);
19596
+ return;
19597
+ }
19598
+ if (host.style.opacity) host.style.opacity = '';
19599
+ paint(frames[at]);
19600
+ }, SPEED);
19601
+
19602
+ host.finish = () => {
19603
+ if (sprint) return Promise.resolve();
19604
+ const from = lapProgress();
19605
+ if (from < SPRINTABLE) return Promise.resolve();
19606
+ host.style.opacity = '';
19607
+ sprint = { from, at: Date.now(), ms: 300 + 500 * (1 - from), done: () => {} };
19608
+ return new Promise((resolve) => { sprint.done = resolve; });
19609
+ };
19610
+ }
19611
+
19612
+ const boards = () => [...document.querySelectorAll('.snake')];
19613
+ const start = () => boards().forEach(mount);
19614
+ window.__mountSnakes = start;
19615
+ window.__snakeFinish = () => Promise.all(boards().map((b) => b.finish?.()));
19616
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
19617
+ else start();
19618
+ })();`;
19619
+
19351
19620
  const VIBE_PAGE_WORKER = '__vibe-page-server';
19352
19621
  // Includes the base64 form of the locally validated image (up to 10 MiB).
19353
19622
  const VIBE_PAGE_HTML_LIMIT = 16 * 1024 * 1024;
@@ -19361,6 +19630,7 @@ const pageUpdates = `(() => {
19361
19630
  const height = current.getBoundingClientRect().height;
19362
19631
  if (animate) await current.animate([{opacity: 1}, {opacity: 0}], {duration: 140, fill: 'forwards'}).finished.catch(() => {});
19363
19632
  current.replaceWith(updated);
19633
+ window.__mountSnakes?.();
19364
19634
  if (animate) await updated.animate([
19365
19635
  {opacity: 0, height: height + 'px', overflow: 'hidden'},
19366
19636
  {opacity: 1, height: updated.getBoundingClientRect().height + 'px', overflow: 'hidden'}
@@ -19368,17 +19638,19 @@ const pageUpdates = `(() => {
19368
19638
  }
19369
19639
  async function show(updated) {
19370
19640
  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,
19641
+ if (!current || current.dataset.rev === updated.dataset.rev) return;
19642
+ if (updated.dataset.phase === 'preview' && current.querySelector('.snake'))
19643
+ await window.__snakeFinish?.();
19644
+ if (updated.dataset.uploaded === 'true' && current.dataset.uploaded !== 'true') {
19645
+ // Show the upload receipt before either the binding or ready preview QR,
19374
19646
  // even when polling skips intermediate upload states.
19375
19647
  const success = updated.cloneNode(true);
19376
19648
  success.dataset.phase = 'upload_success';
19377
- delete success.dataset.uploadedBinding;
19649
+ delete success.dataset.uploaded;
19378
19650
  const heading = success.querySelector('.heading');
19379
19651
  success.querySelector('h1').textContent = heading.dataset.uploadTitle;
19380
19652
  success.querySelector('.subtitle').textContent = heading.dataset.uploadSubtitle;
19381
- success.querySelectorAll('.qr-area, .scan-action, .scan-hint').forEach(node => node.remove());
19653
+ success.querySelectorAll('.qr-area, .scan-action, .scan-hint, .preview-loading').forEach(node => node.remove());
19382
19654
  await replace(current, success);
19383
19655
  current = success;
19384
19656
  await delay(1200);
@@ -19395,6 +19667,7 @@ const pageUpdates = `(() => {
19395
19667
  const updated = next.querySelector('main');
19396
19668
  if (updated) await show(updated);
19397
19669
  document.title = next.title;
19670
+ document.documentElement.lang = next.documentElement.lang;
19398
19671
  }
19399
19672
  } catch { /* Keep the current page readable during a temporary connection failure. */ }
19400
19673
  if (!terminal()) setTimeout(refresh, 2000);
@@ -19404,14 +19677,20 @@ const pageUpdates = `(() => {
19404
19677
  const escape = (s) => s.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
19405
19678
  async function renderVibePage(input) {
19406
19679
  const zh = input.language !== 'en-US';
19680
+ const message = input.localizedMessages?.[zh ? 'zh-CN' : 'en-US'] ?? input.message;
19681
+ const tool = input.minisType === 'MINI_TOOL';
19407
19682
  const phase = input.phase ||
19408
19683
  (input.purpose === 'authorization_binding' ? 'binding' : 'uploading');
19409
19684
  const uploadedBinding = !input.simulation && input.purpose === 'authorization_binding' &&
19410
19685
  (phase === 'waiting_preview' || phase === 'preview' || phase === 'complete');
19686
+ const preview = !input.simulation && phase === 'preview' &&
19687
+ (input.purpose === 'game_preview' || input.purpose === 'tool_preview');
19688
+ const uploaded = uploadedBinding || preview;
19411
19689
  const copy = zh
19412
19690
  ? {
19413
- steps: ['扫码授权', '上传游戏', '完成'],
19691
+ steps: ['扫码授权', '上传游戏', '预览'],
19414
19692
  mock: input.hybrid ? 'PPE + Mock' : '模拟',
19693
+ game: '当前游戏',
19415
19694
  gameIcon: '游戏头像',
19416
19695
  bindingTitle: '上传到 TikTok',
19417
19696
  bindingAction: input.hybrid ? '真实授权二维码 · 混合联调' : '使用 TikTok 扫描二维码',
@@ -19424,6 +19703,7 @@ async function renderVibePage(input) {
19424
19703
  uploadingTitle: '正在上传小游戏',
19425
19704
  uploadingSubtitle: input.simulation ? '正在模拟授权后的上传流程,不会上传游戏内容。' : '授权已完成,正在上传本次游戏内容。',
19426
19705
  uploadLabel: input.simulation ? '模拟上传进度' : '上传进度',
19706
+ uploadStatus: '正在上传游戏,请稍候',
19427
19707
  completeTitle: input.simulation ? '模拟上传完成' : '上传完成',
19428
19708
  completeSubtitle: input.simulation
19429
19709
  ? '绑定和上传流程模拟完成,未向 TikTok 上传游戏。'
@@ -19434,8 +19714,9 @@ async function renderVibePage(input) {
19434
19714
  mockNote: input.hybrid ? '真实创建 / 查询任务 · 授权 / 上传为模拟 · 不改变真实任务状态' : '模拟运行 · 无需扫码,不会真实上传',
19435
19715
  }
19436
19716
  : {
19437
- steps: ['Authorize', 'Upload', 'Done'],
19717
+ steps: ['Scan', 'Upload', 'Preview'],
19438
19718
  mock: input.hybrid ? 'PPE + Mock' : 'MOCK',
19719
+ game: 'YOUR GAME',
19439
19720
  gameIcon: 'Game icon',
19440
19721
  bindingTitle: 'Upload to TikTok',
19441
19722
  bindingAction: input.hybrid ? 'Real authorization QR · integration demo' : 'Scan the QR code with TikTok',
@@ -19452,6 +19733,7 @@ async function renderVibePage(input) {
19452
19733
  uploadLabel: input.simulation
19453
19734
  ? 'Simulated upload progress'
19454
19735
  : 'Upload progress',
19736
+ uploadStatus: 'Uploading your game. Please wait.',
19455
19737
  completeTitle: input.simulation ? 'Simulated upload complete' : 'Upload complete',
19456
19738
  completeSubtitle: input.simulation
19457
19739
  ? 'Binding and upload simulation complete. Nothing was uploaded to TikTok.'
@@ -19461,6 +19743,16 @@ async function renderVibePage(input) {
19461
19743
  accepted: input.simulation ? 'Simulation complete' : 'Uploaded',
19462
19744
  mockNote: input.hybrid ? 'Real task creation / polling · Mock authorization / upload · Real task status unchanged' : 'Mock only · no scan needed, nothing is uploaded',
19463
19745
  };
19746
+ if (tool) {
19747
+ for (const key of Object.keys(copy)) {
19748
+ const value = copy[key];
19749
+ if (typeof value === 'string')
19750
+ copy[key] = value.replace(/小游戏|游戏/g, '工具').replace(/game/gi, 'tool');
19751
+ }
19752
+ copy.steps[1] = zh ? '上传工具' : 'Upload';
19753
+ }
19754
+ if (input.simulation)
19755
+ copy.steps[2] = zh ? '完成' : 'Done';
19464
19756
  if (input.web) {
19465
19757
  copy.steps[0] = zh ? '网页授权' : 'Web authorization';
19466
19758
  copy.bindingAction = zh ? '在浏览器完成授权绑定' : 'Authorize in your browser';
@@ -19469,22 +19761,22 @@ async function renderVibePage(input) {
19469
19761
  : (zh ? '请在已打开的 TikTok 页面确认授权,当前命令会继续查询绑定状态。' : 'Confirm authorization on the opened TikTok page. The command continues polling.');
19470
19762
  copy.bindingStatus = input.simulation ? (zh ? '模拟等待授权' : 'Simulating authorization') : (zh ? '等待网页授权' : 'Waiting for browser authorization');
19471
19763
  }
19472
- const step = phase === 'binding' ? 0 : phase === 'uploading' ? 1 : 2;
19764
+ const step = phase === 'binding' ? 0 : phase === 'uploading' ? 1 : phase === 'preview' ? 3 : 2;
19473
19765
  // Only embed the chosen raster image; never fetch a URL or expose its local path.
19474
19766
  const icon = input.iconDataUrl &&
19475
19767
  input.iconDataUrl.length <= 14 * 1024 * 1024 &&
19476
19768
  /^data:image\/(?:png|jpeg|webp);base64,[A-Za-z0-9+/]+=*$/.test(input.iconDataUrl)
19477
19769
  ? `<img class="game-avatar" width="44" height="44" alt="${copy.gameIcon}" src="${input.iconDataUrl}">`
19478
19770
  : '<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>';
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>`;
19771
+ 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(tool && input.category === 'EFFICIENCY_AND_OFFICE' ? (zh ? '效率办公' : 'Efficiency & Office') : input.category ?? VIBE_PPE_DEFAULT_APP_INFO.category)}</dd></div>${tool ? '' : `<div><dt>${zh ? '子类型' : 'Subcategory'}</dt><dd>${escape(input.subcategory ?? VIBE_PPE_DEFAULT_APP_INFO.subcategory)}</dd></div>`}</div></dl></div>`;
19480
19772
  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>';
19481
19773
  const uploadSuccessTitle = zh ? '上传成功' : 'Upload complete';
19482
19774
  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')
19775
+ ? `${tool ? '工具' : '游戏'}内容已上传,正在返回扫码页面。`
19776
+ : `Your ${tool ? 'tool' : 'game'} has been uploaded. Returning to the scan page.`;
19777
+ const title = uploadedBinding ? (phase === 'waiting_preview' ? (zh ? '正在生成预览' : 'Preparing preview') : zh ? '扫码查看进度' : 'Scan to check progress')
19486
19778
  : phase === 'waiting_preview' ? (zh ? '正在准备预览' : 'Preparing preview')
19487
- : phase === 'preview' ? (zh ? '扫码预览游戏' : 'Scan to preview')
19779
+ : phase === 'preview' ? (zh ? `扫码预览${tool ? '工具' : '游戏'}` : 'Scan to preview')
19488
19780
  : phase === 'binding'
19489
19781
  ? copy.bindingTitle
19490
19782
  : phase === 'uploading'
@@ -19492,55 +19784,83 @@ async function renderVibePage(input) {
19492
19784
  : phase === 'failed'
19493
19785
  ? copy.failedTitle
19494
19786
  : 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)
19787
+ const subtitle = uploadedBinding ? (phase === 'complete' ? escape(message) : phase === 'waiting_preview'
19788
+ ? (zh ? '代码包已上传,正在生成预览。准备好后会自动显示预览二维码。' : 'Package uploaded. Your preview is being prepared; its QR will appear when ready.') : zh
19789
+ ? `本次${tool ? '工具' : '游戏'}代码包已上传完成,再次扫码即可查看预览版本并提交审核。`
19790
+ : `Your ${tool ? 'tool' : 'game'} package has been uploaded. Scan again to view the preview version and submit it for review.`)
19791
+ : phase === 'waiting_preview' ? (zh ? '内容已上传,预览就绪后会自动显示二维码。' : 'Content uploaded. The preview QR will appear when ready.')
19792
+ : phase === 'preview' ? ''
19793
+ : phase === 'complete' && !input.simulation ? escape(message)
19501
19794
  : phase === 'binding'
19502
19795
  ? copy.bindingSubtitle
19503
19796
  : phase === 'uploading'
19504
19797
  ? copy.uploadingSubtitle
19505
19798
  : phase === 'failed'
19506
- ? escape(input.message)
19799
+ ? escape(message)
19507
19800
  : copy.completeSubtitle;
19508
- const qr = (phase === 'binding' || phase === 'preview' || uploadedBinding) && !input.web && input.qrUrl
19801
+ const qr = (phase === 'binding' || phase === 'uploading' || phase === 'preview' || uploadedBinding) && !input.web && input.qrUrl
19509
19802
  ? await QRCode.toDataURL(input.qrUrl, {
19510
19803
  width: 384,
19511
- margin: 4,
19804
+ // Fixed CSS quiet zones keep different QR versions visually equal.
19805
+ margin: 0,
19512
19806
  errorCorrectionLevel: 'M',
19513
19807
  })
19514
19808
  : '';
19515
- const purpose = phase === 'preview' && !uploadedBinding ? (zh ? '游戏预览二维码' : 'Game preview QR') : copy.bindingCode;
19516
- const done = phase === 'complete' || phase === 'preview' || phase === 'waiting_preview';
19809
+ const progressCode = zh ? '查看进度二维码' : 'Progress QR';
19810
+ const purpose = preview ? (zh ? `${tool ? '工具' : '游戏'}预览二维码` : `${tool ? 'Tool' : 'Game'} preview QR`) : uploadedBinding || phase === 'uploading' ? (zh ? '应用页面二维码' : 'App page QR') : copy.bindingCode;
19811
+ const bindingQr = preview && !input.web && input.bindingQrUrl && input.bindingQrUrl !== input.qrUrl
19812
+ ? await QRCode.toDataURL(input.bindingQrUrl, { width: 384, margin: 0, errorCorrectionLevel: 'M' }) : '';
19813
+ const done = phase === 'preview' || phase === 'complete' && input.simulation;
19814
+ const active = phase === 'binding' || phase === 'uploading' || phase === 'waiting_preview';
19815
+ const waiting = phase === 'waiting_preview';
19816
+ const matrix = waiting ? QRCode.create('https://www.tiktok.com/minis/').modules : undefined;
19817
+ const pendingQr = matrix ? `<figure class="qr-code qr-pending"><div class="qr-frame" role="img" aria-label="${zh ? '预览正在生成' : 'Preview is being prepared'}"><div class="snake" aria-hidden="true" data-size="${matrix.size}" data-speed="20" data-matrix='${JSON.stringify(Array.from(matrix.data))}' style="--module:${160 / matrix.size}px"></div></div><figcaption>${zh ? '正在生成预览' : 'Preparing preview'}</figcaption></figure>` : '';
19517
19818
  const progress = typeof input.progressPercent === 'number' &&
19518
19819
  Number.isFinite(input.progressPercent)
19519
19820
  ? Math.max(0, Math.min(100, Math.round(input.progressPercent)))
19520
19821
  : undefined;
19822
+ const progressQr = bindingQr ? `<figure class="qr-code"><div class="qr-frame"><img width="160" height="160" alt="${progressCode}" src="${bindingQr}"></div><figcaption>${zh ? '查看进度' : 'Check progress'}<span>${zh ? '查看应用信息并提交审核' : 'View app details and submit for review'}</span></figcaption></figure>` : '';
19823
+ // Keep the original scan entry on the left while the right side becomes a preview.
19521
19824
  const content = qr
19522
- ? `<div class="qr-area"><div class="qr-frame"><img width="192" height="192" alt="${purpose}" src="${qr}"></div></div>`
19825
+ ? `<div class="qr-area">${progressQr}<figure class="qr-code"><div class="qr-frame"><img width="160" height="160" alt="${purpose}" src="${qr}"></div>${preview ? `<figcaption>${zh ? '扫码预览' : 'Preview'}<span>${zh ? '打开本次上传的预览版本' : 'Open the preview of this upload'}</span></figcaption>` : phase === 'binding' ? `<figcaption>${zh ? '扫码授权' : 'Authorize upload'}</figcaption>` : `<figcaption>${zh ? '查看进度' : 'Check progress'}<span>${zh ? '扫码查看应用信息' : 'Scan to view app details'}</span></figcaption>`}</figure>${pendingQr}</div>`
19523
19826
  : phase === 'uploading' && progress !== undefined
19524
19827
  ? `<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>`
19525
19828
  : '';
19829
+ const previewLoading = phase === 'waiting_preview'
19830
+ ? `<div class="preview-loading" role="progressbar" aria-label="${zh ? '正在生成预览' : 'Preparing preview'}"><span aria-hidden="true"></span></div>`
19831
+ : '';
19832
+ const revision = crypto.createHash('sha256').update(JSON.stringify(input)).digest('hex');
19833
+ const steps = phase === 'failed' ? '' : `<ol class="steps" aria-label="${zh ? '上传步骤' : 'Upload steps'}">${copy.steps.map((label, i) => `<li class="step ${done || i < step ? 'done' : active && i === step ? 'active' : ''}"${!done && active && i === step ? ' aria-current="step"' : ''}><span class="step-number">${done || i < step ? checkIcon : active && i === step ? '<span class="swirl" aria-hidden="true"></span>' : i + 1}</span>${label}</li>`).join('')}</ol>`;
19526
19834
  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>
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>` : ''}
19835
+ <body><main class="shell${bindingQr || pendingQr ? ' dual-qr' : ''}" data-phase="${phase}" data-rev="${revision}"${uploaded ? ' data-uploaded="true"' : ''}>${input.simulation ? `<p class="mock-badge">${copy.mock}</p>` : ''}
19836
+ <section class="card" aria-labelledby="page-title">
19837
+ <div class="heading"${uploaded ? ` data-upload-title="${escape(uploadSuccessTitle)}" data-upload-subtitle="${escape(uploadSuccessSubtitle)}"` : ''}><h1 id="page-title">${title}</h1><p class="subtitle" role="status">${subtitle}</p>${previewLoading}</div>${game}${steps}${phase === 'binding' && (input.web || input.hybrid) ? `<p class="scan-action">${copy.bindingAction}</p>` : ''}${content}<p class="scan-hint">${uploadedBinding ? (zh ? '可扫码查看进度信息,无需重复上传。' : 'Scan to view app details. No need to upload again.') : ''}</p>
19530
19838
  </section>
19531
19839
  ${input.simulation ? `<p class="footer"><span class="dot" aria-hidden="true"></span>${copy.mockNote}</p>` : ''}</main></body></html>`;
19532
19840
  }
19841
+ // Follow the reader's preferred browser language, as in the Tool skill.
19842
+ function browserPageLanguage(header) {
19843
+ const preferred = (header || '').split(',').map(part => {
19844
+ const [tag, ...parameters] = part.trim().split(';');
19845
+ const quality = parameters.find(value => /^\s*q=/i.test(value));
19846
+ const q = quality ? Number(quality.split('=')[1]) : 1;
19847
+ return { tag: tag.toLowerCase(), q };
19848
+ }).filter(item => item.tag && Number.isFinite(item.q) && item.q > 0 && item.q <= 1)
19849
+ .sort((a, b) => b.q - a.q)[0];
19850
+ return preferred?.tag.split('-')[0] === 'zh' ? 'zh-CN' : 'en-US';
19851
+ }
19533
19852
  async function createVibePageServer() {
19534
19853
  const route = `/${crypto.randomBytes(24).toString('hex')}`;
19535
19854
  let html = 'Starting…';
19536
19855
  let etag = '';
19856
+ let variants = {};
19537
19857
  let landingUrl;
19538
19858
  const server = http.createServer((req, res) => {
19539
19859
  const address = server.address();
19540
19860
  const host = address && typeof address !== 'string' ? `127.0.0.1:${address.port}` : '';
19541
19861
  if (req.method === 'GET' && req.url === `${route}?updates` && req.headers.host === host) {
19542
19862
  res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' });
19543
- res.end(pageUpdates);
19863
+ res.end(qrPreviewLoader + '\n' + pageUpdates);
19544
19864
  return;
19545
19865
  }
19546
19866
  if (req.url === `${route}/authorize` && req.headers.host === host &&
@@ -19563,19 +19883,23 @@ async function createVibePageServer() {
19563
19883
  res.writeHead(404).end();
19564
19884
  return;
19565
19885
  }
19566
- if (etag && req.headers['if-none-match'] === etag) {
19567
- res.writeHead(304, { 'ETag': etag, 'Cache-Control': 'no-store' }).end();
19886
+ const localized = variants[browserPageLanguage(req.headers['accept-language'])];
19887
+ const responseHtml = localized?.html ?? html;
19888
+ const responseEtag = localized?.etag ?? etag;
19889
+ if (responseEtag && req.headers['if-none-match'] === responseEtag) {
19890
+ res.writeHead(304, { 'ETag': responseEtag, 'Cache-Control': 'no-store', 'Vary': 'Accept-Language' }).end();
19568
19891
  return;
19569
19892
  }
19570
19893
  res.writeHead(200, {
19571
- 'ETag': etag,
19894
+ 'ETag': responseEtag,
19895
+ 'Vary': 'Accept-Language',
19572
19896
  'Content-Type': 'text/html; charset=utf-8',
19573
19897
  'Cache-Control': 'no-store',
19574
19898
  'Referrer-Policy': 'no-referrer',
19575
19899
  'X-Content-Type-Options': 'nosniff',
19576
19900
  'Content-Security-Policy': "default-src 'none'; img-src data:; style-src 'unsafe-inline'; script-src 'self'; connect-src 'self'; frame-ancestors 'none'",
19577
19901
  });
19578
- res.end(html);
19902
+ res.end(responseHtml);
19579
19903
  });
19580
19904
  await new Promise((resolve, reject) => {
19581
19905
  server.once('error', reject);
@@ -19584,9 +19908,15 @@ async function createVibePageServer() {
19584
19908
  const address = server.address();
19585
19909
  return {
19586
19910
  url: `http://127.0.0.1:${address.port}${route}`,
19587
- update: (value, authorization) => {
19911
+ update: (value, authorization, localizedHtml) => {
19588
19912
  html = value;
19589
19913
  etag = '"' + crypto.createHash('sha256').update(value).digest('hex') + '"';
19914
+ variants = {};
19915
+ for (const language of ['zh-CN', 'en-US']) {
19916
+ const localized = localizedHtml?.[language];
19917
+ if (typeof localized === 'string' && localized.length < VIBE_PAGE_HTML_LIMIT)
19918
+ variants[language] = { html: localized, etag: '"' + crypto.createHash('sha256').update(localized).digest('hex') + '"' };
19919
+ }
19590
19920
  landingUrl = browserLandingUrl(authorization);
19591
19921
  },
19592
19922
  close: () => server.close(),
@@ -19599,24 +19929,34 @@ async function runVibePageWorker() {
19599
19929
  }
19600
19930
  const server = await createVibePageServer();
19601
19931
  const requestedTtl = Number(process.env.TTMG_VIBE_PAGE_TTL_MS);
19602
- const ttl = requestedTtl > 0 && requestedTtl <= 900000 ? requestedTtl : 900000;
19603
- const timer = setTimeout(() => {
19932
+ const ttl = requestedTtl > 0 && requestedTtl <= 1800000 ? requestedTtl : 1800000;
19933
+ let timer;
19934
+ let closing = false;
19935
+ const close = () => {
19936
+ closing = true;
19937
+ clearTimeout(timer);
19604
19938
  server.close();
19605
19939
  if (process.connected)
19606
19940
  process.disconnect();
19607
- }, ttl);
19941
+ };
19942
+ // The parent owns the operation deadline. Keep the page alive during polling,
19943
+ // then start its retention window when the parent detaches (or exits).
19944
+ const retain = () => {
19945
+ if (!closing && !timer)
19946
+ timer = setTimeout(close, ttl);
19947
+ };
19948
+ process.once('disconnect', retain);
19949
+ if (!process.connected)
19950
+ retain();
19608
19951
  process.on('message', (message) => {
19609
19952
  if (message?.type === 'render' &&
19610
19953
  typeof message.html === 'string' &&
19611
19954
  message.html.length < VIBE_PAGE_HTML_LIMIT) {
19612
- server.update(message.html, message.browserLandingUrl);
19955
+ server.update(message.html, message.browserLandingUrl, message.localizedHtml);
19613
19956
  process.send?.({ type: 'rendered', id: message.id });
19614
19957
  }
19615
19958
  if (message?.type === 'close') {
19616
- clearTimeout(timer);
19617
- server.close();
19618
- if (process.connected)
19619
- process.disconnect();
19959
+ close();
19620
19960
  }
19621
19961
  });
19622
19962
  process.send({ type: 'ready', url: server.url });
@@ -19654,11 +19994,15 @@ async function startVibePage() {
19654
19994
  return {
19655
19995
  url: ready.url,
19656
19996
  async update(input) {
19657
- const html = await renderVibePage(input);
19997
+ const [zh, en] = await Promise.all([
19998
+ renderVibePage({ ...input, language: 'zh-CN' }),
19999
+ renderVibePage({ ...input, language: 'en-US' }),
20000
+ ]);
20001
+ const html = input.language === 'en-US' ? en : zh;
19658
20002
  const current = ++id;
19659
20003
  const ack = receive(m => m?.type === 'rendered' && m.id === current);
19660
20004
  const landingUrl = input.phase === 'binding' ? browserLandingUrl(input.browserLandingUrl) : undefined;
19661
- child.send({ type: 'render', id: current, html, browserLandingUrl: landingUrl }, error => {
20005
+ child.send({ type: 'render', id: current, html, localizedHtml: { 'zh-CN': zh, 'en-US': en }, browserLandingUrl: landingUrl }, error => {
19662
20006
  if (error)
19663
20007
  child.emit('error', error);
19664
20008
  });
@@ -19682,6 +20026,7 @@ async function startVibePage() {
19682
20026
 
19683
20027
  // Source identity is project metadata, not a Skill input or a credential.
19684
20028
  function getVibeProjectId(options) {
20029
+ const minisType = resolveMinisType(options.minisType);
19685
20030
  const root = path.resolve(options.package ? path.dirname(options.package) : options.dir || '.');
19686
20031
  const dir = path.join(root, '.ttmg');
19687
20032
  const file = path.join(dir, 'vibe-project.json');
@@ -19689,7 +20034,7 @@ function getVibeProjectId(options) {
19689
20034
  fs.mkdirSync(dir, { recursive: true });
19690
20035
  const id = crypto.randomUUID();
19691
20036
  try {
19692
- fs.writeFileSync(file, JSON.stringify({ schemaVersion: '1', sourceProjectId: id }) + '\n', { flag: 'wx', mode: 0o600 });
20037
+ fs.writeFileSync(file, JSON.stringify({ schemaVersion: '1', sourceProjectId: id, minisType }) + '\n', { flag: 'wx', mode: 0o600 });
19693
20038
  }
19694
20039
  catch (error) {
19695
20040
  if (error.code !== 'EEXIST')
@@ -19700,9 +20045,13 @@ function getVibeProjectId(options) {
19700
20045
  typeof saved.sourceProjectId !== 'string' ||
19701
20046
  !/^[a-zA-Z0-9_-]{1,100}$/.test(saved.sourceProjectId))
19702
20047
  throw new Error();
20048
+ if ((saved.minisType || 'MINI_GAME') !== minisType)
20049
+ throw new VibeError('VIBE_PROJECT_TYPE_CONFLICT');
19703
20050
  return saved.sourceProjectId;
19704
20051
  }
19705
- catch {
20052
+ catch (error) {
20053
+ if (error instanceof VibeError)
20054
+ throw error;
19706
20055
  throw new VibeError('VIBE_PROJECT_STATE_FAILED');
19707
20056
  }
19708
20057
  }
@@ -19790,6 +20139,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19790
20139
  let page;
19791
20140
  let backend = deps.backend;
19792
20141
  let task;
20142
+ let authorizationUrl;
19793
20143
  let detached = false;
19794
20144
  let progressUpdates = Promise.resolve();
19795
20145
  let ppeLane;
@@ -19833,6 +20183,10 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19833
20183
  try {
19834
20184
  if (options.web)
19835
20185
  throw new VibeError('VIBE_WEB_DISABLED');
20186
+ const minisType = resolveMinisType(options.minisType);
20187
+ result.minisType = minisType;
20188
+ if (minisType === 'MINI_TOOL')
20189
+ result.inputRequirements = VIBE_TOOL_INPUT_REQUIREMENTS;
19836
20190
  // Real QR uploads use the release-owned PPE route.
19837
20191
  // Explicit test backends remain opt-in; real failures never fall back to them.
19838
20192
  if (!options.mock && !options.vibeApi && process.env.TTMG_VIBE_BACKEND) {
@@ -19873,7 +20227,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19873
20227
  throw new VibeError('VIBE_BACKEND_OPTION_CONFLICT');
19874
20228
  if (options.pollInterval !== undefined)
19875
20229
  seconds(options.pollInterval, 1);
19876
- const timeoutMs = seconds(options.timeout, 300);
20230
+ const timeoutMs = seconds(options.timeout, 1800);
19877
20231
  timeout = setTimeout(() => {
19878
20232
  timedOut = true;
19879
20233
  controller.abort();
@@ -19908,7 +20262,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19908
20262
  category: (input.appInfo || VIBE_PPE_DEFAULT_APP_INFO).category,
19909
20263
  subcategory: (input.appInfo || VIBE_PPE_DEFAULT_APP_INFO).subcategory,
19910
20264
  source: options.category !== undefined ? 'arguments' : options.appInfo !== undefined ? 'app-info' : 'defaults',
19911
- catalogVersion: GAME_CATEGORY_CATALOG_VERSION,
20265
+ catalogVersion: minisType === 'MINI_TOOL' ? 'mini-tool-ppe-trial-2026-09-19' : GAME_CATEGORY_CATALOG_VERSION,
19912
20266
  },
19913
20267
  iconSizeBytes: input.icon.bytes.length,
19914
20268
  packageSizeBytes: input.code.bytes.length,
@@ -19920,6 +20274,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19920
20274
  return result;
19921
20275
  const submittedInfo = input.appInfo || VIBE_PPE_DEFAULT_APP_INFO;
19922
20276
  pageMetadata = {
20277
+ minisType,
19923
20278
  iconDataUrl: `data:${input.icon.contentType};base64,${input.icon.bytes.toString('base64')}`,
19924
20279
  description: submittedInfo.description,
19925
20280
  category: submittedInfo.category,
@@ -19937,10 +20292,10 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19937
20292
  source_type: 1,
19938
20293
  source_project_id: sourceProjectId,
19939
20294
  task_idempotency_id: result.operationId,
19940
- minis_type: 'MINI_GAME',
20295
+ minis_type: minisType,
19941
20296
  }, controller.signal);
19942
20297
  ensureActive();
19943
- const authorizationUrl = createAuthorizationScheme(task.landingUrl);
20298
+ authorizationUrl = createAuthorizationScheme(task.landingUrl);
19944
20299
  const pollMs = seconds(options.pollInterval, task.pollIntervalSeconds);
19945
20300
  result = {
19946
20301
  ...result,
@@ -20019,6 +20374,8 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20019
20374
  ...pageGame,
20020
20375
  language: getCurrentLanguage(),
20021
20376
  phase: 'uploading',
20377
+ qrUrl: result.simulation ? undefined : authorizationUrl,
20378
+ purpose: result.simulation ? undefined : 'authorization_binding',
20022
20379
  simulation: result.simulation,
20023
20380
  message: text('uploading', result.simulation),
20024
20381
  });
@@ -20057,6 +20414,8 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20057
20414
  ...pageGame,
20058
20415
  language: getCurrentLanguage(),
20059
20416
  phase: 'uploading',
20417
+ qrUrl: result.simulation ? undefined : authorizationUrl,
20418
+ purpose: result.simulation ? undefined : 'authorization_binding',
20060
20419
  simulation: result.simulation,
20061
20420
  message: text('uploading', result.simulation),
20062
20421
  progressPercent: percent(loaded, validTotal),
@@ -20104,17 +20463,20 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20104
20463
  ...pageGame,
20105
20464
  language: getCurrentLanguage(),
20106
20465
  phase: result.previewReady ? 'preview' : 'complete',
20107
- qrUrl: result.simulation ? undefined : authorizationUrl,
20108
- purpose: result.simulation ? undefined : 'authorization_binding',
20466
+ qrUrl: result.simulation ? undefined : result.previewUrl || authorizationUrl,
20467
+ bindingQrUrl: result.previewReady ? authorizationUrl : undefined,
20468
+ purpose: result.simulation ? undefined : result.previewReady
20469
+ ? result.minisType === 'MINI_TOOL' ? 'tool_preview' : 'game_preview'
20470
+ : 'authorization_binding',
20109
20471
  simulation: result.simulation,
20110
20472
  message: text('uploaded', result.simulation),
20111
20473
  });
20112
20474
  page.detach();
20113
20475
  detached = true;
20114
20476
  return emit('uploaded', { terminal: true, nextAction: result.previewReady ? 'scan_qr' : 'none',
20115
- qrPurpose: result.previewReady ? 'authorization_binding' : undefined,
20477
+ qrPurpose: result.previewReady ? result.minisType === 'MINI_TOOL' ? 'tool_preview' : 'game_preview' : undefined,
20116
20478
  ...(result.previewReady ? { message: getCurrentLanguage() === 'zh-CN'
20117
- ? '上传完成。原绑定二维码已保留,可使用 TikTok 再次扫码打开绑定页面。' : 'Upload complete. The original binding QR is available to reopen the binding page with TikTok.' } : {}) });
20479
+ ? '预览已就绪,请使用 TikTok 扫描预览二维码;也可扫描“查看进度”二维码查看应用信息并提交审核。' : 'Preview ready. Scan the preview QR with TikTok, or scan the progress QR to view app details and submit for review.' } : {}) });
20118
20480
  }
20119
20481
  catch (error) {
20120
20482
  const code = timedOut
@@ -20132,6 +20494,9 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20132
20494
  const messages = {
20133
20495
  VIBE_CATEGORY_PAIR_REQUIRED: ['--category 和 --subcategory 必须同时提供;均不传时保留默认分类。使用 ttmg game categories --format json 查询。', 'Provide --category and --subcategory together, or omit both to keep the default. Query ttmg game categories --format json.'],
20134
20496
  VIBE_INVALID_CATEGORY: ['类型或子类型无效,或两者不匹配。使用 ttmg game categories --format json 查询有效组合。', 'Invalid category/subcategory pair. Query valid values with ttmg game categories --format json.'],
20497
+ VIBE_INVALID_MINIS_TYPE: ['应用类型须为 MINI_GAME 或 MINI_TOOL。', 'Minis type must be MINI_GAME or MINI_TOOL.'],
20498
+ VIBE_TOOL_TRIAL_METADATA: ['本地 Tool 试用固定使用效率办公类、US 和 SDK 1.0.0;暂不接受分类或 app-info 覆盖。', 'The local Tool trial uses Efficiency & Office, US and SDK 1.0.0; category and app-info overrides are not supported yet.'],
20499
+ VIBE_PROJECT_TYPE_CONFLICT: ['该工程已使用其他应用类型,请为新的 Tool 使用独立工程目录。', 'This project has a different Minis type. Use a separate project directory for a new Tool.'],
20135
20500
  VIBE_INVALID_DESCRIPTION: ['描述必须是非空的有效 Unicode 文本;省略 --description 可使用默认描述。', 'Description must be non-empty valid Unicode text; omit --description to use the default.'],
20136
20501
  VIBE_AUTH_DENIED: [
20137
20502
  '已取消授权,本次未上传。',
@@ -20213,7 +20578,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20213
20578
  text(status);
20214
20579
  controller.abort();
20215
20580
  await progressUpdates;
20216
- // Remove the binding QR on failure too. Keep a readable terminal result page.
20581
+ // After upload, preserve the original code for checking progress even if preview lookup stops.
20217
20582
  if (page && (code !== 'VIBE_CANCELLED' || result.uploaded)) {
20218
20583
  try {
20219
20584
  await page.update({
@@ -20223,10 +20588,19 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20223
20588
  simulation: result.simulation,
20224
20589
  hybrid: result.backend === 'ppe-mock',
20225
20590
  phase: result.uploaded ? 'complete' : 'failed',
20591
+ qrUrl: result.uploaded && !result.simulation ? authorizationUrl : undefined,
20592
+ purpose: result.uploaded && !result.simulation ? 'authorization_binding' : undefined,
20593
+ localizedMessages: result.uploaded && !result.simulation ? {
20594
+ 'zh-CN': '代码包已上传,本页已停止等待预览。可扫码查看进度,无需重复上传。',
20595
+ 'en-US': 'Package uploaded. This page has stopped waiting for the preview. Scan to check progress; no need to upload again.',
20596
+ } : {
20597
+ 'zh-CN': messages[code]?.[0] || '本次操作未完成,请检查 CLI 输出。',
20598
+ 'en-US': messages[code]?.[1] || 'This operation did not complete. Check the CLI output.',
20599
+ },
20226
20600
  message: result.uploaded && !result.simulation
20227
20601
  ? 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.'
20602
+ ? '代码包已上传,本页已停止等待预览。可扫码查看进度,无需重复上传。'
20603
+ : 'Package uploaded. This page has stopped waiting for the preview. Scan to check progress; no need to upload again.'
20230
20604
  : message,
20231
20605
  });
20232
20606
  page.detach();