tripo-cli 0.7.2 → 0.8.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +16 -14
  3. package/dist/ai/experts.js +26 -20
  4. package/dist/ai/plan-card.js +73 -34
  5. package/dist/ai/wizard.js +19 -19
  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 +50 -8
  13. package/dist/commands/home.js +12 -13
  14. package/dist/commands/login.js +125 -182
  15. package/dist/commands/make.js +50 -25
  16. package/dist/commands/mcp.js +8 -3
  17. package/dist/commands/process-groups.js +29 -18
  18. package/dist/commands/shared.js +22 -13
  19. package/dist/commands/task.js +12 -11
  20. package/dist/commands/topup.js +6 -10
  21. package/dist/commands/update.js +23 -11
  22. package/dist/commands/view.js +3 -4
  23. package/dist/config/config.js +1 -1
  24. package/dist/core/analytics.js +39 -16
  25. package/dist/core/client.js +53 -21
  26. package/dist/core/device-auth.js +49 -1
  27. package/dist/core/download.js +2 -1
  28. package/dist/core/errors.js +24 -14
  29. package/dist/core/output.js +0 -4
  30. package/dist/core/requests.js +94 -31
  31. package/dist/core/task-service.js +21 -20
  32. package/dist/core/upload.js +23 -5
  33. package/dist/core/watch.js +3 -0
  34. package/dist/knowledge/chains.js +16 -17
  35. package/dist/knowledge/error-catalog.js +39 -37
  36. package/dist/knowledge/models.js +147 -27
  37. package/dist/knowledge/params.js +143 -32
  38. package/dist/knowledge/scenarios.js +32 -55
  39. package/dist/pipeline/make.js +38 -30
  40. package/dist/pipeline/runner.js +27 -8
  41. package/dist/pipeline/stdin.js +40 -3
  42. package/dist/program.js +3 -10
  43. package/dist/util/input-detect.js +10 -1
  44. package/package.json +3 -2
  45. package/skill/SKILL.md +52 -15
  46. package/skill/commands/account.md +8 -3
  47. package/skill/commands/batch.md +13 -3
  48. package/skill/commands/generate.md +57 -23
  49. package/skill/commands/make.md +43 -16
  50. package/skill/commands/process.md +29 -24
  51. package/skill/commands/task.md +4 -4
  52. package/skill/commands/view.md +1 -1
  53. package/skill/common-errors.md +14 -10
  54. package/skill/examples/animation.md +4 -2
  55. package/skill/examples/ar-web.md +6 -4
  56. package/skill/examples/game-asset.md +18 -7
  57. package/skill/examples/pipes.md +2 -2
  58. package/skill/examples/print.md +7 -3
  59. package/dist/i18n.js +0 -19
@@ -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 });
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * `tripo batch run <manifest.yaml>` — bulk production with concurrency,
3
3
  * retries, and resumable state (skip jobs already succeeded on re-run).
4
+ * A job is never resubmitted once a paid task may exist for it: such jobs
5
+ * stop as needs_attention instead of retrying.
4
6
  *
5
7
  * Manifest:
6
8
  * concurrency: 2
@@ -14,10 +16,10 @@
14
16
  */
15
17
  import { readFileSync, writeFileSync } from 'node:fs';
16
18
  import { resolve } from 'node:path';
19
+ import pc from 'picocolors';
17
20
  import { parse as parseYaml } from 'yaml';
18
- import { UsageError } from '../core/errors.js';
19
- import { emitResult, errorLine, log, success } from '../core/output.js';
20
- import { t } from '../i18n.js';
21
+ import { CliError, TaskFailedError, UsageError } from '../core/errors.js';
22
+ import { emitResult, errorLine, log, success, warn } from '../core/output.js';
21
23
  import { parseThenExpression } from '../knowledge/chains.js';
22
24
  import { executeMake, planMake } from '../pipeline/make.js';
23
25
  import { newClient } from './shared.js';
