tripo-cli 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +16 -14
  3. package/dist/ai/experts.js +26 -20
  4. package/dist/ai/plan-card.js +73 -34
  5. package/dist/ai/wizard.js +19 -19
  6. package/dist/commands/account.js +2 -6
  7. package/dist/commands/ai.js +18 -16
  8. package/dist/commands/batch.js +58 -18
  9. package/dist/commands/completion.js +1 -1
  10. package/dist/commands/config-cmd.js +1 -1
  11. package/dist/commands/doctor.js +1 -3
  12. package/dist/commands/generate.js +50 -8
  13. package/dist/commands/home.js +12 -13
  14. package/dist/commands/login.js +125 -182
  15. package/dist/commands/make.js +50 -25
  16. package/dist/commands/mcp.js +8 -3
  17. package/dist/commands/process-groups.js +29 -18
  18. package/dist/commands/shared.js +22 -13
  19. package/dist/commands/task.js +12 -11
  20. package/dist/commands/topup.js +6 -10
  21. package/dist/commands/update.js +23 -11
  22. package/dist/commands/view.js +3 -4
  23. package/dist/config/config.js +1 -1
  24. package/dist/core/analytics.js +39 -16
  25. package/dist/core/client.js +53 -21
  26. package/dist/core/device-auth.js +49 -1
  27. package/dist/core/download.js +2 -1
  28. package/dist/core/errors.js +24 -14
  29. package/dist/core/output.js +0 -4
  30. package/dist/core/requests.js +94 -31
  31. package/dist/core/task-service.js +21 -20
  32. package/dist/core/upload.js +23 -5
  33. package/dist/core/watch.js +3 -0
  34. package/dist/knowledge/chains.js +16 -17
  35. package/dist/knowledge/error-catalog.js +39 -37
  36. package/dist/knowledge/models.js +147 -27
  37. package/dist/knowledge/params.js +143 -32
  38. package/dist/knowledge/scenarios.js +32 -55
  39. package/dist/pipeline/make.js +38 -30
  40. package/dist/pipeline/runner.js +27 -8
  41. package/dist/pipeline/stdin.js +40 -3
  42. package/dist/program.js +3 -10
  43. package/dist/util/input-detect.js +10 -1
  44. package/package.json +3 -2
  45. package/skill/SKILL.md +52 -15
  46. package/skill/commands/account.md +8 -3
  47. package/skill/commands/batch.md +13 -3
  48. package/skill/commands/generate.md +57 -23
  49. package/skill/commands/make.md +43 -16
  50. package/skill/commands/process.md +29 -24
  51. package/skill/commands/task.md +4 -4
  52. package/skill/commands/view.md +1 -1
  53. package/skill/common-errors.md +14 -10
  54. package/skill/examples/animation.md +4 -2
  55. package/skill/examples/ar-web.md +6 -4
  56. package/skill/examples/game-asset.md +18 -7
  57. package/skill/examples/pipes.md +2 -2
  58. package/skill/examples/print.md +7 -3
  59. package/dist/i18n.js +0 -19
@@ -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(t({ zh: `Profile「${name}」本地没有存储 API Key`, en: `no API key stored locally for profile "${name}"` }));
42
+ log(`no API key stored locally for profile "${name}"`);
43
43
  }
