tripo-cli 0.7.3 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +10 -7
  3. package/dist/ai/experts.js +25 -19
  4. package/dist/ai/plan-card.js +71 -33
  5. package/dist/ai/wizard.js +13 -17
  6. package/dist/commands/account.js +2 -6
  7. package/dist/commands/ai.js +18 -16
  8. package/dist/commands/batch.js +58 -18
  9. package/dist/commands/completion.js +1 -1
  10. package/dist/commands/config-cmd.js +1 -1
  11. package/dist/commands/doctor.js +1 -3
  12. package/dist/commands/generate.js +35 -4
  13. package/dist/commands/home.js +12 -13
  14. package/dist/commands/login.js +72 -187
  15. package/dist/commands/make.js +47 -25
  16. package/dist/commands/mcp.js +2 -1
  17. package/dist/commands/models.js +47 -0
  18. package/dist/commands/process-groups.js +3 -4
  19. package/dist/commands/shared.js +9 -14
  20. package/dist/commands/task.js +10 -11
  21. package/dist/commands/topup.js +6 -10
  22. package/dist/commands/update.js +6 -10
  23. package/dist/commands/view.js +3 -4
  24. package/dist/config/config.js +1 -1
  25. package/dist/core/client.js +4 -13
  26. package/dist/core/errors.js +16 -11
  27. package/dist/core/output.js +0 -4
  28. package/dist/core/requests.js +61 -17
  29. package/dist/core/task-service.js +19 -20
  30. package/dist/knowledge/chains.js +12 -12
  31. package/dist/knowledge/error-catalog.js +32 -32
  32. package/dist/knowledge/models.js +119 -20
  33. package/dist/knowledge/params.js +96 -8
  34. package/dist/knowledge/scenarios.js +32 -55
  35. package/dist/pipeline/make.js +36 -28
  36. package/dist/pipeline/runner.js +7 -12
  37. package/dist/pipeline/stdin.js +40 -3
  38. package/dist/program.js +6 -11
  39. package/package.json +3 -2
  40. package/skill/SKILL.md +35 -8
  41. package/skill/commands/account.md +1 -1
  42. package/skill/commands/batch.md +13 -3
  43. package/skill/commands/generate.md +35 -10
  44. package/skill/commands/make.md +39 -13
  45. package/skill/commands/process.md +2 -1
  46. package/skill/commands/task.md +2 -2
  47. package/skill/commands/view.md +1 -1
  48. package/skill/common-errors.md +6 -3
  49. package/skill/examples/animation.md +3 -1
  50. package/skill/examples/ar-web.md +6 -4
  51. package/skill/examples/game-asset.md +17 -6
  52. package/skill/examples/pipes.md +2 -2
  53. package/skill/examples/print.md +7 -3
  54. package/dist/i18n.js +0 -19
package/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ ## 0.8.1
4
+
5
+ - Add `tripo models`: lists the model ids each capability accepts (3D generation, text-to-image, image-to-image, texture, rig, segment, decimate) with defaults and CLI aliases. It reads the local whitelist, makes no API call and is excluded from analytics like `docs`/`config`. `--json` prints one JSON line.
6
+
7
+ ## 0.8.0
8
+
9
+ - Preflight every deterministic `make` chain request before uploads or generation. Normal runs and dry runs use the same validation; direct `executeMake` validates before balance/generation calls. Runtime source-dependent checks remain in place.
10
+ - Normalize `H3.1`, `P1.0`, `P2.0` (also `P2.0 Preview`) and legacy `H3.0` / `H2.5` display aliases case-insensitively, alongside existing `tripo-*` names. Reject unknown 3D models; require finite integer generation/convert/decimate face limits; enforce rig version/body/spec and texture-quality compatibility.
11
+ - Validate text-to-image and image-to-image model whitelists, quality, background/format linkage and documented size constraints. Reject `seedream_v4` on image editing and retired `chat_image_1` on both endpoints. Keep `chat_image_1.5` active (announced retirement: 2026-12-01). Never silently switch models or resubmit.
12
+ - Image editing validates deterministic parameters before local uploads. Unknown extra parameter keys remain passthrough; they are not claimed as validated.
13
+ - Derive plan-card details and deliverables from normalized requests, include adjustment warnings, and retain scenario tips only when their assumptions still hold. Explicit model/parameter overrides no longer inherit misleading P2, quad-output or HD-cost claims. P2 is no longer labelled Preview in plan cards, scenario notes or bundled docs. Remove fixed-credit quotes from retopology model-selection warnings.
14
+ - Estimate the generation step locally from official list prices (identical on ov and cn): H3.1 10–40 credits for text and 20–50 for image/multiview by texture tier, plus `geometry_quality=detailed` +20, `quad` +5, `smart_low_poly` +10, `generate_parts` +20; P2 100–130 for every input. Plan cards show it (× `-n` candidates) and dry-run JSON adds `generation_credits_estimate`. P1, legacy models, imports and chain steps stay "not estimated" (`null`), never zero. P2 quad notes no longer claim a higher price tier.
15
+ - `texture=false` without an explicit `pbr` now also sends `pbr=false` (with a warning): `pbr` defaults to true server-side and forces texturing, so the bare-geometry request and its estimate stay consistent.
16
+ - Treat H3.1 as the official default model (`official default → v3.1`). Face budgets below P2's minimum (48) now select H3.1, like budgets above P2's maximum. Narrow real-time prompt hints: board/card/tabletop games, gaming PCs, consoles and animation styles (`animation-style`, `动画风格`, `动画片`) no longer select P2.
17
+ - Complete static preflight on standalone image/multiview-to-model and headless multi-candidate chains before local uploads. Reject deterministic rig/preset-family conflicts before generation, including direct executeMake/runChain callers; do not guess a standalone retarget source's rig version.
18
+ - Validate the effective 3D text prompt's 1024-character limit and image-editing reference limits (Seedream 4, Banana 10, Chat Image 16) before submission/upload.
19
+ - Do not inject a biped-only spec into the built-in anim scenario; a quadruped rig-check recommendation can proceed. Explicit unsupported specs are still rejected locally rather than silently dropped.
20
+ - Handle explicit Chinese/English dense-geometry negation per mention, while retaining separate positive requirements. This is a lexical safeguard, not full language understanding; use an explicit model for ambiguous intent.
21
+ - Preserve existing P-series unsupported-field removal with warnings; normalized payloads, not the original unsupported combination, are what local validation approves.
22
+ - Bundled skill: H3.1 is described as the official default; presets outrank prompt hints and list their models; agents relay the P2 estimate and get confirmation before running it, and ask before `tripo redo` (a new paid task with no plan card or confirmation). P1 is no longer described as fast. The `batch` docs add a Cost section: every job is a full `make` with no plan card or confirmation, so agents estimate the run from a dry run per preset and get confirmation first.
23
+ - All CLI output (logs, plan cards, error messages and suggestions, interactive prompts, help text) is English only. The `language` config key and `LANG`/`LC_ALL`-based switching are removed; Chinese prompts are still understood.
24
+ - `batch run` never resubmits a job once a paid task may exist. It retries only when the failed attempt left no task behind (it failed before submission, or its only task failed and was refunded). Otherwise the job stops as `needs_attention` with `tasks`, `error` and `suggestion` in the state file, is skipped on re-run, and counts toward exit 6 (the summary adds `needs_attention`).
25
+ - Strict pipes: when stdin is not a terminal, process commands without an explicit input never fall back to `@last`. Empty stdin or no `task_id` exits 2; an upstream error JSON exits with the upstream's code; nothing is submitted in either case.
26
+ - A task that succeeds but whose artifact download fails now exits 7 instead of reporting success; the error JSON carries `task_id` and suggests `tripo task get <id> --download`. `make -n` keeps such candidates with `download_error` (exit 7). Error JSON for failed or timed-out tasks also carries `task_id`.
27
+ - `task watch --json` writes progress to stderr and stdout carries only the final JSON line; the NDJSON progress events on stdout are removed. Docs now state the contract as "every result command prints exactly one JSON line" (`docs`/`completion` print text, `mcp` speaks JSON-RPC).
28
+ - The bundled `SKILL.md` starts with YAML frontmatter (`name: tripo-cli`, `description`) so skill loaders can discover it.
29
+ - Fix the color 3D-print recipe: `--for print -p texture=true --then convert:format=OBJ,export_vertex_colors=true`. API 3MF output is single color, so build a multi-color 3MF in the slicer; the print preset no longer lists "3MF (colored)" as a deliverable.
30
+ - `--for ar-web` generates P2 with `face_limit=20000` instead of a follow-up decimate step (saves the 30-credit decimate) and downloads the generated GLB as well as the USDZ. The result adds `source_output_dir` / `source_model_file`, and the plan card lists both deliverables. `--then` replaces the preset chain and keeps the final-step-only download.
31
+ - Update bundled skill and capability documentation and add unit/mock CLI preflight regressions. No real generation, billing or credentials are required by these tests.
32
+
33
+ Known precision boundary: official Seedream size documentation gives rounded MP ranges. This release validates syntax/tiers/aspect ratio, not an invented exact pixel-count cutoff; Banana Pro undocumented pixel limits and arbitrary passthrough fields remain server-validated. Mesh edit stays experimental and disabled by default.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # tripo-cli
2
2
 
