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

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,24 @@ 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
7266
  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.
7267
+ The local QR page opens for every output format; --no-open disables automatic opening.
7268
+ The page displays the submitted title, icon, description, category and subcategory.
7268
7269
  Upload completion is not review approval or production release. See bundled vibe-upload.md and ttmg capabilities --format json.
7269
7270
  `,
7270
7271
  '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.',
7272
+ 'cli.command.auth.open.requested': 'QR page open requested. Scan with TikTok on your phone; the existing upload process continues polling.',
7272
7273
  'cli.command.auth.open.invalidPage': 'Pass the local pageUrl from the running Vibe upload command, not the platform URL.',
7273
7274
  '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
7275
  'cli.command.auth.open.invalidFormat': 'Use text, json, or ndjson output.',
@@ -7467,9 +7468,9 @@ Vibe web upload (real PPE trial):
7467
7468
  'cli.command.upload.dir': 'Game project directory (default: current directory)',
7468
7469
  'cli.command.upload.preview': 'Wait for platform processing and output the preview payload',
7469
7470
  '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',
7471
+ 'cli.command.upload.noOpen': 'Do not automatically open the local preview or Vibe QR page',
7471
7472
  '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)',
7473
+ 'cli.command.upload.vibe': 'Scan with TikTok and upload: provide title and icon; CLI fills other fields with PPE trial defaults (schema v2)',
7473
7474
  'cli.command.upload.vibeTitle': 'Game title supplied by the Skill (--vibe)',
7474
7475
  'cli.command.upload.vibeAppInfo': 'Legacy metadata file compatibility; not needed for current uploads and must not be requested from users',
7475
7476
  'cli.command.upload.vibeIcon': 'Local PNG/JPEG/WebP selected by the user (--vibe)',
@@ -7688,8 +7689,8 @@ Vibe web upload (real PPE trial):
7688
7689
  'cli.command.login.password': '开发者账号密码(implicit 模式)',
7689
7690
  'cli.command.logout.desc': '退出登录并清除本地用户信息',
7690
7691
  'cli.command.auth.desc': '查看本地登录状态',
7691
- 'cli.command.auth.open.desc': '在系统浏览器打开已有 Vibe 任务的授权页',
7692
- 'cli.command.upload.vibeWeb': '网页授权就绪后自动上传到 PPE(--vibe;不能与 Mock 或 --no-open 同用)',
7692
+ 'cli.command.auth.open.desc': '重新打开已有 Vibe 任务的本地二维码页',
7693
+ 'cli.command.upload.vibeWeb': '已关闭;使用 --vibe 并在手机 TikTok 中扫码授权',
7693
7694
  'cli.command.game.desc': '查询平台支持的游戏资料',
7694
7695
  'cli.command.game.categories.desc': '查询游戏类型、子类型及玩法说明(离线、只读)',
7695
7696
  'cli.command.game.categories.category': '按类型的精确值筛选,例如 Puzzle',
@@ -7699,23 +7700,24 @@ Vibe web upload (real PPE trial):
7699
7700
  'cli.command.upload.vibeDescription': '可选游戏描述(--vibe);默认值:Discover the fun of TikTok Mini Games.',
7700
7701
  'cli.command.upload.vibeSubcategory': '所属类型的可选子类型;两者均不传保留 Other/Other(--vibe)',
7701
7702
  'cli.command.upload.vibeWebGuide': `
7702
- Vibe 网页上传(真实 PPE 试用):
7703
- ttmg upload --vibe --web --dir ./game --title "游戏名称" --icon ./avatar.png --format ndjson
7703
+ Vibe 扫码上传(真实 PPE 试用):
7704
+ ttmg upload --vibe --dir ./game --title "游戏名称" --icon ./avatar.png --format ndjson
7704
7705
  已有 ZIP 时,用 --package ./game.zip 替换 --dir。
7705
7706
  只收集标题和头像;其他 app_info 字段由 CLI 使用 PPE 试用默认值补齐。
7706
7707
  Skill 可传 --description "游戏描述";省略时使用 Discover the fun of TikTok Mini Games.(旧资料文件中的描述保留)。
7707
7708
  Skill 可先查 ttmg game categories --format json,再成对传入 --category Puzzle --subcategory Physics。
7708
7709
  两者均不传保留 Other/Other(旧资料文件调用保留文件分类);无效组合在创建任务前报错。
7709
7710
  prepared 和终态事件返回最终 classification 及其来源。