@@ -27,7 +29,7 @@ export function registerBatchCommand(program) {
27
29
  .command('run <manifest>')
28
30
  .description('run all jobs (resumable: finished jobs are skipped on re-run)')
29
31
  .option('--concurrency <n>', 'parallel jobs (default from manifest or 2)', (v) => Number(v))
30
- .option('--retries <n>', 'retries per failed job', (v) => Number(v), 1)
32
+ .option('--retries <n>', 'retries per failed job (only when no paid task was left behind)', (v) => Number(v), 1)
31
33
  .option('--fresh', 'ignore previous state and re-run everything')
32
34
  .action(async (manifestPath, opts) => {
33
35
  const absPath = resolve(manifestPath);
@@ -46,12 +48,17 @@ export function registerBatchCommand(program) {
46
48
  seen.add(key);
47
49
  }
48
50
  const concurrency = Math.max(1, opts.concurrency ?? manifest.concurrency ?? 2);
49
- const queue = jobs.filter(({ key }) => state.jobs[key]?.status !== 'success');
50
- const skipped = jobs.length - queue.length;
51
+ const queue = jobs.filter(({ key }) => state.jobs[key]?.status !== 'success' && state.jobs[key]?.status !== 'needs_attention');
52
+ const attention = jobs.filter(({ key }) => state.jobs[key]?.status === 'needs_attention');
53
+ const skipped = jobs.length - queue.length - attention.length;
51
54
  if (skipped > 0) {
52
- log(t({ zh: `跳过 ${skipped} 个已完成任务(断点续跑)`, en: `skipping ${skipped} already-finished job(s) (resume)` }));
55
+ log(`skipping ${skipped} already-finished job(s) (resume)`);
53
56
  }
54
- log(t({ zh: `批量执行 ${queue.length} 个任务,并发 ${concurrency}`, en: `running ${queue.length} job(s), concurrency ${concurrency}` }));
57
+ for (const { key } of attention) {
58
+ const ids = (state.jobs[key].tasks ?? []).map((t) => t.task_id).join(', ') || 'unknown';
59
+ warn(`[${key}] needs attention, not resubmitted (tasks: ${ids}); delete its entry in ${statePath} or use --fresh to run it again`);
60
+ }
61
+ log(`running ${queue.length} job(s), concurrency ${concurrency}`);
55
62
  const client = newClient();
56
63
  let cursor = 0;
57
64
  const runNext = async () => {
@@ -64,20 +71,34 @@ export function registerBatchCommand(program) {
64
71
  const maxAttempts = 1 + opts.retries;
65
72
  for (let attempt = jobState.attempts + 1; attempt <= maxAttempts; attempt++) {
66
73
  jobState.attempts = attempt;
74
+ const created = [];
67
75
  try {
68
- const outcome = await runJob(client, item.key, item.spec);
76
+ const outcome = await runJob(client, item.key, item.spec, (task) => {
77
+ created.push(task);
78
+ (jobState.tasks ??= []).push(task);
79
+ saveState(statePath, state);
80
+ });
69
81
  jobState.status = 'success';
70
82
  jobState.task_id = outcome.taskId;
71
83
  jobState.output_dir = outcome.outputDir;
72
84
  delete jobState.error;
85
+ delete jobState.suggestion;
73
86
  success(`[${item.key}] done${outcome.outputDir ? ` → ${outcome.outputDir}` : ''}`);
74
87
  break;
75
88
  }
76
89
  catch (err) {
77
90
  const message = err instanceof Error ? err.message : String(err);
78
- jobState.status = 'failed';
91
+ const retryable = mayRetry(err, created);
92
+ jobState.status = retryable ? 'failed' : 'needs_attention';
79
93
  jobState.error = message;
80
- errorLine(`[${item.key}] attempt ${attempt}/${maxAttempts} failed: ${message}`);
94
+ jobState.suggestion = retryable
95
+ ? err instanceof CliError ? err.suggestion : undefined
96
+ : attentionSuggestion(err, jobState.tasks ?? []);
97
+ errorLine(`[${item.key}] attempt ${attempt}/${maxAttempts} failed${retryable ? '' : ' (needs attention: a paid task may exist, not retried)'}: ${message}`);
98
+ if (jobState.suggestion)
99
+ process.stderr.write(pc.dim(` → ${jobState.suggestion}\n`));
100
+ if (!retryable)
101
+ break;
81
102
  }
82
103
  finally {
83
104
  saveState(statePath, state);
@@ -90,19 +111,18 @@ export function registerBatchCommand(program) {
90
111
  total: jobs.length,
91
112
  success: Object.values(state.jobs).filter((j) => j.status === 'success').length,
92
113
  failed: Object.values(state.jobs).filter((j) => j.status === 'failed').length,
114
+ needs_attention: Object.values(state.jobs).filter((j) => j.status === 'needs_attention').length,
93
115
  state_file: statePath,
94
116
  jobs: state.jobs,
95
117
  };
96
- const failed = summary.failed;
97
- emitResult(summary, t({
98
- zh: `批量完成: 成功 ${summary.success}/${summary.total}${failed ? `,失败 ${failed}(重跑同一命令续跑)` : ''}`,
99
- en: `batch finished: ${summary.success}/${summary.total} succeeded${failed ? `, ${failed} failed (re-run the same command to resume)` : ''}`,
100
- }));
101
- if (failed > 0)
118
+ const { failed, needs_attention: needsAttention } = summary;
119
+ emitResult(summary, `batch finished: ${summary.success}/${summary.total} succeeded${failed ? `, ${failed} failed (re-run the same command to resume)` : ''}` +
120
+ `${needsAttention ? `, ${needsAttention} need attention (never resubmitted automatically; see "suggestion" in ${statePath})` : ''}`);
121
+ if (failed > 0 || needsAttention > 0)
102
122
  process.exitCode = 6;
103
123
  });
104
124
  }
105
- async function runJob(client, key, spec) {
125
+ async function runJob(client, key, spec, onTaskCreated) {
106
126
  const inputs = spec.inputs ?? (spec.input !== undefined ? [spec.input] : []);
107
127
  if (inputs.length === 0)
108
128
  throw new UsageError(`job "${key}" has no input`);
@@ -115,12 +135,32 @@ async function runJob(client, key, spec) {
115
135
  params: spec.params ?? {},
116
136
  name: spec.name ?? key,
117
137
  outDir: spec.out,
138
+ onTaskCreated,
118
139
  };
119
140
  const plan = await planMake(client, options);
120
141
  const outcome = await executeMake(client, plan, options);
121
142
  const finalResult = outcome.chainResults[outcome.chainResults.length - 1] ?? outcome.primary;
122
143
  return { taskId: finalResult.task_id, outputDir: finalResult.output_dir ?? outcome.primary.output_dir };
123
144
  }
145
+ /**
146
+ * Retrying is safe only when the failed attempt left no billed or still-running
147
+ * task behind: it failed before anything was submitted, or its only task failed
148
+ * server-side (failed/cancelled tasks are refunded).
149
+ */
150
+ function mayRetry(err, created) {
151
+ if (created.length === 0)
152
+ return !(err instanceof CliError && err.submissionUncertain);
153
+ return (created.length === 1 &&
154
+ err instanceof TaskFailedError &&
155
+ err.taskId === created[0].task_id &&
156
+ (err.status === 'failed' || err.status === 'cancelled'));
157
+ }
158
+ function attentionSuggestion(err, tasks) {
159
+ if (err instanceof CliError && err.suggestion)
160
+ return err.suggestion;
161
+ const list = tasks.map((t) => `${t.task_id} (${t.type})`).join(', ');
162
+ return `paid tasks already exist for this job: ${list}; do not rerun the whole job, continue from the last successful one (tripo make <task_id> --then <remaining steps>)`;
163
+ }
124
164
  function loadManifest(path) {
125
165
  let raw;
126
166
  try {
@@ -9,7 +9,7 @@ const COMMANDS = [
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'],
12
- model: ['refine', 'texture', 'convert', 'import'],
12
+ model: ['texture', 'convert', 'import'],
13
13
  anim: ['check', 'rig', 'retarget'],
14
14
  mesh: ['segment', 'complete', 'decimate', 'uv', ...(isMeshEditEnabled() ? ['edit'] : []), 'smartsegment'],
15
15
  task: ['get', 'list', 'watch'],
@@ -7,7 +7,7 @@ import { coerceValue } from './shared.js';
7
7
  /** Account-bound settings — these live in the *active profile*. */
8
8
  const PROFILE_SETTABLE = new Set(['region', 'api_base_url', 'platform_base_url']);
9
9
  /** User preferences — top-level, shared by all profiles. */
10
- const GLOBAL_SETTABLE = new Set(['language', 'default_output_dir', 'llm.base_url', 'llm.api_key', 'llm.model']);
10
+ const GLOBAL_SETTABLE = new Set(['default_output_dir', 'llm.base_url', 'llm.api_key', 'llm.model']);
11
11
  const SETTABLE = [...PROFILE_SETTABLE, ...GLOBAL_SETTABLE];
12
12
  export function registerConfigCommands(program) {
13
13
  const config = program.command('config').description('global config (~/.tripo) and directory context (.tripo)');
@@ -2,7 +2,7 @@
2
2
  import { statSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import pc from 'picocolors';
5
- import { configFileExists, describeKeySource, listProfiles, loadConfig, resolveApiBaseUrl, resolveApiKey, resolveRegion, tripoHome } from '../config/config.js';
5
+ import { configFileExists, describeKeySource, listProfiles, resolveApiBaseUrl, resolveApiKey, resolveRegion, tripoHome } from '../config/config.js';
6
6
  import { TripoClient } from '../core/client.js';
7
7
  import { CliError } from '../core/errors.js';
8
8
  import { detectRegionForKey } from '../core/region.js';
@@ -93,8 +93,6 @@ export function registerDoctorCommand(program) {
93
93
  detail: `${process.stdout.isTTY ? 'interactive TTY' : 'non-TTY (headless mode)'}${terminalSupportsInlineImages() ? ' · inline images supported' : ''}`,
94
94
  critical: false,
95
95
  });
96
- const language = loadConfig().language ?? 'auto';
97
- checks.push({ name: 'language', ok: true, detail: language, critical: false });
98
96
  const failedCritical = checks.some((c) => c.critical && !c.ok);
99
97
  const human = checks
100
98
  .map((c) => `${c.ok ? pc.green('✓') : c.critical ? pc.red('✗') : pc.yellow('!')} ${c.name.padEnd(34)} ${c.detail}`)
@@ -3,8 +3,9 @@ import { UsageError } from '../core/errors.js';
3
3
  import { warn } from '../core/output.js';
4
4
  import { buildEditMultiview, buildImageToImage, buildImageToModel, buildImageToMultiview, buildImageToSplat, buildMultiviewToModel, buildTextToImage, buildTextToModel, } from '../core/requests.js';
5
5
  import { resolveInputValue, runTask } from '../core/task-service.js';
6
- import { chooseModel } from '../knowledge/models.js';
6
+ import { chooseModel, detectDenseGeometryIntent, detectLowPolyIntent, detectRealtimeIntent } from '../knowledge/models.js';
7
7
  import { assignViews } from '../pipeline/make.js';
8
+ import { detectInput } from '../util/input-detect.js';
8
9
  import { newClient, printTaskResult, withTaskOptions } from './shared.js';
9
10
  export function registerGenerateCommands(program) {
10
11
  const generate = program.command('generate').description('deterministic generation endpoints (text/image/multiview → model/image/splat)');
@@ -38,29 +39,70 @@ export function registerGenerateCommands(program) {
38
39
  };
39
40
  add('text-to-model', 'text → 3D model', 'prompt', async (_c, inputs, flags) => {
40
41
  const prompt = inputs.join(' ');
41
- const { model } = chooseModel({ explicit: flags.model, faceLimit: numberParam(flags, 'face_limit') });
42
+ const { model } = chooseModel({
43
+ explicit: flags.model,
44
+ faceLimit: numberParam(flags, 'face_limit'),
45
+ quad: flags.param.quad === true,
46
+ lowPoly: detectLowPolyIntent(prompt),
47
+ realtime: detectRealtimeIntent(prompt),
48
+ denseGeometry: detectDenseGeometryIntent(prompt),
49
+ params: flags.param,
50
+ });
42
51
  return buildTextToModel(prompt, model, flags.param);
43
52
  });
44
53
  add('image-to-model', 'single image → 3D model', 'single', async (client, inputs, flags) => {
54
+ const intent = flags.prompt ?? '';
55
+ const { model } = chooseModel({
56
+ explicit: flags.model,
57
+ faceLimit: numberParam(flags, 'face_limit'),
58
+ quad: flags.param.quad === true,
59
+ lowPoly: detectLowPolyIntent(intent),
60
+ realtime: detectRealtimeIntent(intent),
61
+ denseGeometry: detectDenseGeometryIntent(intent),
62
+ params: flags.param,
63
+ });
64
+ buildImageToModel(inputs[0], model, flags.param);
45
65
  const { value } = await resolveInputValue(client, inputs[0]);
46
- const { model } = chooseModel({ explicit: flags.model, faceLimit: numberParam(flags, 'face_limit') });
47
66
  return buildImageToModel(value, model, flags.param);
48
67
  });
49
- add('multiview-to-model', '2–4 view images → 3D model', 'multi', async (client, inputs, flags) => {
68
+ add('multiview-to-model', '2–4 view images, or one image-to-multiview/edit-multiview task → 3D model', 'multi', async (client, inputs, flags) => {
69
+ const intent = flags.prompt ?? '';
70
+ const { model } = chooseModel({
71
+ explicit: flags.model,
72
+ faceLimit: numberParam(flags, 'face_limit'),
73
+ quad: flags.param.quad === true,
74
+ lowPoly: detectLowPolyIntent(intent),
75
+ realtime: detectRealtimeIntent(intent),
76
+ denseGeometry: detectDenseGeometryIntent(intent),
77
+ params: flags.param,
78
+ });
79
+ const kind = inputs.length === 1 ? detectInput(inputs[0]).kind : undefined;
80
+ if (kind === 'task-id' || kind === 'history-ref') {
81
+ const { value, sourceTaskType } = await resolveInputValue(client, inputs[0]);
82
+ if (sourceTaskType && sourceTaskType !== 'generate_multiview_image' && sourceTaskType !== 'edit_multiview_image') {
83
+ throw new UsageError(`multiview-to-model reuses an image-to-multiview / edit-multiview task, not ${sourceTaskType}`);
84
+ }
85
+ return buildMultiviewToModel({ task_id: value }, model, flags.param);
86
+ }
87
+ const previewViews = await assignViews(client, inputs, { dryRun: true });
88
+ buildMultiviewToModel(previewViews, model, flags.param);
50
89
  const views = await assignViews(client, inputs);
51
- const { model } = chooseModel({ explicit: flags.model, faceLimit: numberParam(flags, 'face_limit') });
52
90
  return buildMultiviewToModel(views, model, flags.param);
53
91
  });
54
92
  add('text-to-image', 'text → image (concept art / t_pose templates)', 'prompt', async (_c, inputs, flags) => buildTextToImage(inputs.join(' '), { ...(flags.model ? { model: flags.model } : {}), ...flags.param }));
55
- add('image-to-image', 'image + prompt → edited image', 'single', async (client, inputs, flags) => {
56
- const { value } = await resolveInputValue(client, inputs[0]);
93
+ add('image-to-image', 'image(s) + prompt → edited image (several images: cite them as image[1], image[2], ...)', 'multi', async (client, inputs, flags) => {
57
94
  const params = { ...(flags.model ? { model: flags.model } : {}), ...flags.param };
58
95
  if (flags.prompt)
59
96
  params.prompt = flags.prompt;
60
97
  if (!params.prompt && !params.template) {
61
98
  throw new UsageError('image-to-image needs --prompt or --param template=...');
62
99
  }
63
- return buildImageToImage(value, params);
100
+ // Validate deterministic parameters before resolving any local uploads.
101
+ buildImageToImage(inputs, params);
102
+ const values = [];
103
+ for (const input of inputs)
104
+ values.push((await resolveInputValue(client, input)).value);
105
+ return buildImageToImage(values, params);
64
106
  });
65
107
  add('image-to-multiview', 'single image → 4-view sheet', 'single', async (client, inputs, flags) => {
66
108
  const { value } = await resolveInputValue(client, inputs[0]);
@@ -5,7 +5,6 @@ import { activeProfileName, listProfiles, loadConfig, resolveApiKey } from '../c
5
5
  import { readHistory } from '../config/history.js';
6
6
  import { TripoClient } from '../core/client.js';
7
7
  import { isInteractive, log } from '../core/output.js';
8
- import { t } from '../i18n.js';
9
8
  import { runLogin } from './login.js';
10
9
  export function registerHomeAction(program) {
11
10
  program.action(async () => {
@@ -16,30 +15,30 @@ export function registerHomeAction(program) {
16
15
  p.intro(pc.bold('tripo'));
17
16
  const key = resolveApiKey(loadConfig());
18
17
  if (!key) {
19
- log(t({ zh: '还没有配置 API Key,先登录吧(新用户可领免费额度)', en: 'no API key yet — log in first (new accounts get free credits)' }));
18
+ log('no API key yet — log in first (new accounts get free credits)');
20
19
  await runLogin({});
21
20
  return;
22
21
  }
23
22
  let balanceLine = '';
24
23
  try {
25
24
  const balance = await new TripoClient().getBalance();
26
- balanceLine = t({ zh: `余额 ${balance.balance} 积分`, en: `${balance.balance} credits` });
25
+ balanceLine = `${balance.balance} credits`;
27
26
  }
28
27
  catch {
29
- balanceLine = t({ zh: '余额获取失败', en: 'balance unavailable' });
28
+ balanceLine = 'balance unavailable';
30
29
  }
31
30
  const last = readHistory().filter((e) => e.status === 'success').pop();
32
31
  const config = loadConfig();
33
- const profileNote = listProfiles(config).length > 1 ? t({ zh: ` · 账号 ${activeProfileName(config)}`, en: ` · profile ${activeProfileName(config)}` }) : '';
34
- log(`${balanceLine}${profileNote}${last ? t({ zh: ` · 上次任务 @${last.name}`, en: ` · last task @${last.name}` }) : ''}`);
32
+ const profileNote = listProfiles(config).length > 1 ? ` · profile ${activeProfileName(config)}` : '';
33
+ log(`${balanceLine}${profileNote}${last ? ` · last task @${last.name}` : ''}`);
35
34
  const action = await p.select({
36
- message: t({ zh: '想做什么?', en: 'What do you want to do?' }),
35
+ message: 'What do you want to do?',
37
36
  options: [
38
- { value: 'ai', label: t({ zh: '描述需求,让专家帮我定方案 (tripo ai)', en: 'describe what I want, experts plan it (tripo ai)' }) },
39
- { value: 'make', label: t({ zh: '直接生成 (tripo make ...)', en: 'generate directly (tripo make ...)' }) },
40
- ...(last ? [{ value: 'view', label: t({ zh: '看看上一个结果 (tripo view)', en: 'view the last result (tripo view)' }) }] : []),
41
- { value: 'topup', label: t({ zh: '充值 (tripo topup)', en: 'top up credits (tripo topup)' }) },
42
- { value: 'help', label: t({ zh: '看全部命令', en: 'show all commands' }) },
37
+ { value: 'ai', label: 'describe what I want, experts plan it (tripo ai)' },
38
+ { value: 'make', label: 'generate directly (tripo make ...)' },
39
+ ...(last ? [{ value: 'view', label: 'view the last result (tripo view)' }] : []),
40
+ { value: 'topup', label: 'top up credits (tripo topup)' },
41
+ { value: 'help', label: 'show all commands' },
43
42
  ],
44
43
  });
45
44
  if (p.isCancel(action))
@@ -50,7 +49,7 @@ export function registerHomeAction(program) {
50
49
  break;
51
50
  case 'make': {
52
51
  const promptText = await p.text({
53
- message: t({ zh: '输入一句描述或文件路径', en: 'enter a description or file path' }),
52
+ message: 'enter a description or file path',
54
53
  });
55
54
  if (p.isCancel(promptText) || !promptText.trim())
56
55
  return;