@ttmg/cli 0.4.6-vibe-beta.7 → 0.4.6-vibe-beta.8
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 +426 -22
- package/dist/index.js +3413 -181
- package/dist/index.js.map +1 -1
- package/dist/package.json +11 -9
- package/dist/scripts/acceptance-fixture.test.js +33 -0
- package/dist/scripts/dev-capture-smoke.js +76 -0
- package/dist/scripts/dev-client-preview-control-smoke.js +293 -9
- package/dist/scripts/dev-default-smoke.js +22 -1
- package/dist/scripts/dev-workflow-smoke.js +260 -0
- package/dist/scripts/prepare-client-preview-acceptance.js +65 -0
- package/dist/scripts/verify-agent-contract.js +199 -2
- package/dist/scripts/vibe-auth-open-test-preload.cjs +11 -0
- package/dist/scripts/vibe-upload-smoke.js +177 -2
- package/dist/scripts/wasm-smoke.js +53 -0
- package/dist/scripts/wasm-worker.js +63 -0
- package/package.json +11 -9
- package/vibe-upload.md +107 -3
|
@@ -27,6 +27,8 @@ const children = new Set();
|
|
|
27
27
|
let fixtureServer;
|
|
28
28
|
const requests = [];
|
|
29
29
|
let failStatus = false;
|
|
30
|
+
let webMode = false;
|
|
31
|
+
let webPolls = 0;
|
|
30
32
|
const env = {
|
|
31
33
|
...process.env,
|
|
32
34
|
HOME: temp,
|
|
@@ -99,12 +101,65 @@ function events(result) {
|
|
|
99
101
|
.map(line => JSON.parse(line));
|
|
100
102
|
}
|
|
101
103
|
async function main() {
|
|
104
|
+
// Exercise the built worker IPC and new command without creating a platform task.
|
|
105
|
+
const authWorker = spawn(process.execPath, [entry, '__vibe-page-server'], {
|
|
106
|
+
env: {...env, TTMG_VIBE_PAGE_TTL_MS: '30000'}, stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
107
|
+
});
|
|
108
|
+
children.add(authWorker);
|
|
109
|
+
const workerMessage = predicate => new Promise((resolve, reject) => {
|
|
110
|
+
const timer = setTimeout(() => {authWorker.off('message', receive); reject(new Error('worker timeout'));}, 5000);
|
|
111
|
+
function receive(message) {
|
|
112
|
+
if (predicate(message)) {clearTimeout(timer); authWorker.off('message', receive); resolve(message);}
|
|
113
|
+
}
|
|
114
|
+
authWorker.on('message', receive);
|
|
115
|
+
});
|
|
116
|
+
const ready = await workerMessage(message => message.type === 'ready');
|
|
117
|
+
const landing = 'https://developers.tiktok.com/portal_h5/minis_import?task_id=123&import_token=process-private-token';
|
|
118
|
+
let rendered = workerMessage(message => message.type === 'rendered' && message.id === 1);
|
|
119
|
+
authWorker.send({type: 'render', id: 1, html: '<h1>Waiting</h1>', browserLandingUrl: landing});
|
|
120
|
+
await rendered;
|
|
121
|
+
const browserReceipt = path.join(temp, 'browser-open.ndjson');
|
|
122
|
+
const browserEnv = {
|
|
123
|
+
NODE_OPTIONS: `--require=${path.join(__dirname, 'vibe-auth-open-test-preload.cjs')}`,
|
|
124
|
+
TTMG_AUTH_OPEN_TEST_RECEIPT: browserReceipt,
|
|
125
|
+
};
|
|
126
|
+
for (const format of ['json', 'ndjson']) {
|
|
127
|
+
const opened = await run(['auth', 'open', '--page-url', ready.url, '--format', format], undefined, browserEnv);
|
|
128
|
+
assert.equal(opened.code, 0, opened.stderr);
|
|
129
|
+
const result = JSON.parse(opened.stdout);
|
|
130
|
+
assert.equal(result.status, 'open_requested');
|
|
131
|
+
assert.equal(result.platformWrite, false);
|
|
132
|
+
assert.doesNotMatch(opened.stdout + opened.stderr, /import_token|process-private-token/);
|
|
133
|
+
}
|
|
134
|
+
const launches = fs.readFileSync(browserReceipt, 'utf8').trim().split('\n').map(JSON.parse);
|
|
135
|
+
assert.equal(launches.length, 2);
|
|
136
|
+
assert.ok(launches.every(item => item.args.includes(ready.url + '/authorize')));
|
|
137
|
+
const redirect = await fetch(ready.url + '/authorize', {redirect: 'manual'});
|
|
138
|
+
assert.equal(redirect.headers.get('location'), landing);
|
|
139
|
+
assert.equal(redirect.status, 302);
|
|
140
|
+
rendered = workerMessage(message => message.type === 'rendered' && message.id === 2);
|
|
141
|
+
authWorker.send({type: 'render', id: 2, html: '<h1>Complete</h1>'});
|
|
142
|
+
await rendered;
|
|
143
|
+
const stale = await run(['auth', 'open', '--page-url', ready.url, '--format', 'json'], undefined, browserEnv);
|
|
144
|
+
assert.equal(stale.code, 1);
|
|
145
|
+
assert.equal(JSON.parse(stale.stdout).errorCode, 'VIBE_AUTH_PAGE_UNAVAILABLE');
|
|
146
|
+
for (const args of [[], ['--page-url', landing]]) {
|
|
147
|
+
const invalid = await run(['auth', 'open', ...args, '--format', 'json'], undefined, browserEnv);
|
|
148
|
+
assert.equal(invalid.code, 1);
|
|
149
|
+
assert.equal(JSON.parse(invalid.stdout).errorCode, 'VIBE_INVALID_PAGE_URL');
|
|
150
|
+
assert.doesNotMatch(invalid.stdout + invalid.stderr, /process-private-token/);
|
|
151
|
+
}
|
|
152
|
+
assert.equal(fs.readFileSync(browserReceipt, 'utf8').trim().split('\n').length, 2);
|
|
153
|
+
authWorker.kill();
|
|
154
|
+
children.delete(authWorker);
|
|
102
155
|
fixtureServer = http.createServer((req, res) => {
|
|
103
156
|
const url = new URL(req.url, 'http://127.0.0.1');
|
|
104
157
|
requests.push({ path: url.pathname, method: req.method, headers: req.headers });
|
|
105
158
|
req.resume();
|
|
106
159
|
const create = url.pathname.endsWith('/create_minis_import_task_for_agent/');
|
|
107
160
|
const status = url.pathname.endsWith('/get_minis_import_task_status_for_agent/');
|
|
161
|
+
if (create) webPolls = 0;
|
|
162
|
+
if (status && webMode) webPolls++;
|
|
108
163
|
assert.ok(create || status, 'Unexpected real upload endpoint');
|
|
109
164
|
assert.equal(req.headers['x-use-ppe'], '1');
|
|
110
165
|
assert.equal(req.headers['x-tt-env'], 'ppe_op_vc');
|
|
@@ -115,9 +170,9 @@ async function main() {
|
|
|
115
170
|
}
|
|
116
171
|
const data = create ? {
|
|
117
172
|
task_id: '7684370037737553941', import_token: 'process-test-secret',
|
|
118
|
-
landing_url: 'https://developers.tiktok.com/portal_h5/minis_import?import_token=process-test-secret',
|
|
173
|
+
landing_url: 'https://developers.tiktok.com/portal_h5/minis_import?task_id=7684370037737553941&import_token=process-test-secret',
|
|
119
174
|
poll_interval_seconds: 0.01,
|
|
120
|
-
} : { status: 1, ready_for_input:
|
|
175
|
+
} : { status: webMode && webPolls >= 5 ? 2 : 1, ready_for_input: webMode && webPolls >= 5 };
|
|
121
176
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
122
177
|
res.end(JSON.stringify({ ...data, error_info: { code: failStatus && status ? 420500 : 0 } }));
|
|
123
178
|
});
|
|
@@ -137,6 +192,77 @@ async function main() {
|
|
|
137
192
|
});
|
|
138
193
|
|
|
139
194
|
let bindingHtml;
|
|
195
|
+
const appInfoPath = path.join(temp, 'app-info.json');
|
|
196
|
+
fs.writeFileSync(appInfoPath, JSON.stringify({
|
|
197
|
+
description: 'Process contract fixture', category: 'Casual', subcategory: 'Puzzle',
|
|
198
|
+
terms_of_service: 'https://example.invalid/terms', privacy_policy: 'https://example.invalid/privacy',
|
|
199
|
+
copyright_confirmation: true,
|
|
200
|
+
}));
|
|
201
|
+
const badIcon = await run([...input, '--mock', '--app-info', appInfoPath, '--format', 'json']);
|
|
202
|
+
assert.equal(badIcon.code, 1);
|
|
203
|
+
assert.equal(events(badIcon)[0].errorCode, 'VIBE_PLATFORM_ICON_INVALID');
|
|
204
|
+
assert.equal(events(badIcon)[0].platformWrite, false);
|
|
205
|
+
assert.equal(requests.length, 0, 'Invalid input must not create a task');
|
|
206
|
+
assert.equal(fs.existsSync(path.join(temp, '.ttmg/vibe-project.json')), false);
|
|
207
|
+
const platformHeader = Buffer.from(png, 'base64');
|
|
208
|
+
platformHeader.writeUInt32BE(1024, 16);
|
|
209
|
+
platformHeader.writeUInt32BE(1024, 20);
|
|
210
|
+
fs.writeFileSync(icon, platformHeader); // Header preflight fixture, not a decoded image.
|
|
211
|
+
const completeDry = await run([...input, '--app-info', appInfoPath, '--dry-run', '--format', 'json']);
|
|
212
|
+
assert.equal(completeDry.code, 0, completeDry.stderr);
|
|
213
|
+
assert.equal(events(completeDry)[0].status, 'prepared');
|
|
214
|
+
assert.equal(requests.length, 0);
|
|
215
|
+
fs.writeFileSync(appInfoPath, '{private-token');
|
|
216
|
+
const badInfo = await run([...input, '--mock', '--app-info', appInfoPath, '--format', 'json']);
|
|
217
|
+
assert.equal(events(badInfo)[0].errorCode, 'VIBE_INVALID_APP_INFO');
|
|
218
|
+
assert.doesNotMatch(badInfo.stdout + badInfo.stderr, /private-token/);
|
|
219
|
+
assert.equal(requests.length, 0);
|
|
220
|
+
const legacyInfo = await run(['upload', '--app-info', appInfoPath, '--format', 'json']);
|
|
221
|
+
assert.equal(events(legacyInfo)[0].errorCode, 'VIBE_OPTION_REQUIRES_VIBE');
|
|
222
|
+
assert.equal(events(legacyInfo)[0].platformWrite, false);
|
|
223
|
+
fs.writeFileSync(icon, Buffer.from(png, 'base64'));
|
|
224
|
+
// Replay the live package receipt through the built CLI without PPE writes.
|
|
225
|
+
const uploadPaths = [];
|
|
226
|
+
const uploadServer = http.createServer((req, res) => {
|
|
227
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
228
|
+
const endpoint = url.pathname.split('/').filter(Boolean).at(-1);
|
|
229
|
+
uploadPaths.push(endpoint);
|
|
230
|
+
req.resume();
|
|
231
|
+
const replies = {
|
|
232
|
+
create_minis_import_task_for_agent: {
|
|
233
|
+
task_id: '7684370037737619477', import_token: 'receipt-test-secret',
|
|
234
|
+
landing_url: 'https://example.invalid/auth', poll_interval_seconds: 0.01,
|
|
235
|
+
},
|
|
236
|
+
get_minis_import_task_status_for_agent: { status: 2, ready_for_input: true },
|
|
237
|
+
upload_minis_import_app_icon_for_agent: { icon_uri: 'test-icon' },
|
|
238
|
+
submit_minis_import_info_for_agent: {},
|
|
239
|
+
upload_minis_import_package_for_agent: { status: 2, package_uri: 'test-package' },
|
|
240
|
+
};
|
|
241
|
+
res.writeHead(endpoint in replies ? 200 : 404, { 'Content-Type': 'application/json' });
|
|
242
|
+
res.end(JSON.stringify({ error_info: { code: 0 }, ...replies[endpoint] }));
|
|
243
|
+
});
|
|
244
|
+
await new Promise(resolve => uploadServer.listen(0, '127.0.0.1', resolve));
|
|
245
|
+
try {
|
|
246
|
+
const saved = await run([...input, '--format', 'ndjson'], undefined, {
|
|
247
|
+
TTMG_VIBE_BACKEND: 'local-http',
|
|
248
|
+
TTMG_VIBE_API: `http://127.0.0.1:${uploadServer.address().port}`,
|
|
249
|
+
});
|
|
250
|
+
assert.equal(saved.code, 0, saved.stderr);
|
|
251
|
+
const savedEvents = events(saved);
|
|
252
|
+
const savedFinal = savedEvents.at(-1);
|
|
253
|
+
assert.equal(savedFinal.status, 'uploaded', JSON.stringify(savedEvents.map(e => ({ status: e.status, terminal: e.terminal, step: e.uploadStep, uploaded: e.uploaded }))));
|
|
254
|
+
assert.equal(savedFinal.uploaded, true);
|
|
255
|
+
assert.equal(savedFinal.taskStatusRaw, 2);
|
|
256
|
+
assert.equal(savedEvents.filter(e => e.terminal).length, 1);
|
|
257
|
+
assert.deepEqual(uploadPaths, [
|
|
258
|
+
'create_minis_import_task_for_agent', 'get_minis_import_task_status_for_agent',
|
|
259
|
+
'upload_minis_import_app_icon_for_agent', 'submit_minis_import_info_for_agent',
|
|
260
|
+
'upload_minis_import_package_for_agent',
|
|
261
|
+
]);
|
|
262
|
+
assert.doesNotMatch(saved.stdout + saved.stderr, /receipt-test-secret|test-package/);
|
|
263
|
+
} finally {
|
|
264
|
+
uploadServer.close();
|
|
265
|
+
}
|
|
140
266
|
const success = await run(
|
|
141
267
|
[...input, '--mock', '--format', 'ndjson', '--poll-interval', '0.2'],
|
|
142
268
|
async event => {
|
|
@@ -188,6 +314,52 @@ async function main() {
|
|
|
188
314
|
assert.match(previewHtml, /data-phase="complete"/);
|
|
189
315
|
assert.doesNotMatch(previewHtml, /QR|preview|review|Publish to TikTok/);
|
|
190
316
|
|
|
317
|
+
const beforeWeb = requests.length;
|
|
318
|
+
const webConflict = await run([...input, '--web', '--mock', '--format', 'json']);
|
|
319
|
+
assert.equal(events(webConflict)[0].errorCode, 'VIBE_WEB_OPTION_CONFLICT');
|
|
320
|
+
const legacyWeb = await run(['upload', '--web', '--format', 'json']);
|
|
321
|
+
assert.equal(events(legacyWeb)[0].errorCode, 'VIBE_OPTION_REQUIRES_VIBE');
|
|
322
|
+
assert.equal(requests.length, beforeWeb);
|
|
323
|
+
webMode = true;
|
|
324
|
+
for (const format of ['json', 'ndjson']) {
|
|
325
|
+
const receipt = path.join(temp, `upload-web-${format}.ndjson`);
|
|
326
|
+
const web = await run([...input.filter(v => v !== '--no-open'), '--web', '--format', format, '--poll-interval', '0.2'], async event => {
|
|
327
|
+
if (event.status === 'waiting_authorization') {
|
|
328
|
+
assert.equal(event.authorizationMode, 'web');
|
|
329
|
+
assert.equal(event.browserOpenRequested, true);
|
|
330
|
+
assert.equal(event.simulation, false);
|
|
331
|
+
assert.equal(event.nextAction, 'open_browser');
|
|
332
|
+
assert.equal(event.qrPurpose, undefined);
|
|
333
|
+
const response = await fetch(event.pageUrl + '/authorize', {redirect: 'manual'});
|
|
334
|
+
assert.equal(response.status, 302);
|
|
335
|
+
assert.match(response.headers.get('location'), /^https:\/\/developers\.tiktok\.com\/portal_h5\/minis_import\?/);
|
|
336
|
+
const html = await (await fetch(event.pageUrl)).text();
|
|
337
|
+
assert.match(html, /Authorize in your browser/);
|
|
338
|
+
assert.doesNotMatch(html, /alt="Authorization QR"|Scan the QR code/);
|
|
339
|
+
}
|
|
340
|
+
}, {
|
|
341
|
+
NODE_OPTIONS: env.NODE_OPTIONS + ' --require=' + path.join(__dirname, 'vibe-auth-open-test-preload.cjs'),
|
|
342
|
+
TTMG_AUTH_OPEN_TEST_RECEIPT: receipt,
|
|
343
|
+
// Explicit --web must not inherit an old Mock backend setting.
|
|
344
|
+
TTMG_VIBE_BACKEND: 'mock',
|
|
345
|
+
});
|
|
346
|
+
assert.equal(web.code, 1, web.stderr);
|
|
347
|
+
const output = events(web);
|
|
348
|
+
assert.equal(output.at(-1).errorCode, 'VIBE_REAL_UPLOAD_NOT_READY');
|
|
349
|
+
assert.equal(output.at(-1).simulation, false);
|
|
350
|
+
assert.equal(output.at(-1).backend, 'ppe');
|
|
351
|
+
assert.equal(output.at(-1).readyForInput, true);
|
|
352
|
+
assert.equal(output.at(-1).uploadAttempted, false);
|
|
353
|
+
assert.equal(output.at(-1).uploaded, false);
|
|
354
|
+
assert.equal(webPolls, 5, 'Only true backend readiness unlocks authorization');
|
|
355
|
+
if (format === 'json') assert.equal(output.length, 1);
|
|
356
|
+
const opens = fs.readFileSync(receipt, 'utf8').trim().split('\n').map(line => JSON.parse(line));
|
|
357
|
+
assert.equal(opens.length, 1);
|
|
358
|
+
assert.ok(opens[0].args.some(arg => /^http:\/\/127\.0\.0\.1:\d+\/[a-f0-9]{48}\/authorize$/.test(arg)));
|
|
359
|
+
assert.doesNotMatch(JSON.stringify(opens) + web.stdout + web.stderr, /process-test-secret|import_token|aweme:\/\//);
|
|
360
|
+
}
|
|
361
|
+
webMode = false;
|
|
362
|
+
|
|
191
363
|
const json = await run(
|
|
192
364
|
[...input, '--format', 'json', '--poll-interval', '0.01'],
|
|
193
365
|
undefined,
|
|
@@ -310,6 +482,9 @@ async function main() {
|
|
|
310
482
|
assert.equal(help.code, 0);
|
|
311
483
|
assert.match(help.stdout, /--vibe/);
|
|
312
484
|
assert.match(help.stdout, /--client-key/);
|
|
485
|
+
const authHelp = await run(['auth', 'open', '--help']);
|
|
486
|
+
assert.equal(authHelp.code, 0);
|
|
487
|
+
assert.match(authHelp.stdout, /--page-url/);
|
|
313
488
|
|
|
314
489
|
let cancelledPage;
|
|
315
490
|
const cancelled = await run(
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
const { spawnSync } = require('node:child_process');
|
|
7
|
+
const entry = process.argv[2] ? path.resolve(process.argv[2]) : path.resolve(__dirname, '../dist/index.js');
|
|
8
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ttmg-wasm-smoke-'));
|
|
9
|
+
const input = path.join(root, 'original.wasm');
|
|
10
|
+
const wasm = Buffer.from('0061736d010000000105016000017f0304030000000404017000030707010372756e00000909010041000b030001020a1003040010010b0400412a0b040041070b', 'hex');
|
|
11
|
+
const sha = crypto.createHash('sha256').update(wasm).digest('hex');
|
|
12
|
+
function run(args, env = {}) {
|
|
13
|
+
const result = spawnSync(process.execPath, [entry, ...args, '--format', 'json'], {
|
|
14
|
+
cwd: root, encoding: 'utf8', timeout: 15000,
|
|
15
|
+
env: { ...process.env, TTMGRC_PATH: path.join(root, 'rc.json'), ...env },
|
|
16
|
+
});
|
|
17
|
+
assert.ifError(result.error);
|
|
18
|
+
assert.equal(result.stdout.trim().split('\n').length, 1, result.stdout + result.stderr);
|
|
19
|
+
const body = JSON.parse(result.stdout);
|
|
20
|
+
assert.equal(result.status, body.status === 'failed' ? 1 : 0, result.stderr);
|
|
21
|
+
return body;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
fs.writeFileSync(input, wasm);
|
|
25
|
+
const stub = path.join(root, 'missing-binding.js');
|
|
26
|
+
fs.writeFileSync(stub, "const Module=require('node:module'); const load=Module._load; Module._load=function(name,...args){if(name==='@anrans001/ttmg-wasmtool')throw new Error('fixture unavailable');return load.call(this,name,...args)};");
|
|
27
|
+
const unavailable = run(['wasm', 'prepare', '--input', input, '--output', path.join(root, 'missing')], { NODE_OPTIONS: '--require=' + stub });
|
|
28
|
+
assert.equal(unavailable.errorCode, 'WASM_BINDING_UNAVAILABLE');
|
|
29
|
+
assert.equal(fs.existsSync(path.join(root, 'missing')), false);
|
|
30
|
+
const preflight = run(['wasm', 'prepare', '--input', input, '--output', path.join(root, 'dry'), '--dry-run'], { NODE_OPTIONS: '--require=' + stub });
|
|
31
|
+
assert.equal(preflight.status, 'ready');
|
|
32
|
+
assert.equal(preflight.nativeBindingChecked, false);
|
|
33
|
+
assert.equal(fs.existsSync(path.join(root, 'dry')), false);
|
|
34
|
+
const collection = path.join(root, 'collection.json');
|
|
35
|
+
fs.writeFileSync(collection, JSON.stringify({ schemaVersion: '1', wasmSha256: sha, funcIds: [0, 1], bootFuncIds: [0] }));
|
|
36
|
+
const prepared = run(['wasm', 'prepare', '--input', input, '--output', path.join(root, 'prepared')]);
|
|
37
|
+
if (prepared.errorCode === 'WASM_BINDING_UNAVAILABLE') {
|
|
38
|
+
console.log('wasm process contract passed; native execution unavailable on this host, not verified');
|
|
39
|
+
} else {
|
|
40
|
+
assert.equal(prepared.status, 'succeeded', JSON.stringify(prepared));
|
|
41
|
+
const split = run(['wasm', 'split', '--input', input, '--collection', collection, '--output', path.join(root, 'split')]);
|
|
42
|
+
assert.equal(split.status, 'succeeded', JSON.stringify(split));
|
|
43
|
+
assert.equal(split.detail.totalWasmCount, 3);
|
|
44
|
+
assert.equal(split.detail.mainWasmCount, 2);
|
|
45
|
+
assert.equal(split.gameValidation, 'not_evaluated');
|
|
46
|
+
assert.equal(split.input.wasmSha256, sha);
|
|
47
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(split.manifest, 'utf8')), split);
|
|
48
|
+
for (const artifact of split.artifacts) assert.equal(crypto.createHash('sha256').update(fs.readFileSync(artifact.path)).digest('hex'), artifact.sha256);
|
|
49
|
+
assert.deepEqual(fs.readFileSync(input), wasm);
|
|
50
|
+
assert.equal(fs.existsSync(path.join(root, 'rc.json')), false);
|
|
51
|
+
console.log(`wasm process contract passed: native prepare/split on ${process.platform}/${process.arch}, immutable input and verified artifacts`);
|
|
52
|
+
}
|
|
53
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Native Wasm operations run in a disposable process so a deadline can stop
|
|
2
|
+
// synchronous native code without blocking or corrupting the CLI owner.
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const zlib = require('node:zlib');
|
|
6
|
+
|
|
7
|
+
process.once('message', request => {
|
|
8
|
+
let response;
|
|
9
|
+
try {
|
|
10
|
+
let binding;
|
|
11
|
+
try {
|
|
12
|
+
binding = require('@anrans001/ttmg-wasmtool');
|
|
13
|
+
} catch {
|
|
14
|
+
throw Object.assign(new Error('Wasm tool binding is unavailable for this OS/architecture.'), {
|
|
15
|
+
code: 'WASM_BINDING_UNAVAILABLE',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (request.operation === 'prepare') {
|
|
19
|
+
const output = path.join(request.output, 'prepared.wasm');
|
|
20
|
+
const result = binding.prepare(request.input, output);
|
|
21
|
+
if (!fs.existsSync(output) || result.outputSize <= 0)
|
|
22
|
+
throw new Error('Wasm preparation produced no output.');
|
|
23
|
+
fs.writeFileSync(output + '.br', zlib.brotliCompressSync(fs.readFileSync(output), {
|
|
24
|
+
params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 9 },
|
|
25
|
+
}));
|
|
26
|
+
response = { ok: true, detail: { outputSize: result.outputSize, timeCost: result.timeCost } };
|
|
27
|
+
} else if (request.operation === 'split') {
|
|
28
|
+
const result = binding.split({
|
|
29
|
+
input: request.input,
|
|
30
|
+
outputDir: request.output,
|
|
31
|
+
funcIds: request.funcIds,
|
|
32
|
+
alwaysInclude: request.bootFuncIds,
|
|
33
|
+
archive: true,
|
|
34
|
+
compress: true,
|
|
35
|
+
quality: 9,
|
|
36
|
+
callClosure: true,
|
|
37
|
+
callIndirectClosure: false,
|
|
38
|
+
});
|
|
39
|
+
if (result.code !== 0) throw new Error(result.errMsg || 'Wasm split failed.');
|
|
40
|
+
if (![...request.funcIds, ...request.bootFuncIds].every(id => id < result.totalWasmCount))
|
|
41
|
+
throw Object.assign(new Error('Collection contains function IDs outside this Wasm module.'), {
|
|
42
|
+
code: 'WASM_COLLECTION_INVALID',
|
|
43
|
+
});
|
|
44
|
+
const detail = {};
|
|
45
|
+
for (const key of ['totalWasmCount', 'mainWasmCount', 'tableSize', 'timeCost',
|
|
46
|
+
'globalVarList', 'collectFuncCount', 'alwaysIncludeAdded', 'closureAdded',
|
|
47
|
+
'indirectClosureAdded', 'indirectClosureTypes', 'exportAdded']) detail[key] = result[key];
|
|
48
|
+
detail.outputFiles = [result.mainWasmPath, result.subWasmPath, result.funcMetaPath, result.archivePath]
|
|
49
|
+
.filter(Boolean).map(file => {
|
|
50
|
+
if (path.dirname(path.resolve(file)) !== path.resolve(request.output))
|
|
51
|
+
throw new Error('Native output escaped its assigned directory.');
|
|
52
|
+
return path.basename(file);
|
|
53
|
+
});
|
|
54
|
+
response = { ok: true, detail };
|
|
55
|
+
} else throw new Error('Unknown Wasm operation.');
|
|
56
|
+
response.detail.bindingVersion = require('@anrans001/ttmg-wasmtool/package.json').version;
|
|
57
|
+
response.detail.platform = process.platform;
|
|
58
|
+
response.detail.arch = process.arch;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
response = { ok: false, errorCode: error.code || 'WASM_OPERATION_FAILED', error: error.message };
|
|
61
|
+
}
|
|
62
|
+
process.send(response, () => process.disconnect());
|
|
63
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttmg/cli",
|
|
3
|
-
"version": "0.4.6-vibe-beta.
|
|
3
|
+
"version": "0.4.6-vibe-beta.8",
|
|
4
4
|
"description": "TikTok Mini Game Command Line Tool",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"bin": {
|
|
@@ -31,11 +31,12 @@
|
|
|
31
31
|
"test:auth": "node scripts/run-ts-test.js src/commands/common/auth.test.ts",
|
|
32
32
|
"test:capabilities": "node scripts/run-ts-test.js src/commands/common/capabilities.test.ts",
|
|
33
33
|
"test:test-account": "node scripts/run-ts-test.js src/commands/common/testAccount.test.ts",
|
|
34
|
-
"test:dev-session": "node scripts/run-ts-test.js src/commands/native/dev/agentSession.test.ts",
|
|
35
|
-
"test:
|
|
34
|
+
"test:dev-session": "node scripts/run-ts-test.js src/commands/native/dev/agentSession.test.ts && npm run test:packaging-compatibility",
|
|
35
|
+
"test:packaging-compatibility": "node scripts/run-ts-test.js src/commands/native/dev/packagingCompatibility.test.ts",
|
|
36
|
+
"test:dev-session-command": "node scripts/run-ts-test.js src/commands/native/dev/session.test.ts && node scripts/run-ts-test.js src/commands/native/dev/projectSession.test.ts && node scripts/run-ts-test.js src/commands/native/dev/scanPage.test.ts && node scripts/run-ts-test.js src/commands/native/dev/runtime/logArchive.test.ts && node scripts/run-ts-test.js src/commands/native/dev/capture.test.ts",
|
|
36
37
|
"test:dev-runtime-event": "node scripts/run-ts-test.js src/commands/native/dev/utils/scoket.test.ts",
|
|
37
38
|
"test:dev-runtime-client": "node scripts/run-ts-test.js src/commands/native/dev/runtime/runtime.test.ts",
|
|
38
|
-
"test:dev-session-control": "node scripts/run-ts-test.js src/commands/native/dev/runtime/sessionControl.test.ts",
|
|
39
|
+
"test:dev-session-control": "node scripts/run-ts-test.js src/commands/native/dev/runtime/sessionControl.test.ts && node scripts/run-ts-test.js src/commands/native/dev/runtime/previewWorkflow.test.ts",
|
|
39
40
|
"test:dev-runtime-session-follow": "node scripts/run-ts-test.js src/commands/native/dev/runtime/runtimeSessionFollow.test.ts",
|
|
40
41
|
"test:dev-client-preview-orchestrator": "node scripts/run-ts-test.js src/commands/native/dev/runtime/clientPreviewOrchestrator.test.ts",
|
|
41
42
|
"test:dev-watch-change": "node scripts/run-ts-test.js src/commands/native/dev/utils/watchChange.test.ts",
|
|
@@ -46,9 +47,10 @@
|
|
|
46
47
|
"test:check-ai": "node scripts/run-ts-test.js src/commands/native/check/ai.test.ts",
|
|
47
48
|
"test:build-structured": "node scripts/run-ts-test.js src/commands/native/build/build.test.ts",
|
|
48
49
|
"test:doctor": "node scripts/run-ts-test.js src/commands/native/doctor/doctor.test.ts",
|
|
49
|
-
"test:
|
|
50
|
-
"test:agent-contract
|
|
51
|
-
"
|
|
50
|
+
"test:wasm": "node scripts/run-ts-test.js src/commands/native/wasm/wasm.test.ts && node scripts/acceptance-fixture.test.js",
|
|
51
|
+
"test:agent-contract": "npm run test:login && npm run test:auth && npm run test:capabilities && npm run test:test-account && npm run test:upload-preview && npm run test:preview-page && npm run test:check && npm run test:check-ai && npm run test:build-structured && npm run test:doctor && npm run test:wasm && npm run test:dev-session && npm run test:dev-session-command && npm run test:dev-runtime-event && npm run test:dev-runtime-client && npm run test:dev-session-control && npm run test:dev-runtime-session-follow && npm run test:dev-client-preview-orchestrator && npm run test:dev-watch-change && npm run test:network && npm run test:vibe && npx tsc --noEmit && npm run build && npm run test:vibe-process && npm run test:preview-page-process && npm run test:preview-page-detached && npm run verify:dist-version && npm run verify:agent-contract",
|
|
52
|
+
"test:agent-contract:full": "npm run test:agent-contract && npm run test:dev-default-process && npm run test:dev-game-ready-process && npm run test:dev-client-preview-control-process && node scripts/dev-workflow-smoke.js",
|
|
53
|
+
"publish:beta": "npm publish --registry=https://registry.npmjs.org/ --access public --tag vibe-beta",
|
|
52
54
|
"setup": "node scripts/setup.js",
|
|
53
55
|
"reset:local-state": "node scripts/resetLocalState.js",
|
|
54
56
|
"dev:link": "npm run build && npm link",
|
|
@@ -60,7 +62,7 @@
|
|
|
60
62
|
],
|
|
61
63
|
"author": "zhanghongyang.mocha",
|
|
62
64
|
"dependencies": {
|
|
63
|
-
"@anrans001/ttmg-wasmtool": "^0.3.
|
|
65
|
+
"@anrans001/ttmg-wasmtool": "^0.3.3",
|
|
64
66
|
"acorn": "^8.15.0",
|
|
65
67
|
"archiver": "^7.0.1",
|
|
66
68
|
"axios": "^1.11.0",
|
|
@@ -93,7 +95,7 @@
|
|
|
93
95
|
"qs": "6.12.1",
|
|
94
96
|
"semver": "^7.7.2",
|
|
95
97
|
"socks-proxy-agent": "^9.0.0",
|
|
96
|
-
"ttmg-pack": "0.4.13
|
|
98
|
+
"ttmg-pack": "0.4.13",
|
|
97
99
|
"ws": "^8.18.3"
|
|
98
100
|
},
|
|
99
101
|
"devDependencies": {
|
package/vibe-upload.md
CHANGED
|
@@ -26,8 +26,53 @@ ttmg upload --vibe --dir ./game --title "星环守卫" --icon ./avatar.png --dry
|
|
|
26
26
|
|
|
27
27
|
已有 ZIP 时,用 `--package ./game.zip` 替换 `--dir ./game`。名称和头像必须由开发者提供;演示脚本中的临时样本不作为产品默认素材。
|
|
28
28
|
|
|
29
|
+
### 完整应用信息校验
|
|
30
|
+
|
|
31
|
+
维护者可添加 `--app-info ./app-info.json`,为真实上传准备完整参数。该选项不改变现有 `--mock` 用法,也不解除 PPE 素材上传限制。
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
ttmg upload --vibe --package ./game.zip --title "测试游戏" --icon ./avatar.png --app-info ./app-info.json --dry-run --format json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
JSON 文件为以下字段组成的对象,最大 64 KiB;名称由 `--title` 提供,头像 URI 由上传接口返回,不在文件中填写。
|
|
38
|
+
|
|
39
|
+
| 字段 | 要求 |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| description、category、subcategory | 必填非空字符串;分类与子分类应使用平台分类表中的配对值,本地暂不校验分类枚举 |
|
|
42
|
+
| terms_of_service、privacy_policy | 必填 HTTP(S) 链接,不含用户名和密码;须使用游戏自己的真实条款与隐私链接 |
|
|
43
|
+
| copyright_confirmation | 必须显式为 true;只有用户已确认版权时才填写,Skill 不得代为默认确认 |
|
|
44
|
+
| apple_team_id、contact_email、region、min_sdk_version | 可选字符串;填写邮箱时检查基本格式 |
|
|
45
|
+
|
|
46
|
+
提供该文件后,创建任务前检查名称最多 30 个字符,以及头像为 PNG/JPEG、1024×1024、最大 5 MiB。头像仅做文件头预检,完整解码由后端校验。错误分别返回 `VIBE_INVALID_APP_INFO`、`VIBE_TITLE_TOO_LONG`、`VIBE_PLATFORM_ICON_INVALID`,`nextAction=fix_input`。JSON 不接受额外字段,内容不会进入结构化输出;修改后重新运行 dry-run。
|
|
47
|
+
|
|
29
48
|
### Skill 读取什么
|
|
30
49
|
|
|
50
|
+
#### 上传时直接使用网页授权
|
|
51
|
+
|
|
52
|
+
使用 `--web`:CLI 通过内置 `ppe_op_vc` 真实创建任务,在执行机的系统浏览器打开授权页,并持续查询真实绑定状态。Skill 不需要配置环境或再调用 `auth open`。只有服务端返回 `ready_for_input=true` 才算绑定就绪,不模拟授权成功。
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
ttmg upload --vibe --web --dir ./game --title "星环守卫" --icon ./avatar.png --format ndjson
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
原 `upload --vibe --mock` 链路不变。`--web` 与 `--mock`、`--mock-scenario`、`--vibe-api`、`--no-open` 互斥,在创建任务前报错;显式 `--web` 不继承旧 Mock 后端设置。`--dry-run` 不创建任务或打开浏览器。JSON/NDJSON 模式也会打开浏览器,但不污染结构化输出。当前授权就绪后仍返回 `VIBE_REAL_UPLOAD_NOT_READY`,不会伪装成上传成功,也不自动降级 Mock。
|
|
59
|
+
|
|
60
|
+
等待事件返回 `authorizationMode=web` 和 `browserOpenRequested`。真实授权场景的 `nextAction=open_browser` 表示在浏览器完成授权,不代表已授权。打开失败时返回 `browserOpenErrorCode=VIBE_BROWSER_OPEN_FAILED`,仍轮询原任务,可用下面的辅助命令重试,不重新建任务。
|
|
61
|
+
|
|
62
|
+
#### 手动打开浏览器授权
|
|
63
|
+
|
|
64
|
+
保持上传命令运行。收到 `waiting_authorization` 后,如果事件提供 `authorizationOpenCommand`,Skill 可执行该命令,或使用同一事件的 `pageUrl`:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
ttmg auth open --page-url <pageUrl> --format json
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
命令通过临时本地页面跳转到后端原始 `landing_url`,不使用 `aweme://`,不新建任务或上传文件。凭证只留在上传进程与页面服务中,不输出到 Skill 或放进命令参数。原上传进程继续轮询,授权成功与否以其 `readyForInput` 为准。
|
|
71
|
+
|
|
72
|
+
成功返回 `status=open_requested`、`browserOpenRequested=true`、`platformWrite=false`,只表示已请求系统浏览器打开。页面过期、流程结束或没有可用真实授权地址时,返回 `VIBE_AUTH_PAGE_UNAVAILABLE`;URL 输入错误返回 `VIBE_INVALID_PAGE_URL`,浏览器启动失败返回 `VIBE_BROWSER_OPEN_FAILED`。不得因打开失败而自动创建新任务。
|
|
73
|
+
|
|
74
|
+
命令在 CLI 所在电脑执行;远程机器不会打开开发者电脑的浏览器。二维码原路径和 `--mock` 自动推进逻辑保持不变:混合模拟不会等待网页授权,真实网页联调请使用维护者的 PPE 模式。本功能需包含新命令的构建版本。
|
|
75
|
+
|
|
31
76
|
输出 schema 为 `"2"`。每行 NDJSON 是同一 `operationId` 的状态快照;只有最后一行 `terminal=true`。`--format json` 仅在结束时向 stdout 输出一个对象,等待授权的本地页面地址写到 stderr。不要解析中文/英文文案判断状态。
|
|
32
77
|
|
|
33
78
|
| status | Skill 处理 |
|
|
@@ -96,7 +141,7 @@ Mock 与本地 HTTP 共用请求构造、业务响应校验及状态映射,基
|
|
|
96
141
|
| POST create_minis_import_task_for_agent/ | source_type=1、source_project_id、task_idempotency_id、minis_type=MINI_GAME | task_id、import_token、landing_url、poll_interval_seconds |
|
|
97
142
|
| GET get_minis_import_task_status_for_agent/ | Query:task_id、import_token | status、ready_for_input |
|
|
98
143
|
| POST upload_minis_import_app_icon_for_agent/ | Multipart:task_id、import_token、file | icon_uri |
|
|
99
|
-
| POST submit_minis_import_info_for_agent/ | JSON:task_id、import_token、app_info
|
|
144
|
+
| POST submit_minis_import_info_for_agent/ | JSON:task_id、import_token、app_info(完整应用信息,加 CLI 提供的 app_name、icon_uri) | error_info |
|
|
100
145
|
| POST upload_minis_import_package_for_agent/ | Multipart:task_id、import_token、asset_type=2、file | package_uri、status |
|
|
101
146
|
|
|
102
147
|
所有响应必须业务 `error_info.code=0`。最后一步还要求非空 package_uri 和合法非失败状态;完成后不再轮询。HTTP 200、进度100% 都不能单独作为成功判定。19位 task_id 在 CLI 内保留字符串,提交 i64 JSON 时无损编码。
|
|
@@ -109,7 +154,7 @@ TTMG_VIBE_BACKEND=local-http TTMG_VIBE_API=http://127.0.0.1:3100 ttmg upload --v
|
|
|
109
154
|
|
|
110
155
|
**v1 → v2 兼容性变化**:旧 UUID 换 Token 的三接口协议废弃,`--vibe-authorize-url` 返回明确错误;授权地址来自 landing_url。不再输出 preview_ready、game_preview、uploadId、reviewStatus。Skill 必须校验 schemaVersion=2,并按新终态处理。普通不带 --vibe 的 upload 不变。
|
|
111
156
|
|
|
112
|
-
|
|
157
|
+
2026-09-14 已核对 BFF 源码:头像 PNG/JPEG、1024×1024、5 MiB;Native ZIP 和解压总大小上限 100 MiB。本地暂未实现完整 ZIP 内容及解压总大小校验。待真实联调验证:网关包体透传、TOS 权限、手机 OAuth 授权与上传回执。后端接口调整留在适配层处理;若用户动作或成功语义改变,应协商升级 CLI schema。
|
|
113
158
|
|
|
114
159
|
### 验证
|
|
115
160
|
|
|
@@ -133,8 +178,67 @@ Use `--package ./game.zip` instead of --dir for a prepared ZIP. Use `--dry-run -
|
|
|
133
178
|
|
|
134
179
|
The Skill gathers the title and user-selected local icon in conversation. It does not call backend APIs, store credentials, poll services, or introduce a metadata form. The CLI internally connects working steps to PPE and simulates the remaining steps. Removing PPE at production launch is a CLI release change, not a Skill configuration step.
|
|
135
180
|
|
|
181
|
+
### Complete metadata validation
|
|
182
|
+
|
|
183
|
+
Maintainers can add `--app-info ./app-info.json` to prepare complete upload parameters. It does not change the existing --mock entry or enable PPE material uploads.
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
ttmg upload --vibe --package ./game.zip --title "Test game" --icon ./avatar.png --app-info ./app-info.json --dry-run --format json
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The file is a JSON object, at most 64 KiB. The CLI supplies app_name from --title and icon_uri from the upload response; do not put them in this file.
|
|
190
|
+
|
|
191
|
+
| Fields | Requirements |
|
|
192
|
+
| --- | --- |
|
|
193
|
+
| description, category, subcategory | Required nonempty strings; use a valid platform category/subcategory pair. Local enum validation is not implemented yet |
|
|
194
|
+
| terms_of_service, privacy_policy | Required HTTP(S) URLs without embedded credentials; provide the game's actual legal links |
|
|
195
|
+
| copyright_confirmation | Explicit true only after the user confirms ownership; the Skill must not infer consent |
|
|
196
|
+
| apple_team_id, contact_email, region, min_sdk_version | Optional strings; email receives a basic syntax check |
|
|
197
|
+
|
|
198
|
+
With this file, preflight checks the title length (30 characters maximum) and icon (PNG/JPEG, 1024 x 1024, at most 5 MiB) before task creation. Image validation inspects headers only; the backend performs decoding. Failures return VIBE_INVALID_APP_INFO, VIBE_TITLE_TOO_LONG or VIBE_PLATFORM_ICON_INVALID with nextAction=fix_input. Unknown JSON fields are rejected, and metadata is not exposed in structured output. Fix the input and rerun dry-run.
|
|
199
|
+
|
|
200
|
+
Source checked on 2026-09-14: BFF enforces these icon limits and a 100 MiB Native ZIP/uncompressed-size limit. Full local ZIP-content/uncompressed-size validation is not implemented. Gateway transport, TOS access, phone OAuth and upload receipts still require real integration testing.
|
|
201
|
+
|
|
136
202
|
### Output and decisions
|
|
137
203
|
|
|
204
|
+
#### Use browser authorization during upload
|
|
205
|
+
|
|
206
|
+
Use `--web` for real authorization. The CLI creates a task through the built-in `ppe_op_vc` lane, opens authorization in the execution machine's system browser and polls real binding status until `ready_for_input=true`. No environment setup or second Skill command is required. Authorization success is never simulated.
|
|
207
|
+
|
|
208
|
+
```sh
|
|
209
|
+
ttmg upload --vibe --web --dir ./game --title "My game" --icon ./avatar.png --format ndjson
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The original `upload --vibe --mock` workflow is unchanged. `--web` conflicts with `--mock`, `--mock-scenario`, `--vibe-api` and `--no-open` before task creation, and does not inherit an old Mock backend environment setting. Dry-run creates no task and opens no browser. JSON/NDJSON also open the browser without contaminating stdout. Real material upload remains gated: after authorization, the command returns `VIBE_REAL_UPLOAD_NOT_READY`, not upload success or a Mock fallback.
|
|
213
|
+
|
|
214
|
+
Waiting events add `authorizationMode=web` and `browserOpenRequested`. For real authorization, `nextAction=open_browser` requests browser approval; it does not prove approval. If opening fails, `browserOpenErrorCode=VIBE_BROWSER_OPEN_FAILED` is returned while polling continues. Retry the helper below against the same task instead of creating another one.
|
|
215
|
+
|
|
216
|
+
#### Open browser authorization manually
|
|
217
|
+
|
|
218
|
+
Keep the upload process running. On `waiting_authorization`, execute its optional
|
|
219
|
+
`authorizationOpenCommand`, or use the `pageUrl` from that event:
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
ttmg auth open --page-url <pageUrl> --format json
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The temporary loopback page redirects the system browser to the original backend
|
|
226
|
+
`landing_url`, without the `aweme://` wrapper. No new task or upload is performed.
|
|
227
|
+
Credentials stay inside the uploader/page worker, not in Skill output or command
|
|
228
|
+
arguments. The original uploader keeps polling; its `readyForInput` determines
|
|
229
|
+
authorization readiness.
|
|
230
|
+
|
|
231
|
+
Success reports `status=open_requested`, `browserOpenRequested=true`, and
|
|
232
|
+
`platformWrite=false`; it does not mean authorization succeeded. Expired,
|
|
233
|
+
completed, or unavailable authorization pages return `VIBE_AUTH_PAGE_UNAVAILABLE`;
|
|
234
|
+
bad input returns `VIBE_INVALID_PAGE_URL`, and browser launch failures return
|
|
235
|
+
`VIBE_BROWSER_OPEN_FAILED`. Do not create another task automatically on failure.
|
|
236
|
+
|
|
237
|
+
This opens the browser on the CLI computer, not a remote user's computer. Existing
|
|
238
|
+
QR behavior and `--mock` auto-advance stay unchanged; use the maintainer PPE mode
|
|
239
|
+
for real browser authorization, since hybrid simulation does not wait for it.
|
|
240
|
+
Use a build containing the new command; existing released versions are unchanged.
|
|
241
|
+
|
|
138
242
|
Schema **2** provides NDJSON snapshots under one operationId, with exactly one final `terminal=true` event. JSON mode emits one final object on stdout and the authorization page address on stderr. Structured modes do not open the browser automatically; open pageUrl in the AI workspace preview or browser. Text mode opens it unless --no-open.
|
|
139
243
|
|
|
140
244
|
| status | Skill action |
|
|
@@ -174,7 +278,7 @@ The five endpoint contracts are listed in the table above. Mock and loopback HTT
|
|
|
174
278
|
|
|
175
279
|
A business-success response requires error_info.code=0. Package completion additionally requires package_uri and a valid non-failure status; no polling until SUCCEEDED. No platform Cookies or redirects are used. Configure TTMG_VIBE_BACKEND=local-http plus TTMG_VIBE_API=http://127.0.0.1:3100, or pass --vibe-api explicitly; this option rejects remote hosts.
|
|
176
280
|
|
|
177
|
-
For internal PPE integration, set TTMG_VIBE_BACKEND=ppe and TTMG_VIBE_PPE=ppe_op_vc without changing the Skill command. This uses real creation/polling on developers.tiktok.com and encodes the QR as aweme://webview?url=<encodeURIComponent(landing_url)>. The phone must use the same lane. OAuth is not bypassed or simulated.
|
|
281
|
+
For internal PPE integration, set TTMG_VIBE_BACKEND=ppe and TTMG_VIBE_PPE=ppe_op_vc without changing the Skill command. This uses real creation/polling on developers.tiktok.com and encodes the QR as aweme://webview?url=<encodeURIComponent(landing_url)>. The phone must use the same lane. OAuth is not bypassed or simulated. After authorization, VIBE_REAL_UPLOAD_NOT_READY still stops the flow before any material upload; --app-info currently prepares and validates the local contract only.
|
|
178
282
|
|
|
179
283
|
The normal --mock trial internally uses ppe_op_vc without environment variables. Task creation, QR and three polls are real; authorization, icon, app-info and package uploads are simulated with separate in-memory credentials. This changes --mock from an offline run into a PPE integration trial: platformWrite becomes true when task creation is attempted, while simulation remains true. No real material-upload endpoint is called, and real request failures still fail. --mock-scenario controls only simulated steps. Offline unit/process tests use injected transports, not a separate developer configuration.
|
|
180
284
|
|