tripo-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -0
- package/dist/ai/experts.js +115 -0
- package/dist/ai/llm.js +88 -0
- package/dist/ai/plan-card.js +79 -0
- package/dist/ai/wizard.js +105 -0
- package/dist/cli.js +7 -0
- package/dist/commands/account.js +27 -0
- package/dist/commands/ai.js +114 -0
- package/dist/commands/batch.js +148 -0
- package/dist/commands/completion.js +84 -0
- package/dist/commands/config-cmd.js +84 -0
- package/dist/commands/docs.js +30 -0
- package/dist/commands/doctor.js +102 -0
- package/dist/commands/generate.js +81 -0
- package/dist/commands/home.js +68 -0
- package/dist/commands/login.js +309 -0
- package/dist/commands/make.js +142 -0
- package/dist/commands/mcp.js +194 -0
- package/dist/commands/process-groups.js +166 -0
- package/dist/commands/shared.js +75 -0
- package/dist/commands/task.js +128 -0
- package/dist/commands/topup.js +61 -0
- package/dist/commands/view.js +97 -0
- package/dist/config/config.js +99 -0
- package/dist/config/context.js +44 -0
- package/dist/config/history.js +104 -0
- package/dist/core/client.js +129 -0
- package/dist/core/device-auth.js +141 -0
- package/dist/core/download.js +107 -0
- package/dist/core/errors.js +64 -0
- package/dist/core/output.js +72 -0
- package/dist/core/region.js +45 -0
- package/dist/core/requests.js +197 -0
- package/dist/core/task-service.js +167 -0
- package/dist/core/upload.js +42 -0
- package/dist/core/viewer-assets.js +24 -0
- package/dist/core/viewer.js +59 -0
- package/dist/core/watch.js +42 -0
- package/dist/i18n.js +19 -0
- package/dist/knowledge/chains.js +157 -0
- package/dist/knowledge/error-catalog.js +91 -0
- package/dist/knowledge/models.js +158 -0
- package/dist/knowledge/params.js +203 -0
- package/dist/knowledge/scenarios.js +112 -0
- package/dist/pipeline/make.js +233 -0
- package/dist/pipeline/runner.js +73 -0
- package/dist/pipeline/stdin.js +40 -0
- package/dist/program.js +147 -0
- package/dist/src/ai/experts.js +115 -0
- package/dist/src/ai/llm.js +88 -0
- package/dist/src/ai/plan-card.js +79 -0
- package/dist/src/ai/wizard.js +104 -0
- package/dist/src/cli.js +7 -0
- package/dist/src/commands/account.js +27 -0
- package/dist/src/commands/ai.js +113 -0
- package/dist/src/commands/batch.js +148 -0
- package/dist/src/commands/completion.js +84 -0
- package/dist/src/commands/config-cmd.js +84 -0
- package/dist/src/commands/docs.js +30 -0
- package/dist/src/commands/doctor.js +94 -0
- package/dist/src/commands/generate.js +81 -0
- package/dist/src/commands/home.js +68 -0
- package/dist/src/commands/login.js +225 -0
- package/dist/src/commands/make.js +142 -0
- package/dist/src/commands/mcp.js +194 -0
- package/dist/src/commands/process-groups.js +164 -0
- package/dist/src/commands/shared.js +75 -0
- package/dist/src/commands/task.js +127 -0
- package/dist/src/commands/topup.js +61 -0
- package/dist/src/commands/view.js +97 -0
- package/dist/src/config/config.js +79 -0
- package/dist/src/config/context.js +44 -0
- package/dist/src/config/history.js +104 -0
- package/dist/src/core/client.js +120 -0
- package/dist/src/core/download.js +107 -0
- package/dist/src/core/errors.js +64 -0
- package/dist/src/core/output.js +72 -0
- package/dist/src/core/requests.js +169 -0
- package/dist/src/core/task-service.js +161 -0
- package/dist/src/core/upload.js +36 -0
- package/dist/src/core/viewer-assets.js +24 -0
- package/dist/src/core/viewer.js +59 -0
- package/dist/src/core/watch.js +41 -0
- package/dist/src/i18n.js +19 -0
- package/dist/src/knowledge/chains.js +152 -0
- package/dist/src/knowledge/error-catalog.js +91 -0
- package/dist/src/knowledge/models.js +110 -0
- package/dist/src/knowledge/params.js +174 -0
- package/dist/src/knowledge/scenarios.js +112 -0
- package/dist/src/pipeline/make.js +233 -0
- package/dist/src/pipeline/runner.js +73 -0
- package/dist/src/pipeline/stdin.js +40 -0
- package/dist/src/program.js +147 -0
- package/dist/src/types.js +2 -0
- package/dist/src/util/input-detect.js +92 -0
- package/dist/src/util/notify.js +17 -0
- package/dist/src/util/open.js +18 -0
- package/dist/src/util/spinner.js +62 -0
- package/dist/src/util/term-image.js +28 -0
- package/dist/src/version.js +10 -0
- package/dist/types.js +2 -0
- package/dist/util/input-detect.js +93 -0
- package/dist/util/notify.js +17 -0
- package/dist/util/open.js +18 -0
- package/dist/util/spinner.js +62 -0
- package/dist/util/term-image.js +28 -0
- package/dist/version.js +10 -0
- package/package.json +63 -0
- package/skill/SKILL.md +70 -0
- package/skill/commands/account.md +41 -0
- package/skill/commands/batch.md +33 -0
- package/skill/commands/generate.md +34 -0
- package/skill/commands/make.md +47 -0
- package/skill/commands/process.md +50 -0
- package/skill/commands/task.md +27 -0
- package/skill/commands/view.md +25 -0
- package/skill/common-errors.md +38 -0
- package/skill/examples/animation.md +33 -0
- package/skill/examples/ar-web.md +23 -0
- package/skill/examples/film.md +22 -0
- package/skill/examples/game-asset.md +35 -0
- package/skill/examples/pipes.md +39 -0
- package/skill/examples/print.md +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# tripo-cli
|
|
2
|
+
|
|
3
|
+
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
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx tripo-cli make "a cute low poly fox"
|
|
7
|
+
# → generates, polls, downloads: ./tripo-out/a-cute-low-poly-fox-xxxxxxxx/{model.glb, preview.png, task.json}
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The binary is `tripo` once installed:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g tripo-cli
|
|
14
|
+
tripo login # browser approves, key arrives automatically; or --paste / TRIPO_API_KEY
|
|
15
|
+
tripo make knight.png --for game-mobile --then texture,rig,convert:fbx
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Works with both Tripo regions from the same install — international (`openapi.tripo3d.ai`, console `developers.tripo3d.ai`, email login + Stripe) and China mainland (`openapi.tripo3d.com`, console `developers.tripo3d.com`, SMS login + Alipay). `tripo login` runs an RFC 8628 device flow: the terminal shows a short code, the browser signs in (each console's own method) and approves, and the key lands in the CLI automatically — with graceful fallback to open-the-keys-page-and-paste when the device-auth backend is unavailable. You never pick a region when a key is in hand: the key is probed against both regions and whichever accepts it wins; `tripo topup` then opens the matching billing page (`/billing` on ov, `/billing/recharge` on cn). If a config was moved between machines, `tripo doctor` detects the mismatch and prints the one-line fix.
|
|
19
|
+
|
|
20
|
+
## Why this CLI
|
|
21
|
+
|
|
22
|
+
- **One magic command.** `tripo make <anything>` detects what you gave it — prompt text, an image, 2–4 view images, a model file, a URL, a task id, or `@last` — picks the right endpoint, model, and parameters, then waits and downloads the artifacts. No file_token, no polling loops.
|
|
23
|
+
- **A 3D domain brain, not a thin API wrapper.** Model choice is a fixed rule (low-poly intent or face budget ≤ 20000 → `tripo-p1`, everything else → `tripo-v3.1`), P1's 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
|
+
- **Agent-native.** Non-TTY runs are automatically headless (`--json --yes --no-open`), stdout carries exactly one JSON line per command, progress goes to stderr, exit codes are stable, and NDJSON pipes compose: `tripo make cat.png | tripo model texture | tripo anim rig`. An LLM-facing skill package ships inside the npm package (`tripo docs --llm`).
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
| command | what it does |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `tripo make <input...>` | the magic command: generate → chain → download |
|
|
31
|
+
| `tripo ai [description]` | plan with a wizard (no LLM needed) or multi-expert conversation (BYO LLM), confirm a plan card, execute |
|
|
32
|
+
| `tripo view [task\|file]` | interactive 3D preview in your browser (local server, model-viewer) |
|
|
33
|
+
| `tripo redo [task]` | re-run with a fresh seed |
|
|
34
|
+
| `tripo login / logout / whoami` | device flow: browser approves, key arrives automatically (paste fallback); stored 0600 in `~/.tripo/config.json` |
|
|
35
|
+
| `tripo topup / balance / usage` | open the billing page + detect the credits arriving; check spend |
|
|
36
|
+
| `tripo generate <endpoint>` | deterministic access to all 8 generation endpoints |
|
|
37
|
+
| `tripo model / anim / mesh <step>` | refine, texture, stylize, convert, import, rig-check, rig, retarget, segment, complete, decimate, smartsegment |
|
|
38
|
+
| `tripo task get/list/watch` | query and stream task progress (NDJSON in `--json` mode) |
|
|
39
|
+
| `tripo files upload` | upload a file, print the `file_token` |
|
|
40
|
+
| `tripo batch run <manifest.yaml>` | bulk pipelines with concurrency, retries, and resume |
|
|
41
|
+
| `tripo config / doctor` | settings (global + per-directory `.tripo/context.json`) and self-diagnosis |
|
|
42
|
+
| `tripo docs [--topic t]` | print the bundled agent/skill docs |
|
|
43
|
+
| `tripo mcp` | run as an MCP server (Cursor / Claude Desktop) |
|
|
44
|
+
| `tripo completion <shell>` | bash/zsh/fish completion |
|
|
45
|
+
|
|
46
|
+
Every task-producing command supports `-o/--out`, `--no-wait`, `--no-download`, `--name`, `--timeout`, `--notify`, and repeatable `--param key=value` passthrough. Global flags: `--json`, `--yes`, `--quiet`, `--no-open`.
|
|
47
|
+
|
|
48
|
+
## Scenario presets
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
tripo make "sci-fi crate" --for game-mobile # P1 low-poly → texture → FBX
|
|
52
|
+
tripo make hero.png --for film # v3.1, quad topology, 4K PBR → USDZ/OBJ
|
|
53
|
+
tripo make "chess knight" --for print # no textures, watertight → STL, flat bottom
|
|
54
|
+
tripo make sofa.jpg --for ar-web # decimate to mobile budget → GLB + USDZ
|
|
55
|
+
tripo make "orc warrior" --for anim # rig-check gate → rig → FBX
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Pipelines
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# human shorthand
|
|
62
|
+
tripo make cat.png --then texture,rig,convert:fbx
|
|
63
|
+
|
|
64
|
+
# shell pipes (each stage reads the upstream task from stdin)
|
|
65
|
+
tripo make cat.png --json | tripo model texture --json | tripo anim rig --json
|
|
66
|
+
|
|
67
|
+
# batch with resume
|
|
68
|
+
tripo batch run assets.yaml --concurrency 2
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`@last`, `@2`, and `@name` reference your task history everywhere a task id is accepted (history lives in `~/.tripo/history.jsonl`; every artifact directory gets a reproducible `task.json`).
|
|
72
|
+
|
|
73
|
+
## For AI coding agents
|
|
74
|
+
|
|
75
|
+
- `make`/`watch` are blocking: run them, wait for exit, read the single JSON line on stdout. Do not poll yourself.
|
|
76
|
+
- Exit codes: `0` ok · `2` usage · `3` auth · `4` insufficient credits · `5` content policy · `6` task failed (credits auto-refunded) · `7` network · `8` not found · `9` rate limit.
|
|
77
|
+
- `preview.png` in every output directory is your eyes: look at it, then decide to `tripo redo` or continue the chain.
|
|
78
|
+
- Full behavior rules: `tripo docs --llm`, per-command docs via `tripo docs --topic commands/make`.
|
|
79
|
+
|
|
80
|
+
## Environment variables
|
|
81
|
+
|
|
82
|
+
| var | meaning |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `TRIPO_API_KEY` | API key (`tsk_...`), highest precedence |
|
|
85
|
+
| `TRIPO_REGION` | `ov` or `cn` — normally unnecessary: `tripo login` probes both regions with your key and stores the right one automatically |
|
|
86
|
+
| `TRIPO_API_BASE_URL` / `TRIPO_PLATFORM_BASE_URL` | endpoint overrides |
|
|
87
|
+
| `TRIPO_HOME` | config/history directory (default `~/.tripo`) |
|
|
88
|
+
| `TRIPO_LLM_BASE_URL` / `TRIPO_LLM_API_KEY` / `TRIPO_LLM_MODEL` | optional OpenAI-compatible LLM for `tripo ai` conversation mode |
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install
|
|
94
|
+
npm run dev -- make "a cat" --no-wait # run from source (tsx)
|
|
95
|
+
npm test # vitest (mock API, no credits spent)
|
|
96
|
+
npm run typecheck && npm run lint
|
|
97
|
+
npm run build && npm run pack:check # dist + tarball self-check
|
|
98
|
+
TRIPO_API_KEY=tsk_... npm run e2e # real-API smoke (spends ~10 credits)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Node.js ≥ 20. Issues & source: [vast-enterprise/Tripo-API-CLI](https://github.com/vast-enterprise/Tripo-API-CLI).
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-expert agent orchestration (conversation mode).
|
|
3
|
+
*
|
|
4
|
+
* Producer (router) → domain specialist(s) → plan card. The LLM only parses
|
|
5
|
+
* intent and phrases advice; every decision it may make is constrained to the
|
|
6
|
+
* knowledge base (scenario ids, step names, parameter windows), and the
|
|
7
|
+
* result is re-validated locally before anything is spent.
|
|
8
|
+
*/
|
|
9
|
+
import { getLocale, t, tList } from '../i18n.js';
|
|
10
|
+
import { parseThenExpression, validateChain, STEPS } from '../knowledge/chains.js';
|
|
11
|
+
import { chooseModel, detectLowPolyIntent, RIG_MODEL_V25 } from '../knowledge/models.js';
|
|
12
|
+
import { getScenario, SCENARIOS } from '../knowledge/scenarios.js';
|
|
13
|
+
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');
|
|
15
|
+
const STEP_TABLE = STEPS.map((s) => s.name).join(', ');
|
|
16
|
+
const PRODUCER_SYSTEM = `You are the producer/router of the Tripo 3D generation CLI.
|
|
17
|
+
Classify the user's request into exactly one scenario and extract a clean English 3D generation prompt.
|
|
18
|
+
|
|
19
|
+
Scenarios:
|
|
20
|
+
${SCENARIO_TABLE}
|
|
21
|
+
|
|
22
|
+
Rules:
|
|
23
|
+
- "prompt" must describe the object to generate (translate to English, keep it concrete, <= 200 chars).
|
|
24
|
+
- Set "face_limit" only when the user states a face/polygon budget.
|
|
25
|
+
- 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.
|
|
27
|
+
Respond with JSON only: {"scenario","prompt","face_limit","needs_animation","question","notes"}`;
|
|
28
|
+
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.`,
|
|
31
|
+
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 scale_factor/auto_size.`,
|
|
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 (v2.5 rig, spec tripo|mixamo) → retarget (<=5 preset animations, billed per animation). Mixamo/Unity Humanoid → spec=mixamo.`,
|
|
35
|
+
toy: `You are a stylized collectible designer. P1 low-poly or stylize steps (lego/voxel/voronoi/minecraft). STL via convert for physical prints.`,
|
|
36
|
+
};
|
|
37
|
+
function specialistSystem(scenarioId) {
|
|
38
|
+
const persona = SPECIALIST_PROMPTS[scenarioId] ?? SPECIALIST_PROMPTS['game-mobile'];
|
|
39
|
+
return `${persona}
|
|
40
|
+
You fine-tune a Tripo generation plan. You may adjust generation params and the processing chain.
|
|
41
|
+
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.`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Run the expert pipeline. `askUser` is called when the producer needs one
|
|
47
|
+
* clarification (interactive mode only); return undefined to skip.
|
|
48
|
+
*/
|
|
49
|
+
export async function buildExpertPlan(request, askUser) {
|
|
50
|
+
const messages = [
|
|
51
|
+
{ role: 'system', content: PRODUCER_SYSTEM },
|
|
52
|
+
{ role: 'user', content: request },
|
|
53
|
+
];
|
|
54
|
+
let producer = await chatJson(messages);
|
|
55
|
+
if (producer.question && askUser) {
|
|
56
|
+
const answer = await askUser(producer.question);
|
|
57
|
+
if (answer) {
|
|
58
|
+
messages.push({ role: 'assistant', content: JSON.stringify(producer) });
|
|
59
|
+
messages.push({ role: 'user', content: `Additional info: ${answer}. Re-classify with the complete picture; do not ask again.` });
|
|
60
|
+
producer = await chatJson(messages);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const scenario = getScenario(producer.scenario) ?? getScenario('game-mobile');
|
|
64
|
+
const specialist = await chatJson([
|
|
65
|
+
{ role: 'system', content: specialistSystem(scenario.id) },
|
|
66
|
+
{
|
|
67
|
+
role: 'user',
|
|
68
|
+
content: `User request: ${request}\nPrompt: ${producer.prompt}\nBase plan: model=${scenario.model} params=${JSON.stringify(scenario.genParams)} chain=${scenario.chain.map((c) => c.step).join('→') || 'none'}\nneeds_animation=${producer.needs_animation ?? false} face_limit=${producer.face_limit ?? 'default'}`,
|
|
69
|
+
},
|
|
70
|
+
]).catch(() => ({}));
|
|
71
|
+
// 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;
|
|
75
|
+
let chain = scenario.chain;
|
|
76
|
+
if (specialist.chain) {
|
|
77
|
+
try {
|
|
78
|
+
chain = parseThenExpression(specialist.chain);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
chain = scenario.chain; // discard invalid expert chain, keep the preset
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (producer.needs_animation && !chain.some((s) => s.step === 'rig')) {
|
|
85
|
+
chain = [
|
|
86
|
+
...chain.filter((s) => s.step !== 'convert'),
|
|
87
|
+
{ step: 'rig-check', args: {} },
|
|
88
|
+
{ step: 'rig', args: { model: RIG_MODEL_V25 } },
|
|
89
|
+
{ step: 'retarget', args: { animations: ['preset:idle', 'preset:walk'] } },
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
const choice = chooseModel({
|
|
93
|
+
explicit: scenario.model,
|
|
94
|
+
faceLimit: typeof params.face_limit === 'number' ? params.face_limit : undefined,
|
|
95
|
+
lowPoly: detectLowPolyIntent(request),
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
validateChain(chain, 'text_to_model');
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
chain = scenario.chain;
|
|
102
|
+
}
|
|
103
|
+
const card = {
|
|
104
|
+
inputSummary: producer.prompt,
|
|
105
|
+
scenario: scenario.id,
|
|
106
|
+
model: choice.model,
|
|
107
|
+
modelReason: t({ zh: '制片人路由 + 场景预设', en: 'producer routing + scenario preset' }),
|
|
108
|
+
genParams: params,
|
|
109
|
+
chain,
|
|
110
|
+
deliverables: scenario.deliverables,
|
|
111
|
+
notes: tList(scenario.notes),
|
|
112
|
+
expertNotes: [...(producer.notes ?? []), ...(specialist.notes ?? [])],
|
|
113
|
+
};
|
|
114
|
+
return { card, scenarioId: scenario.id, prompt: producer.prompt, chain, params };
|
|
115
|
+
}
|
package/dist/ai/llm.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BYO-LLM client: any OpenAI-compatible /chat/completions endpoint.
|
|
3
|
+
* Configure via `tripo config set llm.base_url/llm.api_key/llm.model`
|
|
4
|
+
* or env TRIPO_LLM_BASE_URL / TRIPO_LLM_API_KEY / TRIPO_LLM_MODEL.
|
|
5
|
+
*/
|
|
6
|
+
import { loadConfig } from '../config/config.js';
|
|
7
|
+
import { CliError } from '../core/errors.js';
|
|
8
|
+
import { EXIT } from '../knowledge/error-catalog.js';
|
|
9
|
+
export function resolveLlmConfig() {
|
|
10
|
+
const env = {
|
|
11
|
+
base_url: process.env.TRIPO_LLM_BASE_URL,
|
|
12
|
+
api_key: process.env.TRIPO_LLM_API_KEY,
|
|
13
|
+
model: process.env.TRIPO_LLM_MODEL,
|
|
14
|
+
};
|
|
15
|
+
if (env.base_url && env.model)
|
|
16
|
+
return { base_url: env.base_url, api_key: env.api_key, model: env.model };
|
|
17
|
+
const config = loadConfig().llm;
|
|
18
|
+
if (config?.base_url && config.model)
|
|
19
|
+
return config;
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
export async function chatCompletion(messages, options = {}) {
|
|
23
|
+
const config = resolveLlmConfig();
|
|
24
|
+
if (!config)
|
|
25
|
+
throw new CliError('no LLM configured', EXIT.USAGE);
|
|
26
|
+
const url = `${config.base_url.replace(/\/$/, '')}/chat/completions`;
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const timer = setTimeout(() => controller.abort(), 60_000);
|
|
29
|
+
let response;
|
|
30
|
+
try {
|
|
31
|
+
response = await fetch(url, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
...(config.api_key ? { Authorization: `Bearer ${config.api_key}` } : {}),
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
model: config.model,
|
|
39
|
+
messages,
|
|
40
|
+
temperature: 0.2,
|
|
41
|
+
...(options.json ? { response_format: { type: 'json_object' } } : {}),
|
|
42
|
+
}),
|
|
43
|
+
signal: controller.signal,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
throw new CliError(`LLM request failed: ${err instanceof Error ? err.message : String(err)}`, EXIT.NETWORK);
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
}
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
const body = await response.text().catch(() => '');
|
|
54
|
+
throw new CliError(`LLM endpoint returned HTTP ${response.status}: ${body.slice(0, 300)}`, EXIT.NETWORK);
|
|
55
|
+
}
|
|
56
|
+
const data = (await response.json());
|
|
57
|
+
const content = data.choices?.[0]?.message?.content;
|
|
58
|
+
if (!content)
|
|
59
|
+
throw new CliError('LLM returned an empty response', EXIT.NETWORK);
|
|
60
|
+
return content;
|
|
61
|
+
}
|
|
62
|
+
/** Chat expecting a JSON object back; tolerates markdown fences. */
|
|
63
|
+
export async function chatJson(messages) {
|
|
64
|
+
const raw = await chatCompletion(messages, { json: true }).catch(async (err) => {
|
|
65
|
+
// Some endpoints reject response_format; retry without it.
|
|
66
|
+
if (err instanceof CliError && err.exitCode === EXIT.NETWORK) {
|
|
67
|
+
return chatCompletion(messages, { json: false });
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
});
|
|
71
|
+
return extractJson(raw);
|
|
72
|
+
}
|
|
73
|
+
export function extractJson(raw) {
|
|
74
|
+
const trimmed = raw.trim();
|
|
75
|
+
const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(trimmed);
|
|
76
|
+
const candidate = fenced ? fenced[1] : trimmed;
|
|
77
|
+
const start = candidate.indexOf('{');
|
|
78
|
+
const end = candidate.lastIndexOf('}');
|
|
79
|
+
if (start === -1 || end === -1 || end < start) {
|
|
80
|
+
throw new CliError(`LLM did not return JSON: ${raw.slice(0, 200)}`, EXIT.NETWORK);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(candidate.slice(start, end + 1));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new CliError(`LLM returned invalid JSON: ${candidate.slice(0, 200)}`, EXIT.NETWORK);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
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 { RIG_MODEL_V25 } from '../knowledge/models.js';
|
|
12
|
+
import { getScenario, SCENARIOS } from '../knowledge/scenarios.js';
|
|
13
|
+
export async function runWizard(initialPrompt) {
|
|
14
|
+
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
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
p.intro('tripo ai');
|
|
21
|
+
let prompt = initialPrompt?.trim() ?? '';
|
|
22
|
+
if (!prompt) {
|
|
23
|
+
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' })),
|
|
26
|
+
});
|
|
27
|
+
if (p.isCancel(answer))
|
|
28
|
+
throw new CliError('cancelled', EXIT.FATAL);
|
|
29
|
+
prompt = answer.trim();
|
|
30
|
+
}
|
|
31
|
+
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)}` })),
|
|
34
|
+
});
|
|
35
|
+
if (p.isCancel(scenarioAnswer))
|
|
36
|
+
throw new CliError('cancelled', EXIT.FATAL);
|
|
37
|
+
const scenario = getScenario(scenarioAnswer);
|
|
38
|
+
let engine;
|
|
39
|
+
if (scenarioAnswer === 'game-mobile' || scenarioAnswer === 'game-pc') {
|
|
40
|
+
const engineAnswer = await p.select({
|
|
41
|
+
message: t({ zh: '目标引擎/软件?', en: 'Target engine/software?' }),
|
|
42
|
+
options: [
|
|
43
|
+
{ value: 'unity', label: 'Unity' },
|
|
44
|
+
{ value: 'unreal', label: 'Unreal' },
|
|
45
|
+
{ value: 'blender', label: 'Blender' },
|
|
46
|
+
{ value: 'any', label: t({ zh: '不确定/通用', en: 'not sure / generic' }) },
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
if (p.isCancel(engineAnswer))
|
|
50
|
+
throw new CliError('cancelled', EXIT.FATAL);
|
|
51
|
+
engine = engineAnswer;
|
|
52
|
+
}
|
|
53
|
+
let animations = false;
|
|
54
|
+
if (scenarioAnswer !== 'print' && scenarioAnswer !== 'film') {
|
|
55
|
+
const animAnswer = await p.confirm({
|
|
56
|
+
message: t({ zh: '需要骨骼/动画吗?', en: 'Do you need a skeleton / animations?' }),
|
|
57
|
+
initialValue: scenarioAnswer === 'anim',
|
|
58
|
+
});
|
|
59
|
+
if (p.isCancel(animAnswer))
|
|
60
|
+
throw new CliError('cancelled', EXIT.FATAL);
|
|
61
|
+
animations = animAnswer;
|
|
62
|
+
}
|
|
63
|
+
const { chain, notes } = adjustChain(scenario.id, engine, animations);
|
|
64
|
+
const card = {
|
|
65
|
+
inputSummary: prompt,
|
|
66
|
+
scenario: scenario.id,
|
|
67
|
+
model: scenario.model,
|
|
68
|
+
modelReason: t({ zh: '场景预设', en: 'scenario preset' }),
|
|
69
|
+
genParams: scenario.genParams,
|
|
70
|
+
chain,
|
|
71
|
+
deliverables: scenario.deliverables,
|
|
72
|
+
notes: [...tList(scenario.notes), ...notes],
|
|
73
|
+
};
|
|
74
|
+
return { card, options: { prompt, scenario: scenario.id, chain } };
|
|
75
|
+
}
|
|
76
|
+
/** Engine/animation adjustments on top of the scenario chain. */
|
|
77
|
+
export function adjustChain(scenarioId, engine, animations) {
|
|
78
|
+
const scenario = getScenario(scenarioId);
|
|
79
|
+
let chain = scenario.chain.map((s) => ({ step: s.step, args: { ...s.args } }));
|
|
80
|
+
const notes = [];
|
|
81
|
+
if (animations && !chain.some((s) => s.step === 'rig')) {
|
|
82
|
+
const rigArgs = { model: RIG_MODEL_V25, out_format: 'fbx' };
|
|
83
|
+
if (engine === 'unity') {
|
|
84
|
+
rigArgs.spec = 'mixamo';
|
|
85
|
+
notes.push(t({ zh: 'Unity Humanoid → spec=mixamo', en: 'Unity Humanoid → spec=mixamo' }));
|
|
86
|
+
}
|
|
87
|
+
chain = [
|
|
88
|
+
...chain.filter((s) => s.step !== 'convert'),
|
|
89
|
+
{ step: 'rig-check', args: {} },
|
|
90
|
+
{ step: 'rig', args: rigArgs },
|
|
91
|
+
{ step: 'retarget', args: { animations: ['preset:idle', 'preset:walk'], out_format: 'fbx' } },
|
|
92
|
+
];
|
|
93
|
+
notes.push(t({ zh: '动画链输出 FBX(骨骼+动画)', en: 'animation chain outputs FBX (skeleton + animations)' }));
|
|
94
|
+
}
|
|
95
|
+
if (engine && !animations) {
|
|
96
|
+
const convert = chain.find((s) => s.step === 'convert');
|
|
97
|
+
if (convert && convert.args.format === 'FBX') {
|
|
98
|
+
if (engine === 'blender')
|
|
99
|
+
convert.args.fbx_preset = 'blender';
|
|
100
|
+
if (engine === 'unreal' || engine === 'unity')
|
|
101
|
+
convert.args.export_orientation = '+x';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { chain, notes };
|
|
105
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
// The wizard is interactive by nature — never enter it headless.
|
|
61
|
+
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);
|
|
66
|
+
}
|
|
67
|
+
const wizard = await runWizard(text || undefined);
|
|
68
|
+
card = wizard.card;
|
|
69
|
+
makeOptions = wizardToOptions(wizard.options, flags);
|
|
70
|
+
}
|
|
71
|
+
process.stderr.write(renderPlanCard(card) + '\n');
|
|
72
|
+
if (!isYes()) {
|
|
73
|
+
const go = await p.confirm({ message: t({ zh: '按方案执行?', en: 'Run this plan?' }) });
|
|
74
|
+
if (p.isCancel(go) || !go) {
|
|
75
|
+
log(t({ zh: '已取消,未消耗积分', en: 'cancelled; no credits spent' }));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const client = newClient();
|
|
80
|
+
const plan = await planMake(client, makeOptions);
|
|
81
|
+
const outcome = await executeMake(client, plan, makeOptions);
|
|
82
|
+
emitMakeOutcome({ results: [outcome.primary], chainResults: outcome.chainResults, stoppedEarly: outcome.stoppedEarly });
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function wizardToOptions(options, flags) {
|
|
86
|
+
return {
|
|
87
|
+
inputs: [options.prompt],
|
|
88
|
+
scenario: options.scenario,
|
|
89
|
+
chain: options.chain,
|
|
90
|
+
params: flags.param,
|
|
91
|
+
name: flags.name,
|
|
92
|
+
outDir: flags.out,
|
|
93
|
+
notify: flags.notify,
|
|
94
|
+
timeoutMs: flags.timeout ? flags.timeout * 1000 : undefined,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function askUser(question) {
|
|
98
|
+
const answer = await p.text({ message: question });
|
|
99
|
+
if (p.isCancel(answer))
|
|
100
|
+
return undefined;
|
|
101
|
+
return answer;
|
|
102
|
+
}
|
|
103
|
+
/** Replace @task-name references in a prompt with their task ids. */
|
|
104
|
+
export function expandHistoryRefs(text) {
|
|
105
|
+
return text.replace(/@[\w\u4e00-\u9fff-]+/g, (ref) => {
|
|
106
|
+
try {
|
|
107
|
+
const entry = resolveReference(ref);
|
|
108
|
+
return entry ? entry.task_id : ref;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return ref; // ambiguity is reported when the value is actually used
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|