@ttmg/cli 0.4.6-vibe-beta.14 → 0.4.6-vibe-beta.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -29
- package/dist/index.js +414 -121
- package/dist/index.js.map +1 -1
- package/dist/package.json +2 -2
- package/dist/scripts/verify-agent-contract.js +16 -2
- package/dist/scripts/vibe-upload-smoke.js +60 -20
- package/dist/scripts/wasm-collect-smoke.js +78 -0
- package/dist/scripts/wasm-collect-test-preload.cjs +13 -0
- package/package.json +2 -2
- package/vibe-upload.md +97 -245
package/dist/index.js
CHANGED
|
@@ -6780,11 +6780,11 @@ function printApiResponseLog(title, result) {
|
|
|
6780
6780
|
['Return Value', result.data],
|
|
6781
6781
|
]);
|
|
6782
6782
|
}
|
|
6783
|
-
async function request({ url, method, data, headers, params, logRequestBody = true, signal, }) {
|
|
6783
|
+
async function request({ url, method, data, headers, params, logRequestBody = true, logApiCalls = true, signal, }) {
|
|
6784
6784
|
const config = getTTMGRC();
|
|
6785
6785
|
const cookie = sanitizeCookieHeader(config?.cookie);
|
|
6786
6786
|
const proxyConfig = getAxiosProxyConfig();
|
|
6787
|
-
if (isVerboseEnabled()) {
|
|
6787
|
+
if (isVerboseEnabled() && logApiCalls) {
|
|
6788
6788
|
printApiRequestLog({
|
|
6789
6789
|
url,
|
|
6790
6790
|
method,
|
|
@@ -6811,7 +6811,7 @@ async function request({ url, method, data, headers, params, logRequestBody = tr
|
|
|
6811
6811
|
},
|
|
6812
6812
|
...proxyConfig,
|
|
6813
6813
|
});
|
|
6814
|
-
if (isVerboseEnabled()) {
|
|
6814
|
+
if (isVerboseEnabled() && logApiCalls) {
|
|
6815
6815
|
printApiResponseLog('API Response', {
|
|
6816
6816
|
status: res?.status,
|
|
6817
6817
|
logid: res?.headers['x-tt-logid'],
|
|
@@ -6839,7 +6839,7 @@ async function request({ url, method, data, headers, params, logRequestBody = tr
|
|
|
6839
6839
|
NetworkCode: axiosError.code,
|
|
6840
6840
|
NetworkMessage: axiosError.message,
|
|
6841
6841
|
};
|
|
6842
|
-
if (isVerboseEnabled()) {
|
|
6842
|
+
if (isVerboseEnabled() && logApiCalls) {
|
|
6843
6843
|
printApiResponseLog('API Error', {
|
|
6844
6844
|
status: axiosError.response?.status,
|
|
6845
6845
|
logid: axiosError.response?.headers['x-tt-logid'],
|
|
@@ -7210,6 +7210,16 @@ const messages = {
|
|
|
7210
7210
|
'en-US': {
|
|
7211
7211
|
'cli.command.wasm.desc': 'Prepare or split Wasm into a separate output directory',
|
|
7212
7212
|
'cli.command.wasm.prepare': 'Instrument original Wasm for function collection',
|
|
7213
|
+
'cli.command.wasm.collect': 'Manage platform function collection using the local CLI login',
|
|
7214
|
+
'cli.command.wasm.collect.start': 'Open or resume a collection window; preserve existing IDs unless --reset is set',
|
|
7215
|
+
'cli.command.wasm.collect.export': 'Export collected IDs to an identity-matched collection.json file',
|
|
7216
|
+
'cli.command.wasm.collect.clientKey': 'Client Key for the game being collected',
|
|
7217
|
+
'cli.command.wasm.collect.md5': 'Original Wasm collection MD5 used by the instrumented game',
|
|
7218
|
+
'cli.command.wasm.collect.timeout': 'Platform request deadline: 1–300 seconds',
|
|
7219
|
+
'cli.command.wasm.collect.reset': 'Clear historical function IDs for this game and Wasm before collecting',
|
|
7220
|
+
'cli.command.wasm.collect.output': 'New collection JSON file (never overwrite)',
|
|
7221
|
+
'wasm.collect.succeeded': 'Wasm collection command completed.',
|
|
7222
|
+
'wasm.collect.failed': 'Wasm collection command failed.',
|
|
7213
7223
|
'cli.command.wasm.split': 'Split original Wasm using an identity-matched collection',
|
|
7214
7224
|
'cli.command.wasm.input': 'Original .wasm or Brotli .wasm.br file',
|
|
7215
7225
|
'cli.command.wasm.output': 'New output directory (never overwrite)',
|
|
@@ -7231,32 +7241,35 @@ const messages = {
|
|
|
7231
7241
|
'cli.command.login.password': 'Developer account password (implicit mode)',
|
|
7232
7242
|
'cli.command.logout.desc': 'Logout and clear local user data',
|
|
7233
7243
|
'cli.command.auth.desc': 'Inspect local authentication state',
|
|
7234
|
-
'cli.command.auth.open.desc': '
|
|
7235
|
-
'cli.command.upload.vibeWeb': '
|
|
7244
|
+
'cli.command.auth.open.desc': 'Reopen the local QR page for an existing Vibe task',
|
|
7245
|
+
'cli.command.upload.vibeWeb': 'Disabled; use --vibe and scan with TikTok on your phone',
|
|
7236
7246
|
'cli.command.game.desc': 'Inspect game metadata supported by the platform',
|
|
7237
7247
|
'cli.command.game.categories.desc': 'List game categories and subcategories with gameplay descriptions (offline, read-only)',
|
|
7238
7248
|
'cli.command.game.categories.category': 'Filter by an exact category value, e.g. Puzzle',
|
|
7239
7249
|
'cli.command.game.categories.unknown': 'Unknown category. Run ttmg game categories --format json for valid values.',
|
|
7240
7250
|
'cli.command.game.categories.invalidFormat': 'Use text, json, or ndjson output.',
|
|
7241
7251
|
'cli.command.upload.vibeCategory': 'Optional game category; provide with --subcategory (--vibe). Query ttmg game categories --format json',
|
|
7252
|
+
'cli.command.upload.vibeDescription': 'Optional game description (--vibe); default: Discover the fun of TikTok Mini Games.',
|
|
7242
7253
|
'cli.command.upload.vibeSubcategory': 'Optional subcategory belonging to --category; omit both to keep Other/Other (--vibe)',
|
|
7243
7254
|
'cli.command.upload.vibeWebGuide': `
|
|
7244
|
-
Vibe
|
|
7245
|
-
ttmg upload --vibe --
|
|
7255
|
+
Vibe QR upload (real PPE trial):
|
|
7256
|
+
ttmg upload --vibe --dir ./game --title "My game" --icon ./avatar.png --format ndjson
|
|
7246
7257
|
Use --package ./game.zip instead of --dir for an existing ZIP.
|
|
7247
7258
|
Collect only title and icon; the CLI fills other app_info fields with PPE trial defaults.
|
|
7259
|
+
Skill may pass --description "Game description"; otherwise use "Discover the fun of TikTok Mini Games." (legacy metadata-file descriptions are preserved).
|
|
7248
7260
|
Skill may query ttmg game categories --format json and pass --category Puzzle --subcategory Physics together.
|
|
7249
7261
|
Omit both to keep Other/Other (or the legacy metadata-file classification). Invalid pairs fail before task creation.
|
|
7250
7262
|
prepared and terminal events expose the submitted classification and its source.
|
|
7251
|
-
Keep this process running. At waiting_authorization,
|
|
7252
|
-
pageUrl is the local
|
|
7263
|
+
Keep this process running. At waiting_authorization, scan the QR on pageUrl with TikTok on your phone.
|
|
7264
|
+
pageUrl is the local QR/progress page on the CLI computer. Reopen it with authorizationOpenCommand.
|
|
7253
7265
|
The CLI polls and uploads avatar, app_info and package automatically after authorization. Do not start a second upload.
|
|
7254
7266
|
Real success: terminal=true, status=uploaded, uploaded=true, simulation=false. On failure, read errorCode and nextAction.
|
|
7255
|
-
|
|
7267
|
+
The local QR page opens for every output format; --no-open disables automatic opening.
|
|
7268
|
+
The page displays the submitted title, icon, description, category and subcategory.
|
|
7256
7269
|
Upload completion is not review approval or production release. See bundled vibe-upload.md and ttmg capabilities --format json.
|
|
7257
7270
|
`,
|
|
7258
7271
|
'cli.command.auth.open.pageUrl': 'Local pageUrl returned by the running Vibe upload command',
|
|
7259
|
-
'cli.command.auth.open.requested': '
|
|
7272
|
+
'cli.command.auth.open.requested': 'QR page open requested. Scan with TikTok on your phone; the existing upload process continues polling.',
|
|
7260
7273
|
'cli.command.auth.open.invalidPage': 'Pass the local pageUrl from the running Vibe upload command, not the platform URL.',
|
|
7261
7274
|
'cli.command.auth.open.unavailable': 'Authorization page is unavailable, expired, or no longer waiting. Check the existing upload process; no new task was created.',
|
|
7262
7275
|
'cli.command.auth.open.invalidFormat': 'Use text, json, or ndjson output.',
|
|
@@ -7455,9 +7468,9 @@ Vibe web upload (real PPE trial):
|
|
|
7455
7468
|
'cli.command.upload.dir': 'Game project directory (default: current directory)',
|
|
7456
7469
|
'cli.command.upload.preview': 'Wait for platform processing and output the preview payload',
|
|
7457
7470
|
'cli.command.upload.servePreviewPage': 'Serve a temporary local preview page with test-account and preview QR codes',
|
|
7458
|
-
'cli.command.upload.noOpen': 'Do not open the local preview
|
|
7471
|
+
'cli.command.upload.noOpen': 'Do not automatically open the local preview or Vibe QR page',
|
|
7459
7472
|
'cli.command.upload.format': 'Output format: text | json | ndjson',
|
|
7460
|
-
'cli.command.upload.vibe': '
|
|
7473
|
+
'cli.command.upload.vibe': 'Scan with TikTok and upload: provide title and icon; CLI fills other fields with PPE trial defaults (schema v2)',
|
|
7461
7474
|
'cli.command.upload.vibeTitle': 'Game title supplied by the Skill (--vibe)',
|
|
7462
7475
|
'cli.command.upload.vibeAppInfo': 'Legacy metadata file compatibility; not needed for current uploads and must not be requested from users',
|
|
7463
7476
|
'cli.command.upload.vibeIcon': 'Local PNG/JPEG/WebP selected by the user (--vibe)',
|
|
@@ -7645,6 +7658,16 @@ Vibe web upload (real PPE trial):
|
|
|
7645
7658
|
'zh-CN': {
|
|
7646
7659
|
'cli.command.wasm.desc': '准备或分包 Wasm,产物写入独立目录',
|
|
7647
7660
|
'cli.command.wasm.prepare': '对原始 Wasm 插桩,用于函数采集',
|
|
7661
|
+
'cli.command.wasm.collect': '复用 CLI 本机登录态管理平台函数采集',
|
|
7662
|
+
'cli.command.wasm.collect.start': '开启或恢复采集窗口;未指定 --reset 时保留已有函数 ID',
|
|
7663
|
+
'cli.command.wasm.collect.export': '导出函数 ID,生成与原始 Wasm 匹配的 collection.json',
|
|
7664
|
+
'cli.command.wasm.collect.clientKey': '采集游戏的 Client Key',
|
|
7665
|
+
'cli.command.wasm.collect.md5': '插桩游戏上报使用的原始 Wasm 采集 MD5',
|
|
7666
|
+
'cli.command.wasm.collect.timeout': '平台请求超时:1–300 秒',
|
|
7667
|
+
'cli.command.wasm.collect.reset': '清空此游戏、此 Wasm 的历史函数 ID,再开始采集',
|
|
7668
|
+
'cli.command.wasm.collect.output': '新的采集 JSON 文件(不覆盖已有文件)',
|
|
7669
|
+
'wasm.collect.succeeded': 'Wasm 采集命令已完成。',
|
|
7670
|
+
'wasm.collect.failed': 'Wasm 采集命令失败。',
|
|
7648
7671
|
'cli.command.wasm.split': '使用与原始 Wasm 匹配的采集数据分包',
|
|
7649
7672
|
'cli.command.wasm.input': '原始 .wasm 或 Brotli .wasm.br 文件',
|
|
7650
7673
|
'cli.command.wasm.output': '新的输出目录(不覆盖已有内容)',
|
|
@@ -7666,32 +7689,35 @@ Vibe web upload (real PPE trial):
|
|
|
7666
7689
|
'cli.command.login.password': '开发者账号密码(implicit 模式)',
|
|
7667
7690
|
'cli.command.logout.desc': '退出登录并清除本地用户信息',
|
|
7668
7691
|
'cli.command.auth.desc': '查看本地登录状态',
|
|
7669
|
-
'cli.command.auth.open.desc': '
|
|
7670
|
-
'cli.command.upload.vibeWeb': '
|
|
7692
|
+
'cli.command.auth.open.desc': '重新打开已有 Vibe 任务的本地二维码页',
|
|
7693
|
+
'cli.command.upload.vibeWeb': '已关闭;使用 --vibe 并在手机 TikTok 中扫码授权',
|
|
7671
7694
|
'cli.command.game.desc': '查询平台支持的游戏资料',
|
|
7672
7695
|
'cli.command.game.categories.desc': '查询游戏类型、子类型及玩法说明(离线、只读)',
|
|
7673
7696
|
'cli.command.game.categories.category': '按类型的精确值筛选,例如 Puzzle',
|
|
7674
7697
|
'cli.command.game.categories.unknown': '类型不存在。使用 ttmg game categories --format json 查询有效值。',
|
|
7675
7698
|
'cli.command.game.categories.invalidFormat': '输出格式请使用 text、json 或 ndjson。',
|
|
7676
7699
|
'cli.command.upload.vibeCategory': '可选游戏类型,须与 --subcategory 同传(--vibe);用 ttmg game categories --format json 查询',
|
|
7700
|
+
'cli.command.upload.vibeDescription': '可选游戏描述(--vibe);默认值:Discover the fun of TikTok Mini Games.',
|
|
7677
7701
|
'cli.command.upload.vibeSubcategory': '所属类型的可选子类型;两者均不传保留 Other/Other(--vibe)',
|
|
7678
7702
|
'cli.command.upload.vibeWebGuide': `
|
|
7679
|
-
Vibe
|
|
7680
|
-
ttmg upload --vibe --
|
|
7703
|
+
Vibe 扫码上传(真实 PPE 试用):
|
|
7704
|
+
ttmg upload --vibe --dir ./game --title "游戏名称" --icon ./avatar.png --format ndjson
|
|
7681
7705
|
已有 ZIP 时,用 --package ./game.zip 替换 --dir。
|
|
7682
7706
|
只收集标题和头像;其他 app_info 字段由 CLI 使用 PPE 试用默认值补齐。
|
|
7707
|
+
Skill 可传 --description "游戏描述";省略时使用 Discover the fun of TikTok Mini Games.(旧资料文件中的描述保留)。
|
|
7683
7708
|
Skill 可先查 ttmg game categories --format json,再成对传入 --category Puzzle --subcategory Physics。
|
|
7684
7709
|
两者均不传保留 Other/Other(旧资料文件调用保留文件分类);无效组合在创建任务前报错。
|
|
7685
7710
|
prepared 和终态事件返回最终 classification 及其来源。
|
|
7686
|
-
保持原进程运行。在 waiting_authorization
|
|
7687
|
-
pageUrl
|
|
7711
|
+
保持原进程运行。在 waiting_authorization 阶段,使用手机 TikTok 扫描 pageUrl 页面中的二维码。
|
|
7712
|
+
pageUrl 是 CLI 所在电脑的二维码/进度页;重新打开时,执行返回的 authorizationOpenCommand。
|
|
7688
7713
|
CLI 负责轮询,授权后自动上传 avatar、app_info、package,不再启动第二次上传。
|
|
7689
7714
|
真实成功:terminal=true、status=uploaded、uploaded=true、simulation=false。失败时读取 errorCode 和 nextAction。
|
|
7690
|
-
|
|
7715
|
+
所有输出格式都会打开本地二维码页;--no-open 可关闭自动打开。
|
|
7716
|
+
页面展示本次提交的标题、头像、描述、类型和子类型。
|
|
7691
7717
|
上传完成不代表审核通过或正式上线。详见包内 vibe-upload.md 和 ttmg capabilities --format json。
|
|
7692
7718
|
`,
|
|
7693
7719
|
'cli.command.auth.open.pageUrl': '正在运行的 Vibe 上传命令返回的本地 pageUrl',
|
|
7694
|
-
'cli.command.auth.open.requested': '
|
|
7720
|
+
'cli.command.auth.open.requested': '已请求打开二维码页。请用手机 TikTok 扫码授权,原上传进程继续轮询。',
|
|
7695
7721
|
'cli.command.auth.open.invalidPage': '请传入正在运行的 Vibe 上传命令返回的本地 pageUrl,不是平台授权链接。',
|
|
7696
7722
|
'cli.command.auth.open.unavailable': '授权页不可用、已过期或已不在等待授权。请查看原上传进程状态;未新建任务。',
|
|
7697
7723
|
'cli.command.auth.open.invalidFormat': '输出格式请使用 text、json 或 ndjson。',
|
|
@@ -7890,9 +7916,9 @@ Vibe 网页上传(真实 PPE 试用):
|
|
|
7890
7916
|
'cli.command.upload.dir': '小游戏项目根目录(默认为当前目录)',
|
|
7891
7917
|
'cli.command.upload.preview': '等待平台处理并输出预览二维码原始内容',
|
|
7892
7918
|
'cli.command.upload.servePreviewPage': '启动包含测试账号与预览二维码的临时本地页面',
|
|
7893
|
-
'cli.command.upload.noOpen': '
|
|
7919
|
+
'cli.command.upload.noOpen': '不自动打开本地预览页面或 Vibe 二维码页',
|
|
7894
7920
|
'cli.command.upload.format': '输出格式:text | json | ndjson',
|
|
7895
|
-
'cli.command.upload.vibe': '
|
|
7921
|
+
'cli.command.upload.vibe': 'TikTok 扫码上传:只收集标题和头像,CLI 自动补齐其他字段的 PPE 试用默认值(schema v2)',
|
|
7896
7922
|
'cli.command.upload.vibeAppInfo': '仅兼容旧资料文件;当前上传无需此参数,不应向用户索要',
|
|
7897
7923
|
'cli.command.upload.vibeTitle': 'Skill 提供的游戏标题(--vibe)',
|
|
7898
7924
|
'cli.command.upload.vibeIcon': '用户指定的本地 PNG/JPEG/WebP 图片(--vibe)',
|
|
@@ -16352,7 +16378,7 @@ async function authStatus(options = {}) {
|
|
|
16352
16378
|
return result;
|
|
16353
16379
|
}
|
|
16354
16380
|
|
|
16355
|
-
var version = "0.4.6-vibe-beta.
|
|
16381
|
+
var version = "0.4.6-vibe-beta.16";
|
|
16356
16382
|
var engines = {
|
|
16357
16383
|
node: ">=20.19.1 <21 || >=22"
|
|
16358
16384
|
};
|
|
@@ -16362,13 +16388,15 @@ var pkg = {
|
|
|
16362
16388
|
};
|
|
16363
16389
|
|
|
16364
16390
|
// Skill-facing v2 contract. Backend credentials never cross this boundary.
|
|
16391
|
+
const VIBE_DEFAULT_DESCRIPTION = 'Discover the fun of TikTok Mini Games.';
|
|
16365
16392
|
const VIBE_INPUT_REQUIREMENTS = {
|
|
16366
16393
|
requiredUserInputs: ['title', 'icon'],
|
|
16367
16394
|
codeSource: ['dir', 'package'],
|
|
16368
16395
|
additionalMetadataSource: 'cli-defaults',
|
|
16369
16396
|
defaultMetadataProfile: 'ppe-trial',
|
|
16370
16397
|
requestAdditionalMetadata: false,
|
|
16371
|
-
optionalSkillInputs: ['category', 'subcategory'],
|
|
16398
|
+
optionalSkillInputs: ['category', 'subcategory', 'description'],
|
|
16399
|
+
defaultDescription: VIBE_DEFAULT_DESCRIPTION,
|
|
16372
16400
|
categoryQuery: 'ttmg game categories --format json',
|
|
16373
16401
|
categoryPairRequiredWhenProvided: true,
|
|
16374
16402
|
defaultCategory: { category: 'Other', subcategory: 'Other' },
|
|
@@ -16963,9 +16991,10 @@ var categories = /*#__PURE__*/Object.freeze({
|
|
|
16963
16991
|
|
|
16964
16992
|
// Current PPE trial defaults: the owner-approved metadata used by the successful
|
|
16965
16993
|
// real upload fixture on 2026-09-14, with owner-specified legal URLs updated on
|
|
16966
|
-
// 2026-09-15
|
|
16994
|
+
// 2026-09-15 and the owner-approved brand description on 2026-09-16.
|
|
16995
|
+
// This is not a production metadata profile.
|
|
16967
16996
|
const VIBE_PPE_DEFAULT_APP_INFO = Object.freeze({
|
|
16968
|
-
description:
|
|
16997
|
+
description: VIBE_DEFAULT_DESCRIPTION,
|
|
16969
16998
|
category: 'Other',
|
|
16970
16999
|
subcategory: 'Other',
|
|
16971
17000
|
terms_of_service: 'https://www.tiktok.com',
|
|
@@ -16979,6 +17008,14 @@ const requiredStrings = [
|
|
|
16979
17008
|
'description', 'category', 'subcategory', 'terms_of_service', 'privacy_policy',
|
|
16980
17009
|
];
|
|
16981
17010
|
const optionalStrings = ['apple_team_id', 'contact_email', 'region', 'min_sdk_version'];
|
|
17011
|
+
function applyVibeDescription(description, appInfo) {
|
|
17012
|
+
if (description === undefined)
|
|
17013
|
+
return appInfo;
|
|
17014
|
+
if (typeof description !== 'string' || !description.trim() ||
|
|
17015
|
+
[...description].some(char => /^[\uD800-\uDFFF]$/.test(char)))
|
|
17016
|
+
throw new VibeError('VIBE_INVALID_DESCRIPTION');
|
|
17017
|
+
return { ...(appInfo || VIBE_PPE_DEFAULT_APP_INFO), description: description.trim() };
|
|
17018
|
+
}
|
|
16982
17019
|
function applyVibeCategory(options, appInfo) {
|
|
16983
17020
|
if (options.category === undefined && options.subcategory === undefined)
|
|
16984
17021
|
return appInfo;
|
|
@@ -17101,7 +17138,7 @@ function getCapabilities() {
|
|
|
17101
17138
|
networkRequired: false,
|
|
17102
17139
|
},
|
|
17103
17140
|
vibeUpload: {
|
|
17104
|
-
command: 'ttmg upload --vibe --
|
|
17141
|
+
command: 'ttmg upload --vibe --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
|
|
17105
17142
|
mockCommand: 'ttmg upload --vibe --mock --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
|
|
17106
17143
|
guide: 'vibe-upload.md',
|
|
17107
17144
|
option: '--vibe',
|
|
@@ -17124,7 +17161,7 @@ function getCapabilities() {
|
|
|
17124
17161
|
host: 'developers.tiktok.com',
|
|
17125
17162
|
taskCreation: true,
|
|
17126
17163
|
statusPolling: true,
|
|
17127
|
-
authorizationScheme: '
|
|
17164
|
+
authorizationScheme: 'landing_url',
|
|
17128
17165
|
uploadEnabled: true,
|
|
17129
17166
|
},
|
|
17130
17167
|
terminalSuccess: 'uploaded',
|
|
@@ -17133,6 +17170,15 @@ function getCapabilities() {
|
|
|
17133
17170
|
sourceProjectIdentity: '.ttmg/vibe-project.json',
|
|
17134
17171
|
metadataInputs: ['title', 'icon', 'dir|package'],
|
|
17135
17172
|
inputRequirements: VIBE_INPUT_REQUIREMENTS,
|
|
17173
|
+
description: {
|
|
17174
|
+
option: '--description',
|
|
17175
|
+
required: false,
|
|
17176
|
+
default: VIBE_PPE_DEFAULT_APP_INFO.description,
|
|
17177
|
+
precedence: ['argument', 'legacy-app-info', 'default'],
|
|
17178
|
+
invalidInput: 'reject-before-task-creation',
|
|
17179
|
+
resultField: 'description',
|
|
17180
|
+
sourceField: 'descriptionSource',
|
|
17181
|
+
},
|
|
17136
17182
|
classification: {
|
|
17137
17183
|
queryCommand: 'ttmg game categories --format json',
|
|
17138
17184
|
options: ['--category', '--subcategory'],
|
|
@@ -17142,7 +17188,27 @@ function getCapabilities() {
|
|
|
17142
17188
|
legacyAppInfo: 'preserved-when-options-omitted; explicit-pair-overrides-category-fields',
|
|
17143
17189
|
resultField: 'classification',
|
|
17144
17190
|
},
|
|
17145
|
-
webAuthorization: { option: '--web',
|
|
17191
|
+
webAuthorization: { option: '--web', available: false, errorCode: 'VIBE_WEB_DISABLED', replacement: '--vibe', materialUploadEnabled: false },
|
|
17192
|
+
qrAuthorization: {
|
|
17193
|
+
command: 'ttmg upload --vibe --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
|
|
17194
|
+
defaultWithoutWeb: true,
|
|
17195
|
+
backend: 'ppe',
|
|
17196
|
+
defaultLane: 'ppe_op_vc',
|
|
17197
|
+
requiresEnvironmentConfiguration: false,
|
|
17198
|
+
simulation: false,
|
|
17199
|
+
opensLocalQrPage: true,
|
|
17200
|
+
formatIndependent: true,
|
|
17201
|
+
disablePageOpenOption: '--no-open',
|
|
17202
|
+
pageUrlField: 'pageUrl',
|
|
17203
|
+
authorizationMode: 'qr',
|
|
17204
|
+
nextAction: 'scan_qr',
|
|
17205
|
+
userAction: 'Open pageUrl on the CLI computer and scan the authorization QR with TikTok on the phone',
|
|
17206
|
+
continuesPolling: true,
|
|
17207
|
+
materialUploadEnabled: true,
|
|
17208
|
+
phoneAuthorizationVerified: true,
|
|
17209
|
+
authorizationQrPayload: 'landing_url',
|
|
17210
|
+
pageMetadata: ['title', 'icon', 'description', 'category', 'subcategory'],
|
|
17211
|
+
},
|
|
17146
17212
|
skillWorkflow: {
|
|
17147
17213
|
preferredFormat: 'ndjson',
|
|
17148
17214
|
packageAlternative: '--package <zipPath> replaces --dir <gameDir>',
|
|
@@ -17151,7 +17217,7 @@ function getCapabilities() {
|
|
|
17151
17217
|
uploadOwner: 'cli',
|
|
17152
17218
|
waitingAuthorization: {
|
|
17153
17219
|
status: 'waiting_authorization',
|
|
17154
|
-
userAction: '
|
|
17220
|
+
userAction: 'Open pageUrl on the CLI computer and scan the QR with TikTok on the phone',
|
|
17155
17221
|
pageUrlField: 'pageUrl',
|
|
17156
17222
|
pageUrlPurpose: 'local binding and upload progress page',
|
|
17157
17223
|
reopenCommandField: 'authorizationOpenCommand',
|
|
@@ -17201,7 +17267,7 @@ function getCapabilities() {
|
|
|
17201
17267
|
previewQr: false,
|
|
17202
17268
|
realAuthorizationVerified: true,
|
|
17203
17269
|
realUploadVerified: true,
|
|
17204
|
-
verificationScope: { backend: 'ppe', assets: 'test-fixtures', date: '2026-09-
|
|
17270
|
+
verificationScope: { backend: 'ppe', authorizationMode: 'qr', assets: 'test-fixtures', date: '2026-09-16', production: false },
|
|
17205
17271
|
autoUploadAfterAuthorization: true,
|
|
17206
17272
|
boundedPolling: true,
|
|
17207
17273
|
cancellation: true,
|
|
@@ -17219,7 +17285,7 @@ function getCapabilities() {
|
|
|
17219
17285
|
requiresRunningUpload: true,
|
|
17220
17286
|
createsTask: false,
|
|
17221
17287
|
outputContainsLandingCredentials: false,
|
|
17222
|
-
target: '
|
|
17288
|
+
target: 'local-qr-page',
|
|
17223
17289
|
successStatus: 'open_requested',
|
|
17224
17290
|
provesAuthorization: false,
|
|
17225
17291
|
},
|
|
@@ -17270,6 +17336,20 @@ function getCapabilities() {
|
|
|
17270
17336
|
projectIntegration: false,
|
|
17271
17337
|
platformWrite: false,
|
|
17272
17338
|
},
|
|
17339
|
+
wasmCollect: {
|
|
17340
|
+
available: true,
|
|
17341
|
+
experimental: true,
|
|
17342
|
+
authentication: 'local_cli_login',
|
|
17343
|
+
credentialOutput: false,
|
|
17344
|
+
nativeBindingRequired: false,
|
|
17345
|
+
platformVerified: false,
|
|
17346
|
+
outputFormats: ['text', 'json'],
|
|
17347
|
+
start: { command: 'ttmg wasm collect start', platformWrite: true,
|
|
17348
|
+
resetDefault: false, resetOption: '--reset', automaticRetry: false },
|
|
17349
|
+
export: { command: 'ttmg wasm collect export', platformWrite: false,
|
|
17350
|
+
collectionSchemaVersion: '1', originalInputRequired: true,
|
|
17351
|
+
outputPathField: 'collection.path', overwrite: false, strategy: 'union' },
|
|
17352
|
+
},
|
|
17273
17353
|
dev: {
|
|
17274
17354
|
command: 'ttmg dev --format json --no-open',
|
|
17275
17355
|
eventsCommand: 'ttmg dev --events ndjson --no-open',
|
|
@@ -17861,6 +17941,8 @@ function getDesktopPath() {
|
|
|
17861
17941
|
const program = new commander.Command();
|
|
17862
17942
|
const isPreviewPageServerWorker = process.argv[2] === '__preview-page-server';
|
|
17863
17943
|
const isVibePageServerWorker = process.argv[2] === '__vibe-page-server';
|
|
17944
|
+
const wasmArgumentIndex = process.argv.indexOf('wasm', 2);
|
|
17945
|
+
const isWasmCollectCommand = wasmArgumentIndex >= 2 && process.argv[wasmArgumentIndex + 1] === 'collect';
|
|
17864
17946
|
const withVerboseOption = (command) => command.option('--verbose', t('cli.option.verbose'));
|
|
17865
17947
|
const structuredFormatIndex = process.argv.indexOf('--format');
|
|
17866
17948
|
const inlineStructuredFormat = process.argv.find(arg => arg.startsWith('--format='));
|
|
@@ -17871,6 +17953,7 @@ const structuredFormat = inlineStructuredFormat
|
|
|
17871
17953
|
: '';
|
|
17872
17954
|
if (!isPreviewPageServerWorker &&
|
|
17873
17955
|
!isVibePageServerWorker &&
|
|
17956
|
+
!(isWasmCollectCommand && !structuredFormat) &&
|
|
17874
17957
|
structuredFormat !== 'json' &&
|
|
17875
17958
|
structuredFormat !== 'ndjson' &&
|
|
17876
17959
|
!process.argv.includes('--implicit')) {
|
|
@@ -17940,9 +18023,10 @@ withVerboseOption(program
|
|
|
17940
18023
|
.option('--serve-preview-page', t('cli.command.upload.servePreviewPage'))
|
|
17941
18024
|
.option('--no-open', t('cli.command.upload.noOpen'))
|
|
17942
18025
|
.option('--vibe', t('cli.command.upload.vibe'))
|
|
17943
|
-
.
|
|
18026
|
+
.addOption(new commander.Option('--web', t('cli.command.upload.vibeWeb')).hideHelp())
|
|
17944
18027
|
// Vibe v2 backend selection (including opt-in ppe-mock) stays outside the Skill workflow.
|
|
17945
18028
|
.option('--title <title>', t('cli.command.upload.vibeTitle'))
|
|
18029
|
+
.option('--description <description>', t('cli.command.upload.vibeDescription'))
|
|
17946
18030
|
.option('--icon <path>', t('cli.command.upload.vibeIcon'))
|
|
17947
18031
|
.option('--category <category>', t('cli.command.upload.vibeCategory'))
|
|
17948
18032
|
.option('--subcategory <subcategory>', t('cli.command.upload.vibeSubcategory'))
|
|
@@ -17964,6 +18048,7 @@ withVerboseOption(program
|
|
|
17964
18048
|
}
|
|
17965
18049
|
if ([
|
|
17966
18050
|
cmd.title,
|
|
18051
|
+
cmd.description,
|
|
17967
18052
|
cmd.icon,
|
|
17968
18053
|
cmd.appInfo,
|
|
17969
18054
|
cmd.category,
|
|
@@ -18310,6 +18395,25 @@ devSessionCmd
|
|
|
18310
18395
|
});
|
|
18311
18396
|
});
|
|
18312
18397
|
const wasmCmd = program.command('wasm').description(t('cli.command.wasm.desc'));
|
|
18398
|
+
const collectCmd = wasmCmd.command('collect').description(t('cli.command.wasm.collect'));
|
|
18399
|
+
for (const operation of ['start', 'export']) {
|
|
18400
|
+
const command = collectCmd.command(operation)
|
|
18401
|
+
.description(t(operation === 'start' ? 'cli.command.wasm.collect.start' : 'cli.command.wasm.collect.export'))
|
|
18402
|
+
.allowExcessArguments(false)
|
|
18403
|
+
.option('--client-key <key>', t('cli.command.wasm.collect.clientKey'))
|
|
18404
|
+
.option('--wasm-md5 <md5>', t('cli.command.wasm.collect.md5'))
|
|
18405
|
+
.option('--timeout <seconds>', t('cli.command.wasm.collect.timeout'), '30')
|
|
18406
|
+
.option('--format <format>', t('cli.command.wasm.format'), 'json');
|
|
18407
|
+
if (operation === 'start')
|
|
18408
|
+
command.option('--reset', t('cli.command.wasm.collect.reset'));
|
|
18409
|
+
else
|
|
18410
|
+
command.option('--input <path>', t('cli.command.wasm.input'))
|
|
18411
|
+
.option('--output <path>', t('cli.command.wasm.collect.output'));
|
|
18412
|
+
command.action(async (options) => {
|
|
18413
|
+
const Native = await Promise.resolve().then(function () { return index; });
|
|
18414
|
+
await Native.wasmCollectCommand(operation, options);
|
|
18415
|
+
});
|
|
18416
|
+
}
|
|
18313
18417
|
for (const operation of ['prepare', 'split']) {
|
|
18314
18418
|
const command = wasmCmd.command(operation)
|
|
18315
18419
|
.description(t(operation === 'prepare' ? 'cli.command.wasm.prepare' : 'cli.command.wasm.split'))
|
|
@@ -18473,7 +18577,7 @@ async function executeVibeAuthOpen(options, deps = { open: openVibeBrowser }) {
|
|
|
18473
18577
|
const url = authorizationOpenUrl(options.pageUrl);
|
|
18474
18578
|
await checkAuthorizationPage(url);
|
|
18475
18579
|
try {
|
|
18476
|
-
await deps.open(
|
|
18580
|
+
await deps.open(options.pageUrl);
|
|
18477
18581
|
}
|
|
18478
18582
|
catch {
|
|
18479
18583
|
throw new VibeError('VIBE_BROWSER_OPEN_FAILED');
|
|
@@ -18888,8 +18992,9 @@ function createMockBackend(scenario = 'success') {
|
|
|
18888
18992
|
}
|
|
18889
18993
|
|
|
18890
18994
|
function createAuthorizationScheme(landingUrl) {
|
|
18891
|
-
//
|
|
18892
|
-
|
|
18995
|
+
// QR and browser authorization use the same backend-provided landing URL.
|
|
18996
|
+
// Preserve its query exactly; do not wrap it in an app scheme or re-encode it.
|
|
18997
|
+
return safeWebUrl(landingUrl);
|
|
18893
18998
|
}
|
|
18894
18999
|
|
|
18895
19000
|
const DEV_PORT = 3000;
|
|
@@ -19126,7 +19231,7 @@ async function prepareVibeInput(options, onPackaging, signal, deps = { check: ex
|
|
|
19126
19231
|
const title = options.title?.trim();
|
|
19127
19232
|
if (!title)
|
|
19128
19233
|
throw new VibeError('VIBE_TITLE_REQUIRED');
|
|
19129
|
-
const appInfo = applyVibeCategory(options, readVibeAppInfo(options.appInfo));
|
|
19234
|
+
const appInfo = applyVibeCategory(options, applyVibeDescription(options.description, readVibeAppInfo(options.appInfo)));
|
|
19130
19235
|
if (options.package && options.dir)
|
|
19131
19236
|
throw new VibeError('VIBE_INPUT_CONFLICT');
|
|
19132
19237
|
const image = readFile(options.icon, VIBE_ICON_LIMIT, 'VIBE_INVALID_ICON');
|
|
@@ -19175,20 +19280,54 @@ async function prepareVibeInput(options, onPackaging, signal, deps = { check: ex
|
|
|
19175
19280
|
}
|
|
19176
19281
|
|
|
19177
19282
|
const vibePageStyles = `
|
|
19178
|
-
|
|
19179
|
-
:root{font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;color:#
|
|
19180
|
-
*{box-sizing:border-box}
|
|
19181
|
-
|
|
19182
|
-
.
|
|
19183
|
-
.
|
|
19184
|
-
.
|
|
19185
|
-
.
|
|
19186
|
-
.
|
|
19187
|
-
|
|
19283
|
+
|
|
19284
|
+
:root{font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;color:#24282f;background:#fff;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased}
|
|
19285
|
+
*{box-sizing:border-box}
|
|
19286
|
+
body{margin:0;min-height:100vh;min-height:100svh;display:flex;align-items:center;justify-content:center;padding:36px 24px;font-size:14px;line-height:1.5}
|
|
19287
|
+
.shell{width:100%;max-width:400px}
|
|
19288
|
+
.steps{display:flex;align-items:center;justify-content:center;gap:10px;margin:0 0 32px;padding:0;list-style:none}
|
|
19289
|
+
.step{display:flex;align-items:center;gap:6px;font-size:11px;color:#9298a2;white-space:nowrap}
|
|
19290
|
+
.step:after{content:"";height:1px;width:25px;margin-left:4px;background:#e9ebef}.step:last-child:after{display:none}
|
|
19291
|
+
.step-number{height:18px;min-width:18px;border-radius:50%;border:1px solid #e2e5e9;display:grid;place-items:center;font-size:10px}
|
|
19292
|
+
.step.active{color:#272c34;font-weight:600}.step.active .step-number{background:#272c34;color:#fff;border-color:#272c34}
|
|
19293
|
+
.step.done{color:#537565}.step.done .step-number{background:#edf6f0;color:#287c54;border-color:#edf6f0}
|
|
19294
|
+
.heading{text-align:center}h1{font-size:26px;font-weight:650;line-height:1.35;letter-spacing:-.6px;margin:0 0 10px}
|
|
19295
|
+
.subtitle{color:#7a818c;font-size:12px;line-height:1.7;margin:0;overflow-wrap:anywhere}
|
|
19296
|
+
.game{margin:28px 0 0;padding-top:24px;border-top:1px solid #eceef1}
|
|
19297
|
+
.game-summary{display:flex;align-items:center;gap:12px}.game-title{margin:0;font-size:15px;font-weight:600;overflow-wrap:anywhere;min-width:0}
|
|
19298
|
+
.game-icon,.game-avatar{width:44px;height:44px;border-radius:10px;flex-shrink:0;background:#f0f2f5}.game-avatar{object-fit:cover}.game-icon{display:grid;place-items:center;color:#707b8b}
|
|
19299
|
+
.game-details{margin:20px 0 0;font-size:13px}.game-details dt{font-size:11px;color:#9298a2;margin-bottom:4px}.game-details dd{margin:0;overflow-wrap:anywhere}
|
|
19300
|
+
.game-description{white-space:pre-wrap;line-height:1.65}.game-categories{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:18px}
|
|
19301
|
+
.scan-action{text-align:center;font-size:13px;font-weight:500;margin:26px 0 10px}.qr-area{display:flex;justify-content:center}.qr-frame{padding:8px;width:208px;height:208px}.qr-frame img{display:block;width:192px;height:192px;max-width:100%}
|
|
19302
|
+
.upload-area{display:flex;align-items:center;flex-direction:column;gap:16px;padding-top:26px}.percentage{font-size:34px;line-height:1;font-variant-numeric:tabular-nums;font-weight:550}.percentage span{font-size:16px;color:#858e9d;padding-left:3px}
|
|
19303
|
+
.progress-wrap{width:100%}progress{display:block;width:100%;height:4px;border:none;border-radius:20px;overflow:hidden;background:#edf0f4;accent-color:#4c6378}progress::-webkit-progress-bar{background:#edf0f4;border-radius:20px}progress::-webkit-progress-value{background:#4c6378;border-radius:20px}progress::-moz-progress-bar{background:#4c6378;border-radius:20px}.transfer-label{text-align:center;font-size:11px;color:#8b939f;margin:8px 0 0}
|
|
19304
|
+
.footer{text-align:center;font-size:10px;line-height:1.7;color:#a0a5ae;margin:28px 0 0}.footer .dot{display:inline-block;height:5px;width:5px;border-radius:50%;background:#c1995e;vertical-align:middle;margin-right:5px}.mock-badge{width:fit-content;margin:0 0 10px auto;font-size:10px;padding:2px 7px;border:1px solid #dfe2e7;border-radius:5px;color:#707783}
|
|
19305
|
+
@media(max-width:380px){body{padding:28px 20px}.steps{gap:7px}.step:after{width:16px;margin-left:1px}h1{font-size:24px}}
|
|
19306
|
+
@media(max-height:640px){body{align-items:flex-start;padding-top:28px;padding-bottom:28px}}
|
|
19307
|
+
`;
|
|
19188
19308
|
|
|
19189
19309
|
const VIBE_PAGE_WORKER = '__vibe-page-server';
|
|
19190
19310
|
// Includes the base64 form of the locally validated image (up to 10 MiB).
|
|
19191
19311
|
const VIBE_PAGE_HTML_LIMIT = 16 * 1024 * 1024;
|
|
19312
|
+
const pageUpdates = `(() => {
|
|
19313
|
+
let etag;
|
|
19314
|
+
const terminal = () => ['complete', 'failed'].includes(document.querySelector('main')?.dataset.phase);
|
|
19315
|
+
async function refresh() {
|
|
19316
|
+
if (terminal()) return;
|
|
19317
|
+
try {
|
|
19318
|
+
const response = await fetch(location.pathname, {cache: 'no-store', headers: etag ? {'If-None-Match': etag} : {}});
|
|
19319
|
+
if (response.ok) {
|
|
19320
|
+
etag = response.headers.get('etag');
|
|
19321
|
+
const next = new DOMParser().parseFromString(await response.text(), 'text/html');
|
|
19322
|
+
const current = document.querySelector('main'), updated = next.querySelector('main');
|
|
19323
|
+
if (current && updated && current.outerHTML !== updated.outerHTML) current.replaceWith(updated);
|
|
19324
|
+
document.title = next.title;
|
|
19325
|
+
}
|
|
19326
|
+
} catch { /* Keep the current page readable during a temporary connection failure. */ }
|
|
19327
|
+
if (!terminal()) setTimeout(refresh, 2000);
|
|
19328
|
+
}
|
|
19329
|
+
if (!terminal()) setTimeout(refresh, 2000);
|
|
19330
|
+
})();`;
|
|
19192
19331
|
const escape = (s) => s.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
19193
19332
|
async function renderVibePage(input) {
|
|
19194
19333
|
const zh = input.language !== 'en-US';
|
|
@@ -19198,7 +19337,6 @@ async function renderVibePage(input) {
|
|
|
19198
19337
|
? {
|
|
19199
19338
|
steps: ['扫码授权', '上传游戏', '完成'],
|
|
19200
19339
|
mock: input.hybrid ? 'PPE + Mock' : '模拟',
|
|
19201
|
-
game: '当前游戏',
|
|
19202
19340
|
gameIcon: '游戏头像',
|
|
19203
19341
|
bindingTitle: '上传到 TikTok',
|
|
19204
19342
|
bindingAction: input.hybrid ? '真实授权二维码 · 混合联调' : '使用 TikTok 扫描二维码',
|
|
@@ -19211,7 +19349,6 @@ async function renderVibePage(input) {
|
|
|
19211
19349
|
uploadingTitle: '正在上传小游戏',
|
|
19212
19350
|
uploadingSubtitle: input.simulation ? '正在模拟授权后的上传流程,不会上传游戏内容。' : '授权已完成,正在上传本次游戏内容。',
|
|
19213
19351
|
uploadLabel: input.simulation ? '模拟上传进度' : '上传进度',
|
|
19214
|
-
uploadStatus: '正在上传游戏,请稍候',
|
|
19215
19352
|
completeTitle: input.simulation ? '模拟上传完成' : '上传完成',
|
|
19216
19353
|
completeSubtitle: input.simulation
|
|
19217
19354
|
? '绑定和上传流程模拟完成,未向 TikTok 上传游戏。'
|
|
@@ -19225,7 +19362,6 @@ async function renderVibePage(input) {
|
|
|
19225
19362
|
: {
|
|
19226
19363
|
steps: ['Authorize', 'Upload', 'Done'],
|
|
19227
19364
|
mock: input.hybrid ? 'PPE + Mock' : 'MOCK',
|
|
19228
|
-
game: 'YOUR GAME',
|
|
19229
19365
|
gameIcon: 'Game icon',
|
|
19230
19366
|
bindingTitle: 'Upload to TikTok',
|
|
19231
19367
|
bindingAction: input.hybrid ? 'Real authorization QR · integration demo' : 'Scan the QR code with TikTok',
|
|
@@ -19242,7 +19378,6 @@ async function renderVibePage(input) {
|
|
|
19242
19378
|
uploadLabel: input.simulation
|
|
19243
19379
|
? 'Simulated upload progress'
|
|
19244
19380
|
: 'Upload progress',
|
|
19245
|
-
uploadStatus: 'Uploading your game. Please wait.',
|
|
19246
19381
|
completeTitle: input.simulation ? 'Simulated upload complete' : 'Upload complete',
|
|
19247
19382
|
completeSubtitle: input.simulation
|
|
19248
19383
|
? 'Binding and upload simulation complete. Nothing was uploaded to TikTok.'
|
|
@@ -19268,7 +19403,7 @@ async function renderVibePage(input) {
|
|
|
19268
19403
|
/^data:image\/(?:png|jpeg|webp);base64,[A-Za-z0-9+/]+=*$/.test(input.iconDataUrl)
|
|
19269
19404
|
? `<img class="game-avatar" width="44" height="44" alt="${copy.gameIcon}" src="${input.iconDataUrl}">`
|
|
19270
19405
|
: '<span class="game-icon" aria-hidden="true"><svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M7 7h10c2 0 3 2 3 4l1 5c.4 2-1 3-2.5 2l-3-2h-7l-3 2C4 19 2.6 18 3 16l1-5c0-2 1-4 3-4Z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M8 10v4m-2-2h4m6-1h.01M18 13h.01" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></span>';
|
|
19271
|
-
const game = `<div class="game">${icon}<
|
|
19406
|
+
const game = `<div class="game"><div class="game-summary">${icon}<p class="game-title">${escape(input.title)}</p></div><dl class="game-details"><div><dt>${zh ? '描述' : 'Description'}</dt><dd class="game-description">${escape(input.description ?? VIBE_PPE_DEFAULT_APP_INFO.description)}</dd></div><div class="game-categories"><div><dt>${zh ? '类型' : 'Category'}</dt><dd>${escape(input.category ?? VIBE_PPE_DEFAULT_APP_INFO.category)}</dd></div><div><dt>${zh ? '子类型' : 'Subcategory'}</dt><dd>${escape(input.subcategory ?? VIBE_PPE_DEFAULT_APP_INFO.subcategory)}</dd></div></div></dl></div>`;
|
|
19272
19407
|
const checkIcon = '<svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="m3 8 3 3 7-7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
19273
19408
|
const title = phase === 'binding'
|
|
19274
19409
|
? copy.bindingTitle
|
|
@@ -19296,50 +19431,43 @@ async function renderVibePage(input) {
|
|
|
19296
19431
|
Number.isFinite(input.progressPercent)
|
|
19297
19432
|
? Math.max(0, Math.min(100, Math.round(input.progressPercent)))
|
|
19298
19433
|
: undefined;
|
|
19299
|
-
const
|
|
19300
|
-
?
|
|
19301
|
-
: phase === 'uploading'
|
|
19302
|
-
? copy.
|
|
19303
|
-
:
|
|
19304
|
-
|
|
19305
|
-
|
|
19306
|
-
|
|
19307
|
-
|
|
19308
|
-
: qr
|
|
19309
|
-
? `<div class="qr-area"><div class="qr-frame"><img width="192" height="192" alt="${purpose}" src="${qr}"></div></div>`
|
|
19310
|
-
: phase === 'uploading'
|
|
19311
|
-
? `<div class="upload-area"><div class="upload-symbol"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 16V4m-4 4 4-4 4 4M5 15v5h14v-5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></div>${progress !== undefined ? `<div class="percentage">${progress}<span>%</span></div><div class="progress-wrap"><progress aria-label="${copy.uploadLabel}" max="100" value="${progress}"></progress><p class="transfer-label">${copy.uploadLabel}</p></div>` : '<span class="spinner" aria-hidden="true"></span>'}</div>`
|
|
19312
|
-
: `<div class="upload-area"><div class="pending-symbol">${phase === 'failed' ? '!' : checkIcon}</div></div>`;
|
|
19313
|
-
return `<!doctype html><html lang="${zh ? 'zh-CN' : 'en'}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta http-equiv="refresh" content="2"><title>${escape(title)} · ${escape(input.title)}</title><style>${vibePageStyles}</style></head>
|
|
19314
|
-
<body><main class="shell" data-phase="${phase}"><header class="brand"><span class="brand-mark" aria-hidden="true"><svg width="16" height="16" viewBox="0 0 20 20" fill="none"><path d="m7 5-4 5 4 5m6-10 4 5-4 5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="brand-name">TikTok Minis</span><span class="brand-tool">DevTool</span>${input.simulation ? `<span class="mock-badge">${copy.mock}</span>` : ''}</header>
|
|
19315
|
-
<section class="card" aria-labelledby="page-title">${phase === 'failed' ? '' : `<ol class="steps" aria-label="${zh ? '上传步骤' : 'Upload steps'}">${copy.steps.map((label, i) => `<li class="step ${i < step ? 'done' : i === step ? 'active' : ''}"${i === step ? ' aria-current="step"' : ''}><span class="step-number">${i < step ? checkIcon : i + 1}</span>${label}</li>`).join('')}</ol>`}
|
|
19316
|
-
<div class="heading"><h1 id="page-title">${title}</h1></div>${game}<div class="heading">${phase === 'binding' ? `<p class="scan-action">${copy.bindingAction}</p>` : ''}<p class="subtitle">${subtitle}</p></div>${content}
|
|
19317
|
-
<p class="status${phase === 'complete' ? ' complete' : ''}" role="status">${phase === 'complete' ? `<span class="success-icon">${checkIcon}</span>` : phase === 'failed' ? '' : '<span class="spinner" aria-hidden="true"></span>'}${status}</p>
|
|
19434
|
+
const content = qr
|
|
19435
|
+
? `<div class="qr-area"><div class="qr-frame"><img width="192" height="192" alt="${purpose}" src="${qr}"></div></div>`
|
|
19436
|
+
: phase === 'uploading' && progress !== undefined
|
|
19437
|
+
? `<div class="upload-area"><div class="percentage">${progress}<span>%</span></div><div class="progress-wrap"><progress aria-label="${copy.uploadLabel}" max="100" value="${progress}"></progress><p class="transfer-label">${copy.uploadLabel}</p></div></div>`
|
|
19438
|
+
: '';
|
|
19439
|
+
return `<!doctype html><html lang="${zh ? 'zh-CN' : 'en'}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${escape(title)} · ${escape(input.title)}</title><style>${vibePageStyles}</style><script src="?updates" defer></script></head>
|
|
19440
|
+
<body><main class="shell" data-phase="${phase}">${input.simulation ? `<p class="mock-badge">${copy.mock}</p>` : ''}
|
|
19441
|
+
<section class="card" aria-labelledby="page-title">${phase === 'failed' ? '' : `<ol class="steps" aria-label="${zh ? '上传步骤' : 'Upload steps'}">${copy.steps.map((label, i) => `<li class="step ${phase === 'complete' || i < step ? 'done' : i === step ? 'active' : ''}"${phase !== 'complete' && i === step ? ' aria-current="step"' : ''}><span class="step-number">${phase === 'complete' || i < step ? checkIcon : i + 1}</span>${label}</li>`).join('')}</ol>`}
|
|
19442
|
+
<div class="heading"><h1 id="page-title">${title}</h1><p class="subtitle" role="status">${subtitle}</p></div>${game}${phase === 'binding' ? `<p class="scan-action">${copy.bindingAction}</p>` : ''}${content}
|
|
19318
19443
|
</section>
|
|
19319
19444
|
<p class="footer">${input.simulation ? `<span class="dot" aria-hidden="true"></span>${copy.mockNote}` : copy.footer}</p></main></body></html>`;
|
|
19320
19445
|
}
|
|
19321
19446
|
async function createVibePageServer() {
|
|
19322
19447
|
const route = `/${crypto.randomBytes(24).toString('hex')}`;
|
|
19323
19448
|
let html = 'Starting…';
|
|
19449
|
+
let etag = '';
|
|
19324
19450
|
let landingUrl;
|
|
19325
19451
|
const server = http.createServer((req, res) => {
|
|
19326
19452
|
const address = server.address();
|
|
19327
19453
|
const host = address && typeof address !== 'string' ? `127.0.0.1:${address.port}` : '';
|
|
19454
|
+
if (req.method === 'GET' && req.url === `${route}?updates` && req.headers.host === host) {
|
|
19455
|
+
res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' });
|
|
19456
|
+
res.end(pageUpdates);
|
|
19457
|
+
return;
|
|
19458
|
+
}
|
|
19328
19459
|
if (req.url === `${route}/authorize` && req.headers.host === host &&
|
|
19329
19460
|
['GET', 'HEAD'].includes(req.method || '') &&
|
|
19330
19461
|
(!req.headers.origin || req.headers.origin === `http://${host}`) &&
|
|
19331
19462
|
req.headers['sec-fetch-site'] !== 'cross-site') {
|
|
19332
19463
|
const headers = { 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer' };
|
|
19333
|
-
if (!landingUrl) {
|
|
19464
|
+
if (!landingUrl || req.method === 'GET') {
|
|
19334
19465
|
res.writeHead(410, { ...headers, 'Content-Type': 'text/plain; charset=utf-8' });
|
|
19335
|
-
res.end('
|
|
19466
|
+
res.end('Web authorization is disabled. Open the local QR page and scan with TikTok.');
|
|
19336
19467
|
}
|
|
19337
19468
|
else if (req.method === 'HEAD') {
|
|
19338
19469
|
res.writeHead(204, { ...headers, 'X-TTMG-Authorization': 'ready' }).end();
|
|
19339
19470
|
}
|
|
19340
|
-
else {
|
|
19341
|
-
res.writeHead(302, { ...headers, Location: landingUrl }).end();
|
|
19342
|
-
}
|
|
19343
19471
|
return;
|
|
19344
19472
|
}
|
|
19345
19473
|
if (req.method !== 'GET' ||
|
|
@@ -19348,12 +19476,17 @@ async function createVibePageServer() {
|
|
|
19348
19476
|
res.writeHead(404).end();
|
|
19349
19477
|
return;
|
|
19350
19478
|
}
|
|
19479
|
+
if (etag && req.headers['if-none-match'] === etag) {
|
|
19480
|
+
res.writeHead(304, { 'ETag': etag, 'Cache-Control': 'no-store' }).end();
|
|
19481
|
+
return;
|
|
19482
|
+
}
|
|
19351
19483
|
res.writeHead(200, {
|
|
19484
|
+
'ETag': etag,
|
|
19352
19485
|
'Content-Type': 'text/html; charset=utf-8',
|
|
19353
19486
|
'Cache-Control': 'no-store',
|
|
19354
19487
|
'Referrer-Policy': 'no-referrer',
|
|
19355
19488
|
'X-Content-Type-Options': 'nosniff',
|
|
19356
|
-
'Content-Security-Policy': "default-src 'none'; img-src data:; style-src 'unsafe-inline'; frame-ancestors 'none'",
|
|
19489
|
+
'Content-Security-Policy': "default-src 'none'; img-src data:; style-src 'unsafe-inline'; script-src 'self'; connect-src 'self'; frame-ancestors 'none'",
|
|
19357
19490
|
});
|
|
19358
19491
|
res.end(html);
|
|
19359
19492
|
});
|
|
@@ -19366,6 +19499,7 @@ async function createVibePageServer() {
|
|
|
19366
19499
|
url: `http://127.0.0.1:${address.port}${route}`,
|
|
19367
19500
|
update: (value, authorization) => {
|
|
19368
19501
|
html = value;
|
|
19502
|
+
etag = '"' + crypto.createHash('sha256').update(value).digest('hex') + '"';
|
|
19369
19503
|
landingUrl = browserLandingUrl(authorization);
|
|
19370
19504
|
},
|
|
19371
19505
|
close: () => server.close(),
|
|
@@ -19572,6 +19706,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
19572
19706
|
let ppeLane;
|
|
19573
19707
|
let ppeSelected = false;
|
|
19574
19708
|
let hybridSelected = false;
|
|
19709
|
+
let pageMetadata;
|
|
19575
19710
|
let result = {
|
|
19576
19711
|
schemaVersion: '2',
|
|
19577
19712
|
command: 'upload',
|
|
@@ -19607,20 +19742,11 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
19607
19742
|
throw new VibeError('VIBE_CANCELLED');
|
|
19608
19743
|
};
|
|
19609
19744
|
try {
|
|
19610
|
-
|
|
19611
|
-
|
|
19612
|
-
//
|
|
19613
|
-
|
|
19614
|
-
|
|
19615
|
-
throw new VibeError('VIBE_WEB_OPTION_CONFLICT');
|
|
19616
|
-
ppeSelected = true;
|
|
19617
|
-
ppeLane = process.env.TTMG_VIBE_PPE || 'ppe_op_vc';
|
|
19618
|
-
if (!/^ppe_[a-zA-Z0-9_-]{1,64}$/.test(ppeLane))
|
|
19619
|
-
throw new VibeError('VIBE_INVALID_PPE_ENV');
|
|
19620
|
-
if (deps.backend && deps.backend.mode !== 'ppe')
|
|
19621
|
-
throw new VibeError('VIBE_WEB_OPTION_CONFLICT');
|
|
19622
|
-
}
|
|
19623
|
-
else if (!options.mock && !options.vibeApi && process.env.TTMG_VIBE_BACKEND) {
|
|
19745
|
+
if (options.web)
|
|
19746
|
+
throw new VibeError('VIBE_WEB_DISABLED');
|
|
19747
|
+
// Real QR uploads use the release-owned PPE route.
|
|
19748
|
+
// Explicit test backends remain opt-in; real failures never fall back to them.
|
|
19749
|
+
if (!options.mock && !options.vibeApi && process.env.TTMG_VIBE_BACKEND) {
|
|
19624
19750
|
const configured = process.env.TTMG_VIBE_BACKEND;
|
|
19625
19751
|
if (configured === 'mock')
|
|
19626
19752
|
options = { ...options, mock: true };
|
|
@@ -19638,6 +19764,12 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
19638
19764
|
else
|
|
19639
19765
|
throw new VibeError('VIBE_BACKEND_NOT_CONFIGURED');
|
|
19640
19766
|
}
|
|
19767
|
+
else if (!options.mock && !options.vibeApi) {
|
|
19768
|
+
ppeSelected = true;
|
|
19769
|
+
ppeLane = process.env.TTMG_VIBE_PPE || 'ppe_op_vc';
|
|
19770
|
+
if (!/^ppe_[a-zA-Z0-9_-]{1,64}$/.test(ppeLane))
|
|
19771
|
+
throw new VibeError('VIBE_INVALID_PPE_ENV');
|
|
19772
|
+
}
|
|
19641
19773
|
if (!['text', 'json', 'ndjson'].includes(options.format || 'text'))
|
|
19642
19774
|
throw new VibeError('VIBE_INVALID_FORMAT');
|
|
19643
19775
|
if (options.clientKey ||
|
|
@@ -19681,10 +19813,12 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
19681
19813
|
terminal: !!options.dryRun,
|
|
19682
19814
|
nextAction: options.dryRun ? 'none' : 'wait',
|
|
19683
19815
|
title: input.title,
|
|
19816
|
+
description: (input.appInfo || VIBE_PPE_DEFAULT_APP_INFO).description,
|
|
19817
|
+
descriptionSource: options.description !== undefined ? 'arguments' : options.appInfo !== undefined ? 'app-info' : 'defaults',
|
|
19684
19818
|
classification: {
|
|
19685
19819
|
category: (input.appInfo || VIBE_PPE_DEFAULT_APP_INFO).category,
|
|
19686
19820
|
subcategory: (input.appInfo || VIBE_PPE_DEFAULT_APP_INFO).subcategory,
|
|
19687
|
-
source: options.category !== undefined ? 'arguments' :
|
|
19821
|
+
source: options.category !== undefined ? 'arguments' : options.appInfo !== undefined ? 'app-info' : 'defaults',
|
|
19688
19822
|
catalogVersion: GAME_CATEGORY_CATALOG_VERSION,
|
|
19689
19823
|
},
|
|
19690
19824
|
iconSizeBytes: input.icon.bytes.length,
|
|
@@ -19695,12 +19829,18 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
19695
19829
|
});
|
|
19696
19830
|
if (options.dryRun)
|
|
19697
19831
|
return result;
|
|
19832
|
+
const submittedInfo = input.appInfo || VIBE_PPE_DEFAULT_APP_INFO;
|
|
19833
|
+
pageMetadata = {
|
|
19834
|
+
iconDataUrl: `data:${input.icon.contentType};base64,${input.icon.bytes.toString('base64')}`,
|
|
19835
|
+
description: submittedInfo.description,
|
|
19836
|
+
category: submittedInfo.category,
|
|
19837
|
+
subcategory: submittedInfo.subcategory,
|
|
19838
|
+
};
|
|
19698
19839
|
const pageGame = {
|
|
19699
19840
|
title: input.title,
|
|
19700
|
-
|
|
19841
|
+
...pageMetadata,
|
|
19701
19842
|
uploadAvailable: backend.uploadAvailable !== false,
|
|
19702
19843
|
hybrid: backend.mode === 'ppe-mock',
|
|
19703
|
-
web: !!options.web,
|
|
19704
19844
|
};
|
|
19705
19845
|
const sourceProjectId = deps.projectId(options);
|
|
19706
19846
|
emit('creating_task', { platformWrite: backend.mode === 'ppe' || backend.mode === 'ppe-mock' });
|
|
@@ -19712,8 +19852,6 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
19712
19852
|
}, controller.signal);
|
|
19713
19853
|
ensureActive();
|
|
19714
19854
|
const authorizationUrl = createAuthorizationScheme(task.landingUrl);
|
|
19715
|
-
if (options.web && !browserLandingUrl(task.landingUrl))
|
|
19716
|
-
throw new VibeError('VIBE_INVALID_RESPONSE');
|
|
19717
19855
|
const pollMs = seconds(options.pollInterval, task.pollIntervalSeconds);
|
|
19718
19856
|
result = {
|
|
19719
19857
|
...result,
|
|
@@ -19722,33 +19860,30 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
19722
19860
|
};
|
|
19723
19861
|
page = await deps.startPage();
|
|
19724
19862
|
ensureActive();
|
|
19725
|
-
const bindingMessage =
|
|
19863
|
+
const bindingMessage = backend.mode === 'ppe-mock'
|
|
19726
19864
|
? (getCurrentLanguage() === 'zh-CN'
|
|
19727
|
-
? '
|
|
19728
|
-
: '
|
|
19729
|
-
: backend.
|
|
19730
|
-
? (getCurrentLanguage() === 'zh-CN'
|
|
19731
|
-
|
|
19732
|
-
: 'Real PPE task and authorization QR. After three polls, authorization and upload are simulated; phone authorization is not required.')
|
|
19733
|
-
: backend.uploadAvailable === false
|
|
19734
|
-
? (getCurrentLanguage() === 'zh-CN' ? '请扫码完成真实 PPE 授权;上传尚未开放。' : 'Scan to authorize in PPE. Upload is not enabled yet.')
|
|
19735
|
-
: text('waiting_authorization', result.simulation);
|
|
19865
|
+
? '真实 PPE 任务和授权二维码;查询三次后模拟授权、上传,无需完成手机授权。'
|
|
19866
|
+
: 'Real PPE task and authorization QR. After three polls, authorization and upload are simulated; phone authorization is not required.')
|
|
19867
|
+
: backend.uploadAvailable === false
|
|
19868
|
+
? (getCurrentLanguage() === 'zh-CN' ? '请扫码完成真实 PPE 授权;上传尚未开放。' : 'Scan to authorize in PPE. Upload is not enabled yet.')
|
|
19869
|
+
: text('waiting_authorization', result.simulation);
|
|
19736
19870
|
await page.update({
|
|
19737
19871
|
...pageGame,
|
|
19738
19872
|
language: getCurrentLanguage(),
|
|
19739
19873
|
phase: 'binding',
|
|
19740
19874
|
simulation: result.simulation,
|
|
19741
19875
|
message: bindingMessage,
|
|
19742
|
-
qrUrl:
|
|
19876
|
+
qrUrl: authorizationUrl,
|
|
19743
19877
|
browserLandingUrl: task.landingUrl,
|
|
19744
19878
|
purpose: 'authorization_binding',
|
|
19745
19879
|
});
|
|
19746
19880
|
ensureActive();
|
|
19747
19881
|
let browserOpenRequested = false;
|
|
19748
19882
|
let browserOpenErrorCode;
|
|
19749
|
-
|
|
19883
|
+
const opensRealPage = backend.mode === 'ppe' && options.open !== false;
|
|
19884
|
+
if (opensRealPage) {
|
|
19750
19885
|
try {
|
|
19751
|
-
await deps.open(
|
|
19886
|
+
await deps.open(page.url);
|
|
19752
19887
|
browserOpenRequested = true;
|
|
19753
19888
|
}
|
|
19754
19889
|
catch {
|
|
@@ -19758,19 +19893,19 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
19758
19893
|
}
|
|
19759
19894
|
emit('waiting_authorization', {
|
|
19760
19895
|
message: browserOpenErrorCode
|
|
19761
|
-
? (getCurrentLanguage() === 'zh-CN' ? '
|
|
19896
|
+
? (getCurrentLanguage() === 'zh-CN' ? '二维码页面未能打开,请在 CLI 所在电脑打开 pageUrl 并使用 TikTok 扫码;当前任务仍在等待授权。' : 'Could not open the QR page. Open pageUrl on the CLI computer and scan with TikTok; this task is still waiting for authorization.')
|
|
19762
19897
|
: bindingMessage,
|
|
19763
19898
|
pageUrl: page.url,
|
|
19764
|
-
authorizationMode:
|
|
19765
|
-
...(
|
|
19899
|
+
authorizationMode: 'qr',
|
|
19900
|
+
...(backend.mode === 'ppe' ? { browserOpenRequested, browserOpenErrorCode } : {}),
|
|
19766
19901
|
authorizationOpenCommand: browserLandingUrl(task.landingUrl)
|
|
19767
19902
|
? `ttmg auth open --page-url ${page.url} --format json` : undefined,
|
|
19768
|
-
qrPurpose:
|
|
19769
|
-
nextAction: result.simulation ? 'wait' :
|
|
19903
|
+
qrPurpose: 'authorization_binding',
|
|
19904
|
+
nextAction: result.simulation ? 'wait' : 'scan_qr',
|
|
19770
19905
|
readyForInput: false,
|
|
19771
19906
|
});
|
|
19772
19907
|
// Opening a browser is presentation only and never gates token polling.
|
|
19773
|
-
if (!
|
|
19908
|
+
if (!opensRealPage && backend.mode !== 'ppe' && (options.format || 'text') === 'text' && options.open !== false)
|
|
19774
19909
|
void deps.open(page.url).catch(() => { });
|
|
19775
19910
|
while (true) {
|
|
19776
19911
|
ensureActive();
|
|
@@ -19880,7 +20015,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
19880
20015
|
const messages = {
|
|
19881
20016
|
VIBE_CATEGORY_PAIR_REQUIRED: ['--category 和 --subcategory 必须同时提供;均不传时保留默认分类。使用 ttmg game categories --format json 查询。', 'Provide --category and --subcategory together, or omit both to keep the default. Query ttmg game categories --format json.'],
|
|
19882
20017
|
VIBE_INVALID_CATEGORY: ['类型或子类型无效,或两者不匹配。使用 ttmg game categories --format json 查询有效组合。', 'Invalid category/subcategory pair. Query valid values with ttmg game categories --format json.'],
|
|
19883
|
-
|
|
20018
|
+
VIBE_INVALID_DESCRIPTION: ['描述必须是非空的有效 Unicode 文本;省略 --description 可使用默认描述。', 'Description must be non-empty valid Unicode text; omit --description to use the default.'],
|
|
19884
20019
|
VIBE_AUTH_DENIED: [
|
|
19885
20020
|
'已取消授权,本次未上传。',
|
|
19886
20021
|
'Authorization cancelled. No upload was performed.',
|
|
@@ -19912,6 +20047,10 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
19912
20047
|
'请检查本地 ZIP 代码包。',
|
|
19913
20048
|
'Check the local ZIP package.',
|
|
19914
20049
|
],
|
|
20050
|
+
VIBE_WEB_DISABLED: [
|
|
20051
|
+
'网页授权已关闭。请移除 --web,使用 ttmg upload --vibe 并在手机 TikTok 中扫码授权。',
|
|
20052
|
+
'Web authorization is disabled. Remove --web and use ttmg upload --vibe to scan with TikTok on your phone.',
|
|
20053
|
+
],
|
|
19915
20054
|
VIBE_BACKEND_NOT_CONFIGURED: [
|
|
19916
20055
|
'尚未配置服务;本地模拟请使用 --mock。',
|
|
19917
20056
|
'No backend configured. Use --mock for local simulation.',
|
|
@@ -19961,6 +20100,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
19961
20100
|
if (page && code !== 'VIBE_CANCELLED') {
|
|
19962
20101
|
try {
|
|
19963
20102
|
await page.update({
|
|
20103
|
+
...pageMetadata,
|
|
19964
20104
|
title: result.title || '',
|
|
19965
20105
|
language: getCurrentLanguage(),
|
|
19966
20106
|
simulation: result.simulation,
|
|
@@ -31195,12 +31335,17 @@ async function snapshotWasm(input, output) {
|
|
|
31195
31335
|
callback(bytes > 512 * 1024 * 1024 ? fail('WASM_INPUT_INVALID', 'Expanded Wasm exceeds 512 MiB.') : null, chunk);
|
|
31196
31336
|
} });
|
|
31197
31337
|
const source = fs$1.createReadStream(input);
|
|
31338
|
+
const fileMd5 = crypto$1.createHash('md5');
|
|
31339
|
+
const identity = new node_stream.Transform({ transform(chunk, _encoding, callback) {
|
|
31340
|
+
fileMd5.update(chunk);
|
|
31341
|
+
callback(null, chunk);
|
|
31342
|
+
} });
|
|
31198
31343
|
const target = fs$1.createWriteStream(output, { flags: 'wx', mode: 0o600 });
|
|
31199
31344
|
try {
|
|
31200
31345
|
if (input.endsWith('.br'))
|
|
31201
|
-
await promises$2.pipeline(source, zlib$1.createBrotliDecompress(), bound, target);
|
|
31346
|
+
await promises$2.pipeline(source, identity, zlib$1.createBrotliDecompress(), bound, target);
|
|
31202
31347
|
else
|
|
31203
|
-
await promises$2.pipeline(source, bound, target);
|
|
31348
|
+
await promises$2.pipeline(source, identity, bound, target);
|
|
31204
31349
|
}
|
|
31205
31350
|
catch (error) {
|
|
31206
31351
|
throw fail(error.code === 'WASM_INPUT_INVALID' ? error.code : 'WASM_INPUT_INVALID', 'Could not read the original Wasm input.');
|
|
@@ -31217,7 +31362,11 @@ async function snapshotWasm(input, output) {
|
|
|
31217
31362
|
throw fail('WASM_INPUT_INVALID', 'Expected a WebAssembly version 1 binary.');
|
|
31218
31363
|
if (!WebAssembly.validate(await fsp.readFile(output)))
|
|
31219
31364
|
throw fail('WASM_INPUT_INVALID', 'Original Wasm is invalid or unsupported by this Node.js WebAssembly engine.');
|
|
31220
|
-
|
|
31365
|
+
const rawMd5 = crypto$1.createHash('md5');
|
|
31366
|
+
for await (const chunk of fs$1.createReadStream(output))
|
|
31367
|
+
rawMd5.update(chunk);
|
|
31368
|
+
return { bytes, wasmSha256: await digest(output), wasmMd5: fileMd5.digest('hex'),
|
|
31369
|
+
uncompressedWasmMd5: rawMd5.digest('hex') };
|
|
31221
31370
|
}
|
|
31222
31371
|
async function executeWasm(operation, options, dependencies = {}) {
|
|
31223
31372
|
const base = { schemaVersion: '1', command: `wasm.${operation}`, operationId: `wasm_${crypto$1.randomUUID()}`,
|
|
@@ -31325,6 +31474,149 @@ async function wasmCommand(operation, options) {
|
|
|
31325
31474
|
process.exitCode = result.status === 'failed' ? 1 : 0;
|
|
31326
31475
|
}
|
|
31327
31476
|
|
|
31477
|
+
class CollectError extends Error {
|
|
31478
|
+
constructor(code, message) {
|
|
31479
|
+
super(message);
|
|
31480
|
+
this.code = code;
|
|
31481
|
+
}
|
|
31482
|
+
}
|
|
31483
|
+
const invalid = (code, message) => { throw new CollectError(code, message); };
|
|
31484
|
+
async function executeWasmCollect(operation, options, overrides = {}) {
|
|
31485
|
+
const deps = { authenticated: () => Boolean(getCurrentUser()?.cookie), request, ...overrides };
|
|
31486
|
+
const base = { schemaVersion: '1', command: `wasm.collect.${operation}`,
|
|
31487
|
+
operationId: `wasm_collect_${crypto$1.randomUUID()}`, platformWrite: operation === 'start',
|
|
31488
|
+
platformWriteAttempted: false, sourceModified: false };
|
|
31489
|
+
let temporary;
|
|
31490
|
+
let staging;
|
|
31491
|
+
let timer;
|
|
31492
|
+
try {
|
|
31493
|
+
const timeout = Number(options.timeout ?? 30);
|
|
31494
|
+
if (!['start', 'export'].includes(operation) || !['json', 'text'].includes(options.format || 'json') ||
|
|
31495
|
+
!/^[A-Za-z0-9_-]{1,128}$/.test(options.clientKey || '') || !/^[a-fA-F0-9]{32}$/.test(options.wasmMd5 || '') ||
|
|
31496
|
+
!Number.isInteger(timeout) || timeout < 1 || timeout > 300 ||
|
|
31497
|
+
(options.reset !== undefined && typeof options.reset !== 'boolean') ||
|
|
31498
|
+
(operation === 'export' && (!options.input || !options.output || options.reset !== undefined)) ||
|
|
31499
|
+
(operation === 'start' && (options.input || options.output))) {
|
|
31500
|
+
invalid('WASM_COLLECT_INVALID_OPTIONS', 'Provide client-key and wasm-md5; export also requires original input and a new output file. Timeout: 1–300 seconds.');
|
|
31501
|
+
}
|
|
31502
|
+
const clientKey = options.clientKey;
|
|
31503
|
+
const wasmMd5 = options.wasmMd5.toLowerCase();
|
|
31504
|
+
const output = options.output ? path$1.resolve(options.output) : undefined;
|
|
31505
|
+
let source;
|
|
31506
|
+
if (operation === 'export') {
|
|
31507
|
+
// lstat detects dangling links as well as existing files. Never overwrite.
|
|
31508
|
+
if (await fsp.lstat(output).catch((e) => { if (e.code !== 'ENOENT')
|
|
31509
|
+
throw e; return null; }))
|
|
31510
|
+
invalid('WASM_OUTPUT_EXISTS', 'Collection output already exists; choose a new file.');
|
|
31511
|
+
temporary = await fsp.mkdtemp(path$1.join(os$1.tmpdir(), 'ttmg-collect-'));
|
|
31512
|
+
try {
|
|
31513
|
+
source = await snapshotWasm(path$1.resolve(options.input), path$1.join(temporary, 'original.wasm'));
|
|
31514
|
+
}
|
|
31515
|
+
catch {
|
|
31516
|
+
return invalid('WASM_INPUT_INVALID', 'Expected a valid original Wasm or Brotli Wasm file.');
|
|
31517
|
+
}
|
|
31518
|
+
if (wasmMd5 !== source.wasmMd5 && wasmMd5 !== source.uncompressedWasmMd5)
|
|
31519
|
+
invalid('WASM_COLLECTION_MISMATCH', 'Collection MD5 does not match this original Wasm (file bytes or uncompressed bytes).');
|
|
31520
|
+
}
|
|
31521
|
+
if (!deps.authenticated())
|
|
31522
|
+
invalid('WASM_AUTH_REQUIRED', 'Run ttmg login on this machine before collecting or exporting.');
|
|
31523
|
+
const controller = new AbortController();
|
|
31524
|
+
const deadline = new Promise((_, reject) => {
|
|
31525
|
+
timer = setTimeout(() => {
|
|
31526
|
+
controller.abort();
|
|
31527
|
+
reject(new CollectError('WASM_COLLECT_TIMEOUT', 'Platform request timed out. A start may have reached the server; retry without --reset to preserve collection.'));
|
|
31528
|
+
}, timeout * 1000);
|
|
31529
|
+
});
|
|
31530
|
+
base.platformWriteAttempted = operation === 'start';
|
|
31531
|
+
const response = await Promise.race([deps.request({
|
|
31532
|
+
url: `${WASM_COLLECT_BASE_URL}/${operation === 'start' ? 'start' : 'export'}`,
|
|
31533
|
+
method: operation === 'start' ? 'POST' : 'GET',
|
|
31534
|
+
...(operation === 'start'
|
|
31535
|
+
? { data: { app_id: clientKey, wasm_md5: wasmMd5, reset: options.reset ?? false } }
|
|
31536
|
+
: { params: { app_id: clientKey, wasm_md5: wasmMd5, strategy: 'union' } }),
|
|
31537
|
+
signal: controller.signal,
|
|
31538
|
+
logRequestBody: false, logApiCalls: false,
|
|
31539
|
+
}), deadline]);
|
|
31540
|
+
clearTimeout(timer);
|
|
31541
|
+
const code = response.error?.code ?? response.error?.StatusCode ?? response.data?.code;
|
|
31542
|
+
const httpStatus = response.ctx?.httpStatusCode;
|
|
31543
|
+
// Upstream bodies/errors are deliberately never included in output: they
|
|
31544
|
+
// can contain credentials or gateway HTML, even with verbose enabled.
|
|
31545
|
+
if ([401, -401].includes(Number(code)) || httpStatus === 401 ||
|
|
31546
|
+
(Number(code) === -1 && /login|log.?in|登录|未登|auth/i.test(String(response.error?.message ?? response.error?.StatusMessage ?? response.data?.message ?? ''))))
|
|
31547
|
+
invalid('WASM_AUTH_REQUIRED', 'Platform login expired. Run ttmg login and retry.');
|
|
31548
|
+
if ([403, -403].includes(Number(code)) || httpStatus === 403)
|
|
31549
|
+
invalid('WASM_COLLECT_FORBIDDEN', 'This account cannot access collection for this game.');
|
|
31550
|
+
if (response.error || response.data?.code !== 0 || (httpStatus && (httpStatus < 200 || httpStatus >= 300)))
|
|
31551
|
+
invalid('WASM_COLLECT_API_FAILED', 'Platform collection request failed; no collection file was written.');
|
|
31552
|
+
if (operation === 'start') {
|
|
31553
|
+
if (response.data.collect_state !== 'open')
|
|
31554
|
+
invalid('WASM_COLLECT_INVALID_RESPONSE', 'Platform did not confirm an open collection window.');
|
|
31555
|
+
return { ...base, status: 'succeeded', clientKey, wasmMd5, reset: options.reset ?? false,
|
|
31556
|
+
collectState: 'open', nextAction: 'run_instrumented_game_and_collect', errorCode: null, error: null };
|
|
31557
|
+
}
|
|
31558
|
+
if (Array.isArray(response.data.func_ids) && response.data.func_ids.length === 0)
|
|
31559
|
+
invalid('WASM_COLLECTION_EMPTY', 'No function IDs collected. Run the instrumented game and export again.');
|
|
31560
|
+
let ids;
|
|
31561
|
+
try {
|
|
31562
|
+
ids = parseCollection({ schemaVersion: '1', wasmSha256: source.wasmSha256,
|
|
31563
|
+
funcIds: response.data.func_ids, bootFuncIds: response.data.boot_func_ids }, source.wasmSha256);
|
|
31564
|
+
}
|
|
31565
|
+
catch {
|
|
31566
|
+
return invalid('WASM_COLLECTION_INVALID', 'Platform returned invalid function IDs; no collection file was written.');
|
|
31567
|
+
}
|
|
31568
|
+
const collection = { schemaVersion: '1', wasmSha256: source.wasmSha256, ...ids,
|
|
31569
|
+
clientKey, wasmMd5, strategy: 'union', exportedAt: new Date().toISOString() };
|
|
31570
|
+
const bytes = Buffer.from(JSON.stringify(collection, null, 2) + '\n');
|
|
31571
|
+
if (bytes.length > 32 * 1024 * 1024)
|
|
31572
|
+
invalid('WASM_COLLECTION_INVALID', 'Collection JSON exceeds 32 MiB.');
|
|
31573
|
+
await fsp.mkdir(path$1.dirname(output), { recursive: true });
|
|
31574
|
+
staging = path$1.join(path$1.dirname(output), `.ttmg-collection-${crypto$1.randomUUID()}.tmp`);
|
|
31575
|
+
await fsp.writeFile(staging, bytes, { flag: 'wx', mode: 0o600 });
|
|
31576
|
+
// Publish a complete file atomically, with exclusive no-clobber semantics.
|
|
31577
|
+
try {
|
|
31578
|
+
await fsp.link(staging, output);
|
|
31579
|
+
}
|
|
31580
|
+
catch (e) {
|
|
31581
|
+
if (e.code === 'EEXIST')
|
|
31582
|
+
invalid('WASM_OUTPUT_EXISTS', 'Collection output already exists; choose a new file.');
|
|
31583
|
+
throw e;
|
|
31584
|
+
}
|
|
31585
|
+
return { ...base, status: 'succeeded', clientKey, wasmMd5,
|
|
31586
|
+
collection: { path: output, schemaVersion: '1', wasmSha256: source.wasmSha256,
|
|
31587
|
+
funcCount: ids.funcIds.length, bootFuncCount: ids.bootFuncIds.length,
|
|
31588
|
+
bytes: bytes.length, sha256: crypto$1.createHash('sha256').update(bytes).digest('hex') },
|
|
31589
|
+
nextAction: 'split_with_collection', errorCode: null, error: null };
|
|
31590
|
+
}
|
|
31591
|
+
catch (error) {
|
|
31592
|
+
const known = error instanceof CollectError;
|
|
31593
|
+
return { ...base, status: 'failed', errorCode: known ? error.code : 'WASM_COLLECT_FAILED',
|
|
31594
|
+
error: known ? error.message : 'Collection operation failed; check the original Wasm, output directory and connection.',
|
|
31595
|
+
nextAction: known && error.code === 'WASM_AUTH_REQUIRED' ? 'login' : 'fix_and_retry' };
|
|
31596
|
+
}
|
|
31597
|
+
finally {
|
|
31598
|
+
if (timer)
|
|
31599
|
+
clearTimeout(timer);
|
|
31600
|
+
if (staging)
|
|
31601
|
+
await fsp.unlink(staging).catch(() => { });
|
|
31602
|
+
if (temporary)
|
|
31603
|
+
await fsp.rm(temporary, { recursive: true, force: true });
|
|
31604
|
+
}
|
|
31605
|
+
}
|
|
31606
|
+
async function wasmCollectCommand(operation, options) {
|
|
31607
|
+
const result = await executeWasmCollect(operation, options);
|
|
31608
|
+
if ((options.format || 'json') === 'json')
|
|
31609
|
+
console.log(JSON.stringify(result));
|
|
31610
|
+
else {
|
|
31611
|
+
console.log(t(result.status === 'failed' ? 'wasm.collect.failed' : 'wasm.collect.succeeded'));
|
|
31612
|
+
if (result.error)
|
|
31613
|
+
console.error(result.error);
|
|
31614
|
+
if ('collection' in result)
|
|
31615
|
+
console.log(result.collection.path);
|
|
31616
|
+
}
|
|
31617
|
+
process.exitCode = result.status === 'failed' ? 1 : 0;
|
|
31618
|
+
}
|
|
31619
|
+
|
|
31328
31620
|
const problem = (code, message) => Object.assign(new Error(message), { code });
|
|
31329
31621
|
const MAX_BYTES = 64 * 1024 * 1024;
|
|
31330
31622
|
// Export one finite Runtime interval. Sequence numbers are global, so gaps in
|
|
@@ -31528,6 +31820,7 @@ var index = /*#__PURE__*/Object.freeze({
|
|
|
31528
31820
|
devSessionStopCommand: devSessionStopCommand,
|
|
31529
31821
|
devWorkflowCommand: devWorkflowCommand,
|
|
31530
31822
|
doctor: doctor,
|
|
31823
|
+
wasmCollectCommand: wasmCollectCommand,
|
|
31531
31824
|
wasmCommand: wasmCommand
|
|
31532
31825
|
});
|
|
31533
31826
|
//# sourceMappingURL=index.js.map
|