tripo-cli 0.4.0 → 0.5.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.
- package/README.md +7 -2
- package/dist/ai/plan-card.js +84 -8
- package/dist/commands/completion.js +1 -1
- package/dist/commands/make.js +209 -9
- package/dist/commands/shared.js +23 -1
- package/dist/commands/update.js +138 -0
- package/dist/core/task-service.js +12 -2
- package/dist/knowledge/chains.js +13 -13
- package/dist/pipeline/make.js +25 -18
- package/dist/program.js +6 -0
- package/package.json +1 -1
- package/skill/SKILL.md +5 -3
- package/skill/commands/generate.md +43 -15
- package/skill/commands/make.md +69 -4
- package/skill/common-errors.md +3 -1
package/README.md
CHANGED
|
@@ -20,7 +20,8 @@ Works with both Tripo regions from the same install — international (`openapi.
|
|
|
20
20
|
## Why this CLI
|
|
21
21
|
|
|
22
22
|
- **One magic command.** `tripo make <anything>` detects what you gave it — prompt text, an image, 2–4 view images, a model file, a URL, a task id, or `@last` — picks the right endpoint, model, and parameters, then waits and downloads the artifacts. No file_token, no polling loops.
|
|
23
|
-
- **A 3D domain brain, not a thin API wrapper.** Model choice is a fixed rule (low-poly intent or face budget ≤ 20000 → `tripo-p1`, everything else → `tripo-v3.1`; opt into the P-series
|
|
23
|
+
- **A 3D domain brain, not a thin API wrapper.** Model choice is a fixed rule (low-poly intent or face budget ≤ 20000 → `tripo-p1`, everything else → `tripo-v3.1`; opt into the P-series **Preview** with `--model tripo-p2` for quad low-poly). Those are CLI aliases — the wire value the API receives is `P1-20260311` / `v3.1-20260211` / `P2-20260801` (full alias ↔ wire ↔ status table: `tripo docs --topic commands/generate`; official [Models & Versions](https://developers.tripo3d.ai/en/docs/models-and-versions) and [P Series](https://developers.tripo3d.ai/en/models/p1) pages). P-series illegal parameters are stripped locally, quad + GLB is rejected before it costs you credits, and 7 scenario presets (`--for game-mobile|game-pc|film|print|ar-web|anim|toy`) encode sane parameters + processing chains + delivery formats.
|
|
24
|
+
- **You see the plan before you pay.** `tripo make` prints a plan card — one numbered line per API call (endpoint, what it does, its arguments) — and asks before submitting; afterwards it reports the total credits **and** a per-task breakdown. `--yes` (automatic when headless) skips the question but keeps the card in the log.
|
|
24
25
|
- **Agent-native.** Non-TTY runs are automatically headless (`--json --yes --no-open`), stdout carries exactly one JSON line per command, progress goes to stderr, exit codes are stable, and NDJSON pipes compose: `tripo make cat.png | tripo model texture | tripo anim rig`. An LLM-facing skill package ships inside the npm package (`tripo docs --llm`).
|
|
25
26
|
|
|
26
27
|
## Commands
|
|
@@ -39,7 +40,8 @@ Works with both Tripo regions from the same install — international (`openapi.
|
|
|
39
40
|
| `tripo files upload` | upload a file, print the `file_token` |
|
|
40
41
|
| `tripo batch run <manifest.yaml>` | bulk pipelines with concurrency, retries, and resume |
|
|
41
42
|
| `tripo config / doctor` | settings (global + per-directory `.tripo/context.json`) and self-diagnosis |
|
|
42
|
-
| `tripo
|
|
43
|
+
| `tripo update [--check]` | upgrade to the latest release (`npm install -g tripo-cli@latest`); every command also hints when a newer version exists |
|
|
44
|
+
| `tripo docs [--topic t]` | print the bundled agent/skill docs (`--topic commands/generate` has the model versions / P series table) |
|
|
43
45
|
| `tripo mcp` | run as an MCP server (Cursor / Claude Desktop) |
|
|
44
46
|
| `tripo completion <shell>` | bash/zsh/fish completion |
|
|
45
47
|
|
|
@@ -88,6 +90,7 @@ tripo batch run assets.yaml --concurrency 2
|
|
|
88
90
|
## For AI coding agents
|
|
89
91
|
|
|
90
92
|
- `make`/`watch` are blocking: run them, wait for exit, read the single JSON line on stdout. Do not poll yourself.
|
|
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.
|
|
91
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.
|
|
92
95
|
- `preview.png` in every output directory is your eyes: look at it, then decide to `tripo redo` or continue the chain.
|
|
93
96
|
- Full behavior rules: `tripo docs --llm`, per-command docs via `tripo docs --topic commands/make`.
|
|
@@ -101,6 +104,8 @@ tripo batch run assets.yaml --concurrency 2
|
|
|
101
104
|
| `TRIPO_REGION` | `ov` or `cn` — normally unnecessary: `tripo login` probes both regions with your key and stores the right one automatically |
|
|
102
105
|
| `TRIPO_API_BASE_URL` / `TRIPO_PLATFORM_BASE_URL` | endpoint overrides |
|
|
103
106
|
| `TRIPO_HOME` | config/history directory (default `~/.tripo`) |
|
|
107
|
+
| `TRIPO_NO_UPDATE_CHECK` | disable the startup "new version available" hint (also off when `CI` is set, headless, or `--json`) |
|
|
108
|
+
| `TRIPO_NPM_REGISTRY` | npm registry used by `tripo update` and the version check (default `https://registry.npmjs.org`; e.g. a mirror) |
|
|
104
109
|
| `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` | standard proxy variables, honored for all CLI traffic (plain Node fetch ignores them) |
|
|
105
110
|
| `TRIPO_LLM_BASE_URL` / `TRIPO_LLM_API_KEY` / `TRIPO_LLM_MODEL` | optional OpenAI-compatible LLM for `tripo ai` conversation mode |
|
|
106
111
|
|
package/dist/ai/plan-card.js
CHANGED
|
@@ -5,7 +5,71 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import pc from 'picocolors';
|
|
7
7
|
import { t } from '../i18n.js';
|
|
8
|
+
import { getStep } from '../knowledge/chains.js';
|
|
8
9
|
import { getScenario } from '../knowledge/scenarios.js';
|
|
10
|
+
const GENERATION_DESCRIBE = {
|
|
11
|
+
text_to_model: { zh: '文本生成 3D 模型', en: 'generate a 3D model from text' },
|
|
12
|
+
image_to_model: { zh: '单图生成 3D 模型', en: 'generate a 3D model from one image' },
|
|
13
|
+
multiview_to_model: { zh: '多视图生成 3D 模型', en: 'generate a 3D model from multiple views' },
|
|
14
|
+
};
|
|
15
|
+
/** Keys that describe the input itself (already shown on the "input" line). */
|
|
16
|
+
const INPUT_KEYS = new Set(['prompt', 'input', 'inputs']);
|
|
17
|
+
/** Build the pre-run card for `tripo make` from the plan it is about to execute. */
|
|
18
|
+
export function cardFromMakePlan(plan, opts) {
|
|
19
|
+
const steps = [];
|
|
20
|
+
const genParams = {};
|
|
21
|
+
if (plan.generation) {
|
|
22
|
+
const { taskType, request } = plan.generation;
|
|
23
|
+
const args = {};
|
|
24
|
+
for (const [k, v] of Object.entries(request.payload)) {
|
|
25
|
+
if (INPUT_KEYS.has(k))
|
|
26
|
+
continue;
|
|
27
|
+
args[k] = v;
|
|
28
|
+
if (k !== 'model')
|
|
29
|
+
genParams[k] = v;
|
|
30
|
+
}
|
|
31
|
+
const importDef = taskType === 'import_model' ? getStep('import') : undefined;
|
|
32
|
+
steps.push({
|
|
33
|
+
name: importDef ? importDef.name : taskType.replace(/_/g, '-'),
|
|
34
|
+
endpoint: request.endpoint,
|
|
35
|
+
describe: importDef ? t(importDef.describe) : t(GENERATION_DESCRIBE[taskType] ?? { zh: '生成', en: 'generate' }),
|
|
36
|
+
args,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
for (const spec of plan.chain) {
|
|
40
|
+
const def = getStep(spec.step);
|
|
41
|
+
steps.push({
|
|
42
|
+
name: spec.step,
|
|
43
|
+
endpoint: def?.endpoint ?? '',
|
|
44
|
+
describe: def ? t(def.describe) : '',
|
|
45
|
+
args: spec.args,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const scenario = opts.scenario ? getScenario(opts.scenario) : undefined;
|
|
49
|
+
const notes = [];
|
|
50
|
+
if (opts.candidates && opts.candidates > 1) {
|
|
51
|
+
notes.push(t({
|
|
52
|
+
zh: `并行生成 ${opts.candidates} 个候选(不同 seed),选中一个后再继续加工链`,
|
|
53
|
+
en: `${opts.candidates} candidates in parallel (distinct seeds); pick one before the chain continues`,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
// Task refs (@last / @name) resolve to an id the human has not seen yet — show it.
|
|
57
|
+
let inputSummary = opts.inputs.join(' ');
|
|
58
|
+
if (plan.sourceTaskId && inputSummary !== plan.sourceTaskId) {
|
|
59
|
+
inputSummary += ` → ${plan.sourceTaskId}${plan.sourceType ? ` (${plan.sourceType})` : ''}`;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
inputSummary,
|
|
63
|
+
scenario: opts.scenario,
|
|
64
|
+
model: plan.model,
|
|
65
|
+
modelReason: plan.modelReason,
|
|
66
|
+
genParams,
|
|
67
|
+
chain: plan.chain,
|
|
68
|
+
deliverables: scenario?.deliverables ?? [],
|
|
69
|
+
notes,
|
|
70
|
+
steps,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
9
73
|
/** Cost-impact annotations derived from the parameter set. */
|
|
10
74
|
export function costNotes(card) {
|
|
11
75
|
const notes = [];
|
|
@@ -48,15 +112,27 @@ export function renderPlanCard(card) {
|
|
|
48
112
|
if (card.model) {
|
|
49
113
|
lines.push(`${t({ zh: '模型', en: 'model' })}: ${card.model}${card.modelReason ? pc.dim(` — ${card.modelReason}`) : ''}`);
|
|
50
114
|
}
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
-
|
|
115
|
+
const fmtArgs = (args) => Object.entries(args).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(' ');
|
|
116
|
+
if (card.steps && card.steps.length > 0) {
|
|
117
|
+
// Numbered step list: what each call does, which endpoint, with which args.
|
|
118
|
+
lines.push(`${t({ zh: '步骤', en: 'steps' })}:`);
|
|
119
|
+
card.steps.forEach((step, i) => {
|
|
120
|
+
lines.push(` ${i + 1}. ${step.name} ${pc.dim(`POST ${step.endpoint}`)}${step.describe ? ` — ${step.describe}` : ''}`);
|
|
121
|
+
if (Object.keys(step.args).length > 0)
|
|
122
|
+
lines.push(pc.dim(` ${fmtArgs(step.args)}`));
|
|
123
|
+
});
|
|
54
124
|
}
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
.
|
|
59
|
-
|
|
125
|
+
else {
|
|
126
|
+
const params = Object.entries(card.genParams);
|
|
127
|
+
if (params.length > 0) {
|
|
128
|
+
lines.push(`${t({ zh: '参数', en: 'params' })}: ${fmtArgs(card.genParams)}`);
|
|
129
|
+
}
|
|
130
|
+
if (card.chain.length > 0) {
|
|
131
|
+
const chainStr = card.chain
|
|
132
|
+
.map((s) => (Object.keys(s.args).length > 0 ? `${s.step}(${Object.entries(s.args).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(',')})` : s.step))
|
|
133
|
+
.join(' → ');
|
|
134
|
+
lines.push(`${t({ zh: '加工链', en: 'chain' })}: ${chainStr}`);
|
|
135
|
+
}
|
|
60
136
|
}
|
|
61
137
|
if (card.deliverables.length > 0) {
|
|
62
138
|
lines.push(`${t({ zh: '交付物', en: 'deliverables' })}: ${card.deliverables.join(', ')}`);
|
|
@@ -5,7 +5,7 @@ const COMMANDS = [
|
|
|
5
5
|
'login', 'logout', 'whoami', 'use', 'balance', 'usage', 'topup',
|
|
6
6
|
'make', 'view', 'redo', 'ai',
|
|
7
7
|
'generate', 'model', 'anim', 'mesh', 'task', 'files', 'batch', 'history',
|
|
8
|
-
'config', 'doctor', 'docs', 'completion', 'mcp',
|
|
8
|
+
'config', 'doctor', 'update', 'docs', 'completion', 'mcp',
|
|
9
9
|
];
|
|
10
10
|
const SUBS = {
|
|
11
11
|
generate: ['text-to-model', 'image-to-model', 'multiview-to-model', 'text-to-image', 'image-to-image', 'image-to-multiview', 'edit-multiview', 'image-to-splat'],
|
package/dist/commands/make.js
CHANGED
|
@@ -4,12 +4,18 @@
|
|
|
4
4
|
* model, the parameters, runs the chain, and downloads the artifacts.
|
|
5
5
|
*/
|
|
6
6
|
import * as p from '@clack/prompts';
|
|
7
|
-
import {
|
|
7
|
+
import { TripoClient } from '../core/client.js';
|
|
8
|
+
import { AuthError, UsageError } from '../core/errors.js';
|
|
8
9
|
import { emitResult, isInteractive, log, warn } from '../core/output.js';
|
|
10
|
+
import { buildStepRequest } from '../core/requests.js';
|
|
9
11
|
import { t } from '../i18n.js';
|
|
12
|
+
import { ChainError, getStep } from '../knowledge/chains.js';
|
|
13
|
+
import { EXIT } from '../knowledge/error-catalog.js';
|
|
10
14
|
import { scenarioIds } from '../knowledge/scenarios.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
15
|
+
import { cardFromMakePlan, costNotes, renderPlanCard } from '../ai/plan-card.js';
|
|
16
|
+
import { checkPlanChain, executeMake, planMake } from '../pipeline/make.js';
|
|
17
|
+
import { detectInput, InputDetectError } from '../util/input-detect.js';
|
|
18
|
+
import { newClient, parseChainFlag, printTaskResult, summarizeCredits, withTaskOptions } from './shared.js';
|
|
13
19
|
import { printInlineImage } from '../util/term-image.js';
|
|
14
20
|
export function registerMakeCommand(program) {
|
|
15
21
|
const cmd = program
|
|
@@ -20,15 +26,38 @@ export function registerMakeCommand(program) {
|
|
|
20
26
|
.option('--then <steps>', 'processing chain, e.g. "texture,rig,convert:fbx"')
|
|
21
27
|
.option('--model <model>', 'force model version (tripo-v3.1 / tripo-p1 / tripo-p2 / ...)')
|
|
22
28
|
.option('-n, --candidates <n>', 'generate N candidates in parallel', (v) => Number(v), 1)
|
|
23
|
-
.option('--seed <n>', 'model_seed for reproducible geometry', (v) => Number(v))
|
|
29
|
+
.option('--seed <n>', 'model_seed for reproducible geometry', (v) => Number(v))
|
|
30
|
+
.option('--dry-run', 'plan + validate only: print model, completed params and chain steps; no upload, no submit, no credits');
|
|
24
31
|
withTaskOptions(cmd);
|
|
25
32
|
cmd.action(async (inputs, flags) => {
|
|
26
33
|
const result = await runMake(inputs, flags);
|
|
34
|
+
if (result.cancelled)
|
|
35
|
+
return;
|
|
27
36
|
emitMakeOutcome(result);
|
|
28
37
|
});
|
|
29
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Show what is about to run (one numbered line per API call) and ask before
|
|
41
|
+
* spending credits. `--yes` / headless callers see the card on stderr but are
|
|
42
|
+
* not prompted. Returns false when the human declines.
|
|
43
|
+
*
|
|
44
|
+
* Gate on isInteractive() (both stdin and stdout TTYs), not just --yes: when
|
|
45
|
+
* stdout is a pipe (`tripo make x | tripo model texture`) the clack prompt
|
|
46
|
+
* would land in the pipe, corrupt the JSON contract and block forever.
|
|
47
|
+
*/
|
|
48
|
+
async function confirmPlan(plan, inputs, flags) {
|
|
49
|
+
const card = cardFromMakePlan(plan, { inputs, scenario: flags.for, candidates: flags.candidates });
|
|
50
|
+
log(renderPlanCard(card));
|
|
51
|
+
if (!isInteractive())
|
|
52
|
+
return true;
|
|
53
|
+
const go = await p.confirm({ message: t({ zh: '按方案执行?', en: 'Run this plan?' }) });
|
|
54
|
+
if (p.isCancel(go) || !go) {
|
|
55
|
+
log(t({ zh: '已取消,未消耗积分', en: 'cancelled; no credits spent' }));
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
30
60
|
export async function runMake(inputs, flags) {
|
|
31
|
-
const client = newClient();
|
|
32
61
|
const params = { ...flags.param };
|
|
33
62
|
if (flags.seed !== undefined)
|
|
34
63
|
params.model_seed = flags.seed;
|
|
@@ -45,9 +74,18 @@ export async function runMake(inputs, flags) {
|
|
|
45
74
|
notify: flags.notify,
|
|
46
75
|
timeoutMs: flags.timeout ? flags.timeout * 1000 : undefined,
|
|
47
76
|
};
|
|
77
|
+
const cancelled = { results: [], chainResults: [], cancelled: true };
|
|
78
|
+
if (flags.dryRun) {
|
|
79
|
+
// The plan itself is the result; nothing is submitted.
|
|
80
|
+
await dryRunMake(inputs, flags, options);
|
|
81
|
+
return cancelled;
|
|
82
|
+
}
|
|
83
|
+
const client = newClient();
|
|
48
84
|
const n = flags.candidates;
|
|
49
85
|
if (n <= 1) {
|
|
50
86
|
const plan = await planMake(client, options);
|
|
87
|
+
if (!(await confirmPlan(plan, inputs, flags)))
|
|
88
|
+
return cancelled;
|
|
51
89
|
const outcome = await executeMake(client, plan, options);
|
|
52
90
|
return { results: [outcome.primary], chainResults: outcome.chainResults, stoppedEarly: outcome.stoppedEarly };
|
|
53
91
|
}
|
|
@@ -63,6 +101,8 @@ export async function runMake(inputs, flags) {
|
|
|
63
101
|
if (chain.length > 0 && !interactive) {
|
|
64
102
|
throw new UsageError('-n with --then needs an interactive terminal to pick the winner; run the chain afterwards with "tripo make <task-id> --then ..."');
|
|
65
103
|
}
|
|
104
|
+
if (!(await confirmPlan(basePlan, inputs, flags)))
|
|
105
|
+
return cancelled;
|
|
66
106
|
log(t({ zh: `并行生成 ${n} 个候选…`, en: `generating ${n} candidates in parallel…` }));
|
|
67
107
|
const seeds = Array.from({ length: n }, () => Math.floor(Math.random() * 2 ** 31));
|
|
68
108
|
const runs = seeds.map((seed, i) => (async () => {
|
|
@@ -118,8 +158,156 @@ export async function runMake(inputs, flags) {
|
|
|
118
158
|
}
|
|
119
159
|
return { results, chainResults, stoppedEarly };
|
|
120
160
|
}
|
|
161
|
+
const UPSTREAM_PLACEHOLDER = '<upstream>';
|
|
162
|
+
/** Dry runs never send a request, so a missing API key must not block them. */
|
|
163
|
+
function dryRunClient() {
|
|
164
|
+
try {
|
|
165
|
+
return newClient();
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
if (err instanceof AuthError)
|
|
169
|
+
return new TripoClient({ apiKey: 'dry-run' });
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* `--dry-run`: build and validate the plan with zero network calls, then emit
|
|
175
|
+
* it as the business result (JSON or the plan card). Blocking problems set
|
|
176
|
+
* exit code 2 so an agent can fix the request before spending credits.
|
|
177
|
+
*/
|
|
178
|
+
async function dryRunMake(inputs, flags, options) {
|
|
179
|
+
const warnings = [];
|
|
180
|
+
const errors = [];
|
|
181
|
+
const pendingUploads = inputs.filter((raw) => {
|
|
182
|
+
try {
|
|
183
|
+
const kind = detectInput(raw).kind;
|
|
184
|
+
return kind === 'image-file' || kind === 'model-file';
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
let plan;
|
|
191
|
+
try {
|
|
192
|
+
plan = await planMake(dryRunClient(), { ...options, dryRun: true });
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
if (!(err instanceof UsageError || err instanceof InputDetectError || err instanceof ChainError))
|
|
196
|
+
throw err;
|
|
197
|
+
errors.push(err.message);
|
|
198
|
+
}
|
|
199
|
+
if (!plan) {
|
|
200
|
+
emitDryRun({
|
|
201
|
+
dry_run: true,
|
|
202
|
+
valid: false,
|
|
203
|
+
input: inputs.join(' '),
|
|
204
|
+
scenario: flags.for,
|
|
205
|
+
steps: [],
|
|
206
|
+
pending_uploads: pendingUploads,
|
|
207
|
+
deliverables: [],
|
|
208
|
+
candidates: flags.candidates,
|
|
209
|
+
warnings,
|
|
210
|
+
errors,
|
|
211
|
+
cost_notes: [],
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
warnings.push(...plan.warnings);
|
|
216
|
+
try {
|
|
217
|
+
checkPlanChain(plan);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
if (!(err instanceof UsageError))
|
|
221
|
+
throw err;
|
|
222
|
+
errors.push(err.message);
|
|
223
|
+
}
|
|
224
|
+
if (plan.chain.length > 0 && !plan.generation && !plan.sourceType) {
|
|
225
|
+
warnings.push(`chain legality unverified: upstream task type unknown (no local history for ${plan.sourceTaskId}); the real run checks it via the API`);
|
|
226
|
+
}
|
|
227
|
+
if (flags.candidates > 4)
|
|
228
|
+
errors.push('at most 4 candidates per run (-n 4)');
|
|
229
|
+
if (flags.candidates > 1 && !plan.generation) {
|
|
230
|
+
errors.push('-n only applies to generation inputs (text/image), not task references');
|
|
231
|
+
}
|
|
232
|
+
// Same card the real run shows; chain-step args are replaced by the
|
|
233
|
+
// normalized payloads so the human sees the completed parameters too.
|
|
234
|
+
const card = cardFromMakePlan(plan, { inputs, scenario: flags.for, candidates: flags.candidates });
|
|
235
|
+
const cardSteps = card.steps ?? [];
|
|
236
|
+
const steps = [];
|
|
237
|
+
if (plan.generation) {
|
|
238
|
+
steps.push({
|
|
239
|
+
name: cardSteps[0]?.name ?? plan.generation.taskType,
|
|
240
|
+
endpoint: plan.generation.request.endpoint,
|
|
241
|
+
task_type: plan.generation.taskType,
|
|
242
|
+
payload: plan.generation.request.payload,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
plan.chain.forEach((spec, i) => {
|
|
246
|
+
const cardStep = cardSteps[(plan.generation ? 1 : 0) + i];
|
|
247
|
+
try {
|
|
248
|
+
const built = buildStepRequest(spec.step, UPSTREAM_PLACEHOLDER, spec.args);
|
|
249
|
+
warnings.push(...built.warnings);
|
|
250
|
+
if (cardStep) {
|
|
251
|
+
const args = { ...built.request.payload };
|
|
252
|
+
delete args.input;
|
|
253
|
+
cardStep.args = args;
|
|
254
|
+
}
|
|
255
|
+
steps.push({ name: spec.step, endpoint: built.request.endpoint, task_type: built.taskType, payload: built.request.payload });
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
if (!(err instanceof UsageError))
|
|
259
|
+
throw err;
|
|
260
|
+
errors.push(`${spec.step}: ${err.message}`);
|
|
261
|
+
steps.push({
|
|
262
|
+
name: spec.step,
|
|
263
|
+
endpoint: cardStep?.endpoint ?? '',
|
|
264
|
+
task_type: getStep(spec.step)?.produces ?? '',
|
|
265
|
+
payload: { input: UPSTREAM_PLACEHOLDER, ...spec.args },
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
emitDryRun({
|
|
270
|
+
dry_run: true,
|
|
271
|
+
valid: errors.length === 0,
|
|
272
|
+
input: card.inputSummary,
|
|
273
|
+
source_task_id: plan.sourceTaskId,
|
|
274
|
+
source_type: plan.sourceType,
|
|
275
|
+
scenario: flags.for,
|
|
276
|
+
model: plan.model,
|
|
277
|
+
model_reason: plan.modelReason,
|
|
278
|
+
steps,
|
|
279
|
+
pending_uploads: pendingUploads,
|
|
280
|
+
deliverables: card.deliverables,
|
|
281
|
+
candidates: flags.candidates,
|
|
282
|
+
warnings,
|
|
283
|
+
errors,
|
|
284
|
+
cost_notes: costNotes(card),
|
|
285
|
+
}, renderPlanCard(card));
|
|
286
|
+
}
|
|
287
|
+
/** stdout: one JSON line, or the card followed by warnings/errors and the verdict. */
|
|
288
|
+
function emitDryRun(result, cardText) {
|
|
289
|
+
const lines = cardText ? [cardText] : [];
|
|
290
|
+
if (result.warnings.length > 0) {
|
|
291
|
+
lines.push(t({ zh: '警告(已自动调整):', en: 'warnings (auto-adjusted):' }));
|
|
292
|
+
for (const w of result.warnings)
|
|
293
|
+
lines.push(` ! ${w}`);
|
|
294
|
+
}
|
|
295
|
+
if (result.errors.length > 0) {
|
|
296
|
+
lines.push(t({ zh: '错误(阻断):', en: 'errors (blocking):' }));
|
|
297
|
+
for (const e of result.errors)
|
|
298
|
+
lines.push(` ✗ ${e}`);
|
|
299
|
+
}
|
|
300
|
+
lines.push(result.valid
|
|
301
|
+
? t({ zh: 'dry run:方案有效;未提交任务,未消耗积分', en: 'dry run: plan is valid; nothing submitted, no credits spent' })
|
|
302
|
+
: t({ zh: 'dry run:方案无效;修正上述错误后重试', en: 'dry run: plan is invalid; fix the errors above and retry' }));
|
|
303
|
+
emitResult(result, lines.join('\n'));
|
|
304
|
+
if (!result.valid)
|
|
305
|
+
process.exitCode = EXIT.USAGE;
|
|
306
|
+
}
|
|
121
307
|
export function emitMakeOutcome(outcome) {
|
|
122
308
|
const finalChain = outcome.chainResults[outcome.chainResults.length - 1];
|
|
309
|
+
// Every task this run paid for: generation(s) first, then each chain step.
|
|
310
|
+
const credits = summarizeCredits([...outcome.results, ...outcome.chainResults]);
|
|
123
311
|
if (outcome.results.length === 1) {
|
|
124
312
|
const primary = outcome.results[0];
|
|
125
313
|
const merged = finalChain
|
|
@@ -129,14 +317,26 @@ export function emitMakeOutcome(outcome) {
|
|
|
129
317
|
merged.chain = outcome.chainResults;
|
|
130
318
|
if (outcome.stoppedEarly)
|
|
131
319
|
merged.stopped_early = outcome.stoppedEarly;
|
|
320
|
+
if (credits.breakdown.length > 1) {
|
|
321
|
+
merged.credits_consumed = credits.total;
|
|
322
|
+
merged.credits_breakdown = credits.breakdown;
|
|
323
|
+
}
|
|
132
324
|
printTaskResult(merged);
|
|
133
325
|
if (merged.preview)
|
|
134
326
|
printInlineImage(merged.preview);
|
|
135
327
|
}
|
|
136
328
|
else {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
.
|
|
140
|
-
|
|
329
|
+
const lines = outcome.results.map((r, i) => `#${i + 1} ${r.task_id} ${r.output_dir ?? r.status}`);
|
|
330
|
+
if (credits.breakdown.length > 0) {
|
|
331
|
+
lines.push(t({ zh: `消耗积分合计: ${credits.total}`, en: `credits consumed (total): ${credits.total}` }));
|
|
332
|
+
for (const line of credits.breakdown)
|
|
333
|
+
lines.push(` ${line.type} ${line.task_id} ${line.credits_consumed}`);
|
|
334
|
+
}
|
|
335
|
+
emitResult({
|
|
336
|
+
candidates: outcome.results,
|
|
337
|
+
chain: outcome.chainResults,
|
|
338
|
+
credits_consumed: credits.total,
|
|
339
|
+
credits_breakdown: credits.breakdown,
|
|
340
|
+
}, lines.join('\n'));
|
|
141
341
|
}
|
|
142
342
|
}
|
package/dist/commands/shared.js
CHANGED
|
@@ -51,6 +51,21 @@ export function parseChainFlag(value) {
|
|
|
51
51
|
return undefined;
|
|
52
52
|
return parseThenExpression(value);
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Sum the spend of every task in a run and keep the per-task lines. Tasks
|
|
56
|
+
* without a settled `credits_consumed` (e.g. --no-wait) are skipped.
|
|
57
|
+
*/
|
|
58
|
+
export function summarizeCredits(tasks) {
|
|
59
|
+
const breakdown = [];
|
|
60
|
+
let total = 0;
|
|
61
|
+
for (const task of tasks) {
|
|
62
|
+
if (task.credits_consumed === undefined)
|
|
63
|
+
continue;
|
|
64
|
+
breakdown.push({ task_id: task.task_id, type: task.type, credits_consumed: task.credits_consumed });
|
|
65
|
+
total += task.credits_consumed;
|
|
66
|
+
}
|
|
67
|
+
return { total, breakdown };
|
|
68
|
+
}
|
|
54
69
|
/** Standard result output for a finished task (human + JSON modes). */
|
|
55
70
|
export function printTaskResult(result) {
|
|
56
71
|
const lines = [];
|
|
@@ -58,7 +73,14 @@ export function printTaskResult(result) {
|
|
|
58
73
|
zh: `任务 ${result.task_id} ${result.status === 'success' ? '完成' : result.status}`,
|
|
59
74
|
en: `task ${result.task_id} ${result.status}`,
|
|
60
75
|
}));
|
|
61
|
-
if (result.
|
|
76
|
+
if (result.credits_breakdown && result.credits_breakdown.length > 1) {
|
|
77
|
+
lines.push(t({ zh: `消耗积分合计: ${result.credits_consumed}`, en: `credits consumed (total): ${result.credits_consumed}` }));
|
|
78
|
+
const typeWidth = Math.max(...result.credits_breakdown.map((l) => l.type.length));
|
|
79
|
+
for (const line of result.credits_breakdown) {
|
|
80
|
+
lines.push(` ${line.type.padEnd(typeWidth)} ${line.task_id} ${line.credits_consumed}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else if (result.credits_consumed !== undefined) {
|
|
62
84
|
lines.push(t({ zh: `消耗积分: ${result.credits_consumed}`, en: `credits consumed: ${result.credits_consumed}` }));
|
|
63
85
|
}
|
|
64
86
|
if (result.output_dir) {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tripo update` — self-update via npm, plus the startup "new version
|
|
3
|
+
* available" hint (like Claude Code / Codex CLI).
|
|
4
|
+
*
|
|
5
|
+
* The startup check never blocks a command: it reads a 24h cache at
|
|
6
|
+
* ~/.tripo/update-check.json and only refreshes it in the background when
|
|
7
|
+
* stale. The hint is printed to stderr, TTY-only, and never in --json mode,
|
|
8
|
+
* so agents and pipes are unaffected.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import pc from 'picocolors';
|
|
14
|
+
import { tripoHome } from '../config/config.js';
|
|
15
|
+
import { CliError, NetworkError } from '../core/errors.js';
|
|
16
|
+
import { emitResult, isJsonMode, log, success } from '../core/output.js';
|
|
17
|
+
import { t } from '../i18n.js';
|
|
18
|
+
import { EXIT } from '../knowledge/error-catalog.js';
|
|
19
|
+
import { cliVersion } from '../version.js';
|
|
20
|
+
const PACKAGE_NAME = 'tripo-cli';
|
|
21
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
22
|
+
const FETCH_TIMEOUT_MS = 3000;
|
|
23
|
+
function registryUrl() {
|
|
24
|
+
const base = process.env.TRIPO_NPM_REGISTRY || 'https://registry.npmjs.org';
|
|
25
|
+
return `${base.replace(/\/+$/, '')}/${PACKAGE_NAME}/latest`;
|
|
26
|
+
}
|
|
27
|
+
function cachePath() {
|
|
28
|
+
return join(tripoHome(), 'update-check.json');
|
|
29
|
+
}
|
|
30
|
+
function readCache() {
|
|
31
|
+
try {
|
|
32
|
+
const raw = JSON.parse(readFileSync(cachePath(), 'utf8'));
|
|
33
|
+
if (typeof raw.checked_at === 'number' && typeof raw.latest === 'string')
|
|
34
|
+
return raw;
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
function writeCache(latest) {
|
|
40
|
+
try {
|
|
41
|
+
mkdirSync(tripoHome(), { recursive: true, mode: 0o700 });
|
|
42
|
+
writeFileSync(cachePath(), JSON.stringify({ checked_at: Date.now(), latest }) + '\n');
|
|
43
|
+
}
|
|
44
|
+
catch { }
|
|
45
|
+
}
|
|
46
|
+
/** Numeric major.minor.patch comparison; pre-release tags are ignored. */
|
|
47
|
+
export function isNewerVersion(candidate, current) {
|
|
48
|
+
const parse = (v) => v.replace(/^v/, '').split('-')[0].split('.').map((x) => Number(x) || 0);
|
|
49
|
+
const a = parse(candidate);
|
|
50
|
+
const b = parse(current);
|
|
51
|
+
for (let i = 0; i < 3; i++) {
|
|
52
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0))
|
|
53
|
+
return (a[i] ?? 0) > (b[i] ?? 0);
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
/** Ask the npm registry for the latest published version. */
|
|
58
|
+
export async function fetchLatestVersion() {
|
|
59
|
+
const url = registryUrl();
|
|
60
|
+
let response;
|
|
61
|
+
try {
|
|
62
|
+
response = await fetch(url, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
throw new NetworkError(`cannot reach the npm registry (${url}): ${err instanceof Error ? err.message : String(err)}`);
|
|
66
|
+
}
|
|
67
|
+
if (!response.ok)
|
|
68
|
+
throw new NetworkError(`npm registry answered HTTP ${response.status} for ${url}`);
|
|
69
|
+
const body = (await response.json());
|
|
70
|
+
if (typeof body.version !== 'string')
|
|
71
|
+
throw new NetworkError(`unexpected npm registry response for ${url}`);
|
|
72
|
+
writeCache(body.version);
|
|
73
|
+
return body.version;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Startup hook: print a one-line hint when a newer version is known.
|
|
77
|
+
* Cheap on every run (one small file read); refreshes the cache in the
|
|
78
|
+
* background at most once a day. Skipped headless, in --json mode, and when
|
|
79
|
+
* TRIPO_NO_UPDATE_CHECK is set.
|
|
80
|
+
*/
|
|
81
|
+
export function notifyIfOutdated() {
|
|
82
|
+
if (process.env.TRIPO_NO_UPDATE_CHECK || process.env.CI)
|
|
83
|
+
return;
|
|
84
|
+
if (isJsonMode() || !process.stderr.isTTY)
|
|
85
|
+
return;
|
|
86
|
+
const current = cliVersion();
|
|
87
|
+
const cache = readCache();
|
|
88
|
+
if (cache && isNewerVersion(cache.latest, current)) {
|
|
89
|
+
process.stderr.write(pc.yellow(t({
|
|
90
|
+
zh: `! 有新版本 tripo-cli ${cache.latest} 可用(当前 ${current}),运行 tripo update 升级`,
|
|
91
|
+
en: `! a new version of tripo-cli is available: ${cache.latest} (current ${current}) — run "tripo update" to upgrade`,
|
|
92
|
+
})) + '\n');
|
|
93
|
+
}
|
|
94
|
+
if (!cache || Date.now() - cache.checked_at > CHECK_INTERVAL_MS) {
|
|
95
|
+
// Fire-and-forget; the result shows up on the next run.
|
|
96
|
+
fetchLatestVersion().catch(() => { });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function runNpmInstall() {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const child = spawn('npm', ['install', '-g', `${PACKAGE_NAME}@latest`], {
|
|
102
|
+
stdio: 'inherit',
|
|
103
|
+
shell: process.platform === 'win32',
|
|
104
|
+
});
|
|
105
|
+
child.on('error', (err) => reject(new CliError(`failed to run npm: ${err.message}`, EXIT.FATAL)));
|
|
106
|
+
child.on('exit', (code) => {
|
|
107
|
+
if (code === 0)
|
|
108
|
+
resolve();
|
|
109
|
+
else
|
|
110
|
+
reject(new CliError(`npm install exited with code ${code}`, EXIT.FATAL, t({ zh: `手动执行: npm install -g ${PACKAGE_NAME}@latest`, en: `run manually: npm install -g ${PACKAGE_NAME}@latest` })));
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export function registerUpdateCommand(program) {
|
|
115
|
+
program
|
|
116
|
+
.command('update')
|
|
117
|
+
.description('update tripo-cli to the latest version (npm install -g tripo-cli@latest)')
|
|
118
|
+
.option('--check', 'only check for a newer version; do not install')
|
|
119
|
+
.action(async (opts) => {
|
|
120
|
+
const current = cliVersion();
|
|
121
|
+
const latest = await fetchLatestVersion();
|
|
122
|
+
const available = isNewerVersion(latest, current);
|
|
123
|
+
if (!available) {
|
|
124
|
+
success(t({ zh: `已是最新版本 ${current}`, en: `already up to date (${current})` }));
|
|
125
|
+
emitResult({ current, latest, update_available: false, updated: false }, current);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (opts.check) {
|
|
129
|
+
log(t({ zh: `有新版本 ${latest} 可用(当前 ${current}),运行 tripo update 升级`, en: `new version ${latest} available (current ${current}); run "tripo update" to upgrade` }));
|
|
130
|
+
emitResult({ current, latest, update_available: true, updated: false }, `${current} → ${latest}`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
log(t({ zh: `正在升级 ${current} → ${latest}…`, en: `updating ${current} → ${latest}…` }));
|
|
134
|
+
await runNpmInstall();
|
|
135
|
+
success(t({ zh: `已升级到 tripo-cli ${latest}`, en: `updated to tripo-cli ${latest}` }));
|
|
136
|
+
emitResult({ current, latest, update_available: true, updated: true }, `${current} → ${latest}`);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -16,8 +16,16 @@ import { defaultOutputDir, downloadArtifacts } from './download.js';
|
|
|
16
16
|
import { ApiError, UsageError, balanceSuggestion } from './errors.js';
|
|
17
17
|
import { isQuiet, log, warn } from './output.js';
|
|
18
18
|
import { uploadLocalFile } from './upload.js';
|
|
19
|
-
/**
|
|
20
|
-
export
|
|
19
|
+
/** Placeholder `input` value for a local file that a dry run would upload. */
|
|
20
|
+
export function uploadPlaceholder(path) {
|
|
21
|
+
return `<upload:${path}>`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a user input string into the API `input` value (upload as needed).
|
|
25
|
+
* `dryRun` skips the upload and returns a `<upload:path>` placeholder so the
|
|
26
|
+
* plan can be built with zero network calls.
|
|
27
|
+
*/
|
|
28
|
+
export async function resolveInputValue(client, raw, opts = {}) {
|
|
21
29
|
const detected = detectInput(raw);
|
|
22
30
|
switch (detected.kind) {
|
|
23
31
|
case 'history-ref': {
|
|
@@ -36,6 +44,8 @@ export async function resolveInputValue(client, raw) {
|
|
|
36
44
|
}
|
|
37
45
|
case 'image-file':
|
|
38
46
|
case 'model-file': {
|
|
47
|
+
if (opts.dryRun)
|
|
48
|
+
return { value: uploadPlaceholder(detected.value) };
|
|
39
49
|
const token = await uploadLocalFile(client, detected.value);
|
|
40
50
|
return { value: token };
|
|
41
51
|
}
|
package/dist/knowledge/chains.js
CHANGED
|
@@ -21,23 +21,23 @@ export const MODEL_PRODUCING_TYPES = new Set([
|
|
|
21
21
|
'animate_retarget',
|
|
22
22
|
]);
|
|
23
23
|
export const STEPS = [
|
|
24
|
-
{ name: 'refine', endpoint: '/v3/models/refine', produces: 'refine_model', accepts: ['text_to_model'], acceptsFile: false },
|
|
25
|
-
{ name: 'texture', endpoint: '/v3/models/texture', produces: 'texture_model', accepts: 'any-model', acceptsFile: true },
|
|
26
|
-
{ name: 'stylize', endpoint: '/v3/models/stylize', produces: 'stylize_model', accepts: 'any-model', acceptsFile: true },
|
|
27
|
-
{ name: 'convert', endpoint: '/v3/models/convert', produces: 'convert_model', accepts: 'any-model', acceptsFile: true },
|
|
28
|
-
{ name: 'import', endpoint: '/v3/models/import', produces: 'import_model', accepts: [], acceptsFile: true },
|
|
29
|
-
{ name: 'rig-check', endpoint: '/v3/animations/rig-check', produces: 'animate_prerigcheck', accepts: 'any-model', acceptsFile: true, passthrough: true, aliases: ['check', 'rigcheck'] },
|
|
30
|
-
{ name: 'rig', endpoint: '/v3/animations/rig', produces: 'animate_rig', accepts: 'any-model', acceptsFile: true },
|
|
31
|
-
{ name: 'retarget', endpoint: '/v3/animations/retarget', produces: 'animate_retarget', accepts: ['animate_rig'], acceptsFile: false, aliases: ['animate'] },
|
|
32
|
-
{ name: 'segment', endpoint: '/v3/mesh/segment', produces: 'mesh_segmentation', accepts: 'any-model', acceptsFile: true },
|
|
33
|
-
{ name: 'complete', endpoint: '/v3/mesh/complete', produces: 'mesh_completion', accepts: ['mesh_segmentation'], acceptsFile: false },
|
|
34
|
-
{ name: 'decimate', endpoint: '/v3/mesh/decimate', produces: 'highpoly_to_lowpoly', accepts: 'any-model', acceptsFile: true, aliases: ['lowpoly'] },
|
|
24
|
+
{ 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' } },
|
|
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: 'stylize', endpoint: '/v3/models/stylize', produces: 'stylize_model', accepts: 'any-model', acceptsFile: true, describe: { zh: '按风格重塑模型(lego/voxel/...)', en: 'restyle the model (lego/voxel/...)' } },
|
|
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
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 },
|
|
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)' } },
|
|
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 },
|
|
40
|
+
{ name: 'smartsegment', endpoint: '/v3/mesh/smartsegment', produces: 'smartsegment_model', accepts: [], acceptsFile: true, describe: { zh: '智能语义分割', en: 'smart semantic segmentation' } },
|
|
41
41
|
];
|
|
42
42
|
const stepIndex = new Map();
|
|
43
43
|
for (const s of STEPS) {
|
package/dist/pipeline/make.js
CHANGED
|
@@ -53,7 +53,7 @@ export async function planMake(client, options) {
|
|
|
53
53
|
case 'image':
|
|
54
54
|
case 'url-image': {
|
|
55
55
|
const raw = classified.values[0].value;
|
|
56
|
-
const { value } = await resolveInputValue(client, raw);
|
|
56
|
+
const { value } = await resolveInputValue(client, raw, { dryRun: options.dryRun });
|
|
57
57
|
const choice = chooseModel({
|
|
58
58
|
explicit: options.model ?? scenario?.model,
|
|
59
59
|
faceLimit: typeof genParams.face_limit === 'number' ? genParams.face_limit : undefined,
|
|
@@ -69,7 +69,7 @@ export async function planMake(client, options) {
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
case 'multiview': {
|
|
72
|
-
const views = await assignViews(client, classified.values.map((v) => v.value));
|
|
72
|
+
const views = await assignViews(client, classified.values.map((v) => v.value), { dryRun: options.dryRun });
|
|
73
73
|
const choice = chooseModel({
|
|
74
74
|
explicit: options.model ?? scenario?.model,
|
|
75
75
|
faceLimit: typeof genParams.face_limit === 'number' ? genParams.face_limit : undefined,
|
|
@@ -86,7 +86,7 @@ export async function planMake(client, options) {
|
|
|
86
86
|
}
|
|
87
87
|
case 'model': {
|
|
88
88
|
const raw = classified.values[0].value;
|
|
89
|
-
const { value } = await resolveInputValue(client, raw);
|
|
89
|
+
const { value } = await resolveInputValue(client, raw, { dryRun: options.dryRun });
|
|
90
90
|
const generation = buildStepRequest('import', value, {});
|
|
91
91
|
if (chain.length === 0) {
|
|
92
92
|
warnings.push(t({
|
|
@@ -116,24 +116,31 @@ export async function planMake(client, options) {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Chain legality against the plan's upstream task type (throws UsageError).
|
|
121
|
+
* Skipped when the upstream type is unknown (raw task id without local history).
|
|
122
|
+
*/
|
|
123
|
+
export function checkPlanChain(plan) {
|
|
124
|
+
if (plan.chain.length === 0)
|
|
125
|
+
return;
|
|
126
|
+
const upstream = plan.generation?.taskType ?? plan.sourceType;
|
|
127
|
+
if (!upstream)
|
|
128
|
+
return;
|
|
129
|
+
try {
|
|
130
|
+
validateChain(plan.chain, upstream);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (error instanceof ChainError)
|
|
134
|
+
throw new UsageError(error.message);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
119
138
|
/** Execute a plan produced by planMake. */
|
|
120
139
|
export async function executeMake(client, plan, options) {
|
|
121
140
|
for (const w of plan.warnings)
|
|
122
141
|
warn(w);
|
|
123
142
|
// Validate chain legality up front so an illegal plan spends zero credits.
|
|
124
|
-
|
|
125
|
-
const upstream = plan.generation?.taskType ?? plan.sourceType;
|
|
126
|
-
if (upstream) {
|
|
127
|
-
try {
|
|
128
|
-
validateChain(plan.chain, upstream);
|
|
129
|
-
}
|
|
130
|
-
catch (error) {
|
|
131
|
-
if (error instanceof ChainError)
|
|
132
|
-
throw new UsageError(error.message);
|
|
133
|
-
throw error;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
143
|
+
checkPlanChain(plan);
|
|
137
144
|
await precheckBalance(client);
|
|
138
145
|
let sourceTaskId;
|
|
139
146
|
let sourceType;
|
|
@@ -195,7 +202,7 @@ export async function executeMake(client, plan, options) {
|
|
|
195
202
|
* Map 2–4 images to views. Filename hints (front/back/left/right, 前/后/左/右)
|
|
196
203
|
* win; remaining slots fill positionally in [front, left, back, right] order.
|
|
197
204
|
*/
|
|
198
|
-
export async function assignViews(client, files) {
|
|
205
|
+
export async function assignViews(client, files, opts = {}) {
|
|
199
206
|
if (files.length < 2 || files.length > 4) {
|
|
200
207
|
throw new UsageError(`multiview needs 2–4 images (got ${files.length})`);
|
|
201
208
|
}
|
|
@@ -227,7 +234,7 @@ export async function assignViews(client, files) {
|
|
|
227
234
|
}
|
|
228
235
|
const resolved = {};
|
|
229
236
|
for (const [view, file] of Object.entries(views)) {
|
|
230
|
-
resolved[view] = (await resolveInputValue(client, file)).value;
|
|
237
|
+
resolved[view] = (await resolveInputValue(client, file, { dryRun: opts.dryRun })).value;
|
|
231
238
|
}
|
|
232
239
|
return resolved;
|
|
233
240
|
}
|
package/dist/program.js
CHANGED
|
@@ -16,6 +16,7 @@ import { registerMcpCommand } from './commands/mcp.js';
|
|
|
16
16
|
import { registerProcessCommands } from './commands/process-groups.js';
|
|
17
17
|
import { registerTaskCommands } from './commands/task.js';
|
|
18
18
|
import { registerTopupCommand } from './commands/topup.js';
|
|
19
|
+
import { notifyIfOutdated, registerUpdateCommand } from './commands/update.js';
|
|
19
20
|
import { registerViewCommands } from './commands/view.js';
|
|
20
21
|
import { loadConfig } from './config/config.js';
|
|
21
22
|
import { RefError } from './config/history.js';
|
|
@@ -55,6 +56,10 @@ export function buildProgram() {
|
|
|
55
56
|
const lang = loadConfig().language;
|
|
56
57
|
if (lang)
|
|
57
58
|
setLocale(lang);
|
|
59
|
+
// Startup version hint (cached, non-blocking). `update` reports on its
|
|
60
|
+
// own; `mcp` speaks JSON-RPC over stdio and must stay silent.
|
|
61
|
+
if (actionCommand.name() !== 'update' && actionCommand.name() !== 'mcp')
|
|
62
|
+
notifyIfOutdated();
|
|
58
63
|
});
|
|
59
64
|
registerMakeCommand(program);
|
|
60
65
|
registerAiCommand(program);
|
|
@@ -68,6 +73,7 @@ export function buildProgram() {
|
|
|
68
73
|
registerBatchCommand(program);
|
|
69
74
|
registerConfigCommands(program);
|
|
70
75
|
registerDoctorCommand(program);
|
|
76
|
+
registerUpdateCommand(program);
|
|
71
77
|
registerDocsCommand(program);
|
|
72
78
|
registerCompletionCommand(program);
|
|
73
79
|
registerMcpCommand(program);
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -30,13 +30,14 @@ Regions (ov = international, cn = China mainland) share one CLI: `tripo login --
|
|
|
30
30
|
|
|
31
31
|
## Behavior rules (read first)
|
|
32
32
|
|
|
33
|
-
1. **`tripo make` is synchronous and blocking.** It
|
|
33
|
+
1. **`tripo make` is synchronous and blocking.** It prints a plan card on stderr (one numbered line per API call: endpoint, purpose, args), then submits, polls, downloads artifacts, and exits. In a TTY it asks "Run this plan?" first; `--yes` (auto-enabled when headless) skips the prompt but still prints the card. Wait for the process to finish and read the final JSON from stdout. Do NOT re-implement polling, do NOT add your own timeout shorter than 15 minutes, do NOT stop just because you saw a task_id in the logs.
|
|
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
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)
|
|
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
|
-
6. **
|
|
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
|
+
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").
|
|
40
41
|
|
|
41
42
|
## The one command you usually need
|
|
42
43
|
|
|
@@ -51,7 +52,7 @@ tripo make @last --then convert:fbx --json --yes # continue from last t
|
|
|
51
52
|
- `--for` scenario presets: `game-mobile` `game-pc` `film` `print` `ar-web` `anim` `toy`
|
|
52
53
|
- `--then` chain steps: `refine texture stylize convert import rig-check rig retarget segment complete decimate smartsegment`
|
|
53
54
|
- step args: `convert:format=FBX,texture_size=2048` (bare value = primary arg: `convert:fbx`, `stylize:lego`, `decimate:5000`)
|
|
54
|
-
- Output JSON: `{"task_id","type","status","credits_consumed","output_dir","files",["model_file"],["preview"],["chain"]}`
|
|
55
|
+
- Output JSON: `{"task_id","type","status","credits_consumed","output_dir","files",["model_file"],["preview"],["chain"],["credits_breakdown"]}` — with `--then`, `credits_consumed` is the **total** for the run and `credits_breakdown` itemizes every task (`[{task_id,type,credits_consumed}]`, generation first)
|
|
55
56
|
|
|
56
57
|
## Pipes (composing steps yourself)
|
|
57
58
|
|
|
@@ -77,6 +78,7 @@ tripo make cat.png --json | tripo anim rig --json | tripo anim retarget --param
|
|
|
77
78
|
## Detailed docs in this package
|
|
78
79
|
|
|
79
80
|
- `commands/` — one file per command with every flag and parameter
|
|
81
|
+
- model versions · H series (`tripo-v3.x`) · P series (`tripo-p1`, `tripo-p2` Preview) · alias → wire value · GA / Preview / Legacy status · quad → `commands/generate` § "Models & Versions" (also the P Series endpoint entry points)
|
|
80
82
|
- `examples/` — copy-paste recipes per scenario (game/print/animation/...)
|
|
81
83
|
- `common-errors.md` — error table with fixes
|
|
82
84
|
|
|
@@ -6,21 +6,46 @@ Everything below is the full parameter reference from the official API docs —
|
|
|
6
6
|
## 3D
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
-
tripo generate text-to-model "a ceramic teapot" [--model tripo-v3.1|tripo-p1] [-p face_limit=15000] ...
|
|
9
|
+
tripo generate text-to-model "a ceramic teapot" [--model tripo-v3.1|tripo-p1|tripo-p2] [-p face_limit=15000] ...
|
|
10
10
|
tripo generate image-to-model photo.png [-p texture_alignment=original_image] [-p orientation=align_image]
|
|
11
11
|
tripo generate multiview-to-model front.png back.png [left.png right.png]
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
###
|
|
14
|
+
### P Series (`tripo-p1` / `tripo-p2`) — same endpoints, different model
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
The P series (low-poly, clean topology) uses the **same three endpoints**; only the `model` value changes. The official docs document it as a separate "P Series" tab on each generation page — this is the direct entry point:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
tripo generate text-to-model "a low poly knight" --model tripo-p1 -p face_limit=8000 # → model: P1-20260311
|
|
20
|
+
tripo generate image-to-model photo.png --model tripo-p2 -p quad=true -p face_limit=12000 # → model: P2-20260801 (Preview)
|
|
21
|
+
tripo generate multiview-to-model front.png back.png --model tripo-p1
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| endpoint | H series (standard) page | P series page |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| `POST /v3/generation/text-to-model` | `…/docs/generation-text-to-model/standard` | <https://developers.tripo3d.ai/en/docs/generation-text-to-model/p> |
|
|
27
|
+
| `POST /v3/generation/image-to-model` | `…/docs/generation-image-to-model/standard` | <https://developers.tripo3d.ai/en/docs/generation-image-to-model/p> |
|
|
28
|
+
| `POST /v3/generation/multiview-to-model` | `…/docs/generation-multiview-to-model/standard` | <https://developers.tripo3d.ai/en/docs/generation-multiview-to-model/p> |
|
|
29
|
+
|
|
30
|
+
(cn: replace `developers.tripo3d.ai/en` with `developers.tripo3d.com/zh`.) P-series constraints — face_limit ranges, unsupported params, P2's `quad` — are in the table below; the CLI enforces them locally.
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
### 3D models (`--model`) — Models & Versions
|
|
33
|
+
|
|
34
|
+
Keywords: model versions · H series (v3.x) · P series (P1 / P2) · Preview · quad · alias → wire value.
|
|
35
|
+
|
|
36
|
+
| alias (what you type) | wire value (what the API receives) | status | positioning | notes |
|
|
37
|
+
| --- | --- | --- | --- | --- |
|
|
38
|
+
| `tripo-v3.1` (default) | `v3.1-20260211` | GA | latest high-precision; AAA / 3D printing | full feature set |
|
|
39
|
+
| `tripo-p1` | `P1-20260311` | GA | best low-poly; games / UGC / mobile | face_limit 48–20000 (CLI enforces ≥50); **no** `quad` `smart_low_poly` `generate_parts` `geometry_quality`; ~2s mesh; extra `export_uv` |
|
|
40
|
+
| `tripo-p2` | `P2-20260801` | **Preview** (2026-08) | P1 upgrade (P series) | adds `quad` (forces FBX); face_limit 48–50000 tri / 48–25000 quad (validated locally); still **no** `smart_low_poly` `generate_parts` `geometry_quality` (stripped with a warning); credits 100 (no texture) / 110 / 120 / 130 for standard / detailed / extreme. Explicit `--model` only — the CLI never auto-selects it |
|
|
41
|
+
| `tripo-v3.0` / `tripo-v2.5` | `v3.0-20250812` / `v2.5-20250123` | Legacy | previous generations | only when the human asks |
|
|
42
|
+
|
|
43
|
+
**Alias vs wire value.** `tripo-*`, `p1`, `p2`, `v3.1` … are CLI-side aliases; the CLI always sends the dated wire value in the `model` field (`--model tripo-p2` → `"model": "P2-20260801"`). The v3 server whitelist accepts wire values only — sending an alias straight to the API returns 1004 `invalid model` with the allowed list. `task.json` records the wire value that was actually used. `tripo-turbo` / `tripo-v2.0` appear in some docs but are **not** on the v3 whitelist (400).
|
|
44
|
+
|
|
45
|
+
Official docs (ov / cn):
|
|
46
|
+
- Models & Versions: <https://developers.tripo3d.ai/en/docs/models-and-versions> · <https://developers.tripo3d.com/zh/docs/models-and-versions>
|
|
47
|
+
- P Series model detail: <https://developers.tripo3d.ai/en/models/p1> · <https://developers.tripo3d.com/zh/models/p1>
|
|
48
|
+
- P Series endpoints: `…/docs/generation-text-to-model/p`, `…/docs/generation-image-to-model/p`, `…/docs/generation-multiview-to-model/p` (the `/standard` tab on each page is the H-series version)
|
|
24
49
|
|
|
25
50
|
### Parameters (text / image / multiview-to-model)
|
|
26
51
|
|
|
@@ -77,12 +102,15 @@ tripo generate image-to-splat photo.png # gaussian splat
|
|
|
77
102
|
|
|
78
103
|
### Image models (`--model`)
|
|
79
104
|
|
|
80
|
-
| model | provider | notes |
|
|
81
|
-
| --- | --- | --- |
|
|
82
|
-
| `seedream_v4` | ByteDance | text-to-image default (balanced); **not accepted by image-to-image** |
|
|
83
|
-
| `seedream_v5` | ByteDance | image-to-image default; higher quality |
|
|
84
|
-
| `gemini-2.5-flash` / `gemini-3-pro` / `gemini-3.1-flash` | Google | fast / high quality / latest fast. Backend aliases `banana` / `banana_pro` / `banana2` are also accepted |
|
|
85
|
-
| `chat_image_1` / `chat_image_1.5`
|
|
105
|
+
| model | provider | status | notes |
|
|
106
|
+
| --- | --- | --- | --- |
|
|
107
|
+
| `seedream_v4` | ByteDance | GA | text-to-image default (balanced); **not accepted by image-to-image** |
|
|
108
|
+
| `seedream_v5` | ByteDance | GA | image-to-image default; higher quality |
|
|
109
|
+
| `gemini-2.5-flash` / `gemini-3-pro` / `gemini-3.1-flash` | Google | GA | fast / high quality / latest fast. Backend aliases `banana` / `banana_pro` / `banana2` are also accepted |
|
|
110
|
+
| `chat_image_1` / `chat_image_1.5` | OpenAI | retiring 2026-10-23 / 2026-12-01 | GPT image; 1.5 = higher quality — move to `chat_image_2` |
|
|
111
|
+
| `chat_image_2` | OpenAI | GA | GPT latest |
|
|
112
|
+
|
|
113
|
+
Image model ids are sent as-is (no alias normalization). Full list with retirement dates: <https://developers.tripo3d.ai/en/docs/models-and-versions>.
|
|
86
114
|
|
|
87
115
|
### Image parameters
|
|
88
116
|
|
package/skill/commands/make.md
CHANGED
|
@@ -6,6 +6,27 @@ The magic command: input in, finished local 3D artifacts out. Blocking; exits wh
|
|
|
6
6
|
tripo make <input...> [options]
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
## Before anything is spent: the plan card
|
|
10
|
+
|
|
11
|
+
`make` first prints a plan card on stderr — input, scenario, model, then **one numbered line per API call** (endpoint, what it does, its args), deliverables and cost notes:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
── Plan ──────────────────────────
|
|
15
|
+
input: a medieval knight
|
|
16
|
+
scenario: game-mobile (Mobile game / low-poly)
|
|
17
|
+
model: P1-20260311 — scenario game-mobile
|
|
18
|
+
steps:
|
|
19
|
+
1. text-to-model POST /v3/generation/text-to-model — generate a 3D model from text
|
|
20
|
+
model="P1-20260311" texture=true pbr=true texture_quality="standard" face_limit=15000
|
|
21
|
+
2. texture POST /v3/models/texture — regenerate textures / PBR materials
|
|
22
|
+
3. convert POST /v3/models/convert — convert format / texture size / orientation
|
|
23
|
+
format="FBX" texture_size=2048
|
|
24
|
+
deliverables: FBX, GLB
|
|
25
|
+
──────────────────────────────────
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
In a TTY it then asks `Run this plan?`; answering no exits 0 with `cancelled; no credits spent`. `--yes` (auto-enabled when stdin/stdout are not TTYs, i.e. for agents and pipes) skips the question but still prints the card, so the log shows exactly what ran. `--quiet` hides it.
|
|
29
|
+
|
|
9
30
|
## Inputs (auto-detected)
|
|
10
31
|
|
|
11
32
|
| you pass | it runs |
|
|
@@ -20,7 +41,7 @@ tripo make <input...> [options]
|
|
|
20
41
|
|
|
21
42
|
- `--for <scenario>` — `game-mobile` `game-pc` `film` `print` `ar-web` `anim` `toy`. Sets model+params+chain+format from the domain knowledge base.
|
|
22
43
|
- `--then <steps>` — processing chain, comma-separated. Steps: `refine texture stylize convert import rig-check rig retarget segment complete decimate smartsegment`. Args: `step:key=value,...`; bare value maps to the step's primary arg (`convert:fbx` → format, `stylize:lego` → style, `decimate:5000` → face_limit, `retarget:preset:walk` → animation). Overrides the scenario chain.
|
|
23
|
-
- `--model <m>` — force `tripo-v3.1` (high fidelity, default), `tripo-p1` (low-poly; face_limit 50–20000; quad/parts/geometry_quality unsupported and stripped) or `tripo-p2` (P-series preview; adds quad, face_limit 48–50000 tri / 48–25000 quad; never auto-selected). The CLI normalizes these aliases to the wire versions (`v3.1-20260211` / `P1-20260311` / `P2-20260801`) the server actually accepts.
|
|
44
|
+
- `--model <m>` — force `tripo-v3.1` (high fidelity, default), `tripo-p1` (low-poly; face_limit 50–20000; quad/parts/geometry_quality unsupported and stripped) or `tripo-p2` (P-series preview; adds quad, face_limit 48–50000 tri / 48–25000 quad; never auto-selected). The CLI normalizes these aliases to the wire versions (`v3.1-20260211` / `P1-20260311` / `P2-20260801`) the server actually accepts. Full table with status (GA / Preview / Legacy) and official links: `tripo docs --topic commands/generate` → "Models & Versions".
|
|
24
45
|
- `-n, --candidates <n>` — up to 4 parallel candidates with different seeds (interactive pick when chaining).
|
|
25
46
|
- `--seed <n>` — fixed model_seed for reproducible geometry.
|
|
26
47
|
- `-p, --param key=value` — extra API parameter, repeatable. Common: `texture=false pbr=false` (bare geometry, cheaper), `texture_quality=detailed`, `face_limit=15000`, `quad=true` (forces FBX), `auto_size=true`, `negative_prompt=...`.
|
|
@@ -30,18 +51,62 @@ tripo make <input...> [options]
|
|
|
30
51
|
- `--name <name>` — history name for `@name` references.
|
|
31
52
|
- `--notify` — desktop notification when done.
|
|
32
53
|
- `--timeout <seconds>` — watch timeout (default 1800).
|
|
54
|
+
- `--yes` (global) — skip the plan-card confirmation; implied when headless.
|
|
55
|
+
- `--dry-run` — plan + validate only; nothing is uploaded or submitted, no credits spent. See below.
|
|
56
|
+
|
|
57
|
+
## Dry run (agents: validate before spending)
|
|
58
|
+
|
|
59
|
+
`tripo make ... --dry-run --json` runs the whole planning stage with **zero network calls** and prints the plan as the result on stdout: the model the dual-model rule picked, the completed request body for every call (scenario defaults merged, aliases normalized to wire values, illegal params stripped, step defaults filled in), and the outcome of every local validation. Works without an API key.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
tripo make concept.png --for game-mobile --then rig,convert:fbx --dry-run --json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{"dry_run":true,"valid":true,
|
|
67
|
+
"input":"concept.png","scenario":"game-mobile",
|
|
68
|
+
"model":"P1-20260311","model_reason":"explicit --model",
|
|
69
|
+
"steps":[
|
|
70
|
+
{"name":"image-to-model","endpoint":"/v3/generation/image-to-model","task_type":"image_to_model",
|
|
71
|
+
"payload":{"input":"<upload:concept.png>","model":"P1-20260311","texture":true,"pbr":true,"texture_quality":"standard","face_limit":15000}},
|
|
72
|
+
{"name":"rig","endpoint":"/v3/animations/rig","task_type":"animate_rig","payload":{"input":"<upstream>","model":"v2.5-20260210"}},
|
|
73
|
+
{"name":"convert","endpoint":"/v3/models/convert","task_type":"convert_model","payload":{"input":"<upstream>","format":"FBX"}}],
|
|
74
|
+
"pending_uploads":["concept.png"],
|
|
75
|
+
"deliverables":["FBX","GLB"],
|
|
76
|
+
"candidates":1,
|
|
77
|
+
"warnings":["--then overrides the default game-mobile processing chain"],
|
|
78
|
+
"errors":[],
|
|
79
|
+
"cost_notes":[]}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- `valid` / `errors` — `errors` are blocking (illegal chain order, out-of-range `face_limit`, quad + GLTF, missing `animation` for retarget, ...). The real run would exit 2 on them before spending anything; the dry run exits 2 too, so branch on the exit code or on `valid`.
|
|
83
|
+
- `warnings` — adjustments the CLI applies automatically (e.g. `P1 does not support quad (removed)`); safe to proceed, but check they match your intent.
|
|
84
|
+
- `steps[].payload` — the exact body that would be POSTed. Local files show as `<upload:path>` (also listed in `pending_uploads`); chain steps use `<upstream>` for the task id that does not exist yet.
|
|
85
|
+
- A bare task id with no local history entry cannot have its type checked offline: chain legality is reported as unverified in `warnings` and checked by the real run.
|
|
86
|
+
- Without `--json` the same plan card as the real run is printed on stdout, followed by the warnings/errors and a verdict line.
|
|
87
|
+
|
|
88
|
+
Typical agent loop: dry-run → fix `errors` / adjust params → dry-run again until `valid` → drop `--dry-run` and run for real.
|
|
33
89
|
|
|
34
90
|
## Output (stdout, --json)
|
|
35
91
|
|
|
36
92
|
```json
|
|
37
|
-
{"task_id":"...","type":"convert_model","status":"success",
|
|
93
|
+
{"task_id":"...","type":"convert_model","status":"success",
|
|
94
|
+
"credits_consumed":145,
|
|
95
|
+
"credits_breakdown":[
|
|
96
|
+
{"task_id":"<generation>","type":"text_to_model","credits_consumed":110},
|
|
97
|
+
{"task_id":"<texture>","type":"texture_model","credits_consumed":30},
|
|
98
|
+
{"task_id":"<convert>","type":"convert_model","credits_consumed":5}],
|
|
38
99
|
"output_dir":"tripo-out/knight-1a2b3c4d","files":["model.fbx","preview.png","task.json"],
|
|
39
100
|
"model_file":".../model.fbx","preview":".../preview.png",
|
|
40
|
-
"
|
|
101
|
+
"source_task_id":"<generation>",
|
|
102
|
+
"chain":[{"task_id":"<texture>","type":"texture_model","credits_consumed":30},{"task_id":"<convert>","type":"convert_model","credits_consumed":5}]}
|
|
41
103
|
```
|
|
42
104
|
|
|
105
|
+
- Single task (no `--then`): `credits_consumed` is that task's spend; no `credits_breakdown`.
|
|
106
|
+
- Chained run: `credits_consumed` is the **total** across generation + every step, and `credits_breakdown` lists each task in run order (generation first). Human mode prints the same as `credits consumed (total): 145` followed by one indented line per task.
|
|
107
|
+
|
|
43
108
|
Exit codes: 0 ok · 2 params · 3 auth · 4 credits · 5 content policy · 6 task failed · 7 network · 9 rate limit.
|
|
44
109
|
|
|
45
110
|
## Cost behavior
|
|
46
111
|
|
|
47
|
-
|
|
112
|
+
The plan card shows every call before anything is submitted; balance is pre-checked before submitting; failed tasks auto-refund frozen credits. After the run, `credits_consumed` / `credits_breakdown` report what was actually billed per task. `texture=false pbr=false` skips texture credits entirely (right for 3D printing).
|
package/skill/common-errors.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
| --- | --- | --- | --- |
|
|
5
5
|
| 3 | 1002 | authentication failed (invalid/missing API key) | set `TRIPO_API_KEY` or run `tripo login`; check the key wasn't revoked |
|
|
6
6
|
| 3 | 1005 | forbidden (no permission for this resource) | check account permissions |
|
|
7
|
-
| 2 | 1003/1004 | malformed request / invalid parameter | check spelling against `tripo docs --topic commands/<cmd>`; the CLI already strips illegal P1 params. Classic 1004s: `generate_parts=true` with texture/pbr on, `ref_image` on segment v1, `quad=true` on P1, non-GLB input to smartsegment `model` / rig-check |
|
|
7
|
+
| 2 | 1003/1004 | malformed request / invalid parameter | check spelling against `tripo docs --topic commands/<cmd>`; the CLI already strips illegal P1 params. Classic 1004s: `generate_parts=true` with texture/pbr on, `ref_image` on segment v1, `quad=true` on P1, non-GLB input to smartsegment `model` / rig-check. `invalid model`: the API only accepts dated wire values (`P2-20260801`, `v3.1-20260211`…) — pass the CLI alias (`--model tripo-p2`) and let the CLI normalize, or the wire value itself; never `tripo-*` straight to the API. Table: `commands/generate` § "Models & Versions" |
|
|
8
8
|
| 9 | 1007 | per-API-key rate limit | wait and retry with backoff; honour `Retry-After` / `X-RateLimit-Reset` headers |
|
|
9
9
|
| 9 | 2000 | per-account concurrency pool full (see table below) | wait for a task in that category to finish, or lower `--concurrency` in batch; other categories are unaffected |
|
|
10
10
|
| 8 | 2001 | task not found | task ids are account-scoped — same key that created it? right region (ov/cn)? |
|
|
@@ -45,6 +45,8 @@ Pools are independent: a full H-series pool does not block P-series or image tas
|
|
|
45
45
|
- multiview without a front view → rejected (name a file "front" or pass it first)
|
|
46
46
|
- `-n` with `--then` needs an interactive terminal (pick the winner first, then chain)
|
|
47
47
|
|
|
48
|
+
All of these surface in `tripo make ... --dry-run --json` as `errors` (blocking) or `warnings` (auto-adjusted) before anything is uploaded or submitted.
|
|
49
|
+
|
|
48
50
|
## Weird-but-normal
|
|
49
51
|
|
|
50
52
|
- `model_url` in task output points to a file that may be named `pbr_model` etc. — normal, just download it (the CLI does).
|