tripo-cli 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/ai/experts.js +2 -2
- package/dist/ai/plan-card.js +84 -8
- package/dist/commands/completion.js +2 -1
- package/dist/commands/make.js +210 -10
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/process-groups.js +19 -1
- package/dist/commands/shared.js +23 -1
- package/dist/core/requests.js +8 -1
- package/dist/core/task-service.js +12 -2
- package/dist/knowledge/chains.js +37 -15
- package/dist/knowledge/models.js +16 -3
- package/dist/knowledge/params.js +84 -8
- package/dist/pipeline/make.js +25 -18
- package/package.json +1 -1
- package/skill/SKILL.md +6 -4
- package/skill/commands/generate.md +109 -12
- package/skill/commands/make.md +69 -4
- package/skill/commands/process.md +68 -10
- package/skill/common-errors.md +23 -2
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`)
|
|
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,7 @@ 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 docs [--topic t]` | print the bundled agent/skill docs |
|
|
43
|
+
| `tripo docs [--topic t]` | print the bundled agent/skill docs (`--topic commands/generate` has the model versions / P series table) |
|
|
43
44
|
| `tripo mcp` | run as an MCP server (Cursor / Claude Desktop) |
|
|
44
45
|
| `tripo completion <shell>` | bash/zsh/fish completion |
|
|
45
46
|
|
|
@@ -88,6 +89,7 @@ tripo batch run assets.yaml --concurrency 2
|
|
|
88
89
|
## For AI coding agents
|
|
89
90
|
|
|
90
91
|
- `make`/`watch` are blocking: run them, wait for exit, read the single JSON line on stdout. Do not poll yourself.
|
|
92
|
+
- 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
93
|
- 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
94
|
- `preview.png` in every output directory is your eyes: look at it, then decide to `tripo redo` or continue the chain.
|
|
93
95
|
- Full behavior rules: `tripo docs --llm`, per-command docs via `tripo docs --topic commands/make`.
|
package/dist/ai/experts.js
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
* result is re-validated locally before anything is spent.
|
|
8
8
|
*/
|
|
9
9
|
import { getLocale, t, tList } from '../i18n.js';
|
|
10
|
-
import { parseThenExpression, validateChain
|
|
10
|
+
import { availableSteps, parseThenExpression, validateChain } from '../knowledge/chains.js';
|
|
11
11
|
import { chooseModel, detectLowPolyIntent, RIG_MODEL_V25 } from '../knowledge/models.js';
|
|
12
12
|
import { getScenario, SCENARIOS } from '../knowledge/scenarios.js';
|
|
13
13
|
import { chatJson } from './llm.js';
|
|
14
14
|
const SCENARIO_TABLE = SCENARIOS.map((s) => `- ${s.id}: ${s.description.en} | model=${s.model} | chain=${s.chain.map((c) => c.step).join('→') || 'none'} | deliver=${s.deliverables.join('/')}`).join('\n');
|
|
15
|
-
const STEP_TABLE =
|
|
15
|
+
const STEP_TABLE = availableSteps().map((s) => s.name).join(', ');
|
|
16
16
|
const PRODUCER_SYSTEM = `You are the producer/router of the Tripo 3D generation CLI.
|
|
17
17
|
Classify the user's request into exactly one scenario and extract a clean English 3D generation prompt.
|
|
18
18
|
|
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(', ')}`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** `tripo completion <shell>` — static shell completion scripts. */
|
|
2
2
|
import { UsageError } from '../core/errors.js';
|
|
3
|
+
import { isMeshEditEnabled } from '../knowledge/chains.js';
|
|
3
4
|
const COMMANDS = [
|
|
4
5
|
'login', 'logout', 'whoami', 'use', 'balance', 'usage', 'topup',
|
|
5
6
|
'make', 'view', 'redo', 'ai',
|
|
@@ -10,7 +11,7 @@ const SUBS = {
|
|
|
10
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'],
|
|
11
12
|
model: ['refine', 'texture', 'stylize', 'convert', 'import'],
|
|
12
13
|
anim: ['check', 'rig', 'retarget'],
|
|
13
|
-
mesh: ['segment', 'complete', 'decimate', 'smartsegment'],
|
|
14
|
+
mesh: ['segment', 'complete', 'decimate', ...(isMeshEditEnabled() ? ['edit'] : []), 'smartsegment'],
|
|
14
15
|
task: ['get', 'list', 'watch'],
|
|
15
16
|
files: ['upload'],
|
|
16
17
|
batch: ['run'],
|
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
|
|
@@ -18,17 +24,40 @@ export function registerMakeCommand(program) {
|
|
|
18
24
|
.argument('<input...>', 'prompt text, image file(s), model file, URL, task id, or @last')
|
|
19
25
|
.option('--for <scenario>', `scenario preset: ${scenarioIds().join('|')}`)
|
|
20
26
|
.option('--then <steps>', 'processing chain, e.g. "texture,rig,convert:fbx"')
|
|
21
|
-
.option('--model <model>', 'force model version (tripo-v3.1 / tripo-p1 / ...)')
|
|
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/mcp.js
CHANGED
|
@@ -26,7 +26,7 @@ const TOOLS = [
|
|
|
26
26
|
input: { type: 'string', description: 'prompt text, image file path, image URL, or task id' },
|
|
27
27
|
scenario: { type: 'string', enum: scenarioIds(), description: 'production scenario preset' },
|
|
28
28
|
then: { type: 'string', description: 'processing chain, e.g. "texture,rig,convert:fbx"' },
|
|
29
|
-
model: { type: 'string', description: 'force model: tripo-v3.1 or tripo-
|
|
29
|
+
model: { type: 'string', description: 'force model: tripo-v3.1, tripo-p1 or tripo-p2 (P-series preview with quad)' },
|
|
30
30
|
output_dir: { type: 'string', description: 'directory for artifacts' },
|
|
31
31
|
},
|
|
32
32
|
required: ['input'],
|
|
@@ -9,7 +9,7 @@ import { log, warn } from '../core/output.js';
|
|
|
9
9
|
import { buildStepRequest } from '../core/requests.js';
|
|
10
10
|
import { resolveInputValue, runTask } from '../core/task-service.js';
|
|
11
11
|
import { t } from '../i18n.js';
|
|
12
|
-
import { getStep } from '../knowledge/chains.js';
|
|
12
|
+
import { getStep, isMeshEditEnabled } from '../knowledge/chains.js';
|
|
13
13
|
import { readPipedTask } from '../pipeline/stdin.js';
|
|
14
14
|
import { newClient, printTaskResult, withTaskOptions } from './shared.js';
|
|
15
15
|
async function resolveStepInput(client, raw) {
|
|
@@ -84,6 +84,8 @@ function collectStepArgs(step, flags) {
|
|
|
84
84
|
return pick(['completion_mode']);
|
|
85
85
|
case 'smartsegment':
|
|
86
86
|
return pick(['seg_type', 'granularity', 'hint']);
|
|
87
|
+
case 'edit':
|
|
88
|
+
return pick(['bbox', 'face_limit', 'ref_image', 'model']);
|
|
87
89
|
default:
|
|
88
90
|
return {};
|
|
89
91
|
}
|
|
@@ -157,6 +159,22 @@ export function registerProcessCommands(program) {
|
|
|
157
159
|
.option('--face-limit <n>', 'target faces 500–20000 tri / 500–10000 quad', (v) => Number(v))
|
|
158
160
|
.option('--quad', 'quad output')
|
|
159
161
|
.option('--no-bake', 'skip baking textures onto the low-poly')).action(makeStepAction('decimate'));
|
|
162
|
+
// Hidden until /v3/mesh/edit is live server-side (see isMeshEditEnabled).
|
|
163
|
+
if (isMeshEditEnabled()) {
|
|
164
|
+
withTaskOptions(mesh
|
|
165
|
+
.command('edit [input]')
|
|
166
|
+
.description('local retry/refine inside a bounding box (P2 pipeline, model P2-20260801)')
|
|
167
|
+
.option('--bbox <x1,y1,z1,x2,y2,z2>', 'required 2×3 axis-aligned bbox in model space')
|
|
168
|
+
.option('--face-limit <n>', 'output face limit (≥100)', (v) => Number(v))
|
|
169
|
+
.option('--ref-image <file-or-url>', 'reference image for semantic guidance')
|
|
170
|
+
.option('--model <v>', 'P2-20260801 (only supported value)')).action(async (input, flags) => {
|
|
171
|
+
// --ref-image may be a local file: upload it to a file_token first.
|
|
172
|
+
if (typeof flags.refImage === 'string') {
|
|
173
|
+
flags.refImage = (await resolveInputValue(newClient(), flags.refImage)).value;
|
|
174
|
+
}
|
|
175
|
+
await makeStepAction('edit')(input, flags);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
160
178
|
withTaskOptions(mesh
|
|
161
179
|
.command('smartsegment [input]')
|
|
162
180
|
.description('smart segmentation (image or model pipeline)')
|
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) {
|
package/dist/core/requests.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `parameters`), so new API options work without a CLI release.
|
|
6
6
|
*/
|
|
7
7
|
import { getStep } from '../knowledge/chains.js';
|
|
8
|
-
import { validateConvertParams, validateDecimateParams, validateGenerationParams, validateRetargetParams, } from '../knowledge/params.js';
|
|
8
|
+
import { validateConvertParams, validateDecimateParams, validateGenerationParams, validateMeshEditParams, validateRetargetParams, } from '../knowledge/params.js';
|
|
9
9
|
import { CAPABILITY_DEFAULTS, SEGMENT_MODELS, TEXTURE_MODELS, normalizeModel, normalizeRigModel, } from '../knowledge/models.js';
|
|
10
10
|
import { UsageError } from './errors.js';
|
|
11
11
|
function ensure(result) {
|
|
@@ -135,6 +135,13 @@ export function buildStepRequest(stepName, input, args) {
|
|
|
135
135
|
warnings.push(...v.warnings);
|
|
136
136
|
break;
|
|
137
137
|
}
|
|
138
|
+
case 'edit': {
|
|
139
|
+
const v = validateMeshEditParams(params);
|
|
140
|
+
ensure(v);
|
|
141
|
+
params = v.params;
|
|
142
|
+
warnings.push(...v.warnings);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
138
145
|
case 'rig': {
|
|
139
146
|
// Marketing names (rig-v1.0/rig-v2.0) are rejected by the server; normalize to wire versions.
|
|
140
147
|
params.model = params.model === undefined
|
|
@@ -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
|
}
|