yingclaw 1.0.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 +124 -38
  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,39 +313,101 @@ 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;
296
- const boxen = (await import('boxen')).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');
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;
297
339
 
340
+ while (true) {
341
+ console.clear();
298
342
  console.log(await getBanner());
343
+ console.log(await renderStatusBar());
344
+ console.log();
299
345
 
300
346
  const config = loadConfig();
301
- const claudeInstalled = (() => {
302
- try { execSync('claude --version', { stdio: 'pipe' }); return true; } catch { return false; }
303
- })();
347
+ const action = await select({
348
+ message: chalk.cyan('选择操作'),
349
+ choices: [
350
+ { name: '🤖 启动 Claude Code', value: 'launch', disabled: !config && '需先完成配置' },
351
+ { name: '📦 安装 Claude Code', value: 'install' },
352
+ { name: config ? '⚙️ 重新配置(输入新的 API Key)' : '⚙️ 首次配置 API Key 和模型', value: 'setup' },
353
+ { name: '🔄 切换厂商/模型(保留当前 Key)', value: 'switch', disabled: !config && '需先完成配置' },
354
+ { name: '📊 查看当前配置', value: 'status', disabled: !config && '需先完成配置' },
355
+ { name: '退出', value: 'exit' },
356
+ ],
357
+ });
304
358
 
305
- if (!config && !claudeInstalled) {
306
- console.log(boxen(
307
- chalk.bold('欢迎使用!按以下步骤开始:\n\n') +
308
- chalk.cyan(' 1. ') + chalk.white('claw install-claude') + chalk.dim(' 安装 Claude Code\n') +
309
- chalk.cyan(' 2. ') + chalk.white('claw setup') + chalk.dim(' 配置国产模型 API\n') +
310
- chalk.cyan(' 3. ') + chalk.white('claude') + chalk.dim(' 开始使用'),
311
- { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'cyan', margin: { top: 1, bottom: 1 } }
312
- ));
313
- } else if (!config) {
314
- console.log(boxen(
315
- chalk.bold('Claude Code 已安装,还差一步:\n\n') +
316
- chalk.cyan(' claw setup') + chalk.dim(' 配置国产模型 API Key'),
317
- { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'yellow', margin: { top: 1, bottom: 1 } }
318
- ));
319
- } else {
320
- console.log(chalk.green('✔ 已配置完成,直接运行 ') + chalk.cyan.bold('claude') + chalk.green(' 即可使用\n'));
321
- await showStatus();
322
- return;
359
+ if (action === 'exit') return;
360
+
361
+ if (action === 'launch') {
362
+ const cfg = loadConfig();
363
+ if (!cfg) continue;
364
+ // 启动 claude 后等它退出,然后回菜单
365
+ await new Promise((resolve) => {
366
+ const child = spawn('claude', [], {
367
+ stdio: 'inherit',
368
+ env: { ...process.env, ANTHROPIC_BASE_URL: cfg.baseUrl, ANTHROPIC_API_KEY: cfg.apiKey },
369
+ });
370
+ child.on('error', () => {
371
+ console.log(chalk.yellow('\nClaude Code 未找到,请先选择"安装 Claude Code"'));
372
+ resolve();
373
+ });
374
+ child.on('exit', resolve);
375
+ });
376
+ continue;
323
377
  }
324
- })();
378
+
379
+ const cmdMap = {
380
+ install: 'install-claude',
381
+ setup: 'setup',
382
+ switch: 'switch',
383
+ status: 'status',
384
+ };
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
+ });
325
411
  } else {
326
412
  program.parse(process.argv);
327
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.0.0",
3
+ "version": "1.3.0",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Qwen、MiniMax、GLM、MiMo",
5
5
  "main": "index.js",
6
6
  "bin": {