7710
- 保持原进程运行。在 waiting_authorization 阶段,在 CLI 所在电脑的系统浏览器完成授权。
7711
- pageUrl 是本机绑定/进度页;重新打开授权页时,执行返回的 authorizationOpenCommand。
7711
+ 保持原进程运行。在 waiting_authorization 阶段,使用手机 TikTok 扫描 pageUrl 页面中的二维码。
7712
+ pageUrl 是 CLI 所在电脑的二维码/进度页;重新打开时,执行返回的 authorizationOpenCommand。
7712
7713
  CLI 负责轮询,授权后自动上传 avatar、app_info、package,不再启动第二次上传。
7713
7714
  真实成功:terminal=true、status=uploaded、uploaded=true、simulation=false。失败时读取 errorCode 和 nextAction。
7714
- --web 在所有输出格式下都会打开浏览器;--mock 是独立的模拟模式。
7715
+ 所有输出格式都会打开本地二维码页;--no-open 可关闭自动打开。
7716
+ 页面展示本次提交的标题、头像、描述、类型和子类型。
7715
7717
  上传完成不代表审核通过或正式上线。详见包内 vibe-upload.md 和 ttmg capabilities --format json。
7716
7718
  `,
7717
7719
  'cli.command.auth.open.pageUrl': '正在运行的 Vibe 上传命令返回的本地 pageUrl',
7718
- 'cli.command.auth.open.requested': '已请求浏览器打开授权页。请在页面完成授权,原上传进程继续轮询。',
7720
+ 'cli.command.auth.open.requested': '已请求打开二维码页。请用手机 TikTok 扫码授权,原上传进程继续轮询。',
7719
7721
  'cli.command.auth.open.invalidPage': '请传入正在运行的 Vibe 上传命令返回的本地 pageUrl,不是平台授权链接。',
7720
7722
  'cli.command.auth.open.unavailable': '授权页不可用、已过期或已不在等待授权。请查看原上传进程状态;未新建任务。',
7721
7723
  'cli.command.auth.open.invalidFormat': '输出格式请使用 text、json 或 ndjson。',
@@ -7914,9 +7916,9 @@ Vibe 网页上传(真实 PPE 试用):
7914
7916
  'cli.command.upload.dir': '小游戏项目根目录(默认为当前目录)',
7915
7917
  'cli.command.upload.preview': '等待平台处理并输出预览二维码原始内容',
7916
7918
  'cli.command.upload.servePreviewPage': '启动包含测试账号与预览二维码的临时本地页面',
7917
- 'cli.command.upload.noOpen': '不使用默认浏览器打开本地预览页面',
7919
+ 'cli.command.upload.noOpen': '不自动打开本地预览页面或 Vibe 二维码页',
7918
7920
  'cli.command.upload.format': '输出格式:text | json | ndjson',
7919
- 'cli.command.upload.vibe': '绑定上传:只收集标题和头像,CLI 自动补齐其他字段的 PPE 试用默认值(schema v2)',
7921
+ 'cli.command.upload.vibe': 'TikTok 扫码上传:只收集标题和头像,CLI 自动补齐其他字段的 PPE 试用默认值(schema v2)',
7920
7922
  'cli.command.upload.vibeAppInfo': '仅兼容旧资料文件;当前上传无需此参数,不应向用户索要',
7921
7923
  'cli.command.upload.vibeTitle': 'Skill 提供的游戏标题(--vibe)',
7922
7924
  'cli.command.upload.vibeIcon': '用户指定的本地 PNG/JPEG/WebP 图片(--vibe)',
@@ -16376,7 +16378,7 @@ async function authStatus(options = {}) {
16376
16378
  return result;
16377
16379
  }
16378
16380
 
16379
- var version = "0.4.6-vibe-beta.15";
16381
+ var version = "0.4.6-vibe-beta.16";
16380
16382
  var engines = {
16381
16383
  node: ">=20.19.1 <21 || >=22"
16382
16384
  };
@@ -17136,7 +17138,7 @@ function getCapabilities() {
17136
17138
  networkRequired: false,
17137
17139
  },
17138
17140
  vibeUpload: {
17139
- command: 'ttmg upload --vibe --web --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
17141
+ command: 'ttmg upload --vibe --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
17140
17142
  mockCommand: 'ttmg upload --vibe --mock --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
17141
17143
  guide: 'vibe-upload.md',
17142
17144
  option: '--vibe',
@@ -17159,7 +17161,7 @@ function getCapabilities() {
17159
17161
  host: 'developers.tiktok.com',
17160
17162
  taskCreation: true,
17161
17163
  statusPolling: true,
17162
- authorizationScheme: 'aweme://webview?url=<encoded landing_url>',
17164
+ authorizationScheme: 'landing_url',
17163
17165
  uploadEnabled: true,
17164
17166
  },
17165
17167
  terminalSuccess: 'uploaded',
@@ -17186,7 +17188,27 @@ function getCapabilities() {
17186
17188
  legacyAppInfo: 'preserved-when-options-omitted; explicit-pair-overrides-category-fields',
17187
17189
  resultField: 'classification',
17188
17190
  },
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 },
17191
+ webAuthorization: { option: '--web', available: false, errorCode: 'VIBE_WEB_DISABLED', replacement: '--vibe', materialUploadEnabled: false },
17192
+ qrAuthorization: {
17193
+ command: 'ttmg upload --vibe --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
17194
+ defaultWithoutWeb: true,
17195
+ backend: 'ppe',
17196
+ defaultLane: 'ppe_op_vc',
17197
+ requiresEnvironmentConfiguration: false,
17198
+ simulation: false,
17199
+ opensLocalQrPage: true,
17200
+ formatIndependent: true,
17201
+ disablePageOpenOption: '--no-open',
17202
+ pageUrlField: 'pageUrl',
17203
+ authorizationMode: 'qr',
17204
+ nextAction: 'scan_qr',
17205
+ userAction: 'Open pageUrl on the CLI computer and scan the authorization QR with TikTok on the phone',
17206
+ continuesPolling: true,
17207
+ materialUploadEnabled: true,
17208
+ phoneAuthorizationVerified: true,
17209
+ authorizationQrPayload: 'landing_url',
17210
+ pageMetadata: ['title', 'icon', 'description', 'category', 'subcategory'],
17211
+ },
17190
17212
  skillWorkflow: {
17191
17213
  preferredFormat: 'ndjson',
17192
17214
  packageAlternative: '--package <zipPath> replaces --dir <gameDir>',
@@ -17195,7 +17217,7 @@ function getCapabilities() {
17195
17217
  uploadOwner: 'cli',
17196
17218
  waitingAuthorization: {
17197
17219
  status: 'waiting_authorization',
17198
- userAction: 'complete authorization in the system browser on the CLI computer',
17220
+ userAction: 'Open pageUrl on the CLI computer and scan the QR with TikTok on the phone',
17199
17221
  pageUrlField: 'pageUrl',
17200
17222
  pageUrlPurpose: 'local binding and upload progress page',
17201
17223
  reopenCommandField: 'authorizationOpenCommand',
@@ -17245,7 +17267,7 @@ function getCapabilities() {
17245
17267
  previewQr: false,
17246
17268
  realAuthorizationVerified: true,
17247
17269
  realUploadVerified: true,
17248
- verificationScope: { backend: 'ppe', assets: 'test-fixtures', date: '2026-09-15', production: false },
17270
+ verificationScope: { backend: 'ppe', authorizationMode: 'qr', assets: 'test-fixtures', date: '2026-09-16', production: false },
17249
17271
  autoUploadAfterAuthorization: true,
17250
17272
  boundedPolling: true,
17251
17273
  cancellation: true,
@@ -17263,7 +17285,7 @@ function getCapabilities() {
17263
17285
  requiresRunningUpload: true,
17264
17286
  createsTask: false,
17265
17287
  outputContainsLandingCredentials: false,
17266
- target: 'system-browser',
17288
+ target: 'local-qr-page',
17267
17289
  successStatus: 'open_requested',
17268
17290
  provesAuthorization: false,
17269
17291
  },
@@ -18001,7 +18023,7 @@ withVerboseOption(program
18001
18023
  .option('--serve-preview-page', t('cli.command.upload.servePreviewPage'))
18002
18024
  .option('--no-open', t('cli.command.upload.noOpen'))
18003
18025
  .option('--vibe', t('cli.command.upload.vibe'))
18004
- .option('--web', t('cli.command.upload.vibeWeb'))
18026
+ .addOption(new commander.Option('--web', t('cli.command.upload.vibeWeb')).hideHelp())
18005
18027
  // Vibe v2 backend selection (including opt-in ppe-mock) stays outside the Skill workflow.
18006
18028
  .option('--title <title>', t('cli.command.upload.vibeTitle'))
18007
18029
  .option('--description <description>', t('cli.command.upload.vibeDescription'))
@@ -18555,7 +18577,7 @@ async function executeVibeAuthOpen(options, deps = { open: openVibeBrowser }) {
18555
18577
  const url = authorizationOpenUrl(options.pageUrl);
18556
18578
  await checkAuthorizationPage(url);
18557
18579
  try {
18558
- await deps.open(url);
18580
+ await deps.open(options.pageUrl);
18559
18581
  }
18560
18582
  catch {
18561
18583
  throw new VibeError('VIBE_BROWSER_OPEN_FAILED');
@@ -18970,8 +18992,9 @@ function createMockBackend(scenario = 'success') {
18970
18992
  }
18971
18993
 
18972
18994
  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))}`;