3
+ 0.8.0 validates all statically known `make` chain requests before uploads or generation, including normal runs, dry runs and direct `executeMake` calls. `H3.1` / `P1.0` / `P2.0` (plus legacy `H3.0` / `H2.5`, case-insensitive) normalize to dated wire IDs and share the same limits as `tripo-*` aliases; unknown 3D/image models are rejected. Image generation validates endpoint-specific models, quality, background, output format and documented size rules before image-edit uploads. `chat_image_1` is retired and rejected; `chat_image_1.5` remains supported until its announced 2026-12-01 retirement. Plans show scenario notes and the official list-price estimate for the generation step; P1, legacy models and chain steps are marked **not estimated**, never an invented zero-credit quote. See [CHANGELOG](CHANGELOG.md). Unknown extra parameter keys remain passthrough, not a guarantee of server validity.
4
+
3
5
  Turn a sentence or an image into an engine-ready 3D asset from your terminal, powered by the [Tripo](https://www.tripo3d.ai) V3 API.
4
6
 
5
7
  ```bash
@@ -20,9 +22,9 @@ Works with both Tripo regions from the same install — international (`openapi.
20
22
  ## Why this CLI
21
23
 
22
24
  - **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 **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.
25
+ - **A 3D domain brain, not a thin API wrapper.** Explicit `--model` always wins. `tripo-v3.1` (H3.1) is the official default flagship model, and dense geometry, `smart_low_poly=true`, `generate_parts=true`, any explicit `geometry_quality`, or a face budget outside P2's range select it first; a `--for` preset comes next and outranks prompt hints. Otherwise real-time, game, editable, low-poly, voxel, quad, or a face budget within P2's range select `tripo-p2`. `tripo-p1` is an explicit lower-cost option. P2 costs 100–130 credits per generation versus 10–50 for H3.1 before add-ons; the plan card shows the estimate before you run. These 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
26
  - **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.
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`).
27
+ - **Agent-native.** Non-TTY runs are automatically headless (`--json --yes --no-open`), every result command prints exactly one JSON line on stdout (`docs`/`completion` print text, `mcp` speaks JSON-RPC), progress goes to stderr, exit codes are stable, and JSON 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`).
26
28
 
27
29
  ## Commands
28
30
 
