yingclaw 1.3.0 → 1.5.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 (2) hide show
  1. package/bin/cli.js +94 -39
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -119,8 +119,10 @@ program
119
119
  choices: [
120
120
  { name: '有梯子 / 海外网络(走官方)', value: 'vpn' },
121
121
  { name: '国内网络 / 没有梯子(走镜像)', value: 'cn' },
122
+ { name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
122
123
  ],
123
124
  });
125
+ if (network === '__BACK__') return;
124
126
 
125
127
  const cmd = network === 'vpn'
126
128
  ? 'npm install -g @anthropic-ai/claude-code'
@@ -183,42 +185,61 @@ program
183
185
  if (!overwrite) return;
184
186
  }
185
187
 
186
- const providerKey = await select({
187
- message: chalk.cyan('选择 AI 厂商'),
188
- choices: Object.entries(PROVIDERS).map(([value, p]) => ({ name: p.name, value })),
189
- });
190
-
191
- const provider = PROVIDERS[providerKey];
192
-
193
- const apiKey = await input({
194
- message: chalk.cyan(`${provider.name} API Key`),
195
- transformer: (v) => v ? chalk.dim(''.repeat(v.length)) : '',
196
- validate: (v) => v.trim().length > 0 ? true : 'API Key 不能为空',
197
- });
198
-
199
- // 联网拉取支持的模型列表
200
- const fetchSpinner = ora('正在获取可用模型...').start();
201
- const onlineModels = await fetchModels(providerKey, apiKey.trim());
202
- let modelChoices;
203
- if (onlineModels && onlineModels.length > 0) {
204
- fetchSpinner.succeed(chalk.green(`已获取 ${onlineModels.length} 个可用模型`));
205
- modelChoices = onlineModels.map(id => ({ name: id, value: id }));
206
- } else {
207
- fetchSpinner.warn(chalk.yellow('无法获取在线列表,使用内置默认列表'));
208
- modelChoices = provider.models;
188
+ let providerKey, provider, apiKey, model;
189
+ let step = 'provider';
190
+
191
+ while (true) {
192
+ if (step === 'provider') {
193
+ providerKey = await select({
194
+ message: chalk.cyan('选择 AI 厂商'),
195
+ choices: [
196
+ ...Object.entries(PROVIDERS).map(([value, p]) => ({ name: p.name, value })),
197
+ { name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
198
+ ],
199
+ });
200
+ if (providerKey === '__BACK__') return;
201
+ provider = PROVIDERS[providerKey];
202
+ step = 'apikey';
203
+ } else if (step === 'apikey') {
204
+ const k = await input({
205
+ message: chalk.cyan(`${provider.name} API Key(输入 b 返回上一步)`),
206
+ transformer: (v) => v && v !== 'b' ? chalk.dim('•'.repeat(v.length)) : v,
207
+ validate: (v) => v.trim().length > 0 ? true : 'API Key 不能为空',
208
+ });
209
+ if (k.trim() === 'b') { step = 'provider'; continue; }
210
+ apiKey = k.trim();
211
+ step = 'model';
212
+ } else if (step === 'model') {
213
+ const fetchSpinner = ora('正在获取可用模型...').start();
214
+ const onlineModels = await fetchModels(providerKey, apiKey);
215
+ let modelChoices;
216
+ if (onlineModels && onlineModels.length > 0) {
217
+ fetchSpinner.succeed(chalk.green(`已获取 ${onlineModels.length} 个可用模型`));
218
+ modelChoices = onlineModels.map(id => ({ name: id, value: id }));
219
+ } else {
220
+ fetchSpinner.warn(chalk.yellow('无法获取在线列表,使用内置默认列表'));
221
+ modelChoices = provider.models;
222
+ }
223
+
224
+ const m = await select({
225
+ message: chalk.cyan('选择模型'),
226
+ choices: [
227
+ ...modelChoices,
228
+ { name: chalk.dim('↩ 返回上一步(重新输入 Key)'), value: '__BACK__' },
229
+ ],
230
+ });
231
+ if (m === '__BACK__') { step = 'apikey'; continue; }
232
+ model = m;
233
+ break;
234
+ }
209
235
  }
210
236
 
211
- const model = await select({
212
- message: chalk.cyan('选择模型'),
213
- choices: modelChoices,
214
- });
215
-
216
237
  const spinner = ora('写入配置...').start();
217
238
  let result, file;
218
239
  try {
219
- const config = { provider: providerKey, model, apiKey: apiKey.trim(), baseUrl: provider.baseUrl };
220
- saveConfig(config);
221
- ({ result, file } = writeEnvToZshrc(provider.baseUrl, apiKey.trim()));
240
+ const cfg = { provider: providerKey, model, apiKey, baseUrl: provider.baseUrl };
241
+ saveConfig(cfg);
242
+ ({ result, file } = writeEnvToZshrc(provider.baseUrl, apiKey));
222
243
  spinner.succeed(chalk.green(result === 'updated' ? `环境变量已更新 → ${file}` : `环境变量已写入 → ${file}`));
223
244
  } catch (e) {
224
245
  spinner.fail(chalk.red(`写入失败: ${e.message}`));
@@ -229,7 +250,7 @@ program
229
250
  console.log(boxen(
230
251
  chalk.bold('配置完成!\n\n') +
231
252
  chalk.dim('ANTHROPIC_BASE_URL ') + chalk.cyan(provider.baseUrl) + '\n' +
232
- chalk.dim('ANTHROPIC_API_KEY ') + chalk.cyan(apiKey.trim().slice(0, 10) + '...') + '\n\n' +
253
+ chalk.dim('ANTHROPIC_API_KEY ') + chalk.cyan(apiKey.slice(0, 10) + '...') + '\n\n' +
233
254
  chalk.white('下次直接输入 ') + chalk.cyan.bold('claude') + chalk.white(' 即可使用'),
234
255
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
235
256
  ));
@@ -240,7 +261,7 @@ program
240
261
 
241
262
  spawn('claude', [], {
242
263
  stdio: 'inherit',
243
- env: { ...process.env, ANTHROPIC_BASE_URL: provider.baseUrl, ANTHROPIC_API_KEY: apiKey.trim() },
264
+ env: { ...process.env, ANTHROPIC_BASE_URL: provider.baseUrl, ANTHROPIC_API_KEY: apiKey },
244
265
  }).on('error', () => {
245
266
  console.log(chalk.yellow('\nClaude Code 未找到,请先运行: claw install-claude'));
246
267
  });
@@ -263,8 +284,12 @@ program
263
284
 
264
285
  const providerKey = await select({
265
286
  message: chalk.cyan('选择 AI 厂商'),
266
- choices: Object.entries(PROVIDERS).map(([value, p]) => ({ name: p.name, value })),
287
+ choices: [
288
+ ...Object.entries(PROVIDERS).map(([value, p]) => ({ name: p.name, value })),
289
+ { name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
290
+ ],
267
291
  });
292
+ if (providerKey === '__BACK__') return;
268
293
 
269
294
  const provider = PROVIDERS[providerKey];
270
295
 
@@ -296,8 +321,12 @@ program
296
321
 
297
322
  const model = await select({
298
323
  message: chalk.cyan('选择模型'),
299
- choices: modelChoices,
324
+ choices: [
325
+ ...modelChoices,
326
+ { name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
327
+ ],
300
328
  });
329
+ if (model === '__BACK__') return;
301
330
 
302
331
  const spinner = ora('切换中...').start();
303
332
  const newConfig = { ...config, provider: providerKey, model, baseUrl: provider.baseUrl, apiKey };
@@ -313,7 +342,7 @@ program
313
342
  .description('查看当前配置和 Key 有效性')
314
343
  .action(showStatus);
315
344
 
316
- async function renderStatusBar() {
345
+ async function renderStatusBar(apiStatus) {
317
346
  const chalk = (await import('chalk')).default;
318
347
  const config = loadConfig();
319
348
  const claudeInstalled = (() => {
@@ -326,7 +355,16 @@ async function renderStatusBar() {
326
355
  let cfgPart;
327
356
  if (config) {
328
357
  const provName = PROVIDERS[config.provider]?.name || config.provider;
329
- cfgPart = chalk.green('●') + ' ' + chalk.white(provName) + chalk.dim(' · ') + chalk.yellow(config.model);
358
+ let dot;
359
+ if (apiStatus === true) dot = chalk.green('●');
360
+ else if (apiStatus === false) dot = chalk.red('●');
361
+ else if (apiStatus === null) dot = chalk.yellow('●');
362
+ else dot = chalk.dim('●'); // 未检测
363
+ const apiTag = apiStatus === true ? chalk.green(' API ✓')
364
+ : apiStatus === false ? chalk.red(' API ✘')
365
+ : apiStatus === null ? chalk.yellow(' 网络异常')
366
+ : '';
367
+ cfgPart = `${dot} ${chalk.white(provName)}${chalk.dim(' · ')}${chalk.yellow(config.model)}${apiTag}`;
330
368
  } else {
331
369
  cfgPart = chalk.red('●') + ' ' + chalk.dim('未配置');
332
370
  }
@@ -336,14 +374,25 @@ async function renderStatusBar() {
336
374
 
337
375
  async function runMenu() {
338
376
  const chalk = (await import('chalk')).default;
377
+ const ora = (await import('ora')).default;
339
378
 
340
379
  while (true) {
341
380
  console.clear();
342
381
  console.log(await getBanner());
343
- console.log(await renderStatusBar());
344
- console.log();
345
382
 
346
383
  const config = loadConfig();
384
+ let apiStatus; // undefined = skipped, true/false/null = checked
385
+ if (config) {
386
+ const spinner = ora('正在检测 API 是否通畅...').start();
387
+ apiStatus = await validateKey(config);
388
+ if (apiStatus === true) spinner.succeed('API 连接正常');
389
+ else if (apiStatus === false) spinner.fail('API Key 无效或已过期');
390
+ else spinner.warn('网络异常,无法连接 API');
391
+ }
392
+
393
+ console.log(await renderStatusBar(apiStatus));
394
+ console.log();
395
+
347
396
  const action = await select({
348
397
  message: chalk.cyan('选择操作'),
349
398
  choices: [
@@ -352,12 +401,18 @@ async function runMenu() {
352
401
  { name: config ? '⚙️ 重新配置(输入新的 API Key)' : '⚙️ 首次配置 API Key 和模型', value: 'setup' },
353
402
  { name: '🔄 切换厂商/模型(保留当前 Key)', value: 'switch', disabled: !config && '需先完成配置' },
354
403
  { name: '📊 查看当前配置', value: 'status', disabled: !config && '需先完成配置' },
404
+ { name: '🔁 重新检测 API', value: 'recheck', disabled: !config && '需先完成配置' },
355
405
  { name: '退出', value: 'exit' },
356
406
  ],
357
407
  });
358
408
 
359
409
  if (action === 'exit') return;
360
410
 
411
+ if (action === 'recheck') {
412
+ // 仅刷新菜单,下次循环会重新检测
413
+ continue;
414
+ }
415
+
361
416
  if (action === 'launch') {
362
417
  const cfg = loadConfig();
363
418
  if (!cfg) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yingclaw",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Qwen、MiniMax、GLM、MiMo",
5
5
  "main": "index.js",
6
6
  "bin": {