18995
+ // QR and browser authorization use the same backend-provided landing URL.
18996
+ // Preserve its query exactly; do not wrap it in an app scheme or re-encode it.
18997
+ return safeWebUrl(landingUrl);
18975
18998
  }
18976
18999
 
18977
19000
  const DEV_PORT = 3000;
@@ -19257,20 +19280,54 @@ async function prepareVibeInput(options, onPackaging, signal, deps = { check: ex
19257
19280
  }
19258
19281
 
19259
19282
  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}}`;
19283
+
19284
+ :root{font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;color:#24282f;background:#fff;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased}
19285
+ *{box-sizing:border-box}
19286
+ body{margin:0;min-height:100vh;min-height:100svh;display:flex;align-items:center;justify-content:center;padding:36px 24px;font-size:14px;line-height:1.5}
19287
+ .shell{width:100%;max-width:400px}
19288
+ .steps{display:flex;align-items:center;justify-content:center;gap:10px;margin:0 0 32px;padding:0;list-style:none}
19289
+ .step{display:flex;align-items:center;gap:6px;font-size:11px;color:#9298a2;white-space:nowrap}
19290
+ .step:after{content:"";height:1px;width:25px;margin-left:4px;background:#e9ebef}.step:last-child:after{display:none}
19291
+ .step-number{height:18px;min-width:18px;border-radius:50%;border:1px solid #e2e5e9;display:grid;place-items:center;font-size:10px}
19292
+ .step.active{color:#272c34;font-weight:600}.step.active .step-number{background:#272c34;color:#fff;border-color:#272c34}
19293
+ .step.done{color:#537565}.step.done .step-number{background:#edf6f0;color:#287c54;border-color:#edf6f0}
19294
+ .heading{text-align:center}h1{font-size:26px;font-weight:650;line-height:1.35;letter-spacing:-.6px;margin:0 0 10px}
19295
+ .subtitle{color:#7a818c;font-size:12px;line-height:1.7;margin:0;overflow-wrap:anywhere}
19296
+ .game{margin:28px 0 0;padding-top:24px;border-top:1px solid #eceef1}
19297
+ .game-summary{display:flex;align-items:center;gap:12px}.game-title{margin:0;font-size:15px;font-weight:600;overflow-wrap:anywhere;min-width:0}
19298
+ .game-icon,.game-avatar{width:44px;height:44px;border-radius:10px;flex-shrink:0;background:#f0f2f5}.game-avatar{object-fit:cover}.game-icon{display:grid;place-items:center;color:#707b8b}
19299
+ .game-details{margin:20px 0 0;font-size:13px}.game-details dt{font-size:11px;color:#9298a2;margin-bottom:4px}.game-details dd{margin:0;overflow-wrap:anywhere}
19300
+ .game-description{white-space:pre-wrap;line-height:1.65}.game-categories{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:18px}
19301
+ .scan-action{text-align:center;font-size:13px;font-weight:500;margin:26px 0 10px}.qr-area{display:flex;justify-content:center}.qr-frame{padding:8px;width:208px;height:208px}.qr-frame img{display:block;width:192px;height:192px;max-width:100%}
19302
+ .upload-area{display:flex;align-items:center;flex-direction:column;gap:16px;padding-top:26px}.percentage{font-size:34px;line-height:1;font-variant-numeric:tabular-nums;font-weight:550}.percentage span{font-size:16px;color:#858e9d;padding-left:3px}
19303
+ .progress-wrap{width:100%}progress{display:block;width:100%;height:4px;border:none;border-radius:20px;overflow:hidden;background:#edf0f4;accent-color:#4c6378}progress::-webkit-progress-bar{background:#edf0f4;border-radius:20px}progress::-webkit-progress-value{background:#4c6378;border-radius:20px}progress::-moz-progress-bar{background:#4c6378;border-radius:20px}.transfer-label{text-align:center;font-size:11px;color:#8b939f;margin:8px 0 0}
19304
+ .footer{text-align:center;font-size:10px;line-height:1.7;color:#a0a5ae;margin:28px 0 0}.footer .dot{display:inline-block;height:5px;width:5px;border-radius:50%;background:#c1995e;vertical-align:middle;margin-right:5px}.mock-badge{width:fit-content;margin:0 0 10px auto;font-size:10px;padding:2px 7px;border:1px solid #dfe2e7;border-radius:5px;color:#707783}
19305
+ @media(max-width:380px){body{padding:28px 20px}.steps{gap:7px}.step:after{width:16px;margin-left:1px}h1{font-size:24px}}
19306
+ @media(max-height:640px){body{align-items:flex-start;padding-top:28px;padding-bottom:28px}}
19307
+ `;
19270
19308
 
