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