@ttmg/cli 0.4.6-vibe-beta.22 → 0.4.6-vibe-beta.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7248,7 +7248,6 @@ const messages = {
7248
7248
  'cli.command.game.categories.category': 'Filter by an exact category value, e.g. Puzzle',
7249
7249
  'cli.command.game.categories.unknown': 'Unknown category. Run ttmg game categories --format json for valid values.',
7250
7250
  'cli.command.game.categories.invalidFormat': 'Use text, json, or ndjson output.',
7251
- 'cli.command.upload.minisType': 'MINI_GAME (default) or MINI_TOOL (local PPE trial; Native ZIP, Efficiency & Office)',
7252
7251
  'cli.command.upload.vibeCategory': 'Optional game category; provide with --subcategory (--vibe). Query ttmg game categories --format json',
7253
7252
  'cli.command.upload.vibeDescription': 'Optional game description (--vibe); default: Discover the fun of TikTok Mini Games.',
7254
7253
  'cli.command.upload.vibeSubcategory': 'Optional subcategory belonging to --category; omit both to keep Other/Other (--vibe)',
@@ -7264,9 +7263,8 @@ Vibe QR upload (real PPE trial):
7264
7263
  Keep this process running. At waiting_authorization, scan the QR on pageUrl with TikTok on your phone.
7265
7264
  pageUrl is the local QR/progress page on the CLI computer. Reopen it with authorizationOpenCommand.
7266
7265
  The CLI polls and uploads avatar, app_info and package automatically after authorization. Do not start a second upload.
7267
- After upload, waiting_preview keeps polling the same task until previewUrl is available, including status 9 (waiting for review submission).
7268
- When previewReady=true, pageUrl keeps the original QR on the left for checking progress and shows the preview QR on the right. qrPurpose is game_preview or tool_preview. A preview timeout preserves uploaded=true; do not reupload.
7269
- The operation timeout defaults to 1800 seconds (maximum 3600). The page stays available during the operation and for 30 minutes after the CLI finishes.
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.
7270
7268
  Real success: terminal=true, status=uploaded, uploaded=true, simulation=false. On failure, read errorCode and nextAction.
7271
7269
  The local QR page opens for every output format; --no-open disables automatic opening.
7272
7270
  The page displays the submitted title, icon, description, category and subcategory.
@@ -7485,7 +7483,7 @@ Vibe QR upload (real PPE trial):
7485
7483
  'cli.command.upload.vibeApi': 'Minis Import API origin on a loopback IP only (--vibe)',
7486
7484
  'cli.command.upload.vibeAuthorizeUrl': 'Obsolete: remove this option; task creation returns landing_url (--vibe)',
7487
7485
  'cli.command.upload.vibePollInterval': 'Override task polling interval; otherwise use poll_interval_seconds (--vibe)',
7488
- 'cli.command.upload.vibeTimeout': 'Operation timeout in seconds (--vibe; default 1800, maximum 3600)',
7486
+ 'cli.command.upload.vibeTimeout': 'Operation timeout in seconds (--vibe; default 300)',
7489
7487
  'cli.command.upload.vibeRequired': 'These options require --vibe; the existing platform upload has not been executed.',
7490
7488
  'cli.command.testAccount.desc': 'Manage Mini Game test-account access',
7491
7489
  'cli.command.testAccount.add.desc': 'Generate the authorization entry for adding a TikTok test account',
@@ -7700,7 +7698,6 @@ Vibe QR upload (real PPE trial):
7700
7698
  'cli.command.game.categories.category': '按类型的精确值筛选,例如 Puzzle',
7701
7699
  'cli.command.game.categories.unknown': '类型不存在。使用 ttmg game categories --format json 查询有效值。',
7702
7700
  'cli.command.game.categories.invalidFormat': '输出格式请使用 text、json 或 ndjson。',
7703
- 'cli.command.upload.minisType': 'MINI_GAME(默认)或 MINI_TOOL(本地 PPE 试用;Native ZIP、效率办公类)',
7704
7701
  'cli.command.upload.vibeCategory': '可选游戏类型,须与 --subcategory 同传(--vibe);用 ttmg game categories --format json 查询',
7705
7702
  'cli.command.upload.vibeDescription': '可选游戏描述(--vibe);默认值:Discover the fun of TikTok Mini Games.',
7706
7703
  'cli.command.upload.vibeSubcategory': '所属类型的可选子类型;两者均不传保留 Other/Other(--vibe)',
@@ -7716,9 +7713,8 @@ Vibe 扫码上传(真实 PPE 试用):
7716
7713
  保持原进程运行。在 waiting_authorization 阶段,使用手机 TikTok 扫描 pageUrl 页面中的二维码。
7717
7714
  pageUrl 是 CLI 所在电脑的二维码/进度页;重新打开时,执行返回的 authorizationOpenCommand。
7718
7715
  CLI 负责轮询,授权后自动上传 avatar、app_info、package,不再启动第二次上传。
7719
- 上传成功后 waiting_preview 继续查询同一任务直到取得 previewUrl,包括状态 9(等待提交代码审核)。
7720
- previewReady=true 时,pageUrl 左侧保留原码用于查看进度,右侧显示预览码;qrPurpose 为 game_preview 或 tool_preview。等待超时仍保留 uploaded=true,请核对任务,不要重传。
7721
- 整体超时默认 1800 秒,最大 3600 秒;页面在任务进行中保持可用,CLI 结束后再保留 30 分钟。
7716
+ 上传后进入 waiting_preview,继续查询同一任务;拿到 previewUrl 后,原 pageUrl 页面展示游戏预览二维码。
7717
+ previewReady=true 且 qrPurpose=game_preview 表示可扫码预览;等待超时仍保留 uploaded=true,请核对任务,不要重传。
7722
7718
  真实成功:terminal=true、status=uploaded、uploaded=true、simulation=false。失败时读取 errorCode 和 nextAction。
7723
7719
  所有输出格式都会打开本地二维码页;--no-open 可关闭自动打开。
7724
7720
  页面展示本次提交的标题、头像、描述、类型和子类型。
@@ -7937,7 +7933,7 @@ Vibe 扫码上传(真实 PPE 试用):
7937
7933
  'cli.command.upload.vibeApi': 'Minis Import 接口地址,仅接受回环 IP(--vibe)',
7938
7934
  'cli.command.upload.vibeAuthorizeUrl': '已废弃,请移除此参数;授权地址由创建任务返回(--vibe)',
7939
7935
  'cli.command.upload.vibePollInterval': '覆盖 task 轮询间隔;默认使用接口 poll_interval_seconds(--vibe)',
7940
- 'cli.command.upload.vibeTimeout': '操作超时秒数(--vibe,默认 1800,最大 3600)',
7936
+ 'cli.command.upload.vibeTimeout': '操作超时秒数(--vibe,默认 300)',
7941
7937
  'cli.command.upload.vibeRequired': '这些参数需要 --vibe;没有执行原有平台上传。',
7942
7938
  'cli.command.testAccount.desc': '管理小游戏测试账号访问',
7943
7939
  'cli.command.testAccount.add.desc': '生成添加 TikTok 测试账号的授权入口',
@@ -16386,7 +16382,7 @@ async function authStatus(options = {}) {
16386
16382
  return result;
16387
16383
  }
16388
16384
 
16389
- var version = "0.4.6-vibe-beta.22";
16385
+ var version = "0.4.6-vibe-beta.23";
16390
16386
  var engines = {
16391
16387
  node: ">=20.19.1 <21 || >=22"
16392
16388
  };
@@ -16409,23 +16405,6 @@ const VIBE_INPUT_REQUIREMENTS = {
16409
16405
  categoryPairRequiredWhenProvided: true,
16410
16406
  defaultCategory: { category: 'Other', subcategory: 'Other' },
16411
16407
  };
16412
- const VIBE_TOOL_INPUT_REQUIREMENTS = {
16413
- ...VIBE_INPUT_REQUIREMENTS,
16414
- defaultMetadataProfile: 'ppe-tool-trial',
16415
- optionalSkillInputs: ['description', 'minisType'],
16416
- defaultDescription: 'A simple productivity tool for everyday tasks.',
16417
- categoryQuery: undefined,
16418
- categoryPairRequiredWhenProvided: false,
16419
- defaultCategory: { category: 'EFFICIENCY_AND_OFFICE', subcategory: '' },
16420
- region: 'US', minSDKVersion: '1.0.0',
16421
- };
16422
- function resolveMinisType(value) {
16423
- if (value === undefined)
16424
- return 'MINI_GAME';
16425
- if (value !== 'MINI_GAME' && value !== 'MINI_TOOL')
16426
- throw new VibeError('VIBE_INVALID_MINIS_TYPE');
16427
- return value;
16428
- }
16429
16408
  class VibeError extends Error {
16430
16409
  constructor(code) {
16431
16410
  super(code);
@@ -17163,10 +17142,6 @@ function getCapabilities() {
17163
17142
  networkRequired: false,
17164
17143
  },
17165
17144
  vibeUpload: {
17166
- minisType: {
17167
- option: '--minis-type', default: 'MINI_GAME', values: ['MINI_GAME', 'MINI_TOOL'],
17168
- tool: { readiness: 'local-ppe-trial', deviceVerified: false, category: 'EFFICIENCY_AND_OFFICE', region: 'US', minSDKVersion: '1.0.0', assetType: 'NativeRuntime', metadataOverrides: false },
17169
- },
17170
17145
  command: 'ttmg upload --vibe --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
17171
17146
  mockCommand: 'ttmg upload --vibe --mock --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
17172
17147
  guide: 'vibe-upload.md',
@@ -17300,9 +17275,6 @@ function getCapabilities() {
17300
17275
  resultFields: ['previewUrl', 'previewReady', 'clientKey', 'appId', 'codeAssetId', 'minisCenterHistoryId'],
17301
17276
  readiness: 'nonempty-valid-preview-url',
17302
17277
  qrPurpose: 'game_preview',
17303
- toolQrPurpose: 'tool_preview',
17304
- pageQrSource: 'preview-url',
17305
- retainsBindingQr: true,
17306
17278
  sameTask: true,
17307
17279
  timeout: 'shared-operation-timeout',
17308
17280
  failurePreservesUploaded: true,
@@ -18066,7 +18038,6 @@ withVerboseOption(program
18066
18038
  .option('--serve-preview-page', t('cli.command.upload.servePreviewPage'))
18067
18039
  .option('--no-open', t('cli.command.upload.noOpen'))
18068
18040
  .option('--vibe', t('cli.command.upload.vibe'))
18069
- .option('--minis-type <type>', t('cli.command.upload.minisType'))
18070
18041
  .addOption(new commander.Option('--web', t('cli.command.upload.vibeWeb')).hideHelp())
18071
18042
  // Vibe v2 backend selection (including opt-in ppe-mock) stays outside the Skill workflow.
18072
18043
  .option('--title <title>', t('cli.command.upload.vibeTitle'))
@@ -18084,15 +18055,15 @@ withVerboseOption(program
18084
18055
  .option('--poll-interval <seconds>', t('cli.command.upload.vibePollInterval'))
18085
18056
  .option('--timeout <seconds>', t('cli.command.upload.vibeTimeout'))
18086
18057
  .option('--format <format>', t('cli.command.upload.format'), 'text')
18087
- .addHelpText('after', t('cli.command.upload.vibeWebGuide'))).action(async (cmd) => {
18088
- if (cmd.vibe) {
18058
+ .addHelpText('after', t('cli.command.upload.vibeWebGuide'))).action(async (cmd, command) => {
18059
+ const uploadOptions = { ...command.optsWithGlobals(), ...cmd };
18060
+ if (uploadOptions.vibe) {
18089
18061
  const { vibeUpload } = await Promise.resolve().then(function () { return index$1; });
18090
- await vibeUpload(cmd);
18062
+ await vibeUpload(uploadOptions);
18091
18063
  return;
18092
18064
  }
18093
18065
  if ([
18094
18066
  cmd.title,
18095
- cmd.minisType,
18096
18067
  cmd.description,
18097
18068
  cmd.icon,
18098
18069
  cmd.appInfo,
@@ -18668,6 +18639,50 @@ const MINIS_IMPORT_PATHS = {
18668
18639
  info: `${prefix}submit_minis_import_info_for_agent/`,
18669
18640
  package: `${prefix}upload_minis_import_package_for_agent/`,
18670
18641
  };
18642
+ const sensitiveKey = /authorization|cookie|password|secret|token|email/i;
18643
+ function safeTraceUrl(value) {
18644
+ try {
18645
+ const url = new URL(value);
18646
+ for (const key of url.searchParams.keys()) {
18647
+ if (sensitiveKey.test(key))
18648
+ url.searchParams.set(key, '[REDACTED]');
18649
+ }
18650
+ return url.toString();
18651
+ }
18652
+ catch {
18653
+ return value;
18654
+ }
18655
+ }
18656
+ function safeTraceError(value) {
18657
+ return String(value ?? '')
18658
+ .replace(/([?&](?:import_token|access_token|token)=)[^&#\s]*/gi, '$1[REDACTED]')
18659
+ .replace(/((?:import_token|access_token|token)[=:]\s*)[^,;\s]*/gi, '$1[REDACTED]');
18660
+ }
18661
+ function safeTraceValue(value, key = '', seen = new WeakSet()) {
18662
+ if (sensitiveKey.test(key))
18663
+ return '[REDACTED]';
18664
+ if (Buffer.isBuffer(value))
18665
+ return { type: 'Buffer', sizeBytes: value.length };
18666
+ if (typeof value === 'string')
18667
+ return /url$/i.test(key) || /^https?:\/\//i.test(value) ? safeTraceUrl(value) : value;
18668
+ if (!value || typeof value !== 'object')
18669
+ return value;
18670
+ if (seen.has(value))
18671
+ return '[Circular]';
18672
+ seen.add(value);
18673
+ if (Array.isArray(value))
18674
+ return value.map(item => safeTraceValue(item, '', seen));
18675
+ return Object.fromEntries(Object.entries(value).map(([childKey, childValue]) => [
18676
+ childKey,
18677
+ safeTraceValue(childValue, childKey, seen),
18678
+ ]));
18679
+ }
18680
+ function emitTrace(sink, trace) {
18681
+ try {
18682
+ sink?.(trace);
18683
+ }
18684
+ catch { /* Diagnostics must never affect uploads. */ }
18685
+ }
18671
18686
  function safeWebUrl(value) {
18672
18687
  if (typeof value !== 'string')
18673
18688
  throw new VibeError('VIBE_INVALID_RESPONSE');
@@ -18693,10 +18708,7 @@ function taskId(value) {
18693
18708
  return value;
18694
18709
  }
18695
18710
  function status(value) {
18696
- // IDL status 9 waits for code review submission; preview may already be ready.
18697
- // Keep polling it just like other non-failure states and use preview_url to
18698
- // determine preview readiness, rather than treating review as a prerequisite.
18699
- if (!Number.isInteger(value) || Number(value) < 1 || Number(value) > 9)
18711
+ if (!Number.isInteger(value) || Number(value) < 1 || Number(value) > 8)
18700
18712
  throw new VibeError('VIBE_INVALID_RESPONSE');
18701
18713
  return Number(value);
18702
18714
  }
@@ -18758,7 +18770,6 @@ function createBackend(mode, transport) {
18758
18770
  signal,
18759
18771
  });
18760
18772
  if (!nonempty(data.import_token) ||
18761
- !nonempty(data.landing_url) ||
18762
18773
  !Number.isFinite(data.poll_interval_seconds) ||
18763
18774
  data.poll_interval_seconds <= 0 ||
18764
18775
  data.poll_interval_seconds > 3600)
@@ -18766,7 +18777,7 @@ function createBackend(mode, transport) {
18766
18777
  return {
18767
18778
  taskId: taskId(data.task_id),
18768
18779
  importToken: data.import_token,
18769
- landingUrl: data.landing_url,
18780
+ landingUrl: safeWebUrl(data.landing_url),
18770
18781
  pollIntervalSeconds: data.poll_interval_seconds,
18771
18782
  };
18772
18783
  },
@@ -18808,7 +18819,7 @@ function createBackend(mode, transport) {
18808
18819
  path: MINIS_IMPORT_PATHS.info,
18809
18820
  data: {
18810
18821
  ...credentials(task),
18811
- app_info: { ...Object.fromEntries(Object.entries(appInfo || VIBE_PPE_DEFAULT_APP_INFO).filter(([, value]) => value !== '')), app_name: title, icon_uri: iconUri },
18822
+ app_info: { ...(appInfo || VIBE_PPE_DEFAULT_APP_INFO), app_name: title, icon_uri: iconUri },
18812
18823
  },
18813
18824
  signal,
18814
18825
  });
@@ -18833,7 +18844,7 @@ function createBackend(mode, transport) {
18833
18844
  },
18834
18845
  };
18835
18846
  }
18836
- function createLocalHttpBackend(baseUrl) {
18847
+ function createLocalHttpBackend(baseUrl, adapter, trace) {
18837
18848
  let base;
18838
18849
  try {
18839
18850
  base = new URL(safeWebUrl(baseUrl));
@@ -18846,16 +18857,16 @@ function createLocalHttpBackend(baseUrl) {
18846
18857
  base.search ||
18847
18858
  base.hash)
18848
18859
  throw new VibeError('VIBE_INVALID_LOCAL_ENDPOINT');
18849
- return createHttpBackend('local-http', base.origin);
18860
+ return createHttpBackend('local-http', base.origin, {}, adapter, trace);
18850
18861
  }
18851
- function createPpeBackend(lane, adapter) {
18862
+ function createPpeBackend(lane, adapter, trace) {
18852
18863
  // No default lane and no production fallback. Keep PPE requests on the known host.
18853
18864
  if (!lane || !/^ppe_[a-zA-Z0-9_-]{1,64}$/.test(lane))
18854
18865
  throw new VibeError('VIBE_INVALID_PPE_ENV');
18855
18866
  const backend = createHttpBackend('ppe', 'https://developers.tiktok.com', {
18856
18867
  'x-use-ppe': '1',
18857
18868
  'x-tt-env': lane,
18858
- }, adapter);
18869
+ }, adapter, trace);
18859
18870
  return {
18860
18871
  ...backend,
18861
18872
  uploadAvailable: true,
@@ -18864,11 +18875,11 @@ function createPpeBackend(lane, adapter) {
18864
18875
  /** Opt-in integration demo: only creation and polling can reach PPE. */
18865
18876
  // Release-owned configuration, not a developer setup step. Remove PPE routing
18866
18877
  // here when the production chain is ready; do not change the Skill command.
18867
- function createVibeTrialBackend(scenario, adapter) {
18868
- return createPpeMockBackend('ppe_op_vc', adapter, scenario);
18878
+ function createVibeTrialBackend(scenario, adapter, trace) {
18879
+ return createPpeMockBackend('ppe_op_vc', adapter, scenario, trace);
18869
18880
  }
18870
- function createPpeMockBackend(lane, adapter, scenario = 'success') {
18871
- const real = createPpeBackend(lane, adapter);
18881
+ function createPpeMockBackend(lane, adapter, scenario = 'success', trace) {
18882
+ const real = createPpeBackend(lane, adapter, trace);
18872
18883
  const mock = createMockBackend(scenario);
18873
18884
  let realTask;
18874
18885
  let mockTask;
@@ -18914,7 +18925,7 @@ function createPpeMockBackend(lane, adapter, scenario = 'success') {
18914
18925
  uploadPackage: async (task, input, signal, onProgress) => mock.uploadPackage(uploadTask(task), input, signal, onProgress),
18915
18926
  };
18916
18927
  }
18917
- function createHttpBackend(mode, baseURL, environmentHeaders = {}, adapter) {
18928
+ function createHttpBackend(mode, baseURL, environmentHeaders = {}, adapter, trace) {
18918
18929
  // Own the agents so an early multipart response cannot leave upload sockets
18919
18930
  // keeping the CLI alive after its terminal result. Never destroy global agents.
18920
18931
  const httpAgent = new http.Agent();
@@ -18928,7 +18939,11 @@ function createHttpBackend(mode, baseURL, environmentHeaders = {}, adapter) {
18928
18939
  httpAgent,
18929
18940
  httpsAgent,
18930
18941
  });
18942
+ let requestSequence = 0;
18931
18943
  const backend = createBackend(mode, async (request) => {
18944
+ const requestId = ++requestSequence;
18945
+ const startedAt = Date.now();
18946
+ const url = new URL(request.path, baseURL).toString();
18932
18947
  try {
18933
18948
  let body;
18934
18949
  let headers = {};
@@ -18950,18 +18965,42 @@ function createHttpBackend(mode, baseURL, environmentHeaders = {}, adapter) {
18950
18965
  body = JSON.stringify(request.data).replace(/"task_id":"([1-9]\d*)"/, '"task_id":$1');
18951
18966
  headers = { 'Content-Type': 'application/json' };
18952
18967
  }
18953
- return (await client.request({
18968
+ const requestHeaders = { ...environmentHeaders, ...headers };
18969
+ emitTrace(trace, {
18970
+ event: 'http_request', requestId, method: request.method, url,
18971
+ headers: safeTraceValue(requestHeaders),
18972
+ request: safeTraceValue(request.data),
18973
+ });
18974
+ const response = await client.request({
18954
18975
  method: request.method,
18955
18976
  url: request.path,
18956
18977
  params: request.method === 'GET' ? request.data : undefined,
18957
18978
  data: body,
18958
- headers: { ...environmentHeaders, ...headers },
18979
+ headers: requestHeaders,
18959
18980
  signal: request.signal,
18960
18981
  maxBodyLength: Infinity,
18961
18982
  onUploadProgress: p => request.onProgress?.(p.loaded, p.total),
18962
- })).data;
18983
+ });
18984
+ emitTrace(trace, {
18985
+ event: 'http_response', requestId, method: request.method, url,
18986
+ statusCode: response.status,
18987
+ headers: safeTraceValue(response.headers),
18988
+ response: safeTraceValue(response.data),
18989
+ elapsedMs: Date.now() - startedAt,
18990
+ });
18991
+ return response.data;
18963
18992
  }
18964
- catch {
18993
+ catch (error) {
18994
+ emitTrace(trace, {
18995
+ event: 'http_error', requestId, method: request.method, url,
18996
+ statusCode: error?.response?.status,
18997
+ headers: safeTraceValue(error?.response?.headers),
18998
+ response: safeTraceValue(error?.response?.data),
18999
+ elapsedMs: Date.now() - startedAt,
19000
+ error: request.signal.aborted
19001
+ ? 'Request cancelled'
19002
+ : safeTraceError(error?.message || error),
19003
+ });
18965
19004
  throw new VibeError(request.signal.aborted
18966
19005
  ? 'VIBE_CANCELLED'
18967
19006
  : 'VIBE_BACKEND_REQUEST_FAILED');
@@ -19000,7 +19039,7 @@ function createMockBackend(scenario = 'success') {
19000
19039
  if (path === MINIS_IMPORT_PATHS.create) {
19001
19040
  if (created ||
19002
19041
  data.source_type !== 1 ||
19003
- !['MINI_GAME', 'MINI_TOOL'].includes(data.minis_type) ||
19042
+ data.minis_type !== 'MINI_GAME' ||
19004
19043
  !nonempty(data.source_project_id) ||
19005
19044
  !nonempty(data.task_idempotency_id))
19006
19045
  throw new VibeError('VIBE_INVALID_RESPONSE');
@@ -19069,7 +19108,7 @@ function createMockBackend(scenario = 'success') {
19069
19108
  function createAuthorizationScheme(landingUrl) {
19070
19109
  // QR and browser authorization use the same backend-provided landing URL.
19071
19110
  // Preserve its query exactly; do not wrap it in an app scheme or re-encode it.
19072
- return landingUrl;
19111
+ return safeWebUrl(landingUrl);
19073
19112
  }
19074
19113
 
19075
19114
  const DEV_PORT = 3000;
@@ -19304,19 +19343,9 @@ function readFile(file, limit, code) {
19304
19343
  }
19305
19344
  async function prepareVibeInput(options, onPackaging, signal, deps = { check: executeCheck, zip: zipCwdToBuffer }) {
19306
19345
  const title = options.title?.trim();
19307
- const minisType = resolveMinisType(options.minisType);
19308
19346
  if (!title)
19309
19347
  throw new VibeError('VIBE_TITLE_REQUIRED');
19310
- if (minisType === 'MINI_TOOL' && (options.appInfo || options.category || options.subcategory))
19311
- throw new VibeError('VIBE_TOOL_TRIAL_METADATA');
19312
- const appInfo = minisType === 'MINI_TOOL'
19313
- ? applyVibeDescription(options.description, {
19314
- description: 'A simple productivity tool for everyday tasks.',
19315
- category: 'EFFICIENCY_AND_OFFICE', subcategory: '',
19316
- terms_of_service: 'https://www.tiktok.com', privacy_policy: 'https://www.tiktok.com',
19317
- copyright_confirmation: true, region: 'US', min_sdk_version: '1.0.0',
19318
- })
19319
- : applyVibeCategory(options, applyVibeDescription(options.description, readVibeAppInfo(options.appInfo)));
19348
+ const appInfo = applyVibeCategory(options, applyVibeDescription(options.description, readVibeAppInfo(options.appInfo)));
19320
19349
  if (options.package && options.dir)
19321
19350
  throw new VibeError('VIBE_INPUT_CONFLICT');
19322
19351
  const image = readFile(options.icon, VIBE_ICON_LIMIT, 'VIBE_INVALID_ICON');
@@ -19350,7 +19379,6 @@ async function prepareVibeInput(options, onPackaging, signal, deps = { check: ex
19350
19379
  if (signal.aborted)
19351
19380
  throw new VibeError('VIBE_CANCELLED');
19352
19381
  const input = {
19353
- minisType,
19354
19382
  title,
19355
19383
  ...(appInfo ? { appInfo } : {}),
19356
19384
  icon: {
@@ -19370,294 +19398,33 @@ const vibePageStyles = `
19370
19398
  :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}
19371
19399
  *{box-sizing:border-box}
19372
19400
  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}
19373
- .shell{position:relative;width:480px;max-width:100%}
19374
- .steps{display:flex;align-items:center;justify-content:center;gap:10px;margin:24px 0 20px;padding:0;list-style:none}
19401
+ .shell{position:relative;width:100%;max-width:400px}
19402
+ .steps{display:flex;align-items:center;justify-content:center;gap:10px;margin:0 0 24px;padding:0;list-style:none}
19375
19403
  .step{display:flex;align-items:center;gap:6px;font-size:11px;color:#9298a2;white-space:nowrap}
19376
19404
  .step:after{content:"";height:1px;width:25px;margin-left:4px;background:#e9ebef}.step:last-child:after{display:none}
19377
19405
  .step-number{height:18px;min-width:18px;border-radius:50%;border:1px solid #e2e5e9;display:grid;place-items:center;font-size:10px}
19378
- .step.active{color:#272c34;font-weight:600}.step.active .step-number{background:none;color:#272c34;border-color:transparent}
19379
- .step.done{color:#7a818c}.step.done .step-number{background:#a3e635;color:#24282f;border-color:#a3e635}
19380
- .swirl{display:block;width:18px;height:18px;border-radius:50%;background:conic-gradient(transparent 0 .1turn,currentColor 1turn);-webkit-mask:radial-gradient(farthest-side,#0000 calc(100% - 2.5px),#000 0);mask:radial-gradient(farthest-side,#0000 calc(100% - 2.5px),#000 0);animation:swirl-spin .9s linear infinite}
19381
- @keyframes swirl-spin{to{transform:rotate(1turn)}}
19382
- .heading{text-align:center}h1{font-size:20px;font-weight:600;line-height:1.6;letter-spacing:-.3px;margin:0 0 12px}
19383
- .subtitle{color:#7a818c;font-size:14px;line-height:1.6;margin:0;min-height:22px;overflow-wrap:anywhere}
19384
- .preview-loading{width:min(240px,100%);height:4px;margin:18px auto 0;border-radius:4px;overflow:hidden;background:#eceef1}
19385
- .preview-loading span{display:block;width:40%;height:100%;border-radius:inherit;background:#858c96;animation:preview-loading-slide 1.8s ease-in-out infinite}
19386
- @keyframes preview-loading-slide{from{transform:translateX(-100%)}to{transform:translateX(250%)}}
19387
- @media(prefers-reduced-motion:reduce){.preview-loading span{animation:none;transform:translateX(75%)}}
19388
- .game{display:grid;grid-template-columns:44px minmax(0,1fr);column-gap:14px;align-items:start;margin:24px 22px 0;padding:16px 18px;border-radius:16px;background:#f7f8fa;text-align:left}
19389
- .game-summary{display:contents}.game-title{grid-column:2;max-width:100%;margin:0;font-size:15px;font-weight:600;overflow-wrap:anywhere;min-width:0}
19406
+ .step.active{color:#272c34;font-weight:600}.step.active .step-number{background:#272c34;color:#fff;border-color:#272c34}
19407
+ .step.done{color:#537565}.step.done .step-number{background:#edf6f0;color:#287c54;border-color:#edf6f0}
19408
+ .heading{text-align:center}h1{font-size:26px;font-weight:650;line-height:1.35;letter-spacing:-.6px;margin:0 0 10px}
19409
+ .subtitle{color:#7a818c;font-size:12px;line-height:1.7;margin:0;overflow-wrap:anywhere}
19410
+ .game{margin:24px 0 0;padding-top:20px;border-top:1px solid #eceef1;text-align:center}
19411
+ .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}
19390
19412
  .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}
19391
- .game-icon,.game-avatar{grid-column:1;grid-row:1 / 3;align-self:center}
19392
- .game-details{grid-column:2;margin:4px 0 0;font-size:12px;color:#7a818c}.game-details>div:first-child,.game-categories>div{display:flex;align-items:baseline;gap:6px}.game-details dt{flex-shrink:0;font-size:11px;color:#9298a2;margin:0}.game-details dd{min-width:0;margin:0;text-align:left;overflow-wrap:anywhere}
19393
- .game-description{white-space:pre-wrap;line-height:1.65}.game-categories{display:flex;flex-wrap:wrap;gap:6px 16px;margin-top:6px}
19394
- /* 32px preserves at least four quiet modules even for a 21-module QR at 160px. */
19395
- .scan-action{text-align:center;font-size:13px;font-weight:500;margin:26px 0 10px}.qr-area{display:flex;justify-content:center}.qr-frame{padding:32px;width:224px;height:224px;background:#fff}.qr-frame img{display:block;width:160px;height:160px;max-width:100%}
19396
- .game+.qr-area{margin-top:20px}.scan-hint{text-align:center;font-size:11px;color:#7a818c;line-height:1.7;margin:8px 0 0;min-height:19px}
19397
- .qr-area{flex-wrap:wrap;gap:20px;min-height:288px;align-content:flex-start}.qr-code{margin:0;width:224px;text-align:center}.qr-code figcaption{font-size:13px;font-weight:600;margin-top:4px;min-height:60px}.qr-code figcaption span{display:block;font-size:11px;font-weight:400;color:#7a818c;margin-top:4px;min-height:35px}
19398
- .snake{display:grid;grid-template-columns:repeat(var(--cells),var(--module));color:#24282f;width:160px;height:160px}
19399
- .snake i{width:var(--module);height:var(--module)}.snake i.lit,.snake i.on{background:currentColor}.snake i.fresh{animation:snake-settle .5s ease-out}.snake i.apple{background:#a3e635;animation:snake-apple 1.2s ease-in-out infinite}
19400
- @keyframes snake-settle{from{background:#a3e635}to{background:currentColor}}@keyframes snake-apple{50%{opacity:.4}}
19401
- @media(prefers-reduced-motion:reduce){.swirl,.snake i.fresh,.snake i.apple{animation:none}}
19413
+ .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}
19414
+ .game-description{white-space:pre-wrap;line-height:1.65}.game-categories{display:grid;grid-template-columns:1fr;gap:16px;margin-top:16px}
19415
+ .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%}
19402
19416
  .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}
19403
19417
  .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}
19404
19418
  .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}
19405
- @media(max-width:380px){body{padding-left:20px;padding-right:20px}.steps{gap:7px}.step:after{width:16px;margin-left:1px}}
19406
- @media(max-width:520px){.game{margin-left:0;margin-right:0}.scan-hint{min-height:38px}}
19419
+ @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}}
19407
19420
  `;
19408
19421
 
19409
- // Adapted from tiktok-minis-skills publish_tool.js at 132cd91.
19410
- // Decorative waiting animation only; real preview QRs always use previewUrl.
19411
- const qrPreviewLoader = `(() => {
19412
- const neighbours = (p, w) => {
19413
- const row = (p / w) | 0, col = p % w, out = [];
19414
- if (row > 0) out.push(p - w);
19415
- if (row < w - 1) out.push(p + w);
19416
- if (col > 0) out.push(p - 1);
19417
- if (col < w - 1) out.push(p + 1);
19418
- return out;
19419
- };
19420
-
19421
- /** First step of a shortest path from 'from' to 'to', body cells excluded. */
19422
- function stepToward(from, to, blocked, w) {
19423
- if (from === to) return from;
19424
- const prev = new Map([[from, from]]);
19425
- let front = [from];
19426
- while (front.length) {
19427
- const next = [];
19428
- for (const cell of front)
19429
- for (const nb of neighbours(cell, w)) {
19430
- if (prev.has(nb) || (blocked.has(nb) && nb !== to)) continue;
19431
- prev.set(nb, cell);
19432
- if (nb === to) {
19433
- let node = nb;
19434
- while (prev.get(node) !== from) node = prev.get(node);
19435
- return node;
19436
- }
19437
- next.push(nb);
19438
- }
19439
- front = next;
19440
- }
19441
- return -1;
19442
- }
19443
-
19444
- /** Nearest still-unlit module, by plain grid distance from the head. */
19445
- function nearestTarget(head, pending, w) {
19446
- const hr = (head / w) | 0, hc = head % w;
19447
- let best = -1, bestD = Infinity;
19448
- for (const cell of pending) {
19449
- const d = Math.abs(((cell / w) | 0) - hr) + Math.abs((cell % w) - hc);
19450
- if (d < bestD) { bestD = d; best = cell; }
19451
- }
19452
- return best;
19453
- }
19454
-
19455
- function play(matrix, w, maxLength) {
19456
- const pending = new Set();
19457
- for (let i = 0; i < w * w; i++) if (matrix[i]) pending.add(i);
19458
-
19459
- // The snake starts on a dark module, and only moving lights one, so the
19460
- // starting square has to be lit here or the finished code is a module
19461
- // short -- a hole the error correction hides rather than a code that is
19462
- // actually right.
19463
- const start = [...pending][0];
19464
- pending.delete(start);
19465
- const snake = [start];
19466
- const body = new Set(snake);
19467
- const lit = new Set([start]);
19468
- const frames = [];
19469
- let budget = w * w * 6;
19470
-
19471
- while (pending.size && --budget > 0) {
19472
- const head = snake[snake.length - 1];
19473
- const target = nearestTarget(head, pending, w);
19474
- if (target < 0) break;
19475
- const move = stepToward(head, target, body, w);
19476
- if (move < 0) break;
19477
-
19478
- snake.push(move);
19479
- body.add(move);
19480
- while (snake.length > maxLength) body.delete(snake.shift());
19481
- if (matrix[move]) { lit.add(move); pending.delete(move); }
19482
-
19483
- frames.push({ snake: [...snake], lit: [...lit], apple: target });
19484
- }
19485
- return frames;
19486
- }
19487
-
19488
- function mount(host) {
19489
- if (host.dataset.mounted) return;
19490
- host.dataset.mounted = '1';
19491
- const matrix = JSON.parse(host.dataset.matrix);
19492
- const w = Number(host.dataset.size);
19493
- const SPEED = Number(host.dataset.speed) || 20; // ms a step
19494
- host.style.setProperty('--cells', w);
19495
- host.replaceChildren(...Array.from({ length: w * w }, () => document.createElement('i')));
19496
- const dots = Array.from(host.children);
19497
-
19498
- const frames = play(matrix, w, 14);
19499
- const last = frames.length - 1;
19500
- if (last < 1) return;
19501
- const all = frames[last].lit;
19502
-
19503
- // A lap stops short of the last modules and fades, rather than holding a
19504
- // finished code that would read as the preview having arrived.
19505
- const DRAW = Math.round(0.95 * last);
19506
- const FADE = 20;
19507
- const lap = DRAW + FADE;
19508
- // Below this a lap is too unfinished for a sprint to be worth watching.
19509
- const SPRINTABLE = 0.6;
19510
-
19511
- // One clock for the whole wait. <main> is replaced as the upload advances,
19512
- // so a board may be a later stand-in for an earlier one; it has to carry
19513
- // on from where that one stopped instead of starting the code over.
19514
- window.__snakeStart = window.__snakeStart || Date.now();
19515
-
19516
- let sprint = null;
19517
-
19518
- /**
19519
- * Only the cells that changed. Repainting all 841 every frame meant ~2500
19520
- * class writes 50 times a second to move a 14-cell snake; this touches
19521
- * about 30. The lit list only ever grows within a pass, so the new
19522
- * modules are its tail, and the body and the apple are small.
19523
- *
19524
- * The settle flag is off while the snake is only wandering: the flash
19525
- * means a module was just revealed, and nothing is revealed then.
19526
- */
19527
- const litSet = new Set();
19528
- let litCount = 0;
19529
- let prevBody = [];
19530
- let prevApple = -1;
19531
-
19532
- const cell = (k, className, opacity) => {
19533
- const dot = dots[k];
19534
- if (dot.className !== className) dot.className = className;
19535
- if (dot.style.opacity !== opacity) dot.style.opacity = opacity;
19536
- };
19537
- const restingClass = (k) => (litSet.has(k) ? 'lit' : '');
19538
-
19539
- const paint = ({ snake, lit, apple }, settle = true) => {
19540
- if (lit.length < litCount) { // a fresh pass: start from an empty board
19541
- litSet.clear(); litCount = 0; prevBody = []; prevApple = -1;
19542
- for (const dot of dots) { dot.className = ''; dot.style.opacity = ''; }
19543
- }
19544
- // Paint every new module here rather than relying on the body to
19545
- // uncover it: frames are skipped whenever the board moves faster than
19546
- // one step a tick -- the sprint skips dozens -- and a module the body
19547
- // never sat on would otherwise stay dark in a finished code.
19548
- for (let i = litCount; i < lit.length; i++) { litSet.add(lit[i]); cell(lit[i], 'lit', ''); }
19549
- litCount = lit.length;
19550
-
19551
- // The module the snake lit is under its head; it shows when the tail
19552
- // clears it, so the flash belongs to the cell the body just left.
19553
- for (const k of prevBody)
19554
- if (!snake.includes(k))
19555
- cell(k, litSet.has(k) && settle ? 'lit fresh' : restingClass(k), '');
19556
-
19557
- const length = snake.length;
19558
- snake.forEach((k, i) => cell(k, 'on', String(0.2 + 0.55 * ((i + 1) / length))));
19559
- prevBody = snake;
19560
-
19561
- if (prevApple !== apple) {
19562
- if (prevApple >= 0 && !snake.includes(prevApple))
19563
- cell(prevApple, restingClass(prevApple), '');
19564
- if (apple >= 0) cell(apple, 'apple', '');
19565
- prevApple = apple;
19566
- }
19567
- };
19568
-
19569
- const step = () => Math.floor((Date.now() - window.__snakeStart) / SPEED) % lap;
19570
- /** How far into its draw this lap is, 0 to 1. */
19571
- const lapProgress = () => Math.min(1, step() / DRAW);
19572
-
19573
- if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
19574
- paint(frames[Math.floor(DRAW / 3)], false);
19575
- host.finish = () => Promise.resolve();
19576
- return;
19577
- }
19578
-
19579
- const timer = setInterval(() => {
19580
- // The page swaps <main> as the upload advances; stop with the old board.
19581
- if (!host.isConnected) { clearInterval(timer); return; }
19582
- if (sprint) {
19583
- const k = Math.min(1, (Date.now() - sprint.at) / sprint.ms);
19584
- const p = sprint.from + (1 - sprint.from) * k;
19585
- if (p >= 1) {
19586
- paint({ snake: [], lit: all, apple: -1 });
19587
- clearInterval(timer);
19588
- sprint.done();
19589
- return;
19590
- }
19591
- paint(frames[Math.round(p * last)]);
19592
- return;
19593
- }
19594
- const at = step();
19595
- if (at > DRAW) { // fading out before the next lap
19596
- host.style.opacity = String(1 - (at - DRAW) / FADE);
19597
- return;
19598
- }
19599
- if (host.style.opacity) host.style.opacity = '';
19600
- paint(frames[at]);
19601
- }, SPEED);
19602
-
19603
- host.finish = () => {
19604
- if (sprint) return Promise.resolve();
19605
- const from = lapProgress();
19606
- if (from < SPRINTABLE) return Promise.resolve();
19607
- host.style.opacity = '';
19608
- sprint = { from, at: Date.now(), ms: 300 + 500 * (1 - from), done: () => {} };
19609
- return new Promise((resolve) => { sprint.done = resolve; });
19610
- };
19611
- }
19612
-
19613
- const boards = () => [...document.querySelectorAll('.snake')];
19614
- const start = () => boards().forEach(mount);
19615
- window.__mountSnakes = start;
19616
- window.__snakeFinish = () => Promise.all(boards().map((b) => b.finish?.()));
19617
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
19618
- else start();
19619
- })();`;
19620
-
19621
19422
  const VIBE_PAGE_WORKER = '__vibe-page-server';
19622
19423
  // Includes the base64 form of the locally validated image (up to 10 MiB).
19623
19424
  const VIBE_PAGE_HTML_LIMIT = 16 * 1024 * 1024;
19624
19425
  const pageUpdates = `(() => {
19625
19426
  let etag;
19626
19427
  const terminal = () => ['complete', 'preview', 'failed'].includes(document.querySelector('main')?.dataset.phase);
19627
- const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
19628
- async function replace(current, updated) {
19629
- const animate = current.dataset.phase !== updated.dataset.phase && current.animate &&
19630
- !window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
19631
- const height = current.getBoundingClientRect().height;
19632
- if (animate) await current.animate([{opacity: 1}, {opacity: 0}], {duration: 140, fill: 'forwards'}).finished.catch(() => {});
19633
- current.replaceWith(updated);
19634
- window.__mountSnakes?.();
19635
- if (animate) await updated.animate([
19636
- {opacity: 0, height: height + 'px', overflow: 'hidden'},
19637
- {opacity: 1, height: updated.getBoundingClientRect().height + 'px', overflow: 'hidden'}
19638
- ], {duration: 280, easing: 'ease-out'}).finished.catch(() => {});
19639
- }
19640
- async function show(updated) {
19641
- let current = document.querySelector('main');
19642
- if (!current || current.dataset.rev === updated.dataset.rev) return;
19643
- if (updated.dataset.phase === 'preview' && current.querySelector('.snake'))
19644
- await window.__snakeFinish?.();
19645
- if (updated.dataset.uploaded === 'true' && current.dataset.uploaded !== 'true') {
19646
- // Show the upload receipt before either the binding or ready preview QR,
19647
- // even when polling skips intermediate upload states.
19648
- const success = updated.cloneNode(true);
19649
- success.dataset.phase = 'upload_success';
19650
- delete success.dataset.uploaded;
19651
- const heading = success.querySelector('.heading');
19652
- success.querySelector('h1').textContent = heading.dataset.uploadTitle;
19653
- success.querySelector('.subtitle').textContent = heading.dataset.uploadSubtitle;
19654
- success.querySelectorAll('.qr-area, .scan-action, .scan-hint, .preview-loading').forEach(node => node.remove());
19655
- await replace(current, success);
19656
- current = success;
19657
- await delay(1200);
19658
- }
19659
- await replace(current, updated);
19660
- }
19661
19428
  async function refresh() {
19662
19429
  if (terminal()) return;
19663
19430
  try {
@@ -19665,10 +19432,9 @@ const pageUpdates = `(() => {
19665
19432
  if (response.ok) {
19666
19433
  etag = response.headers.get('etag');
19667
19434
  const next = new DOMParser().parseFromString(await response.text(), 'text/html');
19668
- const updated = next.querySelector('main');
19669
- if (updated) await show(updated);
19435
+ const current = document.querySelector('main'), updated = next.querySelector('main');
19436
+ if (current && updated && current.outerHTML !== updated.outerHTML) current.replaceWith(updated);
19670
19437
  document.title = next.title;
19671
- document.documentElement.lang = next.documentElement.lang;
19672
19438
  }
19673
19439
  } catch { /* Keep the current page readable during a temporary connection failure. */ }
19674
19440
  if (!terminal()) setTimeout(refresh, 2000);
@@ -19678,20 +19444,12 @@ const pageUpdates = `(() => {
19678
19444
  const escape = (s) => s.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
19679
19445
  async function renderVibePage(input) {
19680
19446
  const zh = input.language !== 'en-US';
19681
- const message = input.localizedMessages?.[zh ? 'zh-CN' : 'en-US'] ?? input.message;
19682
- const tool = input.minisType === 'MINI_TOOL';
19683
19447
  const phase = input.phase ||
19684
19448
  (input.purpose === 'authorization_binding' ? 'binding' : 'uploading');
19685
- const uploadedBinding = !input.simulation && input.purpose === 'authorization_binding' &&
19686
- (phase === 'waiting_preview' || phase === 'preview' || phase === 'complete');
19687
- const preview = !input.simulation && phase === 'preview' &&
19688
- (input.purpose === 'game_preview' || input.purpose === 'tool_preview');
19689
- const uploaded = uploadedBinding || preview;
19690
19449
  const copy = zh
19691
19450
  ? {
19692
- steps: ['扫码授权', '上传游戏', '预览'],
19451
+ steps: ['扫码授权', '上传游戏', '完成'],
19693
19452
  mock: input.hybrid ? 'PPE + Mock' : '模拟',
19694
- game: '当前游戏',
19695
19453
  gameIcon: '游戏头像',
19696
19454
  bindingTitle: '上传到 TikTok',
19697
19455
  bindingAction: input.hybrid ? '真实授权二维码 · 混合联调' : '使用 TikTok 扫描二维码',
@@ -19704,7 +19462,6 @@ async function renderVibePage(input) {
19704
19462
  uploadingTitle: '正在上传小游戏',
19705
19463
  uploadingSubtitle: input.simulation ? '正在模拟授权后的上传流程,不会上传游戏内容。' : '授权已完成,正在上传本次游戏内容。',
19706
19464
  uploadLabel: input.simulation ? '模拟上传进度' : '上传进度',
19707
- uploadStatus: '正在上传游戏,请稍候',
19708
19465
  completeTitle: input.simulation ? '模拟上传完成' : '上传完成',
19709
19466
  completeSubtitle: input.simulation
19710
19467
  ? '绑定和上传流程模拟完成,未向 TikTok 上传游戏。'
@@ -19713,11 +19470,11 @@ async function renderVibePage(input) {
19713
19470
  bindingCode: '授权绑定二维码',
19714
19471
  accepted: input.simulation ? '模拟完成' : '已上传',
19715
19472
  mockNote: input.hybrid ? '真实创建 / 查询任务 · 授权 / 上传为模拟 · 不改变真实任务状态' : '模拟运行 · 无需扫码,不会真实上传',
19473
+ footer: '临时页面 · 15 分钟后关闭',
19716
19474
  }
19717
19475
  : {
19718
- steps: ['Scan', 'Upload', 'Preview'],
19476
+ steps: ['Authorize', 'Upload', 'Done'],
19719
19477
  mock: input.hybrid ? 'PPE + Mock' : 'MOCK',
19720
- game: 'YOUR GAME',
19721
19478
  gameIcon: 'Game icon',
19722
19479
  bindingTitle: 'Upload to TikTok',
19723
19480
  bindingAction: input.hybrid ? 'Real authorization QR · integration demo' : 'Scan the QR code with TikTok',
@@ -19734,7 +19491,6 @@ async function renderVibePage(input) {
19734
19491
  uploadLabel: input.simulation
19735
19492
  ? 'Simulated upload progress'
19736
19493
  : 'Upload progress',
19737
- uploadStatus: 'Uploading your game. Please wait.',
19738
19494
  completeTitle: input.simulation ? 'Simulated upload complete' : 'Upload complete',
19739
19495
  completeSubtitle: input.simulation
19740
19496
  ? 'Binding and upload simulation complete. Nothing was uploaded to TikTok.'
@@ -19743,17 +19499,8 @@ async function renderVibePage(input) {
19743
19499
  bindingCode: 'Authorization QR',
19744
19500
  accepted: input.simulation ? 'Simulation complete' : 'Uploaded',
19745
19501
  mockNote: input.hybrid ? 'Real task creation / polling · Mock authorization / upload · Real task status unchanged' : 'Mock only · no scan needed, nothing is uploaded',
19502
+ footer: 'Temporary page · closes after 15 minutes',
19746
19503
  };
19747
- if (tool) {
19748
- for (const key of Object.keys(copy)) {
19749
- const value = copy[key];
19750
- if (typeof value === 'string')
19751
- copy[key] = value.replace(/小游戏|游戏/g, '工具').replace(/game/gi, 'tool');
19752
- }
19753
- copy.steps[1] = zh ? '上传工具' : 'Upload';
19754
- }
19755
- if (input.simulation)
19756
- copy.steps[2] = zh ? '完成' : 'Done';
19757
19504
  if (input.web) {
19758
19505
  copy.steps[0] = zh ? '网页授权' : 'Web authorization';
19759
19506
  copy.bindingAction = zh ? '在浏览器完成授权绑定' : 'Authorize in your browser';
@@ -19762,106 +19509,70 @@ async function renderVibePage(input) {
19762
19509
  : (zh ? '请在已打开的 TikTok 页面确认授权,当前命令会继续查询绑定状态。' : 'Confirm authorization on the opened TikTok page. The command continues polling.');
19763
19510
  copy.bindingStatus = input.simulation ? (zh ? '模拟等待授权' : 'Simulating authorization') : (zh ? '等待网页授权' : 'Waiting for browser authorization');
19764
19511
  }
19765
- const step = phase === 'binding' ? 0 : phase === 'uploading' ? 1 : phase === 'preview' ? 3 : 2;
19512
+ const step = phase === 'binding' ? 0 : phase === 'uploading' ? 1 : 2;
19766
19513
  // Only embed the chosen raster image; never fetch a URL or expose its local path.
19767
19514
  const icon = input.iconDataUrl &&
19768
19515
  input.iconDataUrl.length <= 14 * 1024 * 1024 &&
19769
19516
  /^data:image\/(?:png|jpeg|webp);base64,[A-Za-z0-9+/]+=*$/.test(input.iconDataUrl)
19770
19517
  ? `<img class="game-avatar" width="44" height="44" alt="${copy.gameIcon}" src="${input.iconDataUrl}">`
19771
19518
  : '<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>';
19772
- const game = `<div class="game"><div class="game-summary">${icon}<p class="game-title">${escape(input.title)}</p></div><dl class="game-details"><div><dt>${zh ? '描述' : 'Description'}</dt><dd class="game-description">${escape(input.description ?? VIBE_PPE_DEFAULT_APP_INFO.description)}</dd></div><div class="game-categories"><div><dt>${zh ? '类型' : 'Category'}</dt><dd>${escape(tool && input.category === 'EFFICIENCY_AND_OFFICE' ? (zh ? '效率办公' : 'Efficiency & Office') : input.category ?? VIBE_PPE_DEFAULT_APP_INFO.category)}</dd></div>${tool ? '' : `<div><dt>${zh ? '子类型' : 'Subcategory'}</dt><dd>${escape(input.subcategory ?? VIBE_PPE_DEFAULT_APP_INFO.subcategory)}</dd></div>`}</div></dl></div>`;
19519
+ 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>`;
19773
19520
  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>';
19774
- const uploadSuccessTitle = zh ? '上传成功' : 'Upload complete';
19775
- const uploadSuccessSubtitle = zh
19776
- ? `${tool ? '工具' : '游戏'}内容已上传,正在返回扫码页面。`
19777
- : `Your ${tool ? 'tool' : 'game'} has been uploaded. Returning to the scan page.`;
19778
- const title = uploadedBinding ? (phase === 'waiting_preview' ? (zh ? '正在生成预览' : 'Preparing preview') : zh ? '扫码查看进度' : 'Scan to check progress')
19779
- : phase === 'waiting_preview' ? (zh ? '正在准备预览' : 'Preparing preview')
19780
- : phase === 'preview' ? (zh ? `扫码预览${tool ? '工具' : '游戏'}` : 'Scan to preview')
19521
+ const title = phase === 'waiting_preview' ? (zh ? '正在准备预览' : 'Preparing preview')
19522
+ : phase === 'preview' ? (zh ? '扫码预览游戏' : 'Scan to preview')
19523
+ : phase === 'binding'
19524
+ ? copy.bindingTitle
19525
+ : phase === 'uploading'
19526
+ ? copy.uploadingTitle
19527
+ : phase === 'failed'
19528
+ ? copy.failedTitle
19529
+ : copy.completeTitle;
19530
+ const subtitle = phase === 'waiting_preview' ? (zh ? '游戏内容已上传,预览就绪后会自动显示二维码。' : 'Game content uploaded. The preview QR will appear when ready.')
19531
+ : phase === 'preview' ? (zh ? '上传完成,使用手机 TikTok 扫描下方二维码。' : 'Upload complete. Scan the QR below with TikTok on your phone.')
19532
+ : phase === 'complete' && !input.simulation ? escape(input.message)
19781
19533
  : phase === 'binding'
19782
- ? copy.bindingTitle
19534
+ ? copy.bindingSubtitle
19783
19535
  : phase === 'uploading'
19784
- ? copy.uploadingTitle
19536
+ ? copy.uploadingSubtitle
19785
19537
  : phase === 'failed'
19786
- ? copy.failedTitle
19787
- : copy.completeTitle;
19788
- const subtitle = uploadedBinding ? (phase === 'complete' ? escape(message) : phase === 'waiting_preview'
19789
- ? (zh ? '代码包已上传,正在生成预览。准备好后会自动显示预览二维码。' : 'Package uploaded. Your preview is being prepared; its QR will appear when ready.') : zh
19790
- ? `本次${tool ? '工具' : '游戏'}代码包已上传完成,再次扫码即可查看预览版本并提交审核。`
19791
- : `Your ${tool ? 'tool' : 'game'} package has been uploaded. Scan again to view the preview version and submit it for review.`)
19792
- : phase === 'waiting_preview' ? (zh ? '内容已上传,预览就绪后会自动显示二维码。' : 'Content uploaded. The preview QR will appear when ready.')
19793
- : phase === 'preview' ? ''
19794
- : phase === 'complete' && !input.simulation ? escape(message)
19795
- : phase === 'binding'
19796
- ? copy.bindingSubtitle
19797
- : phase === 'uploading'
19798
- ? copy.uploadingSubtitle
19799
- : phase === 'failed'
19800
- ? escape(message)
19801
- : copy.completeSubtitle;
19802
- const qr = (phase === 'binding' || phase === 'uploading' || phase === 'preview' || uploadedBinding) && !input.web && input.qrUrl
19538
+ ? escape(input.message)
19539
+ : copy.completeSubtitle;
19540
+ const qr = (phase === 'binding' || phase === 'preview') && !input.web && input.qrUrl
19803
19541
  ? await QRCode.toDataURL(input.qrUrl, {
19804
19542
  width: 384,
19805
- // Fixed CSS quiet zones keep different QR versions visually equal.
19806
- margin: 0,
19543
+ margin: 4,
19807
19544
  errorCorrectionLevel: 'M',
19808
19545
  })
19809
19546
  : '';
19810
- const progressCode = zh ? '查看进度二维码' : 'Progress QR';
19811
- const purpose = preview ? (zh ? `${tool ? '工具' : '游戏'}预览二维码` : `${tool ? 'Tool' : 'Game'} preview QR`) : uploadedBinding || phase === 'uploading' ? (zh ? '应用页面二维码' : 'App page QR') : copy.bindingCode;
19812
- const bindingQr = preview && !input.web && input.bindingQrUrl && input.bindingQrUrl !== input.qrUrl
19813
- ? await QRCode.toDataURL(input.bindingQrUrl, { width: 384, margin: 0, errorCorrectionLevel: 'M' }) : '';
19814
- const done = phase === 'preview' || phase === 'complete' && input.simulation;
19815
- const active = phase === 'binding' || phase === 'uploading' || phase === 'waiting_preview';
19816
- const waiting = phase === 'waiting_preview';
19817
- const matrix = waiting ? QRCode.create('https://www.tiktok.com/minis/').modules : undefined;
19818
- const pendingQr = matrix ? `<figure class="qr-code qr-pending"><div class="qr-frame" role="img" aria-label="${zh ? '预览正在生成' : 'Preview is being prepared'}"><div class="snake" aria-hidden="true" data-size="${matrix.size}" data-speed="20" data-matrix='${JSON.stringify(Array.from(matrix.data))}' style="--module:${160 / matrix.size}px"></div></div><figcaption>${zh ? '正在生成预览' : 'Preparing preview'}</figcaption></figure>` : '';
19547
+ const purpose = phase === 'preview' ? (zh ? '游戏预览二维码' : 'Game preview QR') : copy.bindingCode;
19548
+ const done = phase === 'complete' || phase === 'preview';
19819
19549
  const progress = typeof input.progressPercent === 'number' &&
19820
19550
  Number.isFinite(input.progressPercent)
19821
19551
  ? Math.max(0, Math.min(100, Math.round(input.progressPercent)))
19822
19552
  : undefined;
19823
- const progressQr = bindingQr ? `<figure class="qr-code"><div class="qr-frame"><img width="160" height="160" alt="${progressCode}" src="${bindingQr}"></div><figcaption>${zh ? '查看进度' : 'Check progress'}<span>${zh ? '查看应用信息并提交审核' : 'View app details and submit for review'}</span></figcaption></figure>` : '';
19824
- // Keep the original scan entry on the left while the right side becomes a preview.
19825
19553
  const content = qr
19826
- ? `<div class="qr-area">${progressQr}<figure class="qr-code"><div class="qr-frame"><img width="160" height="160" alt="${purpose}" src="${qr}"></div>${preview ? `<figcaption>${zh ? '扫码预览' : 'Preview'}<span>${zh ? '打开本次上传的预览版本' : 'Open the preview of this upload'}</span></figcaption>` : phase === 'binding' ? `<figcaption>${zh ? '扫码授权' : 'Authorize upload'}</figcaption>` : `<figcaption>${zh ? '查看进度' : 'Check progress'}<span>${zh ? '扫码查看应用信息' : 'Scan to view app details'}</span></figcaption>`}</figure>${pendingQr}</div>`
19554
+ ? `<div class="qr-area"><div class="qr-frame"><img width="192" height="192" alt="${purpose}" src="${qr}"></div></div>`
19827
19555
  : phase === 'uploading' && progress !== undefined
19828
19556
  ? `<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>`
19829
19557
  : '';
19830
- const previewLoading = phase === 'waiting_preview'
19831
- ? `<div class="preview-loading" role="progressbar" aria-label="${zh ? '正在生成预览' : 'Preparing preview'}"><span aria-hidden="true"></span></div>`
19832
- : '';
19833
- const revision = crypto.createHash('sha256').update(JSON.stringify(input)).digest('hex');
19834
- const steps = phase === 'failed' ? '' : `<ol class="steps" aria-label="${zh ? '上传步骤' : 'Upload steps'}">${copy.steps.map((label, i) => `<li class="step ${done || i < step ? 'done' : active && i === step ? 'active' : ''}"${!done && active && i === step ? ' aria-current="step"' : ''}><span class="step-number">${done || i < step ? checkIcon : active && i === step ? '<span class="swirl" aria-hidden="true"></span>' : i + 1}</span>${label}</li>`).join('')}</ol>`;
19835
19558
  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>
19836
- <body><main class="shell${bindingQr || pendingQr ? ' dual-qr' : ''}" data-phase="${phase}" data-rev="${revision}"${uploaded ? ' data-uploaded="true"' : ''}>${input.simulation ? `<p class="mock-badge">${copy.mock}</p>` : ''}
19837
- <section class="card" aria-labelledby="page-title">
19838
- <div class="heading"${uploaded ? ` data-upload-title="${escape(uploadSuccessTitle)}" data-upload-subtitle="${escape(uploadSuccessSubtitle)}"` : ''}><h1 id="page-title">${title}</h1><p class="subtitle" role="status">${subtitle}</p>${previewLoading}</div>${game}${steps}${phase === 'binding' && (input.web || input.hybrid) ? `<p class="scan-action">${copy.bindingAction}</p>` : ''}${content}<p class="scan-hint">${uploadedBinding ? (zh ? '可扫码查看进度信息,无需重复上传。' : 'Scan to view app details. No need to upload again.') : ''}</p>
19559
+ <body><main class="shell" data-phase="${phase}">${input.simulation ? `<p class="mock-badge">${copy.mock}</p>` : ''}
19560
+ <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>`}
19561
+ <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}
19839
19562
  </section>
19840
- ${input.simulation ? `<p class="footer"><span class="dot" aria-hidden="true"></span>${copy.mockNote}</p>` : ''}</main></body></html>`;
19841
- }
19842
- // Follow the reader's preferred browser language, as in the Tool skill.
19843
- function browserPageLanguage(header) {
19844
- const preferred = (header || '').split(',').map(part => {
19845
- const [tag, ...parameters] = part.trim().split(';');
19846
- const quality = parameters.find(value => /^\s*q=/i.test(value));
19847
- const q = quality ? Number(quality.split('=')[1]) : 1;
19848
- return { tag: tag.toLowerCase(), q };
19849
- }).filter(item => item.tag && Number.isFinite(item.q) && item.q > 0 && item.q <= 1)
19850
- .sort((a, b) => b.q - a.q)[0];
19851
- return preferred?.tag.split('-')[0] === 'zh' ? 'zh-CN' : 'en-US';
19563
+ <p class="footer">${input.simulation ? `<span class="dot" aria-hidden="true"></span>${copy.mockNote}` : copy.footer}</p></main></body></html>`;
19852
19564
  }
19853
19565
  async function createVibePageServer() {
19854
19566
  const route = `/${crypto.randomBytes(24).toString('hex')}`;
19855
19567
  let html = 'Starting…';
19856
19568
  let etag = '';
19857
- let variants = {};
19858
19569
  let landingUrl;
19859
19570
  const server = http.createServer((req, res) => {
19860
19571
  const address = server.address();
19861
19572
  const host = address && typeof address !== 'string' ? `127.0.0.1:${address.port}` : '';
19862
19573
  if (req.method === 'GET' && req.url === `${route}?updates` && req.headers.host === host) {
19863
19574
  res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' });
19864
- res.end(qrPreviewLoader + '\n' + pageUpdates);
19575
+ res.end(pageUpdates);
19865
19576
  return;
19866
19577
  }
19867
19578
  if (req.url === `${route}/authorize` && req.headers.host === host &&
@@ -19884,23 +19595,19 @@ async function createVibePageServer() {
19884
19595
  res.writeHead(404).end();
19885
19596
  return;
19886
19597
  }
19887
- const localized = variants[browserPageLanguage(req.headers['accept-language'])];
19888
- const responseHtml = localized?.html ?? html;
19889
- const responseEtag = localized?.etag ?? etag;
19890
- if (responseEtag && req.headers['if-none-match'] === responseEtag) {
19891
- res.writeHead(304, { 'ETag': responseEtag, 'Cache-Control': 'no-store', 'Vary': 'Accept-Language' }).end();
19598
+ if (etag && req.headers['if-none-match'] === etag) {
19599
+ res.writeHead(304, { 'ETag': etag, 'Cache-Control': 'no-store' }).end();
19892
19600
  return;
19893
19601
  }
19894
19602
  res.writeHead(200, {
19895
- 'ETag': responseEtag,
19896
- 'Vary': 'Accept-Language',
19603
+ 'ETag': etag,
19897
19604
  'Content-Type': 'text/html; charset=utf-8',
19898
19605
  'Cache-Control': 'no-store',
19899
19606
  'Referrer-Policy': 'no-referrer',
19900
19607
  'X-Content-Type-Options': 'nosniff',
19901
19608
  'Content-Security-Policy': "default-src 'none'; img-src data:; style-src 'unsafe-inline'; script-src 'self'; connect-src 'self'; frame-ancestors 'none'",
19902
19609
  });
19903
- res.end(responseHtml);
19610
+ res.end(html);
19904
19611
  });
19905
19612
  await new Promise((resolve, reject) => {
19906
19613
  server.once('error', reject);
@@ -19909,15 +19616,9 @@ async function createVibePageServer() {
19909
19616
  const address = server.address();
19910
19617
  return {
19911
19618
  url: `http://127.0.0.1:${address.port}${route}`,
19912
- update: (value, authorization, localizedHtml) => {
19619
+ update: (value, authorization) => {
19913
19620
  html = value;
19914
19621
  etag = '"' + crypto.createHash('sha256').update(value).digest('hex') + '"';
19915
- variants = {};
19916
- for (const language of ['zh-CN', 'en-US']) {
19917
- const localized = localizedHtml?.[language];
19918
- if (typeof localized === 'string' && localized.length < VIBE_PAGE_HTML_LIMIT)
19919
- variants[language] = { html: localized, etag: '"' + crypto.createHash('sha256').update(localized).digest('hex') + '"' };
19920
- }
19921
19622
  landingUrl = browserLandingUrl(authorization);
19922
19623
  },
19923
19624
  close: () => server.close(),
@@ -19930,34 +19631,24 @@ async function runVibePageWorker() {
19930
19631
  }
19931
19632
  const server = await createVibePageServer();
19932
19633
  const requestedTtl = Number(process.env.TTMG_VIBE_PAGE_TTL_MS);
19933
- const ttl = requestedTtl > 0 && requestedTtl <= 1800000 ? requestedTtl : 1800000;
19934
- let timer;
19935
- let closing = false;
19936
- const close = () => {
19937
- closing = true;
19938
- clearTimeout(timer);
19634
+ const ttl = requestedTtl > 0 && requestedTtl <= 900000 ? requestedTtl : 900000;
19635
+ const timer = setTimeout(() => {
19939
19636
  server.close();
19940
19637
  if (process.connected)
19941
19638
  process.disconnect();
19942
- };
19943
- // The parent owns the operation deadline. Keep the page alive during polling,
19944
- // then start its retention window when the parent detaches (or exits).
19945
- const retain = () => {
19946
- if (!closing && !timer)
19947
- timer = setTimeout(close, ttl);
19948
- };
19949
- process.once('disconnect', retain);
19950
- if (!process.connected)
19951
- retain();
19639
+ }, ttl);
19952
19640
  process.on('message', (message) => {
19953
19641
  if (message?.type === 'render' &&
19954
19642
  typeof message.html === 'string' &&
19955
19643
  message.html.length < VIBE_PAGE_HTML_LIMIT) {
19956
- server.update(message.html, message.browserLandingUrl, message.localizedHtml);
19644
+ server.update(message.html, message.browserLandingUrl);
19957
19645
  process.send?.({ type: 'rendered', id: message.id });
19958
19646
  }
19959
19647
  if (message?.type === 'close') {
19960
- close();
19648
+ clearTimeout(timer);
19649
+ server.close();
19650
+ if (process.connected)
19651
+ process.disconnect();
19961
19652
  }
19962
19653
  });
19963
19654
  process.send({ type: 'ready', url: server.url });
@@ -19995,15 +19686,11 @@ async function startVibePage() {
19995
19686
  return {
19996
19687
  url: ready.url,
19997
19688
  async update(input) {
19998
- const [zh, en] = await Promise.all([
19999
- renderVibePage({ ...input, language: 'zh-CN' }),
20000
- renderVibePage({ ...input, language: 'en-US' }),
20001
- ]);
20002
- const html = input.language === 'en-US' ? en : zh;
19689
+ const html = await renderVibePage(input);
20003
19690
  const current = ++id;
20004
19691
  const ack = receive(m => m?.type === 'rendered' && m.id === current);
20005
19692
  const landingUrl = input.phase === 'binding' ? browserLandingUrl(input.browserLandingUrl) : undefined;
20006
- child.send({ type: 'render', id: current, html, localizedHtml: { 'zh-CN': zh, 'en-US': en }, browserLandingUrl: landingUrl }, error => {
19693
+ child.send({ type: 'render', id: current, html, browserLandingUrl: landingUrl }, error => {
20007
19694
  if (error)
20008
19695
  child.emit('error', error);
20009
19696
  });
@@ -20027,7 +19714,6 @@ async function startVibePage() {
20027
19714
 
20028
19715
  // Source identity is project metadata, not a Skill input or a credential.
20029
19716
  function getVibeProjectId(options) {
20030
- const minisType = resolveMinisType(options.minisType);
20031
19717
  const root = path.resolve(options.package ? path.dirname(options.package) : options.dir || '.');
20032
19718
  const dir = path.join(root, '.ttmg');
20033
19719
  const file = path.join(dir, 'vibe-project.json');
@@ -20035,7 +19721,7 @@ function getVibeProjectId(options) {
20035
19721
  fs.mkdirSync(dir, { recursive: true });
20036
19722
  const id = crypto.randomUUID();
20037
19723
  try {
20038
- fs.writeFileSync(file, JSON.stringify({ schemaVersion: '1', sourceProjectId: id, minisType }) + '\n', { flag: 'wx', mode: 0o600 });
19724
+ fs.writeFileSync(file, JSON.stringify({ schemaVersion: '1', sourceProjectId: id }) + '\n', { flag: 'wx', mode: 0o600 });
20039
19725
  }
20040
19726
  catch (error) {
20041
19727
  if (error.code !== 'EEXIST')
@@ -20046,13 +19732,9 @@ function getVibeProjectId(options) {
20046
19732
  typeof saved.sourceProjectId !== 'string' ||
20047
19733
  !/^[a-zA-Z0-9_-]{1,100}$/.test(saved.sourceProjectId))
20048
19734
  throw new Error();
20049
- if ((saved.minisType || 'MINI_GAME') !== minisType)
20050
- throw new VibeError('VIBE_PROJECT_TYPE_CONFLICT');
20051
19735
  return saved.sourceProjectId;
20052
19736
  }
20053
- catch (error) {
20054
- if (error instanceof VibeError)
20055
- throw error;
19737
+ catch {
20056
19738
  throw new VibeError('VIBE_PROJECT_STATE_FAILED');
20057
19739
  }
20058
19740
  }
@@ -20140,7 +19822,6 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20140
19822
  let page;
20141
19823
  let backend = deps.backend;
20142
19824
  let task;
20143
- let authorizationUrl;
20144
19825
  let detached = false;
20145
19826
  let progressUpdates = Promise.resolve();
20146
19827
  let ppeLane;
@@ -20166,6 +19847,16 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20166
19847
  errorCode: null,
20167
19848
  error: null,
20168
19849
  };
19850
+ const httpTrace = options.verbose
19851
+ ? trace => console.error(JSON.stringify({
19852
+ schemaVersion: '1',
19853
+ command: 'upload',
19854
+ mode: 'vibe',
19855
+ operationId: result.operationId,
19856
+ ts: Date.now(),
19857
+ ...trace,
19858
+ }))
19859
+ : undefined;
20169
19860
  const emit = (status, fields = {}) => {
20170
19861
  result = {
20171
19862
  ...result,
@@ -20184,10 +19875,6 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20184
19875
  try {
20185
19876
  if (options.web)
20186
19877
  throw new VibeError('VIBE_WEB_DISABLED');
20187
- const minisType = resolveMinisType(options.minisType);
20188
- result.minisType = minisType;
20189
- if (minisType === 'MINI_TOOL')
20190
- result.inputRequirements = VIBE_TOOL_INPUT_REQUIREMENTS;
20191
19878
  // Real QR uploads use the release-owned PPE route.
20192
19879
  // Explicit test backends remain opt-in; real failures never fall back to them.
20193
19880
  if (!options.mock && !options.vibeApi && process.env.TTMG_VIBE_BACKEND) {
@@ -20228,20 +19915,20 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20228
19915
  throw new VibeError('VIBE_BACKEND_OPTION_CONFLICT');
20229
19916
  if (options.pollInterval !== undefined)
20230
19917
  seconds(options.pollInterval, 1);
20231
- const timeoutMs = seconds(options.timeout, 1800);
19918
+ const timeoutMs = seconds(options.timeout, 300);
20232
19919
  timeout = setTimeout(() => {
20233
19920
  timedOut = true;
20234
19921
  controller.abort();
20235
19922
  }, timeoutMs);
20236
19923
  if (!options.dryRun && !backend) {
20237
19924
  if (options.mock)
20238
- backend = deps.createTrialBackend(options.mockScenario);
19925
+ backend = deps.createTrialBackend(options.mockScenario, undefined, httpTrace);
20239
19926
  else if (hybridSelected)
20240
- backend = createPpeMockBackend(ppeLane, undefined, options.mockScenario);
19927
+ backend = createPpeMockBackend(ppeLane, undefined, options.mockScenario, httpTrace);
20241
19928
  else if (ppeSelected)
20242
- backend = createPpeBackend(ppeLane);
19929
+ backend = createPpeBackend(ppeLane, undefined, httpTrace);
20243
19930
  else if (options.vibeApi)
20244
- backend = createLocalHttpBackend(options.vibeApi);
19931
+ backend = createLocalHttpBackend(options.vibeApi, undefined, httpTrace);
20245
19932
  else
20246
19933
  throw new VibeError('VIBE_BACKEND_NOT_CONFIGURED');
20247
19934
  }
@@ -20263,7 +19950,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20263
19950
  category: (input.appInfo || VIBE_PPE_DEFAULT_APP_INFO).category,
20264
19951
  subcategory: (input.appInfo || VIBE_PPE_DEFAULT_APP_INFO).subcategory,
20265
19952
  source: options.category !== undefined ? 'arguments' : options.appInfo !== undefined ? 'app-info' : 'defaults',
20266
- catalogVersion: minisType === 'MINI_TOOL' ? 'mini-tool-ppe-trial-2026-09-19' : GAME_CATEGORY_CATALOG_VERSION,
19953
+ catalogVersion: GAME_CATEGORY_CATALOG_VERSION,
20267
19954
  },
20268
19955
  iconSizeBytes: input.icon.bytes.length,
20269
19956
  packageSizeBytes: input.code.bytes.length,
@@ -20275,7 +19962,6 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20275
19962
  return result;
20276
19963
  const submittedInfo = input.appInfo || VIBE_PPE_DEFAULT_APP_INFO;
20277
19964
  pageMetadata = {
20278
- minisType,
20279
19965
  iconDataUrl: `data:${input.icon.contentType};base64,${input.icon.bytes.toString('base64')}`,
20280
19966
  description: submittedInfo.description,
20281
19967
  category: submittedInfo.category,
@@ -20288,15 +19974,18 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20288
19974
  hybrid: backend.mode === 'ppe-mock',
20289
19975
  };
20290
19976
  const sourceProjectId = deps.projectId(options);
19977
+ // The project ID identifies the game; the task request ID is new for each upload.
19978
+ // Keep it separate from the operation ID used in progress events.
19979
+ const taskIdempotencyId = crypto.randomUUID();
20291
19980
  emit('creating_task', { platformWrite: backend.mode === 'ppe' || backend.mode === 'ppe-mock' });
20292
19981
  task = await backend.createTask({
20293
19982
  source_type: 1,
20294
19983
  source_project_id: sourceProjectId,
20295
- task_idempotency_id: result.operationId,
20296
- minis_type: minisType,
19984
+ task_idempotency_id: taskIdempotencyId,
19985
+ minis_type: 'MINI_GAME',
20297
19986
  }, controller.signal);
20298
19987
  ensureActive();
20299
- authorizationUrl = createAuthorizationScheme(task.landingUrl);
19988
+ const authorizationUrl = createAuthorizationScheme(task.landingUrl);
20300
19989
  const pollMs = seconds(options.pollInterval, task.pollIntervalSeconds);
20301
19990
  result = {
20302
19991
  ...result,
@@ -20375,8 +20064,6 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20375
20064
  ...pageGame,
20376
20065
  language: getCurrentLanguage(),
20377
20066
  phase: 'uploading',
20378
- qrUrl: result.simulation ? undefined : authorizationUrl,
20379
- purpose: result.simulation ? undefined : 'authorization_binding',
20380
20067
  simulation: result.simulation,
20381
20068
  message: text('uploading', result.simulation),
20382
20069
  });
@@ -20415,8 +20102,6 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20415
20102
  ...pageGame,
20416
20103
  language: getCurrentLanguage(),
20417
20104
  phase: 'uploading',
20418
- qrUrl: result.simulation ? undefined : authorizationUrl,
20419
- purpose: result.simulation ? undefined : 'authorization_binding',
20420
20105
  simulation: result.simulation,
20421
20106
  message: text('uploading', result.simulation),
20422
20107
  progressPercent: percent(loaded, validTotal),
@@ -20438,9 +20123,8 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20438
20123
  throw progressPageError;
20439
20124
  if (!result.simulation) {
20440
20125
  emit('waiting_preview', { previewReady: false, terminal: false, nextAction: 'wait',
20441
- qrPurpose: 'authorization_binding', authorizationOpenCommand: undefined });
20126
+ qrPurpose: undefined, authorizationOpenCommand: undefined });
20442
20127
  await page.update({ ...pageGame, language: getCurrentLanguage(), phase: 'waiting_preview',
20443
- qrUrl: authorizationUrl, purpose: 'authorization_binding',
20444
20128
  simulation: false, message: text('waiting_preview') });
20445
20129
  while (true) {
20446
20130
  ensureActive();
@@ -20464,20 +20148,17 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20464
20148
  ...pageGame,
20465
20149
  language: getCurrentLanguage(),
20466
20150
  phase: result.previewReady ? 'preview' : 'complete',
20467
- qrUrl: result.simulation ? undefined : result.previewUrl || authorizationUrl,
20468
- bindingQrUrl: result.previewReady ? authorizationUrl : undefined,
20469
- purpose: result.simulation ? undefined : result.previewReady
20470
- ? result.minisType === 'MINI_TOOL' ? 'tool_preview' : 'game_preview'
20471
- : 'authorization_binding',
20151
+ qrUrl: result.previewUrl,
20152
+ purpose: result.previewReady ? 'game_preview' : undefined,
20472
20153
  simulation: result.simulation,
20473
20154
  message: text('uploaded', result.simulation),
20474
20155
  });
20475
20156
  page.detach();
20476
20157
  detached = true;
20477
20158
  return emit('uploaded', { terminal: true, nextAction: result.previewReady ? 'scan_qr' : 'none',
20478
- qrPurpose: result.previewReady ? result.minisType === 'MINI_TOOL' ? 'tool_preview' : 'game_preview' : undefined,
20159
+ qrPurpose: result.previewReady ? 'game_preview' : undefined,
20479
20160
  ...(result.previewReady ? { message: getCurrentLanguage() === 'zh-CN'
20480
- ? '预览已就绪,请使用 TikTok 扫描预览二维码;也可扫描“查看进度”二维码查看应用信息并提交审核。' : 'Preview ready. Scan the preview QR with TikTok, or scan the progress QR to view app details and submit for review.' } : {}) });
20161
+ ? '上传完成,请使用 TikTok 扫码预览游戏。' : 'Upload complete. Scan with TikTok to preview your game.' } : {}) });
20481
20162
  }
20482
20163
  catch (error) {
20483
20164
  const code = timedOut
@@ -20495,9 +20176,6 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20495
20176
  const messages = {
20496
20177
  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.'],
20497
20178
  VIBE_INVALID_CATEGORY: ['类型或子类型无效,或两者不匹配。使用 ttmg game categories --format json 查询有效组合。', 'Invalid category/subcategory pair. Query valid values with ttmg game categories --format json.'],
20498
- VIBE_INVALID_MINIS_TYPE: ['应用类型须为 MINI_GAME 或 MINI_TOOL。', 'Minis type must be MINI_GAME or MINI_TOOL.'],
20499
- VIBE_TOOL_TRIAL_METADATA: ['本地 Tool 试用固定使用效率办公类、US 和 SDK 1.0.0;暂不接受分类或 app-info 覆盖。', 'The local Tool trial uses Efficiency & Office, US and SDK 1.0.0; category and app-info overrides are not supported yet.'],
20500
- VIBE_PROJECT_TYPE_CONFLICT: ['该工程已使用其他应用类型,请为新的 Tool 使用独立工程目录。', 'This project has a different Minis type. Use a separate project directory for a new Tool.'],
20501
20179
  VIBE_INVALID_DESCRIPTION: ['描述必须是非空的有效 Unicode 文本;省略 --description 可使用默认描述。', 'Description must be non-empty valid Unicode text; omit --description to use the default.'],
20502
20180
  VIBE_AUTH_DENIED: [
20503
20181
  '已取消授权,本次未上传。',
@@ -20579,7 +20257,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20579
20257
  text(status);
20580
20258
  controller.abort();
20581
20259
  await progressUpdates;
20582
- // After upload, preserve the original code for checking progress even if preview lookup stops.
20260
+ // Remove the binding QR on failure too. Keep a readable terminal result page.
20583
20261
  if (page && (code !== 'VIBE_CANCELLED' || result.uploaded)) {
20584
20262
  try {
20585
20263
  await page.update({
@@ -20589,20 +20267,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
20589
20267
  simulation: result.simulation,
20590
20268
  hybrid: result.backend === 'ppe-mock',
20591
20269
  phase: result.uploaded ? 'complete' : 'failed',
20592
- qrUrl: result.uploaded && !result.simulation ? authorizationUrl : undefined,
20593
- purpose: result.uploaded && !result.simulation ? 'authorization_binding' : undefined,
20594
- localizedMessages: result.uploaded && !result.simulation ? {
20595
- 'zh-CN': '代码包已上传,本页已停止等待预览。可扫码查看进度,无需重复上传。',
20596
- 'en-US': 'Package uploaded. This page has stopped waiting for the preview. Scan to check progress; no need to upload again.',
20597
- } : {
20598
- 'zh-CN': messages[code]?.[0] || '本次操作未完成,请检查 CLI 输出。',
20599
- 'en-US': messages[code]?.[1] || 'This operation did not complete. Check the CLI output.',
20600
- },
20601
- message: result.uploaded && !result.simulation
20602
- ? getCurrentLanguage() === 'zh-CN'
20603
- ? '代码包已上传,本页已停止等待预览。可扫码查看进度,无需重复上传。'
20604
- : 'Package uploaded. This page has stopped waiting for the preview. Scan to check progress; no need to upload again.'
20605
- : message,
20270
+ message,
20606
20271
  });
20607
20272
  page.detach();
20608
20273
  detached = true;