tripo-cli 0.6.0 → 0.7.0
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 +14 -1
- package/dist/ai/experts.js +1 -1
- package/dist/cli.js +7 -4
- package/dist/commands/completion.js +1 -1
- package/dist/commands/login.js +13 -2
- package/dist/commands/mcp.js +14 -1
- package/dist/commands/process-groups.js +1 -8
- package/dist/commands/task.js +1 -1
- package/dist/core/analytics.js +251 -0
- package/dist/core/client.js +12 -3
- package/dist/core/device-auth.js +6 -1
- package/dist/core/download.js +50 -40
- package/dist/core/requests.js +0 -6
- package/dist/core/task-service.js +3 -1
- package/dist/knowledge/chains.js +1 -3
- package/dist/knowledge/models.js +0 -1
- package/dist/knowledge/scenarios.js +3 -3
- package/dist/program.js +9 -0
- package/package.json +1 -1
- package/skill/SKILL.md +14 -6
- package/skill/commands/make.md +1 -1
- package/skill/commands/process.md +0 -2
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Works with both Tripo regions from the same install — international (`openapi.
|
|
|
35
35
|
| `tripo login / logout / whoami / use` | device flow: browser approves, key arrives automatically (paste fallback); stored 0600 in `~/.tripo/config.json`; `use` switches accounts |
|
|
36
36
|
| `tripo topup / balance / usage` | open the billing page + detect the credits arriving; check spend |
|
|
37
37
|
| `tripo generate <endpoint>` | deterministic access to all 8 generation endpoints |
|
|
38
|
-
| `tripo model / anim / mesh <step>` | refine, texture,
|
|
38
|
+
| `tripo model / anim / mesh <step>` | refine, texture, convert, import, rig-check, rig, retarget, segment, complete, decimate, uv, smartsegment |
|
|
39
39
|
| `tripo task get/list/watch` | query and stream task progress (NDJSON in `--json` mode) |
|
|
40
40
|
| `tripo files upload` | upload a file, print the `file_token` |
|
|
41
41
|
| `tripo batch run <manifest.yaml>` | bulk pipelines with concurrency, retries, and resume |
|
|
@@ -105,12 +105,25 @@ tripo batch run assets.yaml --concurrency 2
|
|
|
105
105
|
| `TRIPO_API_BASE_URL` / `TRIPO_PLATFORM_BASE_URL` | endpoint overrides |
|
|
106
106
|
| `TRIPO_HOME` | config/history directory (default `~/.tripo`) |
|
|
107
107
|
| `TRIPO_NO_UPDATE_CHECK` | disable the startup "new version available" hint (also off when `CI` is set, headless, or `--json`) |
|
|
108
|
+
| `TRIPO_ANALYTICS_ENABLED` | set to `0` to turn off anonymous usage analytics (see [Telemetry](#telemetry)) |
|
|
108
109
|
| `TRIPO_NPM_REGISTRY` | npm registry used by `tripo update` and the version check (default `https://registry.npmjs.org`; e.g. a mirror) |
|
|
109
110
|
| `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` | standard proxy variables, honored for all CLI traffic (plain Node fetch ignores them) |
|
|
110
111
|
| `TRIPO_LLM_BASE_URL` / `TRIPO_LLM_API_KEY` / `TRIPO_LLM_MODEL` | optional OpenAI-compatible LLM for `tripo ai` conversation mode |
|
|
111
112
|
|
|
112
113
|
Setting `TRIPO_API_KEY` per shell — bash/zsh: `export TRIPO_API_KEY=tsk_...` · PowerShell: `$env:TRIPO_API_KEY = "tsk_..."` (persist for future sessions with `setx TRIPO_API_KEY "tsk_..."`) · CMD: `set TRIPO_API_KEY=tsk_...`
|
|
113
114
|
|
|
115
|
+
## Telemetry
|
|
116
|
+
|
|
117
|
+
Since 0.7.0 the CLI sends anonymous usage events to Tripo (`https://sshu-api.tripo3d.ai`) for calls to the international API (`openapi.tripo3d.ai`). It is on by default, needs no setup, prints nothing, and never changes stdout, exit codes, or credit behavior.
|
|
118
|
+
|
|
119
|
+
- **Sent:** command or MCP tool name and its result, sign-in method and result, error category with HTTP status / API code, task id (and parent task id for chains), task type, model, status, credits consumed, durations, downloaded format and file count, CLI version, and the calling agent with its version — `codex` / `workbuddy` / `zcode` / `kimi` (otherwise `unknown`), from `TRIPO_CLIENT_APP` / `TRIPO_DISTRIBUTION_CHANNEL` (set by agent plugins) or the MCP host's client info.
|
|
120
|
+
- **Identity:** a random installation id (`analytics_distinct_id` in `~/.tripo/config.json`), never your account.
|
|
121
|
+
- **Never sent:** prompts, file paths, input images or models, API keys, download URLs.
|
|
122
|
+
- **Nothing is sent for:** China-region (`cn`) calls, custom API endpoints, `--dry-run`, help/version, `docs`, `completion`, `config`, `update`, `use`, `logout`, and the bare `tripo` home screen.
|
|
123
|
+
- **Opt out:** `TRIPO_ANALYTICS_ENABLED=0`.
|
|
124
|
+
|
|
125
|
+
Delivery is best-effort: each send (including the flush on exit) is capped at 2 seconds, the in-memory queue holds at most 100 events, and nothing is retried or written to disk. Maintainers can redirect or tag events with `TRIPO_ANALYTICS_SERVER_URL`, `TRIPO_ANALYTICS_APP_ID`, and `TRIPO_ANALYTICS_ENVIRONMENT=production|pre|development`; tests send nothing unless `TRIPO_ANALYTICS_ENABLED=1` and a local receiver are both set.
|
|
126
|
+
|
|
114
127
|
## Development
|
|
115
128
|
|
|
116
129
|
```bash
|
package/dist/ai/experts.js
CHANGED
|
@@ -32,7 +32,7 @@ const SPECIALIST_PROMPTS = {
|
|
|
32
32
|
print: `You are a 3D-printing engineer. Geometry only (texture=false,pbr=false), watertight meshes, flatten_bottom for FDM, STL or colored 3MF via convert, real-world sizing via scale_factor/auto_size.`,
|
|
33
33
|
'ar-web': `You are a realtime AR/Web 3D engineer. Keep faces <= 20000 via decimate, 2K textures, deliver GLB + USDZ (iOS QuickLook).`,
|
|
34
34
|
anim: `You are a character animation TD. Chain: rig-check → rig (v2.5 rig, spec tripo|mixamo) → retarget (<=5 preset animations, billed per animation). Mixamo/Unity Humanoid → spec=mixamo.`,
|
|
35
|
-
toy: `You are a stylized collectible designer. P1 low-poly
|
|
35
|
+
toy: `You are a stylized collectible designer. P1 low-poly. STL via convert for physical prints.`,
|
|
36
36
|
};
|
|
37
37
|
function specialistSystem(scenarioId) {
|
|
38
38
|
const persona = SPECIALIST_PROMPTS[scenarioId] ?? SPECIALIST_PROMPTS['game-mobile'];
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { installProxyFromEnv } from './core/proxy.js';
|
|
3
3
|
import { runCli } from './program.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
process.
|
|
7
|
-
|
|
4
|
+
import { finishOperation } from './core/analytics.js';
|
|
5
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
6
|
+
process.on(signal, () => {
|
|
7
|
+
process.stderr.write('\n');
|
|
8
|
+
void finishOperation('interrupted').finally(() => process.exit(signal === 'SIGINT' ? 130 : 143));
|
|
9
|
+
});
|
|
10
|
+
}
|
|
8
11
|
await installProxyFromEnv();
|
|
9
12
|
void runCli(process.argv);
|
|
@@ -9,7 +9,7 @@ const COMMANDS = [
|
|
|
9
9
|
];
|
|
10
10
|
const SUBS = {
|
|
11
11
|
generate: ['text-to-model', 'image-to-model', 'multiview-to-model', 'text-to-image', 'image-to-image', 'image-to-multiview', 'edit-multiview', 'image-to-splat'],
|
|
12
|
-
model: ['refine', 'texture', '
|
|
12
|
+
model: ['refine', 'texture', 'convert', 'import'],
|
|
13
13
|
anim: ['check', 'rig', 'retarget'],
|
|
14
14
|
mesh: ['segment', 'complete', 'decimate', 'uv', ...(isMeshEditEnabled() ? ['edit'] : []), 'smartsegment'],
|
|
15
15
|
task: ['get', 'list', 'watch'],
|
package/dist/commands/login.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import * as p from '@clack/prompts';
|
|
12
12
|
import { activeProfileName, DEFAULT_PROFILE, deleteProfile, describeKeySource, isValidProfileName, listProfiles, loadConfig, maskKey, REGION_DEFAULTS, resolveApiKey, resolvePlatformBaseUrl, resolveRegion, setActiveProfile, updateProfile, } from '../config/config.js';
|
|
13
13
|
import { TripoClient } from '../core/client.js';
|
|
14
|
+
import { trackEvent } from '../core/analytics.js';
|
|
14
15
|
import { DeviceAuthUnavailableError, requestDeviceCode, waitForDeviceApproval, } from '../core/device-auth.js';
|
|
15
16
|
import { AuthError, CliError, NetworkError, UsageError } from '../core/errors.js';
|
|
16
17
|
import { detectRegionForKey } from '../core/region.js';
|
|
@@ -220,8 +221,11 @@ async function runHeadlessLogin(region, profile, paste) {
|
|
|
220
221
|
}
|
|
221
222
|
if (!region) {
|
|
222
223
|
throw new UsageError(t({
|
|
223
|
-
zh:
|
|
224
|
-
en:
|
|
224
|
+
zh: `非交互登录需要 --region:先问用户账号在哪个站,不要猜(授权只在该站有效)— cn = ${siteLabel('cn')};ov = ${siteLabel('ov')};还没有账号:中国大陆选 cn,其它地区选 ov`,
|
|
225
|
+
en: `non-interactive login needs --region: ask the human which site their account is on, do not guess (authorization only works on that site) — cn = ${siteLabel('cn')}; ov = ${siteLabel('ov')}; no account yet: mainland China → cn, elsewhere → ov`,
|
|
226
|
+
}), t({
|
|
227
|
+
zh: 'tripo login --region cn|ov;已有 Key 时用 tripo login --key tsk_...(自动识别站点)',
|
|
228
|
+
en: '"tripo login --region cn|ov" — or, with a key in hand: "tripo login --key tsk_..." (site auto-detected)',
|
|
225
229
|
}));
|
|
226
230
|
}
|
|
227
231
|
let grant;
|
|
@@ -273,6 +277,12 @@ async function runHeadlessLogin(region, profile, paste) {
|
|
|
273
277
|
const saved = await validateAndSave(result.apikey, region, profile);
|
|
274
278
|
emitResult({ status: 'logged_in', profile, region: saved.region, key: maskKey(result.apikey), balance: saved.balance }, t({ zh: `登录完成(profile=${profile}, region=${saved.region})`, en: `logged in (profile=${profile}, region=${saved.region})` }));
|
|
275
279
|
}
|
|
280
|
+
/** What a human recognizes a console by — they know their sign-in method and how they pay, rarely the region code. */
|
|
281
|
+
function siteLabel(region) {
|
|
282
|
+
return region === 'cn'
|
|
283
|
+
? t({ zh: '国内站 developers.tripo3d.com(手机号登录,支付宝)', en: 'China mainland site developers.tripo3d.com (phone/SMS sign-in, Alipay)' })
|
|
284
|
+
: t({ zh: '海外站 developers.tripo3d.ai(邮箱登录,Stripe)', en: 'international site developers.tripo3d.ai (email sign-in, Stripe)' });
|
|
285
|
+
}
|
|
276
286
|
/**
|
|
277
287
|
* Preferred path: RFC 8628 device flow — the browser does the sign-in and a
|
|
278
288
|
* one-click approve, the key arrives over the CLI↔console-API channel, and
|
|
@@ -494,6 +504,7 @@ function finishSave(key, region, balance, profile) {
|
|
|
494
504
|
// Saving activates the profile: "the account I just logged into is the
|
|
495
505
|
// account I am using" — no second command needed for the common case.
|
|
496
506
|
updateProfile(profile, { api_key: key, region });
|
|
507
|
+
trackEvent('agent_auth_result', { result: 'success', auth_method: 'login' }, region, REGION_DEFAULTS[region].api);
|
|
497
508
|
const where = profile === DEFAULT_PROFILE ? '~/.tripo/config.json' : `~/.tripo/config.json · profile "${profile}"`;
|
|
498
509
|
// "mode 0600" is only true where POSIX modes exist; on Windows the file
|
|
499
510
|
// relies on the user profile's NTFS ACLs, so the claim would be wrong.
|
package/dist/commands/mcp.js
CHANGED
|
@@ -16,6 +16,7 @@ import { parseThenExpression } from '../knowledge/chains.js';
|
|
|
16
16
|
import { scenarioIds } from '../knowledge/scenarios.js';
|
|
17
17
|
import { executeMake, planMake } from '../pipeline/make.js';
|
|
18
18
|
import { cliVersion } from '../version.js';
|
|
19
|
+
import { beginOperation, finishOperation } from '../core/analytics.js';
|
|
19
20
|
const TOOLS = [
|
|
20
21
|
{
|
|
21
22
|
name: 'tripo_make',
|
|
@@ -77,6 +78,7 @@ export function registerMcpCommand(program) {
|
|
|
77
78
|
});
|
|
78
79
|
}
|
|
79
80
|
async function serveMcp() {
|
|
81
|
+
let clientInfo = {};
|
|
80
82
|
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
81
83
|
const write = (message) => {
|
|
82
84
|
process.stdout.write(JSON.stringify(message) + '\n');
|
|
@@ -96,11 +98,22 @@ async function serveMcp() {
|
|
|
96
98
|
continue; // notification — nothing to answer
|
|
97
99
|
}
|
|
98
100
|
try {
|
|
101
|
+
if (request.method === 'initialize' && request.params?.clientInfo && typeof request.params.clientInfo === 'object') {
|
|
102
|
+
clientInfo = request.params.clientInfo;
|
|
103
|
+
}
|
|
104
|
+
if (request.method === 'tools/call')
|
|
105
|
+
beginOperation(String(request.params?.name ?? 'unknown'), false, clientInfo);
|
|
99
106
|
const result = await dispatch(request.method, request.params ?? {});
|
|
100
107
|
if (result !== SKIP)
|
|
101
108
|
write({ jsonrpc: '2.0', id: request.id, result });
|
|
109
|
+
if (request.method === 'tools/call') {
|
|
110
|
+
const failed = typeof result === 'object' && result !== null && 'isError' in result && result.isError === true;
|
|
111
|
+
await finishOperation(failed ? 'failed' : 'success');
|
|
112
|
+
}
|
|
102
113
|
}
|
|
103
114
|
catch (err) {
|
|
115
|
+
if (request.method === 'tools/call')
|
|
116
|
+
await finishOperation('failed', err);
|
|
104
117
|
write({
|
|
105
118
|
jsonrpc: '2.0',
|
|
106
119
|
id: request.id,
|
|
@@ -179,7 +192,7 @@ async function callTool(params) {
|
|
|
179
192
|
}
|
|
180
193
|
case 'tripo_history': {
|
|
181
194
|
const limit = typeof args.limit === 'number' ? args.limit : 10;
|
|
182
|
-
return asText(readHistory().slice(-limit));
|
|
195
|
+
return asText(readHistory().slice(-limit).map(({ analytics_origin: _origin, analytics_finished: _finished, ...entry }) => entry));
|
|
183
196
|
}
|
|
184
197
|
default:
|
|
185
198
|
return { content: [{ type: 'text', text: `unknown tool: ${params.name}` }], isError: true };
|
|
@@ -70,8 +70,6 @@ function collectStepArgs(step, flags) {
|
|
|
70
70
|
return pick(['format', 'texture_size', 'texture_format', 'face_limit', 'quad', 'flatten_bottom', 'fbx_preset', 'export_orientation', 'scale_factor', 'auto_size', 'pivot_to_center_bottom', 'export_vertex_colors']);
|
|
71
71
|
case 'texture':
|
|
72
72
|
return pick(['model', 'texture_quality', 'texture_alignment', 'pbr']);
|
|
73
|
-
case 'stylize':
|
|
74
|
-
return pick(['style', 'block_size']);
|
|
75
73
|
case 'rig':
|
|
76
74
|
return pick(['model', 'rig_type', 'spec', 'out_format']);
|
|
77
75
|
case 'retarget':
|
|
@@ -94,18 +92,13 @@ function collectStepArgs(step, flags) {
|
|
|
94
92
|
}
|
|
95
93
|
export function registerProcessCommands(program) {
|
|
96
94
|
// ---- tripo model ----
|
|
97
|
-
const model = program.command('model').description('model processing: refine / texture /
|
|
95
|
+
const model = program.command('model').description('model processing: refine / texture / convert / import');
|
|
98
96
|
withTaskOptions(model.command('refine [input]').description('refine a draft text-to-model task (task id / @last)')).action(makeStepAction('refine'));
|
|
99
97
|
withTaskOptions(model
|
|
100
98
|
.command('texture [input]')
|
|
101
99
|
.description('re-texture a model (task/@last/file/url)')
|
|
102
100
|
.option('--texture-quality <q>', 'standard|detailed|extreme')
|
|
103
101
|
.option('--texture-alignment <a>', 'original_image|geometry')).action(makeStepAction('texture'));
|
|
104
|
-
withTaskOptions(model
|
|
105
|
-
.command('stylize [input]')
|
|
106
|
-
.description('stylize a model (lego/voxel/voronoi/minecraft)')
|
|
107
|
-
.option('--style <style>', 'lego|voxel|voronoi|minecraft')
|
|
108
|
-
.option('--block-size <n>', 'minecraft block size 32-128', (v) => Number(v))).action(makeStepAction('stylize'));
|
|
109
102
|
withTaskOptions(model
|
|
110
103
|
.command('convert [input]')
|
|
111
104
|
.description('convert format (GLTF/USDZ/FBX/OBJ/STL/3MF)')
|
package/dist/commands/task.js
CHANGED
|
@@ -119,7 +119,7 @@ export function registerTaskCommands(program) {
|
|
|
119
119
|
.description('recent tasks created by this CLI (@name references)')
|
|
120
120
|
.option('--limit <n>', 'rows', (v) => Number(v), 20)
|
|
121
121
|
.action((opts) => {
|
|
122
|
-
const entries = readHistory().slice(-opts.limit);
|
|
122
|
+
const entries = readHistory().slice(-opts.limit).map(({ analytics_origin: _origin, analytics_finished: _finished, ...entry }) => entry);
|
|
123
123
|
const human = entries
|
|
124
124
|
.map((e) => `${e.created_at.slice(0, 19)} ${(e.name ?? '').padEnd(24)} ${e.type.padEnd(22)} ${e.status.padEnd(9)} ${e.task_id}`)
|
|
125
125
|
.join('\n');
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/** Anonymous Agent analytics. Collection must never change a business result. */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
|
+
import { loadConfig, updateConfig, resolveApiBaseUrl, resolveRegion } from '../config/config.js';
|
|
5
|
+
import { readHistory, updateHistoryStatus } from '../config/history.js';
|
|
6
|
+
import { cliVersion } from '../version.js';
|
|
7
|
+
import { ApiError, AuthError, NetworkError, TaskFailedError, UsageError } from './errors.js';
|
|
8
|
+
export const ANALYTICS_SERVER_URL = "https://sshu-api.tripo3d.ai";
|
|
9
|
+
export const ANALYTICS_APP_ID = "2908364badd34996a1cd44a5f7a71223";
|
|
10
|
+
const operations = new AsyncLocalStorage();
|
|
11
|
+
let activeOperation;
|
|
12
|
+
let installationId;
|
|
13
|
+
const currentOperation = () => operations.getStore() ?? activeOperation;
|
|
14
|
+
export function beginOperation(command, disabled = false, info, regionHint) {
|
|
15
|
+
activeOperation = { command, started: Date.now(), disabled, finished: false, collectors: new Map(), regionHint, context: {
|
|
16
|
+
...attribution(process.env.TRIPO_CLIENT_APP, process.env.TRIPO_DISTRIBUTION_CHANNEL, info),
|
|
17
|
+
distinctId: randomUUID(), operation_id: randomUUID(), integration_mode: info ? "local_mcp" : "cli",
|
|
18
|
+
} };
|
|
19
|
+
operations.enterWith(activeOperation);
|
|
20
|
+
}
|
|
21
|
+
export function analyticsContext() { return currentOperation()?.context; }
|
|
22
|
+
function collector(region = currentOperation()?.regionHint ?? resolveRegion(), apiUrl = resolveApiBaseUrl()) {
|
|
23
|
+
const operation = currentOperation();
|
|
24
|
+
if (!operation || operation.disabled)
|
|
25
|
+
return undefined;
|
|
26
|
+
const key = `${region}:${apiUrl}`;
|
|
27
|
+
let analytics = operation.collectors.get(key);
|
|
28
|
+
if (!analytics) {
|
|
29
|
+
analytics = new Analytics("tripo-cli", cliVersion(), region, apiUrl);
|
|
30
|
+
operation.collectors.set(key, analytics);
|
|
31
|
+
}
|
|
32
|
+
if (analytics.enabled && !installationId) {
|
|
33
|
+
try {
|
|
34
|
+
const config = loadConfig();
|
|
35
|
+
installationId = typeof config.analytics_distinct_id === "string" && /^[a-f0-9-]{36}$/.test(config.analytics_distinct_id)
|
|
36
|
+
? config.analytics_distinct_id : randomUUID();
|
|
37
|
+
if (config.analytics_distinct_id !== installationId)
|
|
38
|
+
updateConfig({ analytics_distinct_id: installationId });
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
installationId = randomUUID();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (installationId)
|
|
45
|
+
operation.context.distinctId = installationId;
|
|
46
|
+
return analytics;
|
|
47
|
+
}
|
|
48
|
+
export function errorProperties(error) {
|
|
49
|
+
const category = error instanceof TaskFailedError ? error.status === 'timeout' ? 'waiting_timeout' : 'task_failed'
|
|
50
|
+
: error instanceof ApiError ? error.apiCode === 2010 ? 'balance' : 'upstream'
|
|
51
|
+
: error instanceof AuthError ? 'authorization' : error instanceof NetworkError ? 'network'
|
|
52
|
+
: error instanceof UsageError ? 'validation' : 'internal';
|
|
53
|
+
return { error_category: category, ...(error instanceof ApiError ? { http_status: error.httpStatus, api_code: error.apiCode } : {}) };
|
|
54
|
+
}
|
|
55
|
+
export function trackEvent(event, properties = {}, region, apiUrl) {
|
|
56
|
+
const operation = currentOperation();
|
|
57
|
+
// At most one authorization outcome per operation; later reports are dropped.
|
|
58
|
+
if (operation && event === 'agent_auth_started')
|
|
59
|
+
operation.authMethod = properties.auth_method;
|
|
60
|
+
if (operation && event === 'agent_auth_result') {
|
|
61
|
+
if (operation.authReported)
|
|
62
|
+
return;
|
|
63
|
+
operation.authReported = true;
|
|
64
|
+
}
|
|
65
|
+
const analytics = collector(region, apiUrl);
|
|
66
|
+
const context = analyticsContext();
|
|
67
|
+
if (analytics && context)
|
|
68
|
+
analytics.track(event, context, properties);
|
|
69
|
+
}
|
|
70
|
+
export async function flushAnalytics() {
|
|
71
|
+
const operation = currentOperation();
|
|
72
|
+
if (operation)
|
|
73
|
+
await Promise.all([...operation.collectors.values()].map((analytics) => analytics.flush()));
|
|
74
|
+
}
|
|
75
|
+
export async function finishOperation(result, error) {
|
|
76
|
+
const operation = currentOperation();
|
|
77
|
+
if (!operation || operation.finished)
|
|
78
|
+
return;
|
|
79
|
+
operation.finished = true;
|
|
80
|
+
if (operation.command === 'login' && result !== 'success') {
|
|
81
|
+
trackEvent('agent_auth_result', { result, auth_method: operation.authMethod ?? 'login', ...(error ? errorProperties(error) : {}) });
|
|
82
|
+
}
|
|
83
|
+
trackEvent('agent_tool_result', { command: operation.command,
|
|
84
|
+
...(operation.context.integration_mode === 'local_mcp' ? { tool_name: operation.command } : {}),
|
|
85
|
+
result, duration_ms: Date.now() - operation.started, ...(error ? errorProperties(error) : {}) });
|
|
86
|
+
await flushAnalytics();
|
|
87
|
+
}
|
|
88
|
+
export function taskOrigin(model) {
|
|
89
|
+
const context = analyticsContext();
|
|
90
|
+
return context ? { creation_client_app: context.client_app, creation_distribution_channel: context.distribution_channel,
|
|
91
|
+
creation_integration_mode: context.integration_mode, ...(typeof model === 'string' ? { model } : {}) } : undefined;
|
|
92
|
+
}
|
|
93
|
+
const observed = new Set();
|
|
94
|
+
export function observeTask(task, region, apiUrl) {
|
|
95
|
+
if (!collector(region, apiUrl)?.enabled || !['success', 'failed', 'cancelled', 'banned', 'expired'].includes(task.status))
|
|
96
|
+
return;
|
|
97
|
+
const key = `${region}:${task.task_id}`;
|
|
98
|
+
const history = readHistory().find((entry) => entry.task_id === task.task_id);
|
|
99
|
+
if (observed.has(key) || history?.analytics_finished)
|
|
100
|
+
return;
|
|
101
|
+
observed.add(key);
|
|
102
|
+
// Bound this cache for long-lived stdio hosts; history preserves known task markers.
|
|
103
|
+
if (observed.size > 1000)
|
|
104
|
+
observed.delete(observed.values().next().value);
|
|
105
|
+
trackEvent('agent_task_finished', { ...history?.analytics_origin,
|
|
106
|
+
creation_client_app: history?.analytics_origin?.creation_client_app ?? 'unknown',
|
|
107
|
+
creation_distribution_channel: history?.analytics_origin?.creation_distribution_channel ?? 'unknown',
|
|
108
|
+
origin_known: Boolean(history?.analytics_origin), task_id: task.task_id, task_type: task.type,
|
|
109
|
+
status: task.status, credits_consumed: task.credits_consumed,
|
|
110
|
+
generation_duration_ms: task.created_at && task.completed_at ? Math.max(0, Date.parse(task.completed_at) - Date.parse(task.created_at)) : undefined,
|
|
111
|
+
}, region, apiUrl);
|
|
112
|
+
if (history) {
|
|
113
|
+
try {
|
|
114
|
+
updateHistoryStatus(task.task_id, task.status, { analytics_finished: true });
|
|
115
|
+
}
|
|
116
|
+
catch { /* best effort marker */ }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function clientApp(value) {
|
|
120
|
+
if (typeof value !== "string")
|
|
121
|
+
return "unknown";
|
|
122
|
+
const name = value.trim().toLowerCase();
|
|
123
|
+
for (const app of ["codex", "workbuddy", "zcode", "kimi"]) {
|
|
124
|
+
if (new RegExp(`(^|[\\s_./-])${app}($|[\\s_./-])`).test(name))
|
|
125
|
+
return app;
|
|
126
|
+
}
|
|
127
|
+
return "unknown";
|
|
128
|
+
}
|
|
129
|
+
export function attribution(explicit, distribution, info, oauthName) {
|
|
130
|
+
const tagged = clientApp(explicit);
|
|
131
|
+
const initialized = clientApp(info?.name);
|
|
132
|
+
const oauth = clientApp(oauthName);
|
|
133
|
+
const chosen = tagged !== "unknown" ? tagged : initialized !== "unknown" ? initialized : oauth;
|
|
134
|
+
const channels = ["codex_plugin", "workbuddy_plugin", "zcode_plugin", "kimi_plugin"];
|
|
135
|
+
return {
|
|
136
|
+
client_app: chosen,
|
|
137
|
+
distribution_channel: typeof distribution === "string" && channels.includes(distribution) ? distribution : "unknown",
|
|
138
|
+
attribution_method: tagged !== "unknown" ? "explicit_tag" : initialized !== "unknown" ? "mcp_client_info" : oauth !== "unknown" ? "oauth_client_name" : "unknown",
|
|
139
|
+
attribution_conflict: [tagged, initialized, oauth].some((app) => app !== "unknown" && chosen !== app),
|
|
140
|
+
...(typeof info?.version === "string" && /^[a-zA-Z0-9._+-]{1,64}$/.test(info.version) && !/tsk_|bearer|token/i.test(info.version) ? { client_version: info.version } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const FIELDS = new Set([
|
|
144
|
+
"tool_name", "command", "result", "status", "task_id", "parent_task_id", "model", "task_type",
|
|
145
|
+
"duration_ms", "generation_duration_ms", "credits_consumed", "error_category", "http_status", "api_code",
|
|
146
|
+
"auth_flow_id", "auth_method", "observation_source", "creation_client_app", "creation_distribution_channel",
|
|
147
|
+
"creation_integration_mode", "origin_known", "model_downloaded", "file_count", "format", "interrupted",
|
|
148
|
+
]);
|
|
149
|
+
export class Analytics {
|
|
150
|
+
app;
|
|
151
|
+
version;
|
|
152
|
+
enabled;
|
|
153
|
+
url;
|
|
154
|
+
appId;
|
|
155
|
+
environment;
|
|
156
|
+
queue = [];
|
|
157
|
+
pending;
|
|
158
|
+
scheduled = false;
|
|
159
|
+
stats = { sent: 0, failed: 0, dropped: 0 };
|
|
160
|
+
constructor(app, version, region, apiUrl, env = process.env) {
|
|
161
|
+
this.app = app;
|
|
162
|
+
this.version = version;
|
|
163
|
+
const receiver = env.TRIPO_ANALYTICS_SERVER_URL ?? ANALYTICS_SERVER_URL;
|
|
164
|
+
this.appId = env.TRIPO_ANALYTICS_APP_ID ?? ANALYTICS_APP_ID;
|
|
165
|
+
const environment = env.TRIPO_ANALYTICS_ENVIRONMENT ?? (env.NODE_ENV === "production" ? "production" : app === "tripo-cli" ? "production" : "development");
|
|
166
|
+
this.environment = ["production", "pre", "development"].includes(environment) ? environment : "development";
|
|
167
|
+
this.url = "";
|
|
168
|
+
let officialApi = false;
|
|
169
|
+
let testReceiver = false;
|
|
170
|
+
try {
|
|
171
|
+
const url = new URL(receiver);
|
|
172
|
+
this.url = new URL("sync_json", url.href.endsWith("/") ? url : `${url.href}/`).href;
|
|
173
|
+
officialApi = new URL(apiUrl).hostname === "openapi.tripo3d.ai";
|
|
174
|
+
testReceiver = (url.protocol === "http:" || url.protocol === "https:") && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
175
|
+
if (url.username || url.password || url.search || url.hash || !["http:", "https:"].includes(url.protocol))
|
|
176
|
+
this.url = "";
|
|
177
|
+
}
|
|
178
|
+
catch { /* invalid analytics configuration is disabled, never fatal */ }
|
|
179
|
+
const testing = env.NODE_ENV === "test" || Boolean(env.VITEST);
|
|
180
|
+
this.enabled = region === "ov" && Boolean(this.url && this.appId) && env.TRIPO_ANALYTICS_ENABLED !== "0"
|
|
181
|
+
&& (testing ? env.TRIPO_ANALYTICS_ENABLED === "1" && testReceiver : officialApi || testReceiver);
|
|
182
|
+
}
|
|
183
|
+
track(event, context, properties = {}) {
|
|
184
|
+
if (!this.enabled || !/^agent_[a-z_]+$/.test(event))
|
|
185
|
+
return;
|
|
186
|
+
if (this.queue.length >= 100) {
|
|
187
|
+
this.stats.dropped++;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const safe = {};
|
|
191
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
192
|
+
if (!FIELDS.has(key))
|
|
193
|
+
continue;
|
|
194
|
+
if (typeof value === "number" && Number.isFinite(value) || typeof value === "boolean")
|
|
195
|
+
safe[key] = value;
|
|
196
|
+
else if (typeof value === "string" && /^[a-zA-Z0-9_.:-]{1,128}$/.test(value) && !/tsk_|bearer|token/i.test(value))
|
|
197
|
+
safe[key] = value;
|
|
198
|
+
}
|
|
199
|
+
this.queue.push({ appid: this.appId, data: {
|
|
200
|
+
"#type": "track", "#event_name": event, "#distinct_id": context.distinctId,
|
|
201
|
+
"#time": new Date().toISOString().replace("T", " ").replace("Z", ""),
|
|
202
|
+
properties: {
|
|
203
|
+
...safe, ...attribution(context.client_app, context.distribution_channel, { version: context.client_version }),
|
|
204
|
+
attribution_method: context.attribution_method, attribution_conflict: context.attribution_conflict ?? false,
|
|
205
|
+
app: this.app, event_source: this.app === "tripo-mcp" ? "server" : "cli", site_region: "global",
|
|
206
|
+
environment: this.environment, schema_version: 2, agent_schema_version: 1, app_version: this.version,
|
|
207
|
+
integration_mode: context.integration_mode, operation_id: context.operation_id, event_id: randomUUID(),
|
|
208
|
+
identity_type: this.app === "tripo-mcp" ? "authorization" : "installation", observation_source: "client", "#zone_offset": 0,
|
|
209
|
+
},
|
|
210
|
+
} });
|
|
211
|
+
if (!this.scheduled) {
|
|
212
|
+
this.scheduled = true;
|
|
213
|
+
queueMicrotask(() => { this.scheduled = false; void this.flush(); });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async flush() {
|
|
217
|
+
if (this.pending)
|
|
218
|
+
return this.pending;
|
|
219
|
+
if (!this.queue.length)
|
|
220
|
+
return;
|
|
221
|
+
// ponytail: bounded best-effort delivery; no persistent spool or retries in v1.
|
|
222
|
+
const send = async () => {
|
|
223
|
+
const deadline = Date.now() + 2000;
|
|
224
|
+
while (this.queue.length && Date.now() < deadline) {
|
|
225
|
+
const batch = this.queue.splice(0, 100);
|
|
226
|
+
try {
|
|
227
|
+
const response = await fetch(this.url, {
|
|
228
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch),
|
|
229
|
+
signal: AbortSignal.timeout(Math.max(1, deadline - Date.now())),
|
|
230
|
+
});
|
|
231
|
+
const body = await response.json();
|
|
232
|
+
if (!response.ok || body.code !== 0)
|
|
233
|
+
throw new Error("analytics rejected");
|
|
234
|
+
this.stats.sent += batch.length;
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
this.stats.failed += batch.length;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
this.stats.dropped += this.queue.length;
|
|
241
|
+
this.queue = [];
|
|
242
|
+
};
|
|
243
|
+
this.pending = send();
|
|
244
|
+
try {
|
|
245
|
+
await this.pending;
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
this.pending = undefined;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
package/dist/core/client.js
CHANGED
|
@@ -8,7 +8,10 @@ import { activeProfileName, DEFAULT_PROFILE, listProfiles, loadConfig, resolveAp
|
|
|
8
8
|
import { t } from '../i18n.js';
|
|
9
9
|
import { EXIT } from '../knowledge/error-catalog.js';
|
|
10
10
|
import { ApiError, AuthError, NetworkError } from './errors.js';
|
|
11
|
+
import { observeTask, trackEvent, taskOrigin } from './analytics.js';
|
|
12
|
+
import { resolveRegion } from '../config/config.js';
|
|
11
13
|
export class TripoClient {
|
|
14
|
+
region = resolveRegion();
|
|
12
15
|
baseUrl;
|
|
13
16
|
apiKey;
|
|
14
17
|
timeoutMs;
|
|
@@ -117,9 +120,11 @@ export class TripoClient {
|
|
|
117
120
|
return envelope.data;
|
|
118
121
|
}
|
|
119
122
|
// ---- typed convenience wrappers ----
|
|
120
|
-
getTask(taskId, step = false) {
|
|
123
|
+
async getTask(taskId, step = false) {
|
|
121
124
|
const suffix = step ? '?step=1' : '';
|
|
122
|
-
|
|
125
|
+
const task = await this.request('GET', `/v3/tasks/${encodeURIComponent(taskId)}${suffix}`);
|
|
126
|
+
observeTask(task, this.region, this.baseUrl);
|
|
127
|
+
return task;
|
|
123
128
|
}
|
|
124
129
|
/**
|
|
125
130
|
* Batch task query. The server returns `{tasks: {id: detail}, missed: [id]}`
|
|
@@ -128,6 +133,8 @@ export class TripoClient {
|
|
|
128
133
|
async listTasks(taskIds) {
|
|
129
134
|
const data = await this.request('POST', '/v3/tasks/list', { task_ids: taskIds });
|
|
130
135
|
const byId = data.tasks ?? {};
|
|
136
|
+
for (const [id, task] of Object.entries(byId))
|
|
137
|
+
observeTask({ ...task, task_id: task.task_id ?? id }, this.region, this.baseUrl);
|
|
131
138
|
const tasks = taskIds
|
|
132
139
|
.filter((id) => byId[id])
|
|
133
140
|
.map((id) => ({ ...byId[id], task_id: byId[id].task_id ?? id }));
|
|
@@ -140,9 +147,11 @@ export class TripoClient {
|
|
|
140
147
|
return this.request('GET', '/v3/account/usage');
|
|
141
148
|
}
|
|
142
149
|
/** Create a task; returns its id. Response may be {task_id} or a full detail. */
|
|
143
|
-
async createTask(endpoint, payload) {
|
|
150
|
+
async createTask(endpoint, payload, taskType) {
|
|
144
151
|
try {
|
|
145
152
|
const data = await this.request('POST', endpoint, payload, { idempotent: false });
|
|
153
|
+
trackEvent('agent_task_created', { ...taskOrigin(payload.model), task_id: data.task_id, task_type: taskType,
|
|
154
|
+
...([payload.original_model_task_id, payload.task_id, payload.input].some((value) => typeof value === 'string' && /^[a-f0-9-]{36}$/.test(value)) ? { parent_task_id: [payload.original_model_task_id, payload.task_id, payload.input].find((value) => typeof value === 'string' && /^[a-f0-9-]{36}$/.test(value)) } : {}) }, this.region, this.baseUrl);
|
|
146
155
|
return data.task_id;
|
|
147
156
|
}
|
|
148
157
|
catch (err) {
|
package/dist/core/device-auth.js
CHANGED
|
@@ -13,6 +13,7 @@ import { hostname } from 'node:os';
|
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { REGION_DEFAULTS, tripoHome } from '../config/config.js';
|
|
15
15
|
import { sleep } from './client.js';
|
|
16
|
+
import { trackEvent } from './analytics.js';
|
|
16
17
|
export const DEVICE_CODE_EXPIRED = 4290;
|
|
17
18
|
export const DEVICE_CODE_SLOW_DOWN = 4291;
|
|
18
19
|
export const DEVICE_CODE_DENIED = 4292;
|
|
@@ -79,6 +80,7 @@ export function deviceKeyName(profile, host = hostname(), id = deviceId()) {
|
|
|
79
80
|
return `${prefix} - ${shownHost} ${id}`.replace(/\s+/g, ' ').slice(0, 32).trim();
|
|
80
81
|
}
|
|
81
82
|
export async function requestDeviceCode(region, profile) {
|
|
83
|
+
trackEvent('agent_auth_started', { auth_method: 'device' }, region, REGION_DEFAULTS[region].api);
|
|
82
84
|
const body = await post(`${consoleApiBaseUrl(region)}/cli/device/code`, { name: deviceKeyName(profile) });
|
|
83
85
|
const grant = body.data;
|
|
84
86
|
if (!grant ||
|
|
@@ -138,9 +140,12 @@ export async function waitForDeviceApproval(region, grant, onTick) {
|
|
|
138
140
|
intervalMs += 5000;
|
|
139
141
|
continue;
|
|
140
142
|
}
|
|
141
|
-
if (poll.status !== 'pending')
|
|
143
|
+
if (poll.status !== 'pending') {
|
|
144
|
+
trackEvent('agent_auth_result', { result: poll.status === 'approved' ? 'success' : poll.status, auth_method: 'device' }, region, REGION_DEFAULTS[region].api);
|
|
142
145
|
return poll;
|
|
146
|
+
}
|
|
143
147
|
}
|
|
148
|
+
trackEvent('agent_auth_result', { result: 'expired', auth_method: 'device' }, region, REGION_DEFAULTS[region].api);
|
|
144
149
|
return { status: 'expired' };
|
|
145
150
|
}
|
|
146
151
|
/**
|
package/dist/core/download.js
CHANGED
|
@@ -10,6 +10,7 @@ import { basename, extname, join } from 'node:path';
|
|
|
10
10
|
import { Readable } from 'node:stream';
|
|
11
11
|
import { pipeline } from 'node:stream/promises';
|
|
12
12
|
import { NetworkError } from './errors.js';
|
|
13
|
+
import { trackEvent, errorProperties } from './analytics.js';
|
|
13
14
|
const MODEL_EXTS = new Set(['.glb', '.gltf', '.fbx', '.obj', '.stl', '.3mf', '.usdz', '.zip', '.ply']);
|
|
14
15
|
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp']);
|
|
15
16
|
/** Collect every downloadable URL in the output map (string or {url} object). */
|
|
@@ -59,48 +60,57 @@ async function downloadOne(url, dest) {
|
|
|
59
60
|
}
|
|
60
61
|
/** Download all artifacts of a finished task + write task.json. */
|
|
61
62
|
export async function downloadArtifacts(task, outputDir, historyEntry, options = {}) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
63
|
+
const started = Date.now();
|
|
64
|
+
try {
|
|
65
|
+
await mkdir(outputDir, { recursive: true });
|
|
66
|
+
const urls = collectOutputUrls(task.output);
|
|
67
|
+
const files = [];
|
|
68
|
+
let modelFile;
|
|
69
|
+
let preview;
|
|
70
|
+
for (const { field, url } of urls) {
|
|
71
|
+
const name = fileNameFor(field, url);
|
|
72
|
+
const dest = join(outputDir, name);
|
|
73
|
+
options.onFile?.(name);
|
|
74
|
+
await downloadOne(url, dest);
|
|
75
|
+
files.push(dest);
|
|
76
|
+
const ext = extname(name).toLowerCase();
|
|
77
|
+
if (!modelFile && MODEL_EXTS.has(ext))
|
|
78
|
+
modelFile = dest;
|
|
79
|
+
// uv_render (mesh/uv) is a flat UV atlas, not a render of the model.
|
|
80
|
+
if (!preview && !/uv_render/.test(field) && (IMAGE_EXTS.has(ext) || /rendered|preview/i.test(field)))
|
|
81
|
+
preview = dest;
|
|
82
|
+
}
|
|
83
|
+
// Normalize the preview name so agents can rely on preview.png existing.
|
|
84
|
+
if (preview && basename(preview) !== 'preview.png') {
|
|
85
|
+
const { copyFile } = await import('node:fs/promises');
|
|
86
|
+
const canonical = join(outputDir, 'preview.png');
|
|
87
|
+
try {
|
|
88
|
+
await copyFile(preview, canonical);
|
|
89
|
+
files.push(canonical);
|
|
90
|
+
preview = canonical;
|
|
91
|
+
}
|
|
92
|
+
catch { }
|
|
88
93
|
}
|
|
89
|
-
|
|
94
|
+
const record = {
|
|
95
|
+
task_id: task.task_id,
|
|
96
|
+
type: task.type,
|
|
97
|
+
status: task.status,
|
|
98
|
+
input: task.input ?? historyEntry?.payload ?? {},
|
|
99
|
+
output_fields: urls.map((u) => u.field),
|
|
100
|
+
credits_consumed: task.credits_consumed,
|
|
101
|
+
created_at: task.created_at,
|
|
102
|
+
completed_at: task.completed_at,
|
|
103
|
+
files: files.map((f) => basename(f)),
|
|
104
|
+
};
|
|
105
|
+
await writeFile(join(outputDir, 'task.json'), JSON.stringify(record, null, 2) + '\n');
|
|
106
|
+
trackEvent('agent_download_result', { task_id: task.task_id, result: 'success', model_downloaded: Boolean(modelFile),
|
|
107
|
+
file_count: files.length, duration_ms: Date.now() - started, format: modelFile ? extname(modelFile).slice(1) : undefined });
|
|
108
|
+
return { outputDir, files, modelFile, preview };
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
trackEvent('agent_download_result', { task_id: task.task_id, result: 'failed', duration_ms: Date.now() - started, ...errorProperties(error) });
|
|
112
|
+
throw error;
|
|
90
113
|
}
|
|
91
|
-
const record = {
|
|
92
|
-
task_id: task.task_id,
|
|
93
|
-
type: task.type,
|
|
94
|
-
status: task.status,
|
|
95
|
-
input: task.input ?? historyEntry?.payload ?? {},
|
|
96
|
-
output_fields: urls.map((u) => u.field),
|
|
97
|
-
credits_consumed: task.credits_consumed,
|
|
98
|
-
created_at: task.created_at,
|
|
99
|
-
completed_at: task.completed_at,
|
|
100
|
-
files: files.map((f) => basename(f)),
|
|
101
|
-
};
|
|
102
|
-
await writeFile(join(outputDir, 'task.json'), JSON.stringify(record, null, 2) + '\n');
|
|
103
|
-
return { outputDir, files, modelFile, preview };
|
|
104
114
|
}
|
|
105
115
|
/** Default output directory: ./tripo-out/<slug>-<id8>/ */
|
|
106
116
|
export function defaultOutputDir(baseDir, slug, taskId) {
|
package/dist/core/requests.js
CHANGED
|
@@ -165,12 +165,6 @@ export function buildStepRequest(stepName, input, args) {
|
|
|
165
165
|
}
|
|
166
166
|
break;
|
|
167
167
|
}
|
|
168
|
-
case 'stylize': {
|
|
169
|
-
if (typeof params.style !== 'string') {
|
|
170
|
-
throw new UsageError('stylize requires style (lego/voxel/voronoi/minecraft)');
|
|
171
|
-
}
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
168
|
case 'smartsegment': {
|
|
175
169
|
if (params.seg_type === undefined)
|
|
176
170
|
params.seg_type = 'model';
|
|
@@ -13,6 +13,7 @@ import { detectInput } from '../util/input-detect.js';
|
|
|
13
13
|
import { notifyDone } from '../util/notify.js';
|
|
14
14
|
import { createSpinner } from '../util/spinner.js';
|
|
15
15
|
import { defaultOutputDir, downloadArtifacts } from './download.js';
|
|
16
|
+
import { taskOrigin } from './analytics.js';
|
|
16
17
|
import { ApiError, CliError, TaskFailedError, UsageError, balanceSuggestion } from './errors.js';
|
|
17
18
|
import { isQuiet, log, warn } from './output.js';
|
|
18
19
|
import { uploadLocalFile } from './upload.js';
|
|
@@ -85,9 +86,10 @@ export async function runTask(options) {
|
|
|
85
86
|
const { client, request, taskType } = options;
|
|
86
87
|
if (options.precheckBalance !== false)
|
|
87
88
|
await precheckBalance(client);
|
|
88
|
-
const taskId = await client.createTask(request.endpoint, request.payload);
|
|
89
|
+
const taskId = await client.createTask(request.endpoint, request.payload, taskType);
|
|
89
90
|
const name = uniqueName(slugify(options.name));
|
|
90
91
|
const entry = {
|
|
92
|
+
analytics_origin: taskOrigin(request.payload.model),
|
|
91
93
|
task_id: taskId,
|
|
92
94
|
type: taskType,
|
|
93
95
|
name,
|
package/dist/knowledge/chains.js
CHANGED
|
@@ -24,7 +24,6 @@ export const MODEL_PRODUCING_TYPES = new Set([
|
|
|
24
24
|
export const STEPS = [
|
|
25
25
|
{ name: 'refine', endpoint: '/v3/models/refine', produces: 'refine_model', accepts: ['text_to_model'], acceptsFile: false, describe: { zh: '精修文生模型的几何细节', en: 'refine geometry of a text-to-model result' } },
|
|
26
26
|
{ name: 'texture', endpoint: '/v3/models/texture', produces: 'texture_model', accepts: 'any-model', acceptsFile: true, describe: { zh: '重新生成贴图 / PBR 材质', en: 'regenerate textures / PBR materials' } },
|
|
27
|
-
{ name: 'stylize', endpoint: '/v3/models/stylize', produces: 'stylize_model', accepts: 'any-model', acceptsFile: true, describe: { zh: '按风格重塑模型(lego/voxel/...)', en: 'restyle the model (lego/voxel/...)' } },
|
|
28
27
|
{ name: 'convert', endpoint: '/v3/models/convert', produces: 'convert_model', accepts: 'any-model', acceptsFile: true, describe: { zh: '转换输出格式 / 贴图尺寸 / 朝向', en: 'convert format / texture size / orientation' } },
|
|
29
28
|
{ name: 'import', endpoint: '/v3/models/import', produces: 'import_model', accepts: [], acceptsFile: true, describe: { zh: '导入本地模型文件作为加工起点', en: 'import a local model file as the chain source' } },
|
|
30
29
|
{ name: 'rig-check', endpoint: '/v3/animations/rig-check', produces: 'animate_prerigcheck', accepts: 'any-model', acceptsFile: true, passthrough: true, aliases: ['check', 'rigcheck'], describe: { zh: '检查是否可绑骨(不产出模型,不可绑则止损)', en: 'check riggability (no model output; stops the chain if not riggable)' } },
|
|
@@ -71,7 +70,7 @@ export function getStep(name) {
|
|
|
71
70
|
* Parse a --then expression: "texture,rig,convert:fbx" or
|
|
72
71
|
* "convert:format=fbx,texture_size=2048". A bare value after ":" is sugar for
|
|
73
72
|
* the step's primary argument (convert→format, retarget→animation,
|
|
74
|
-
*
|
|
73
|
+
* decimate→face_limit).
|
|
75
74
|
*/
|
|
76
75
|
export function parseThenExpression(expr) {
|
|
77
76
|
const out = [];
|
|
@@ -116,7 +115,6 @@ function applyPrimaryArg(step, args, value) {
|
|
|
116
115
|
const primary = {
|
|
117
116
|
convert: 'format',
|
|
118
117
|
retarget: 'animation',
|
|
119
|
-
stylize: 'style',
|
|
120
118
|
decimate: 'face_limit',
|
|
121
119
|
texture: 'texture_quality',
|
|
122
120
|
rig: 'rig_type',
|
package/dist/knowledge/models.js
CHANGED
|
@@ -139,7 +139,6 @@ export const TEXTURE_FORMATS = [
|
|
|
139
139
|
];
|
|
140
140
|
export const FBX_PRESETS = ['blender', '3dsmax', 'mixamo'];
|
|
141
141
|
export const EXPORT_ORIENTATIONS = ['+x', '-x', '+y', '-y'];
|
|
142
|
-
export const STYLIZE_STYLES = ['lego', 'voxel', 'voronoi', 'minecraft'];
|
|
143
142
|
export const TEXTURE_QUALITIES = ['standard', 'detailed', 'extreme'];
|
|
144
143
|
/** Normalize any accepted alias to the canonical wire value; unknown values pass through. */
|
|
145
144
|
export function normalizeModel(input) {
|
|
@@ -92,14 +92,14 @@ export const SCENARIOS = [
|
|
|
92
92
|
{
|
|
93
93
|
id: 'toy',
|
|
94
94
|
label: { zh: '风格化摆件', en: 'Stylized toy / desk piece' },
|
|
95
|
-
description: { zh: '
|
|
95
|
+
description: { zh: '低模可爱风摆件', en: 'Cute low-poly desk piece' },
|
|
96
96
|
model: MODEL_P1,
|
|
97
97
|
genParams: { texture: true, pbr: true, face_limit: 20000 },
|
|
98
98
|
chain: [],
|
|
99
99
|
deliverables: ['GLB', 'STL via convert'],
|
|
100
100
|
notes: {
|
|
101
|
-
zh: ['P1 出干净低模', '
|
|
102
|
-
en: ['P1 produces clean low-poly', 'append --then
|
|
101
|
+
zh: ['P1 出干净低模', '要打印实物追加 --then convert:stl'],
|
|
102
|
+
en: ['P1 produces clean low-poly', 'append --then convert:stl to print it'],
|
|
103
103
|
},
|
|
104
104
|
},
|
|
105
105
|
];
|
package/dist/program.js
CHANGED
|
@@ -25,6 +25,7 @@ import { configureOutput, errorLine, isJsonMode } from './core/output.js';
|
|
|
25
25
|
import { setLocale, t } from './i18n.js';
|
|
26
26
|
import { EXIT } from './knowledge/error-catalog.js';
|
|
27
27
|
import { cliVersion } from './version.js';
|
|
28
|
+
import { beginOperation, finishOperation } from './core/analytics.js';
|
|
28
29
|
export function buildProgram() {
|
|
29
30
|
const program = new Command();
|
|
30
31
|
program
|
|
@@ -56,11 +57,18 @@ export function buildProgram() {
|
|
|
56
57
|
const lang = loadConfig().language;
|
|
57
58
|
if (lang)
|
|
58
59
|
setLocale(lang);
|
|
60
|
+
const path = [];
|
|
61
|
+
for (let command = actionCommand; command && command !== program; command = command.parent)
|
|
62
|
+
path.unshift(command.name());
|
|
63
|
+
const excluded = ['mcp', 'docs', 'completion', 'config', 'update', 'use', 'logout', 'home'];
|
|
64
|
+
if (actionCommand.name() !== 'mcp')
|
|
65
|
+
beginOperation(path.join(':'), path.length === 0 || excluded.includes(path[0]) || actionCommand.opts().dryRun === true, undefined, actionCommand.opts().region === 'cn' || actionCommand.opts().region === 'ov' ? actionCommand.opts().region : undefined);
|
|
59
66
|
// Startup version hint (cached, non-blocking). `update` reports on its
|
|
60
67
|
// own; `mcp` speaks JSON-RPC over stdio and must stay silent.
|
|
61
68
|
if (actionCommand.name() !== 'update' && actionCommand.name() !== 'mcp')
|
|
62
69
|
notifyIfOutdated();
|
|
63
70
|
});
|
|
71
|
+
program.hook('postAction', async () => { await finishOperation('success'); });
|
|
64
72
|
registerMakeCommand(program);
|
|
65
73
|
registerAiCommand(program);
|
|
66
74
|
registerViewCommands(program);
|
|
@@ -134,6 +142,7 @@ export async function runCli(argv) {
|
|
|
134
142
|
await program.parseAsync(argv);
|
|
135
143
|
}
|
|
136
144
|
catch (err) {
|
|
145
|
+
await finishOperation('failed', err);
|
|
137
146
|
handleError(err);
|
|
138
147
|
}
|
|
139
148
|
}
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -9,13 +9,20 @@ One command turns a text prompt or an image into local files: a 3D model (glb/fb
|
|
|
9
9
|
npm install -g tripo-cli # or: npx tripo-cli ...
|
|
10
10
|
export TRIPO_API_KEY=tsk_... # highest-priority auth; no interaction needed
|
|
11
11
|
# (PowerShell: $env:TRIPO_API_KEY = "tsk_...")
|
|
12
|
+
# China-site key? also export TRIPO_REGION=cn (env keys default to ov)
|
|
12
13
|
tripo doctor # verify key, network, balance
|
|
13
14
|
```
|
|
14
15
|
|
|
15
|
-
If there is no key, run the device login **yourself** — it works without a TTY
|
|
16
|
+
If there is no key, run the device login **yourself** — it works without a TTY.
|
|
17
|
+
First ask the human which Tripo site their account is on, and do not guess: the
|
|
18
|
+
login only works on the site it was started for.
|
|
19
|
+
|
|
20
|
+
- `cn` — China mainland site developers.tripo3d.com (phone/SMS sign-in, Alipay)
|
|
21
|
+
- `ov` — international site developers.tripo3d.ai (email sign-in, Stripe)
|
|
22
|
+
- no account yet: mainland China → `cn`, everywhere else → `ov`
|
|
16
23
|
|
|
17
24
|
```bash
|
|
18
|
-
tripo login --region ov
|
|
25
|
+
tripo login --region <cn|ov> # the site the human named
|
|
19
26
|
```
|
|
20
27
|
|
|
21
28
|
It prints a verification URL and a one-time code, then blocks until the human
|
|
@@ -24,9 +31,10 @@ and code to the human and tell them: sign in on that page first if it asks
|
|
|
24
31
|
(sign-up gives free credits), enter the code, pick an API key if the account has
|
|
25
32
|
several (one is created automatically if there are none), click Authorize. The
|
|
26
33
|
key is then saved locally; never ask the human to paste a key into the chat. If
|
|
27
|
-
the code expires (exit 3), just run the same command again
|
|
34
|
+
the code expires (exit 3), just run the same command again; if the human says
|
|
35
|
+
the page is the wrong site, stop it and rerun with the other `--region`.
|
|
28
36
|
|
|
29
|
-
Regions (ov = international, cn = China mainland) share one CLI: `tripo login --key tsk_...` auto-detects which region the key belongs to. On exit code 3 (auth), run `tripo doctor` — it diagnoses key-vs-region mismatches and prints the exact fix (`tripo config set region cn|ov`).
|
|
37
|
+
Regions (ov = international, cn = China mainland) share one CLI: `tripo login --key tsk_...` auto-detects which region the key belongs to. Already logged into the wrong site? `tripo login --region <other>` replaces the active profile's key and region. On exit code 3 (auth), run `tripo doctor` — it diagnoses key-vs-region mismatches and prints the exact fix (`tripo config set region cn|ov`).
|
|
30
38
|
|
|
31
39
|
## Behavior rules (read first)
|
|
32
40
|
|
|
@@ -50,8 +58,8 @@ tripo make @last --then convert:fbx --json --yes # continue from last t
|
|
|
50
58
|
```
|
|
51
59
|
|
|
52
60
|
- `--for` scenario presets: `game-mobile` `game-pc` `film` `print` `ar-web` `anim` `toy`
|
|
53
|
-
- `--then` chain steps: `refine texture
|
|
54
|
-
- step args: `convert:format=FBX,texture_size=2048` (bare value = primary arg: `convert:fbx`, `
|
|
61
|
+
- `--then` chain steps: `refine texture convert import rig-check rig retarget segment complete decimate uv smartsegment`
|
|
62
|
+
- step args: `convert:format=FBX,texture_size=2048` (bare value = primary arg: `convert:fbx`, `decimate:5000`)
|
|
55
63
|
- Output JSON: `{"task_id","type","status","credits_consumed","output_dir","files",["model_file"],["preview"],["chain"],["credits_breakdown"]}` — with `--then`, `credits_consumed` is the **total** for the run and `credits_breakdown` itemizes every task (`[{task_id,type,credits_consumed}]`, generation first)
|
|
56
64
|
|
|
57
65
|
## Pipes (composing steps yourself)
|
package/skill/commands/make.md
CHANGED
|
@@ -40,7 +40,7 @@ In a TTY it then asks `Run this plan?`; answering no exits 0 with `cancelled; no
|
|
|
40
40
|
## Options
|
|
41
41
|
|
|
42
42
|
- `--for <scenario>` — `game-mobile` `game-pc` `film` `print` `ar-web` `anim` `toy`. Sets model+params+chain+format from the domain knowledge base.
|
|
43
|
-
- `--then <steps>` — processing chain, comma-separated. Steps: `refine texture
|
|
43
|
+
- `--then <steps>` — processing chain, comma-separated. Steps: `refine texture convert import rig-check rig retarget segment complete decimate uv smartsegment`. Args: `step:key=value,...`; bare value maps to the step's primary arg (`convert:fbx` → format, `decimate:5000` → face_limit, `retarget:preset:walk` → animation). Overrides the scenario chain.
|
|
44
44
|
- `--model <m>` — force `tripo-v3.1` (high fidelity, default), `tripo-p1` (low-poly; face_limit 50–20000; quad/parts/geometry_quality unsupported and stripped) or `tripo-p2` (P-series preview; adds quad, face_limit 48–50000 tri / 48–25000 quad; never auto-selected). The CLI normalizes these aliases to the wire versions (`v3.1-20260211` / `P1-20260311` / `P2-20260801`) the server actually accepts. Full table with status (GA / Preview / Legacy) and official links: `tripo docs --topic commands/generate` → "Models & Versions".
|
|
45
45
|
- `-n, --candidates <n>` — up to 4 parallel candidates with different seeds (interactive pick when chaining).
|
|
46
46
|
- `--seed <n>` — fixed model_seed for reproducible geometry.
|
|
@@ -9,7 +9,6 @@ Any API parameter without a first-class flag can be passed with `-p key=value` (
|
|
|
9
9
|
```bash
|
|
10
10
|
tripo model refine [input] # only accepts text_to_model tasks
|
|
11
11
|
tripo model texture [input] --texture-quality detailed --texture-alignment geometry [-p pbr=true]
|
|
12
|
-
tripo model stylize [input] --style lego|voxel|voronoi|minecraft [--block-size 80]
|
|
13
12
|
tripo model convert [input] --format GLTF|USDZ|FBX|OBJ|STL|3MF \
|
|
14
13
|
[--texture-size 2048] [--texture-format JPEG|PNG|OPEN_EXR|...] [--face-limit N] \
|
|
15
14
|
[--quad] [--flatten-bottom] [--fbx-preset blender|3dsmax|mixamo] \
|
|
@@ -22,7 +21,6 @@ tripo model import <file-or-url> # GLB/GLTF/FBX/OBJ/STL ≤150MB
|
|
|
22
21
|
| --- | --- | --- |
|
|
23
22
|
| `refine` | successful `text_to_model` task only | `model` (default `tripo-v3.1`) |
|
|
24
23
|
| `texture` | task / file / URL (file/URL is imported implicitly) | `model` `v3.0-20250812` (default) / `v2.5-20250123`; `texture_quality` standard/detailed/extreme; `pbr` (default true); `texture_seed`; `texture_alignment` original_image/geometry |
|
|
25
|
-
| `stylize` | task / file (no URL) | `style` lego/voxel/voronoi/minecraft; `block_size` 32–128 (minecraft only, default 80) |
|
|
26
24
|
| `convert` | task / file | see table below; `format` required |
|
|
27
25
|
| `import` | file / URL | GLB / GLTF / FBX / OBJ / STL ≤150 MB. `.gltf` references external files, so it only works as a URL — upload `.glb` |
|
|
28
26
|
|