yingclaw 1.1.0 → 1.3.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 +110 -44
  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');
@@ -190,17 +190,29 @@ program
190
190
 
191
191
  const provider = PROVIDERS[providerKey];
192
192
 
193
- const model = await select({
194
- message: chalk.cyan('选择模型'),
195
- choices: provider.models,
196
- });
197
-
198
193
  const apiKey = await input({
199
194
  message: chalk.cyan(`${provider.name} API Key`),
200
195
  transformer: (v) => v ? chalk.dim('•'.repeat(v.length)) : '',
201
196
  validate: (v) => v.trim().length > 0 ? true : 'API Key 不能为空',
202
197
  });
203
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;
209
+ }
210
+
211
+ const model = await select({
212
+ message: chalk.cyan('选择模型'),
213
+ choices: modelChoices,
214
+ });
215
+
204
216
  const spinner = ora('写入配置...').start();
205
217
  let result, file;
206
218
  try {
@@ -256,11 +268,6 @@ program
256
268
 
257
269
  const provider = PROVIDERS[providerKey];
258
270
 
259
- const model = await select({
260
- message: chalk.cyan('选择模型'),
261
- choices: provider.models,
262
- });
263
-
264
271
  // 切换厂商时询问是否更换 Key
265
272
  let apiKey = config.apiKey;
266
273
  if (providerKey !== config.provider) {
@@ -275,6 +282,23 @@ program
275
282
  }
276
283
  }
277
284
 
285
+ // 联网拉模型
286
+ const fetchSpinner = ora('正在获取可用模型...').start();
287
+ const onlineModels = await fetchModels(providerKey, apiKey);
288
+ let modelChoices;
289
+ if (onlineModels && onlineModels.length > 0) {
290
+ fetchSpinner.succeed(chalk.green(`已获取 ${onlineModels.length} 个可用模型`));
291
+ modelChoices = onlineModels.map(id => ({ name: id, value: id }));
292
+ } else {
293
+ fetchSpinner.warn(chalk.yellow('无法获取在线列表,使用内置默认列表'));
294
+ modelChoices = provider.models;
295
+ }
296
+
297
+ const model = await select({
298
+ message: chalk.cyan('选择模型'),
299
+ choices: modelChoices,
300
+ });
301
+
278
302
  const spinner = ora('切换中...').start();
279
303
  const newConfig = { ...config, provider: providerKey, model, baseUrl: provider.baseUrl, apiKey };
280
304
  saveConfig(newConfig);
@@ -289,34 +313,44 @@ program
289
313
  .description('查看当前配置和 Key 有效性')
290
314
  .action(showStatus);
291
315
 
292
- // 无参数时显示交互式菜单
293
- if (process.argv.length === 2) {
294
- (async () => {
295
- const chalk = (await import('chalk')).default;
316
+ async function renderStatusBar() {
317
+ const chalk = (await import('chalk')).default;
318
+ const config = loadConfig();
319
+ const claudeInstalled = (() => {
320
+ try { execSync('claude --version', { stdio: 'pipe' }); return true; } catch { return false; }
321
+ })();
322
+
323
+ const claudeIcon = claudeInstalled ? chalk.green('●') : chalk.red('●');
324
+ const claudeText = chalk.dim('Claude');
296
325
 
326
+ let cfgPart;
327
+ if (config) {
328
+ const provName = PROVIDERS[config.provider]?.name || config.provider;
329
+ cfgPart = chalk.green('●') + ' ' + chalk.white(provName) + chalk.dim(' · ') + chalk.yellow(config.model);
330
+ } else {
331
+ cfgPart = chalk.red('●') + ' ' + chalk.dim('未配置');
332
+ }
333
+
334
+ return ` ${claudeIcon} ${claudeText} ${cfgPart}`;
335
+ }
336
+
337
+ async function runMenu() {
338
+ const chalk = (await import('chalk')).default;
339
+
340
+ while (true) {
341
+ console.clear();
297
342
  console.log(await getBanner());
343
+ console.log(await renderStatusBar());
344
+ console.log();
298
345
 
299
346
  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
347
  const action = await select({
314
348
  message: chalk.cyan('选择操作'),
315
349
  choices: [
316
350
  { name: '🤖 启动 Claude Code', value: 'launch', disabled: !config && '需先完成配置' },
317
351
  { name: '📦 安装 Claude Code', value: 'install' },
318
- { name: '⚙️ 配置 API Key 和模型', value: 'setup' },
319
- { name: '🔄 切换模型', value: 'switch', disabled: !config && '需先完成配置' },
352
+ { name: config ? '⚙️ 重新配置(输入新的 API Key)' : '⚙️ 首次配置 API Key 和模型', value: 'setup' },
353
+ { name: '🔄 切换厂商/模型(保留当前 Key)', value: 'switch', disabled: !config && '需先完成配置' },
320
354
  { name: '📊 查看当前配置', value: 'status', disabled: !config && '需先完成配置' },
321
355
  { name: '退出', value: 'exit' },
322
356
  ],
@@ -324,24 +358,56 @@ if (process.argv.length === 2) {
324
358
 
325
359
  if (action === 'exit') return;
326
360
 
327
- // 把选中的命令转发给 commander
328
- const cmdMap = {
329
- launch: () => {
330
- if (!config) return;
331
- spawn('claude', [], {
361
+ if (action === 'launch') {
362
+ const cfg = loadConfig();
363
+ if (!cfg) continue;
364
+ // 启动 claude 后等它退出,然后回菜单
365
+ await new Promise((resolve) => {
366
+ const child = spawn('claude', [], {
332
367
  stdio: 'inherit',
333
- env: { ...process.env, ANTHROPIC_BASE_URL: config.baseUrl, ANTHROPIC_API_KEY: config.apiKey },
334
- }).on('error', () => {
368
+ env: { ...process.env, ANTHROPIC_BASE_URL: cfg.baseUrl, ANTHROPIC_API_KEY: cfg.apiKey },
369
+ });
370
+ child.on('error', () => {
335
371
  console.log(chalk.yellow('\nClaude Code 未找到,请先选择"安装 Claude Code"'));
372
+ resolve();
336
373
  });
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']),
374
+ child.on('exit', resolve);
375
+ });
376
+ continue;
377
+ }
378
+
379
+ const cmdMap = {
380
+ install: 'install-claude',
381
+ setup: 'setup',
382
+ switch: 'switch',
383
+ status: 'status',
342
384
  };
343
- await cmdMap[action]();
344
- })();
385
+
386
+ // 执行子命令
387
+ await new Promise((resolve) => {
388
+ const child = spawn(process.execPath, [__filename, cmdMap[action]], { stdio: 'inherit' });
389
+ child.on('exit', resolve);
390
+ child.on('error', resolve);
391
+ });
392
+
393
+ // 操作结束,提示返回菜单
394
+ console.log();
395
+ const next = await select({
396
+ message: chalk.cyan('下一步'),
397
+ choices: [
398
+ { name: '↩ 返回主菜单', value: 'menu' },
399
+ { name: '退出', value: 'exit' },
400
+ ],
401
+ });
402
+ if (next === 'exit') return;
403
+ }
404
+ }
405
+
406
+ if (process.argv.length === 2) {
407
+ runMenu().catch((e) => {
408
+ if (e?.name === 'ExitPromptError') return; // Ctrl+C
409
+ console.error(e);
410
+ });
345
411
  } else {
346
412
  program.parse(process.argv);
347
413
  }
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.3.0",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Qwen、MiniMax、GLM、MiMo",
5
5
  "main": "index.js",
6
6
  "bin": {