tripo-cli 0.7.2 → 0.8.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +16 -14
  3. package/dist/ai/experts.js +26 -20
  4. package/dist/ai/plan-card.js +73 -34
  5. package/dist/ai/wizard.js +19 -19
  6. package/dist/commands/account.js +2 -6
  7. package/dist/commands/ai.js +18 -16
  8. package/dist/commands/batch.js +58 -18
  9. package/dist/commands/completion.js +1 -1
  10. package/dist/commands/config-cmd.js +1 -1
  11. package/dist/commands/doctor.js +1 -3
  12. package/dist/commands/generate.js +50 -8
  13. package/dist/commands/home.js +12 -13
  14. package/dist/commands/login.js +125 -182
  15. package/dist/commands/make.js +50 -25
  16. package/dist/commands/mcp.js +8 -3
  17. package/dist/commands/process-groups.js +29 -18
  18. package/dist/commands/shared.js +22 -13
  19. package/dist/commands/task.js +12 -11
  20. package/dist/commands/topup.js +6 -10
  21. package/dist/commands/update.js +23 -11
  22. package/dist/commands/view.js +3 -4
  23. package/dist/config/config.js +1 -1
  24. package/dist/core/analytics.js +39 -16
  25. package/dist/core/client.js +53 -21
  26. package/dist/core/device-auth.js +49 -1
  27. package/dist/core/download.js +2 -1
  28. package/dist/core/errors.js +24 -14
  29. package/dist/core/output.js +0 -4
  30. package/dist/core/requests.js +94 -31
  31. package/dist/core/task-service.js +21 -20
  32. package/dist/core/upload.js +23 -5
  33. package/dist/core/watch.js +3 -0
  34. package/dist/knowledge/chains.js +16 -17
  35. package/dist/knowledge/error-catalog.js +39 -37
  36. package/dist/knowledge/models.js +147 -27
  37. package/dist/knowledge/params.js +143 -32
  38. package/dist/knowledge/scenarios.js +32 -55
  39. package/dist/pipeline/make.js +38 -30
  40. package/dist/pipeline/runner.js +27 -8
  41. package/dist/pipeline/stdin.js +40 -3
  42. package/dist/program.js +3 -10
  43. package/dist/util/input-detect.js +10 -1
  44. package/package.json +3 -2
  45. package/skill/SKILL.md +52 -15
  46. package/skill/commands/account.md +8 -3
  47. package/skill/commands/batch.md +13 -3
  48. package/skill/commands/generate.md +57 -23
  49. package/skill/commands/make.md +43 -16
  50. package/skill/commands/process.md +29 -24
  51. package/skill/commands/task.md +4 -4
  52. package/skill/commands/view.md +1 -1
  53. package/skill/common-errors.md +14 -10
  54. package/skill/examples/animation.md +4 -2
  55. package/skill/examples/ar-web.md +6 -4
  56. package/skill/examples/game-asset.md +18 -7
  57. package/skill/examples/pipes.md +2 -2
  58. package/skill/examples/print.md +7 -3
  59. package/dist/i18n.js +0 -19
@@ -5,8 +5,8 @@
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';
9
- import { CAPABILITY_DEFAULTS, SEGMENT_MODELS, TEXTURE_MODELS, normalizeModel, normalizeRigModel, } from '../knowledge/models.js';
8
+ import { applyTextureV35, validateConvertParams, validateDecimateParams, validateGenerationParams, validateImageParams, validateMeshEditParams, validateRetargetParams, } from '../knowledge/params.js';
9
+ import { CAPABILITY_DEFAULTS, RIG_MODEL_V1, RIG_MODEL_V25, RIG_MODEL_V30, RIG_SPECS, RIG_TYPES, SEGMENT_MODELS, TEXTURE_MODELS, normalizeModel, normalizeRigModel, } from '../knowledge/models.js';
10
10
  import { UsageError } from './errors.js';
