tripo-cli 0.5.1 → 0.5.2

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 CHANGED
@@ -91,7 +91,7 @@ tripo batch run assets.yaml --concurrency 2
91
91
 
92
92
  - `make`/`watch` are blocking: run them, wait for exit, read the single JSON line on stdout. Do not poll yourself.
93
93
  - The plan card on stderr lists every call before it runs; the final JSON carries `credits_consumed` (total for the run) plus `credits_breakdown` (per task) when a chain ran.
94
- - Exit codes: `0` ok · `2` usage · `3` auth · `4` insufficient credits · `5` content policy · `6` task failed (credits auto-refunded) · `7` network · `8` not found · `9` rate limit.
94
+ - Exit codes: `0` ok · `2` usage · `3` auth · `4` insufficient credits · `5` content policy · `6` task failed (credits auto-refunded; a `timeout` means it is still running) · `7` network (a failed submit is never resent — check `tripo balance` before re-running) · `8` not found · `9` rate limit.
95
95
  - `preview.png` in every output directory is your eyes: look at it, then decide to `tripo redo` or continue the chain.
96
96
  - Full behavior rules: `tripo docs --llm`, per-command docs via `tripo docs --topic commands/make`.
97
97
 
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Lightweight V3 API client on top of global fetch (Node 20+).
3
3
  * Handles the response envelope, error translation, timeouts, and
4
- * exponential-backoff retries for transient failures (429/5xx/network).
4
+ * exponential-backoff retries for transient failures (429/5xx/network;
5
+ * task creation is only resent on 429).
5
6
  */
6
7
  import { activeProfileName, DEFAULT_PROFILE, listProfiles, loadConfig, resolveApiBaseUrl, resolveApiKey } from '../config/config.js';
7
8
  import { t } from '../i18n.js';
9
+ import { EXIT } from '../knowledge/error-catalog.js';
8
10
  import { ApiError, AuthError, NetworkError } from './errors.js';