19271
19309
  const VIBE_PAGE_WORKER = '__vibe-page-server';
19272
19310
  // Includes the base64 form of the locally validated image (up to 10 MiB).
19273
19311
  const VIBE_PAGE_HTML_LIMIT = 16 * 1024 * 1024;
19312
+ const pageUpdates = `(() => {
19313
+ let etag;
19314
+ const terminal = () => ['complete', 'failed'].includes(document.querySelector('main')?.dataset.phase);
19315
+ async function refresh() {
19316
+ if (terminal()) return;
19317
+ try {
19318
+ const response = await fetch(location.pathname, {cache: 'no-store', headers: etag ? {'If-None-Match': etag} : {}});
19319
+ if (response.ok) {
19320
+ etag = response.headers.get('etag');
19321
+ const next = new DOMParser().parseFromString(await response.text(), 'text/html');
19322
+ const current = document.querySelector('main'), updated = next.querySelector('main');
19323
+ if (current && updated && current.outerHTML !== updated.outerHTML) current.replaceWith(updated);
19324
+ document.title = next.title;
19325
+ }
19326
+ } catch { /* Keep the current page readable during a temporary connection failure. */ }
19327
+ if (!terminal()) setTimeout(refresh, 2000);
19328
+ }
19329
+ if (!terminal()) setTimeout(refresh, 2000);
19330
+ })();`;
19274
19331
  const escape = (s) => s.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