44
44
  else {
45
- success(t({ zh: `已删除 profile「${name}」的本地 API Key`, en: `local API key removed (profile "${name}")` }));
45
+ success(`local API key removed (profile "${name}")`);
46
46
  const rest = listProfiles();
47
47
  if (rest.length > 0) {
48
- log(t({
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(t({
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
- t({ zh: `余额 ${balance.balance} 积分(冻结 ${balance.frozen})`, en: `balance ${balance.balance} credits (${balance.frozen} frozen)` }),
70
+ `balance ${balance.balance} credits (${balance.frozen} frozen)`,
77
71
  ];
78
72
  if (others.length > 0) {
79
- lines.push(t({
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(t({ zh: '还没有任何账号', en: 'no profiles yet' }), t({ zh: '先运行 tripo login 登录', en: 'run "tripo login" first' }));
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(t({
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: t({ zh: '切换到哪个账号?', en: 'Switch to which account?' }),
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 ? t({ zh: '(当前)', en: ' (current)' }) : ''),
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(t({
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(t({
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(t({
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(t({
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(t({
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: t({
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: t({ zh: '海外站 developers.tripo3d.ai(Stripe 支付)', en: 'International — developers.tripo3d.ai (Stripe)' }) },
192
- { value: 'cn', label: t({ zh: '国内站 developers.tripo3d.com(支付宝)', en: 'China mainland — developers.tripo3d.com (Alipay)' }) },
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(t({
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(t({
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(t({
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
- try {
233
- grant = await requestDeviceCode(region, profile === DEFAULT_PROFILE ? undefined : profile);
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
- catch (err) {
236
- if (err instanceof DeviceAuthUnavailableError) {
237
- if (err.kind === 'unreachable') {
238
- throw new CliError(t({
239
- zh: `连不上授权服务(${err.message})— 请检查本机网络/防火墙,出网走 HTTP 代理时设置 HTTPS_PROXY;或改用 TRIPO_API_KEY / tripo login --key tsk_...`,
240
- en: `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_..."`,
241
- }), EXIT.NETWORK);
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
- // No paste fallback without a TTY — fail with every remaining option spelled out.
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
- throw err;
224
+ savePendingGrant(region, profile, grant);
250
225
  }
251
226
  const minutes = Math.max(1, Math.round(grant.expires_in / 60));
252
- log(t({
253
- zh: [
254
- '需要在浏览器完成授权;本命令会一直等待,直到授权完成或过期。请转告用户:',
255
- ` 1. 打开: ${grant.verification_url_complete}`,
256
- ' (页面如果提示登录,先登录或注册 Tripo 账号 — 新账号有免费额度)',
257
- ` 2. 在「Verification code」输入框填入终端码: ${grant.user_code}`,
258
- ' 3. 账号已有多把 API Key 时页面会让用户选择一把;一把没有则自动创建',
259
- ` 4. 点击「Authorize」。此码 ${minutes} 分钟内有效`,
260
- ].join('\n'),
261
- en: [
262
- 'Authorization happens in a browser; this command keeps waiting until approved or expired. Relay these steps:',
263
- ` 1. Open: ${grant.verification_url_complete}`,
264
- ' (if the page asks to sign in, sign in or sign up first — new accounts get free credits)',
265
- ` 2. Enter this one-time code in the "Verification code" field: ${grant.user_code}`,
266
- ' 3. An account with several API keys is asked to pick one; with none, a key is created automatically',
267
- ` 4. Click "Authorize". The code expires in ${minutes} minutes`,
268
- ].join('\n'),
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(t({ zh: '授权被拒绝;重新运行 tripo login 再试', en: 'authorization denied — run "tripo login" again to retry' }), EXIT.AUTH);
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(t({ zh: '授权超时或已过期;重新运行 tripo login 获取新的终端码', en: 'authorization expired — run "tripo login" again for a fresh code' }), EXIT.AUTH);
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 }, t({ zh: `登录完成(profile=${profile}, region=${saved.region})`, en: `logged in (profile=${profile}, region=${saved.region})` }));
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
- ? t({ zh: '国内站 developers.tripo3d.com(手机号登录,支付宝)', en: 'China mainland site developers.tripo3d.com (phone/SMS sign-in, Alipay)' })
284
- : t({ zh: '海外站 developers.tripo3d.ai(邮箱登录,Stripe)', en: 'international site developers.tripo3d.ai (email sign-in, Stripe)' });
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(t({
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(t({
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(t({ zh: '浏览器授权服务暂未开通,改用粘贴方式登录', en: 'device authorization not available yet — falling back to paste login' }));
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, t({ zh: '你的终端码', en: 'Your one-time code' }));
310
+ p.note(grant.user_code, 'Your one-time code');
330
311
  const willOpen = shouldOpen();
331
- log(t({
332
- zh: [
333
- '接下来三步:',
334
- willOpen ? ' 1. 浏览器即将打开授权页面(如未登录,先登录或注册 Tripo 账号)' : ` 1. 在浏览器打开: ${grant.verification_url_complete}(如未登录,先登录或注册)`,
335
- ` 2. 在页面的「Verification code」输入框填入终端码: ${grant.user_code}`,
336
- ' 3. 点击「Authorize」完成授权,回到这个终端等待即可',
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(t({ zh: `(浏览器未弹出?手动访问: ${grant.verification_url_complete})`, en: `(browser didn't open? visit: ${grant.verification_url_complete})` }));
320
+ log(`(browser didn't open? visit: ${grant.verification_url_complete})`);
348
321
  }
349
- const spinner = createSpinner(t({
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(t({ zh: '网页授权完成', en: 'authorization approved' }));
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(t({ zh: '授权被拒绝;重新运行 tripo login 再试', en: 'authorization denied — run tripo login to try again' }), EXIT.AUTH);
330
+ throw new CliError('authorization denied — run tripo login to try again', EXIT.AUTH);
361
331
  }
362
- throw new CliError(t({
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(t({ zh: `已打开控制台 API Keys 页面(未弹出请手动访问):\n ${keysUrl}`, en: `opened the console API Keys page (visit manually if it didn't open):\n ${keysUrl}` }));
375
+ log(`opened the console API Keys page (visit manually if it didn't open):\n ${keysUrl}`);
409
376
  }
410
377
  else {
411
- log(t({ zh: `请访问: ${keysUrl}`, en: `visit: ${keysUrl}` }));
378
+ log(`visit: ${keysUrl}`);
412
379
  }
413
- log(t({
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: t({ zh: '粘贴 API Key (tsk_...)', en: 'Paste your API key (tsk_...)' }),
420
- validate: (v) => (v && v.trim().startsWith('tsk_') ? undefined : t({ zh: 'Key 应以 tsk_ 开头', en: 'keys start with tsk_' })),
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(t({ zh: '登录超时(5 分钟)', en: 'login timed out (5 minutes)' }), EXIT.AUTH)), LOGIN_TIMEOUT_MS));
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(t({
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(t({
482
- zh: 'Key 验证失败:国内(openapi.tripo3d.com)和海外(openapi.tripo3d.ai)都不接受这把 Key — 无效或已被删除',
483
- en: 'key validation failed: rejected by both cn (openapi.tripo3d.com) and ov (openapi.tripo3d.ai) — invalid or deleted',
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(t({
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(t({
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' ? '' : t({ zh: ',权限 0600', en: ', mode 0600' });
512
- success(t({
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(t({
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(t({ zh: 'Key 验证失败:无效或已被删除', en: 'key validation failed: invalid or deleted' }));
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
  }