@@ -34,9 +36,10 @@ Works with both Tripo regions from the same install — international (`openapi.
34
36
  | `tripo redo [task]` | re-run with a fresh seed |
35
37
  | `tripo login / logout / whoami / use` | device flow: browser approves, key arrives automatically (paste fallback); stored 0600 in `~/.tripo/config.json`; `use` switches accounts |
36
38
  | `tripo topup / balance / usage` | open the billing page + detect the credits arriving; check spend |
39
+ | `tripo models` | list the model ids each capability accepts, with defaults and aliases (local whitelist, no API call) |
37
40
  | `tripo generate <endpoint>` | deterministic access to all 8 generation endpoints |
38
41
  | `tripo model / anim / mesh <step>` | texture, convert, import, rig-check, rig, retarget, segment, complete, decimate, uv, smartsegment |
39
- | `tripo task get/list/watch` | query and stream task progress (NDJSON in `--json` mode) |
42
+ | `tripo task get/list/watch` | query tasks and wait for them (progress on stderr) |
40
43
  | `tripo files upload` | upload a file, print the `file_token` |
41
44
  | `tripo batch run <manifest.yaml>` | bulk pipelines with concurrency, retries, and resume |
42
45
  | `tripo config / doctor` | settings (global + per-directory `.tripo/context.json`) and self-diagnosis |
@@ -65,10 +68,10 @@ Each profile carries its own key + region, so switching accounts switches region
65
68
  ## Scenario presets
66
69
 
67
70
  ```bash
68
- tripo make "sci-fi crate" --for game-mobile # P1 low-poly → texture → FBX
71
+ tripo make "sci-fi crate" --for game-mobile # P2 editable topology → texture → FBX
69
72
  tripo make hero.png --for film # v3.1, quad topology, 4K PBR → USDZ/OBJ
70
73
  tripo make "chess knight" --for print # no textures, watertight → STL, flat bottom
71
- tripo make sofa.jpg --for ar-web # decimate to mobile budget → GLB + USDZ
74
+ tripo make sofa.jpg --for ar-web # P2 capped at 20k faces → GLB + USDZ
72
75
  tripo make "orc warrior" --for anim # rig-check gate → rig → FBX
73
76
  ```
74
77
 
@@ -91,8 +94,8 @@ tripo batch run assets.yaml --concurrency 2
91
94
 
92
95
  - `make`/`watch` are blocking: run them, wait for exit, read the single JSON line on stdout. Do not poll yourself.
93
96
  - The plan card on stderr lists every call before it runs; the final JSON carries `credits_consumed` (total for the run) plus `credits_breakdown` (per task) when a chain ran.
94
- - Exit codes: `0` ok · `2` usage · `3` auth · `4` insufficient credits · `5` content policy · `6` task failed (failed/cancelled tasks are auto-refunded; a `timeout` means it is still running) · `7` network (a failed submit is never resent — check `tripo balance` before re-running) · `8` not found · `9` rate limit.
95
- - `preview.png` in every output directory is your eyes: look at it, then decide to `tripo redo` or continue the chain.
97
+ - Exit codes: `0` ok · `2` usage · `3` auth · `4` insufficient credits · `5` content policy · `6` task failed (failed/cancelled tasks are auto-refunded; a `timeout` means it is still running) · `7` network (a failed submit is never resent — check `tripo balance` before re-running; a task that succeeded but failed to download carries `task_id` in the error JSON — run `tripo task get <task_id> --download`, never re-make) · `8` not found · `9` rate limit.
98
+ - `preview.png` in every output directory is your eyes: look at it, then continue the chain or ask the human before `tripo redo` (a new paid task with no plan card or confirmation).
96
99
  - Full behavior rules: `tripo docs --llm`, per-command docs via `tripo docs --topic commands/make`.
97
100
 
98
101
  ## Environment variables
@@ -6,12 +6,11 @@
6
6
  * knowledge base (scenario ids, step names, parameter windows), and the
7
7
  * result is re-validated locally before anything is spent.
8
8
  */
9
- import { getLocale, t, tList } from '../i18n.js';
10
9
  import { availableSteps, parseThenExpression, validateChain } from '../knowledge/chains.js';
11
- import { CAPABILITY_DEFAULTS, chooseModel, detectLowPolyIntent } from '../knowledge/models.js';
10
+ import { CAPABILITY_DEFAULTS, chooseModel, detectDenseGeometryIntent, detectLowPolyIntent, detectRealtimeIntent } from '../knowledge/models.js';
12
11
  import { getScenario, SCENARIOS } from '../knowledge/scenarios.js';
13
12
  import { chatJson } from './llm.js';
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');
13
+ const SCENARIO_TABLE = SCENARIOS.map((s) => `- ${s.id}: ${s.description} | model=${s.model} | chain=${s.chain.map((c) => c.step).join('→') || 'none'} | deliver=${s.deliverables.join('/')}`).join('\n');
15
14
  const STEP_TABLE = availableSteps().map((s) => s.name).join(', ');
16
15
  const PRODUCER_SYSTEM = `You are the producer/router of the Tripo 3D generation CLI.
17
16
  Classify the user's request into exactly one scenario and extract a clean English 3D generation prompt.
@@ -23,30 +22,30 @@ Rules:
23
22
  - "prompt" must describe the object to generate (translate to English, keep it concrete, <= 200 chars).
24
23
  - Set "face_limit" only when the user states a face/polygon budget.
25
24
  - Set "needs_animation" true only when the user wants skeleton/animations.
26
- - If one critical piece of information is missing and materially changes the plan, set "question" (one short question in the user's language). Otherwise omit it.
25
+ - If one critical piece of information is missing and materially changes the plan, set "question" (one short question in English). Otherwise omit it.
27
26
  Respond with JSON only: {"scenario","prompt","face_limit","needs_animation","question","notes"}`;
28
27
  const SPECIALIST_PROMPTS = {
29
- 'game-mobile': `You are a senior game technical artist (mobile). Budgets: 500–20000 faces, 1–2K textures, clean topology, FBX/GLB delivery. P1 model is used (no quad/parts/geometry_quality).`,
30
- 'game-pc': `You are a senior game technical artist (PC/console). PBR detailed textures, optional decimate for LODs (bake=true), FBX delivery with engine presets.`,
28
+ 'game-mobile': `You are a senior game technical artist (mobile). Prefer P2 editable topology for 48–25000 quad or 48–50000 triangle faces, 1–2K textures, and FBX/GLB delivery. Call out P2's higher credit cost.`,
29
+ 'game-pc': `You are a senior game technical artist (PC/console). Prefer P2 editable topology with detailed PBR textures, optional decimate for LODs (bake=true), and FBX delivery with engine presets. Use H only when dense geometry or H-only parameters are required. Call out P2's higher credit cost.`,
31
30
  film: `You are a film/offline-rendering artist. quad=true subdivision-friendly topology (FBX output), geometry_quality=detailed, texture_quality=detailed, 4K textures, USD/OBJ handoff via convert.`,
32
- print: `You are a 3D-printing engineer. Geometry only (texture=false,pbr=false), watertight meshes, flatten_bottom for FDM, STL or colored 3MF via convert, real-world sizing via auto_size at generation or scale_factor at convert.`,
33
- 'ar-web': `You are a realtime AR/Web 3D engineer. Keep faces <= 20000 via decimate, 2K textures, deliver GLB + USDZ (iOS QuickLook).`,
34
- anim: `You are a character animation TD. Chain: rig-check → rig (v3.0 rig, spec tripo|mixamo|actorcore|unity|unreal|vrm, biped only) → retarget (<=5 preset animations, billed per animation). Mixamo → spec=mixamo; Unity Humanoid → spec=unity; Unreal → spec=unreal.`,
35
- toy: `You are a stylized collectible designer. P1 low-poly. STL via convert for physical prints.`,
31
+ print: `You are a 3D-printing engineer. Geometry only (texture=false,pbr=false), watertight meshes, flatten_bottom for FDM, STL via convert (API 3MF output is single color; for color prints set texture=true and convert to OBJ with export_vertex_colors=true), real-world sizing via auto_size at generation or scale_factor at convert.`,
32
+ 'ar-web': `You are a realtime AR/Web 3D engineer. Prefer P2 editable topology, keep faces <= 20000 with face_limit at generation (no decimate step), use 2K textures, and deliver GLB + USDZ (iOS QuickLook). Call out P2's higher credit cost.`,
33
+ anim: `You are a character animation TD. Prefer P2 editable topology. Chain: rig-check → rig (v3.0 rig, spec tripo|mixamo|actorcore|unity|unreal|vrm, biped only) → retarget (<=5 preset animations, billed per animation). Mixamo → spec=mixamo; Unity Humanoid → spec=unity; Unreal → spec=unreal. Call out P2's higher credit cost.`,
34
+ toy: `You are a stylized collectible designer. Prefer P2 editable low-poly topology; use STL via convert for physical prints. Call out P2's higher credit cost.`,
36
35
  };
37
36
  function specialistSystem(scenarioId) {
38
37
  const persona = SPECIALIST_PROMPTS[scenarioId] ?? SPECIALIST_PROMPTS['game-mobile'];
39
38
  return `${persona}
40
39
  You fine-tune a Tripo generation plan. You may adjust generation params and the processing chain.
41
40
  Valid chain steps: ${STEP_TABLE}. Chain syntax: "step:key=value,step2" (e.g. "texture,rig,convert:format=FBX").
42
- Constraints: pbr=>texture; generate_parts excludes texture/pbr/quad; P1 face_limit 50-20000 and forbids quad/smart_low_poly/generate_parts/geometry_quality; retarget <= 5 animations.
43
- Respond with JSON only: {"params":{...},"chain":"...","notes":["short practical notes in ${getLocale() === 'zh' ? 'Chinese' : 'English'}"]}. Omit fields you don't want to change.`;
41
+ Constraints: pbr=>texture; generate_parts excludes texture/pbr/quad; P2 face_limit is 48-50000 triangles or 48-25000 quads and forbids smart_low_poly/generate_parts/geometry_quality; retarget <= 5 animations.
42
+ Respond with JSON only: {"params":{...},"chain":"...","notes":["short practical notes in English"]}. Omit fields you don't want to change.`;
44
43
  }
45
44
  /**
46
45
  * Run the expert pipeline. `askUser` is called when the producer needs one
47
46
  * clarification (interactive mode only); return undefined to skip.
48
47
  */
49
- export async function buildExpertPlan(request, askUser) {
48
+ export async function buildExpertPlan(request, askUser, overrides = {}) {
50
49
  const messages = [
51
50
  { role: 'system', content: PRODUCER_SYSTEM },
52
51
  { role: 'user', content: request },
@@ -69,9 +68,12 @@ export async function buildExpertPlan(request, askUser) {
69
68
  },
70
69
  ]).catch(() => ({}));
71
70
  // Merge + re-validate everything the LLM produced.
72
- const params = { ...scenario.genParams, ...(specialist.params ?? {}) };
73
- if (producer.face_limit)
74
- params.face_limit = producer.face_limit;
71
+ const params = {
72
+ ...scenario.genParams,
73
+ ...(specialist.params ?? {}),
74
+ ...(producer.face_limit === undefined ? {} : { face_limit: producer.face_limit }),
75
+ ...overrides,
76
+ };
75
77
  let chain = scenario.chain;
76
78
  if (specialist.chain) {
77
79
  try {
@@ -90,9 +92,13 @@ export async function buildExpertPlan(request, askUser) {
90
92
  ];
91
93
  }
92
94
  const choice = chooseModel({
93
- explicit: scenario.model,
95
+ preferred: scenario.model,
94
96
  faceLimit: typeof params.face_limit === 'number' ? params.face_limit : undefined,
97
+ quad: params.quad === true,
95
98
  lowPoly: detectLowPolyIntent(request),
99
+ realtime: detectRealtimeIntent(request),
100
+ denseGeometry: detectDenseGeometryIntent(request),
101
+ params,
96
102
  });
97
103
  try {
98
104
  validateChain(chain, 'text_to_model');
@@ -104,11 +110,11 @@ export async function buildExpertPlan(request, askUser) {
104
110
  inputSummary: producer.prompt,
105
111
  scenario: scenario.id,
106
112
  model: choice.model,
107
- modelReason: t({ zh: '制片人路由 + 场景预设', en: 'producer routing + scenario preset' }),
113
+ modelReason: 'producer routing + scenario preset',
108
114
  genParams: params,
109
115
  chain,
110
116
  deliverables: scenario.deliverables,
111
- notes: tList(scenario.notes),
117
+ notes: scenario.notes,
112
118
  expertNotes: [...(producer.notes ?? []), ...(specialist.notes ?? [])],
113
119
  };
114
120
  return { card, scenarioId: scenario.id, prompt: producer.prompt, chain, params };
@@ -4,14 +4,15 @@
4
4
  * conversation mode. Decisions come from the knowledge base either way.
5
5
  */
6
6
  import pc from 'picocolors';
7
- import { t } from '../i18n.js';
7
+ import { estimateGenerationCredits, isP2, normalizeModel } from '../knowledge/models.js';
8
+ import { buildStepRequest } from '../core/requests.js';
8
9
  import { getStep } from '../knowledge/chains.js';
9
10
  import { isComplexConvert } from '../knowledge/params.js';
10
11
  import { getScenario } from '../knowledge/scenarios.js';
11
12
  const GENERATION_DESCRIBE = {
12
- text_to_model: { zh: '文本生成 3D 模型', en: 'generate a 3D model from text' },
13
- image_to_model: { zh: '单图生成 3D 模型', en: 'generate a 3D model from one image' },
14
- multiview_to_model: { zh: '多视图生成 3D 模型', en: 'generate a 3D model from multiple views' },
13
+ text_to_model: 'generate a 3D model from text',
14
+ image_to_model: 'generate a 3D model from one image',
15
+ multiview_to_model: 'generate a 3D model from multiple views',
15
16
  };
16
17
  /** Keys that describe the input itself (already shown on the "input" line). */
17
18
  const INPUT_KEYS = new Set(['prompt', 'input', 'inputs']);
@@ -19,6 +20,8 @@ const INPUT_KEYS = new Set(['prompt', 'input', 'inputs']);
19
20
  export function cardFromMakePlan(plan, opts) {
20
21
  const steps = [];
21
22
  const genParams = {};
23
+ const notes = [...plan.warnings];
24
+ const chain = [];
22
25
  if (plan.generation) {
23
26
  const { taskType, request } = plan.generation;
24
27
  const args = {};
@@ -33,26 +36,46 @@ export function cardFromMakePlan(plan, opts) {
33
36
  steps.push({
34
37
  name: importDef ? importDef.name : taskType.replace(/_/g, '-'),
35
38
  endpoint: request.endpoint,
36
- describe: importDef ? t(importDef.describe) : t(GENERATION_DESCRIBE[taskType] ?? { zh: '生成', en: 'generate' }),
39
+ describe: importDef ? importDef.describe : GENERATION_DESCRIBE[taskType] ?? 'generate',
37
40
  args,
38
41
  });
39
42
  }
40
43
  for (const spec of plan.chain) {
41
44
  const def = getStep(spec.step);
45
+ const built = buildStepRequest(spec.step, '<upstream>', spec.args);
46
+ const args = { ...built.request.payload };
47
+ delete args.input;
48
+ notes.push(...built.warnings);
49
+ chain.push({ step: def?.name ?? spec.step, args });
42
50
  steps.push({
43
51
  name: spec.step,
44
52
  endpoint: def?.endpoint ?? '',
45
- describe: def ? t(def.describe) : '',
46
- args: spec.args,
53
+ describe: def ? def.describe : '',
54
+ args,
47
55
  });
48
56
  }
49
57
  const scenario = opts.scenario ? getScenario(opts.scenario) : undefined;
50
- const notes = [];
58
+ // Static tips are valid only when the scenario's assumptions survived normalization.
59
+ if (scenario && plan.model && normalizeModel(plan.model) === normalizeModel(scenario.model) &&
60
+ Object.entries(scenario.genParams).every(([key, value]) => genParams[key] === value) &&
61
+ JSON.stringify(plan.chain) === JSON.stringify(scenario.chain)) {
62
+ notes.unshift(...scenario.notes);
63
+ }
64
+ // Advertise actual selected outputs, never optional scenario formats or removed quads.
65
+ const generated = plan.generation && Object.hasOwn(GENERATION_DESCRIBE, plan.generation.taskType)
66
+ ? [genParams.quad === true ? 'FBX' : 'GLB'] : [];
67
+ let deliverables = generated;
68
+ for (const spec of chain) {
69
+ if (getStep(spec.step)?.passthrough)
70
+ continue;
71
+ const format = spec.step === 'convert' ? (spec.args.quad === true ? 'FBX' : spec.args.format)
72
+ : spec.step === 'rig' ? spec.args.out_format : undefined;
73
+ deliverables = typeof format === 'string' ? [format.toUpperCase()] : [];
74
+ }
75
+ if (plan.downloadGeneration)
76
+ deliverables = [...new Set([...generated, ...deliverables])];
51
77
  if (opts.candidates && opts.candidates > 1) {
52
- notes.push(t({
53
- zh: `并行生成 ${opts.candidates} 个候选(不同 seed),选中一个后再继续加工链`,
54
- en: `${opts.candidates} candidates in parallel (distinct seeds); pick one before the chain continues`,
55
- }));
78
+ notes.push(`${opts.candidates} candidates in parallel (distinct seeds); pick one before the chain continues`);
56
79
  }
57
80
  // Task refs (@last / @name) resolve to an id the human has not seen yet — show it.
58
81
  let inputSummary = opts.inputs.join(' ');
@@ -64,39 +87,54 @@ export function cardFromMakePlan(plan, opts) {
64
87
  scenario: opts.scenario,
65
88
  model: plan.model,
66
89
  modelReason: plan.modelReason,
90
+ taskType: plan.generation?.taskType,
91
+ candidates: opts.candidates,
67
92
  genParams,
68
- chain: plan.chain,
69
- deliverables: scenario?.deliverables ?? [],
70
- notes,
93
+ chain,
94
+ deliverables,
95
+ notes: [...new Set(notes)],
71
96
  steps,
72
97
  };
73
98
  }
99
+ /** List-price credits for the generation step × candidates; undefined when no official price applies. */
100
+ export function generationCreditsEstimate(card) {
101
+ if (!card.model || !card.taskType)
102
+ return undefined;
103
+ const unit = estimateGenerationCredits(card.model, card.taskType, card.genParams);
104
+ return unit === undefined ? undefined : unit * (card.candidates && card.candidates > 1 ? card.candidates : 1);
105
+ }
74
106
  /** Cost-impact annotations derived from the parameter set. */
75
107
  export function costNotes(card) {
76
- const notes = [];
108
+ const total = generationCreditsEstimate(card);
109
+ const n = card.candidates && card.candidates > 1 ? card.candidates : 1;
110
+ const amount = n > 1 && total !== undefined ? `${n} × ${total / n} = ${total}` : String(total);
111
+ const chainNote = card.chain.length > 0;
112
+ const notes = [total === undefined ? 'cost not estimated (not zero credits); notes below are cost impacts only; check regional official pricing and balance before running' : `estimated generation cost: ${amount} credits (official list price${chainNote ? '; chain steps not estimated' : ''})`];
77
113
  const params = card.genParams;
78
114
  if (params.texture === false) {
79
- notes.push(t({ zh: '白模生成:不消耗贴图积分', en: 'bare geometry: no texture credits' }));
115
+ notes.push('bare geometry: no texture credits');
80
116
  }
81
117
  if (params.texture_quality === 'detailed' || params.texture_quality === 'extreme') {
82
- notes.push(t({ zh: `贴图质量 ${params.texture_quality}:价签更高`, en: `texture_quality ${params.texture_quality}: higher price tier` }));
118
+ notes.push(`texture_quality ${params.texture_quality}: higher price tier`);
83
119
  }
84
120
  if (params.geometry_quality === 'detailed') {
85
- notes.push(t({ zh: 'geometry detailed(Ultra):加收 HD credit', en: 'geometry detailed (Ultra): extra HD credit' }));
121
+ notes.push('geometry detailed (Ultra): extra HD credit');
86
122
  }
87
123
  if (params.quad === true) {
88
- notes.push(t({ zh: 'quad 四边面:价签更高,输出 FBX', en: 'quad topology: higher price tier, FBX output' }));
124
+ notes.push(card.model && isP2(card.model)
125
+ ? 'quad topology: FBX output'
126
+ : 'quad topology: higher price tier, FBX output');
89
127
  }
90
128
  if (params.generate_parts === true) {
91
- notes.push(t({ zh: '分件生成:价签更高,出裸几何', en: 'part generation: higher price tier, bare geometry' }));
129
+ notes.push('part generation: higher price tier, bare geometry');
92
130
  }
93
131
  for (const step of card.chain) {
94
132
  if (step.step === 'convert' && isComplexConvert(step.args)) {
95
- notes.push(t({ zh: 'convert 非默认参数:按 complex 价签计费', en: 'non-default convert options bill as complex convert' }));
133
+ notes.push('non-default convert options bill as complex convert');
96
134
  }
97
135
  if (step.step === 'retarget') {
98
136
  const count = Array.isArray(step.args.animations) ? step.args.animations.length : 1;
99
- notes.push(t({ zh: `retarget ${count} 条动画:按条计费`, en: `retarget ${count} animation(s): billed per animation` }));
137
+ notes.push(`retarget ${count} animation(s): billed per animation`);
100
138
  }
101
139
  }
102
140
  return notes;
@@ -105,18 +143,18 @@ export function costNotes(card) {
105
143
  export function renderPlanCard(card) {
106
144
  const scenario = card.scenario ? getScenario(card.scenario) : undefined;
107
145
  const lines = [];
108
- const title = t({ zh: '方案卡', en: 'Plan' });
146
+ const title = 'Plan';
109
147
  lines.push(pc.bold(`── ${title} ──────────────────────────`));
110
- lines.push(`${t({ zh: '输入', en: 'input' })}: ${card.inputSummary}`);
148
+ lines.push(`${'input'}: ${card.inputSummary}`);
111
149
  if (scenario)
112
- lines.push(`${t({ zh: '场景', en: 'scenario' })}: ${scenario.id} (${t(scenario.label)})`);
150
+ lines.push(`${'scenario'}: ${scenario.id} (${scenario.label})`);
113
151
  if (card.model) {
114
- lines.push(`${t({ zh: '模型', en: 'model' })}: ${card.model}${card.modelReason ? pc.dim(` — ${card.modelReason}`) : ''}`);
152
+ lines.push(`${'model'}: ${card.model}${card.modelReason ? pc.dim(` — ${card.modelReason}`) : ''}`);
115
153
  }
116
154
  const fmtArgs = (args) => Object.entries(args).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(' ');
117
155
  if (card.steps && card.steps.length > 0) {
118
156
  // Numbered step list: what each call does, which endpoint, with which args.
119
- lines.push(`${t({ zh: '步骤', en: 'steps' })}:`);
157
+ lines.push(`${'steps'}:`);
120
158
  card.steps.forEach((step, i) => {
121
159
  lines.push(` ${i + 1}. ${step.name} ${pc.dim(`POST ${step.endpoint}`)}${step.describe ? ` — ${step.describe}` : ''}`);
122
160
  if (Object.keys(step.args).length > 0)
@@ -126,28 +164,28 @@ export function renderPlanCard(card) {
126
164
  else {
127
165
  const params = Object.entries(card.genParams);
128
166
  if (params.length > 0) {
129
- lines.push(`${t({ zh: '参数', en: 'params' })}: ${fmtArgs(card.genParams)}`);
167
+ lines.push(`${'params'}: ${fmtArgs(card.genParams)}`);
130
168
  }
131
169
  if (card.chain.length > 0) {
132
170
  const chainStr = card.chain
133
171
  .map((s) => (Object.keys(s.args).length > 0 ? `${s.step}(${Object.entries(s.args).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(',')})` : s.step))
134
172
  .join(' → ');
135
- lines.push(`${t({ zh: '加工链', en: 'chain' })}: ${chainStr}`);
173
+ lines.push(`${'chain'}: ${chainStr}`);
136
174
  }
137
175
  }
138
176
  if (card.deliverables.length > 0) {
139
- lines.push(`${t({ zh: '交付物', en: 'deliverables' })}: ${card.deliverables.join(', ')}`);
177
+ lines.push(`${'deliverables'}: ${card.deliverables.join(', ')}`);
140
178
  }
141
179
  const cost = costNotes(card);
142
180
  if (cost.length > 0) {
143
- lines.push(`${t({ zh: '成本影响', en: 'cost impact' })}:`);
181
+ lines.push(`${'cost impact'}:`);
144
182
  for (const note of cost)
145
183
  lines.push(` · ${note}`);
146
184
  }
147
185
  for (const note of card.notes)
148
186
  lines.push(pc.dim(` ${note}`));
149
187
  if (card.expertNotes && card.expertNotes.length > 0) {
150
- lines.push(`${t({ zh: '专家意见', en: 'expert notes' })}:`);
188
+ lines.push(`${'expert notes'}:`);
151
189
  for (const note of card.expertNotes)
152
190
  lines.push(` · ${note}`);
153
191
  }
package/dist/ai/wizard.js CHANGED
@@ -6,31 +6,27 @@
6
6
  import * as p from '@clack/prompts';
7
7
  import { CliError, UsageError } from '../core/errors.js';
8
8
  import { isInteractive } from '../core/output.js';
9
- import { t, tList } from '../i18n.js';
10
9
  import { EXIT } from '../knowledge/error-catalog.js';
11
10
  import { CAPABILITY_DEFAULTS } from '../knowledge/models.js';
12
11
  import { getScenario, SCENARIOS } from '../knowledge/scenarios.js';
13
12
  export async function runWizard(initialPrompt) {
14
13
  if (!isInteractive()) {
15
- throw new UsageError(t({
16
- zh: '向导需要交互终端;无头环境请直接用 tripo make --for <场景> 或配置 LLM 后用 tripo ai --yes',
17
- en: 'the wizard needs an interactive terminal; headless callers should use "tripo make --for <scenario>" instead',
18
- }));
14
+ throw new UsageError('the wizard needs an interactive terminal; headless callers should use "tripo make --for <scenario>" instead');
19
15
  }
20
16
  p.intro('tripo ai');
21
17
  let prompt = initialPrompt?.trim() ?? '';
22
18
  if (!prompt) {
23
19
  const answer = await p.text({
24
- message: t({ zh: '想做个什么?一句话描述(或图片路径)', en: 'What do you want to make? one sentence (or an image path)' }),
25
- validate: (v) => (v && v.trim().length > 0 ? undefined : t({ zh: '说点什么吧', en: 'say something' })),
20
+ message: 'What do you want to make? one sentence (or an image path)',
21
+ validate: (v) => (v && v.trim().length > 0 ? undefined : 'say something'),
26
22
  });
27
23
  if (p.isCancel(answer))
28
24
  throw new CliError('cancelled', EXIT.FATAL);
29
25
  prompt = answer.trim();
30
26
  }
31
27
  const scenarioAnswer = await p.select({
32
- message: t({ zh: '用在什么地方?', en: 'What is it for?' }),
33
- options: SCENARIOS.map((s) => ({ value: s.id, label: `${t(s.label)} — ${t(s.description)}` })),
28
+ message: 'What is it for?',
29
+ options: SCENARIOS.map((s) => ({ value: s.id, label: `${s.label} — ${s.description}` })),
34
30
  });
35
31
  if (p.isCancel(scenarioAnswer))
36
32
  throw new CliError('cancelled', EXIT.FATAL);
@@ -38,12 +34,12 @@ export async function runWizard(initialPrompt) {
38
34
  let engine;
39
35
  if (scenarioAnswer === 'game-mobile' || scenarioAnswer === 'game-pc') {
40
36
  const engineAnswer = await p.select({
41
- message: t({ zh: '目标引擎/软件?', en: 'Target engine/software?' }),
37
+ message: 'Target engine/software?',
42
38
  options: [
43
39
  { value: 'unity', label: 'Unity' },
44
40
  { value: 'unreal', label: 'Unreal' },
45
41
  { value: 'blender', label: 'Blender' },
46
- { value: 'any', label: t({ zh: '不确定/通用', en: 'not sure / generic' }) },
42
+ { value: 'any', label: 'not sure / generic' },
47
43
  ],
48
44
  });
49
45
  if (p.isCancel(engineAnswer))
@@ -53,7 +49,7 @@ export async function runWizard(initialPrompt) {
53
49
  let animations = false;
54
50
  if (scenarioAnswer !== 'print' && scenarioAnswer !== 'film') {
55
51
  const animAnswer = await p.confirm({
56
- message: t({ zh: '需要骨骼/动画吗?', en: 'Do you need a skeleton / animations?' }),
52
+ message: 'Do you need a skeleton / animations?',
57
53
  initialValue: scenarioAnswer === 'anim',
58
54
  });
59
55
  if (p.isCancel(animAnswer))
@@ -65,11 +61,11 @@ export async function runWizard(initialPrompt) {
65
61
  inputSummary: prompt,
66
62
  scenario: scenario.id,
67
63
  model: scenario.model,
68
- modelReason: t({ zh: '场景预设', en: 'scenario preset' }),
64
+ modelReason: 'scenario preset',
69
65
  genParams: scenario.genParams,
70
66
  chain,
71
67
  deliverables: scenario.deliverables,
72
- notes: [...tList(scenario.notes), ...notes],
68
+ notes: [...scenario.notes, ...notes],
73
69
  };
74
70
  return { card, options: { prompt, scenario: scenario.id, chain } };
75
71
  }
@@ -82,11 +78,11 @@ export function adjustChain(scenarioId, engine, animations) {
82
78
  const rigArgs = { model: CAPABILITY_DEFAULTS.rig, out_format: 'fbx' };
83
79
  if (engine === 'unity') {
84
80
  rigArgs.spec = 'unity';
85
- notes.push(t({ zh: 'Unity Humanoid → spec=unity', en: 'Unity Humanoid → spec=unity' }));
81
+ notes.push('Unity Humanoid → spec=unity');
86
82
  }
87
83
  else if (engine === 'unreal') {
88
84
  rigArgs.spec = 'unreal';
89
- notes.push(t({ zh: 'Unreal Mannequin → spec=unreal', en: 'Unreal Mannequin → spec=unreal' }));
85
+ notes.push('Unreal Mannequin → spec=unreal');
90
86
  }
91
87
  chain = [
92
88
  ...chain.filter((s) => s.step !== 'convert'),
@@ -94,7 +90,7 @@ export function adjustChain(scenarioId, engine, animations) {
94
90
  { step: 'rig', args: rigArgs },
95
91
  { step: 'retarget', args: { animations: ['preset:idle', 'preset:walk'], out_format: 'fbx' } },
96
92
  ];
97
- notes.push(t({ zh: '动画链输出 FBX(骨骼+动画)', en: 'animation chain outputs FBX (skeleton + animations)' }));
93
+ notes.push('animation chain outputs FBX (skeleton + animations)');
98
94
  }
99
95
  if (engine && !animations) {
100
96
  const convert = chain.find((s) => s.step === 'convert');
@@ -1,17 +1,13 @@
1
1
  /** `tripo balance` / `tripo usage` — account visibility. */
2
2
  import { TripoClient } from '../core/client.js';
3
3
  import { emitResult } from '../core/output.js';
4
- import { t } from '../i18n.js';
5
4
  export function registerAccountCommands(program) {
6
5
  program
7
6
  .command('balance')
8
7
  .description('show available and frozen credits')
9
8
  .action(async () => {
10
9
  const data = await new TripoClient().getBalance();
11
- emitResult(data, t({
12
- zh: `余额 ${data.balance} 积分(冻结 ${data.frozen})`,
13
- en: `balance: ${data.balance} credits (${data.frozen} frozen)`,
14
- }));
10
+ emitResult(data, `balance: ${data.balance} credits (${data.frozen} frozen)`);
15
11
  });
16
12
  program
17
13
  .command('usage')
@@ -22,6 +18,6 @@ export function registerAccountCommands(program) {
22
18
  const human = items
23
19
  .map((i) => `${(i.created_at ?? '').slice(0, 19).padEnd(20)} ${i.type.padEnd(24)} ${String(i.credits_consumed ?? 0).padStart(6)} cr ${i.task_id}`)
24
20
  .join('\n');
25
- emitResult(items, human || t({ zh: '暂无用量记录', en: 'no usage yet' }));
21
+ emitResult(items, human || 'no usage yet');
26
22
  });
27
23
  }
@@ -9,10 +9,10 @@ import * as p from '@clack/prompts';
9
9
  import { resolveReference } from '../config/history.js';
10
10
  import { CliError } from '../core/errors.js';
11
11
  import { isInteractive, isYes, log, warn } from '../core/output.js';
12
- import { t } from '../i18n.js';
13
12
  import { EXIT } from '../knowledge/error-catalog.js';
13
+ import { getScenario } from '../knowledge/scenarios.js';
14
14
  import { executeMake, planMake } from '../pipeline/make.js';
15
- import { renderPlanCard } from '../ai/plan-card.js';
15
+ import { cardFromMakePlan, renderPlanCard } from '../ai/plan-card.js';
16
16
  import { resolveLlmConfig } from '../ai/llm.js';
17
17
  import { buildExpertPlan } from '../ai/experts.js';
18
18
  import { runWizard } from '../ai/wizard.js';
@@ -22,7 +22,7 @@ export function registerAiCommand(program) {
22
22
  const cmd = program
23
23
  .command('ai')
24
24
  .description('describe what you want in natural language; experts plan it, you confirm, it runs')
25
- .argument('[request...]', 'e.g. "给我的手游做一个低模骑士,要跑步动画"')
25
+ .argument('[request...]', 'e.g. "a low-poly knight for my mobile game, with a run animation"')
26
26
  .option('--wizard', 'force wizard mode even when an LLM is configured');
27
27
  withTaskOptions(cmd);
28
28
  cmd.action(async (request, flags) => {
@@ -31,14 +31,15 @@ export function registerAiCommand(program) {
31
31
  let card;
32
32
  let makeOptions;
33
33
  if (llm && !flags.wizard && text) {
34
- log(t({ zh: `对话模式(${llm.model})…`, en: `conversation mode (${llm.model})…` }));
34
+ log(`conversation mode (${llm.model})…`);
35
35
  try {
36
- const plan = await buildExpertPlan(text, isInteractive() ? askUser : undefined);
36
+ const plan = await buildExpertPlan(text, isInteractive() ? askUser : undefined, flags.param);
37
37
  card = plan.card;
38
38
  makeOptions = {
39
39
  inputs: [plan.prompt],
40
40
  scenario: plan.scenarioId,
41
41
  chain: plan.chain,
42
+ model: plan.card.model,
42
43
  params: plan.params,
43
44
  name: flags.name,
44
45
  outDir: flags.out,
@@ -47,10 +48,7 @@ export function registerAiCommand(program) {
47
48
  };
48
49
  }
49
50
  catch (err) {
50
- warn(t({
51
- zh: `LLM 编排失败(${err instanceof Error ? err.message : err}),回落到向导模式`,
52
- en: `LLM planning failed (${err instanceof Error ? err.message : err}); falling back to the wizard`,
53
- }));
51
+ warn(`LLM planning failed (${err instanceof Error ? err.message : err}); falling back to the wizard`);
54
52
  const wizard = await runWizard(text);
55
53
  card = wizard.card;
56
54
  makeOptions = wizardToOptions(wizard.options, flags);
@@ -59,24 +57,28 @@ export function registerAiCommand(program) {
59
57
  else {
60
58
  // The wizard is interactive by nature — never enter it headless.
61
59
  if (!isInteractive()) {
62
- throw new CliError(t({
63
- zh: '向导模式需要交互终端;无头环境请用 tripo make --for <场景>(或配置 llm.base_url/llm.model 走对话模式)',
64
- en: 'wizard mode needs an interactive terminal; headless callers should use "tripo make --for <scenario>" (or configure llm.base_url/llm.model for conversation mode)',
65
- }), EXIT.USAGE);
60
+ throw new CliError('wizard mode needs an interactive terminal; headless callers should use "tripo make --for <scenario>" (or configure llm.base_url/llm.model for conversation mode)', EXIT.USAGE);
66
61
  }
67
62
  const wizard = await runWizard(text || undefined);
68
63
  card = wizard.card;
69
64
  makeOptions = wizardToOptions(wizard.options, flags);
70
65
  }
66
+ // An explicit chain counts as a --then override in planMake, which drops the preset's dual delivery.
67
+ const preset = getScenario(makeOptions.scenario ?? '');
68
+ if (preset && JSON.stringify(makeOptions.chain) === JSON.stringify(preset.chain))
69
+ makeOptions.chain = undefined;
70
+ const client = newClient();
71
+ const preview = await planMake(client, { ...makeOptions, dryRun: true });
72
+ card = { ...card, ...cardFromMakePlan(preview, makeOptions), notes: card.notes };
73
+ makeOptions.model = preview.model;
71
74
  process.stderr.write(renderPlanCard(card) + '\n');
72
75
  if (!isYes()) {
73
- const go = await p.confirm({ message: t({ zh: '按方案执行?', en: 'Run this plan?' }) });
76
+ const go = await p.confirm({ message: 'Run this plan?' });
74
77
  if (p.isCancel(go) || !go) {
75
- log(t({ zh: '已取消,未消耗积分', en: 'cancelled; no credits spent' }));
78
+ log('cancelled; no credits spent');
76
79
  return;
77
80
  }
78
81
  }
79
- const client = newClient();
80
82
  const plan = await planMake(client, makeOptions);
81
83
  const outcome = await executeMake(client, plan, makeOptions);
82
84
  emitMakeOutcome({ results: [outcome.primary], chainResults: outcome.chainResults, stoppedEarly: outcome.stoppedEarly });