19275
19332
  async function renderVibePage(input) {
19276
19333
  const zh = input.language !== 'en-US';
@@ -19280,7 +19337,6 @@ async function renderVibePage(input) {
19280
19337
  ? {
19281
19338
  steps: ['扫码授权', '上传游戏', '完成'],
19282
19339
  mock: input.hybrid ? 'PPE + Mock' : '模拟',
19283
- game: '当前游戏',
19284
19340
  gameIcon: '游戏头像',
19285
19341
  bindingTitle: '上传到 TikTok',
19286
19342
  bindingAction: input.hybrid ? '真实授权二维码 · 混合联调' : '使用 TikTok 扫描二维码',
@@ -19293,7 +19349,6 @@ async function renderVibePage(input) {
19293
19349
  uploadingTitle: '正在上传小游戏',
19294
19350
  uploadingSubtitle: input.simulation ? '正在模拟授权后的上传流程,不会上传游戏内容。' : '授权已完成,正在上传本次游戏内容。',
19295
19351
  uploadLabel: input.simulation ? '模拟上传进度' : '上传进度',
19296
- uploadStatus: '正在上传游戏,请稍候',
19297
19352
  completeTitle: input.simulation ? '模拟上传完成' : '上传完成',
19298
19353
  completeSubtitle: input.simulation
19299
19354
  ? '绑定和上传流程模拟完成,未向 TikTok 上传游戏。'
@@ -19307,7 +19362,6 @@ async function renderVibePage(input) {
19307
19362
  : {
19308
19363
  steps: ['Authorize', 'Upload', 'Done'],
19309
19364
  mock: input.hybrid ? 'PPE + Mock' : 'MOCK',
19310
- game: 'YOUR GAME',
19311
19365
  gameIcon: 'Game icon',
19312
19366
  bindingTitle: 'Upload to TikTok',
19313
19367
  bindingAction: input.hybrid ? 'Real authorization QR · integration demo' : 'Scan the QR code with TikTok',
@@ -19324,7 +19378,6 @@ async function renderVibePage(input) {
19324
19378
  uploadLabel: input.simulation
19325
19379
  ? 'Simulated upload progress'
19326
19380
  : 'Upload progress',
19327
- uploadStatus: 'Uploading your game. Please wait.',
19328
19381
  completeTitle: input.simulation ? 'Simulated upload complete' : 'Upload complete',
19329
19382
  completeSubtitle: input.simulation
19330
19383
  ? 'Binding and upload simulation complete. Nothing was uploaded to TikTok.'
@@ -19350,7 +19403,7 @@ async function renderVibePage(input) {
19350
19403
  /^data:image\/(?:png|jpeg|webp);base64,[A-Za-z0-9+/]+=*$/.test(input.iconDataUrl)
19351
19404
  ? `<img class="game-avatar" width="44" height="44" alt="${copy.gameIcon}" src="${input.iconDataUrl}">`
19352
19405
  : '<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>`;
19406
+ 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
19407
  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
19408
  const title = phase === 'binding'
19356
19409
  ? copy.bindingTitle
@@ -19378,50 +19431,43 @@ async function renderVibePage(input) {
19378
19431
  Number.isFinite(input.progressPercent)
19379
19432
  ? Math.max(0, Math.min(100, Math.round(input.progressPercent)))
19380
19433
  : 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>
19434
+ const content = qr
19435
+ ? `<div class="qr-area"><div class="qr-frame"><img width="192" height="192" alt="${purpose}" src="${qr}"></div></div>`
19436
+ : phase === 'uploading' && progress !== undefined
19437
+ ? `<div class="upload-area"><div class="percentage">${progress}<span>%</span></div><div class="progress-wrap"><progress aria-label="${copy.uploadLabel}" max="100" value="${progress}"></progress><p class="transfer-label">${copy.uploadLabel}</p></div></div>`
19438
+ : '';
19439
+ return `<!doctype html><html lang="${zh ? 'zh-CN' : 'en'}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${escape(title)} · ${escape(input.title)}</title><style>${vibePageStyles}</style><script src="?updates" defer></script></head>
19440
+ <body><main class="shell" data-phase="${phase}">${input.simulation ? `<p class="mock-badge">${copy.mock}</p>` : ''}
19441
+ <section class="card" aria-labelledby="page-title">${phase === 'failed' ? '' : `<ol class="steps" aria-label="${zh ? '上传步骤' : 'Upload steps'}">${copy.steps.map((label, i) => `<li class="step ${phase === 'complete' || i < step ? 'done' : i === step ? 'active' : ''}"${phase !== 'complete' && i === step ? ' aria-current="step"' : ''}><span class="step-number">${phase === 'complete' || i < step ? checkIcon : i + 1}</span>${label}</li>`).join('')}</ol>`}
19442
+ <div class="heading"><h1 id="page-title">${title}</h1><p class="subtitle" role="status">${subtitle}</p></div>${game}${phase === 'binding' ? `<p class="scan-action">${copy.bindingAction}</p>` : ''}${content}
19400
19443
  </section>
19401
19444
  <p class="footer">${input.simulation ? `<span class="dot" aria-hidden="true"></span>${copy.mockNote}` : copy.footer}</p></main></body></html>`;
19402
19445
  }
19403
19446
  async function createVibePageServer() {
19404
19447
  const route = `/${crypto.randomBytes(24).toString('hex')}`;
19405
19448
  let html = 'Starting…';
19449
+ let etag = '';
19406
19450
  let landingUrl;
19407
19451
  const server = http.createServer((req, res) => {
19408
19452
  const address = server.address();
19409
19453
  const host = address && typeof address !== 'string' ? `127.0.0.1:${address.port}` : '';
19454
+ if (req.method === 'GET' && req.url === `${route}?updates` && req.headers.host === host) {
19455
+ res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' });
19456
+ res.end(pageUpdates);
19457
+ return;
19458
+ }
19410
19459
  if (req.url === `${route}/authorize` && req.headers.host === host &&
19411
19460
  ['GET', 'HEAD'].includes(req.method || '') &&
19412
19461
  (!req.headers.origin || req.headers.origin === `http://${host}`) &&
19413
19462
  req.headers['sec-fetch-site'] !== 'cross-site') {
19414
19463
  const headers = { 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer' };
19415
- if (!landingUrl) {
19464
+ if (!landingUrl || req.method === 'GET') {
19416
19465
  res.writeHead(410, { ...headers, 'Content-Type': 'text/plain; charset=utf-8' });
19417
- res.end('Authorization is no longer available. Return to the upload process.');
19466
+ res.end('Web authorization is disabled. Open the local QR page and scan with TikTok.');
19418
19467
  }
19419
19468
  else if (req.method === 'HEAD') {
19420
19469
  res.writeHead(204, { ...headers, 'X-TTMG-Authorization': 'ready' }).end();
19421
19470
  }
19422
- else {
19423
- res.writeHead(302, { ...headers, Location: landingUrl }).end();
19424
- }
19425
19471
  return;
19426
19472
  }
19427
19473
  if (req.method !== 'GET' ||
@@ -19430,12 +19476,17 @@ async function createVibePageServer() {
19430
19476
  res.writeHead(404).end();
19431
19477
  return;
19432
19478
  }
19479
+ if (etag && req.headers['if-none-match'] === etag) {
19480
+ res.writeHead(304, { 'ETag': etag, 'Cache-Control': 'no-store' }).end();
19481
+ return;
19482
+ }
19433
19483
  res.writeHead(200, {
19484
+ 'ETag': etag,
19434
19485
  'Content-Type': 'text/html; charset=utf-8',
19435
19486
  'Cache-Control': 'no-store',
19436
19487
  'Referrer-Policy': 'no-referrer',
19437
19488
  'X-Content-Type-Options': 'nosniff',
19438
- 'Content-Security-Policy': "default-src 'none'; img-src data:; style-src 'unsafe-inline'; frame-ancestors 'none'",
19489
+ 'Content-Security-Policy': "default-src 'none'; img-src data:; style-src 'unsafe-inline'; script-src 'self'; connect-src 'self'; frame-ancestors 'none'",
19439
19490
  });
19440
19491
  res.end(html);
19441
19492
  });
@@ -19448,6 +19499,7 @@ async function createVibePageServer() {
19448
19499
  url: `http://127.0.0.1:${address.port}${route}`,
19449
19500
  update: (value, authorization) => {
19450
19501
  html = value;
19502
+ etag = '"' + crypto.createHash('sha256').update(value).digest('hex') + '"';
19451
19503
  landingUrl = browserLandingUrl(authorization);
19452
19504
  },
19453
19505
  close: () => server.close(),
@@ -19654,6 +19706,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19654
19706
  let ppeLane;
19655
19707
  let ppeSelected = false;
19656
19708
  let hybridSelected = false;
19709
+ let pageMetadata;
19657
19710
  let result = {
19658
19711
  schemaVersion: '2',
19659
19712
  command: 'upload',
@@ -19689,20 +19742,11 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19689
19742
  throw new VibeError('VIBE_CANCELLED');
19690
19743
  };
19691
19744
  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) {
19745
+ if (options.web)
19746
+ throw new VibeError('VIBE_WEB_DISABLED');
19747
+ // Real QR uploads use the release-owned PPE route.
19748
+ // Explicit test backends remain opt-in; real failures never fall back to them.
19749
+ if (!options.mock && !options.vibeApi && process.env.TTMG_VIBE_BACKEND) {
19706
19750
  const configured = process.env.TTMG_VIBE_BACKEND;
19707
19751
  if (configured === 'mock')
19708
19752
  options = { ...options, mock: true };
@@ -19720,6 +19764,12 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19720
19764
  else
19721
19765
  throw new VibeError('VIBE_BACKEND_NOT_CONFIGURED');
19722
19766
  }
19767
+ else if (!options.mock && !options.vibeApi) {
19768
+ ppeSelected = true;
19769
+ ppeLane = process.env.TTMG_VIBE_PPE || 'ppe_op_vc';
19770
+ if (!/^ppe_[a-zA-Z0-9_-]{1,64}$/.test(ppeLane))
19771
+ throw new VibeError('VIBE_INVALID_PPE_ENV');
19772
+ }
19723
19773
  if (!['text', 'json', 'ndjson'].includes(options.format || 'text'))
19724
19774
  throw new VibeError('VIBE_INVALID_FORMAT');
19725
19775
  if (options.clientKey ||
@@ -19779,12 +19829,18 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19779
19829
  });
19780
19830
  if (options.dryRun)
19781
19831
  return result;
19832
+ const submittedInfo = input.appInfo || VIBE_PPE_DEFAULT_APP_INFO;
19833
+ pageMetadata = {
19834
+ iconDataUrl: `data:${input.icon.contentType};base64,${input.icon.bytes.toString('base64')}`,
19835
+ description: submittedInfo.description,
19836
+ category: submittedInfo.category,
19837
+ subcategory: submittedInfo.subcategory,
19838
+ };
19782
19839
  const pageGame = {
19783
19840
  title: input.title,
19784
- iconDataUrl: `data:${input.icon.contentType};base64,${input.icon.bytes.toString('base64')}`,
19841
+ ...pageMetadata,
19785
19842
  uploadAvailable: backend.uploadAvailable !== false,
19786
19843
  hybrid: backend.mode === 'ppe-mock',
19787
- web: !!options.web,
19788
19844
  };
19789
19845
  const sourceProjectId = deps.projectId(options);
19790
19846
  emit('creating_task', { platformWrite: backend.mode === 'ppe' || backend.mode === 'ppe-mock' });
@@ -19796,8 +19852,6 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19796
19852
  }, controller.signal);
19797
19853
  ensureActive();
19798
19854
  const authorizationUrl = createAuthorizationScheme(task.landingUrl);
19799
- if (options.web && !browserLandingUrl(task.landingUrl))
19800
- throw new VibeError('VIBE_INVALID_RESPONSE');
19801
19855
  const pollMs = seconds(options.pollInterval, task.pollIntervalSeconds);
19802
19856
  result = {
19803
19857
  ...result,
@@ -19806,33 +19860,30 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19806
19860
  };
19807
19861
  page = await deps.startPage();
19808
19862
  ensureActive();
19809
- const bindingMessage = options.web
19863
+ const bindingMessage = backend.mode === 'ppe-mock'
19810
19864
  ? (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);
19865
+ ? '真实 PPE 任务和授权二维码;查询三次后模拟授权、上传,无需完成手机授权。'
19866
+ : 'Real PPE task and authorization QR. After three polls, authorization and upload are simulated; phone authorization is not required.')
19867
+ : backend.uploadAvailable === false
19868
+ ? (getCurrentLanguage() === 'zh-CN' ? '请扫码完成真实 PPE 授权;上传尚未开放。' : 'Scan to authorize in PPE. Upload is not enabled yet.')
19869
+ : text('waiting_authorization', result.simulation);
19820
19870
  await page.update({
19821
19871
  ...pageGame,
19822
19872
  language: getCurrentLanguage(),
19823
19873
  phase: 'binding',
19824
19874
  simulation: result.simulation,
19825
19875
  message: bindingMessage,
19826
- qrUrl: options.web ? undefined : authorizationUrl,
19876
+ qrUrl: authorizationUrl,
19827
19877
  browserLandingUrl: task.landingUrl,
19828
19878
  purpose: 'authorization_binding',
19829
19879
  });
19830
19880
  ensureActive();
19831
19881
  let browserOpenRequested = false;
19832
19882
  let browserOpenErrorCode;
19833
- if (options.web) {
19883
+ const opensRealPage = backend.mode === 'ppe' && options.open !== false;
19884
+ if (opensRealPage) {
19834
19885
  try {
19835
- await deps.open(authorizationOpenUrl(page.url));
19886
+ await deps.open(page.url);
19836
19887
  browserOpenRequested = true;
19837
19888
  }
19838
19889
  catch {
@@ -19842,19 +19893,19 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19842
19893
  }
19843
19894
  emit('waiting_authorization', {
19844
19895
  message: browserOpenErrorCode
19845
- ? (getCurrentLanguage() === 'zh-CN' ? '浏览器未能打开,请使用 authorizationOpenCommand 重试;当前任务仍在等待授权。' : 'Browser opening failed. Retry authorizationOpenCommand; this task is still waiting for authorization.')
19896
+ ? (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
19897
  : bindingMessage,
19847
19898
  pageUrl: page.url,
19848
- authorizationMode: options.web ? 'web' : 'qr',
19849
- ...(options.web ? { browserOpenRequested, browserOpenErrorCode } : {}),
19899
+ authorizationMode: 'qr',
19900
+ ...(backend.mode === 'ppe' ? { browserOpenRequested, browserOpenErrorCode } : {}),
19850
19901
  authorizationOpenCommand: browserLandingUrl(task.landingUrl)
19851
19902
  ? `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',
19903
+ qrPurpose: 'authorization_binding',
19904
+ nextAction: result.simulation ? 'wait' : 'scan_qr',
19854
19905
  readyForInput: false,
19855
19906
  });
19856
19907
  // Opening a browser is presentation only and never gates token polling.
19857
- if (!options.web && (options.format || 'text') === 'text' && options.open !== false)
19908
+ if (!opensRealPage && backend.mode !== 'ppe' && (options.format || 'text') === 'text' && options.open !== false)
19858
19909
  void deps.open(page.url).catch(() => { });
19859
19910
  while (true) {
19860
19911
  ensureActive();
@@ -19965,7 +20016,6 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19965
20016
  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
20017
  VIBE_INVALID_CATEGORY: ['类型或子类型无效,或两者不匹配。使用 ttmg game categories --format json 查询有效组合。', 'Invalid category/subcategory pair. Query valid values with ttmg game categories --format json.'],
19967
20018
  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
20019
  VIBE_AUTH_DENIED: [
19970
20020
  '已取消授权,本次未上传。',
19971
20021
  'Authorization cancelled. No upload was performed.',
@@ -19997,6 +20047,10 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19997
20047
  '请检查本地 ZIP 代码包。',
19998
20048
  'Check the local ZIP package.',
19999
20049
  ],
20050
+ VIBE_WEB_DISABLED: [
20051
+ '网页授权已关闭。请移除 --web,使用 ttmg upload --vibe 并在手机 TikTok 中扫码授权。',
20052
+ 'Web authorization is disabled. Remove --web and use ttmg upload --vibe to scan with TikTok on your phone.',
20053
+ ],
20000
20054
  VIBE_BACKEND_NOT_CONFIGURED: [
20001
20055
  '尚未配置服务;本地模拟请使用 --mock。',
20002
20056
  'No backend configured. Use --mock for local simulation.',
@@ -20046,6 +20100,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20046
20100
  if (page && code !== 'VIBE_CANCELLED') {
20047
20101
  try {
20048
20102
  await page.update({
20103
+ ...pageMetadata,
20049
20104
  title: result.title || '',
20050
20105
  language: getCurrentLanguage(),
20051
20106
  simulation: result.simulation,