yingclaw 1.1.0 → 1.4.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 (3) hide show
  1. package/bin/cli.js +157 -62
  2. package/lib/config.js +48 -1
  3. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { Command } = require('commander');
4
4
  const { select, input, confirm } = require('@inquirer/prompts');
5
- const { loadConfig, saveConfig, writeEnvToZshrc, PROVIDERS } = require('../lib/config');
5
+ const { loadConfig, saveConfig, writeEnvToZshrc, fetchModels, PROVIDERS } = require('../lib/config');
6
6
  const { execSync, spawn, spawnSync } = require('child_process');
7
7
  const https = require('https');
8
8
  const pkg = require('../package.json');
@@ -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,30 +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 model = await select({
194
- message: chalk.cyan('选择模型'),
195
- choices: provider.models,
196
- });
197
-
198
- const apiKey = await input({
199
- message: chalk.cyan(`${provider.name} API Key`),
200
- transformer: (v) => v ? chalk.dim(''.repeat(v.length)) : '',
201
- validate: (v) => v.trim().length > 0 ? true : 'API Key 不能为空',
202
- });
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
+ }
235
+ }
203
236
 
204
237
  const spinner = ora('写入配置...').start();
205
238
  let result, file;
206
239
  try {
207
- const config = { provider: providerKey, model, apiKey: apiKey.trim(), baseUrl: provider.baseUrl };
208
- saveConfig(config);
209
- ({ 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));
210
243
  spinner.succeed(chalk.green(result === 'updated' ? `环境变量已更新 → ${file}` : `环境变量已写入 → ${file}`));
211
244
  } catch (e) {
212
245
  spinner.fail(chalk.red(`写入失败: ${e.message}`));
@@ -217,7 +250,7 @@ program
217
250
  console.log(boxen(
218
251
  chalk.bold('配置完成!\n\n') +
219
252
  chalk.dim('ANTHROPIC_BASE_URL ') + chalk.cyan(provider.baseUrl) + '\n' +
220
- 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' +
221
254
  chalk.white('下次直接输入 ') + chalk.cyan.bold('claude') + chalk.white(' 即可使用'),
222
255
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
223
256
  ));
@@ -228,7 +261,7 @@ program
228
261
 
229
262
  spawn('claude', [], {
230
263
  stdio: 'inherit',
231
- 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 },
232
265
  }).on('error', () => {
233
266
  console.log(chalk.yellow('\nClaude Code 未找到,请先运行: claw install-claude'));
234
267
  });
@@ -251,16 +284,15 @@ program
251
284
 
252
285
  const providerKey = await select({
253
286
  message: chalk.cyan('选择 AI 厂商'),
254
- 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
+ ],
255
291
  });
292
+ if (providerKey === '__BACK__') return;
256
293
 
257
294
  const provider = PROVIDERS[providerKey];
258
295
 
259
- const model = await select({
260
- message: chalk.cyan('选择模型'),
261
- choices: provider.models,
262
- });
263
-
264
296
  // 切换厂商时询问是否更换 Key
265
297
  let apiKey = config.apiKey;
266
298
  if (providerKey !== config.provider) {
@@ -275,6 +307,27 @@ program
275
307
  }
276
308
  }
277
309
 