9
11
  export class TripoClient {
10
12
  baseUrl;
@@ -37,10 +39,11 @@ export class TripoClient {
37
39
  this.maxRetries = options.maxRetries ?? 3;
38
40
  this.onRetry = options.onRetry;
39
41
  }
40
- async request(method, path, body) {
42
+ async request(method, path, body, options = {}) {
41
43
  return this.send(method, path, {
42
44
  headers: { 'Content-Type': 'application/json' },
43
45
  body: body === undefined ? undefined : JSON.stringify(body),
46
+ idempotent: options.idempotent,
44
47
  });
45
48
  }
46
49
  async uploadFile(fileName, data) {
@@ -61,7 +64,11 @@ export class TripoClient {
61
64
  }
62
65
  catch (err) {
63
66
  lastError = err;
64
- const retryable = err instanceof NetworkError || (err instanceof ApiError && err.retryable);
67
+ // A create that timed out or hit a 5xx may already be accepted (and
68
+ // billed); only a rate-limit rejection proves the server did nothing.
69
+ const retryable = init.idempotent === false
70
+ ? err instanceof ApiError && err.exitCode === EXIT.RATE_LIMIT
71
+ : err instanceof NetworkError || (err instanceof ApiError && err.retryable);
65
72
  if (!retryable || attempt === this.maxRetries)
66
73
  throw err;
67
74
  }
@@ -134,8 +141,19 @@ export class TripoClient {
134
141
  }
135
142
  /** Create a task; returns its id. Response may be {task_id} or a full detail. */
136
143
  async createTask(endpoint, payload) {
137
- const data = await this.request('POST', endpoint, payload);
138
- return data.task_id;
144
+ try {
145
+ const data = await this.request('POST', endpoint, payload, { idempotent: false });
146
+ return data.task_id;
147
+ }
148
+ catch (err) {
149
+ if (err instanceof NetworkError || (err instanceof ApiError && err.exitCode === EXIT.NETWORK)) {
150
+ err.suggestion = t({
151
+ zh: '任务可能已在服务端创建并冻结积分;重试前先用 tripo balance 查看冻结额或到控制台确认,避免重复扣费',
152
+ en: 'the task may already exist server-side (credits frozen); check "tripo balance" or the console before retrying, to avoid a duplicate charge',
153
+ });
154
+ }
155
+ throw err;
156
+ }
139
157
  }
140
158
  }
141
159
  export function sleep(ms) {
@@ -45,13 +45,17 @@ export class NetworkError extends CliError {
45
45
  }
46
46
  export class TaskFailedError extends CliError {
47
47
  taskId;
48
+ status;
48
49
  constructor(taskId, status, detail) {
49
50
  const refundNote = t({
50
51
  zh: '预扣积分已自动退还',
51
52
  en: 'frozen credits have been refunded automatically',
52
53
  });
53
- super(`task ${taskId} ${status}${detail ? `: ${detail}` : ''} (${refundNote})`, EXIT.TASK_FAILED);
54
+ super(
55
+ // A timed-out task is still running server-side: nothing has been refunded.
56
+ `task ${taskId} ${status}${detail ? `: ${detail}` : ''}${status === 'timeout' ? '' : ` (${refundNote})`}`, EXIT.TASK_FAILED);
54
57
  this.taskId = taskId;
58
+ this.status = status;
55
59
  }
56
60
  }
57
61
  /** Decorate insufficient-balance errors with the topup path and the gap. */
@@ -13,7 +13,7 @@ import { detectInput } from '../util/input-detect.js';
13
13
  import { notifyDone } from '../util/notify.js';
14
14
  import { createSpinner } from '../util/spinner.js';
15
15
  import { defaultOutputDir, downloadArtifacts } from './download.js';
16
- import { ApiError, UsageError, balanceSuggestion } from './errors.js';
16
+ import { ApiError, CliError, TaskFailedError, UsageError, balanceSuggestion } from './errors.js';
17
17
  import { isQuiet, log, warn } from './output.js';
18
18
  import { uploadLocalFile } from './upload.js';
19
19
  /** Placeholder `input` value for a local file that a dry run would upload. */
@@ -115,7 +115,15 @@ export async function runTask(options) {
115
115
  }
116
116
  catch (err) {
117
117
  spinner.fail(`${taskType} · ${taskId}`);
118
- updateHistoryStatus(taskId, 'failed');
118
+ if (err instanceof TaskFailedError && err.status !== 'timeout') {
119
+ updateHistoryStatus(taskId, 'failed');
120
+ }
121
+ else if (err instanceof CliError) {
122
+ err.suggestion = t({
123
+ zh: `任务仍在服务端运行,成功后照常扣费;用 tripo task watch ${taskId} --download 继续等待,不要重新提交`,
124
+ en: `the task keeps running server-side and is billed if it succeeds; resume with "tripo task watch ${taskId} --download" instead of resubmitting`,
125
+ });
126
+ }
119
127
  throw err;
120
128
  }
121
129
  updateHistoryStatus(taskId, 'success', { credits_consumed: task.credits_consumed });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tripo-cli",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Tripo 3D intelligent CLI — turn a prompt or an image into engine-ready 3D assets from your terminal (Tripo V3 API)",
5
5
  "keywords": [
6
6
  "tripo",
package/skill/SKILL.md CHANGED
@@ -34,7 +34,7 @@ Regions (ov = international, cn = China mainland) share one CLI: `tripo login --
34
34
  2. **stdout is the contract; stderr is commentary.** With `--json` (auto-enabled when piped), stdout carries exactly one final JSON line. Progress/logs go to stderr.
35
35
  3. **Artifacts are local files.** The result JSON has `output_dir`, `model_file`, `preview`. Look at `preview.png` to judge quality; re-roll with `tripo redo` if needed.
36
36
  4. **Exit codes are stable** — branch on them:
37
- - 0 success · 2 usage/params · 3 auth · 4 insufficient credits (tell the human to run `tripo topup`) · 5 content policy · 6 task failed (credits auto-refunded) · 7 network · 8 not found · 9 rate limit (retry with backoff)
37
+ - 0 success · 2 usage/params · 3 auth · 4 insufficient credits (tell the human to run `tripo topup`) · 5 content policy · 6 task failed (credits auto-refunded; on `timeout` it is still running — `tripo task watch <id>`, never redo) · 7 network (a failed submit is never resent and may already exist — check `tripo balance` before re-running) · 8 not found · 9 rate limit (retry with backoff)
38
38
  5. **Never invent parameters.** Unknown `--param key=value` pairs pass through to the API; stick to documented ones. The full parameter/model/preset reference from the official API docs is bundled: `tripo docs --topic commands/generate` (3D + image generation) and `tripo docs --topic commands/process` (texture/convert/rig/retarget/mesh) — read those instead of browsing the website.
39
39
  6. **Validate before spending: `--dry-run --json`.** Same command plus `--dry-run` prints the plan as JSON with zero network calls and no credits: the chosen `model`, the completed `steps[].payload` for every API call, `warnings` (auto-adjustments) and `errors` (blocking; exit 2). When `valid` is true, re-run without `--dry-run`. Details: `tripo docs --topic commands/make` § "Dry run".
40
40
  7. **Don't pick legacy model versions.** The CLI auto-selects `tripo-v3.1` (high fidelity) or `tripo-p1` (low-poly, face budget ≤ 20000). Only override `--model` when the human asks (e.g. `--model tripo-p2` for the P-series **Preview** with quad support, or `tripo-v3.0` / `tripo-v2.5` for reproducing old projects). `tripo-*` names are CLI aliases — the CLI sends the dated wire value (`tripo-p2` → `P2-20260801`); the API rejects the alias itself with 1004. Alias ↔ wire ↔ status table: `tripo docs --topic commands/generate` ("Models & Versions").
@@ -12,7 +12,7 @@ tripo task get @last --download --json
12
12
 
13
13
  Block until the task finishes. In `--json` mode, progress streams as NDJSON events on stdout (`{"event":"progress",...}` lines) and the final task detail is the last line. `--download` grabs artifacts on success. `--timeout <seconds>` (default 1800).
14
14
 
15
- Rules for agents: treat `watch` as synchronous; read lines until process exit; exit code 6 means the task failed (credits refunded).
15
+ Rules for agents: treat `watch` as synchronous; read lines until process exit; exit code 6 means the task failed (credits refunded) — or, if the error says `timeout`, that it is still running (watch it again; do not resubmit).
16
16
 
17
17
  ## tripo task list [ids...]
18
18
 
@@ -19,9 +19,9 @@
19
19
  | 2 | 2018 | too complex to remesh | lower `face_limit` or simplify the input model |
20
20
  | 8 | 2019 | file not found (token expired?) | re-upload; file tokens are short-lived |
21
21
  | 2 | 2020/2021/2022 | bad image URL / file too large / image too large | fix the URL or shrink the file |
22
- | 7 | 1000/1001 | server-side error | 1000 auto-retries; if persistent, contact support with the `request_id` |
23
- | 6 | — | task failed server-side | frozen credits auto-refund; `tripo redo` often succeeds with a new seed |
24
- | 7 | — | network/5xx | auto-retried 3×; check `tripo doctor`; region mismatch (ov/cn) is a common cause |
22
+ | 7 | 1000/1001 | server-side error | queries auto-retry on 1000, task creation does not (the task may already exist and be billed — check `tripo balance` frozen credits before re-running); if persistent, contact support with the `request_id` |
23
+ | 6 | — | task failed server-side, or the watch timed out | failed: frozen credits auto-refund; `tripo redo` often succeeds with a new seed. `timeout`: the task is still running and is billed if it succeeds — `tripo task watch <id> --download`, never redo |
24
+ | 7 | — | network/5xx | queries auto-retried 3×, task creation is not (follow the `suggestion`: check `tripo balance` before re-running); check `tripo doctor`; region mismatch (ov/cn) is a common cause |
25
25
 
26
26
  ## Concurrency pools (default per account; error 2000 when full)
27
27