tripo-cli 0.7.3 → 0.8.1

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 (54) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +10 -7
  3. package/dist/ai/experts.js +25 -19
  4. package/dist/ai/plan-card.js +71 -33
  5. package/dist/ai/wizard.js +13 -17
  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 +35 -4
  13. package/dist/commands/home.js +12 -13
  14. package/dist/commands/login.js +72 -187
  15. package/dist/commands/make.js +47 -25
  16. package/dist/commands/mcp.js +2 -1
  17. package/dist/commands/models.js +47 -0
  18. package/dist/commands/process-groups.js +3 -4
  19. package/dist/commands/shared.js +9 -14
  20. package/dist/commands/task.js +10 -11
  21. package/dist/commands/topup.js +6 -10
  22. package/dist/commands/update.js +6 -10
  23. package/dist/commands/view.js +3 -4
  24. package/dist/config/config.js +1 -1
  25. package/dist/core/client.js +4 -13
  26. package/dist/core/errors.js +16 -11
  27. package/dist/core/output.js +0 -4
  28. package/dist/core/requests.js +61 -17
  29. package/dist/core/task-service.js +19 -20
  30. package/dist/knowledge/chains.js +12 -12
  31. package/dist/knowledge/error-catalog.js +32 -32
  32. package/dist/knowledge/models.js +119 -20
  33. package/dist/knowledge/params.js +96 -8
  34. package/dist/knowledge/scenarios.js +32 -55
  35. package/dist/pipeline/make.js +36 -28
  36. package/dist/pipeline/runner.js +7 -12
  37. package/dist/pipeline/stdin.js +40 -3
  38. package/dist/program.js +6 -11
  39. package/package.json +3 -2
  40. package/skill/SKILL.md +35 -8
  41. package/skill/commands/account.md +1 -1
  42. package/skill/commands/batch.md +13 -3
  43. package/skill/commands/generate.md +35 -10
  44. package/skill/commands/make.md +39 -13
  45. package/skill/commands/process.md +2 -1
  46. package/skill/commands/task.md +2 -2
  47. package/skill/commands/view.md +1 -1
  48. package/skill/common-errors.md +6 -3
  49. package/skill/examples/animation.md +3 -1
  50. package/skill/examples/ar-web.md +6 -4
  51. package/skill/examples/game-asset.md +17 -6
  52. package/skill/examples/pipes.md +2 -2
  53. package/skill/examples/print.md +7 -3
  54. package/dist/i18n.js +0 -19
@@ -1,9 +1,10 @@
1
1
  /** CLI error types carrying stable exit codes and human translations. */
2
- import { t } from '../i18n.js';
3
2
  import { EXIT, exitCodeFor, isRetryable, lookupError } from '../knowledge/error-catalog.js';
