@ttmg/cli 0.4.6-vibe-beta.14 → 0.4.6-vibe-beta.15
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 +68 -3
- package/dist/index.js +251 -13
- package/dist/index.js.map +1 -1
- package/dist/package.json +2 -2
- package/dist/scripts/verify-agent-contract.js +13 -0
- package/dist/scripts/vibe-upload-smoke.js +28 -3
- 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 +43 -3
package/README.md
CHANGED
|
@@ -9,6 +9,10 @@ Vibe real web authorization and upload: use `--web` with the title, local icon a
|
|
|
9
9
|
|
|
10
10
|
#### Installation 安装
|
|
11
11
|
|
|
12
|
+
`0.4.6-vibe-beta.15`:Vibe 上传支持 Skill 传入 `--description "游戏描述"`。省略时默认使用 `Discover the fun of TikTok Mini Games.`;旧 `--app-info` 文件中的描述继续保留。详见 [Quick Start 与描述参数](vibe-upload.md)。
|
|
13
|
+
|
|
14
|
+
`0.4.6-vibe-beta.15`: Vibe upload accepts `--description "Game description"` from a Skill. When omitted, it uses `Discover the fun of TikTok Mini Games.` or preserves the description from a legacy `--app-info` file. See [Quick Start and description usage](vibe-upload.md).
|
|
15
|
+
|
|
12
16
|
`0.4.6-vibe-beta.14` 支持分类查询与可选上传分类参数。Skill 可用 `ttmg game categories --format json` 查询,再向 Vibe 上传传入 `--category Puzzle --subcategory Physics`。两个参数都省略时保留 Other/Other;详见 [分类用法](vibe-upload.md)。
|
|
13
17
|
|
|
14
18
|
`0.4.6-vibe-beta.14` supports category discovery and optional upload classification. Query `ttmg game categories --format json`, then pass `--category Puzzle --subcategory Physics` to Vibe upload. Omitting both preserves Other/Other. See [category usage](vibe-upload.md).
|
|
@@ -30,7 +34,7 @@ For Agent and Monetization workflows, the published beta is:
|
|
|
30
34
|
Agent、商业化流程的已发布 beta 为:
|
|
31
35
|
|
|
32
36
|
```bash
|
|
33
|
-
npm install -g @ttmg/cli@0.4.6-agent-beta.
|
|
37
|
+
npm install -g @ttmg/cli@0.4.6-agent-beta.3
|
|
34
38
|
ttmg --version
|
|
35
39
|
ttmg capabilities --format json
|
|
36
40
|
```
|
|
@@ -237,9 +241,70 @@ ttmg dev session stop <SESSION_ID> --format json
|
|
|
237
241
|
|
|
238
242
|
### Wasm prepare and split / Wasm 准备与分包
|
|
239
243
|
|
|
244
|
+
#### Platform collection commands (agent-beta.3) / 平台采集命令
|
|
245
|
+
|
|
246
|
+
Install `0.4.6-agent-beta.3` and check `commands.wasmCollect` in `ttmg capabilities`.
|
|
247
|
+
The CLI uses its own `ttmg login` session; do not pass or read Cookie values in
|
|
248
|
+
Agent prompts, arguments or local HTTP requests. No running DevTool server is required.
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
ttmg wasm prepare --input original.wasm.br --output .ttmg/wasm/prepared --format json
|
|
252
|
+
# Use prepare's input.wasmMd5 for <MD5> and configure the instrumented game
|
|
253
|
+
# to report that same original MD5 and Client Key.
|
|
254
|
+
ttmg wasm collect start --client-key <CLIENT_KEY> --wasm-md5 <MD5> --format json
|
|
255
|
+
# Integrate the prepared copy, scan, clear device storage and play the game.
|
|
256
|
+
ttmg wasm collect export --client-key <CLIENT_KEY> --wasm-md5 <MD5> \
|
|
257
|
+
--input original.wasm.br --output .ttmg/wasm/collection.json --format json
|
|
258
|
+
ttmg wasm split --input original.wasm.br --collection .ttmg/wasm/collection.json --output .ttmg/wasm/split --format json
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Run these examples at the game project root. Keep collection and temporary
|
|
262
|
+
artifacts outside the project or under its root `.ttmg`; an ordinary JSON file
|
|
263
|
+
in game assets can be included in future builds and uploads.
|
|
264
|
+
`start` opens or resumes a window and preserves existing IDs by default.
|
|
265
|
+
`--reset` clears platform collection for this game and MD5; it does not clear
|
|
266
|
+
phone storage. On an uncertain start or timeout, retry without `--reset`.
|
|
267
|
+
`export` reads a union snapshot without closing the window. It needs the original
|
|
268
|
+
Wasm, validates the collection MD5 against its file bytes or uncompressed bytes,
|
|
269
|
+
and writes schema 1 with the uncompressed `wasmSha256`, `funcIds` and `bootFuncIds`.
|
|
270
|
+
Read the file at the returned `collection.path`. Existing files are rejected;
|
|
271
|
+
use a new output name for each snapshot. Credentials and raw API responses are
|
|
272
|
+
omitted even with `--verbose`.
|
|
273
|
+
|
|
274
|
+
Only `start` writes platform state. `export` only reads collection and writes a
|
|
275
|
+
local file. Neither command changes Unity files or clears device storage.
|
|
276
|
+
Platform requests default to 30 seconds (`--timeout`, 1–300). Missing or expired
|
|
277
|
+
login returns `WASM_AUTH_REQUIRED`; empty collection returns `WASM_COLLECTION_EMPTY`;
|
|
278
|
+
an unmatched original returns `WASM_COLLECTION_MISMATCH`. No file is published on
|
|
279
|
+
these failures. A nonempty collection does not prove gameplay coverage.
|
|
280
|
+
The commands reuse the existing platform request environment; local fixture
|
|
281
|
+
tests do not establish real platform or device acceptance.
|
|
282
|
+
|
|
283
|
+
安装 `0.4.6-agent-beta.3`,通过 `ttmg capabilities` 中的 `commands.wasmCollect` 确认支持。
|
|
284
|
+
CLI 内部复用 `ttmg login` 登录态,无需运行 DevTool 本地服务,Agent 不读取或传递 Cookie。
|
|
285
|
+
按上方命令执行:prepare → 开采集窗口 → 接入插桩副本并扫码、清存储、玩游戏 → export → split。
|
|
286
|
+
`<MD5>` 使用 prepare 返回的 `input.wasmMd5`,插桩游戏须上报相同的原始 MD5 和 Client Key。
|
|
287
|
+
|
|
288
|
+
示例在游戏工程根目录执行。采集文件和临时产物应放工程外或根目录 `.ttmg` 下;
|
|
289
|
+
放在普通游戏资源目录的 JSON 会参与后续构建和上传。
|
|
290
|
+
`start` 默认保留已有函数 ID;只有 `--reset` 清空平台上此游戏、此 MD5 的历史采集,
|
|
291
|
+
不会清理手机存储。超时或不确定是否成功时,不带 `--reset` 重试。
|
|
292
|
+
`export` 读取当前合集快照,不关闭采集窗口;必须提供原始 Wasm,命令校验文件字节或
|
|
293
|
+
解压后字节的 MD5,再写入 schema 1、解压后原始 `wasmSha256`、`funcIds` 和 `bootFuncIds`。
|
|
294
|
+
从返回的 `collection.path` 读取文件;已有文件不覆盖,每轮快照使用新文件名。
|
|
295
|
+
即使开启 `--verbose`,也不输出登录凭证或原始接口响应。
|
|
296
|
+
|
|
297
|
+
仅 `start` 写平台状态;`export` 只读平台并写本地文件。两者均不写回 Unity 工程或清理手机存储。
|
|
298
|
+
请求默认超时 30 秒,`--timeout` 范围为 1–300 秒。未登录或登录过期返回 `WASM_AUTH_REQUIRED`,
|
|
299
|
+
空采集返回 `WASM_COLLECTION_EMPTY`,原始文件不匹配返回 `WASM_COLLECTION_MISMATCH`;
|
|
300
|
+
这些失败不会生成采集文件。非空结果不等于玩法覆盖完整。
|
|
301
|
+
命令沿用现有平台请求环境;本地模拟测试不代表真实平台或真机验收。
|
|
302
|
+
|
|
303
|
+
#### Local artifacts / 本地产物
|
|
304
|
+
|
|
240
305
|
```bash
|
|
241
|
-
ttmg wasm prepare --input original.wasm.br --output prepared --format json
|
|
242
|
-
ttmg wasm split --input original.wasm.br --collection collection.json --output split --format json
|
|
306
|
+
ttmg wasm prepare --input original.wasm.br --output .ttmg/wasm/prepared --format json
|
|
307
|
+
ttmg wasm split --input original.wasm.br --collection .ttmg/wasm/collection.json --output .ttmg/wasm/split --format json
|
|
243
308
|
```
|
|
244
309
|
|
|
245
310
|
These experimental commands create artifacts in a new directory; they do not
|
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)',
|
|
@@ -7239,12 +7249,14 @@ const messages = {
|
|
|
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
7255
|
Vibe web upload (real PPE trial):
|
|
7245
7256
|
ttmg upload --vibe --web --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.
|
|
@@ -7645,6 +7657,16 @@ Vibe web upload (real PPE trial):
|
|
|
7645
7657
|
'zh-CN': {
|
|
7646
7658
|
'cli.command.wasm.desc': '准备或分包 Wasm,产物写入独立目录',
|
|
7647
7659
|
'cli.command.wasm.prepare': '对原始 Wasm 插桩,用于函数采集',
|
|
7660
|
+
'cli.command.wasm.collect': '复用 CLI 本机登录态管理平台函数采集',
|
|
7661
|
+
'cli.command.wasm.collect.start': '开启或恢复采集窗口;未指定 --reset 时保留已有函数 ID',
|
|
7662
|
+
'cli.command.wasm.collect.export': '导出函数 ID,生成与原始 Wasm 匹配的 collection.json',
|
|
7663
|
+
'cli.command.wasm.collect.clientKey': '采集游戏的 Client Key',
|
|
7664
|
+
'cli.command.wasm.collect.md5': '插桩游戏上报使用的原始 Wasm 采集 MD5',
|
|
7665
|
+
'cli.command.wasm.collect.timeout': '平台请求超时:1–300 秒',
|
|
7666
|
+
'cli.command.wasm.collect.reset': '清空此游戏、此 Wasm 的历史函数 ID,再开始采集',
|
|
7667
|
+
'cli.command.wasm.collect.output': '新的采集 JSON 文件(不覆盖已有文件)',
|
|
7668
|
+
'wasm.collect.succeeded': 'Wasm 采集命令已完成。',
|
|
7669
|
+
'wasm.collect.failed': 'Wasm 采集命令失败。',
|
|
7648
7670
|
'cli.command.wasm.split': '使用与原始 Wasm 匹配的采集数据分包',
|
|
7649
7671
|
'cli.command.wasm.input': '原始 .wasm 或 Brotli .wasm.br 文件',
|
|
7650
7672
|
'cli.command.wasm.output': '新的输出目录(不覆盖已有内容)',
|
|
@@ -7674,12 +7696,14 @@ Vibe web upload (real PPE trial):
|
|
|
7674
7696
|
'cli.command.game.categories.unknown': '类型不存在。使用 ttmg game categories --format json 查询有效值。',
|
|
7675
7697
|
'cli.command.game.categories.invalidFormat': '输出格式请使用 text、json 或 ndjson。',
|
|
7676
7698
|
'cli.command.upload.vibeCategory': '可选游戏类型,须与 --subcategory 同传(--vibe);用 ttmg game categories --format json 查询',
|
|
7699
|
+
'cli.command.upload.vibeDescription': '可选游戏描述(--vibe);默认值:Discover the fun of TikTok Mini Games.',
|
|
7677
7700
|
'cli.command.upload.vibeSubcategory': '所属类型的可选子类型;两者均不传保留 Other/Other(--vibe)',
|
|
7678
7701
|
'cli.command.upload.vibeWebGuide': `
|
|
7679
7702
|
Vibe 网页上传(真实 PPE 试用):
|
|
7680
7703
|
ttmg upload --vibe --web --dir ./game --title "游戏名称" --icon ./avatar.png --format ndjson
|
|
7681
7704
|
已有 ZIP 时,用 --package ./game.zip 替换 --dir。
|
|
7682
7705
|
只收集标题和头像;其他 app_info 字段由 CLI 使用 PPE 试用默认值补齐。
|
|
7706
|
+
Skill 可传 --description "游戏描述";省略时使用 Discover the fun of TikTok Mini Games.(旧资料文件中的描述保留)。
|
|
7683
7707
|
Skill 可先查 ttmg game categories --format json,再成对传入 --category Puzzle --subcategory Physics。
|
|
7684
7708
|
两者均不传保留 Other/Other(旧资料文件调用保留文件分类);无效组合在创建任务前报错。
|
|
7685
7709
|
prepared 和终态事件返回最终 classification 及其来源。
|
|
@@ -16352,7 +16376,7 @@ async function authStatus(options = {}) {
|
|
|
16352
16376
|
return result;
|
|
16353
16377
|
}
|
|
16354
16378
|
|
|
16355
|
-
var version = "0.4.6-vibe-beta.
|
|
16379
|
+
var version = "0.4.6-vibe-beta.15";
|
|
16356
16380
|
var engines = {
|
|
16357
16381
|
node: ">=20.19.1 <21 || >=22"
|
|
16358
16382
|
};
|
|
@@ -16362,13 +16386,15 @@ var pkg = {
|
|
|
16362
16386
|
};
|
|
16363
16387
|
|
|
16364
16388
|
// Skill-facing v2 contract. Backend credentials never cross this boundary.
|
|
16389
|
+
const VIBE_DEFAULT_DESCRIPTION = 'Discover the fun of TikTok Mini Games.';
|
|
16365
16390
|
const VIBE_INPUT_REQUIREMENTS = {
|
|
16366
16391
|
requiredUserInputs: ['title', 'icon'],
|
|
16367
16392
|
codeSource: ['dir', 'package'],
|
|
16368
16393
|
additionalMetadataSource: 'cli-defaults',
|
|
16369
16394
|
defaultMetadataProfile: 'ppe-trial',
|
|
16370
16395
|
requestAdditionalMetadata: false,
|
|
16371
|
-
optionalSkillInputs: ['category', 'subcategory'],
|
|
16396
|
+
optionalSkillInputs: ['category', 'subcategory', 'description'],
|
|
16397
|
+
defaultDescription: VIBE_DEFAULT_DESCRIPTION,
|
|
16372
16398
|
categoryQuery: 'ttmg game categories --format json',
|
|
16373
16399
|
categoryPairRequiredWhenProvided: true,
|
|
16374
16400
|
defaultCategory: { category: 'Other', subcategory: 'Other' },
|
|
@@ -16963,9 +16989,10 @@ var categories = /*#__PURE__*/Object.freeze({
|
|
|
16963
16989
|
|
|
16964
16990
|
// Current PPE trial defaults: the owner-approved metadata used by the successful
|
|
16965
16991
|
// real upload fixture on 2026-09-14, with owner-specified legal URLs updated on
|
|
16966
|
-
// 2026-09-15
|
|
16992
|
+
// 2026-09-15 and the owner-approved brand description on 2026-09-16.
|
|
16993
|
+
// This is not a production metadata profile.
|
|
16967
16994
|
const VIBE_PPE_DEFAULT_APP_INFO = Object.freeze({
|
|
16968
|
-
description:
|
|
16995
|
+
description: VIBE_DEFAULT_DESCRIPTION,
|
|
16969
16996
|
category: 'Other',
|
|
16970
16997
|
subcategory: 'Other',
|
|
16971
16998
|
terms_of_service: 'https://www.tiktok.com',
|
|
@@ -16979,6 +17006,14 @@ const requiredStrings = [
|
|
|
16979
17006
|
'description', 'category', 'subcategory', 'terms_of_service', 'privacy_policy',
|
|
16980
17007
|
];
|
|
16981
17008
|
const optionalStrings = ['apple_team_id', 'contact_email', 'region', 'min_sdk_version'];
|
|
17009
|
+
function applyVibeDescription(description, appInfo) {
|
|
17010
|
+
if (description === undefined)
|
|
17011
|
+
return appInfo;
|
|
17012
|
+
if (typeof description !== 'string' || !description.trim() ||
|
|
17013
|
+
[...description].some(char => /^[\uD800-\uDFFF]$/.test(char)))
|
|
17014
|
+
throw new VibeError('VIBE_INVALID_DESCRIPTION');
|
|
17015
|
+
return { ...(appInfo || VIBE_PPE_DEFAULT_APP_INFO), description: description.trim() };
|
|
17016
|
+
}
|
|
16982
17017
|
function applyVibeCategory(options, appInfo) {
|
|
16983
17018
|
if (options.category === undefined && options.subcategory === undefined)
|
|
16984
17019
|
return appInfo;
|
|
@@ -17133,6 +17168,15 @@ function getCapabilities() {
|
|
|
17133
17168
|
sourceProjectIdentity: '.ttmg/vibe-project.json',
|
|
17134
17169
|
metadataInputs: ['title', 'icon', 'dir|package'],
|
|
17135
17170
|
inputRequirements: VIBE_INPUT_REQUIREMENTS,
|
|
17171
|
+
description: {
|
|
17172
|
+
option: '--description',
|
|
17173
|
+
required: false,
|
|
17174
|
+
default: VIBE_PPE_DEFAULT_APP_INFO.description,
|
|
17175
|
+
precedence: ['argument', 'legacy-app-info', 'default'],
|
|
17176
|
+
invalidInput: 'reject-before-task-creation',
|
|
17177
|
+
resultField: 'description',
|
|
17178
|
+
sourceField: 'descriptionSource',
|
|
17179
|
+
},
|
|
17136
17180
|
classification: {
|
|
17137
17181
|
queryCommand: 'ttmg game categories --format json',
|
|
17138
17182
|
options: ['--category', '--subcategory'],
|
|
@@ -17270,6 +17314,20 @@ function getCapabilities() {
|
|
|
17270
17314
|
projectIntegration: false,
|
|
17271
17315
|
platformWrite: false,
|
|
17272
17316
|
},
|
|
17317
|
+
wasmCollect: {
|
|
17318
|
+
available: true,
|
|
17319
|
+
experimental: true,
|
|
17320
|
+
authentication: 'local_cli_login',
|
|
17321
|
+
credentialOutput: false,
|
|
17322
|
+
nativeBindingRequired: false,
|
|
17323
|
+
platformVerified: false,
|
|
17324
|
+
outputFormats: ['text', 'json'],
|
|
17325
|
+
start: { command: 'ttmg wasm collect start', platformWrite: true,
|
|
17326
|
+
resetDefault: false, resetOption: '--reset', automaticRetry: false },
|
|
17327
|
+
export: { command: 'ttmg wasm collect export', platformWrite: false,
|
|
17328
|
+
collectionSchemaVersion: '1', originalInputRequired: true,
|
|
17329
|
+
outputPathField: 'collection.path', overwrite: false, strategy: 'union' },
|
|
17330
|
+
},
|
|
17273
17331
|
dev: {
|
|
17274
17332
|
command: 'ttmg dev --format json --no-open',
|
|
17275
17333
|
eventsCommand: 'ttmg dev --events ndjson --no-open',
|
|
@@ -17861,6 +17919,8 @@ function getDesktopPath() {
|
|
|
17861
17919
|
const program = new commander.Command();
|
|
17862
17920
|
const isPreviewPageServerWorker = process.argv[2] === '__preview-page-server';
|
|
17863
17921
|
const isVibePageServerWorker = process.argv[2] === '__vibe-page-server';
|
|
17922
|
+
const wasmArgumentIndex = process.argv.indexOf('wasm', 2);
|
|
17923
|
+
const isWasmCollectCommand = wasmArgumentIndex >= 2 && process.argv[wasmArgumentIndex + 1] === 'collect';
|
|
17864
17924
|
const withVerboseOption = (command) => command.option('--verbose', t('cli.option.verbose'));
|
|
17865
17925
|
const structuredFormatIndex = process.argv.indexOf('--format');
|
|
17866
17926
|
const inlineStructuredFormat = process.argv.find(arg => arg.startsWith('--format='));
|
|
@@ -17871,6 +17931,7 @@ const structuredFormat = inlineStructuredFormat
|
|
|
17871
17931
|
: '';
|
|
17872
17932
|
if (!isPreviewPageServerWorker &&
|
|
17873
17933
|
!isVibePageServerWorker &&
|
|
17934
|
+
!(isWasmCollectCommand && !structuredFormat) &&
|
|
17874
17935
|
structuredFormat !== 'json' &&
|
|
17875
17936
|
structuredFormat !== 'ndjson' &&
|
|
17876
17937
|
!process.argv.includes('--implicit')) {
|
|
@@ -17943,6 +18004,7 @@ withVerboseOption(program
|
|
|
17943
18004
|
.option('--web', t('cli.command.upload.vibeWeb'))
|
|
17944
18005
|
// Vibe v2 backend selection (including opt-in ppe-mock) stays outside the Skill workflow.
|
|
17945
18006
|
.option('--title <title>', t('cli.command.upload.vibeTitle'))
|
|
18007
|
+
.option('--description <description>', t('cli.command.upload.vibeDescription'))
|
|
17946
18008
|
.option('--icon <path>', t('cli.command.upload.vibeIcon'))
|
|
17947
18009
|
.option('--category <category>', t('cli.command.upload.vibeCategory'))
|
|
17948
18010
|
.option('--subcategory <subcategory>', t('cli.command.upload.vibeSubcategory'))
|
|
@@ -17964,6 +18026,7 @@ withVerboseOption(program
|
|
|
17964
18026
|
}
|
|
17965
18027
|
if ([
|
|
17966
18028
|
cmd.title,
|
|
18029
|
+
cmd.description,
|
|
17967
18030
|
cmd.icon,
|
|
17968
18031
|
cmd.appInfo,
|
|
17969
18032
|
cmd.category,
|
|
@@ -18310,6 +18373,25 @@ devSessionCmd
|
|
|
18310
18373
|
});
|
|
18311
18374
|
});
|
|
18312
18375
|
const wasmCmd = program.command('wasm').description(t('cli.command.wasm.desc'));
|
|
18376
|
+
const collectCmd = wasmCmd.command('collect').description(t('cli.command.wasm.collect'));
|
|
18377
|
+
for (const operation of ['start', 'export']) {
|
|
18378
|
+
const command = collectCmd.command(operation)
|
|
18379
|
+
.description(t(operation === 'start' ? 'cli.command.wasm.collect.start' : 'cli.command.wasm.collect.export'))
|
|
18380
|
+
.allowExcessArguments(false)
|
|
18381
|
+
.option('--client-key <key>', t('cli.command.wasm.collect.clientKey'))
|
|
18382
|
+
.option('--wasm-md5 <md5>', t('cli.command.wasm.collect.md5'))
|
|
18383
|
+
.option('--timeout <seconds>', t('cli.command.wasm.collect.timeout'), '30')
|
|
18384
|
+
.option('--format <format>', t('cli.command.wasm.format'), 'json');
|
|
18385
|
+
if (operation === 'start')
|
|
18386
|
+
command.option('--reset', t('cli.command.wasm.collect.reset'));
|
|
18387
|
+
else
|
|
18388
|
+
command.option('--input <path>', t('cli.command.wasm.input'))
|
|
18389
|
+
.option('--output <path>', t('cli.command.wasm.collect.output'));
|
|
18390
|
+
command.action(async (options) => {
|
|
18391
|
+
const Native = await Promise.resolve().then(function () { return index; });
|
|
18392
|
+
await Native.wasmCollectCommand(operation, options);
|
|
18393
|
+
});
|
|
18394
|
+
}
|
|
18313
18395
|
for (const operation of ['prepare', 'split']) {
|
|
18314
18396
|
const command = wasmCmd.command(operation)
|
|
18315
18397
|
.description(t(operation === 'prepare' ? 'cli.command.wasm.prepare' : 'cli.command.wasm.split'))
|
|
@@ -19126,7 +19208,7 @@ async function prepareVibeInput(options, onPackaging, signal, deps = { check: ex
|
|
|
19126
19208
|
const title = options.title?.trim();
|
|
19127
19209
|
if (!title)
|
|
19128
19210
|
throw new VibeError('VIBE_TITLE_REQUIRED');
|
|
19129
|
-
const appInfo = applyVibeCategory(options, readVibeAppInfo(options.appInfo));
|
|
19211
|
+
const appInfo = applyVibeCategory(options, applyVibeDescription(options.description, readVibeAppInfo(options.appInfo)));
|
|
19130
19212
|
if (options.package && options.dir)
|
|
19131
19213
|
throw new VibeError('VIBE_INPUT_CONFLICT');
|
|
19132
19214
|
const image = readFile(options.icon, VIBE_ICON_LIMIT, 'VIBE_INVALID_ICON');
|
|
@@ -19681,10 +19763,12 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
19681
19763
|
terminal: !!options.dryRun,
|
|
19682
19764
|
nextAction: options.dryRun ? 'none' : 'wait',
|
|
19683
19765
|
title: input.title,
|
|
19766
|
+
description: (input.appInfo || VIBE_PPE_DEFAULT_APP_INFO).description,
|
|
19767
|
+
descriptionSource: options.description !== undefined ? 'arguments' : options.appInfo !== undefined ? 'app-info' : 'defaults',
|
|
19684
19768
|
classification: {
|
|
19685
19769
|
category: (input.appInfo || VIBE_PPE_DEFAULT_APP_INFO).category,
|
|
19686
19770
|
subcategory: (input.appInfo || VIBE_PPE_DEFAULT_APP_INFO).subcategory,
|
|
19687
|
-
source: options.category !== undefined ? 'arguments' :
|
|
19771
|
+
source: options.category !== undefined ? 'arguments' : options.appInfo !== undefined ? 'app-info' : 'defaults',
|
|
19688
19772
|
catalogVersion: GAME_CATEGORY_CATALOG_VERSION,
|
|
19689
19773
|
},
|
|
19690
19774
|
iconSizeBytes: input.icon.bytes.length,
|
|
@@ -19880,6 +19964,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
19880
19964
|
const messages = {
|
|
19881
19965
|
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
19966
|
VIBE_INVALID_CATEGORY: ['类型或子类型无效,或两者不匹配。使用 ttmg game categories --format json 查询有效组合。', 'Invalid category/subcategory pair. Query valid values with ttmg game categories --format json.'],
|
|
19967
|
+
VIBE_INVALID_DESCRIPTION: ['描述必须是非空的有效 Unicode 文本;省略 --description 可使用默认描述。', 'Description must be non-empty valid Unicode text; omit --description to use the default.'],
|
|
19883
19968
|
VIBE_WEB_OPTION_CONFLICT: ['--web 使用真实网页授权,不能与 --mock、--mock-scenario、--vibe-api 或 --no-open 同时使用。', '--web uses real browser authorization and cannot be combined with --mock, --mock-scenario, --vibe-api or --no-open.'],
|
|
19884
19969
|
VIBE_AUTH_DENIED: [
|
|
19885
19970
|
'已取消授权,本次未上传。',
|
|
@@ -31195,12 +31280,17 @@ async function snapshotWasm(input, output) {
|
|
|
31195
31280
|
callback(bytes > 512 * 1024 * 1024 ? fail('WASM_INPUT_INVALID', 'Expanded Wasm exceeds 512 MiB.') : null, chunk);
|
|
31196
31281
|
} });
|
|
31197
31282
|
const source = fs$1.createReadStream(input);
|
|
31283
|
+
const fileMd5 = crypto$1.createHash('md5');
|
|
31284
|
+
const identity = new node_stream.Transform({ transform(chunk, _encoding, callback) {
|
|
31285
|
+
fileMd5.update(chunk);
|
|
31286
|
+
callback(null, chunk);
|
|
31287
|
+
} });
|
|
31198
31288
|
const target = fs$1.createWriteStream(output, { flags: 'wx', mode: 0o600 });
|
|
31199
31289
|
try {
|
|
31200
31290
|
if (input.endsWith('.br'))
|
|
31201
|
-
await promises$2.pipeline(source, zlib$1.createBrotliDecompress(), bound, target);
|
|
31291
|
+
await promises$2.pipeline(source, identity, zlib$1.createBrotliDecompress(), bound, target);
|
|
31202
31292
|
else
|
|
31203
|
-
await promises$2.pipeline(source, bound, target);
|
|
31293
|
+
await promises$2.pipeline(source, identity, bound, target);
|
|
31204
31294
|
}
|
|
31205
31295
|
catch (error) {
|
|
31206
31296
|
throw fail(error.code === 'WASM_INPUT_INVALID' ? error.code : 'WASM_INPUT_INVALID', 'Could not read the original Wasm input.');
|
|
@@ -31217,7 +31307,11 @@ async function snapshotWasm(input, output) {
|
|
|
31217
31307
|
throw fail('WASM_INPUT_INVALID', 'Expected a WebAssembly version 1 binary.');
|
|
31218
31308
|
if (!WebAssembly.validate(await fsp.readFile(output)))
|
|
31219
31309
|
throw fail('WASM_INPUT_INVALID', 'Original Wasm is invalid or unsupported by this Node.js WebAssembly engine.');
|
|
31220
|
-
|
|
31310
|
+
const rawMd5 = crypto$1.createHash('md5');
|
|
31311
|
+
for await (const chunk of fs$1.createReadStream(output))
|
|
31312
|
+
rawMd5.update(chunk);
|
|
31313
|
+
return { bytes, wasmSha256: await digest(output), wasmMd5: fileMd5.digest('hex'),
|
|
31314
|
+
uncompressedWasmMd5: rawMd5.digest('hex') };
|
|
31221
31315
|
}
|
|
31222
31316
|
async function executeWasm(operation, options, dependencies = {}) {
|
|
31223
31317
|
const base = { schemaVersion: '1', command: `wasm.${operation}`, operationId: `wasm_${crypto$1.randomUUID()}`,
|
|
@@ -31325,6 +31419,149 @@ async function wasmCommand(operation, options) {
|
|
|
31325
31419
|
process.exitCode = result.status === 'failed' ? 1 : 0;
|
|
31326
31420
|
}
|
|
31327
31421
|
|
|
31422
|
+
class CollectError extends Error {
|
|
31423
|
+
constructor(code, message) {
|
|
31424
|
+
super(message);
|
|
31425
|
+
this.code = code;
|
|
31426
|
+
}
|
|
31427
|
+
}
|
|
31428
|
+
const invalid = (code, message) => { throw new CollectError(code, message); };
|
|
31429
|
+
async function executeWasmCollect(operation, options, overrides = {}) {
|
|
31430
|
+
const deps = { authenticated: () => Boolean(getCurrentUser()?.cookie), request, ...overrides };
|
|
31431
|
+
const base = { schemaVersion: '1', command: `wasm.collect.${operation}`,
|
|
31432
|
+
operationId: `wasm_collect_${crypto$1.randomUUID()}`, platformWrite: operation === 'start',
|
|
31433
|
+
platformWriteAttempted: false, sourceModified: false };
|
|
31434
|
+
let temporary;
|
|
31435
|
+
let staging;
|
|
31436
|
+
let timer;
|
|
31437
|
+
try {
|
|
31438
|
+
const timeout = Number(options.timeout ?? 30);
|
|
31439
|
+
if (!['start', 'export'].includes(operation) || !['json', 'text'].includes(options.format || 'json') ||
|
|
31440
|
+
!/^[A-Za-z0-9_-]{1,128}$/.test(options.clientKey || '') || !/^[a-fA-F0-9]{32}$/.test(options.wasmMd5 || '') ||
|
|
31441
|
+
!Number.isInteger(timeout) || timeout < 1 || timeout > 300 ||
|
|
31442
|
+
(options.reset !== undefined && typeof options.reset !== 'boolean') ||
|
|
31443
|
+
(operation === 'export' && (!options.input || !options.output || options.reset !== undefined)) ||
|
|
31444
|
+
(operation === 'start' && (options.input || options.output))) {
|
|
31445
|
+
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.');
|
|
31446
|
+
}
|
|
31447
|
+
const clientKey = options.clientKey;
|
|
31448
|
+
const wasmMd5 = options.wasmMd5.toLowerCase();
|
|
31449
|
+
const output = options.output ? path$1.resolve(options.output) : undefined;
|
|
31450
|
+
let source;
|
|
31451
|
+
if (operation === 'export') {
|
|
31452
|
+
// lstat detects dangling links as well as existing files. Never overwrite.
|
|
31453
|
+
if (await fsp.lstat(output).catch((e) => { if (e.code !== 'ENOENT')
|
|
31454
|
+
throw e; return null; }))
|
|
31455
|
+
invalid('WASM_OUTPUT_EXISTS', 'Collection output already exists; choose a new file.');
|
|
31456
|
+
temporary = await fsp.mkdtemp(path$1.join(os$1.tmpdir(), 'ttmg-collect-'));
|
|
31457
|
+
try {
|
|
31458
|
+
source = await snapshotWasm(path$1.resolve(options.input), path$1.join(temporary, 'original.wasm'));
|
|
31459
|
+
}
|
|
31460
|
+
catch {
|
|
31461
|
+
return invalid('WASM_INPUT_INVALID', 'Expected a valid original Wasm or Brotli Wasm file.');
|
|
31462
|
+
}
|
|
31463
|
+
if (wasmMd5 !== source.wasmMd5 && wasmMd5 !== source.uncompressedWasmMd5)
|
|
31464
|
+
invalid('WASM_COLLECTION_MISMATCH', 'Collection MD5 does not match this original Wasm (file bytes or uncompressed bytes).');
|
|
31465
|
+
}
|
|
31466
|
+
if (!deps.authenticated())
|
|
31467
|
+
invalid('WASM_AUTH_REQUIRED', 'Run ttmg login on this machine before collecting or exporting.');
|
|
31468
|
+
const controller = new AbortController();
|
|
31469
|
+
const deadline = new Promise((_, reject) => {
|
|
31470
|
+
timer = setTimeout(() => {
|
|
31471
|
+
controller.abort();
|
|
31472
|
+
reject(new CollectError('WASM_COLLECT_TIMEOUT', 'Platform request timed out. A start may have reached the server; retry without --reset to preserve collection.'));
|
|
31473
|
+
}, timeout * 1000);
|
|
31474
|
+
});
|
|
31475
|
+
base.platformWriteAttempted = operation === 'start';
|
|
31476
|
+
const response = await Promise.race([deps.request({
|
|
31477
|
+
url: `${WASM_COLLECT_BASE_URL}/${operation === 'start' ? 'start' : 'export'}`,
|
|
31478
|
+
method: operation === 'start' ? 'POST' : 'GET',
|
|
31479
|
+
...(operation === 'start'
|
|
31480
|
+
? { data: { app_id: clientKey, wasm_md5: wasmMd5, reset: options.reset ?? false } }
|
|
31481
|
+
: { params: { app_id: clientKey, wasm_md5: wasmMd5, strategy: 'union' } }),
|
|
31482
|
+
signal: controller.signal,
|
|
31483
|
+
logRequestBody: false, logApiCalls: false,
|
|
31484
|
+
}), deadline]);
|
|
31485
|
+
clearTimeout(timer);
|
|
31486
|
+
const code = response.error?.code ?? response.error?.StatusCode ?? response.data?.code;
|
|
31487
|
+
const httpStatus = response.ctx?.httpStatusCode;
|
|
31488
|
+
// Upstream bodies/errors are deliberately never included in output: they
|
|
31489
|
+
// can contain credentials or gateway HTML, even with verbose enabled.
|
|
31490
|
+
if ([401, -401].includes(Number(code)) || httpStatus === 401 ||
|
|
31491
|
+
(Number(code) === -1 && /login|log.?in|登录|未登|auth/i.test(String(response.error?.message ?? response.error?.StatusMessage ?? response.data?.message ?? ''))))
|
|
31492
|
+
invalid('WASM_AUTH_REQUIRED', 'Platform login expired. Run ttmg login and retry.');
|
|
31493
|
+
if ([403, -403].includes(Number(code)) || httpStatus === 403)
|
|
31494
|
+
invalid('WASM_COLLECT_FORBIDDEN', 'This account cannot access collection for this game.');
|
|
31495
|
+
if (response.error || response.data?.code !== 0 || (httpStatus && (httpStatus < 200 || httpStatus >= 300)))
|
|
31496
|
+
invalid('WASM_COLLECT_API_FAILED', 'Platform collection request failed; no collection file was written.');
|
|
31497
|
+
if (operation === 'start') {
|
|
31498
|
+
if (response.data.collect_state !== 'open')
|
|
31499
|
+
invalid('WASM_COLLECT_INVALID_RESPONSE', 'Platform did not confirm an open collection window.');
|
|
31500
|
+
return { ...base, status: 'succeeded', clientKey, wasmMd5, reset: options.reset ?? false,
|
|
31501
|
+
collectState: 'open', nextAction: 'run_instrumented_game_and_collect', errorCode: null, error: null };
|
|
31502
|
+
}
|
|
31503
|
+
if (Array.isArray(response.data.func_ids) && response.data.func_ids.length === 0)
|
|
31504
|
+
invalid('WASM_COLLECTION_EMPTY', 'No function IDs collected. Run the instrumented game and export again.');
|
|
31505
|
+
let ids;
|
|
31506
|
+
try {
|
|
31507
|
+
ids = parseCollection({ schemaVersion: '1', wasmSha256: source.wasmSha256,
|
|
31508
|
+
funcIds: response.data.func_ids, bootFuncIds: response.data.boot_func_ids }, source.wasmSha256);
|
|
31509
|
+
}
|
|
31510
|
+
catch {
|
|
31511
|
+
return invalid('WASM_COLLECTION_INVALID', 'Platform returned invalid function IDs; no collection file was written.');
|
|
31512
|
+
}
|
|
31513
|
+
const collection = { schemaVersion: '1', wasmSha256: source.wasmSha256, ...ids,
|
|
31514
|
+
clientKey, wasmMd5, strategy: 'union', exportedAt: new Date().toISOString() };
|
|
31515
|
+
const bytes = Buffer.from(JSON.stringify(collection, null, 2) + '\n');
|
|
31516
|
+
if (bytes.length > 32 * 1024 * 1024)
|
|
31517
|
+
invalid('WASM_COLLECTION_INVALID', 'Collection JSON exceeds 32 MiB.');
|
|
31518
|
+
await fsp.mkdir(path$1.dirname(output), { recursive: true });
|
|
31519
|
+
staging = path$1.join(path$1.dirname(output), `.ttmg-collection-${crypto$1.randomUUID()}.tmp`);
|
|
31520
|
+
await fsp.writeFile(staging, bytes, { flag: 'wx', mode: 0o600 });
|
|
31521
|
+
// Publish a complete file atomically, with exclusive no-clobber semantics.
|
|
31522
|
+
try {
|
|
31523
|
+
await fsp.link(staging, output);
|
|
31524
|
+
}
|
|
31525
|
+
catch (e) {
|
|
31526
|
+
if (e.code === 'EEXIST')
|
|
31527
|
+
invalid('WASM_OUTPUT_EXISTS', 'Collection output already exists; choose a new file.');
|
|
31528
|
+
throw e;
|
|
31529
|
+
}
|
|
31530
|
+
return { ...base, status: 'succeeded', clientKey, wasmMd5,
|
|
31531
|
+
collection: { path: output, schemaVersion: '1', wasmSha256: source.wasmSha256,
|
|
31532
|
+
funcCount: ids.funcIds.length, bootFuncCount: ids.bootFuncIds.length,
|
|
31533
|
+
bytes: bytes.length, sha256: crypto$1.createHash('sha256').update(bytes).digest('hex') },
|
|
31534
|
+
nextAction: 'split_with_collection', errorCode: null, error: null };
|
|
31535
|
+
}
|
|
31536
|
+
catch (error) {
|
|
31537
|
+
const known = error instanceof CollectError;
|
|
31538
|
+
return { ...base, status: 'failed', errorCode: known ? error.code : 'WASM_COLLECT_FAILED',
|
|
31539
|
+
error: known ? error.message : 'Collection operation failed; check the original Wasm, output directory and connection.',
|
|
31540
|
+
nextAction: known && error.code === 'WASM_AUTH_REQUIRED' ? 'login' : 'fix_and_retry' };
|
|
31541
|
+
}
|
|
31542
|
+
finally {
|
|
31543
|
+
if (timer)
|
|
31544
|
+
clearTimeout(timer);
|
|
31545
|
+
if (staging)
|
|
31546
|
+
await fsp.unlink(staging).catch(() => { });
|
|
31547
|
+
if (temporary)
|
|
31548
|
+
await fsp.rm(temporary, { recursive: true, force: true });
|
|
31549
|
+
}
|
|
31550
|
+
}
|
|
31551
|
+
async function wasmCollectCommand(operation, options) {
|
|
31552
|
+
const result = await executeWasmCollect(operation, options);
|
|
31553
|
+
if ((options.format || 'json') === 'json')
|
|
31554
|
+
console.log(JSON.stringify(result));
|
|
31555
|
+
else {
|
|
31556
|
+
console.log(t(result.status === 'failed' ? 'wasm.collect.failed' : 'wasm.collect.succeeded'));
|
|
31557
|
+
if (result.error)
|
|
31558
|
+
console.error(result.error);
|
|
31559
|
+
if ('collection' in result)
|
|
31560
|
+
console.log(result.collection.path);
|
|
31561
|
+
}
|
|
31562
|
+
process.exitCode = result.status === 'failed' ? 1 : 0;
|
|
31563
|
+
}
|
|
31564
|
+
|
|
31328
31565
|
const problem = (code, message) => Object.assign(new Error(message), { code });
|
|
31329
31566
|
const MAX_BYTES = 64 * 1024 * 1024;
|
|
31330
31567
|
// Export one finite Runtime interval. Sequence numbers are global, so gaps in
|
|
@@ -31528,6 +31765,7 @@ var index = /*#__PURE__*/Object.freeze({
|
|
|
31528
31765
|
devSessionStopCommand: devSessionStopCommand,
|
|
31529
31766
|
devWorkflowCommand: devWorkflowCommand,
|
|
31530
31767
|
doctor: doctor,
|
|
31768
|
+
wasmCollectCommand: wasmCollectCommand,
|
|
31531
31769
|
wasmCommand: wasmCommand
|
|
31532
31770
|
});
|
|
31533
31771
|
//# sourceMappingURL=index.js.map
|