tripo-cli 0.7.2 → 0.7.3
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 +8 -8
- package/dist/ai/experts.js +4 -4
- package/dist/ai/plan-card.js +2 -1
- package/dist/ai/wizard.js +8 -4
- package/dist/commands/completion.js +1 -1
- package/dist/commands/generate.js +16 -5
- package/dist/commands/login.js +79 -21
- package/dist/commands/make.js +3 -0
- package/dist/commands/mcp.js +7 -3
- package/dist/commands/process-groups.js +26 -14
- package/dist/commands/shared.js +17 -3
- package/dist/commands/task.js +2 -0
- package/dist/commands/update.js +18 -2
- package/dist/core/analytics.js +39 -16
- package/dist/core/client.js +49 -8
- package/dist/core/device-auth.js +49 -1
- package/dist/core/download.js +2 -1
- package/dist/core/errors.js +8 -3
- package/dist/core/requests.js +36 -17
- package/dist/core/task-service.js +2 -0
- package/dist/core/upload.js +23 -5
- package/dist/core/watch.js +3 -0
- package/dist/knowledge/chains.js +4 -5
- package/dist/knowledge/error-catalog.js +10 -8
- package/dist/knowledge/models.js +28 -7
- package/dist/knowledge/params.js +48 -25
- package/dist/knowledge/scenarios.js +4 -4
- package/dist/pipeline/make.js +2 -2
- package/dist/pipeline/runner.js +25 -1
- package/dist/util/input-detect.js +10 -1
- package/package.json +1 -1
- package/skill/SKILL.md +19 -8
- package/skill/commands/account.md +7 -2
- package/skill/commands/generate.md +24 -15
- package/skill/commands/make.md +4 -3
- package/skill/commands/process.md +27 -23
- package/skill/commands/task.md +2 -2
- package/skill/common-errors.md +10 -9
- package/skill/examples/animation.md +1 -1
- package/skill/examples/game-asset.md +1 -1
package/dist/core/analytics.js
CHANGED
|
@@ -10,10 +10,12 @@ export const ANALYTICS_APP_ID = "2908364badd34996a1cd44a5f7a71223";
|
|
|
10
10
|
const operations = new AsyncLocalStorage();
|
|
11
11
|
let activeOperation;
|
|
12
12
|
let installationId;
|
|
13
|
+
/** Account lookups held only in this process's memory: at most one per key per CLI command or MCP session. */
|
|
14
|
+
const identities = new Map();
|
|
13
15
|
const currentOperation = () => operations.getStore() ?? activeOperation;
|
|
14
16
|
export function beginOperation(command, disabled = false, info, regionHint) {
|
|
15
17
|
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),
|
|
18
|
+
...attribution(process.env.TRIPO_CLIENT_APP, process.env.TRIPO_DISTRIBUTION_CHANNEL, info, undefined, hostClientApp()),
|
|
17
19
|
distinctId: randomUUID(), operation_id: randomUUID(), integration_mode: info ? "local_mcp" : "cli",
|
|
18
20
|
} };
|
|
19
21
|
operations.enterWith(activeOperation);
|
|
@@ -27,8 +29,15 @@ export function identifyUser(apiKey, region, apiUrl) {
|
|
|
27
29
|
return;
|
|
28
30
|
const consoleApi = process.env.TRIPO_CONSOLE_API_BASE_URL ?? REGION_DEFAULTS[region].consoleApi;
|
|
29
31
|
const key = createHash('sha256').update(JSON.stringify([region, apiUrl, consoleApi, apiKey])).digest('hex');
|
|
30
|
-
if (operation.identity?.key
|
|
31
|
-
|
|
32
|
+
if (operation.identity?.key === key)
|
|
33
|
+
return;
|
|
34
|
+
let value = identities.get(key);
|
|
35
|
+
if (!value) {
|
|
36
|
+
// A CLI command ends its lookup when it finishes; an MCP session lets it outlive the tool call, for at most 10 s.
|
|
37
|
+
const signal = operation.context.integration_mode === 'local_mcp' ? AbortSignal.timeout(10_000) : (operation.lookups ??= new AbortController()).signal;
|
|
38
|
+
identities.set(key, value = analytics.identify(apiKey, consoleApi, signal));
|
|
39
|
+
}
|
|
40
|
+
operation.identity = { key, value };
|
|
32
41
|
}
|
|
33
42
|
function collector(region = currentOperation()?.regionHint ?? resolveRegion(), apiUrl = resolveApiBaseUrl()) {
|
|
34
43
|
const operation = currentOperation();
|
|
@@ -95,6 +104,7 @@ export async function finishOperation(result, error) {
|
|
|
95
104
|
...(operation.context.integration_mode === 'local_mcp' ? { tool_name: operation.command } : {}),
|
|
96
105
|
result, duration_ms: Date.now() - operation.started, ...(error ? errorProperties(error) : {}) });
|
|
97
106
|
await flushAnalytics();
|
|
107
|
+
operation.lookups?.abort();
|
|
98
108
|
}
|
|
99
109
|
export function taskOrigin(model) {
|
|
100
110
|
const context = analyticsContext();
|
|
@@ -136,23 +146,35 @@ export function clientApp(value) {
|
|
|
136
146
|
if (typeof value !== "string")
|
|
137
147
|
return "unknown";
|
|
138
148
|
const name = value.trim().toLowerCase();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return "unknown";
|
|
149
|
+
if (name === "dsh")
|
|
150
|
+
return "deepseek";
|
|
151
|
+
const matches = ["codex", "workbuddy", "zcode", "kimi", "deepseek", "claude", "cursor", "chatgpt"]
|
|
152
|
+
.filter((app) => new RegExp(`(^|[\\s_./():-])${app}($|[\\s_./():-])`).test(name));
|
|
153
|
+
return matches.length === 1 ? matches[0] : "unknown";
|
|
154
|
+
}
|
|
155
|
+
function hostClientApp() {
|
|
156
|
+
// Session markers identify the caller; install paths and terminal names do not.
|
|
157
|
+
const hosts = [];
|
|
158
|
+
if (process.env.CODEX_THREAD_ID?.trim())
|
|
159
|
+
hosts.push("codex");
|
|
160
|
+
if (process.env.ZCODE_SESSION_ID?.trim())
|
|
161
|
+
hosts.push("zcode");
|
|
162
|
+
if (process.env.CODEBUDDY_HOST === "workbuddy-desktop")
|
|
163
|
+
hosts.push("workbuddy");
|
|
164
|
+
return hosts.length === 1 ? hosts[0] : "unknown";
|
|
144
165
|
}
|
|
145
|
-
export function attribution(explicit, distribution, info, oauthName) {
|
|
166
|
+
export function attribution(explicit, distribution, info, oauthName, hostName) {
|
|
146
167
|
const tagged = clientApp(explicit);
|
|
147
168
|
const initialized = clientApp(info?.name);
|
|
148
169
|
const oauth = clientApp(oauthName);
|
|
149
|
-
const
|
|
170
|
+
const host = clientApp(hostName);
|
|
171
|
+
const chosen = tagged !== "unknown" ? tagged : initialized !== "unknown" ? initialized : oauth !== "unknown" ? oauth : host;
|
|
150
172
|
const channels = ["codex_plugin", "workbuddy_plugin", "zcode_plugin", "kimi_plugin", "deepseek_plugin"];
|
|
151
173
|
return {
|
|
152
174
|
client_app: chosen,
|
|
153
175
|
distribution_channel: typeof distribution === "string" && channels.includes(distribution) ? distribution : "unknown",
|
|
154
|
-
attribution_method: tagged !== "unknown" ? "explicit_tag" : initialized !== "unknown" ? "mcp_client_info" : oauth !== "unknown" ? "oauth_client_name" : "unknown",
|
|
155
|
-
attribution_conflict: [tagged, initialized, oauth].some((app) => app !== "unknown" && chosen !== app),
|
|
176
|
+
attribution_method: tagged !== "unknown" ? "explicit_tag" : initialized !== "unknown" ? "mcp_client_info" : oauth !== "unknown" ? "oauth_client_name" : host !== "unknown" ? "host_environment" : "unknown",
|
|
177
|
+
attribution_conflict: [tagged, initialized, oauth, host].some((app) => app !== "unknown" && chosen !== app),
|
|
156
178
|
...(typeof info?.version === "string" && /^[a-zA-Z0-9._+-]{1,64}$/.test(info.version) && !/tsk_|bearer|token/i.test(info.version) ? { client_version: info.version } : {}),
|
|
157
179
|
};
|
|
158
180
|
}
|
|
@@ -202,7 +224,7 @@ export class Analytics {
|
|
|
202
224
|
&& (testing ? env.TRIPO_ANALYTICS_ENABLED === "1" && testReceiver : officialApi || testReceiver);
|
|
203
225
|
this.identityEnabled = this.enabled && (testing ? localApi : officialApi);
|
|
204
226
|
}
|
|
205
|
-
async identify(apiKey, consoleApi) {
|
|
227
|
+
async identify(apiKey, consoleApi, signal = AbortSignal.timeout(1000)) {
|
|
206
228
|
if (!this.identityEnabled)
|
|
207
229
|
return undefined;
|
|
208
230
|
try {
|
|
@@ -214,7 +236,7 @@ export class Analytics {
|
|
|
214
236
|
const response = await fetch(url, {
|
|
215
237
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
216
238
|
body: JSON.stringify({ apikey: apiKey, with_detail: true }),
|
|
217
|
-
redirect: "error", signal
|
|
239
|
+
redirect: "error", signal,
|
|
218
240
|
});
|
|
219
241
|
if (!response.ok)
|
|
220
242
|
return undefined;
|
|
@@ -268,9 +290,10 @@ export class Analytics {
|
|
|
268
290
|
const deadline = Date.now() + 2000;
|
|
269
291
|
while (this.queue.length && Date.now() < deadline) {
|
|
270
292
|
const batch = this.queue.splice(0, 100);
|
|
271
|
-
//
|
|
293
|
+
// A lookup may outlive its events, so wait at most 1 s for it; that leaves the send at least 1 s of the budget.
|
|
294
|
+
const wait = new Promise((resolve) => setTimeout(() => resolve(undefined), 1000).unref());
|
|
272
295
|
await Promise.all(batch.map(async ({ event, identity }) => {
|
|
273
|
-
const userId = await identity;
|
|
296
|
+
const userId = await Promise.race([identity, wait]);
|
|
274
297
|
if (userId)
|
|
275
298
|
event.data.properties.platform_user_id = userId;
|
|
276
299
|
}));
|
package/dist/core/client.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Lightweight V3 API client on top of global fetch (Node 20+).
|
|
3
3
|
* Handles the response envelope, error translation, timeouts, and
|
|
4
4
|
* exponential-backoff retries for transient failures (429/5xx/network;
|
|
5
|
-
* task creation is only resent on 429).
|
|
5
|
+
* task creation is only resent on 429). A 429 waits at least as long as the
|
|
6
|
+
* server's Retry-After / X-RateLimit-Reset hint, capped at 60s per wait.
|
|
6
7
|
*/
|
|
7
8
|
import { activeProfileName, DEFAULT_PROFILE, listProfiles, loadConfig, resolveApiBaseUrl, resolveApiKey } from '../config/config.js';
|
|
8
9
|
import { t } from '../i18n.js';
|
|
@@ -54,12 +55,35 @@ export class TripoClient {
|
|
|
54
55
|
form.append('file', data, fileName);
|
|
55
56
|
return this.send('POST', '/v3/files', { body: form });
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Large-file upload: presign, then PUT straight to storage. The PUT carries
|
|
60
|
+
* no Authorization header and no request timeout (files up to 150MB).
|
|
61
|
+
*/
|
|
62
|
+
async uploadFileViaPresign(format, data) {
|
|
63
|
+
const { presigned_url, file_token } = await this.request('POST', '/v3/files/presign', { format });
|
|
64
|
+
let response;
|
|
65
|
+
try {
|
|
66
|
+
response = await fetch(presigned_url, {
|
|
67
|
+
method: 'PUT',
|
|
68
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
69
|
+
body: data,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
throw new NetworkError(`presigned upload failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
74
|
+
}
|
|
75
|
+
if (!response.ok)
|
|
76
|
+
throw new NetworkError(`presigned upload failed: HTTP ${response.status}`);
|
|
77
|
+
return { file_token };
|
|
78
|
+
}
|
|
57
79
|
async send(method, path, init) {
|
|
58
80
|
identifyUser(this.apiKey, this.region, this.baseUrl);
|
|
59
81
|
let lastError;
|
|
60
82
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
61
83
|
if (attempt > 0) {
|
|
62
|
-
const
|
|
84
|
+
const backoff = Math.min(1000 * 2 ** (attempt - 1), 8000);
|
|
85
|
+
const hint = lastError instanceof ApiError ? (lastError.retryAfterMs ?? 0) : 0;
|
|
86
|
+
const delay = Math.min(Math.max(backoff, hint), 60_000);
|
|
63
87
|
this.onRetry?.(attempt, delay, lastError?.message ?? 'retry');
|
|
64
88
|
await sleep(delay);
|
|
65
89
|
}
|
|
@@ -99,13 +123,14 @@ export class TripoClient {
|
|
|
99
123
|
finally {
|
|
100
124
|
clearTimeout(timer);
|
|
101
125
|
}
|
|
126
|
+
const retryAfterMs = response.status === 429 ? retryHintMs(response.headers) : undefined;
|
|
102
127
|
let envelope;
|
|
103
128
|
try {
|
|
104
129
|
envelope = (await response.json());
|
|
105
130
|
}
|
|
106
131
|
catch {
|
|
107
132
|
if (!response.ok) {
|
|
108
|
-
throw new ApiError({ httpStatus: response.status, message: `HTTP ${response.status} with non-JSON body
|
|
133
|
+
throw new ApiError({ httpStatus: response.status, message: `HTTP ${response.status} with non-JSON body`, retryAfterMs });
|
|
109
134
|
}
|
|
110
135
|
throw new NetworkError(`invalid JSON response from ${path}`);
|
|
111
136
|
}
|
|
@@ -116,6 +141,7 @@ export class TripoClient {
|
|
|
116
141
|
message: envelope.message,
|
|
117
142
|
suggestion: envelope.suggestion,
|
|
118
143
|
requestId: envelope.request_id,
|
|
144
|
+
retryAfterMs,
|
|
119
145
|
});
|
|
120
146
|
}
|
|
121
147
|
return envelope.data;
|
|
@@ -128,18 +154,23 @@ export class TripoClient {
|
|
|
128
154
|
return task;
|
|
129
155
|
}
|
|
130
156
|
/**
|
|
131
|
-
* Batch task query
|
|
132
|
-
*
|
|
157
|
+
* Batch task query, at most 100 ids per request. The server returns
|
|
158
|
+
* `{tasks: {id: detail}, missed: [id]}`.
|
|
133
159
|
*/
|
|
134
160
|
async listTasks(taskIds) {
|
|
135
|
-
const
|
|
136
|
-
const
|
|
161
|
+
const byId = {};
|
|
162
|
+
const missed = [];
|
|
163
|
+
for (let i = 0; i < taskIds.length; i += 100) {
|
|
164
|
+
const data = await this.request('POST', '/v3/tasks/list', { task_ids: taskIds.slice(i, i + 100) });
|
|
165
|
+
Object.assign(byId, data.tasks ?? {});
|
|
166
|
+
missed.push(...(data.missed ?? []));
|
|
167
|
+
}
|
|
137
168
|
for (const [id, task] of Object.entries(byId))
|
|
138
169
|
observeTask({ ...task, task_id: task.task_id ?? id }, this.region, this.baseUrl);
|
|
139
170
|
const tasks = taskIds
|
|
140
171
|
.filter((id) => byId[id])
|
|
141
172
|
.map((id) => ({ ...byId[id], task_id: byId[id].task_id ?? id }));
|
|
142
|
-
return { tasks, missed
|
|
173
|
+
return { tasks, missed };
|
|
143
174
|
}
|
|
144
175
|
getBalance() {
|
|
145
176
|
return this.request('GET', '/v3/account/balance');
|
|
@@ -169,3 +200,13 @@ export class TripoClient {
|
|
|
169
200
|
export function sleep(ms) {
|
|
170
201
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
171
202
|
}
|
|
203
|
+
/** Retry-After is in seconds; X-RateLimit-Reset is a unix timestamp in seconds. */
|
|
204
|
+
function retryHintMs(headers) {
|
|
205
|
+
const after = Number(headers.get('retry-after') ?? NaN);
|
|
206
|
+
if (Number.isFinite(after))
|
|
207
|
+
return after * 1000;
|
|
208
|
+
const reset = Number(headers.get('x-ratelimit-reset') ?? NaN);
|
|
209
|
+
if (Number.isFinite(reset))
|
|
210
|
+
return Math.max(reset * 1000 - Date.now(), 0);
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
package/dist/core/device-auth.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* /cli/device endpoints 404 and the caller falls back to paste login.
|
|
9
9
|
*/
|
|
10
10
|
import { randomBytes } from 'node:crypto';
|
|
11
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { chmodSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { hostname } from 'node:os';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { REGION_DEFAULTS, tripoHome } from '../config/config.js';
|
|
@@ -148,6 +148,54 @@ export async function waitForDeviceApproval(region, grant, onTick) {
|
|
|
148
148
|
trackEvent('agent_auth_result', { result: 'expired', auth_method: 'device' }, region, REGION_DEFAULTS[region].api);
|
|
149
149
|
return { status: 'expired' };
|
|
150
150
|
}
|
|
151
|
+
/** A single poll without waiting; slow_down only means "asked too soon", so it reads as pending. */
|
|
152
|
+
export async function checkDeviceApproval(region, grant) {
|
|
153
|
+
const poll = await pollDeviceToken(region, grant.device_code);
|
|
154
|
+
if (poll.status === 'pending' || poll.status === 'slow_down')
|
|
155
|
+
return { status: 'pending' };
|
|
156
|
+
trackEvent('agent_auth_result', { result: poll.status === 'approved' ? 'success' : poll.status, auth_method: 'device' }, region, REGION_DEFAULTS[region].api);
|
|
157
|
+
return poll;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* A headless login must outlive its process: a chat agent can only relay the
|
|
161
|
+
* code by ending its turn, and its shell may be killed or suspended before the
|
|
162
|
+
* human approves. The grant is parked here (0600, removed once redeemed,
|
|
163
|
+
* denied, or expired) so the next `tripo login` resumes the code the human
|
|
164
|
+
* already has. device_code redeems the key — it never goes to stdout or logs.
|
|
165
|
+
*/
|
|
166
|
+
function pendingGrantFile() {
|
|
167
|
+
return join(tripoHome(), 'pending-login.json');
|
|
168
|
+
}
|
|
169
|
+
export function savePendingGrant(region, profile, grant) {
|
|
170
|
+
mkdirSync(tripoHome(), { recursive: true, mode: 0o700 });
|
|
171
|
+
const file = pendingGrantFile();
|
|
172
|
+
writeFileSync(file, JSON.stringify({ region, profile, expires_at: Date.now() + grant.expires_in * 1000, grant }) + '\n', { mode: 0o600 });
|
|
173
|
+
chmodSync(file, 0o600);
|
|
174
|
+
}
|
|
175
|
+
/** The parked grant for this region + profile, with its remaining lifetime as expires_in. */
|
|
176
|
+
export function loadPendingGrant(region, profile) {
|
|
177
|
+
let saved;
|
|
178
|
+
try {
|
|
179
|
+
saved = JSON.parse(readFileSync(pendingGrantFile(), 'utf8'));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const left = Math.floor(((saved.expires_at ?? 0) - Date.now()) / 1000);
|
|
185
|
+
if (left <= 0 || typeof saved.grant?.device_code !== 'string') {
|
|
186
|
+
clearPendingGrant();
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
if (saved.region !== region || saved.profile !== profile)
|
|
190
|
+
return undefined;
|
|
191
|
+
return { ...saved.grant, expires_in: left };
|
|
192
|
+
}
|
|
193
|
+
export function clearPendingGrant() {
|
|
194
|
+
try {
|
|
195
|
+
unlinkSync(pendingGrantFile());
|
|
196
|
+
}
|
|
197
|
+
catch { }
|
|
198
|
+
}
|
|
151
199
|
/**
|
|
152
200
|
* POST JSON and return the response envelope regardless of HTTP status —
|
|
153
201
|
* device-auth errors (4290/4291/4292) arrive as HTTP 400/429 with a JSON
|
package/dist/core/download.js
CHANGED
|
@@ -11,7 +11,7 @@ import { Readable } from 'node:stream';
|
|
|
11
11
|
import { pipeline } from 'node:stream/promises';
|
|
12
12
|
import { NetworkError } from './errors.js';
|
|
13
13
|
import { trackEvent, errorProperties } from './analytics.js';
|
|
14
|
-
const MODEL_EXTS = new Set(['.glb', '.gltf', '.fbx', '.obj', '.stl', '.3mf', '.usdz', '.zip', '.ply']);
|
|
14
|
+
const MODEL_EXTS = new Set(['.glb', '.gltf', '.fbx', '.obj', '.stl', '.3mf', '.usdz', '.zip', '.ply', '.splat']);
|
|
15
15
|
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp']);
|
|
16
16
|
/** Collect every downloadable URL in the output map (string or {url} object). */
|
|
17
17
|
export function collectOutputUrls(output) {
|
|
@@ -97,6 +97,7 @@ export async function downloadArtifacts(task, outputDir, historyEntry, options =
|
|
|
97
97
|
status: task.status,
|
|
98
98
|
input: task.input ?? historyEntry?.payload ?? {},
|
|
99
99
|
output_fields: urls.map((u) => u.field),
|
|
100
|
+
normalize_scale: task.output?.normalize_scale,
|
|
100
101
|
credits_consumed: task.credits_consumed,
|
|
101
102
|
created_at: task.created_at,
|
|
102
103
|
completed_at: task.completed_at,
|
package/dist/core/errors.js
CHANGED
|
@@ -25,8 +25,12 @@ export class ApiError extends CliError {
|
|
|
25
25
|
httpStatus;
|
|
26
26
|
requestId;
|
|
27
27
|
retryable;
|
|
28
|
+
/** Server wait hint on 429 (Retry-After / X-RateLimit-Reset). */
|
|
29
|
+
retryAfterMs;
|
|
28
30
|
constructor(opts) {
|
|
29
|
-
|
|
31
|
+
// HTTP 401 reads as an auth failure whatever code the body carries (see error-catalog).
|
|
32
|
+
const code = opts.httpStatus === 401 ? 1002 : opts.apiCode;
|
|
33
|
+
const info = code !== undefined ? lookupError(code) : undefined;
|
|
30
34
|
const translated = info ? t(info) : undefined;
|
|
31
35
|
const parts = [translated ?? opts.message ?? `API error (HTTP ${opts.httpStatus})`];
|
|
32
36
|
if (translated && opts.message && opts.message !== translated)
|
|
@@ -36,6 +40,7 @@ export class ApiError extends CliError {
|
|
|
36
40
|
this.httpStatus = opts.httpStatus;
|
|
37
41
|
this.requestId = opts.requestId;
|
|
38
42
|
this.retryable = isRetryable(opts.apiCode, opts.httpStatus);
|
|
43
|
+
this.retryAfterMs = opts.retryAfterMs;
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
46
|
export class NetworkError extends CliError {
|
|
@@ -52,8 +57,8 @@ export class TaskFailedError extends CliError {
|
|
|
52
57
|
en: 'frozen credits have been refunded automatically',
|
|
53
58
|
});
|
|
54
59
|
super(
|
|
55
|
-
//
|
|
56
|
-
`task ${taskId} ${status}${detail ? `: ${detail}` : ''}${status === '
|
|
60
|
+
// Only failed/cancelled tasks are refunded; a timed-out task is still running server-side.
|
|
61
|
+
`task ${taskId} ${status}${detail ? `: ${detail}` : ''}${status === 'failed' || status === 'cancelled' ? ` (${refundNote})` : ''}`, EXIT.TASK_FAILED);
|
|
57
62
|
this.taskId = taskId;
|
|
58
63
|
this.status = status;
|
|
59
64
|
}
|
package/dist/core/requests.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `parameters`), so new API options work without a CLI release.
|
|
6
6
|
*/
|
|
7
7
|
import { getStep } from '../knowledge/chains.js';
|
|
8
|
-
import { validateConvertParams, validateDecimateParams, validateGenerationParams, validateMeshEditParams, validateRetargetParams, } from '../knowledge/params.js';
|
|
8
|
+
import { applyTextureV35, validateConvertParams, validateDecimateParams, validateGenerationParams, validateMeshEditParams, validateRetargetParams, } from '../knowledge/params.js';
|
|
9
9
|
import { CAPABILITY_DEFAULTS, SEGMENT_MODELS, TEXTURE_MODELS, normalizeModel, normalizeRigModel, } from '../knowledge/models.js';
|
|
10
10
|
import { UsageError } from './errors.js';
|
|
11
11
|
function ensure(result) {
|
|
@@ -39,17 +39,26 @@ export function buildImageToModel(input, model, params) {
|
|
|
39
39
|
warnings: validated.warnings,
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* 3D generation: multiview-to-model. Views ordered [front, left, back, right],
|
|
44
|
+
* or `{task_id}` of an image-to-multiview / edit-multiview task to reuse its 4 views.
|
|
45
|
+
*/
|
|
43
46
|
export function buildMultiviewToModel(views, model, params) {
|
|
44
47
|
const validated = validateGenerationParams(model, params);
|
|
45
48
|
ensure(validated);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
let inputs;
|
|
50
|
+
if ('task_id' in views) {
|
|
51
|
+
inputs = [{ task_id: views.task_id }];
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
inputs = ['front', 'left', 'back', 'right']
|
|
55
|
+
.filter((v) => views[v])
|
|
56
|
+
.map((v) => ({ [v]: views[v] }));
|
|
57
|
+
if (!views.front)
|
|
58
|
+
throw new UsageError('multiview requires a front view');
|
|
59
|
+
if (inputs.length < 2)
|
|
60
|
+
throw new UsageError('multiview requires at least 2 views');
|
|
61
|
+
}
|
|
53
62
|
return {
|
|
54
63
|
request: {
|
|
55
64
|
endpoint: '/v3/generation/multiview-to-model',
|
|
@@ -72,11 +81,16 @@ export function buildTextToImage(prompt, params) {
|
|
|
72
81
|
warnings: [],
|
|
73
82
|
};
|
|
74
83
|
}
|
|
75
|
-
|
|
84
|
+
/** One reference image goes as `input`, several as `inputs` (prompt cites them as image[1], image[2], ...). */
|
|
85
|
+
export function buildImageToImage(inputs, params) {
|
|
76
86
|
return {
|
|
77
87
|
request: {
|
|
78
88
|
endpoint: '/v3/generation/image-to-image',
|
|
79
|
-
payload: {
|
|
89
|
+
payload: {
|
|
90
|
+
...(inputs.length > 1 ? { inputs } : { input: inputs[0] }),
|
|
91
|
+
model: params.model ?? CAPABILITY_DEFAULTS.imageToImage,
|
|
92
|
+
...omit(params, ['model']),
|
|
93
|
+
},
|
|
80
94
|
},
|
|
81
95
|
taskType: 'generate_image',
|
|
82
96
|
warnings: [],
|
|
@@ -150,13 +164,18 @@ export function buildStepRequest(stepName, input, args) {
|
|
|
150
164
|
break;
|
|
151
165
|
}
|
|
152
166
|
case 'texture': {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
167
|
+
if (params.model !== undefined) {
|
|
168
|
+
const model = normalizeModel(String(params.model));
|
|
169
|
+
if (!TEXTURE_MODELS.includes(model)) {
|
|
170
|
+
throw new UsageError(`texture model must be one of ${TEXTURE_MODELS.join('/')} (got "${String(params.model)}")`);
|
|
171
|
+
}
|
|
172
|
+
params.model = model;
|
|
158
173
|
}
|
|
159
|
-
|
|
174
|
+
const errors = [];
|
|
175
|
+
applyTextureV35(params, 'model', warnings, errors);
|
|
176
|
+
ensure({ params, warnings, errors });
|
|
177
|
+
if (params.model === undefined)
|
|
178
|
+
params.model = CAPABILITY_DEFAULTS.texture;
|
|
160
179
|
break;
|
|
161
180
|
}
|
|
162
181
|
case 'segment': {
|
|
@@ -139,6 +139,8 @@ export async function runTask(options) {
|
|
|
139
139
|
status: task.status,
|
|
140
140
|
credits_consumed: task.credits_consumed,
|
|
141
141
|
};
|
|
142
|
+
if (typeof task.output?.normalize_scale === 'number')
|
|
143
|
+
result.normalize_scale = task.output.normalize_scale;
|
|
142
144
|
if (!options.noDownload) {
|
|
143
145
|
const baseDir = options.outDir ?? loadContext().output_dir ?? process.cwd();
|
|
144
146
|
const dir = defaultOutputDir(baseDir, name, taskId);
|
package/dist/core/upload.js
CHANGED
|
@@ -5,6 +5,8 @@ import { UsageError } from './errors.js';
|
|
|
5
5
|
import { IMAGE_EXTS, MODEL_EXTS } from '../util/input-detect.js';
|
|
6
6
|
const IMAGE_MAX = 20 * 1024 * 1024;
|
|
7
7
|
const MODEL_MAX = 150 * 1024 * 1024;
|
|
8
|
+
/** Above this size the docs recommend the presigned direct upload. */
|
|
9
|
+
const PRESIGN_THRESHOLD = 60 * 1024 * 1024;
|
|
8
10
|
const MIME = {
|
|
9
11
|
'.png': 'image/png',
|
|
10
12
|
'.jpg': 'image/jpeg',
|
|
@@ -25,18 +27,34 @@ export async function uploadLocalFile(client, filePath) {
|
|
|
25
27
|
if (!isImage && !isModel) {
|
|
26
28
|
throw new UsageError(`unsupported file type "${ext}" for upload`);
|
|
27
29
|
}
|
|
28
|
-
// The server's upload whitelist has no gltf: a .gltf references external
|
|
29
|
-
// .bin/texture files, so a single-file upload would be incomplete anyway.
|
|
30
|
-
if (ext === '.gltf') {
|
|
31
|
-
throw new UsageError('the upload API does not accept .gltf (it references external files) — export as .glb, or host it and pass the URL');
|
|
32
|
-
}
|
|
33
30
|
const info = await stat(filePath);
|
|
34
31
|
const limit = isImage ? IMAGE_MAX : MODEL_MAX;
|
|
35
32
|
if (info.size > limit) {
|
|
36
33
|
throw new UsageError(`${basename(filePath)} is ${(info.size / 1024 / 1024).toFixed(1)}MB, exceeding the ${limit / 1024 / 1024}MB ${isImage ? 'image' : 'model'} limit`);
|
|
37
34
|
}
|
|
38
35
|
const bytes = await readFile(filePath);
|
|
36
|
+
// .gltf goes through presign: the multipart whitelist has rejected it, the presign one lists it.
|
|
37
|
+
if (ext === '.gltf')
|
|
38
|
+
assertSelfContainedGltf(filePath, bytes);
|
|
39
|
+
if (ext === '.gltf' || info.size > PRESIGN_THRESHOLD) {
|
|
40
|
+
const { file_token } = await client.uploadFileViaPresign(ext.slice(1), new Blob([bytes]));
|
|
41
|
+
return file_token;
|
|
42
|
+
}
|
|
39
43
|
const blob = new Blob([bytes], { type: MIME[ext] ?? 'application/octet-stream' });
|
|
40
44
|
const { file_token } = await client.uploadFile(basename(filePath), blob);
|
|
41
45
|
return file_token;
|
|
42
46
|
}
|
|
47
|
+
/** A single-file upload cannot carry a .gltf's external .bin/texture files. */
|
|
48
|
+
function assertSelfContainedGltf(filePath, bytes) {
|
|
49
|
+
let doc;
|
|
50
|
+
try {
|
|
51
|
+
doc = JSON.parse(bytes.toString('utf8'));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new UsageError(`${basename(filePath)} is not valid glTF JSON`);
|
|
55
|
+
}
|
|
56
|
+
const external = [...(doc.buffers ?? []), ...(doc.images ?? [])].some((ref) => typeof ref.uri === 'string' && !ref.uri.startsWith('data:'));
|
|
57
|
+
if (external) {
|
|
58
|
+
throw new UsageError(`${basename(filePath)} references external files (.bin/textures) — export as .glb, or host it and pass the URL`);
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/core/watch.js
CHANGED
|
@@ -36,6 +36,9 @@ export async function watchTask(client, taskId, options = {}) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
function extractFailureReason(task) {
|
|
39
|
+
if (task.error_message) {
|
|
40
|
+
return task.error_code !== undefined ? `${task.error_message} (code ${task.error_code})` : task.error_message;
|
|
41
|
+
}
|
|
39
42
|
const out = task.output ?? {};
|
|
40
43
|
const candidate = out.message ?? out.error ?? out.reason;
|
|
41
44
|
return typeof candidate === 'string' ? candidate : undefined;
|
package/dist/knowledge/chains.js
CHANGED
|
@@ -22,7 +22,6 @@ export const MODEL_PRODUCING_TYPES = new Set([
|
|
|
22
22
|
'animate_retarget',
|
|
23
23
|
]);
|
|
24
24
|
export const STEPS = [
|
|
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
25
|
{ name: 'texture', endpoint: '/v3/models/texture', produces: 'texture_model', accepts: 'any-model', acceptsFile: true, describe: { zh: '重新生成贴图 / PBR 材质', en: 'regenerate textures / PBR materials' } },
|
|
27
26
|
{ name: 'convert', endpoint: '/v3/models/convert', produces: 'convert_model', accepts: 'any-model', acceptsFile: true, describe: { zh: '转换输出格式 / 贴图尺寸 / 朝向', en: 'convert format / texture size / orientation' } },
|
|
28
27
|
{ name: 'import', endpoint: '/v3/models/import', produces: 'import_model', accepts: [], acceptsFile: true, describe: { zh: '导入本地模型文件作为加工起点', en: 'import a local model file as the chain source' } },
|
|
@@ -47,9 +46,10 @@ for (const s of STEPS) {
|
|
|
47
46
|
stepIndex.set(a, s);
|
|
48
47
|
}
|
|
49
48
|
/**
|
|
50
|
-
* `/v3/mesh/edit`
|
|
51
|
-
*
|
|
52
|
-
* to opt in
|
|
49
|
+
* `/v3/mesh/edit` has a reference page that is still left out of the docs
|
|
50
|
+
* index, and cn returned 4001 when last checked. The step stays hidden; set
|
|
51
|
+
* TRIPO_EXPERIMENTAL_MESH_EDIT=1 to opt in. Flip this default once the
|
|
52
|
+
* endpoint is listed and live.
|
|
53
53
|
*/
|
|
54
54
|
export function isMeshEditEnabled() {
|
|
55
55
|
return process.env.TRIPO_EXPERIMENTAL_MESH_EDIT === '1';
|
|
@@ -171,7 +171,6 @@ export function validateChain(chain, sourceType) {
|
|
|
171
171
|
}
|
|
172
172
|
function hintFor(step) {
|
|
173
173
|
const hints = {
|
|
174
|
-
refine: ' (refine only accepts a successful text-to-model task)',
|
|
175
174
|
complete: ' (run "segment" first: complete only accepts mesh segmentation tasks)',
|
|
176
175
|
retarget: ' (run "rig" first: retarget only accepts rigged tasks)',
|
|
177
176
|
};
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* CLI exit code each error maps to.
|
|
4
4
|
*
|
|
5
5
|
* Source of truth: backend ErrorCode enum (platform-common .../error/ErrorCode.java,
|
|
6
|
-
* itself aligned with openapi-server errors.gen.go).
|
|
7
|
-
*
|
|
8
|
-
* 1000/1001
|
|
6
|
+
* itself aligned with openapi-server errors.gen.go). The public docs call
|
|
7
|
+
* 1000/1001 auth errors while the backend sends 1002 for auth failures and
|
|
8
|
+
* 1000/1001 for 5xx server errors — so HTTP 401 wins over the code table.
|
|
9
9
|
*/
|
|
10
10
|
export const EXIT = {
|
|
11
11
|
OK: 0,
|
|
@@ -29,9 +29,9 @@ export const ERROR_CATALOG = [
|
|
|
29
29
|
{ code: 1007, zh: '请求过于频繁,触发全局速率限制', en: 'Rate limit exceeded — too many requests in a short time', retryable: true, exitCode: EXIT.RATE_LIMIT },
|
|
30
30
|
{ code: 2000, zh: '触发生成并发上限,同系列任务共享并发池', en: 'Generation concurrency limit hit (task families share a pool)', retryable: true, exitCode: EXIT.RATE_LIMIT },
|
|
31
31
|
{ code: 2001, zh: '任务不存在(确认任务属于当前 Key、区域正确)', en: 'Task not found (same key that created it? right region?)', retryable: false, exitCode: EXIT.NOT_FOUND },
|
|
32
|
-
{ code: 2002, zh: '
|
|
33
|
-
{ code: 2003, zh: '
|
|
34
|
-
{ code: 2004, zh: '
|
|
32
|
+
{ code: 2002, zh: '不支持的请求参数,请检查字段名和取值', en: 'Unsupported request parameter (check field names and values)', retryable: false, exitCode: EXIT.USAGE },
|
|
33
|
+
{ code: 2003, zh: '输入文件为空,请检查文件是否损坏', en: 'The input file is empty (corrupted upload?)', retryable: false, exitCode: EXIT.USAGE },
|
|
34
|
+
{ code: 2004, zh: '不支持的文件类型', en: 'Unsupported file type', retryable: false, exitCode: EXIT.USAGE },
|
|
35
35
|
{ code: 2005, zh: 'draft 任务不是成功状态,请用成功的任务重试', en: 'The draft task is not a success; retry with a successful one', retryable: false, exitCode: EXIT.USAGE },
|
|
36
36
|
{ code: 2006, zh: '上游任务类型不受支持,请检查任务链', en: 'The original task type is not supported for this operation', retryable: false, exitCode: EXIT.USAGE },
|
|
37
37
|
{ code: 2007, zh: '上游任务不是成功状态', en: 'The original task has not succeeded', retryable: false, exitCode: EXIT.USAGE },
|
|
@@ -58,13 +58,13 @@ export function lookupError(code) {
|
|
|
58
58
|
}
|
|
59
59
|
/** Map an API error code + HTTP status to a stable CLI exit code. */
|
|
60
60
|
export function exitCodeFor(apiCode, httpStatus) {
|
|
61
|
+
if (httpStatus === 401)
|
|
62
|
+
return EXIT.AUTH;
|
|
61
63
|
if (apiCode !== undefined) {
|
|
62
64
|
const info = byCode.get(apiCode);
|
|
63
65
|
if (info)
|
|
64
66
|
return info.exitCode;
|
|
65
67
|
}
|
|
66
|
-
if (httpStatus === 401)
|
|
67
|
-
return EXIT.AUTH;
|
|
68
68
|
if (httpStatus === 403)
|
|
69
69
|
return EXIT.AUTH;
|
|
70
70
|
if (httpStatus === 404)
|
|
@@ -78,6 +78,8 @@ export function exitCodeFor(apiCode, httpStatus) {
|
|
|
78
78
|
return EXIT.FATAL;
|
|
79
79
|
}
|
|
80
80
|
export function isRetryable(apiCode, httpStatus) {
|
|
81
|
+
if (httpStatus === 401)
|
|
82
|
+
return false;
|
|
81
83
|
if (apiCode !== undefined) {
|
|
82
84
|
const info = byCode.get(apiCode);
|
|
83
85
|
if (info)
|