tripo-cli 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -4
- package/dist/commands/completion.js +1 -1
- package/dist/commands/config-cmd.js +32 -17
- package/dist/commands/doctor.js +6 -5
- package/dist/commands/home.js +4 -2
- package/dist/commands/login.js +201 -39
- package/dist/config/config.js +103 -14
- package/dist/core/client.js +16 -2
- package/dist/core/device-auth.js +41 -9
- package/dist/program.js +21 -1
- package/package.json +2 -2
- package/skill/SKILL.md +13 -1
- package/skill/commands/account.md +19 -5
- package/dist/src/ai/experts.js +0 -115
- package/dist/src/ai/llm.js +0 -88
- package/dist/src/ai/plan-card.js +0 -79
- package/dist/src/ai/wizard.js +0 -104
- package/dist/src/cli.js +0 -7
- package/dist/src/commands/account.js +0 -27
- package/dist/src/commands/ai.js +0 -113
- package/dist/src/commands/batch.js +0 -148
- package/dist/src/commands/completion.js +0 -84
- package/dist/src/commands/config-cmd.js +0 -84
- package/dist/src/commands/docs.js +0 -30
- package/dist/src/commands/doctor.js +0 -94
- package/dist/src/commands/generate.js +0 -81
- package/dist/src/commands/home.js +0 -68
- package/dist/src/commands/login.js +0 -225
- package/dist/src/commands/make.js +0 -142
- package/dist/src/commands/mcp.js +0 -194
- package/dist/src/commands/process-groups.js +0 -164
- package/dist/src/commands/shared.js +0 -75
- package/dist/src/commands/task.js +0 -127
- package/dist/src/commands/topup.js +0 -61
- package/dist/src/commands/view.js +0 -97
- package/dist/src/config/config.js +0 -79
- package/dist/src/config/context.js +0 -44
- package/dist/src/config/history.js +0 -104
- package/dist/src/core/client.js +0 -120
- package/dist/src/core/download.js +0 -107
- package/dist/src/core/errors.js +0 -64
- package/dist/src/core/output.js +0 -72
- package/dist/src/core/requests.js +0 -169
- package/dist/src/core/task-service.js +0 -161
- package/dist/src/core/upload.js +0 -36
- package/dist/src/core/viewer-assets.js +0 -24
- package/dist/src/core/viewer.js +0 -59
- package/dist/src/core/watch.js +0 -41
- package/dist/src/i18n.js +0 -19
- package/dist/src/knowledge/chains.js +0 -152
- package/dist/src/knowledge/error-catalog.js +0 -91
- package/dist/src/knowledge/models.js +0 -110
- package/dist/src/knowledge/params.js +0 -174
- package/dist/src/knowledge/scenarios.js +0 -112
- package/dist/src/pipeline/make.js +0 -233
- package/dist/src/pipeline/runner.js +0 -73
- package/dist/src/pipeline/stdin.js +0 -40
- package/dist/src/program.js +0 -147
- package/dist/src/types.js +0 -2
- package/dist/src/util/input-detect.js +0 -92
- package/dist/src/util/notify.js +0 -17
- package/dist/src/util/open.js +0 -18
- package/dist/src/util/spinner.js +0 -62
- package/dist/src/util/term-image.js +0 -28
- package/dist/src/version.js +0 -10
package/dist/src/ai/plan-card.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The plan card: one shared "here is what I will do and what it costs"
|
|
3
|
-
* confirmation format used by both the wizard (zero-LLM) and the expert
|
|
4
|
-
* conversation mode. Decisions come from the knowledge base either way.
|
|
5
|
-
*/
|
|
6
|
-
import pc from 'picocolors';
|
|
7
|
-
import { t } from '../i18n.js';
|
|
8
|
-
import { getScenario } from '../knowledge/scenarios.js';
|
|
9
|
-
/** Cost-impact annotations derived from the parameter set. */
|
|
10
|
-
export function costNotes(card) {
|
|
11
|
-
const notes = [];
|
|
12
|
-
const params = card.genParams;
|
|
13
|
-
if (params.texture === false) {
|
|
14
|
-
notes.push(t({ zh: '白模生成:不消耗贴图积分', en: 'bare geometry: no texture credits' }));
|
|
15
|
-
}
|
|
16
|
-
if (params.texture_quality === 'detailed' || params.texture_quality === 'extreme') {
|
|
17
|
-
notes.push(t({ zh: `贴图质量 ${params.texture_quality}:价签更高`, en: `texture_quality ${params.texture_quality}: higher price tier` }));
|
|
18
|
-
}
|
|
19
|
-
if (params.geometry_quality === 'detailed') {
|
|
20
|
-
notes.push(t({ zh: 'geometry detailed(Ultra):加收 HD credit', en: 'geometry detailed (Ultra): extra HD credit' }));
|
|
21
|
-
}
|
|
22
|
-
if (params.quad === true) {
|
|
23
|
-
notes.push(t({ zh: 'quad 四边面:价签更高,输出 FBX', en: 'quad topology: higher price tier, FBX output' }));
|
|
24
|
-
}
|
|
25
|
-
if (params.generate_parts === true) {
|
|
26
|
-
notes.push(t({ zh: '分件生成:价签更高,出裸几何', en: 'part generation: higher price tier, bare geometry' }));
|
|
27
|
-
}
|
|
28
|
-
for (const step of card.chain) {
|
|
29
|
-
if (step.step === 'convert' && Object.keys(step.args).length > 1) {
|
|
30
|
-
notes.push(t({ zh: 'convert 非默认参数:按 complex 价签计费', en: 'non-default convert options bill as complex convert' }));
|
|
31
|
-
}
|
|
32
|
-
if (step.step === 'retarget') {
|
|
33
|
-
const count = Array.isArray(step.args.animations) ? step.args.animations.length : 1;
|
|
34
|
-
notes.push(t({ zh: `retarget ${count} 条动画:按条计费`, en: `retarget ${count} animation(s): billed per animation` }));
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return notes;
|
|
38
|
-
}
|
|
39
|
-
/** Render the card for terminal display (stderr). */
|
|
40
|
-
export function renderPlanCard(card) {
|
|
41
|
-
const scenario = card.scenario ? getScenario(card.scenario) : undefined;
|
|
42
|
-
const lines = [];
|
|
43
|
-
const title = t({ zh: '方案卡', en: 'Plan' });
|
|
44
|
-
lines.push(pc.bold(`── ${title} ──────────────────────────`));
|
|
45
|
-
lines.push(`${t({ zh: '输入', en: 'input' })}: ${card.inputSummary}`);
|
|
46
|
-
if (scenario)
|
|
47
|
-
lines.push(`${t({ zh: '场景', en: 'scenario' })}: ${scenario.id} (${t(scenario.label)})`);
|
|
48
|
-
if (card.model) {
|
|
49
|
-
lines.push(`${t({ zh: '模型', en: 'model' })}: ${card.model}${card.modelReason ? pc.dim(` — ${card.modelReason}`) : ''}`);
|
|
50
|
-
}
|
|
51
|
-
const params = Object.entries(card.genParams);
|
|
52
|
-
if (params.length > 0) {
|
|
53
|
-
lines.push(`${t({ zh: '参数', en: 'params' })}: ${params.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(' ')}`);
|
|
54
|
-
}
|
|
55
|
-
if (card.chain.length > 0) {
|
|
56
|
-
const chainStr = card.chain
|
|
57
|
-
.map((s) => (Object.keys(s.args).length > 0 ? `${s.step}(${Object.entries(s.args).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(',')})` : s.step))
|
|
58
|
-
.join(' → ');
|
|
59
|
-
lines.push(`${t({ zh: '加工链', en: 'chain' })}: ${chainStr}`);
|
|
60
|
-
}
|
|
61
|
-
if (card.deliverables.length > 0) {
|
|
62
|
-
lines.push(`${t({ zh: '交付物', en: 'deliverables' })}: ${card.deliverables.join(', ')}`);
|
|
63
|
-
}
|
|
64
|
-
const cost = costNotes(card);
|
|
65
|
-
if (cost.length > 0) {
|
|
66
|
-
lines.push(`${t({ zh: '成本影响', en: 'cost impact' })}:`);
|
|
67
|
-
for (const note of cost)
|
|
68
|
-
lines.push(` · ${note}`);
|
|
69
|
-
}
|
|
70
|
-
for (const note of card.notes)
|
|
71
|
-
lines.push(pc.dim(` ${note}`));
|
|
72
|
-
if (card.expertNotes && card.expertNotes.length > 0) {
|
|
73
|
-
lines.push(`${t({ zh: '专家意见', en: 'expert notes' })}:`);
|
|
74
|
-
for (const note of card.expertNotes)
|
|
75
|
-
lines.push(` · ${note}`);
|
|
76
|
-
}
|
|
77
|
-
lines.push(pc.bold('──────────────────────────────────'));
|
|
78
|
-
return lines.join('\n');
|
|
79
|
-
}
|
package/dist/src/ai/wizard.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wizard mode (zero-LLM): 3–4 multiple-choice questions drive the same
|
|
3
|
-
* knowledge-base decision tree that experts use. Every user gets 80% of the
|
|
4
|
-
* intelligence with no LLM configured.
|
|
5
|
-
*/
|
|
6
|
-
import * as p from '@clack/prompts';
|
|
7
|
-
import { CliError, UsageError } from '../core/errors.js';
|
|
8
|
-
import { isInteractive } from '../core/output.js';
|
|
9
|
-
import { t, tList } from '../i18n.js';
|
|
10
|
-
import { EXIT } from '../knowledge/error-catalog.js';
|
|
11
|
-
import { getScenario, SCENARIOS } from '../knowledge/scenarios.js';
|
|
12
|
-
export async function runWizard(initialPrompt) {
|
|
13
|
-
if (!isInteractive()) {
|
|
14
|
-
throw new UsageError(t({
|
|
15
|
-
zh: '向导需要交互终端;无头环境请直接用 tripo make --for <场景> 或配置 LLM 后用 tripo ai --yes',
|
|
16
|
-
en: 'the wizard needs an interactive terminal; headless callers should use "tripo make --for <scenario>" instead',
|
|
17
|
-
}));
|
|
18
|
-
}
|
|
19
|
-
p.intro('tripo ai');
|
|
20
|
-
let prompt = initialPrompt?.trim() ?? '';
|
|
21
|
-
if (!prompt) {
|
|
22
|
-
const answer = await p.text({
|
|
23
|
-
message: t({ zh: '想做个什么?一句话描述(或图片路径)', en: 'What do you want to make? one sentence (or an image path)' }),
|
|
24
|
-
validate: (v) => (v && v.trim().length > 0 ? undefined : t({ zh: '说点什么吧', en: 'say something' })),
|
|
25
|
-
});
|
|
26
|
-
if (p.isCancel(answer))
|
|
27
|
-
throw new CliError('cancelled', EXIT.FATAL);
|
|
28
|
-
prompt = answer.trim();
|
|
29
|
-
}
|
|
30
|
-
const scenarioAnswer = await p.select({
|
|
31
|
-
message: t({ zh: '用在什么地方?', en: 'What is it for?' }),
|
|
32
|
-
options: SCENARIOS.map((s) => ({ value: s.id, label: `${t(s.label)} — ${t(s.description)}` })),
|
|
33
|
-
});
|
|
34
|
-
if (p.isCancel(scenarioAnswer))
|
|
35
|
-
throw new CliError('cancelled', EXIT.FATAL);
|
|
36
|
-
const scenario = getScenario(scenarioAnswer);
|
|
37
|
-
let engine;
|
|
38
|
-
if (scenarioAnswer === 'game-mobile' || scenarioAnswer === 'game-pc') {
|
|
39
|
-
const engineAnswer = await p.select({
|
|
40
|
-
message: t({ zh: '目标引擎/软件?', en: 'Target engine/software?' }),
|
|
41
|
-
options: [
|
|
42
|
-
{ value: 'unity', label: 'Unity' },
|
|
43
|
-
{ value: 'unreal', label: 'Unreal' },
|
|
44
|
-
{ value: 'blender', label: 'Blender' },
|
|
45
|
-
{ value: 'any', label: t({ zh: '不确定/通用', en: 'not sure / generic' }) },
|
|
46
|
-
],
|
|
47
|
-
});
|
|
48
|
-
if (p.isCancel(engineAnswer))
|
|
49
|
-
throw new CliError('cancelled', EXIT.FATAL);
|
|
50
|
-
engine = engineAnswer;
|
|
51
|
-
}
|
|
52
|
-
let animations = false;
|
|
53
|
-
if (scenarioAnswer !== 'print' && scenarioAnswer !== 'film') {
|
|
54
|
-
const animAnswer = await p.confirm({
|
|
55
|
-
message: t({ zh: '需要骨骼/动画吗?', en: 'Do you need a skeleton / animations?' }),
|
|
56
|
-
initialValue: scenarioAnswer === 'anim',
|
|
57
|
-
});
|
|
58
|
-
if (p.isCancel(animAnswer))
|
|
59
|
-
throw new CliError('cancelled', EXIT.FATAL);
|
|
60
|
-
animations = animAnswer;
|
|
61
|
-
}
|
|
62
|
-
const { chain, notes } = adjustChain(scenario.id, engine, animations);
|
|
63
|
-
const card = {
|
|
64
|
-
inputSummary: prompt,
|
|
65
|
-
scenario: scenario.id,
|
|
66
|
-
model: scenario.model,
|
|
67
|
-
modelReason: t({ zh: '场景预设', en: 'scenario preset' }),
|
|
68
|
-
genParams: scenario.genParams,
|
|
69
|
-
chain,
|
|
70
|
-
deliverables: scenario.deliverables,
|
|
71
|
-
notes: [...tList(scenario.notes), ...notes],
|
|
72
|
-
};
|
|
73
|
-
return { card, options: { prompt, scenario: scenario.id, chain } };
|
|
74
|
-
}
|
|
75
|
-
/** Engine/animation adjustments on top of the scenario chain. */
|
|
76
|
-
export function adjustChain(scenarioId, engine, animations) {
|
|
77
|
-
const scenario = getScenario(scenarioId);
|
|
78
|
-
let chain = scenario.chain.map((s) => ({ step: s.step, args: { ...s.args } }));
|
|
79
|
-
const notes = [];
|
|
80
|
-
if (animations && !chain.some((s) => s.step === 'rig')) {
|
|
81
|
-
const rigArgs = { model: 'rig-v2.0', out_format: 'fbx' };
|
|
82
|
-
if (engine === 'unity') {
|
|
83
|
-
rigArgs.spec = 'mixamo';
|
|
84
|
-
notes.push(t({ zh: 'Unity Humanoid → spec=mixamo', en: 'Unity Humanoid → spec=mixamo' }));
|
|
85
|
-
}
|
|
86
|
-
chain = [
|
|
87
|
-
...chain.filter((s) => s.step !== 'convert'),
|
|
88
|
-
{ step: 'rig-check', args: {} },
|
|
89
|
-
{ step: 'rig', args: rigArgs },
|
|
90
|
-
{ step: 'retarget', args: { animations: ['preset:idle', 'preset:walk'], out_format: 'fbx' } },
|
|
91
|
-
];
|
|
92
|
-
notes.push(t({ zh: '动画链输出 FBX(骨骼+动画)', en: 'animation chain outputs FBX (skeleton + animations)' }));
|
|
93
|
-
}
|
|
94
|
-
if (engine && !animations) {
|
|
95
|
-
const convert = chain.find((s) => s.step === 'convert');
|
|
96
|
-
if (convert && convert.args.format === 'FBX') {
|
|
97
|
-
if (engine === 'blender')
|
|
98
|
-
convert.args.fbx_preset = 'blender';
|
|
99
|
-
if (engine === 'unreal' || engine === 'unity')
|
|
100
|
-
convert.args.export_orientation = '+x';
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return { chain, notes };
|
|
104
|
-
}
|
package/dist/src/cli.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/** `tripo balance` / `tripo usage` — account visibility. */
|
|
2
|
-
import { TripoClient } from '../core/client.js';
|
|
3
|
-
import { emitResult } from '../core/output.js';
|
|
4
|
-
import { t } from '../i18n.js';
|
|
5
|
-
export function registerAccountCommands(program) {
|
|
6
|
-
program
|
|
7
|
-
.command('balance')
|
|
8
|
-
.description('show available and frozen credits')
|
|
9
|
-
.action(async () => {
|
|
10
|
-
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
|
-
}));
|
|
15
|
-
});
|
|
16
|
-
program
|
|
17
|
-
.command('usage')
|
|
18
|
-
.description('recent task credit usage')
|
|
19
|
-
.option('--limit <n>', 'max rows to show', (v) => Number(v), 20)
|
|
20
|
-
.action(async (opts) => {
|
|
21
|
-
const items = (await new TripoClient().getUsage()).slice(0, opts.limit);
|
|
22
|
-
const human = items
|
|
23
|
-
.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
|
-
.join('\n');
|
|
25
|
-
emitResult(items, human || t({ zh: '暂无用量记录', en: 'no usage yet' }));
|
|
26
|
-
});
|
|
27
|
-
}
|
package/dist/src/commands/ai.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `tripo ai` — dual mode:
|
|
3
|
-
* wizard (default without an LLM): knowledge-base decision-tree Q&A
|
|
4
|
-
* conversation (with an LLM configured): producer + specialist agents
|
|
5
|
-
* Both produce the same plan card; confirmed plans execute through the same
|
|
6
|
-
* deterministic engine as `tripo make`.
|
|
7
|
-
*/
|
|
8
|
-
import * as p from '@clack/prompts';
|
|
9
|
-
import { resolveReference } from '../config/history.js';
|
|
10
|
-
import { CliError } from '../core/errors.js';
|
|
11
|
-
import { isInteractive, isYes, log, warn } from '../core/output.js';
|
|
12
|
-
import { t } from '../i18n.js';
|
|
13
|
-
import { EXIT } from '../knowledge/error-catalog.js';
|
|
14
|
-
import { executeMake, planMake } from '../pipeline/make.js';
|
|
15
|
-
import { renderPlanCard } from '../ai/plan-card.js';
|
|
16
|
-
import { resolveLlmConfig } from '../ai/llm.js';
|
|
17
|
-
import { buildExpertPlan } from '../ai/experts.js';
|
|
18
|
-
import { runWizard } from '../ai/wizard.js';
|
|
19
|
-
import { emitMakeOutcome } from './make.js';
|
|
20
|
-
import { newClient, withTaskOptions } from './shared.js';
|
|
21
|
-
export function registerAiCommand(program) {
|
|
22
|
-
const cmd = program
|
|
23
|
-
.command('ai')
|
|
24
|
-
.description('describe what you want in natural language; experts plan it, you confirm, it runs')
|
|
25
|
-
.argument('[request...]', 'e.g. "给我的手游做一个低模骑士,要跑步动画"')
|
|
26
|
-
.option('--wizard', 'force wizard mode even when an LLM is configured');
|
|
27
|
-
withTaskOptions(cmd);
|
|
28
|
-
cmd.action(async (request, flags) => {
|
|
29
|
-
const text = expandHistoryRefs(request.join(' ').trim());
|
|
30
|
-
const llm = resolveLlmConfig();
|
|
31
|
-
let card;
|
|
32
|
-
let makeOptions;
|
|
33
|
-
if (llm && !flags.wizard && text) {
|
|
34
|
-
log(t({ zh: `对话模式(${llm.model})…`, en: `conversation mode (${llm.model})…` }));
|
|
35
|
-
try {
|
|
36
|
-
const plan = await buildExpertPlan(text, isInteractive() ? askUser : undefined);
|
|
37
|
-
card = plan.card;
|
|
38
|
-
makeOptions = {
|
|
39
|
-
inputs: [plan.prompt],
|
|
40
|
-
scenario: plan.scenarioId,
|
|
41
|
-
chain: plan.chain,
|
|
42
|
-
params: plan.params,
|
|
43
|
-
name: flags.name,
|
|
44
|
-
outDir: flags.out,
|
|
45
|
-
notify: flags.notify,
|
|
46
|
-
timeoutMs: flags.timeout ? flags.timeout * 1000 : undefined,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
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
|
-
}));
|
|
54
|
-
const wizard = await runWizard(text);
|
|
55
|
-
card = wizard.card;
|
|
56
|
-
makeOptions = wizardToOptions(wizard.options, flags);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
if (llm === undefined && text && !isInteractive()) {
|
|
61
|
-
throw new CliError(t({
|
|
62
|
-
zh: '无 LLM 配置时 tripo ai 需要交互终端;无头环境请用 tripo make --for <场景>(或配置 llm.base_url/llm.model)',
|
|
63
|
-
en: 'without an LLM, "tripo ai" needs an interactive terminal; headless callers should use "tripo make --for <scenario>" (or configure llm.base_url/llm.model)',
|
|
64
|
-
}), EXIT.USAGE);
|
|
65
|
-
}
|
|
66
|
-
const wizard = await runWizard(text || undefined);
|
|
67
|
-
card = wizard.card;
|
|
68
|
-
makeOptions = wizardToOptions(wizard.options, flags);
|
|
69
|
-
}
|
|
70
|
-
process.stderr.write(renderPlanCard(card) + '\n');
|
|
71
|
-
if (!isYes()) {
|
|
72
|
-
const go = await p.confirm({ message: t({ zh: '按方案执行?', en: 'Run this plan?' }) });
|
|
73
|
-
if (p.isCancel(go) || !go) {
|
|
74
|
-
log(t({ zh: '已取消,未消耗积分', en: 'cancelled; no credits spent' }));
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const client = newClient();
|
|
79
|
-
const plan = await planMake(client, makeOptions);
|
|
80
|
-
const outcome = await executeMake(client, plan, makeOptions);
|
|
81
|
-
emitMakeOutcome({ results: [outcome.primary], chainResults: outcome.chainResults, stoppedEarly: outcome.stoppedEarly });
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
function wizardToOptions(options, flags) {
|
|
85
|
-
return {
|
|
86
|
-
inputs: [options.prompt],
|
|
87
|
-
scenario: options.scenario,
|
|
88
|
-
chain: options.chain,
|
|
89
|
-
params: flags.param,
|
|
90
|
-
name: flags.name,
|
|
91
|
-
outDir: flags.out,
|
|
92
|
-
notify: flags.notify,
|
|
93
|
-
timeoutMs: flags.timeout ? flags.timeout * 1000 : undefined,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
async function askUser(question) {
|
|
97
|
-
const answer = await p.text({ message: question });
|
|
98
|
-
if (p.isCancel(answer))
|
|
99
|
-
return undefined;
|
|
100
|
-
return answer;
|
|
101
|
-
}
|
|
102
|
-
/** Replace @task-name references in a prompt with their task ids. */
|
|
103
|
-
export function expandHistoryRefs(text) {
|
|
104
|
-
return text.replace(/@[\w\u4e00-\u9fff-]+/g, (ref) => {
|
|
105
|
-
try {
|
|
106
|
-
const entry = resolveReference(ref);
|
|
107
|
-
return entry ? entry.task_id : ref;
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
return ref; // ambiguity is reported when the value is actually used
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `tripo batch run <manifest.yaml>` — bulk production with concurrency,
|
|
3
|
-
* retries, and resumable state (skip jobs already succeeded on re-run).
|
|
4
|
-
*
|
|
5
|
-
* Manifest:
|
|
6
|
-
* concurrency: 2
|
|
7
|
-
* defaults: { for: game-mobile, then: "texture,convert:fbx", out: ./assets }
|
|
8
|
-
* jobs:
|
|
9
|
-
* - input: "a sword"
|
|
10
|
-
* - input: cat.png
|
|
11
|
-
* for: print
|
|
12
|
-
* - inputs: [hero-front.png, hero-back.png]
|
|
13
|
-
* name: hero
|
|
14
|
-
*/
|
|
15
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
-
import { resolve } from 'node:path';
|
|
17
|
-
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 { parseThenExpression } from '../knowledge/chains.js';
|
|
22
|
-
import { executeMake, planMake } from '../pipeline/make.js';
|
|
23
|
-
import { newClient } from './shared.js';
|
|
24
|
-
export function registerBatchCommand(program) {
|
|
25
|
-
const batch = program.command('batch').description('bulk pipelines from a YAML manifest');
|
|
26
|
-
batch
|
|
27
|
-
.command('run <manifest>')
|
|
28
|
-
.description('run all jobs (resumable: finished jobs are skipped on re-run)')
|
|
29
|
-
.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)
|
|
31
|
-
.option('--fresh', 'ignore previous state and re-run everything')
|
|
32
|
-
.action(async (manifestPath, opts) => {
|
|
33
|
-
const absPath = resolve(manifestPath);
|
|
34
|
-
const manifest = loadManifest(absPath);
|
|
35
|
-
const statePath = `${absPath}.state.json`;
|
|
36
|
-
const state = opts.fresh ? { jobs: {} } : loadState(statePath);
|
|
37
|
-
const jobs = manifest.jobs.map((job, index) => ({
|
|
38
|
-
key: job.name ?? `job-${index + 1}`,
|
|
39
|
-
spec: { ...manifest.defaults, ...job },
|
|
40
|
-
}));
|
|
41
|
-
// Duplicate names would corrupt state bookkeeping — fail fast.
|
|
42
|
-
const seen = new Set();
|
|
43
|
-
for (const { key } of jobs) {
|
|
44
|
-
if (seen.has(key))
|
|
45
|
-
throw new UsageError(`duplicate job name "${key}" in manifest`);
|
|
46
|
-
seen.add(key);
|
|
47
|
-
}
|
|
48
|
-
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
|
-
if (skipped > 0) {
|
|
52
|
-
log(t({ zh: `跳过 ${skipped} 个已完成任务(断点续跑)`, en: `skipping ${skipped} already-finished job(s) (resume)` }));
|
|
53
|
-
}
|
|
54
|
-
log(t({ zh: `批量执行 ${queue.length} 个任务,并发 ${concurrency}`, en: `running ${queue.length} job(s), concurrency ${concurrency}` }));
|
|
55
|
-
const client = newClient();
|
|
56
|
-
let cursor = 0;
|
|
57
|
-
const runNext = async () => {
|
|
58
|
-
for (;;) {
|
|
59
|
-
const item = queue[cursor++];
|
|
60
|
-
if (!item)
|
|
61
|
-
return;
|
|
62
|
-
const jobState = state.jobs[item.key] ?? { status: 'pending', attempts: 0 };
|
|
63
|
-
state.jobs[item.key] = jobState;
|
|
64
|
-
const maxAttempts = 1 + opts.retries;
|
|
65
|
-
for (let attempt = jobState.attempts + 1; attempt <= maxAttempts; attempt++) {
|
|
66
|
-
jobState.attempts = attempt;
|
|
67
|
-
try {
|
|
68
|
-
const outcome = await runJob(client, item.key, item.spec);
|
|
69
|
-
jobState.status = 'success';
|
|
70
|
-
jobState.task_id = outcome.taskId;
|
|
71
|
-
jobState.output_dir = outcome.outputDir;
|
|
72
|
-
delete jobState.error;
|
|
73
|
-
success(`[${item.key}] done${outcome.outputDir ? ` → ${outcome.outputDir}` : ''}`);
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
catch (err) {
|
|
77
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
78
|
-
jobState.status = 'failed';
|
|
79
|
-
jobState.error = message;
|
|
80
|
-
errorLine(`[${item.key}] attempt ${attempt}/${maxAttempts} failed: ${message}`);
|
|
81
|
-
}
|
|
82
|
-
finally {
|
|
83
|
-
saveState(statePath, state);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, runNext));
|
|
89
|
-
const summary = {
|
|
90
|
-
total: jobs.length,
|
|
91
|
-
success: Object.values(state.jobs).filter((j) => j.status === 'success').length,
|
|
92
|
-
failed: Object.values(state.jobs).filter((j) => j.status === 'failed').length,
|
|
93
|
-
state_file: statePath,
|
|
94
|
-
jobs: state.jobs,
|
|
95
|
-
};
|
|
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)
|
|
102
|
-
process.exitCode = 6;
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
async function runJob(client, key, spec) {
|
|
106
|
-
const inputs = spec.inputs ?? (spec.input !== undefined ? [spec.input] : []);
|
|
107
|
-
if (inputs.length === 0)
|
|
108
|
-
throw new UsageError(`job "${key}" has no input`);
|
|
109
|
-
const chain = spec.then ? parseThenExpression(spec.then) : undefined;
|
|
110
|
-
const options = {
|
|
111
|
-
inputs,
|
|
112
|
-
scenario: spec.for,
|
|
113
|
-
chain,
|
|
114
|
-
model: spec.model,
|
|
115
|
-
params: spec.params ?? {},
|
|
116
|
-
name: spec.name ?? key,
|
|
117
|
-
outDir: spec.out,
|
|
118
|
-
};
|
|
119
|
-
const plan = await planMake(client, options);
|
|
120
|
-
const outcome = await executeMake(client, plan, options);
|
|
121
|
-
const finalResult = outcome.chainResults[outcome.chainResults.length - 1] ?? outcome.primary;
|
|
122
|
-
return { taskId: finalResult.task_id, outputDir: finalResult.output_dir ?? outcome.primary.output_dir };
|
|
123
|
-
}
|
|
124
|
-
function loadManifest(path) {
|
|
125
|
-
let raw;
|
|
126
|
-
try {
|
|
127
|
-
raw = readFileSync(path, 'utf8');
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
throw new UsageError(`manifest not found: ${path}`);
|
|
131
|
-
}
|
|
132
|
-
const doc = parseYaml(raw);
|
|
133
|
-
if (!doc || !Array.isArray(doc.jobs) || doc.jobs.length === 0) {
|
|
134
|
-
throw new UsageError('manifest must contain a non-empty "jobs" list');
|
|
135
|
-
}
|
|
136
|
-
return doc;
|
|
137
|
-
}
|
|
138
|
-
function loadState(path) {
|
|
139
|
-
try {
|
|
140
|
-
return JSON.parse(readFileSync(path, 'utf8'));
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return { jobs: {} };
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function saveState(path, state) {
|
|
147
|
-
writeFileSync(path, JSON.stringify(state, null, 2) + '\n');
|
|
148
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/** `tripo completion <shell>` — static shell completion scripts. */
|
|
2
|
-
import { UsageError } from '../core/errors.js';
|
|
3
|
-
const COMMANDS = [
|
|
4
|
-
'login', 'logout', 'whoami', 'balance', 'usage', 'topup',
|
|
5
|
-
'make', 'view', 'redo', 'ai',
|
|
6
|
-
'generate', 'model', 'anim', 'mesh', 'task', 'files', 'batch', 'history',
|
|
7
|
-
'config', 'doctor', 'docs', 'completion', 'mcp',
|
|
8
|
-
];
|
|
9
|
-
const SUBS = {
|
|
10
|
-
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
|
-
model: ['refine', 'texture', 'stylize', 'convert', 'import'],
|
|
12
|
-
anim: ['check', 'rig', 'retarget'],
|
|
13
|
-
mesh: ['segment', 'complete', 'decimate', 'smartsegment'],
|
|
14
|
-
task: ['get', 'list', 'watch'],
|
|
15
|
-
files: ['upload'],
|
|
16
|
-
batch: ['run'],
|
|
17
|
-
config: ['list', 'set', 'unset', 'context'],
|
|
18
|
-
};
|
|
19
|
-
export function registerCompletionCommand(program) {
|
|
20
|
-
program
|
|
21
|
-
.command('completion <shell>')
|
|
22
|
-
.description('print a completion script (bash|zsh|fish); e.g. tripo completion zsh >> ~/.zshrc')
|
|
23
|
-
.action((shell) => {
|
|
24
|
-
switch (shell) {
|
|
25
|
-
case 'bash':
|
|
26
|
-
process.stdout.write(bashScript());
|
|
27
|
-
break;
|
|
28
|
-
case 'zsh':
|
|
29
|
-
process.stdout.write(zshScript());
|
|
30
|
-
break;
|
|
31
|
-
case 'fish':
|
|
32
|
-
process.stdout.write(fishScript());
|
|
33
|
-
break;
|
|
34
|
-
default:
|
|
35
|
-
throw new UsageError('supported shells: bash, zsh, fish');
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
function bashScript() {
|
|
40
|
-
const cases = Object.entries(SUBS)
|
|
41
|
-
.map(([cmd, subs]) => ` ${cmd}) COMPREPLY=($(compgen -W "${subs.join(' ')}" -- "$cur"));;`)
|
|
42
|
-
.join('\n');
|
|
43
|
-
return `# tripo bash completion
|
|
44
|
-
_tripo_completions() {
|
|
45
|
-
local cur prev
|
|
46
|
-
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
47
|
-
prev="\${COMP_WORDS[1]}"
|
|
48
|
-
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
49
|
-
COMPREPLY=($(compgen -W "${COMMANDS.join(' ')}" -- "$cur"))
|
|
50
|
-
return
|
|
51
|
-
fi
|
|
52
|
-
case "$prev" in
|
|
53
|
-
${cases}
|
|
54
|
-
esac
|
|
55
|
-
}
|
|
56
|
-
complete -F _tripo_completions tripo
|
|
57
|
-
`;
|
|
58
|
-
}
|
|
59
|
-
function zshScript() {
|
|
60
|
-
return `# tripo zsh completion
|
|
61
|
-
autoload -Uz compinit && compinit
|
|
62
|
-
_tripo() {
|
|
63
|
-
local -a cmds
|
|
64
|
-
cmds=(${COMMANDS.join(' ')})
|
|
65
|
-
if (( CURRENT == 2 )); then
|
|
66
|
-
compadd -a cmds
|
|
67
|
-
else
|
|
68
|
-
case $words[2] in
|
|
69
|
-
${Object.entries(SUBS)
|
|
70
|
-
.map(([cmd, subs]) => ` ${cmd}) compadd ${subs.join(' ')};;`)
|
|
71
|
-
.join('\n')}
|
|
72
|
-
esac
|
|
73
|
-
fi
|
|
74
|
-
}
|
|
75
|
-
compdef _tripo tripo
|
|
76
|
-
`;
|
|
77
|
-
}
|
|
78
|
-
function fishScript() {
|
|
79
|
-
const lines = [
|
|
80
|
-
`complete -c tripo -f -n '__fish_use_subcommand' -a '${COMMANDS.join(' ')}'`,
|
|
81
|
-
...Object.entries(SUBS).map(([cmd, subs]) => `complete -c tripo -f -n '__fish_seen_subcommand_from ${cmd}' -a '${subs.join(' ')}'`),
|
|
82
|
-
];
|
|
83
|
-
return lines.join('\n') + '\n';
|
|
84
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/** `tripo config` — global config + directory context management. */
|
|
2
|
-
import { loadConfig, maskKey, saveConfig } from '../config/config.js';
|
|
3
|
-
import { findContextFile, loadContext, updateContext } from '../config/context.js';
|
|
4
|
-
import { UsageError } from '../core/errors.js';
|
|
5
|
-
import { emitResult, success } from '../core/output.js';
|
|
6
|
-
import { coerceValue } from './shared.js';
|
|
7
|
-
const SETTABLE = new Set([
|
|
8
|
-
'region',
|
|
9
|
-
'api_base_url',
|
|
10
|
-
'platform_base_url',
|
|
11
|
-
'language',
|
|
12
|
-
'default_output_dir',
|
|
13
|
-
'llm.base_url',
|
|
14
|
-
'llm.api_key',
|
|
15
|
-
'llm.model',
|
|
16
|
-
]);
|
|
17
|
-
export function registerConfigCommands(program) {
|
|
18
|
-
const config = program.command('config').description('global config (~/.tripo) and directory context (.tripo)');
|
|
19
|
-
config
|
|
20
|
-
.command('list')
|
|
21
|
-
.description('show effective config (key masked)')
|
|
22
|
-
.action(() => {
|
|
23
|
-
const cfg = loadConfig();
|
|
24
|
-
const shown = {
|
|
25
|
-
...cfg,
|
|
26
|
-
api_key: cfg.api_key ? maskKey(cfg.api_key) : undefined,
|
|
27
|
-
llm: cfg.llm ? { ...cfg.llm, api_key: cfg.llm.api_key ? maskKey(cfg.llm.api_key) : undefined } : undefined,
|
|
28
|
-
};
|
|
29
|
-
const context = loadContext();
|
|
30
|
-
emitResult({ config: shown, context, context_file: findContextFile() }, JSON.stringify({ config: shown, context }, null, 2));
|
|
31
|
-
});
|
|
32
|
-
config
|
|
33
|
-
.command('set <key> <value>')
|
|
34
|
-
.description(`set a config value (${[...SETTABLE].join(', ')})`)
|
|
35
|
-
.action((key, value) => {
|
|
36
|
-
if (!SETTABLE.has(key)) {
|
|
37
|
-
throw new UsageError(`unknown config key "${key}" (valid: ${[...SETTABLE].join(', ')})`);
|
|
38
|
-
}
|
|
39
|
-
const cfg = loadConfig();
|
|
40
|
-
applyPath(cfg, key.split('.'), coerceValue(value));
|
|
41
|
-
saveConfig(cfg);
|
|
42
|
-
success(`${key} = ${key.includes('api_key') ? maskKey(value) : value}`);
|
|
43
|
-
});
|
|
44
|
-
config
|
|
45
|
-
.command('unset <key>')
|
|
46
|
-
.description('remove a config value')
|
|
47
|
-
.action((key) => {
|
|
48
|
-
const cfg = loadConfig();
|
|
49
|
-
const parts = key.split('.');
|
|
50
|
-
const parent = parts.slice(0, -1).reduce((acc, part) => (acc && typeof acc[part] === 'object' ? acc[part] : undefined), cfg);
|
|
51
|
-
if (parent)
|
|
52
|
-
delete parent[parts[parts.length - 1]];
|
|
53
|
-
saveConfig(cfg);
|
|
54
|
-
success(`${key} removed`);
|
|
55
|
-
});
|
|
56
|
-
config
|
|
57
|
-
.command('context [entries...]')
|
|
58
|
-
.description('set directory context: tripo config context default_scenario=game-mobile output_dir=./assets')
|
|
59
|
-
.action((entries) => {
|
|
60
|
-
if (entries.length === 0) {
|
|
61
|
-
const ctx = loadContext();
|
|
62
|
-
emitResult({ context: ctx, file: findContextFile() }, JSON.stringify(ctx, null, 2));
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
const patch = {};
|
|
66
|
-
for (const entry of entries) {
|
|
67
|
-
const eq = entry.indexOf('=');
|
|
68
|
-
if (eq <= 0)
|
|
69
|
-
throw new UsageError(`context entries are key=value (got "${entry}")`);
|
|
70
|
-
patch[entry.slice(0, eq)] = coerceValue(entry.slice(eq + 1));
|
|
71
|
-
}
|
|
72
|
-
updateContext(patch);
|
|
73
|
-
success(`context updated: ${Object.keys(patch).join(', ')}`);
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
function applyPath(obj, path, value) {
|
|
77
|
-
let node = obj;
|
|
78
|
-
for (const part of path.slice(0, -1)) {
|
|
79
|
-
if (typeof node[part] !== 'object' || node[part] === null)
|
|
80
|
-
node[part] = {};
|
|
81
|
-
node = node[part];
|
|
82
|
-
}
|
|
83
|
-
node[path[path.length - 1]] = value;
|
|
84
|
-
}
|