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.
- package/CHANGELOG.md +29 -0
- package/README.md +16 -14
- package/dist/ai/experts.js +26 -20
- package/dist/ai/plan-card.js +73 -34
- package/dist/ai/wizard.js +19 -19
- package/dist/commands/account.js +2 -6
- package/dist/commands/ai.js +18 -16
- package/dist/commands/batch.js +58 -18
- package/dist/commands/completion.js +1 -1
- package/dist/commands/config-cmd.js +1 -1
- package/dist/commands/doctor.js +1 -3
- package/dist/commands/generate.js +50 -8
- package/dist/commands/home.js +12 -13
- package/dist/commands/login.js +125 -182
- package/dist/commands/make.js +50 -25
- package/dist/commands/mcp.js +8 -3
- package/dist/commands/process-groups.js +29 -18
- package/dist/commands/shared.js +22 -13
- package/dist/commands/task.js +12 -11
- package/dist/commands/topup.js +6 -10
- package/dist/commands/update.js +23 -11
- package/dist/commands/view.js +3 -4
- package/dist/config/config.js +1 -1
- package/dist/core/analytics.js +39 -16
- package/dist/core/client.js +53 -21
- package/dist/core/device-auth.js +49 -1
- package/dist/core/download.js +2 -1
- package/dist/core/errors.js +24 -14
- package/dist/core/output.js +0 -4
- package/dist/core/requests.js +94 -31
- package/dist/core/task-service.js +21 -20
- package/dist/core/upload.js +23 -5
- package/dist/core/watch.js +3 -0
- package/dist/knowledge/chains.js +16 -17
- package/dist/knowledge/error-catalog.js +39 -37
- package/dist/knowledge/models.js +147 -27
- package/dist/knowledge/params.js +143 -32
- package/dist/knowledge/scenarios.js +32 -55
- package/dist/pipeline/make.js +38 -30
- package/dist/pipeline/runner.js +27 -8
- package/dist/pipeline/stdin.js +40 -3
- package/dist/program.js +3 -10
- package/dist/util/input-detect.js +10 -1
- package/package.json +3 -2
- package/skill/SKILL.md +52 -15
- package/skill/commands/account.md +8 -3
- package/skill/commands/batch.md +13 -3
- package/skill/commands/generate.md +57 -23
- package/skill/commands/make.md +43 -16
- package/skill/commands/process.md +29 -24
- package/skill/commands/task.md +4 -4
- package/skill/commands/view.md +1 -1
- package/skill/common-errors.md +14 -10
- package/skill/examples/animation.md +4 -2
- package/skill/examples/ar-web.md +6 -4
- package/skill/examples/game-asset.md +18 -7
- package/skill/examples/pipes.md +2 -2
- package/skill/examples/print.md +7 -3
- package/dist/i18n.js +0 -19
package/dist/commands/login.js
CHANGED
|
@@ -12,11 +12,10 @@ import * as p from '@clack/prompts';
|
|
|
12
12
|
import { activeProfileName, DEFAULT_PROFILE, deleteProfile, describeKeySource, isValidProfileName, listProfiles, loadConfig, maskKey, REGION_DEFAULTS, resolveApiKey, resolvePlatformBaseUrl, resolveRegion, setActiveProfile, updateProfile, } from '../config/config.js';
|
|
13
13
|
import { TripoClient } from '../core/client.js';
|
|
14
14
|
import { trackEvent } from '../core/analytics.js';
|
|
15
|
-
import { DeviceAuthUnavailableError, requestDeviceCode, waitForDeviceApproval, } from '../core/device-auth.js';
|
|
15
|
+
import { checkDeviceApproval, clearPendingGrant, DeviceAuthUnavailableError, loadPendingGrant, requestDeviceCode, savePendingGrant, waitForDeviceApproval, } from '../core/device-auth.js';
|
|
16
16
|
import { AuthError, CliError, NetworkError, UsageError } from '../core/errors.js';
|
|
17
17
|
import { detectRegionForKey } from '../core/region.js';
|
|
18
18
|
import { emitResult, isInteractive, log, shouldOpen, success, warn } from '../core/output.js';
|
|
19
|
-
import { t } from '../i18n.js';
|
|
20
19
|
import { EXIT } from '../knowledge/error-catalog.js';
|
|
21
20
|
import { openExternal } from '../util/open.js';
|
|
22
21
|
import { createSpinner } from '../util/spinner.js';
|
|
@@ -29,6 +28,7 @@ export function registerAuthCommands(program) {
|
|
|
29
28
|
.option('--key <key>', 'provide the API key directly (tsk_...); region is auto-detected')
|
|
30
29
|
.option('--region <region>', 'ov or cn — needed for the browser flow; required for non-interactive login')
|
|
31
30
|
.option('--profile <name>', 'save into a named profile (for a second account); becomes the active profile')
|
|
31
|
+
.option('--no-wait', 'print the verification URL + code and exit at once; rerun the same command after approval to finish')
|
|
32
32
|
.action(async (opts) => {
|
|
33
33
|
await runLogin(opts);
|
|
34
34
|
});
|
|
@@ -39,22 +39,16 @@ export function registerAuthCommands(program) {
|
|
|
39
39
|
.action((opts) => {
|
|
40
40
|
const name = opts.profile ?? activeProfileName();
|
|
41
41
|
if (!deleteProfile(name)) {
|
|
42
|
-
log(
|
|
42
|
+
log(`no API key stored locally for profile "${name}"`);
|
|
43
43
|
}
|
|
44
44
|
else {
|
|
45
|
-
success(
|
|
45
|
+
success(`local API key removed (profile "${name}")`);
|
|
46
46
|
const rest = listProfiles();
|
|
47
47
|
if (rest.length > 0) {
|
|
48
|
-
log(
|
|
49
|
-
zh: `其余账号仍在: ${rest.map((x) => x.name).join(', ')}(tripo use 切换)`,
|
|
50
|
-
en: `remaining profiles: ${rest.map((x) => x.name).join(', ')} (switch with "tripo use")`,
|
|
51
|
-
}));
|
|
48
|
+
log(`remaining profiles: ${rest.map((x) => x.name).join(', ')} (switch with "tripo use")`);
|
|
52
49
|
}
|
|
53
50
|
}
|
|
54
|
-
log(
|
|
55
|
-
zh: `如需吊销 Key 本身,请前往 ${resolvePlatformBaseUrl()} 的 API Keys 页面`,
|
|
56
|
-
en: `to revoke the key itself, visit the API Keys page at ${resolvePlatformBaseUrl()}`,
|
|
57
|
-
}));
|
|
51
|
+
log(`to revoke the key itself, visit the API Keys page at ${resolvePlatformBaseUrl()}`);
|
|
58
52
|
});
|
|
59
53
|
program
|
|
60
54
|
.command('whoami')
|
|
@@ -73,13 +67,10 @@ export function registerAuthCommands(program) {
|
|
|
73
67
|
`key ${maskKey(key)} (${source})`,
|
|
74
68
|
`region ${region}`,
|
|
75
69
|
`api ${client.baseUrl}`,
|
|
76
|
-
|
|
70
|
+
`balance ${balance.balance} credits (${balance.frozen} frozen)`,
|
|
77
71
|
];
|
|
78
72
|
if (others.length > 0) {
|
|
79
|
-
lines.push(
|
|
80
|
-
zh: `其它账号 ${others.map((x) => profileLabel(x.name, x.profile.region)).join(', ')}(tripo use 切换)`,
|
|
81
|
-
en: `others ${others.map((x) => profileLabel(x.name, x.profile.region)).join(', ')} (switch with "tripo use")`,
|
|
82
|
-
}));
|
|
73
|
+
lines.push(`others ${others.map((x) => profileLabel(x.name, x.profile.region)).join(', ')} (switch with "tripo use")`);
|
|
83
74
|
}
|
|
84
75
|
emitResult({
|
|
85
76
|
key: maskKey(key),
|
|
@@ -105,21 +96,18 @@ function profileLabel(name, region) {
|
|
|
105
96
|
async function runUse(name) {
|
|
106
97
|
const profiles = listProfiles();
|
|
107
98
|
if (profiles.length === 0) {
|
|
108
|
-
throw new AuthError(
|
|
99
|
+
throw new AuthError('no profiles yet', 'run "tripo login" first');
|
|
109
100
|
}
|
|
110
101
|
if (!name) {
|
|
111
102
|
if (!isInteractive()) {
|
|
112
|
-
throw new UsageError(
|
|
113
|
-
zh: `非交互环境请指定名字: tripo use <${profiles.map((x) => x.name).join('|')}>`,
|
|
114
|
-
en: `specify a profile in non-interactive mode: tripo use <${profiles.map((x) => x.name).join('|')}>`,
|
|
115
|
-
}));
|
|
103
|
+
throw new UsageError(`specify a profile in non-interactive mode: tripo use <${profiles.map((x) => x.name).join('|')}>`);
|
|
116
104
|
}
|
|
117
105
|
const answer = await p.select({
|
|
118
|
-
message:
|
|
106
|
+
message: 'Switch to which account?',
|
|
119
107
|
initialValue: profiles.find((x) => x.active)?.name,
|
|
120
108
|
options: profiles.map((x) => ({
|
|
121
109
|
value: x.name,
|
|
122
|
-
label: profileLabel(x.name, x.profile.region) + (x.active ?
|
|
110
|
+
label: profileLabel(x.name, x.profile.region) + (x.active ? ' (current)' : ''),
|
|
123
111
|
hint: x.profile.api_key ? maskKey(x.profile.api_key) : undefined,
|
|
124
112
|
})),
|
|
125
113
|
});
|
|
@@ -129,21 +117,12 @@ async function runUse(name) {
|
|
|
129
117
|
}
|
|
130
118
|
const match = profiles.find((x) => x.name === name);
|
|
131
119
|
if (!match) {
|
|
132
|
-
throw new UsageError(
|
|
133
|
-
zh: `没有叫「${name}」的 profile(现有: ${profiles.map((x) => x.name).join(', ')})`,
|
|
134
|
-
en: `no profile named "${name}" (have: ${profiles.map((x) => x.name).join(', ')})`,
|
|
135
|
-
}), t({ zh: `新账号用 tripo login --profile ${name} 登录`, en: `log a new account in with: tripo login --profile ${name}` }));
|
|
120
|
+
throw new UsageError(`no profile named "${name}" (have: ${profiles.map((x) => x.name).join(', ')})`, `log a new account in with: tripo login --profile ${name}`);
|
|
136
121
|
}
|
|
137
122
|
setActiveProfile(name);
|
|
138
|
-
success(
|
|
139
|
-
zh: `已切换到「${name}」${match.profile.region ? `(region=${match.profile.region})` : ''}`,
|
|
140
|
-
en: `switched to "${name}"${match.profile.region ? ` (region=${match.profile.region})` : ''}`,
|
|
141
|
-
}));
|
|
123
|
+
success(`switched to "${name}"${match.profile.region ? ` (region=${match.profile.region})` : ''}`);
|
|
142
124
|
if (process.env.TRIPO_API_KEY) {
|
|
143
|
-
warn(
|
|
144
|
-
zh: '注意:环境变量 TRIPO_API_KEY 仍然优先于 profile,如需使用 profile 请先 unset',
|
|
145
|
-
en: 'note: TRIPO_API_KEY env var still overrides the profile — unset it to use the profile key',
|
|
146
|
-
}));
|
|
125
|
+
warn('note: TRIPO_API_KEY env var still overrides the profile — unset it to use the profile key');
|
|
147
126
|
}
|
|
148
127
|
}
|
|
149
128
|
export async function runLogin(opts) {
|
|
@@ -152,25 +131,19 @@ export async function runLogin(opts) {
|
|
|
152
131
|
// re-authenticates work, it does not silently overwrite default.
|
|
153
132
|
const profile = opts.profile ?? activeProfileName();
|
|
154
133
|
if (!isValidProfileName(profile)) {
|
|
155
|
-
throw new UsageError(
|
|
156
|
-
zh: `profile 名「${profile}」不合法(小写字母/数字开头,可含 - _,最长 32)`,
|
|
157
|
-
en: `invalid profile name "${profile}" (lowercase alphanumeric start, may contain - _, max 32 chars)`,
|
|
158
|
-
}));
|
|
134
|
+
throw new UsageError(`invalid profile name "${profile}" (lowercase alphanumeric start, may contain - _, max 32 chars)`);
|
|
159
135
|
}
|
|
160
136
|
const replacing = loadConfig().profiles?.[profile]?.api_key;
|
|
161
137
|
if (replacing) {
|
|
162
|
-
log(
|
|
163
|
-
zh: `将替换 profile「${profile}」现有的 Key ${maskKey(replacing)}(新账号请用 tripo login --profile 名字)`,
|
|
164
|
-
en: `this will replace the key ${maskKey(replacing)} in profile "${profile}" (for a second account use: tripo login --profile <name>)`,
|
|
165
|
-
}));
|
|
138
|
+
log(`this will replace the key ${maskKey(replacing)} in profile "${profile}" (for a second account use: tripo login --profile <name>)`);
|
|
166
139
|
}
|
|
167
140
|
if (opts.key) {
|
|
168
141
|
// Key in hand → region is detected, not asked (a key lives in exactly one region).
|
|
169
142
|
await validateAndSave(opts.key, region, profile);
|
|
170
143
|
return;
|
|
171
144
|
}
|
|
172
|
-
if (!isInteractive()) {
|
|
173
|
-
await runHeadlessLogin(region, profile, opts.paste);
|
|
145
|
+
if (!isInteractive() || opts.wait === false) {
|
|
146
|
+
await runHeadlessLogin(region, profile, opts.paste, opts.wait === false);
|
|
174
147
|
return;
|
|
175
148
|
}
|
|
176
149
|
p.intro('tripo login');
|
|
@@ -182,14 +155,11 @@ export async function runLogin(opts) {
|
|
|
182
155
|
// key's real region wins.
|
|
183
156
|
if (!region) {
|
|
184
157
|
const answer = await p.select({
|
|
185
|
-
message:
|
|
186
|
-
zh: '你的账号在哪个站?(选错没关系,验证 Key 时会自动纠正)',
|
|
187
|
-
en: 'Where is your account? (a wrong pick self-corrects once the key is verified)',
|
|
188
|
-
}),
|
|
158
|
+
message: 'Where is your account? (a wrong pick self-corrects once the key is verified)',
|
|
189
159
|
initialValue: guessRegion(),
|
|
190
160
|
options: [
|
|
191
|
-
{ value: 'ov', label:
|
|
192
|
-
{ value: 'cn', label:
|
|
161
|
+
{ value: 'ov', label: 'International — developers.tripo3d.ai (Stripe)' },
|
|
162
|
+
{ value: 'cn', label: 'China mainland — developers.tripo3d.com (Alipay)' },
|
|
193
163
|
],
|
|
194
164
|
});
|
|
195
165
|
if (p.isCancel(answer))
|
|
@@ -198,10 +168,7 @@ export async function runLogin(opts) {
|
|
|
198
168
|
}
|
|
199
169
|
const key = opts.paste ? await keyFromConsole(region, false) : await deviceFlowWithFallback(region, profile);
|
|
200
170
|
await validateAndSave(key, region, profile);
|
|
201
|
-
p.outro(
|
|
202
|
-
zh: '登录完成!试试: tripo make "一只可爱的机器人"',
|
|
203
|
-
en: 'logged in! try: tripo make "a cute robot"',
|
|
204
|
-
}));
|
|
171
|
+
p.outro('logged in! try: tripo make "a cute robot"');
|
|
205
172
|
}
|
|
206
173
|
/**
|
|
207
174
|
* Non-interactive login (agents, CI without a provisioned key): the same
|
|
@@ -211,77 +178,97 @@ export async function runLogin(opts) {
|
|
|
211
178
|
* path are impossible here, so --region must be explicit (there is no key
|
|
212
179
|
* yet to probe, and the two regions have different console hosts) and
|
|
213
180
|
* --paste is rejected outright.
|
|
181
|
+
*
|
|
182
|
+
* A rerun resumes the parked grant instead of issuing a new code, so a
|
|
183
|
+
* killed poll never strands the human's approval. --no-wait exits right after
|
|
184
|
+
* printing (chat agents relay the code by ending their turn); rerunning it
|
|
185
|
+
* checks once and finishes the login when the human has approved.
|
|
214
186
|
*/
|
|
215
|
-
async function runHeadlessLogin(region, profile, paste) {
|
|
187
|
+
async function runHeadlessLogin(region, profile, paste, noWait) {
|
|
216
188
|
if (paste) {
|
|
217
|
-
throw new UsageError(
|
|
218
|
-
zh: '--paste 需要交互终端;非交互环境请用设备码登录(tripo login --region ov|cn)或 TRIPO_API_KEY / --key',
|
|
219
|
-
en: '--paste needs an interactive terminal — use device login ("tripo login --region ov|cn"), TRIPO_API_KEY, or --key instead',
|
|
220
|
-
}));
|
|
189
|
+
throw new UsageError('--paste needs an interactive terminal — use device login ("tripo login --region ov|cn"), TRIPO_API_KEY, or --key instead');
|
|
221
190
|
}
|
|
222
191
|
if (!region) {
|
|
223
|
-
throw new UsageError(
|
|
224
|
-
zh: `非交互登录需要 --region:先问用户账号在哪个站,不要猜(授权只在该站有效)— cn = ${siteLabel('cn')};ov = ${siteLabel('ov')};还没有账号:中国大陆选 cn,其它地区选 ov`,
|
|
225
|
-
en: `non-interactive login needs --region: ask the human which site their account is on, do not guess (authorization only works on that site) — cn = ${siteLabel('cn')}; ov = ${siteLabel('ov')}; no account yet: mainland China → cn, elsewhere → ov`,
|
|
226
|
-
}), t({
|
|
227
|
-
zh: 'tripo login --region cn|ov;已有 Key 时用 tripo login --key tsk_...(自动识别站点)',
|
|
228
|
-
en: '"tripo login --region cn|ov" — or, with a key in hand: "tripo login --key tsk_..." (site auto-detected)',
|
|
229
|
-
}));
|
|
192
|
+
throw new UsageError(`non-interactive login needs --region: ask the human which site their account is on, do not guess (authorization only works on that site) — cn = ${siteLabel('cn')}; ov = ${siteLabel('ov')}; no account yet: mainland China → cn, elsewhere → ov`, '"tripo login --region cn|ov" — or, with a key in hand: "tripo login --key tsk_..." (site auto-detected)');
|
|
230
193
|
}
|
|
231
|
-
let grant;
|
|
232
|
-
|
|
233
|
-
|
|
194
|
+
let grant = loadPendingGrant(region, profile);
|
|
195
|
+
const resumed = grant !== undefined;
|
|
196
|
+
if (grant && noWait) {
|
|
197
|
+
let poll;
|
|
198
|
+
try {
|
|
199
|
+
poll = await checkDeviceApproval(region, grant);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
throw new NetworkError(`authorization service unavailable (${err.message}); the code is still valid`, 'run the same command again shortly');
|
|
203
|
+
}
|
|
204
|
+
if (poll.status !== 'pending') {
|
|
205
|
+
clearPendingGrant();
|
|
206
|
+
await finishDeviceLogin(poll, region, profile);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
234
209
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
210
|
+
if (!grant) {
|
|
211
|
+
try {
|
|
212
|
+
grant = await requestDeviceCode(region, profile === DEFAULT_PROFILE ? undefined : profile);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
if (err instanceof DeviceAuthUnavailableError) {
|
|
216
|
+
if (err.kind === 'unreachable') {
|
|
217
|
+
throw new CliError(`cannot reach the authorization service (${err.message}) — check this machine's network/firewall (set HTTPS_PROXY when behind a proxy), or use TRIPO_API_KEY / "tripo login --key tsk_..."`, EXIT.NETWORK);
|
|
218
|
+
}
|
|
219
|
+
// No paste fallback without a TTY — fail with every remaining option spelled out.
|
|
220
|
+
throw new CliError(`device authorization unavailable (${err.message}) — set TRIPO_API_KEY, run "tripo login --key tsk_...", or run "tripo login" in an interactive terminal`, EXIT.AUTH);
|
|
242
221
|
}
|
|
243
|
-
|
|
244
|
-
throw new CliError(t({
|
|
245
|
-
zh: `浏览器授权服务不可用(${err.message});请设置 TRIPO_API_KEY,运行 tripo login --key tsk_...,或在交互终端里运行 tripo login`,
|
|
246
|
-
en: `device authorization unavailable (${err.message}) — set TRIPO_API_KEY, run "tripo login --key tsk_...", or run "tripo login" in an interactive terminal`,
|
|
247
|
-
}), EXIT.AUTH);
|
|
222
|
+
throw err;
|
|
248
223
|
}
|
|
249
|
-
|
|
224
|
+
savePendingGrant(region, profile, grant);
|
|
250
225
|
}
|
|
251
226
|
const minutes = Math.max(1, Math.round(grant.expires_in / 60));
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
'
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
227
|
+
const rerun = `tripo login --region ${region}${profile === DEFAULT_PROFILE ? '' : ` --profile ${profile}`}${noWait ? ' --no-wait' : ''}`;
|
|
228
|
+
const header = noWait
|
|
229
|
+
? `Authorization happens in a browser; this command exits now. Once the human says it is done, run ${rerun} to finish (same code — do not start over). Relay these steps:`
|
|
230
|
+
: resumed
|
|
231
|
+
? 'Resuming the authorization started earlier (same code); waiting until approved or expired. If the human has not done it yet, relay:'
|
|
232
|
+
: 'Authorization happens in a browser; this command keeps waiting until approved or expired. Relay these steps:';
|
|
233
|
+
log([
|
|
234
|
+
header,
|
|
235
|
+
` 1. Open: ${grant.verification_url_complete}`,
|
|
236
|
+
' (if the page asks to sign in, sign in or sign up first — new accounts get free credits)',
|
|
237
|
+
` 2. Enter this one-time code in the "Verification code" field: ${grant.user_code}`,
|
|
238
|
+
' 3. An account with several API keys is asked to pick one; with none, a key is created automatically',
|
|
239
|
+
` 4. Click "Authorize". The code expires in ${minutes} minutes`,
|
|
240
|
+
].join('\n'));
|
|
241
|
+
if (noWait) {
|
|
242
|
+
emitResult({
|
|
243
|
+
status: 'pending',
|
|
244
|
+
profile,
|
|
245
|
+
region,
|
|
246
|
+
user_code: grant.user_code,
|
|
247
|
+
verification_url: grant.verification_url_complete,
|
|
248
|
+
expires_in: grant.expires_in,
|
|
249
|
+
next: rerun,
|
|
250
|
+
}, `waiting for approval (code ${grant.user_code}); then run: ${rerun}`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
270
253
|
const result = await waitForDeviceApproval(region, grant);
|
|
254
|
+
clearPendingGrant();
|
|
255
|
+
await finishDeviceLogin(result, region, profile);
|
|
256
|
+
}
|
|
257
|
+
async function finishDeviceLogin(result, region, profile) {
|
|
271
258
|
if (result.status === 'denied') {
|
|
272
|
-
throw new CliError(
|
|
259
|
+
throw new CliError('authorization denied — run "tripo login" again to retry', EXIT.AUTH);
|
|
273
260
|
}
|
|
274
261
|
if (result.status !== 'approved') {
|
|
275
|
-
throw new CliError(
|
|
262
|
+
throw new CliError('authorization expired — run "tripo login" again for a fresh code', EXIT.AUTH);
|
|
276
263
|
}
|
|
277
264
|
const saved = await validateAndSave(result.apikey, region, profile);
|
|
278
|
-
emitResult({ status: 'logged_in', profile, region: saved.region, key: maskKey(result.apikey), balance: saved.balance },
|
|
265
|
+
emitResult({ status: 'logged_in', profile, region: saved.region, key: maskKey(result.apikey), balance: saved.balance }, `logged in (profile=${profile}, region=${saved.region})`);
|
|
279
266
|
}
|
|
280
267
|
/** What a human recognizes a console by — they know their sign-in method and how they pay, rarely the region code. */
|
|
281
268
|
function siteLabel(region) {
|
|
282
269
|
return region === 'cn'
|
|
283
|
-
?
|
|
284
|
-
:
|
|
270
|
+
? 'China mainland site developers.tripo3d.com (phone/SMS sign-in, Alipay)'
|
|
271
|
+
: 'international site developers.tripo3d.ai (email sign-in, Stripe)';
|
|
285
272
|
}
|
|
286
273
|
/**
|
|
287
274
|
* Preferred path: RFC 8628 device flow — the browser does the sign-in and a
|
|
@@ -302,21 +289,15 @@ async function deviceFlowWithFallback(region, profile) {
|
|
|
302
289
|
if (err.kind === 'backend_error') {
|
|
303
290
|
// Deployed but broken: fall back so the user is not blocked, but
|
|
304
291
|
// report the failure loudly — a masked 5xx is undebuggable.
|
|
305
|
-
warn(
|
|
306
|
-
zh: `浏览器授权服务故障(${err.message}),请把此错误报给平台;已改用粘贴方式登录`,
|
|
307
|
-
en: `device authorization backend error (${err.message}) — please report this; falling back to paste login`,
|
|
308
|
-
}));
|
|
292
|
+
warn(`device authorization backend error (${err.message}) — please report this; falling back to paste login`);
|
|
309
293
|
}
|
|
310
294
|
else if (err.kind === 'unreachable') {
|
|
311
295
|
// The console API could not be reached at all — the same network
|
|
312
296
|
// problem will likely hit key validation next, so say it loudly now.
|
|
313
|
-
warn(
|
|
314
|
-
zh: `连不上授权服务(${err.message})— 疑似网络/代理问题(出网走 HTTP 代理时请设置 HTTPS_PROXY);已改用粘贴方式登录`,
|
|
315
|
-
en: `cannot reach the authorization service (${err.message}) — likely a network/proxy issue (set HTTPS_PROXY if this machine needs a proxy); falling back to paste login`,
|
|
316
|
-
}));
|
|
297
|
+
warn(`cannot reach the authorization service (${err.message}) — likely a network/proxy issue (set HTTPS_PROXY if this machine needs a proxy); falling back to paste login`);
|
|
317
298
|
}
|
|
318
299
|
else {
|
|
319
|
-
log(
|
|
300
|
+
log('device authorization not available yet — falling back to paste login');
|
|
320
301
|
}
|
|
321
302
|
return keyFromConsole(region, true);
|
|
322
303
|
}
|
|
@@ -326,43 +307,29 @@ async function deviceFlowWithFallback(region, profile) {
|
|
|
326
307
|
// must already be on screen when the user lands on the login page — the
|
|
327
308
|
// page deliberately never pre-fills the code (anti-phishing), so typing it
|
|
328
309
|
// is the one thing the user has to do and the one thing we spell out.
|
|
329
|
-
p.note(grant.user_code,
|
|
310
|
+
p.note(grant.user_code, 'Your one-time code');
|
|
330
311
|
const willOpen = shouldOpen();
|
|
331
|
-
log(
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
].join('\n'),
|
|
338
|
-
en: [
|
|
339
|
-
'Three steps:',
|
|
340
|
-
willOpen ? ' 1. your browser will open the authorization page (sign in or sign up first if needed)' : ` 1. open in a browser: ${grant.verification_url_complete} (sign in or sign up first if needed)`,
|
|
341
|
-
` 2. type this one-time code into the "Verification code" field: ${grant.user_code}`,
|
|
342
|
-
' 3. click "Authorize", then come back to this terminal and wait',
|
|
343
|
-
].join('\n'),
|
|
344
|
-
}));
|
|
312
|
+
log([
|
|
313
|
+
'Three steps:',
|
|
314
|
+
willOpen ? ' 1. your browser will open the authorization page (sign in or sign up first if needed)' : ` 1. open in a browser: ${grant.verification_url_complete} (sign in or sign up first if needed)`,
|
|
315
|
+
` 2. type this one-time code into the "Verification code" field: ${grant.user_code}`,
|
|
316
|
+
' 3. click "Authorize", then come back to this terminal and wait',
|
|
317
|
+
].join('\n'));
|
|
345
318
|
if (willOpen) {
|
|
346
319
|
openExternal(grant.verification_url_complete);
|
|
347
|
-
log(
|
|
320
|
+
log(`(browser didn't open? visit: ${grant.verification_url_complete})`);
|
|
348
321
|
}
|
|
349
|
-
const spinner = createSpinner(
|
|
350
|
-
zh: `等待网页授权… 终端码 ${grant.user_code}(Ctrl+C 取消,或改用 tripo login --paste)`,
|
|
351
|
-
en: `waiting for approval… code ${grant.user_code} (Ctrl+C to cancel, or use tripo login --paste)`,
|
|
352
|
-
}));
|
|
322
|
+
const spinner = createSpinner(`waiting for approval… code ${grant.user_code} (Ctrl+C to cancel, or use tripo login --paste)`);
|
|
353
323
|
const result = await waitForDeviceApproval(region, grant);
|
|
354
324
|
if (result.status === 'approved') {
|
|
355
|
-
spinner.succeed(
|
|
325
|
+
spinner.succeed('authorization approved');
|
|
356
326
|
return result.apikey;
|
|
357
327
|
}
|
|
358
328
|
spinner.stop();
|
|
359
329
|
if (result.status === 'denied') {
|
|
360
|
-
throw new CliError(
|
|
330
|
+
throw new CliError('authorization denied — run tripo login to try again', EXIT.AUTH);
|
|
361
331
|
}
|
|
362
|
-
throw new CliError(
|
|
363
|
-
zh: '授权超时或已过期;重新运行 tripo login,或改用 tripo login --paste 粘贴 Key',
|
|
364
|
-
en: 'authorization expired — run tripo login again, or use tripo login --paste',
|
|
365
|
-
}), EXIT.AUTH);
|
|
332
|
+
throw new CliError('authorization expired — run tripo login again, or use tripo login --paste', EXIT.AUTH);
|
|
366
333
|
}
|
|
367
334
|
function normalizeRegion(value) {
|
|
368
335
|
if (value === undefined)
|
|
@@ -405,26 +372,23 @@ async function keyFromConsole(region, openBrowser) {
|
|
|
405
372
|
const keysUrl = `${platformUrlFor(region)}/${REGION_DEFAULTS[region].lang}/keys`;
|
|
406
373
|
if (openBrowser && shouldOpen()) {
|
|
407
374
|
openExternal(keysUrl);
|
|
408
|
-
log(
|
|
375
|
+
log(`opened the console API Keys page (visit manually if it didn't open):\n ${keysUrl}`);
|
|
409
376
|
}
|
|
410
377
|
else {
|
|
411
|
-
log(
|
|
378
|
+
log(`visit: ${keysUrl}`);
|
|
412
379
|
}
|
|
413
|
-
log(
|
|
414
|
-
zh: `在网页上登录并创建/复制 Key,回来粘贴(新用户注册可领免费额度;选错站?Ctrl+C 后运行 tripo login --region ${region === 'cn' ? 'ov' : 'cn'})`,
|
|
415
|
-
en: `sign in on the page, create/copy a key, then paste it here (new accounts get free credits; wrong site? Ctrl+C and run: tripo login --region ${region === 'cn' ? 'ov' : 'cn'})`,
|
|
416
|
-
}));
|
|
380
|
+
log(`sign in on the page, create/copy a key, then paste it here (new accounts get free credits; wrong site? Ctrl+C and run: tripo login --region ${region === 'cn' ? 'ov' : 'cn'})`);
|
|
417
381
|
const pastePromise = p
|
|
418
382
|
.password({
|
|
419
|
-
message:
|
|
420
|
-
validate: (v) => (v && v.trim().startsWith('tsk_') ? undefined :
|
|
383
|
+
message: 'Paste your API key (tsk_...)',
|
|
384
|
+
validate: (v) => (v && v.trim().startsWith('tsk_') ? undefined : 'keys start with tsk_'),
|
|
421
385
|
})
|
|
422
386
|
.then((v) => {
|
|
423
387
|
if (p.isCancel(v))
|
|
424
388
|
throw new CliError('cancelled', EXIT.FATAL);
|
|
425
389
|
return v.trim();
|
|
426
390
|
});
|
|
427
|
-
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new CliError(
|
|
391
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new CliError('login timed out (5 minutes)', EXIT.AUTH)), LOGIN_TIMEOUT_MS));
|
|
428
392
|
try {
|
|
429
393
|
return await Promise.race([pastePromise, timeoutPromise]);
|
|
430
394
|
}
|
|
@@ -460,10 +424,7 @@ async function validateAndSave(key, selectedRegion, profile) {
|
|
|
460
424
|
throw regionDetectionFailure(detected.failures);
|
|
461
425
|
}
|
|
462
426
|
if (selectedRegion && selectedRegion !== detected.region) {
|
|
463
|
-
warn(
|
|
464
|
-
zh: `这把 Key 属于${detected.region === 'cn' ? '国内' : '海外'}站,已自动切换 region=${detected.region}(你选的 ${selectedRegion} 不生效)`,
|
|
465
|
-
en: `this key belongs to the ${detected.region} region — auto-switched (your choice "${selectedRegion}" was wrong)`,
|
|
466
|
-
}));
|
|
427
|
+
warn(`this key belongs to the ${detected.region} region — auto-switched (your choice "${selectedRegion}" was wrong)`);
|
|
467
428
|
}
|
|
468
429
|
finishSave(key, detected.region, detected.balance.balance, profile);
|
|
469
430
|
return { region: detected.region, balance: detected.balance.balance };
|
|
@@ -478,27 +439,15 @@ const REGION_HOSTS = { ov: 'openapi.tripo3d.ai', cn: 'openapi.tripo3d.com' };
|
|
|
478
439
|
*/
|
|
479
440
|
function regionDetectionFailure(failures) {
|
|
480
441
|
if (failures.ov === 'rejected' && failures.cn === 'rejected') {
|
|
481
|
-
return new AuthError(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}));
|
|
485
|
-
}
|
|
486
|
-
const networkHint = t({
|
|
487
|
-
zh: '检查本机网络/防火墙;出网需要 HTTP 代理时,设置 HTTPS_PROXY 环境变量后重试;tripo doctor 可自检',
|
|
488
|
-
en: 'check this machine\'s network/firewall; if outbound traffic needs an HTTP proxy, set HTTPS_PROXY and retry; "tripo doctor" helps diagnose',
|
|
489
|
-
});
|
|
442
|
+
return new AuthError('key validation failed: rejected by both cn (openapi.tripo3d.com) and ov (openapi.tripo3d.ai) — invalid or deleted');
|
|
443
|
+
}
|
|
444
|
+
const networkHint = 'check this machine\'s network/firewall; if outbound traffic needs an HTTP proxy, set HTTPS_PROXY and retry; "tripo doctor" helps diagnose';
|
|
490
445
|
if (failures.ov === 'unreachable' && failures.cn === 'unreachable') {
|
|
491
|
-
return new NetworkError(
|
|
492
|
-
zh: '无法验证 Key:海外(openapi.tripo3d.ai)和国内(openapi.tripo3d.com)都连接失败 — 这是网络问题,不代表 Key 无效',
|
|
493
|
-
en: 'could not validate the key: both ov (openapi.tripo3d.ai) and cn (openapi.tripo3d.com) are unreachable — a network problem, not proof of a bad key',
|
|
494
|
-
}), networkHint);
|
|
446
|
+
return new NetworkError('could not validate the key: both ov (openapi.tripo3d.ai) and cn (openapi.tripo3d.com) are unreachable — a network problem, not proof of a bad key', networkHint);
|
|
495
447
|
}
|
|
496
448
|
const down = failures.ov === 'unreachable' ? 'ov' : 'cn';
|
|
497
449
|
const up = down === 'ov' ? 'cn' : 'ov';
|
|
498
|
-
return new NetworkError(
|
|
499
|
-
zh: `Key 验证失败:${up === 'cn' ? '国内' : '海外'}站(${REGION_HOSTS[up]})拒绝了这把 Key,而${down === 'cn' ? '国内' : '海外'}站(${REGION_HOSTS[down]})无法连接 — 如果 Key 属于${down === 'cn' ? '国内' : '海外'}站,请先解决网络问题`,
|
|
500
|
-
en: `key validation failed: ${up} (${REGION_HOSTS[up]}) rejected the key and ${down} (${REGION_HOSTS[down]}) is unreachable — if the key belongs to ${down}, fix the network first`,
|
|
501
|
-
}), networkHint);
|
|
450
|
+
return new NetworkError(`key validation failed: ${up} (${REGION_HOSTS[up]}) rejected the key and ${down} (${REGION_HOSTS[down]}) is unreachable — if the key belongs to ${down}, fix the network first`, networkHint);
|
|
502
451
|
}
|
|
503
452
|
function finishSave(key, region, balance, profile) {
|
|
504
453
|
// Saving activates the profile: "the account I just logged into is the
|
|
@@ -508,21 +457,15 @@ function finishSave(key, region, balance, profile) {
|
|
|
508
457
|
const where = profile === DEFAULT_PROFILE ? '~/.tripo/config.json' : `~/.tripo/config.json · profile "${profile}"`;
|
|
509
458
|
// "mode 0600" is only true where POSIX modes exist; on Windows the file
|
|
510
459
|
// relies on the user profile's NTFS ACLs, so the claim would be wrong.
|
|
511
|
-
const mode = process.platform === 'win32' ? '' :
|
|
512
|
-
success(
|
|
513
|
-
zh: `Key 验证通过(region=${region}),当前余额 ${balance} 积分(已保存到 ${where}${mode})`,
|
|
514
|
-
en: `key verified (region=${region}); balance ${balance} credits (saved to ${where}${mode})`,
|
|
515
|
-
}));
|
|
460
|
+
const mode = process.platform === 'win32' ? '' : ', mode 0600';
|
|
461
|
+
success(`key verified (region=${region}); balance ${balance} credits (saved to ${where}${mode})`);
|
|
516
462
|
if (Number(balance) === 0) {
|
|
517
|
-
warn(
|
|
518
|
-
zh: '余额为 0:新账号可在控制台领取免费额度,或运行 tripo topup 充值',
|
|
519
|
-
en: 'balance is 0: claim free credits in the console for new accounts, or run "tripo topup"',
|
|
520
|
-
}));
|
|
463
|
+
warn('balance is 0: claim free credits in the console for new accounts, or run "tripo topup"');
|
|
521
464
|
}
|
|
522
465
|
}
|
|
523
466
|
function asAuthError(err) {
|
|
524
467
|
if (err instanceof CliError && err.exitCode === EXIT.AUTH) {
|
|
525
|
-
return new AuthError(
|
|
468
|
+
return new AuthError('key validation failed: invalid or deleted');
|
|
526
469
|
}
|
|
527
470
|
return err instanceof Error ? err : new Error(String(err));
|
|
528
471
|
}
|