310
+ // 联网拉模型
311
+ const fetchSpinner = ora('正在获取可用模型...').start();
312
+ const onlineModels = await fetchModels(providerKey, apiKey);
313
+ let modelChoices;
314
+ if (onlineModels && onlineModels.length > 0) {
315
+ fetchSpinner.succeed(chalk.green(`已获取 ${onlineModels.length} 个可用模型`));
316
+ modelChoices = onlineModels.map(id => ({ name: id, value: id }));
317
+ } else {
318
+ fetchSpinner.warn(chalk.yellow('无法获取在线列表,使用内置默认列表'));
319
+ modelChoices = provider.models;
320
+ }
321
+
322
+ const model = await select({
323
+ message: chalk.cyan('选择模型'),
324
+ choices: [
325
+ ...modelChoices,
326
+ { name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
327
+ ],
328
+ });
329
+ if (model === '__BACK__') return;
330
+
278
331
  const spinner = ora('切换中...').start();
279
332
  const newConfig = { ...config, provider: providerKey, model, baseUrl: provider.baseUrl, apiKey };
280
333
  saveConfig(newConfig);
@@ -289,34 +342,44 @@ program
289
342
  .description('查看当前配置和 Key 有效性')
290
343
  .action(showStatus);
291
344
 
292
- // 无参数时显示交互式菜单
293
- if (process.argv.length === 2) {
294
- (async () => {
295
- const chalk = (await import('chalk')).default;
345
+ async function renderStatusBar() {
346
+ const chalk = (await import('chalk')).default;
347
+ const config = loadConfig();
348
+ const claudeInstalled = (() => {
349
+ try { execSync('claude --version', { stdio: 'pipe' }); return true; } catch { return false; }
350
+ })();
351
+
352
+ const claudeIcon = claudeInstalled ? chalk.green('●') : chalk.red('●');
353
+ const claudeText = chalk.dim('Claude');
354
+
355
+ let cfgPart;
356
+ if (config) {
357
+ const provName = PROVIDERS[config.provider]?.name || config.provider;
358
+ cfgPart = chalk.green('●') + ' ' + chalk.white(provName) + chalk.dim(' · ') + chalk.yellow(config.model);
359
+ } else {
360
+ cfgPart = chalk.red('●') + ' ' + chalk.dim('未配置');
361
+ }
362
+
363
+ return ` ${claudeIcon} ${claudeText} ${cfgPart}`;
364
+ }
296
365
 
366
+ async function runMenu() {
367
+ const chalk = (await import('chalk')).default;
368
+
369
+ while (true) {
370
+ console.clear();
297
371
  console.log(await getBanner());
372
+ console.log(await renderStatusBar());
373
+ console.log();
298
374
 
299
375
  const config = loadConfig();
300
- const claudeInstalled = (() => {
301
- try { execSync('claude --version', { stdio: 'pipe' }); return true; } catch { return false; }
302
- })();
303
-
304
- // 根据当前状态生成提示
305
- if (config) {
306
- console.log(chalk.green(` 当前:${PROVIDERS[config.provider]?.name || config.provider} · ${config.model}\n`));
307
- } else if (claudeInstalled) {
308
- console.log(chalk.yellow(' Claude Code 已安装,还未配置 API\n'));
309
- } else {
310
- console.log(chalk.dim(' 首次使用,请先安装 Claude Code\n'));
311
- }
312
-
313
376
  const action = await select({
314
377
  message: chalk.cyan('选择操作'),
315
378
  choices: [
316
379
  { name: '🤖 启动 Claude Code', value: 'launch', disabled: !config && '需先完成配置' },
317
380
  { name: '📦 安装 Claude Code', value: 'install' },
318
- { name: '⚙️ 配置 API Key 和模型', value: 'setup' },
319
- { name: '🔄 切换模型', value: 'switch', disabled: !config && '需先完成配置' },
381
+ { name: config ? '⚙️ 重新配置(输入新的 API Key)' : '⚙️ 首次配置 API Key 和模型', value: 'setup' },
382
+ { name: '🔄 切换厂商/模型(保留当前 Key)', value: 'switch', disabled: !config && '需先完成配置' },
320
383
  { name: '📊 查看当前配置', value: 'status', disabled: !config && '需先完成配置' },
321
384
  { name: '退出', value: 'exit' },
322
385
  ],
@@ -324,24 +387,56 @@ if (process.argv.length === 2) {
324
387
 
325
388
  if (action === 'exit') return;
326
389
 
327
- // 把选中的命令转发给 commander
328
- const cmdMap = {
329
- launch: () => {
330
- if (!config) return;
331
- spawn('claude', [], {
390
+ if (action === 'launch') {
391
+ const cfg = loadConfig();
392
+ if (!cfg) continue;
393
+ // 启动 claude 后等它退出,然后回菜单
394
+ await new Promise((resolve) => {
395
+ const child = spawn('claude', [], {
332
396
  stdio: 'inherit',
333
- env: { ...process.env, ANTHROPIC_BASE_URL: config.baseUrl, ANTHROPIC_API_KEY: config.apiKey },
334
- }).on('error', () => {
397
+ env: { ...process.env, ANTHROPIC_BASE_URL: cfg.baseUrl, ANTHROPIC_API_KEY: cfg.apiKey },
398
+ });
399
+ child.on('error', () => {
335
400
  console.log(chalk.yellow('\nClaude Code 未找到,请先选择"安装 Claude Code"'));
401
+ resolve();
336
402
  });
337
- },
338
- install: () => program.parseAsync(['node', 'claw', 'install-claude']),
339
- setup: () => program.parseAsync(['node', 'claw', 'setup']),
340
- switch: () => program.parseAsync(['node', 'claw', 'switch']),
341
- status: () => program.parseAsync(['node', 'claw', 'status']),
403
+ child.on('exit', resolve);
404
+ });
405
+ continue;
406
+ }
407
+
408
+ const cmdMap = {
409
+ install: 'install-claude',
410
+ setup: 'setup',
411
+ switch: 'switch',
412
+ status: 'status',
342
413
  };
343
- await cmdMap[action]();
344
- })();
414
+
415
+ // 执行子命令
416
+ await new Promise((resolve) => {
417
+ const child = spawn(process.execPath, [__filename, cmdMap[action]], { stdio: 'inherit' });
418
+ child.on('exit', resolve);
419
+ child.on('error', resolve);
420
+ });
421
+
422
+ // 操作结束,提示返回菜单
423
+ console.log();
424
+ const next = await select({
425
+ message: chalk.cyan('下一步'),
426
+ choices: [
427
+ { name: '↩ 返回主菜单', value: 'menu' },
428
+ { name: '退出', value: 'exit' },
429
+ ],
430
+ });
431
+ if (next === 'exit') return;
432
+ }
433
+ }
434
+
435
+ if (process.argv.length === 2) {
436
+ runMenu().catch((e) => {
437
+ if (e?.name === 'ExitPromptError') return; // Ctrl+C
438
+ console.error(e);
439
+ });
345
440
  } else {
346
441
  program.parse(process.argv);
347
442
  }
package/lib/config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
+ const https = require('https');
4
5
 
5
6
  const CONFIG_FILE = path.join(os.homedir(), '.clawai.json');
6
7
 
@@ -8,6 +9,7 @@ const PROVIDERS = {
8
9
  deepseek: {
9
10
  name: 'DeepSeek',
10
11
  baseUrl: 'https://api.deepseek.com/anthropic',
12
+ modelsUrl: 'https://api.deepseek.com/v1/models',
11
13
  models: [
12
14
  { name: 'DeepSeek V4 Flash(快速)', value: 'deepseek-v4-flash' },
13
15
  { name: 'DeepSeek V4 Pro(强力)', value: 'deepseek-v4-pro' },
@@ -16,6 +18,7 @@ const PROVIDERS = {
16
18
  qwen: {
17
19
  name: '阿里云百炼 (Qwen)',
18
20
  baseUrl: 'https://dashscope.aliyuncs.com/apps/anthropic',
21
+ modelsUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1/models',
19
22
  models: [
20
23
  { name: 'Qwen3 Max(强力)', value: 'qwen3-max' },
21
24
  { name: 'Qwen3 Plus(均衡)', value: 'qwen3-plus' },
@@ -25,6 +28,7 @@ const PROVIDERS = {
25
28
  minimax: {
26
29
  name: 'MiniMax',
27
30
  baseUrl: 'https://api.minimaxi.com/anthropic',
31
+ modelsUrl: 'https://api.minimaxi.com/v1/models',
28
32
  models: [
29
33
  { name: 'MiniMax M2.7(旗舰)', value: 'MiniMax-M2.7' },
30
34
  { name: 'MiniMax M2.7 Turbo(快速)', value: 'MiniMax-M2.7-Turbo' },
@@ -34,6 +38,7 @@ const PROVIDERS = {
34
38
  glm: {
35
39
  name: '智谱 GLM',
36
40
  baseUrl: 'https://open.bigmodel.cn/api/anthropic',
41
+ modelsUrl: 'https://open.bigmodel.cn/api/paas/v4/models',
37
42
  models: [
38
43
  { name: 'GLM-4.7(旗舰)', value: 'GLM-4.7' },
39
44
  { name: 'GLM-5.1(强力)', value: 'GLM-5.1' },
@@ -44,12 +49,54 @@ const PROVIDERS = {
44
49
  mimo: {
45
50
  name: '小米 MiMo',
46
51
  baseUrl: 'https://api.xiaomimimo.com/anthropic',
52
+ modelsUrl: 'https://api.xiaomimimo.com/v1/models',
47
53
  models: [
48
54
  { name: 'MiMo V2.5 Pro(旗舰)', value: 'mimo-v2.5-pro' },
49
55
  ],
50
56
  },
51
57
  };
52
58
 
59
+ // 联网拉取厂商支持的模型列表,失败返回 null
60
+ function fetchModels(providerKey, apiKey) {
61
+ return new Promise((resolve) => {
62
+ const provider = PROVIDERS[providerKey];
63
+ if (!provider?.modelsUrl) return resolve(null);
64
+
65
+ let url;
66
+ try { url = new URL(provider.modelsUrl); } catch { return resolve(null); }
67
+
68
+ const req = https.request({
69
+ hostname: url.hostname,
70
+ path: url.pathname,
71
+ method: 'GET',
72
+ timeout: 6000,
73
+ headers: {
74
+ 'authorization': `Bearer ${apiKey}`,
75
+ 'api-key': apiKey, // MiMo 用这个 header
76
+ },
77
+ }, (res) => {
78
+ let data = '';
79
+ res.on('data', (c) => data += c);
80
+ res.on('end', () => {
81
+ try {
82
+ const parsed = JSON.parse(data);
83
+ // OpenAI 格式: { data: [{ id, ... }] }
84
+ // GLM 格式: { data: [{ id, ... }] } 类似
85
+ const list = parsed.data || parsed.models || [];
86
+ const ids = list.map(m => m.id || m.model || m.name).filter(Boolean);
87
+ if (ids.length === 0) return resolve(null);
88
+ resolve(ids);
89
+ } catch {
90
+ resolve(null);
91
+ }
92
+ });
93
+ });
94
+ req.on('error', () => resolve(null));
95
+ req.on('timeout', () => { req.destroy(); resolve(null); });
96
+ req.end();
97
+ });
98
+ }
99
+
53
100
  function loadConfig() {
54
101
  try {
55
102
  return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
@@ -94,4 +141,4 @@ function writeEnvToZshrc(baseUrl, apiKey) {
94
141
  }
95
142
  }
96
143
 
97
- module.exports = { loadConfig, saveConfig, writeEnvToZshrc, PROVIDERS, CONFIG_FILE };
144
+ module.exports = { loadConfig, saveConfig, writeEnvToZshrc, fetchModels, PROVIDERS, CONFIG_FILE };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yingclaw",
3
- "version": "1.1.0",
3
+ "version": "1.4.0",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Qwen、MiniMax、GLM、MiMo",
5
5
  "main": "index.js",
6
6
  "bin": {