11
11
  function ensure(result) {
12
12
  if (result.errors.length > 0) {
@@ -15,12 +15,12 @@ function ensure(result) {
15
15
  }
16
16
  /** 3D generation: text-to-model. */
17
17
  export function buildTextToModel(prompt, model, params) {
18
- const validated = validateGenerationParams(model, params);
18
+ const validated = validateGenerationParams(model, { prompt, ...params });
19
19
  ensure(validated);
20
20
  return {
21
21
  request: {
22
22
  endpoint: '/v3/generation/text-to-model',
23
- payload: { prompt, model: normalizeModel(model), ...validated.params },
23
+ payload: { prompt, ...validated.params, model: normalizeModel(model) },
24
24
  },
25
25
  taskType: 'text_to_model',
26
26
  warnings: validated.warnings,
@@ -33,27 +33,36 @@ export function buildImageToModel(input, model, params) {
33
33
  return {
34
34
  request: {
35
35
  endpoint: '/v3/generation/image-to-model',
36
- payload: { input, model: normalizeModel(model), ...validated.params },
36
+ payload: { input, ...validated.params, model: normalizeModel(model) },
37
37
  },
38
38
  taskType: 'image_to_model',
39
39
  warnings: validated.warnings,
40
40
  };
41
41
  }
42
- /** 3D generation: multiview-to-model. Views ordered [front, left, back, right]. */
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
- const inputs = ['front', 'left', 'back', 'right']
47
- .filter((v) => views[v])
48
- .map((v) => ({ [v]: views[v] }));
49
- if (!views.front)
50
- throw new UsageError('multiview requires a front view');
51
- if (inputs.length < 2)
52
- throw new UsageError('multiview requires at least 2 views');
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',
56
- payload: { inputs, model: normalizeModel(model), ...validated.params },
65
+ payload: { inputs, ...validated.params, model: normalizeModel(model) },
57
66
  },
58
67
  taskType: 'multiview_to_model',
59
68
  warnings: validated.warnings,
@@ -63,23 +72,38 @@ export function buildMultiviewToModel(views, model, params) {
63
72
  * Server task types (TaskType enum): text-to-image → `text_to_image`,
64
73
  * image-to-image → `generate_image`. */
65
74
  export function buildTextToImage(prompt, params) {
75
+ const validated = validateImageParams(params, false);
76
+ ensure(validated);
66
77
  return {
67
78
  request: {
68
79
  endpoint: '/v3/generation/text-to-image',
69
- payload: { prompt, model: params.model ?? CAPABILITY_DEFAULTS.textToImage, ...omit(params, ['model']) },
80
+ payload: { prompt, ...validated.params },
70
81
  },
71
82
  taskType: 'text_to_image',
72
- warnings: [],
83
+ warnings: validated.warnings,
73
84
  };
74
85
  }
75
- export function buildImageToImage(input, params) {
86
+ /** One reference image goes as `input`, several as `inputs` (prompt cites them as image[1], image[2], ...). */
87
+ export function buildImageToImage(inputs, params) {
88
+ const validated = validateImageParams(params, true);
89
+ ensure(validated);
90
+ const model = String(validated.params.model);
91
+ const maxInputs = model.startsWith('seedream') ? 4 : model.startsWith('banana') ? 10 : 16;
92
+ if (inputs.length === 0)
93
+ throw new UsageError('image-to-image requires at least one reference image');
94
+ if (inputs.length > maxInputs) {
95
+ throw new UsageError(`${model} accepts at most ${maxInputs} reference images (got ${inputs.length})`);
96
+ }
76
97
  return {
77
98
  request: {
78
99
  endpoint: '/v3/generation/image-to-image',
79
- payload: { input, model: params.model ?? CAPABILITY_DEFAULTS.imageToImage, ...omit(params, ['model']) },
100
+ payload: {
101
+ ...(inputs.length > 1 ? { inputs } : { input: inputs[0] }),
102
+ ...validated.params,
103
+ },
80
104
  },
81
105
  taskType: 'generate_image',
82
- warnings: [],
106
+ warnings: validated.warnings,
83
107
  };
84
108
  }
85
109
  export function buildImageToMultiview(input, params) {
@@ -103,6 +127,27 @@ export function buildImageToSplat(input, params) {
103
127
  warnings: [],
104
128
  };
105
129
  }
130
+ /** Validate deterministic request and rig/preset constraints before any earlier task runs. */
131
+ export function validateChainRequests(chain) {
132
+ let rigModel;
133
+ for (const spec of chain) {
134
+ const built = buildStepRequest(spec.step, '<upstream>', spec.args);
135
+ if (built.taskType === 'animate_retarget' && rigModel) {
136
+ const { animation, animations } = built.request.payload;
137
+ const requested = Array.isArray(animations) ? animations : [animation];
138
+ for (const preset of requested) {
139
+ if (typeof preset !== 'string' || !preset.startsWith('preset:'))
140
+ continue;
141
+ if ((rigModel === RIG_MODEL_V1) !== preset.startsWith('preset:biped:')) {
142
+ throw new UsageError(`preset ${preset} is incompatible with rig model ${rigModel}; v1.0 uses preset:biped:*, v2.5/v3.0 use generic or body-type presets`);
143
+ }
144
+ }
145
+ }
146
+ if (!getStep(spec.step)?.passthrough) {
147
+ rigModel = built.taskType === 'animate_rig' ? String(built.request.payload.model) : undefined;
148
+ }
149
+ }
150
+ }
106
151
  /**
107
152
  * Processing step (texture/convert/rig/...): validate per step, then build.
108
153
  * `input` must already be a task_id / file_token / URL.
@@ -147,16 +192,40 @@ export function buildStepRequest(stepName, input, args) {
147
192
  params.model = params.model === undefined
148
193
  ? CAPABILITY_DEFAULTS.rig
149
194
  : normalizeRigModel(String(params.model));
195
+ if (![RIG_MODEL_V1, RIG_MODEL_V25, RIG_MODEL_V30].includes(String(params.model))) {
196
+ throw new UsageError('unsupported rig model');
197
+ }
198
+ const rigType = params.rig_type === undefined ? 'biped' : params.rig_type;
199
+ if (!RIG_TYPES.includes(rigType))
200
+ throw new UsageError('unsupported rig_type');
201
+ if (params.model === RIG_MODEL_V1 && rigType !== 'biped')
202
+ throw new UsageError('rig v1.0 only supports biped');
203
+ if (rigType === 'others' && params.model !== RIG_MODEL_V30)
204
+ throw new UsageError('rig_type others requires rig v3.0');
205
+ if (params.spec !== undefined) {
206
+ if (!RIG_SPECS.includes(params.spec))
207
+ throw new UsageError('unsupported rig spec');
208
+ if (rigType !== 'biped')
209
+ throw new UsageError('rig spec only applies to biped');
210
+ if (!['tripo', 'mixamo'].includes(String(params.spec)) && params.model !== RIG_MODEL_V30) {
211
+ throw new UsageError('spec actorcore/unity/unreal/vrm requires rig v3.0');
212
+ }
213
+ }
150
214
  break;
151
215
  }
152
216
  case 'texture': {
153
- const model = params.model === undefined
154
- ? CAPABILITY_DEFAULTS.texture
155
- : normalizeModel(String(params.model));
156
- if (!TEXTURE_MODELS.includes(model)) {
157
- throw new UsageError(`texture model must be one of ${TEXTURE_MODELS.join('/')} (got "${String(params.model)}")`);
217
+ if (params.model !== undefined) {
218
+ const model = normalizeModel(String(params.model));
219
+ if (!TEXTURE_MODELS.includes(model)) {
220
+ throw new UsageError(`texture model must be one of ${TEXTURE_MODELS.join('/')} (got "${String(params.model)}")`);
221
+ }
222
+ params.model = model;
158
223
  }
159
- params.model = model;
224
+ const errors = [];
225
+ applyTextureV35(params, 'model', warnings, errors);
226
+ ensure({ params, warnings, errors });
227
+ if (params.model === undefined)
228
+ params.model = CAPABILITY_DEFAULTS.texture;
160
229
  break;
161
230
  }
162
231
  case 'segment': {
@@ -190,9 +259,3 @@ export function buildStepRequest(stepName, input, args) {
190
259
  warnings,
191
260
  };
192
261
  }
193
- function omit(obj, keys) {
194
- const out = { ...obj };
195
- for (const k of keys)
196
- delete out[k];
197
- return out;
198
- }
@@ -8,13 +8,12 @@ import { basename } from 'node:path';
8
8
  import { resolvePlatformBaseUrl, resolveRegion } from '../config/config.js';
9
9
  import { loadContext, updateContext } from '../config/context.js';
10
10
  import { RefError, appendHistory, readHistory, resolveReference, slugify, updateHistoryStatus } from '../config/history.js';
11
- import { t } from '../i18n.js';
12
11
  import { detectInput } from '../util/input-detect.js';
13
12
  import { notifyDone } from '../util/notify.js';
14
13
  import { createSpinner } from '../util/spinner.js';
15
14
  import { defaultOutputDir, downloadArtifacts } from './download.js';
16
15
  import { taskOrigin } from './analytics.js';
17
- import { ApiError, CliError, TaskFailedError, UsageError, balanceSuggestion } from './errors.js';
16
+ import { ApiError, ArtifactDownloadError, CliError, TaskFailedError, UsageError, balanceSuggestion } from './errors.js';
18
17
  import { isQuiet, log, warn } from './output.js';
19
18
  import { uploadLocalFile } from './upload.js';
20
19
  /** Placeholder `input` value for a local file that a dry run would upload. */
@@ -75,10 +74,7 @@ export async function precheckBalance(client) {
75
74
  throw new ApiError({ apiCode: 2010, suggestion: balanceSuggestion(undefined, balance) });
76
75
  }
77
76
  if (balance < 30) {
78
- warn(t({
79
- zh: `余额仅剩 ${balance} 积分,任务可能因余额不足失败(tripo topup 充值)`,
80
- en: `balance is down to ${balance} credits; tasks may fail (run "tripo topup")`,
81
- }));
77
+ warn(`balance is down to ${balance} credits; tasks may fail (run "tripo topup")`);
82
78
  }
83
79
  }
84
80
  /** Submit one task and (by default) watch it to completion + download. */
@@ -87,6 +83,8 @@ export async function runTask(options) {
87
83
  if (options.precheckBalance !== false)
88
84
  await precheckBalance(client);
89
85
  const taskId = await client.createTask(request.endpoint, request.payload, taskType);
86
+ // Report the task before any local bookkeeping that can throw: batch decides retry safety from it.
87
+ options.onCreated?.({ task_id: taskId, type: taskType });
90
88
  const name = uniqueName(slugify(options.name));
91
89
  const entry = {
92
90
  analytics_origin: taskOrigin(request.payload.model),
@@ -99,8 +97,13 @@ export async function runTask(options) {
99
97
  payload: request.payload,
100
98
  };
101
99
  appendHistory(entry);
102
- updateContext({ last_task_id: taskId });
103
- log(t({ zh: `任务已创建 ${taskId} (@${name})`, en: `task created ${taskId} (@${name})` }));
100
+ try {
101
+ updateContext({ last_task_id: taskId });
102
+ }
103
+ catch (err) {
104
+ warn(`could not update .tripo/context.json (${err instanceof Error ? err.message : String(err)})`);
105
+ }
106
+ log(`task created ${taskId} (@${name})`);
104
107
  if (options.noWait) {
105
108
  return { task_id: taskId, type: taskType, status: 'queued' };
106
109
  }
@@ -121,17 +124,14 @@ export async function runTask(options) {
121
124
  updateHistoryStatus(taskId, 'failed');
122
125
  }
123
126
  else if (err instanceof CliError) {
124
- err.suggestion = t({
125
- zh: `任务仍在服务端运行,成功后照常扣费;用 tripo task watch ${taskId} --download 继续等待,不要重新提交`,
126
- en: `the task keeps running server-side and is billed if it succeeds; resume with "tripo task watch ${taskId} --download" instead of resubmitting`,
127
- });
127
+ err.suggestion = `the task keeps running server-side and is billed if it succeeds; resume with "tripo task watch ${taskId} --download" instead of resubmitting`;
128
128
  }
129
129
  throw err;
130
130
  }
131
131
  updateHistoryStatus(taskId, 'success', { credits_consumed: task.credits_consumed });
132
132
  spinner.succeed(`${taskType} · ${taskId}` +
133
133
  (task.credits_consumed !== undefined
134
- ? t({ zh: ` · 消耗 ${task.credits_consumed} 积分`, en: ` · ${task.credits_consumed} credits` })
134
+ ? ` · ${task.credits_consumed} credits`
135
135
  : ''));
136
136
  const result = {
137
137
  task_id: taskId,
@@ -139,15 +139,17 @@ 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);
145
- const dlSpinner = createSpinner(t({ zh: '下载产物…', en: 'downloading artifacts…' }), isQuiet());
147
+ const dlSpinner = createSpinner('downloading artifacts…', isQuiet());
146
148
  try {
147
149
  const artifacts = await downloadArtifacts(task, dir, entry, {
148
- onFile: (f) => dlSpinner.update(t({ zh: `下载 ${f}`, en: `downloading ${f}` })),
150
+ onFile: (f) => dlSpinner.update(`downloading ${f}`),
149
151
  });
150
- dlSpinner.succeed(t({ zh: `产物已保存 ${artifacts.outputDir}`, en: `artifacts saved to ${artifacts.outputDir}` }));
152
+ dlSpinner.succeed(`artifacts saved to ${artifacts.outputDir}`);
151
153
  result.output_dir = artifacts.outputDir;
152
154
  result.files = artifacts.files.map((f) => basename(f));
153
155
  result.model_file = artifacts.modelFile;
@@ -155,13 +157,12 @@ export async function runTask(options) {
155
157
  updateHistoryStatus(taskId, 'success', { output_dir: artifacts.outputDir });
156
158
  }
157
159
  catch (err) {
158
- dlSpinner.fail(t({ zh: '产物下载失败(任务本身已成功)', en: 'artifact download failed (the task itself succeeded)' }));
159
- warn(String(err instanceof Error ? err.message : err));
160
- warn(t({ zh: `可重试:tripo task get ${taskId} --download`, en: `retry with: tripo task get ${taskId} --download` }));
160
+ dlSpinner.fail('artifact download failed (the task itself succeeded)');
161
+ throw new ArtifactDownloadError(taskId, result, err instanceof Error ? err.message : String(err));
161
162
  }
162
163
  }
163
164
  if (options.notify) {
164
- notifyDone('Tripo', t({ zh: `${taskType} 完成`, en: `${taskType} finished` }));
165
+ notifyDone('Tripo', `${taskType} finished`);
165
166
  }
166
167
  return result;
167
168
  }
@@ -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
+ }
@@ -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;
@@ -22,23 +22,22 @@ 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
- { name: 'texture', endpoint: '/v3/models/texture', produces: 'texture_model', accepts: 'any-model', acceptsFile: true, describe: { zh: '重新生成贴图 / PBR 材质', en: 'regenerate textures / PBR materials' } },
27
- { name: 'convert', endpoint: '/v3/models/convert', produces: 'convert_model', accepts: 'any-model', acceptsFile: true, describe: { zh: '转换输出格式 / 贴图尺寸 / 朝向', en: 'convert format / texture size / orientation' } },
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' } },
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)' } },
30
- { name: 'rig', endpoint: '/v3/animations/rig', produces: 'animate_rig', accepts: 'any-model', acceptsFile: true, describe: { zh: '自动绑定骨骼', en: 'auto-rig a skeleton' } },
31
- { name: 'retarget', endpoint: '/v3/animations/retarget', produces: 'animate_retarget', accepts: ['animate_rig'], acceptsFile: false, aliases: ['animate'], describe: { zh: '把预设动画重定向到绑骨模型', en: 'retarget preset animations onto the rigged model' } },
32
- { name: 'segment', endpoint: '/v3/mesh/segment', produces: 'mesh_segmentation', accepts: 'any-model', acceptsFile: true, describe: { zh: '把网格分割成部件', en: 'segment the mesh into parts' } },
33
- { name: 'complete', endpoint: '/v3/mesh/complete', produces: 'mesh_completion', accepts: ['mesh_segmentation'], acceptsFile: false, describe: { zh: '补全分割后的部件', en: 'complete the segmented parts' } },
34
- { name: 'decimate', endpoint: '/v3/mesh/decimate', produces: 'highpoly_to_lowpoly', accepts: 'any-model', acceptsFile: true, aliases: ['lowpoly'], describe: { zh: '减面到目标面数', en: 'decimate to a target face count' } },
35
- { name: 'uv', endpoint: '/v3/mesh/uv', produces: 'mesh_uv', accepts: 'any-model', acceptsFile: true, describe: { zh: '智能展 UV,原贴图/骨骼/动画映射到新布局', en: 'smart UV unwrap; remaps textures/rig/animation onto the new layout' } },
25
+ { name: 'texture', endpoint: '/v3/models/texture', produces: 'texture_model', accepts: 'any-model', acceptsFile: true, describe: 'regenerate textures / PBR materials' },
26
+ { name: 'convert', endpoint: '/v3/models/convert', produces: 'convert_model', accepts: 'any-model', acceptsFile: true, describe: 'convert format / texture size / orientation' },
27
+ { name: 'import', endpoint: '/v3/models/import', produces: 'import_model', accepts: [], acceptsFile: true, describe: 'import a local model file as the chain source' },
28
+ { name: 'rig-check', endpoint: '/v3/animations/rig-check', produces: 'animate_prerigcheck', accepts: 'any-model', acceptsFile: true, passthrough: true, aliases: ['check', 'rigcheck'], describe: 'check riggability (no model output; stops the chain if not riggable)' },
29
+ { name: 'rig', endpoint: '/v3/animations/rig', produces: 'animate_rig', accepts: 'any-model', acceptsFile: true, describe: 'auto-rig a skeleton' },
30
+ { name: 'retarget', endpoint: '/v3/animations/retarget', produces: 'animate_retarget', accepts: ['animate_rig'], acceptsFile: false, aliases: ['animate'], describe: 'retarget preset animations onto the rigged model' },
31
+ { name: 'segment', endpoint: '/v3/mesh/segment', produces: 'mesh_segmentation', accepts: 'any-model', acceptsFile: true, describe: 'segment the mesh into parts' },
32
+ { name: 'complete', endpoint: '/v3/mesh/complete', produces: 'mesh_completion', accepts: ['mesh_segmentation'], acceptsFile: false, describe: 'complete the segmented parts' },
33
+ { name: 'decimate', endpoint: '/v3/mesh/decimate', produces: 'highpoly_to_lowpoly', accepts: 'any-model', acceptsFile: true, aliases: ['lowpoly'], describe: 'decimate to a target face count' },
34
+ { name: 'uv', endpoint: '/v3/mesh/uv', produces: 'mesh_uv', accepts: 'any-model', acceptsFile: true, describe: 'smart UV unwrap; remaps textures/rig/animation onto the new layout' },
36
35
  // mesh/edit: local retry/refine inside a bbox, P2 pipeline (model fixed to P2-20260801).
37
- { name: 'edit', endpoint: '/v3/mesh/edit', produces: 'mesh_edit', accepts: 'any-model', acceptsFile: true, describe: { zh: '在 bbox 区域内局部重做(P2 管线)', en: 'redo one bbox region locally (P2 pipeline)' } },
36
+ { name: 'edit', endpoint: '/v3/mesh/edit', produces: 'mesh_edit', accepts: 'any-model', acceptsFile: true, describe: 'redo one bbox region locally (P2 pipeline)' },
38
37
  // smartsegment's `input` is file_token/URL only (the server does not resolve
39
38
  // task ids here), so it cannot chain after a task. Type is refined to
40
39
  // smartsegment_image|smartsegment_model at request-build time per seg_type.
41
- { name: 'smartsegment', endpoint: '/v3/mesh/smartsegment', produces: 'smartsegment_model', accepts: [], acceptsFile: true, describe: { zh: '智能语义分割', en: 'smart semantic segmentation' } },
40
+ { name: 'smartsegment', endpoint: '/v3/mesh/smartsegment', produces: 'smartsegment_model', accepts: [], acceptsFile: true, describe: 'smart semantic segmentation' },
42
41
  ];
43
42
  const stepIndex = new Map();
44
43
  for (const s of STEPS) {
@@ -47,9 +46,10 @@ for (const s of STEPS) {
47
46
  stepIndex.set(a, s);
48
47
  }
49
48
  /**
50
- * `/v3/mesh/edit` is documented but not yet deployed (cn returns 4001). The
51
- * step stays hidden until the backend ships; set TRIPO_EXPERIMENTAL_MESH_EDIT=1
52
- * to opt in early. Flip this default once the endpoint is live.
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
  };
@@ -1,11 +1,11 @@
1
1
  /**
2
- * V3 error-code catalog: human translations (zh/en), retryability, and the
2
+ * V3 error-code catalog: human-readable messages, retryability, and the
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). Note the public docs page
7
- * is stale for 1000/1001 — the server actually sends 1002 for auth failures;
8
- * 1000/1001 are 5xx server-side errors.
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,
@@ -20,37 +20,37 @@ export const EXIT = {
20
20
  RATE_LIMIT: 9,
21
21
  };
22
22
  export const ERROR_CATALOG = [
23
- { code: 1000, zh: '服务端未知错误,请携带 request_id 联系支持', en: 'Unknown server-side error; contact support with the request id', retryable: true, exitCode: EXIT.NETWORK },
24
- { code: 1001, zh: '服务端严重错误,请携带 request_id 联系支持', en: 'Fatal server-side error; contact support with the request id', retryable: false, exitCode: EXIT.NETWORK },
25
- { code: 1002, zh: '认证失败:API Key 无效或区域不匹配(国内 Key 打了海外 API?),运行 tripo doctor 自动诊断', en: 'Authentication failed: invalid key or wrong region (cn key against the ov API?) — run "tripo doctor" to auto-diagnose', retryable: false, exitCode: EXIT.AUTH },
26
- { code: 1003, zh: '请求体格式不合法', en: 'The request body is malformed', retryable: false, exitCode: EXIT.USAGE },
27
- { code: 1004, zh: '一个或多个参数不合法,请对照文档检查', en: 'One or more parameters are invalid (check the docs)', retryable: false, exitCode: EXIT.USAGE },
28
- { code: 1005, zh: '无权访问该资源,请检查账号权限', en: 'Not allowed to access this resource (check permissions)', retryable: false, exitCode: EXIT.AUTH },
29
- { code: 1007, zh: '请求过于频繁,触发全局速率限制', en: 'Rate limit exceeded — too many requests in a short time', retryable: true, exitCode: EXIT.RATE_LIMIT },
30
- { code: 2000, zh: '触发生成并发上限,同系列任务共享并发池', en: 'Generation concurrency limit hit (task families share a pool)', retryable: true, exitCode: EXIT.RATE_LIMIT },
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: '该任务类型不受支持', en: 'This task type is not supported', retryable: false, exitCode: EXIT.USAGE },
33
- { code: 2003, zh: '输入图片文件为空,请检查文件是否损坏', en: 'The image file is empty (corrupted upload?)', retryable: false, exitCode: EXIT.USAGE },
34
- { code: 2004, zh: '不支持的图片文件类型', en: 'Unsupported image file type', retryable: false, exitCode: EXIT.USAGE },
35
- { code: 2005, zh: 'draft 任务不是成功状态,请用成功的任务重试', en: 'The draft task is not a success; retry with a successful one', retryable: false, exitCode: EXIT.USAGE },
36
- { code: 2006, zh: '上游任务类型不受支持,请检查任务链', en: 'The original task type is not supported for this operation', retryable: false, exitCode: EXIT.USAGE },
37
- { code: 2007, zh: '上游任务不是成功状态', en: 'The original task has not succeeded', retryable: false, exitCode: EXIT.USAGE },
38
- { code: 2008, zh: '输入违反内容政策,请更换输入', en: 'Input violates the content policy; change the input', retryable: false, exitCode: EXIT.CONTENT_POLICY },
39
- { code: 2009, zh: 'prompt 含有非法字符,请修改后重试', en: 'The prompt contains invalid characters', retryable: false, exitCode: EXIT.USAGE },
40
- { code: 2010, zh: '积分不足', en: 'Insufficient credits', retryable: false, exitCode: EXIT.BALANCE },
41
- { code: 2011, zh: 'rig-check 的输入任务没有模型产物', en: 'The rig-check input task has no model output', retryable: false, exitCode: EXIT.USAGE },
42
- { code: 2012, zh: 'retarget 只接受 rig 任务作为输入', en: 'retarget only accepts a rig task as input', retryable: false, exitCode: EXIT.USAGE },
43
- { code: 2013, zh: 'priority 取值无效(更高优先级需联系支持)', en: 'Invalid priority (contact support for higher priority)', retryable: false, exitCode: EXIT.USAGE },
44
- { code: 2014, zh: '内容审核过程出错,请联系支持', en: 'Error occurred during content auditing; contact support', retryable: false, exitCode: EXIT.FATAL },
45
- { code: 2015, zh: '模型版本已弃用,请升级 model 参数', en: 'Model version deprecated; upgrade the model parameter', retryable: false, exitCode: EXIT.USAGE },
46
- { code: 2016, zh: '该请求类型已弃用,请改用其他类型', en: 'This request type is deprecated; use an alternative', retryable: false, exitCode: EXIT.USAGE },
47
- { code: 2017, zh: 'model 版本取值无效', en: 'The model version value is invalid', retryable: false, exitCode: EXIT.USAGE },
48
- { code: 2018, zh: '模型太复杂无法重网格化,降低 face_limit 或简化输入', en: 'Model too complex to remesh; lower face_limit or simplify input', retryable: false, exitCode: EXIT.USAGE },
49
- { code: 2019, zh: '文件不存在,请确认上传是否成功、file_token 是否过期', en: 'File not found (upload succeeded? token expired?)', retryable: false, exitCode: EXIT.NOT_FOUND },
50
- { code: 2020, zh: '图片 URL 无效,请确认可公开访问', en: 'The image URL is invalid (must be publicly reachable)', retryable: false, exitCode: EXIT.USAGE },
51
- { code: 2021, zh: '文件大小超过上限,请压缩后重试', en: 'File size exceeds the limit; reduce and retry', retryable: false, exitCode: EXIT.USAGE },
52
- { code: 2022, zh: '图片尺寸超过上限,请缩小后重试', en: 'Image size exceeds the limit; reduce and retry', retryable: false, exitCode: EXIT.USAGE },
53
- { code: 4001, zh: '请求的接口不存在,请检查 API 路径', en: 'The requested endpoint does not exist (check the API path)', retryable: false, exitCode: EXIT.NOT_FOUND },
23
+ { code: 1000, message: 'Unknown server-side error; contact support with the request id', retryable: true, exitCode: EXIT.NETWORK },
24
+ { code: 1001, message: 'Fatal server-side error; contact support with the request id', retryable: false, exitCode: EXIT.NETWORK },
25
+ { code: 1002, message: 'Authentication failed: invalid key or wrong region (cn key against the ov API?) — run "tripo doctor" to auto-diagnose', retryable: false, exitCode: EXIT.AUTH },
26
+ { code: 1003, message: 'The request body is malformed', retryable: false, exitCode: EXIT.USAGE },
27
+ { code: 1004, message: 'One or more parameters are invalid (check the docs)', retryable: false, exitCode: EXIT.USAGE },
28
+ { code: 1005, message: 'Not allowed to access this resource (check permissions)', retryable: false, exitCode: EXIT.AUTH },
29
+ { code: 1007, message: 'Rate limit exceeded — too many requests in a short time', retryable: true, exitCode: EXIT.RATE_LIMIT },
30
+ { code: 2000, message: 'Generation concurrency limit hit (task families share a pool)', retryable: true, exitCode: EXIT.RATE_LIMIT },
31
+ { code: 2001, message: 'Task not found (same key that created it? right region?)', retryable: false, exitCode: EXIT.NOT_FOUND },
32
+ { code: 2002, message: 'Unsupported request parameter (check field names and values)', retryable: false, exitCode: EXIT.USAGE },
33
+ { code: 2003, message: 'The input file is empty (corrupted upload?)', retryable: false, exitCode: EXIT.USAGE },
34
+ { code: 2004, message: 'Unsupported file type', retryable: false, exitCode: EXIT.USAGE },
35
+ { code: 2005, message: 'The draft task is not a success; retry with a successful one', retryable: false, exitCode: EXIT.USAGE },
36
+ { code: 2006, message: 'The original task type is not supported for this operation', retryable: false, exitCode: EXIT.USAGE },
37
+ { code: 2007, message: 'The original task has not succeeded', retryable: false, exitCode: EXIT.USAGE },
38
+ { code: 2008, message: 'Input violates the content policy; change the input', retryable: false, exitCode: EXIT.CONTENT_POLICY },
39
+ { code: 2009, message: 'The prompt contains invalid characters', retryable: false, exitCode: EXIT.USAGE },
40
+ { code: 2010, message: 'Insufficient credits', retryable: false, exitCode: EXIT.BALANCE },
41
+ { code: 2011, message: 'The rig-check input task has no model output', retryable: false, exitCode: EXIT.USAGE },
42
+ { code: 2012, message: 'retarget only accepts a rig task as input', retryable: false, exitCode: EXIT.USAGE },
43
+ { code: 2013, message: 'Invalid priority (contact support for higher priority)', retryable: false, exitCode: EXIT.USAGE },
44
+ { code: 2014, message: 'Error occurred during content auditing; contact support', retryable: false, exitCode: EXIT.FATAL },
45
+ { code: 2015, message: 'Model version deprecated; upgrade the model parameter', retryable: false, exitCode: EXIT.USAGE },
46
+ { code: 2016, message: 'This request type is deprecated; use an alternative', retryable: false, exitCode: EXIT.USAGE },
47
+ { code: 2017, message: 'The model version value is invalid', retryable: false, exitCode: EXIT.USAGE },
48
+ { code: 2018, message: 'Model too complex to remesh; lower face_limit or simplify input', retryable: false, exitCode: EXIT.USAGE },
49
+ { code: 2019, message: 'File not found (upload succeeded? token expired?)', retryable: false, exitCode: EXIT.NOT_FOUND },
50
+ { code: 2020, message: 'The image URL is invalid (must be publicly reachable)', retryable: false, exitCode: EXIT.USAGE },
51
+ { code: 2021, message: 'File size exceeds the limit; reduce and retry', retryable: false, exitCode: EXIT.USAGE },
52
+ { code: 2022, message: 'Image size exceeds the limit; reduce and retry', retryable: false, exitCode: EXIT.USAGE },
53
+ { code: 4001, message: 'The requested endpoint does not exist (check the API path)', retryable: false, exitCode: EXIT.NOT_FOUND },
54
54
  ];
55
55
  const byCode = new Map(ERROR_CATALOG.map((e) => [e.code, e]));
56
56
  export function lookupError(code) {
@@ -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)