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.
- package/bin/cli.js +157 -62
- package/lib/config.js +48 -1
- 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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
208
|
-
saveConfig(
|
|
209
|
-
({ result, file } = writeEnvToZshrc(provider.baseUrl, apiKey
|
|
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.
|
|
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
|
|
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:
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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: '⚙️
|
|
319
|
-
{ name: '🔄
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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:
|
|
334
|
-
})
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
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 };
|