4
3
  export class CliError extends Error {
5
4
  exitCode;
6
5
  suggestion;
6
+ /** A create request failed without a response: the paid task may exist server-side. */
7
+ submissionUncertain;
7
8
  constructor(message, exitCode = EXIT.FATAL, suggestion) {
8
9
  super(message);
9
10
  this.exitCode = exitCode;
@@ -17,7 +18,7 @@ export class UsageError extends CliError {
17
18
  }
18
19
  export class AuthError extends CliError {
19
20
  constructor(message, suggestion) {
20
- super(message, EXIT.AUTH, suggestion ?? t({ zh: '运行 tripo login 配置 API Key', en: 'run "tripo login" to configure an API key' }));
21
+ super(message, EXIT.AUTH, suggestion ?? 'run "tripo login" to configure an API key');
21
22
  }
22
23
  }
23
24
  export class ApiError extends CliError {
@@ -31,7 +32,7 @@ export class ApiError extends CliError {
31
32
  // HTTP 401 reads as an auth failure whatever code the body carries (see error-catalog).
32
33
  const code = opts.httpStatus === 401 ? 1002 : opts.apiCode;
33
34
  const info = code !== undefined ? lookupError(code) : undefined;
34
- const translated = info ? t(info) : undefined;
35
+ const translated = info?.message;
35
36
  const parts = [translated ?? opts.message ?? `API error (HTTP ${opts.httpStatus})`];
36
37
  if (translated && opts.message && opts.message !== translated)
37
38
  parts.push(`(${opts.message})`);
@@ -48,14 +49,21 @@ export class NetworkError extends CliError {
48
49
  super(message, EXIT.NETWORK, suggestion);
49
50
  }
50
51
  }
52
+ /** The task succeeded (and was billed); only fetching its artifacts failed. */
53
+ export class ArtifactDownloadError extends NetworkError {
54
+ taskId;
55
+ result;
56
+ constructor(taskId, result, reason) {
57
+ super(`task ${taskId} succeeded but downloading its artifacts failed: ${reason}`, `the task is finished and billed; do not regenerate. Download it with "tripo task get ${taskId} --download"`);
58
+ this.taskId = taskId;
59
+ this.result = result;
60
+ }
61
+ }
51
62
  export class TaskFailedError extends CliError {
52
63
  taskId;
53
64
  status;
54
65
  constructor(taskId, status, detail) {
55
- const refundNote = t({
56
- zh: '预扣积分已自动退还',
57
- en: 'frozen credits have been refunded automatically',
58
- });
66
+ const refundNote = 'frozen credits have been refunded automatically';
59
67
  super(
60
68
  // Only failed/cancelled tasks are refunded; a timed-out task is still running server-side.
61
69
  `task ${taskId} ${status}${detail ? `: ${detail}` : ''}${status === 'failed' || status === 'cancelled' ? ` (${refundNote})` : ''}`, EXIT.TASK_FAILED);
@@ -66,8 +74,5 @@ export class TaskFailedError extends CliError {
66
74
  /** Decorate insufficient-balance errors with the topup path and the gap. */
67
75
  export function balanceSuggestion(needed, balance) {
68
76
  const gap = needed !== undefined && balance !== undefined ? Math.max(0, needed - balance) : undefined;
69
- return t({
70
- zh: `运行 tripo topup 充值${gap !== undefined ? `(还差约 ${gap} 积分)` : ''}`,
71
- en: `run "tripo topup" to add credits${gap !== undefined ? ` (about ${gap} more needed)` : ''}`,
72
- });
77
+ return `run "tripo topup" to add credits${gap !== undefined ? ` (about ${gap} more needed)` : ''}`;
73
78
  }
@@ -66,7 +66,3 @@ export function emitResult(data, human) {
66
66
  process.stdout.write((human ?? JSON.stringify(data, null, 2)) + '\n');
67
67
  }
68
68
  }
69
- /** Stream one NDJSON event to stdout (used by watch-style streaming output). */
70
- export function emitNdjson(event) {
71
- process.stdout.write(JSON.stringify(event) + '\n');
72
- }
@@ -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 { applyTextureV35, 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,7 +33,7 @@ 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,
@@ -62,7 +62,7 @@ export function buildMultiviewToModel(views, model, params) {
62
62
  return {
63
63
  request: {
64
64
  endpoint: '/v3/generation/multiview-to-model',
65
- payload: { inputs, model: normalizeModel(model), ...validated.params },
65
+ payload: { inputs, ...validated.params, model: normalizeModel(model) },
66
66
  },
67
67
  taskType: 'multiview_to_model',
68
68
  warnings: validated.warnings,
@@ -72,28 +72,38 @@ export function buildMultiviewToModel(views, model, params) {
72
72
  * Server task types (TaskType enum): text-to-image → `text_to_image`,
73
73
  * image-to-image → `generate_image`. */
74
74
  export function buildTextToImage(prompt, params) {
75
+ const validated = validateImageParams(params, false);
76
+ ensure(validated);
75
77
  return {
76
78
  request: {
77
79
  endpoint: '/v3/generation/text-to-image',
78
- payload: { prompt, model: params.model ?? CAPABILITY_DEFAULTS.textToImage, ...omit(params, ['model']) },
80
+ payload: { prompt, ...validated.params },
79
81
  },
80
82
  taskType: 'text_to_image',
81
- warnings: [],
83
+ warnings: validated.warnings,
82
84
  };
83
85
  }
84
86
  /** One reference image goes as `input`, several as `inputs` (prompt cites them as image[1], image[2], ...). */
85
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
+ }
86
97
  return {
87
98
  request: {
88
99
  endpoint: '/v3/generation/image-to-image',
89
100
  payload: {
90
101
  ...(inputs.length > 1 ? { inputs } : { input: inputs[0] }),
91
- model: params.model ?? CAPABILITY_DEFAULTS.imageToImage,
92
- ...omit(params, ['model']),
102
+ ...validated.params,
93
103
  },
94
104
  },
95
105
  taskType: 'generate_image',
96
- warnings: [],
106
+ warnings: validated.warnings,
97
107
  };
98
108
  }
99
109
  export function buildImageToMultiview(input, params) {
@@ -117,6 +127,27 @@ export function buildImageToSplat(input, params) {
117
127
  warnings: [],
118
128
  };
119
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
+ }
120
151
  /**
121
152
  * Processing step (texture/convert/rig/...): validate per step, then build.
122
153
  * `input` must already be a task_id / file_token / URL.
@@ -161,6 +192,25 @@ export function buildStepRequest(stepName, input, args) {
161
192
  params.model = params.model === undefined
162
193
  ? CAPABILITY_DEFAULTS.rig
163
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
+ }
164
214
  break;
165
215
  }
166
216
  case 'texture': {
@@ -209,9 +259,3 @@ export function buildStepRequest(stepName, input, args) {
209
259
  warnings,
210
260
  };
211
261
  }
212
- function omit(obj, keys) {
213
- const out = { ...obj };
214
- for (const k of keys)
215
- delete out[k];
216
- return out;
217
- }
@@ -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,
@@ -144,12 +144,12 @@ export async function runTask(options) {
144
144
  if (!options.noDownload) {
145
145
  const baseDir = options.outDir ?? loadContext().output_dir ?? process.cwd();
146
146
  const dir = defaultOutputDir(baseDir, name, taskId);
147
- const dlSpinner = createSpinner(t({ zh: '下载产物…', en: 'downloading artifacts…' }), isQuiet());
147
+ const dlSpinner = createSpinner('downloading artifacts…', isQuiet());
148
148
  try {
149
149
  const artifacts = await downloadArtifacts(task, dir, entry, {
150
- onFile: (f) => dlSpinner.update(t({ zh: `下载 ${f}`, en: `downloading ${f}` })),
150
+ onFile: (f) => dlSpinner.update(`downloading ${f}`),
151
151
  });
152
- dlSpinner.succeed(t({ zh: `产物已保存 ${artifacts.outputDir}`, en: `artifacts saved to ${artifacts.outputDir}` }));
152
+ dlSpinner.succeed(`artifacts saved to ${artifacts.outputDir}`);
153
153
  result.output_dir = artifacts.outputDir;
154
154
  result.files = artifacts.files.map((f) => basename(f));
155
155
  result.model_file = artifacts.modelFile;
@@ -157,13 +157,12 @@ export async function runTask(options) {
157
157
  updateHistoryStatus(taskId, 'success', { output_dir: artifacts.outputDir });
158
158
  }
159
159
  catch (err) {
160
- dlSpinner.fail(t({ zh: '产物下载失败(任务本身已成功)', en: 'artifact download failed (the task itself succeeded)' }));
161
- warn(String(err instanceof Error ? err.message : err));
162
- 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));
163
162
  }
164
163
  }
165
164
  if (options.notify) {
166
- notifyDone('Tripo', t({ zh: `${taskType} 完成`, en: `${taskType} finished` }));
165
+ notifyDone('Tripo', `${taskType} finished`);
167
166
  }
168
167
  return result;
169
168
  }
@@ -22,22 +22,22 @@ export const MODEL_PRODUCING_TYPES = new Set([
22
22
  'animate_retarget',
23
23
  ]);
24
24
  export const STEPS = [
25
- { name: 'texture', endpoint: '/v3/models/texture', produces: 'texture_model', accepts: 'any-model', acceptsFile: true, describe: { zh: '重新生成贴图 / PBR 材质', en: 'regenerate textures / PBR materials' } },
26
- { name: 'convert', endpoint: '/v3/models/convert', produces: 'convert_model', accepts: 'any-model', acceptsFile: true, describe: { zh: '转换输出格式 / 贴图尺寸 / 朝向', en: 'convert format / texture size / orientation' } },
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' } },
28
- { 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)' } },
29
- { name: 'rig', endpoint: '/v3/animations/rig', produces: 'animate_rig', accepts: 'any-model', acceptsFile: true, describe: { zh: '自动绑定骨骼', en: 'auto-rig a skeleton' } },
30
- { 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' } },
31
- { name: 'segment', endpoint: '/v3/mesh/segment', produces: 'mesh_segmentation', accepts: 'any-model', acceptsFile: true, describe: { zh: '把网格分割成部件', en: 'segment the mesh into parts' } },
32
- { name: 'complete', endpoint: '/v3/mesh/complete', produces: 'mesh_completion', accepts: ['mesh_segmentation'], acceptsFile: false, describe: { zh: '补全分割后的部件', en: 'complete the segmented parts' } },
33
- { 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' } },
34
- { 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' },
35
35
  // mesh/edit: local retry/refine inside a bbox, P2 pipeline (model fixed to P2-20260801).
36
- { 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)' },
37
37
  // smartsegment's `input` is file_token/URL only (the server does not resolve
38
38
  // task ids here), so it cannot chain after a task. Type is refined to
39
39
  // smartsegment_image|smartsegment_model at request-build time per seg_type.
40
- { 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' },
41
41
  ];
42
42
  const stepIndex = new Map();
43
43
  for (const s of STEPS) {
@@ -1,5 +1,5 @@
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,
@@ -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: '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
- { 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) {