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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7241,8 +7241,8 @@ const messages = {
7241
7241
  'cli.command.login.password': 'Developer account password (implicit mode)',
7242
7242
  'cli.command.logout.desc': 'Logout and clear local user data',
7243
7243
  'cli.command.auth.desc': 'Inspect local authentication state',
7244
- 'cli.command.auth.open.desc': 'Open an existing Vibe task authorization page in the system browser',
7245
- 'cli.command.upload.vibeWeb': 'Authorize in the browser, then upload to PPE (--vibe; no Mock or --no-open)',
7244
+ 'cli.command.auth.open.desc': 'Reopen the local QR page for an existing Vibe task',
7245
+ 'cli.command.upload.vibeWeb': 'Disabled; use --vibe and scan with TikTok on your phone',
7246
7246
  'cli.command.game.desc': 'Inspect game metadata supported by the platform',
7247
7247
  'cli.command.game.categories.desc': 'List game categories and subcategories with gameplay descriptions (offline, read-only)',
7248
7248
  'cli.command.game.categories.category': 'Filter by an exact category value, e.g. Puzzle',
@@ -7252,23 +7252,26 @@ const messages = {
7252
7252
  'cli.command.upload.vibeDescription': 'Optional game description (--vibe); default: Discover the fun of TikTok Mini Games.',
7253
7253
  'cli.command.upload.vibeSubcategory': 'Optional subcategory belonging to --category; omit both to keep Other/Other (--vibe)',
7254
7254
  'cli.command.upload.vibeWebGuide': `
7255
- Vibe web upload (real PPE trial):
7256
- ttmg upload --vibe --web --dir ./game --title "My game" --icon ./avatar.png --format ndjson
7255
+ Vibe QR upload (real PPE trial):
7256
+ ttmg upload --vibe --dir ./game --title "My game" --icon ./avatar.png --format ndjson
7257
7257
  Use --package ./game.zip instead of --dir for an existing ZIP.
7258
7258
  Collect only title and icon; the CLI fills other app_info fields with PPE trial defaults.
7259
7259
  Skill may pass --description "Game description"; otherwise use "Discover the fun of TikTok Mini Games." (legacy metadata-file descriptions are preserved).
7260
7260
  Skill may query ttmg game categories --format json and pass --category Puzzle --subcategory Physics together.
7261
7261
  Omit both to keep Other/Other (or the legacy metadata-file classification). Invalid pairs fail before task creation.
7262
7262
  prepared and terminal events expose the submitted classification and its source.
7263
- Keep this process running. At waiting_authorization, complete authorization in the system browser on the CLI computer.
7264
- pageUrl is the local binding/progress page. To reopen authorization, execute the returned authorizationOpenCommand.
7263
+ Keep this process running. At waiting_authorization, scan the QR on pageUrl with TikTok on your phone.
7264
+ pageUrl is the local QR/progress page on the CLI computer. Reopen it with authorizationOpenCommand.
7265
7265
  The CLI polls and uploads avatar, app_info and package automatically after authorization. Do not start a second upload.
7266
+ After upload, waiting_preview keeps polling the same task until previewUrl is available; pageUrl then shows the game preview QR.
7267
+ Preview ready: previewReady=true and qrPurpose=game_preview. A preview timeout preserves uploaded=true; check the task, do not reupload.
7266
7268
  Real success: terminal=true, status=uploaded, uploaded=true, simulation=false. On failure, read errorCode and nextAction.
7267
- --web opens the browser for every output format. --mock is a separate simulation mode.
7269
+ The local QR page opens for every output format; --no-open disables automatic opening.
7270
+ The page displays the submitted title, icon, description, category and subcategory.
7268
7271
  Upload completion is not review approval or production release. See bundled vibe-upload.md and ttmg capabilities --format json.
7269
7272
  `,
7270
7273
  'cli.command.auth.open.pageUrl': 'Local pageUrl returned by the running Vibe upload command',
7271
- 'cli.command.auth.open.requested': 'Browser open requested. Complete authorization there; the existing upload process continues polling.',
7274
+ 'cli.command.auth.open.requested': 'QR page open requested. Scan with TikTok on your phone; the existing upload process continues polling.',
7272
7275
  'cli.command.auth.open.invalidPage': 'Pass the local pageUrl from the running Vibe upload command, not the platform URL.',
7273
7276
  'cli.command.auth.open.unavailable': 'Authorization page is unavailable, expired, or no longer waiting. Check the existing upload process; no new task was created.',
7274
7277
  'cli.command.auth.open.invalidFormat': 'Use text, json, or ndjson output.',
@@ -7467,9 +7470,9 @@ Vibe web upload (real PPE trial):
7467
7470
  'cli.command.upload.dir': 'Game project directory (default: current directory)',
7468
7471
  'cli.command.upload.preview': 'Wait for platform processing and output the preview payload',
7469
7472
  'cli.command.upload.servePreviewPage': 'Serve a temporary local preview page with test-account and preview QR codes',
7470
- 'cli.command.upload.noOpen': 'Do not open the local preview page in the default browser',
7473
+ 'cli.command.upload.noOpen': 'Do not automatically open the local preview or Vibe QR page',
7471
7474
  'cli.command.upload.format': 'Output format: text | json | ndjson',
7472
- 'cli.command.upload.vibe': 'Binding and upload: collect only title and icon; CLI fills other fields with PPE trial defaults (schema v2)',
7475
+ 'cli.command.upload.vibe': 'Scan with TikTok and upload: provide title and icon; CLI fills other fields with PPE trial defaults (schema v2)',
7473
7476
  'cli.command.upload.vibeTitle': 'Game title supplied by the Skill (--vibe)',
7474
7477
  'cli.command.upload.vibeAppInfo': 'Legacy metadata file compatibility; not needed for current uploads and must not be requested from users',
7475
7478
  'cli.command.upload.vibeIcon': 'Local PNG/JPEG/WebP selected by the user (--vibe)',
@@ -7688,8 +7691,8 @@ Vibe web upload (real PPE trial):
7688
7691
  'cli.command.login.password': '开发者账号密码(implicit 模式)',
7689
7692
  'cli.command.logout.desc': '退出登录并清除本地用户信息',
7690
7693
  'cli.command.auth.desc': '查看本地登录状态',
7691
- 'cli.command.auth.open.desc': '在系统浏览器打开已有 Vibe 任务的授权页',
7692
- 'cli.command.upload.vibeWeb': '网页授权就绪后自动上传到 PPE(--vibe;不能与 Mock 或 --no-open 同用)',
7694
+ 'cli.command.auth.open.desc': '重新打开已有 Vibe 任务的本地二维码页',
7695
+ 'cli.command.upload.vibeWeb': '已关闭;使用 --vibe 并在手机 TikTok 中扫码授权',
7693
7696
  'cli.command.game.desc': '查询平台支持的游戏资料',
7694
7697
  'cli.command.game.categories.desc': '查询游戏类型、子类型及玩法说明(离线、只读)',
7695
7698
  'cli.command.game.categories.category': '按类型的精确值筛选,例如 Puzzle',
@@ -7699,23 +7702,26 @@ Vibe web upload (real PPE trial):
7699
7702
  'cli.command.upload.vibeDescription': '可选游戏描述(--vibe);默认值:Discover the fun of TikTok Mini Games.',
7700
7703
  'cli.command.upload.vibeSubcategory': '所属类型的可选子类型;两者均不传保留 Other/Other(--vibe)',
7701
7704
  'cli.command.upload.vibeWebGuide': `
7702
- Vibe 网页上传(真实 PPE 试用):
7703
- ttmg upload --vibe --web --dir ./game --title "游戏名称" --icon ./avatar.png --format ndjson
7705
+ Vibe 扫码上传(真实 PPE 试用):
7706
+ ttmg upload --vibe --dir ./game --title "游戏名称" --icon ./avatar.png --format ndjson
7704
7707
  已有 ZIP 时,用 --package ./game.zip 替换 --dir。
7705
7708
  只收集标题和头像;其他 app_info 字段由 CLI 使用 PPE 试用默认值补齐。
7706
7709
  Skill 可传 --description "游戏描述";省略时使用 Discover the fun of TikTok Mini Games.(旧资料文件中的描述保留)。
7707
7710
  Skill 可先查 ttmg game categories --format json,再成对传入 --category Puzzle --subcategory Physics。
7708
7711
  两者均不传保留 Other/Other(旧资料文件调用保留文件分类);无效组合在创建任务前报错。
7709
7712
  prepared 和终态事件返回最终 classification 及其来源。
7710
- 保持原进程运行。在 waiting_authorization 阶段,在 CLI 所在电脑的系统浏览器完成授权。
7711
- pageUrl 是本机绑定/进度页;重新打开授权页时,执行返回的 authorizationOpenCommand。
7713
+ 保持原进程运行。在 waiting_authorization 阶段,使用手机 TikTok 扫描 pageUrl 页面中的二维码。
7714
+ pageUrl 是 CLI 所在电脑的二维码/进度页;重新打开时,执行返回的 authorizationOpenCommand。
7712
7715
  CLI 负责轮询,授权后自动上传 avatar、app_info、package,不再启动第二次上传。
7716
+ 上传后进入 waiting_preview,继续查询同一任务;拿到 previewUrl 后,原 pageUrl 页面展示游戏预览二维码。
7717
+ previewReady=true 且 qrPurpose=game_preview 表示可扫码预览;等待超时仍保留 uploaded=true,请核对任务,不要重传。
7713
7718
  真实成功:terminal=true、status=uploaded、uploaded=true、simulation=false。失败时读取 errorCode 和 nextAction。
7714
- --web 在所有输出格式下都会打开浏览器;--mock 是独立的模拟模式。
7719
+ 所有输出格式都会打开本地二维码页;--no-open 可关闭自动打开。
7720
+ 页面展示本次提交的标题、头像、描述、类型和子类型。
7715
7721
  上传完成不代表审核通过或正式上线。详见包内 vibe-upload.md 和 ttmg capabilities --format json。
7716
7722
  `,
7717
7723
  'cli.command.auth.open.pageUrl': '正在运行的 Vibe 上传命令返回的本地 pageUrl',
7718
- 'cli.command.auth.open.requested': '已请求浏览器打开授权页。请在页面完成授权,原上传进程继续轮询。',
7724
+ 'cli.command.auth.open.requested': '已请求打开二维码页。请用手机 TikTok 扫码授权,原上传进程继续轮询。',
7719
7725
  'cli.command.auth.open.invalidPage': '请传入正在运行的 Vibe 上传命令返回的本地 pageUrl,不是平台授权链接。',
7720
7726
  'cli.command.auth.open.unavailable': '授权页不可用、已过期或已不在等待授权。请查看原上传进程状态;未新建任务。',
7721
7727
  'cli.command.auth.open.invalidFormat': '输出格式请使用 text、json 或 ndjson。',
@@ -7914,9 +7920,9 @@ Vibe 网页上传(真实 PPE 试用):
7914
7920
  'cli.command.upload.dir': '小游戏项目根目录(默认为当前目录)',
7915
7921
  'cli.command.upload.preview': '等待平台处理并输出预览二维码原始内容',
7916
7922
  'cli.command.upload.servePreviewPage': '启动包含测试账号与预览二维码的临时本地页面',
7917
- 'cli.command.upload.noOpen': '不使用默认浏览器打开本地预览页面',
7923
+ 'cli.command.upload.noOpen': '不自动打开本地预览页面或 Vibe 二维码页',
7918
7924
  'cli.command.upload.format': '输出格式:text | json | ndjson',
7919
- 'cli.command.upload.vibe': '绑定上传:只收集标题和头像,CLI 自动补齐其他字段的 PPE 试用默认值(schema v2)',
7925
+ 'cli.command.upload.vibe': 'TikTok 扫码上传:只收集标题和头像,CLI 自动补齐其他字段的 PPE 试用默认值(schema v2)',
7920
7926
  'cli.command.upload.vibeAppInfo': '仅兼容旧资料文件;当前上传无需此参数,不应向用户索要',
7921
7927
  'cli.command.upload.vibeTitle': 'Skill 提供的游戏标题(--vibe)',
7922
7928
  'cli.command.upload.vibeIcon': '用户指定的本地 PNG/JPEG/WebP 图片(--vibe)',
@@ -16376,7 +16382,7 @@ async function authStatus(options = {}) {
16376
16382
  return result;
16377
16383
  }
16378
16384
 
16379
- var version = "0.4.6-vibe-beta.15";
16385
+ var version = "0.4.6-vibe-beta.17";
16380
16386
  var engines = {
16381
16387
  node: ">=20.19.1 <21 || >=22"
16382
16388
  };
@@ -17136,7 +17142,7 @@ function getCapabilities() {
17136
17142
  networkRequired: false,
17137
17143
  },
17138
17144
  vibeUpload: {
17139
- command: 'ttmg upload --vibe --web --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
17145
+ command: 'ttmg upload --vibe --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
17140
17146
  mockCommand: 'ttmg upload --vibe --mock --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
17141
17147
  guide: 'vibe-upload.md',
17142
17148
  option: '--vibe',
@@ -17159,7 +17165,7 @@ function getCapabilities() {
17159
17165
  host: 'developers.tiktok.com',
17160
17166
  taskCreation: true,
17161
17167
  statusPolling: true,
17162
- authorizationScheme: 'aweme://webview?url=<encoded landing_url>',
17168
+ authorizationScheme: 'landing_url',
17163
17169
  uploadEnabled: true,
17164
17170
  },
17165
17171
  terminalSuccess: 'uploaded',
@@ -17186,7 +17192,27 @@ function getCapabilities() {
17186
17192
  legacyAppInfo: 'preserved-when-options-omitted; explicit-pair-overrides-category-fields',
17187
17193
  resultField: 'classification',
17188
17194
  },
17189
- webAuthorization: { option: '--web', backend: 'ppe', defaultLane: 'ppe_op_vc', simulation: false, opensSystemBrowser: true, continuesPolling: true, formatIndependent: true, conflictsWith: ['--mock', '--mock-scenario', '--vibe-api', '--no-open'], mockBehaviorUnchanged: true, materialUploadEnabled: true },
17195
+ webAuthorization: { option: '--web', available: false, errorCode: 'VIBE_WEB_DISABLED', replacement: '--vibe', materialUploadEnabled: false },
17196
+ qrAuthorization: {
17197
+ command: 'ttmg upload --vibe --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
17198
+ defaultWithoutWeb: true,
17199
+ backend: 'ppe',
17200
+ defaultLane: 'ppe_op_vc',
17201
+ requiresEnvironmentConfiguration: false,
17202
+ simulation: false,
17203
+ opensLocalQrPage: true,
17204
+ formatIndependent: true,
17205
+ disablePageOpenOption: '--no-open',
17206
+ pageUrlField: 'pageUrl',
17207
+ authorizationMode: 'qr',
17208
+ nextAction: 'scan_qr',
17209
+ userAction: 'Open pageUrl on the CLI computer and scan the authorization QR with TikTok on the phone',
17210
+ continuesPolling: true,
17211
+ materialUploadEnabled: true,
17212
+ phoneAuthorizationVerified: true,
17213
+ authorizationQrPayload: 'landing_url',
17214
+ pageMetadata: ['title', 'icon', 'description', 'category', 'subcategory'],
17215
+ },
17190
17216
  skillWorkflow: {
17191
17217
  preferredFormat: 'ndjson',
17192
17218
  packageAlternative: '--package <zipPath> replaces --dir <gameDir>',
@@ -17195,7 +17221,7 @@ function getCapabilities() {
17195
17221
  uploadOwner: 'cli',
17196
17222
  waitingAuthorization: {
17197
17223
  status: 'waiting_authorization',
17198
- userAction: 'complete authorization in the system browser on the CLI computer',
17224
+ userAction: 'Open pageUrl on the CLI computer and scan the QR with TikTok on the phone',
17199
17225
  pageUrlField: 'pageUrl',
17200
17226
  pageUrlPurpose: 'local binding and upload progress page',
17201
17227
  reopenCommandField: 'authorizationOpenCommand',
@@ -17242,10 +17268,21 @@ function getCapabilities() {
17242
17268
  provisionalHttpAdapter: 'loopback-only',
17243
17269
  httpContract: 'minis-import-task',
17244
17270
  authorizationQr: true,
17245
- previewQr: false,
17271
+ previewQr: true,
17272
+ preview: {
17273
+ taskEndpoint: 'get_minis_import_task_detail_for_agent',
17274
+ waitingStatus: 'waiting_preview',
17275
+ resultFields: ['previewUrl', 'previewReady', 'clientKey', 'appId', 'codeAssetId', 'minisCenterHistoryId'],
17276
+ readiness: 'nonempty-valid-preview-url',
17277
+ qrPurpose: 'game_preview',
17278
+ sameTask: true,
17279
+ timeout: 'shared-operation-timeout',
17280
+ failurePreservesUploaded: true,
17281
+ deviceRunVerified: false,
17282
+ },
17246
17283
  realAuthorizationVerified: true,
17247
17284
  realUploadVerified: true,
17248
- verificationScope: { backend: 'ppe', assets: 'test-fixtures', date: '2026-09-15', production: false },
17285
+ verificationScope: { backend: 'ppe', authorizationMode: 'qr', assets: 'test-fixtures', date: '2026-09-16', production: false },
17249
17286
  autoUploadAfterAuthorization: true,
17250
17287
  boundedPolling: true,
17251
17288
  cancellation: true,
@@ -17263,7 +17300,7 @@ function getCapabilities() {
17263
17300
  requiresRunningUpload: true,
17264
17301
  createsTask: false,
17265
17302
  outputContainsLandingCredentials: false,
17266
- target: 'system-browser',
17303
+ target: 'local-qr-page',
17267
17304
  successStatus: 'open_requested',
17268
17305
  provesAuthorization: false,
17269
17306
  },
@@ -18001,7 +18038,7 @@ withVerboseOption(program
18001
18038
  .option('--serve-preview-page', t('cli.command.upload.servePreviewPage'))
18002
18039
  .option('--no-open', t('cli.command.upload.noOpen'))
18003
18040
  .option('--vibe', t('cli.command.upload.vibe'))
18004
- .option('--web', t('cli.command.upload.vibeWeb'))
18041
+ .addOption(new commander.Option('--web', t('cli.command.upload.vibeWeb')).hideHelp())
18005
18042
  // Vibe v2 backend selection (including opt-in ppe-mock) stays outside the Skill workflow.
18006
18043
  .option('--title <title>', t('cli.command.upload.vibeTitle'))
18007
18044
  .option('--description <description>', t('cli.command.upload.vibeDescription'))
@@ -18555,7 +18592,7 @@ async function executeVibeAuthOpen(options, deps = { open: openVibeBrowser }) {
18555
18592
  const url = authorizationOpenUrl(options.pageUrl);
18556
18593
  await checkAuthorizationPage(url);
18557
18594
  try {
18558
- await deps.open(url);
18595
+ await deps.open(options.pageUrl);
18559
18596
  }
18560
18597
  catch {
18561
18598
  throw new VibeError('VIBE_BROWSER_OPEN_FAILED');
@@ -18596,7 +18633,7 @@ var authOpen = /*#__PURE__*/Object.freeze({
18596
18633
  const prefix = '/bff/app_info/minis_import/';
18597
18634
  const MINIS_IMPORT_PATHS = {
18598
18635
  create: `${prefix}create_minis_import_task_for_agent/`,
18599
- status: `${prefix}get_minis_import_task_status_for_agent/`,
18636
+ status: `${prefix}get_minis_import_task_detail_for_agent/`,
18600
18637
  icon: `${prefix}upload_minis_import_app_icon_for_agent/`,
18601
18638
  info: `${prefix}submit_minis_import_info_for_agent/`,
18602
18639
  package: `${prefix}upload_minis_import_package_for_agent/`,
@@ -18655,6 +18692,31 @@ function createBackend(mode, transport) {
18655
18692
  });
18656
18693
  return {
18657
18694
  mode,
18695
+ async getTaskDetail(task, signal) {
18696
+ const data = await call({ method: 'GET', path: MINIS_IMPORT_PATHS.status,
18697
+ data: credentials(task), signal });
18698
+ const raw = status(data.status);
18699
+ if (raw === 8)
18700
+ throw new VibeError('VIBE_AUTH_EXPIRED');
18701
+ if (raw === 7)
18702
+ throw new VibeError('VIBE_TASK_FAILED');
18703
+ if (data.ready_for_input !== undefined && typeof data.ready_for_input !== 'boolean')
18704
+ throw new VibeError('VIBE_INVALID_RESPONSE');
18705
+ const detail = { statusRaw: raw, readyForInput: data.ready_for_input === true };
18706
+ for (const [wire, field] of [['app_id', 'appId'], ['code_asset_id', 'codeAssetId'],
18707
+ ['minis_center_history_id', 'minisCenterHistoryId']]) {
18708
+ if (data[wire] !== undefined && data[wire] !== '')
18709
+ detail[field] = taskId(data[wire]);
18710
+ }
18711
+ if (data.client_key !== undefined && data.client_key !== '') {
18712
+ if (!nonempty(data.client_key))
18713
+ throw new VibeError('VIBE_INVALID_RESPONSE');
18714
+ detail.clientKey = data.client_key;
18715
+ }
18716
+ if (data.preview_url !== undefined && data.preview_url !== '')
18717
+ detail.previewUrl = safeWebUrl(data.preview_url);
18718
+ return detail;
18719
+ },
18658
18720
  async createTask(input, signal) {
18659
18721
  const data = await call({
18660
18722
  method: 'POST',
@@ -18785,6 +18847,7 @@ function createPpeMockBackend(lane, adapter, scenario = 'success') {
18785
18847
  };
18786
18848
  return {
18787
18849
  mode: 'ppe-mock',
18850
+ getTaskDetail: (task, signal) => real.getTaskDetail(task, signal),
18788
18851
  uploadAvailable: true,
18789
18852
  dispose: () => real.dispose?.(),
18790
18853
  async createTask(input, signal) {
@@ -18970,8 +19033,9 @@ function createMockBackend(scenario = 'success') {
18970
19033
  }
18971
19034
 
18972
19035
  function createAuthorizationScheme(landingUrl) {
18973
- // Encode the entire landing URL once so its token/query stays inside the WebView.
18974
- return `aweme://webview?url=${encodeURIComponent(safeWebUrl(landingUrl))}`;
19036
+ // QR and browser authorization use the same backend-provided landing URL.
19037
+ // Preserve its query exactly; do not wrap it in an app scheme or re-encode it.
19038
+ return safeWebUrl(landingUrl);
18975
19039
  }
18976
19040
 
18977
19041
  const DEV_PORT = 3000;
@@ -19257,20 +19321,53 @@ async function prepareVibeInput(options, onPackaging, signal, deps = { check: ex
19257
19321
  }
19258
19322
 
19259
19323
  const vibePageStyles = `
19260
- [data-phase="failed"] .pending-symbol{background:#fff1ef;color:#b84c3a;font-size:26px;font-weight:600}
19261
- :root{font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;color:#202329;background:#f5f6f8;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased}
19262
- *{box-sizing:border-box}body{margin:0;min-height:100vh;min-height:100svh;display:flex;align-items:center;justify-content:center;padding:28px 16px;font-size:14px;line-height:1.5}
19263
- .shell{width:100%;max-width:424px}.brand{display:flex;align-items:center;gap:9px;margin:0 4px 18px}.brand-mark{width:25px;height:25px;display:grid;place-items:center;background:#202329;color:#fff;border-radius:7px}.brand-name{font-size:14px;font-weight:650;letter-spacing:-.3px}.brand-tool{font-size:12px;color:#8b909a;border-left:1px solid #daddE3;padding-left:9px;margin-left:1px}.mock-badge{margin-left:auto;font-size:10px;line-height:20px;padding:0 7px;border:1px solid #dfe2e7;border-radius:5px;color:#707783;letter-spacing:.03em}
19264
- .card{background:#fff;border:1px solid #e5e7eb;border-radius:20px;padding:26px 28px 24px;box-shadow:0 4px 8px #19223c03,0 16px 40px #19223c05}.steps{display:flex;align-items:center;gap:9px;margin:0 0 25px;padding:0;list-style:none}.step{display:flex;align-items:center;gap:6px;font-size:11px;color:#9298a2;white-space:nowrap}.step:after{content:"";height:1px;width:27px;margin-left:3px;background:#e9ebef}.step:last-child:after{display:none}.step-number{height:18px;min-width:18px;border-radius:50%;border:1px solid #e2e5e9;display:grid;place-items:center;font-size:10px}.step.active{color:#272c34;font-weight:600}.step.active .step-number{background:#272c34;color:white;border-color:#272c34}.step.done{color:#537565}.step.done .step-number{background:#edf6f0;color:#287c54;border-color:#edf6f0}
19265
- .heading{text-align:center}h1{font-size:24px;font-weight:650;line-height:1.35;letter-spacing:-.7px;margin:0 0 9px}.subtitle{color:#777e88;font-size:13px;line-height:1.75;margin:0 auto;max-width:330px;overflow-wrap:anywhere}.qr-area{display:flex;flex-direction:column;align-items:center;margin-top:22px}.qr-frame{background:white;border:1px solid #eceef1;border-radius:15px;padding:10px;width:214px;height:214px;box-shadow:0 3px 12px #20232904}.qr-frame img{width:192px;height:192px;display:block;max-width:100%}.qr-caption{font-size:11px;color:#8c929b;margin:10px 0 0}.status{display:flex;justify-content:center;align-items:center;gap:7px;font-size:12px;color:#6d7580;margin:15px 0 0}.status.complete{color:#2d7b58}.spinner{height:12px;width:12px;border:1.5px solid #d8dce2;border-top-color:#747f8e;border-radius:50%;animation:turn 1s linear infinite;flex-shrink:0}.success-icon{height:14px;width:14px;display:inline-flex;align-items:center;justify-content:center}
19266
- .game{display:flex;align-items:center;gap:11px;margin-top:24px;padding:12px;background:#f7f8fa;border-radius:11px}.game-icon{height:32px;width:32px;display:grid;place-items:center;background:#eceff3;border:1px solid #e4e7ed;border-radius:9px;color:#707b8b;flex-shrink:0}.game-text{min-width:0}.game-label{font-size:10px;color:#9298a2;margin:0 0 1px}.game-title{margin:0;font-size:13px;font-weight:550;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.footer{text-align:center;color:#8a919d;font-size:11px;margin:15px 0 0;line-height:1.7}.footer .dot{display:inline-block;height:5px;width:5px;border-radius:50%;background:#c1995e;vertical-align:middle;margin-right:5px}
19267
- .upload-area{height:268px;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:20px}.upload-symbol{display:grid;place-items:center;height:52px;width:52px;border-radius:16px;background:#f1f4f7;color:#64748b}.percentage{font-size:42px;line-height:1;letter-spacing:-2px;font-variant-numeric:tabular-nums;font-weight:550}.percentage span{font-size:19px;letter-spacing:0;color:#858e9d;padding-left:3px}.progress-wrap{width:100%}progress{display:block;width:100%;height:5px;border:none;border-radius:20px;overflow:hidden;background:#e9edf1;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}.pending-symbol{display:grid;place-items:center;height:64px;width:64px;border-radius:50%;background:#eef6f1;color:#38805b}
19268
- .game{margin:20px 0;padding:12px 14px}.game-icon,.game-avatar{width:44px;height:44px;border-radius:10px;flex-shrink:0}.game-avatar{object-fit:cover;background:#eceff3;border:1px solid #e4e7ed}.game-label{font-size:11px;color:#717985}.game-title{font-size:15px;font-weight:600}.scan-action{font-size:15px;font-weight:600;margin:0 0 6px}.subtitle{color:#656e7b;font-size:12px}.qr-area{margin-top:18px}.footer{color:#727b89}
19269
- @keyframes turn{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.spinner{animation:none}}@media(max-width:420px){body{padding:20px 14px}.card{padding:24px 20px 21px;border-radius:17px}h1{font-size:22px}.brand{margin-bottom:14px}.steps{gap:7px;justify-content:center}.step:after{width:20px}}@media(max-height:700px){body{align-items:flex-start;padding-top:20px;padding-bottom:20px}}`;
19324
+
19325
+ :root{font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;color:#24282f;background:#fff;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased}
19326
+ *{box-sizing:border-box}
19327
+ body{margin:0;min-height:100vh;min-height:100svh;display:flex;align-items:center;justify-content:center;padding:64px 24px;font-size:14px;line-height:1.5}
19328
+ .shell{position:relative;width:100%;max-width:400px}
19329
+ .steps{display:flex;align-items:center;justify-content:center;gap:10px;margin:0 0 24px;padding:0;list-style:none}
19330
+ .step{display:flex;align-items:center;gap:6px;font-size:11px;color:#9298a2;white-space:nowrap}
19331
+ .step:after{content:"";height:1px;width:25px;margin-left:4px;background:#e9ebef}.step:last-child:after{display:none}
19332
+ .step-number{height:18px;min-width:18px;border-radius:50%;border:1px solid #e2e5e9;display:grid;place-items:center;font-size:10px}
19333
+ .step.active{color:#272c34;font-weight:600}.step.active .step-number{background:#272c34;color:#fff;border-color:#272c34}
19334
+ .step.done{color:#537565}.step.done .step-number{background:#edf6f0;color:#287c54;border-color:#edf6f0}
19335
+ .heading{text-align:center}h1{font-size:26px;font-weight:650;line-height:1.35;letter-spacing:-.6px;margin:0 0 10px}
19336
+ .subtitle{color:#7a818c;font-size:12px;line-height:1.7;margin:0;overflow-wrap:anywhere}
19337
+ .game{margin:24px 0 0;padding-top:20px;border-top:1px solid #eceef1;text-align:center}
19338
+ .game-summary{display:flex;align-items:center;justify-content:center;gap:12px}.game-title{max-width:100%;margin:0;font-size:15px;font-weight:600;overflow-wrap:anywhere;min-width:0}
19339
+ .game-icon,.game-avatar{width:44px;height:44px;border-radius:10px;flex-shrink:0;background:#f0f2f5}.game-avatar{object-fit:cover}.game-icon{display:grid;place-items:center;color:#707b8b}
19340
+ .game-details{margin:16px 0 0;font-size:13px}.game-details>div:first-child,.game-categories>div{display:flex;align-items:baseline;justify-content:center;gap:8px}.game-details dt{flex-shrink:0;font-size:11px;color:#9298a2;margin:0}.game-details dd{min-width:0;margin:0;text-align:left;overflow-wrap:anywhere}
19341
+ .game-description{white-space:pre-wrap;line-height:1.65}.game-categories{display:grid;grid-template-columns:1fr;gap:16px;margin-top:16px}
19342
+ .scan-action{text-align:center;font-size:13px;font-weight:500;margin:26px 0 10px}.qr-area{display:flex;justify-content:center}.qr-frame{padding:8px;width:208px;height:208px}.qr-frame img{display:block;width:192px;height:192px;max-width:100%}
19343
+ .upload-area{display:flex;align-items:center;flex-direction:column;gap:16px;padding-top:26px}.percentage{font-size:34px;line-height:1;font-variant-numeric:tabular-nums;font-weight:550}.percentage span{font-size:16px;color:#858e9d;padding-left:3px}
19344
+ .progress-wrap{width:100%}progress{display:block;width:100%;height:4px;border:none;border-radius:20px;overflow:hidden;background:#edf0f4;accent-color:#4c6378}progress::-webkit-progress-bar{background:#edf0f4;border-radius:20px}progress::-webkit-progress-value{background:#4c6378;border-radius:20px}progress::-moz-progress-bar{background:#4c6378;border-radius:20px}.transfer-label{text-align:center;font-size:11px;color:#8b939f;margin:8px 0 0}
19345
+ .footer{text-align:center;font-size:10px;line-height:1.7;color:#a0a5ae;margin:24px 0 0}.footer .dot{display:inline-block;height:5px;width:5px;border-radius:50%;background:#c1995e;vertical-align:middle;margin-right:5px}.mock-badge{width:fit-content;margin:0 0 10px auto;font-size:10px;padding:2px 7px;border:1px solid #dfe2e7;border-radius:5px;color:#707783}
19346
+ @media(max-width:380px){body{padding-left:20px;padding-right:20px}.steps{gap:7px}.step:after{width:16px;margin-left:1px}h1{font-size:24px}}
19347
+ `;
19270
19348
 
19271
19349
  const VIBE_PAGE_WORKER = '__vibe-page-server';
19272
19350
  // Includes the base64 form of the locally validated image (up to 10 MiB).
19273
19351
  const VIBE_PAGE_HTML_LIMIT = 16 * 1024 * 1024;
19352
+ const pageUpdates = `(() => {
19353
+ let etag;
19354
+ const terminal = () => ['complete', 'preview', 'failed'].includes(document.querySelector('main')?.dataset.phase);
19355
+ async function refresh() {
19356
+ if (terminal()) return;
19357
+ try {
19358
+ const response = await fetch(location.pathname, {cache: 'no-store', headers: etag ? {'If-None-Match': etag} : {}});
19359
+ if (response.ok) {
19360
+ etag = response.headers.get('etag');
19361
+ const next = new DOMParser().parseFromString(await response.text(), 'text/html');
19362
+ const current = document.querySelector('main'), updated = next.querySelector('main');
19363
+ if (current && updated && current.outerHTML !== updated.outerHTML) current.replaceWith(updated);
19364
+ document.title = next.title;
19365
+ }
19366
+ } catch { /* Keep the current page readable during a temporary connection failure. */ }
19367
+ if (!terminal()) setTimeout(refresh, 2000);
19368
+ }
19369
+ if (!terminal()) setTimeout(refresh, 2000);
19370
+ })();`;
19274
19371
  const escape = (s) => s.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
19275
19372
  async function renderVibePage(input) {
19276
19373
  const zh = input.language !== 'en-US';
@@ -19280,7 +19377,6 @@ async function renderVibePage(input) {
19280
19377
  ? {
19281
19378
  steps: ['扫码授权', '上传游戏', '完成'],
19282
19379
  mock: input.hybrid ? 'PPE + Mock' : '模拟',
19283
- game: '当前游戏',
19284
19380
  gameIcon: '游戏头像',
19285
19381
  bindingTitle: '上传到 TikTok',
19286
19382
  bindingAction: input.hybrid ? '真实授权二维码 · 混合联调' : '使用 TikTok 扫描二维码',
@@ -19293,7 +19389,6 @@ async function renderVibePage(input) {
19293
19389
  uploadingTitle: '正在上传小游戏',
19294
19390
  uploadingSubtitle: input.simulation ? '正在模拟授权后的上传流程,不会上传游戏内容。' : '授权已完成,正在上传本次游戏内容。',
19295
19391
  uploadLabel: input.simulation ? '模拟上传进度' : '上传进度',
19296
- uploadStatus: '正在上传游戏,请稍候',
19297
19392
  completeTitle: input.simulation ? '模拟上传完成' : '上传完成',
19298
19393
  completeSubtitle: input.simulation
19299
19394
  ? '绑定和上传流程模拟完成,未向 TikTok 上传游戏。'
@@ -19307,7 +19402,6 @@ async function renderVibePage(input) {
19307
19402
  : {
19308
19403
  steps: ['Authorize', 'Upload', 'Done'],
19309
19404
  mock: input.hybrid ? 'PPE + Mock' : 'MOCK',
19310
- game: 'YOUR GAME',
19311
19405
  gameIcon: 'Game icon',
19312
19406
  bindingTitle: 'Upload to TikTok',
19313
19407
  bindingAction: input.hybrid ? 'Real authorization QR · integration demo' : 'Scan the QR code with TikTok',
@@ -19324,7 +19418,6 @@ async function renderVibePage(input) {
19324
19418
  uploadLabel: input.simulation
19325
19419
  ? 'Simulated upload progress'
19326
19420
  : 'Upload progress',
19327
- uploadStatus: 'Uploading your game. Please wait.',
19328
19421
  completeTitle: input.simulation ? 'Simulated upload complete' : 'Upload complete',
19329
19422
  completeSubtitle: input.simulation
19330
19423
  ? 'Binding and upload simulation complete. Nothing was uploaded to TikTok.'
@@ -19350,78 +19443,77 @@ async function renderVibePage(input) {
19350
19443
  /^data:image\/(?:png|jpeg|webp);base64,[A-Za-z0-9+/]+=*$/.test(input.iconDataUrl)
19351
19444
  ? `<img class="game-avatar" width="44" height="44" alt="${copy.gameIcon}" src="${input.iconDataUrl}">`
19352
19445
  : '<span class="game-icon" aria-hidden="true"><svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M7 7h10c2 0 3 2 3 4l1 5c.4 2-1 3-2.5 2l-3-2h-7l-3 2C4 19 2.6 18 3 16l1-5c0-2 1-4 3-4Z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M8 10v4m-2-2h4m6-1h.01M18 13h.01" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></span>';
19353
- const game = `<div class="game">${icon}<div class="game-text"><p class="game-label">${copy.game}</p><p class="game-title" title="${escape(input.title)}">${escape(input.title)}</p></div></div>`;
19446
+ const game = `<div class="game"><div class="game-summary">${icon}<p class="game-title">${escape(input.title)}</p></div><dl class="game-details"><div><dt>${zh ? '描述' : 'Description'}</dt><dd class="game-description">${escape(input.description ?? VIBE_PPE_DEFAULT_APP_INFO.description)}</dd></div><div class="game-categories"><div><dt>${zh ? '类型' : 'Category'}</dt><dd>${escape(input.category ?? VIBE_PPE_DEFAULT_APP_INFO.category)}</dd></div><div><dt>${zh ? '子类型' : 'Subcategory'}</dt><dd>${escape(input.subcategory ?? VIBE_PPE_DEFAULT_APP_INFO.subcategory)}</dd></div></div></dl></div>`;
19354
19447
  const checkIcon = '<svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="m3 8 3 3 7-7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
19355
- const title = phase === 'binding'
19356
- ? copy.bindingTitle
19357
- : phase === 'uploading'
19358
- ? copy.uploadingTitle
19359
- : phase === 'failed'
19360
- ? copy.failedTitle
19361
- : copy.completeTitle;
19362
- const subtitle = phase === 'binding'
19363
- ? copy.bindingSubtitle
19364
- : phase === 'uploading'
19365
- ? copy.uploadingSubtitle
19366
- : phase === 'failed'
19367
- ? escape(input.message)
19368
- : copy.completeSubtitle;
19369
- const qr = phase === 'binding' && !input.web && input.qrUrl
19448
+ const title = phase === 'waiting_preview' ? (zh ? '正在准备预览' : 'Preparing preview')
19449
+ : phase === 'preview' ? (zh ? '扫码预览游戏' : 'Scan to preview')
19450
+ : phase === 'binding'
19451
+ ? copy.bindingTitle
19452
+ : phase === 'uploading'
19453
+ ? copy.uploadingTitle
19454
+ : phase === 'failed'
19455
+ ? copy.failedTitle
19456
+ : copy.completeTitle;
19457
+ const subtitle = phase === 'waiting_preview' ? (zh ? '游戏内容已上传,预览就绪后会自动显示二维码。' : 'Game content uploaded. The preview QR will appear when ready.')
19458
+ : phase === 'preview' ? (zh ? '上传完成,使用手机 TikTok 扫描下方二维码。' : 'Upload complete. Scan the QR below with TikTok on your phone.')
19459
+ : phase === 'complete' && !input.simulation ? escape(input.message)
19460
+ : phase === 'binding'
19461
+ ? copy.bindingSubtitle
19462
+ : phase === 'uploading'
19463
+ ? copy.uploadingSubtitle
19464
+ : phase === 'failed'
19465
+ ? escape(input.message)
19466
+ : copy.completeSubtitle;
19467
+ const qr = (phase === 'binding' || phase === 'preview') && !input.web && input.qrUrl
19370
19468
  ? await QRCode.toDataURL(input.qrUrl, {
19371
19469
  width: 384,
19372
19470
  margin: 4,
19373
19471
  errorCorrectionLevel: 'M',
19374
19472
  })
19375
19473
  : '';
19376
- const purpose = copy.bindingCode;
19474
+ const purpose = phase === 'preview' ? (zh ? '游戏预览二维码' : 'Game preview QR') : copy.bindingCode;
19475
+ const done = phase === 'complete' || phase === 'preview';
19377
19476
  const progress = typeof input.progressPercent === 'number' &&
19378
19477
  Number.isFinite(input.progressPercent)
19379
19478
  ? Math.max(0, Math.min(100, Math.round(input.progressPercent)))
19380
19479
  : undefined;
19381
- const status = phase === 'binding'
19382
- ? copy.bindingStatus
19383
- : phase === 'uploading'
19384
- ? copy.uploadStatus
19385
- : phase === 'failed'
19386
- ? copy.failedTitle
19387
- : copy.accepted;
19388
- const content = phase === 'binding' && input.web
19389
- ? '<div class="upload-area"><span class="spinner" aria-hidden="true"></span></div>'
19390
- : qr
19391
- ? `<div class="qr-area"><div class="qr-frame"><img width="192" height="192" alt="${purpose}" src="${qr}"></div></div>`
19392
- : phase === 'uploading'
19393
- ? `<div class="upload-area"><div class="upload-symbol"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 16V4m-4 4 4-4 4 4M5 15v5h14v-5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></div>${progress !== undefined ? `<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>` : '<span class="spinner" aria-hidden="true"></span>'}</div>`
19394
- : `<div class="upload-area"><div class="pending-symbol">${phase === 'failed' ? '!' : checkIcon}</div></div>`;
19395
- return `<!doctype html><html lang="${zh ? 'zh-CN' : 'en'}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta http-equiv="refresh" content="2"><title>${escape(title)} · ${escape(input.title)}</title><style>${vibePageStyles}</style></head>
19396
- <body><main class="shell" data-phase="${phase}"><header class="brand"><span class="brand-mark" aria-hidden="true"><svg width="16" height="16" viewBox="0 0 20 20" fill="none"><path d="m7 5-4 5 4 5m6-10 4 5-4 5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="brand-name">TikTok Minis</span><span class="brand-tool">DevTool</span>${input.simulation ? `<span class="mock-badge">${copy.mock}</span>` : ''}</header>
19397
- <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 ${i < step ? 'done' : i === step ? 'active' : ''}"${i === step ? ' aria-current="step"' : ''}><span class="step-number">${i < step ? checkIcon : i + 1}</span>${label}</li>`).join('')}</ol>`}
19398
- <div class="heading"><h1 id="page-title">${title}</h1></div>${game}<div class="heading">${phase === 'binding' ? `<p class="scan-action">${copy.bindingAction}</p>` : ''}<p class="subtitle">${subtitle}</p></div>${content}
19399
- <p class="status${phase === 'complete' ? ' complete' : ''}" role="status">${phase === 'complete' ? `<span class="success-icon">${checkIcon}</span>` : phase === 'failed' ? '' : '<span class="spinner" aria-hidden="true"></span>'}${status}</p>
19480
+ const content = qr
19481
+ ? `<div class="qr-area"><div class="qr-frame"><img width="192" height="192" alt="${purpose}" src="${qr}"></div></div>`
19482
+ : phase === 'uploading' && progress !== undefined
19483
+ ? `<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>`
19484
+ : '';
19485
+ return `<!doctype html><html lang="${zh ? 'zh-CN' : 'en'}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${escape(title)} · ${escape(input.title)}</title><style>${vibePageStyles}</style><script src="?updates" defer></script></head>
19486
+ <body><main class="shell" data-phase="${phase}">${input.simulation ? `<p class="mock-badge">${copy.mock}</p>` : ''}
19487
+ <section class="card" aria-labelledby="page-title">${phase === 'failed' ? '' : `<ol class="steps" aria-label="${zh ? '上传步骤' : 'Upload steps'}">${copy.steps.map((label, i) => `<li class="step ${done || i < step ? 'done' : i === step ? 'active' : ''}"${!done && i === step ? ' aria-current="step"' : ''}><span class="step-number">${done || i < step ? checkIcon : i + 1}</span>${label}</li>`).join('')}</ol>`}
19488
+ <div class="heading"><h1 id="page-title">${title}</h1><p class="subtitle" role="status">${subtitle}</p></div>${game}${phase === 'binding' ? `<p class="scan-action">${copy.bindingAction}</p>` : ''}${content}
19400
19489
  </section>
19401
19490
  <p class="footer">${input.simulation ? `<span class="dot" aria-hidden="true"></span>${copy.mockNote}` : copy.footer}</p></main></body></html>`;
19402
19491
  }
19403
19492
  async function createVibePageServer() {
19404
19493
  const route = `/${crypto.randomBytes(24).toString('hex')}`;
19405
19494
  let html = 'Starting…';
19495
+ let etag = '';
19406
19496
  let landingUrl;
19407
19497
  const server = http.createServer((req, res) => {
19408
19498
  const address = server.address();
19409
19499
  const host = address && typeof address !== 'string' ? `127.0.0.1:${address.port}` : '';
19500
+ if (req.method === 'GET' && req.url === `${route}?updates` && req.headers.host === host) {
19501
+ res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' });
19502
+ res.end(pageUpdates);
19503
+ return;
19504
+ }
19410
19505
  if (req.url === `${route}/authorize` && req.headers.host === host &&
19411
19506
  ['GET', 'HEAD'].includes(req.method || '') &&
19412
19507
  (!req.headers.origin || req.headers.origin === `http://${host}`) &&
19413
19508
  req.headers['sec-fetch-site'] !== 'cross-site') {
19414
19509
  const headers = { 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer' };
19415
- if (!landingUrl) {
19510
+ if (!landingUrl || req.method === 'GET') {
19416
19511
  res.writeHead(410, { ...headers, 'Content-Type': 'text/plain; charset=utf-8' });
19417
- res.end('Authorization is no longer available. Return to the upload process.');
19512
+ res.end('Web authorization is disabled. Open the local QR page and scan with TikTok.');
19418
19513
  }
19419
19514
  else if (req.method === 'HEAD') {
19420
19515
  res.writeHead(204, { ...headers, 'X-TTMG-Authorization': 'ready' }).end();
19421
19516
  }
19422
- else {
19423
- res.writeHead(302, { ...headers, Location: landingUrl }).end();
19424
- }
19425
19517
  return;
19426
19518
  }
19427
19519
  if (req.method !== 'GET' ||
@@ -19430,12 +19522,17 @@ async function createVibePageServer() {
19430
19522
  res.writeHead(404).end();
19431
19523
  return;
19432
19524
  }
19525
+ if (etag && req.headers['if-none-match'] === etag) {
19526
+ res.writeHead(304, { 'ETag': etag, 'Cache-Control': 'no-store' }).end();
19527
+ return;
19528
+ }
19433
19529
  res.writeHead(200, {
19530
+ 'ETag': etag,
19434
19531
  'Content-Type': 'text/html; charset=utf-8',
19435
19532
  'Cache-Control': 'no-store',
19436
19533
  'Referrer-Policy': 'no-referrer',
19437
19534
  'X-Content-Type-Options': 'nosniff',
19438
- 'Content-Security-Policy': "default-src 'none'; img-src data:; style-src 'unsafe-inline'; frame-ancestors 'none'",
19535
+ 'Content-Security-Policy': "default-src 'none'; img-src data:; style-src 'unsafe-inline'; script-src 'self'; connect-src 'self'; frame-ancestors 'none'",
19439
19536
  });
19440
19537
  res.end(html);
19441
19538
  });
@@ -19448,6 +19545,7 @@ async function createVibePageServer() {
19448
19545
  url: `http://127.0.0.1:${address.port}${route}`,
19449
19546
  update: (value, authorization) => {
19450
19547
  html = value;
19548
+ etag = '"' + crypto.createHash('sha256').update(value).digest('hex') + '"';
19451
19549
  landingUrl = browserLandingUrl(authorization);
19452
19550
  },
19453
19551
  close: () => server.close(),
@@ -19582,6 +19680,7 @@ const stageText = {
19582
19680
  ],
19583
19681
  uploading: ['授权完成,正在自动上传', 'Authorized; uploading automatically'],
19584
19682
  uploaded: ['上传完成', 'Upload complete'],
19683
+ waiting_preview: ['上传完成,正在准备预览', 'Upload complete; preparing preview'],
19585
19684
  failed: [
19586
19685
  '本次操作失败,请根据错误码处理',
19587
19686
  'Operation failed; check the error code',
@@ -19649,11 +19748,13 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19649
19748
  let timedOut = false;
19650
19749
  let page;
19651
19750
  let backend = deps.backend;
19751
+ let task;
19652
19752
  let detached = false;
19653
19753
  let progressUpdates = Promise.resolve();
19654
19754
  let ppeLane;
19655
19755
  let ppeSelected = false;
19656
19756
  let hybridSelected = false;
19757
+ let pageMetadata;
19657
19758
  let result = {
19658
19759
  schemaVersion: '2',
19659
19760
  command: 'upload',
@@ -19689,20 +19790,11 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19689
19790
  throw new VibeError('VIBE_CANCELLED');
19690
19791
  };
19691
19792
  try {
19692
- // --mock is the stable trial entry: working steps use the built-in PPE lane,
19693
- // blocked steps remain simulated. No environment setup is required.
19694
- // Explicit command-line selection wins; real failures never fall back to mock.
19695
- if (options.web) {
19696
- if (options.mock || options.mockScenario || options.vibeApi || options.open === false)
19697
- throw new VibeError('VIBE_WEB_OPTION_CONFLICT');
19698
- ppeSelected = true;
19699
- ppeLane = process.env.TTMG_VIBE_PPE || 'ppe_op_vc';
19700
- if (!/^ppe_[a-zA-Z0-9_-]{1,64}$/.test(ppeLane))
19701
- throw new VibeError('VIBE_INVALID_PPE_ENV');
19702
- if (deps.backend && deps.backend.mode !== 'ppe')
19703
- throw new VibeError('VIBE_WEB_OPTION_CONFLICT');
19704
- }
19705
- else if (!options.mock && !options.vibeApi && process.env.TTMG_VIBE_BACKEND) {
19793
+ if (options.web)
19794
+ throw new VibeError('VIBE_WEB_DISABLED');
19795
+ // Real QR uploads use the release-owned PPE route.
19796
+ // Explicit test backends remain opt-in; real failures never fall back to them.
19797
+ if (!options.mock && !options.vibeApi && process.env.TTMG_VIBE_BACKEND) {
19706
19798
  const configured = process.env.TTMG_VIBE_BACKEND;
19707
19799
  if (configured === 'mock')
19708
19800
  options = { ...options, mock: true };
@@ -19720,6 +19812,12 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19720
19812
  else
19721
19813
  throw new VibeError('VIBE_BACKEND_NOT_CONFIGURED');
19722
19814
  }
19815
+ else if (!options.mock && !options.vibeApi) {
19816
+ ppeSelected = true;
19817
+ ppeLane = process.env.TTMG_VIBE_PPE || 'ppe_op_vc';
19818
+ if (!/^ppe_[a-zA-Z0-9_-]{1,64}$/.test(ppeLane))
19819
+ throw new VibeError('VIBE_INVALID_PPE_ENV');
19820
+ }
19723
19821
  if (!['text', 'json', 'ndjson'].includes(options.format || 'text'))
19724
19822
  throw new VibeError('VIBE_INVALID_FORMAT');
19725
19823
  if (options.clientKey ||
@@ -19779,16 +19877,22 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19779
19877
  });
19780
19878
  if (options.dryRun)
19781
19879
  return result;
19880
+ const submittedInfo = input.appInfo || VIBE_PPE_DEFAULT_APP_INFO;
19881
+ pageMetadata = {
19882
+ iconDataUrl: `data:${input.icon.contentType};base64,${input.icon.bytes.toString('base64')}`,
19883
+ description: submittedInfo.description,
19884
+ category: submittedInfo.category,
19885
+ subcategory: submittedInfo.subcategory,
19886
+ };
19782
19887
  const pageGame = {
19783
19888
  title: input.title,
19784
- iconDataUrl: `data:${input.icon.contentType};base64,${input.icon.bytes.toString('base64')}`,
19889
+ ...pageMetadata,
19785
19890
  uploadAvailable: backend.uploadAvailable !== false,
19786
19891
  hybrid: backend.mode === 'ppe-mock',
19787
- web: !!options.web,
19788
19892
  };
19789
19893
  const sourceProjectId = deps.projectId(options);
19790
19894
  emit('creating_task', { platformWrite: backend.mode === 'ppe' || backend.mode === 'ppe-mock' });
19791
- const task = await backend.createTask({
19895
+ task = await backend.createTask({
19792
19896
  source_type: 1,
19793
19897
  source_project_id: sourceProjectId,
19794
19898
  task_idempotency_id: result.operationId,
@@ -19796,8 +19900,6 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19796
19900
  }, controller.signal);
19797
19901
  ensureActive();
19798
19902
  const authorizationUrl = createAuthorizationScheme(task.landingUrl);
19799
- if (options.web && !browserLandingUrl(task.landingUrl))
19800
- throw new VibeError('VIBE_INVALID_RESPONSE');
19801
19903
  const pollMs = seconds(options.pollInterval, task.pollIntervalSeconds);
19802
19904
  result = {
19803
19905
  ...result,
@@ -19806,33 +19908,30 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19806
19908
  };
19807
19909
  page = await deps.startPage();
19808
19910
  ensureActive();
19809
- const bindingMessage = options.web
19911
+ const bindingMessage = backend.mode === 'ppe-mock'
19810
19912
  ? (getCurrentLanguage() === 'zh-CN'
19811
- ? '请在浏览器完成真实授权,当前命令会继续查询绑定状态。'
19812
- : 'Complete real authorization in the browser; this command keeps polling the binding status.')
19813
- : backend.mode === 'ppe-mock'
19814
- ? (getCurrentLanguage() === 'zh-CN'
19815
- ? '真实 PPE 任务和授权二维码;查询三次后模拟授权、上传,无需完成手机授权。'
19816
- : 'Real PPE task and authorization QR. After three polls, authorization and upload are simulated; phone authorization is not required.')
19817
- : backend.uploadAvailable === false
19818
- ? (getCurrentLanguage() === 'zh-CN' ? '请扫码完成真实 PPE 授权;上传尚未开放。' : 'Scan to authorize in PPE. Upload is not enabled yet.')
19819
- : text('waiting_authorization', result.simulation);
19913
+ ? '真实 PPE 任务和授权二维码;查询三次后模拟授权、上传,无需完成手机授权。'
19914
+ : 'Real PPE task and authorization QR. After three polls, authorization and upload are simulated; phone authorization is not required.')
19915
+ : backend.uploadAvailable === false
19916
+ ? (getCurrentLanguage() === 'zh-CN' ? '请扫码完成真实 PPE 授权;上传尚未开放。' : 'Scan to authorize in PPE. Upload is not enabled yet.')
19917
+ : text('waiting_authorization', result.simulation);
19820
19918
  await page.update({
19821
19919
  ...pageGame,
19822
19920
  language: getCurrentLanguage(),
19823
19921
  phase: 'binding',
19824
19922
  simulation: result.simulation,
19825
19923
  message: bindingMessage,
19826
- qrUrl: options.web ? undefined : authorizationUrl,
19924
+ qrUrl: authorizationUrl,
19827
19925
  browserLandingUrl: task.landingUrl,
19828
19926
  purpose: 'authorization_binding',
19829
19927
  });
19830
19928
  ensureActive();
19831
19929
  let browserOpenRequested = false;
19832
19930
  let browserOpenErrorCode;
19833
- if (options.web) {
19931
+ const opensRealPage = backend.mode === 'ppe' && options.open !== false;
19932
+ if (opensRealPage) {
19834
19933
  try {
19835
- await deps.open(authorizationOpenUrl(page.url));
19934
+ await deps.open(page.url);
19836
19935
  browserOpenRequested = true;
19837
19936
  }
19838
19937
  catch {
@@ -19842,19 +19941,19 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19842
19941
  }
19843
19942
  emit('waiting_authorization', {
19844
19943
  message: browserOpenErrorCode
19845
- ? (getCurrentLanguage() === 'zh-CN' ? '浏览器未能打开,请使用 authorizationOpenCommand 重试;当前任务仍在等待授权。' : 'Browser opening failed. Retry authorizationOpenCommand; this task is still waiting for authorization.')
19944
+ ? (getCurrentLanguage() === 'zh-CN' ? '二维码页面未能打开,请在 CLI 所在电脑打开 pageUrl 并使用 TikTok 扫码;当前任务仍在等待授权。' : 'Could not open the QR page. Open pageUrl on the CLI computer and scan with TikTok; this task is still waiting for authorization.')
19846
19945
  : bindingMessage,
19847
19946
  pageUrl: page.url,
19848
- authorizationMode: options.web ? 'web' : 'qr',
19849
- ...(options.web ? { browserOpenRequested, browserOpenErrorCode } : {}),
19947
+ authorizationMode: 'qr',
19948
+ ...(backend.mode === 'ppe' ? { browserOpenRequested, browserOpenErrorCode } : {}),
19850
19949
  authorizationOpenCommand: browserLandingUrl(task.landingUrl)
19851
19950
  ? `ttmg auth open --page-url ${page.url} --format json` : undefined,
19852
- qrPurpose: options.web ? undefined : 'authorization_binding',
19853
- nextAction: result.simulation ? 'wait' : options.web ? 'open_browser' : 'scan_qr',
19951
+ qrPurpose: 'authorization_binding',
19952
+ nextAction: result.simulation ? 'wait' : 'scan_qr',
19854
19953
  readyForInput: false,
19855
19954
  });
19856
19955
  // Opening a browser is presentation only and never gates token polling.
19857
- if (!options.web && (options.format || 'text') === 'text' && options.open !== false)
19956
+ if (!opensRealPage && backend.mode !== 'ppe' && (options.format || 'text') === 'text' && options.open !== false)
19858
19957
  void deps.open(page.url).catch(() => { });
19859
19958
  while (true) {
19860
19959
  ensureActive();
@@ -19928,7 +20027,6 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19928
20027
  }
19929
20028
  }
19930
20029
  });
19931
- task.importToken = '';
19932
20030
  task.landingUrl = '';
19933
20031
  // A validated upload receipt is authoritative even if result-page rendering fails.
19934
20032
  result = { ...result, uploaded: true, ...(backend.mode === 'ppe-mock'
@@ -19937,20 +20035,48 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19937
20035
  await progressUpdates;
19938
20036
  if (progressPageError)
19939
20037
  throw progressPageError;
20038
+ if (!result.simulation) {
20039
+ emit('waiting_preview', { previewReady: false, terminal: false, nextAction: 'wait',
20040
+ qrPurpose: undefined, authorizationOpenCommand: undefined });
20041
+ await page.update({ ...pageGame, language: getCurrentLanguage(), phase: 'waiting_preview',
20042
+ simulation: false, message: text('waiting_preview') });
20043
+ while (true) {
20044
+ ensureActive();
20045
+ const detail = await backend.getTaskDetail(task, controller.signal);
20046
+ ensureActive();
20047
+ const fields = { taskStatusRaw: detail.statusRaw,
20048
+ ...(detail.clientKey ? { clientKey: detail.clientKey } : {}),
20049
+ ...(detail.appId ? { appId: detail.appId } : {}),
20050
+ ...(detail.codeAssetId ? { codeAssetId: detail.codeAssetId } : {}),
20051
+ ...(detail.minisCenterHistoryId ? { minisCenterHistoryId: detail.minisCenterHistoryId } : {}) };
20052
+ if (detail.previewUrl) {
20053
+ result = { ...result, ...fields, previewUrl: detail.previewUrl, previewReady: true };
20054
+ break;
20055
+ }
20056
+ if (Object.entries(fields).some(([key, value]) => result[key] !== value))
20057
+ emit('waiting_preview', fields);
20058
+ await deps.sleep(pollMs, controller.signal);
20059
+ }
20060
+ }
19940
20061
  await page.update({
19941
20062
  ...pageGame,
19942
20063
  language: getCurrentLanguage(),
19943
- phase: 'complete',
20064
+ phase: result.previewReady ? 'preview' : 'complete',
20065
+ qrUrl: result.previewUrl,
20066
+ purpose: result.previewReady ? 'game_preview' : undefined,
19944
20067
  simulation: result.simulation,
19945
20068
  message: text('uploaded', result.simulation),
19946
20069
  });
19947
20070
  page.detach();
19948
20071
  detached = true;
19949
- return emit('uploaded', { terminal: true, nextAction: 'none' });
20072
+ return emit('uploaded', { terminal: true, nextAction: result.previewReady ? 'scan_qr' : 'none',
20073
+ qrPurpose: result.previewReady ? 'game_preview' : undefined,
20074
+ ...(result.previewReady ? { message: getCurrentLanguage() === 'zh-CN'
20075
+ ? '上传完成,请使用 TikTok 扫码预览游戏。' : 'Upload complete. Scan with TikTok to preview your game.' } : {}) });
19950
20076
  }
19951
20077
  catch (error) {
19952
20078
  const code = timedOut
19953
- ? 'VIBE_TIMEOUT'
20079
+ ? result.uploaded ? 'VIBE_PREVIEW_TIMEOUT' : 'VIBE_TIMEOUT'
19954
20080
  : controller.signal.aborted
19955
20081
  ? 'VIBE_CANCELLED'
19956
20082
  : error instanceof VibeError
@@ -19965,7 +20091,6 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19965
20091
  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.'],
19966
20092
  VIBE_INVALID_CATEGORY: ['类型或子类型无效,或两者不匹配。使用 ttmg game categories --format json 查询有效组合。', 'Invalid category/subcategory pair. Query valid values with ttmg game categories --format json.'],
19967
20093
  VIBE_INVALID_DESCRIPTION: ['描述必须是非空的有效 Unicode 文本;省略 --description 可使用默认描述。', 'Description must be non-empty valid Unicode text; omit --description to use the default.'],
19968
- VIBE_WEB_OPTION_CONFLICT: ['--web 使用真实网页授权,不能与 --mock、--mock-scenario、--vibe-api 或 --no-open 同时使用。', '--web uses real browser authorization and cannot be combined with --mock, --mock-scenario, --vibe-api or --no-open.'],
19969
20094
  VIBE_AUTH_DENIED: [
19970
20095
  '已取消授权,本次未上传。',
19971
20096
  'Authorization cancelled. No upload was performed.',
@@ -19997,6 +20122,10 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19997
20122
  '请检查本地 ZIP 代码包。',
19998
20123
  'Check the local ZIP package.',
19999
20124
  ],
20125
+ VIBE_WEB_DISABLED: [
20126
+ '网页授权已关闭。请移除 --web,使用 ttmg upload --vibe 并在手机 TikTok 中扫码授权。',
20127
+ 'Web authorization is disabled. Remove --web and use ttmg upload --vibe to scan with TikTok on your phone.',
20128
+ ],
20000
20129
  VIBE_BACKEND_NOT_CONFIGURED: [
20001
20130
  '尚未配置服务;本地模拟请使用 --mock。',
20002
20131
  'No backend configured. Use --mock for local simulation.',
@@ -20036,16 +20165,17 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20036
20165
  };
20037
20166
  const message = result.uploaded
20038
20167
  ? getCurrentLanguage() === 'zh-CN'
20039
- ? '上传已完成,但结果页面显示失败,请勿重复上传。'
20040
- : 'Upload completed, but the result page failed. Do not upload again.'
20168
+ ? `游戏内容已上传,但${code === 'VIBE_PREVIEW_TIMEOUT' ? '等待预览超时' : code === 'VIBE_CANCELLED' ? '已停止等待预览' : '预览查询或页面显示未完成'}。请核对任务,请勿重复上传。`
20169
+ : `Game content was uploaded, but ${code === 'VIBE_PREVIEW_TIMEOUT' ? 'preview polling timed out' : code === 'VIBE_CANCELLED' ? 'preview polling was cancelled' : 'preview lookup or display did not complete'}. Check the task; do not upload again.`
20041
20170
  : messages[code]?.[getCurrentLanguage() === 'zh-CN' ? 0 : 1] ||
20042
20171
  text(status);
20043
20172
  controller.abort();
20044
20173
  await progressUpdates;
20045
20174
  // Remove the binding QR on failure too. Keep a readable terminal result page.
20046
- if (page && code !== 'VIBE_CANCELLED') {
20175
+ if (page && (code !== 'VIBE_CANCELLED' || result.uploaded)) {
20047
20176
  try {
20048
20177
  await page.update({
20178
+ ...pageMetadata,
20049
20179
  title: result.title || '',
20050
20180
  language: getCurrentLanguage(),
20051
20181
  simulation: result.simulation,
@@ -20062,7 +20192,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20062
20192
  }
20063
20193
  return emit(status, {
20064
20194
  terminal: true,
20065
- nextAction: result.uploaded || code === 'VIBE_CANCELLED'
20195
+ nextAction: result.uploaded ? 'check_task' : code === 'VIBE_CANCELLED'
20066
20196
  ? 'none'
20067
20197
  : result.uploadAttempted ||
20068
20198
  (result.taskId &&
@@ -20086,6 +20216,10 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20086
20216
  });
20087
20217
  }
20088
20218
  finally {
20219
+ if (task) {
20220
+ task.importToken = '';
20221
+ task.landingUrl = '';
20222
+ }
20089
20223
  clearTimeout(timeout);
20090
20224
  externalSignal?.removeEventListener('abort', cancel);
20091
20225
  backend?.dispose?.();