yingclaw 1.6.3 → 1.7.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Claude Code × 国产大模型,一键接入。
4
4
 
5
- 支持 DeepSeek、阿里云百炼(Qwen)、MiniMax、智谱 GLM、小米 MiMo,无需梯子即可使用 Claude Code。
5
+ 支持 DeepSeek、阿里云百炼(Qwen)、MiniMax、智谱 GLM、小米 MiMo,也支持自定义 Anthropic 兼容接口,无需梯子即可使用 Claude Code。
6
6
 
7
7
  ## 安装
8
8
 
@@ -14,6 +14,8 @@ npm install -g yingclaw
14
14
 
15
15
  ## 使用步骤
16
16
 
17
+ > 当前自动写入环境变量支持 macOS / Linux 的 zsh、bash。Windows 用户可参考“原理”中的环境变量手动配置,PowerShell 自动写入后续版本支持。
18
+
17
19
  **第一步:安装 Claude Code**
18
20
  ```bash
19
21
  claw install-claude
@@ -24,7 +26,15 @@ claw install-claude
24
26
  ```bash
25
27
  claw setup
26
28
  ```
27
- 选择厂商 → 选择模型 → 输入 API Key,自动写入环境变量,配置完成后自动启动 Claude。
29
+ 选择厂商 → 输入 API Key → 选择模型,自动写入环境变量,配置完成后自动启动 Claude。
30
+
31
+ 选择“自定义 Anthropic 兼容接口”时,需要输入:
32
+
33
+ - `ANTHROPIC_BASE_URL`
34
+ - API Key
35
+ - Models URL(可选)
36
+
37
+ 如果填写了 Models URL,会自动获取模型列表;如果留空或获取失败,则手动输入主模型和快速模型。
28
38
 
29
39
  **第三步:以后直接用**
30
40
  ```bash
@@ -40,6 +50,7 @@ claude
40
50
  | MiniMax | M2.7、M2.7 Turbo、M2.5 |
41
51
  | 智谱 GLM | GLM-4.7、GLM-5.1、GLM-5 Turbo、GLM-4.5 Air |
42
52
  | 小米 MiMo | MiMo V2.5 Pro |
53
+ | 自定义接口 | 自动获取或手动输入 |
43
54
 
44
55
  ## 其他命令
45
56
 
package/bin/cli.js CHANGED
@@ -18,6 +18,7 @@ const { execSync, spawn, spawnSync } = require('child_process');
18
18
  const https = require('https');
19
19
  const pkg = require('../package.json');
20
20
  const { buildMenuStatusLines, buildStatusView } = require('../lib/panel');
21
+ const { buildClaudeInstallCommand } = require('../lib/install');
21
22
 
22
23
  const program = new Command();
23
24
 
@@ -81,6 +82,102 @@ function isClaudeInstalled() {
81
82
  }
82
83
  }
83
84
 
85
+ function isValidUrl(value) {
86
+ try {
87
+ new URL(value);
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ async function promptModelFromChoices({ chalk, choices, message, backLabel = '↩ 返回上一步', allowManual = true }) {
95
+ const selected = await select({ loop: false,
96
+ message: chalk.cyan(message),
97
+ choices: [
98
+ ...choices,
99
+ ...(allowManual ? [{ name: '手动输入模型名', value: '__MANUAL__' }] : []),
100
+ { name: chalk.dim(backLabel), value: '__BACK__' },
101
+ ],
102
+ });
103
+ if (selected === '__BACK__') return '__BACK__';
104
+ if (selected !== '__MANUAL__') return selected;
105
+
106
+ return input({
107
+ message: chalk.cyan('输入模型名'),
108
+ validate: (v) => v.trim().length > 0 ? true : '模型名不能为空',
109
+ }).then(v => v.trim());
110
+ }
111
+
112
+ async function promptManualModel(chalk, message, defaultValue) {
113
+ return input({
114
+ message: chalk.cyan(message),
115
+ default: defaultValue,
116
+ validate: (v) => v.trim().length > 0 ? true : '模型名不能为空',
117
+ }).then(v => v.trim());
118
+ }
119
+
120
+ async function configureCustomProvider({ chalk, ora, existingConfig }) {
121
+ const baseUrl = await input({
122
+ message: chalk.cyan('Anthropic Base URL'),
123
+ default: existingConfig?.provider === 'custom' ? existingConfig.baseUrl : undefined,
124
+ validate: (v) => v.trim().length > 0 && isValidUrl(v.trim()) ? true : '请输入有效 URL',
125
+ }).then(v => v.trim().replace(/\/+$/, ''));
126
+
127
+ let apiKey = existingConfig?.provider === 'custom' ? existingConfig.apiKey : '';
128
+ if (apiKey) {
129
+ const keepKey = await confirm({ message: '沿用当前 API Key?', default: true });
130
+ if (!keepKey) apiKey = '';
131
+ }
132
+ if (!apiKey) {
133
+ apiKey = await input({
134
+ message: chalk.cyan('API Key'),
135
+ transformer: (v) => v ? chalk.dim('•'.repeat(v.length)) : '',
136
+ validate: (v) => v.trim().length > 0 ? true : 'API Key 不能为空',
137
+ }).then(v => v.trim());
138
+ }
139
+
140
+ const modelsUrl = await input({
141
+ message: chalk.cyan('Models URL(可选,留空则手动输入模型)'),
142
+ default: existingConfig?.provider === 'custom' ? existingConfig.modelsUrl : undefined,
143
+ validate: (v) => !v.trim() || isValidUrl(v.trim()) ? true : '请输入有效 URL,或留空',
144
+ }).then(v => v.trim());
145
+
146
+ let modelChoices = [];
147
+ if (modelsUrl) {
148
+ const fetchSpinner = ora('正在获取可用模型...').start();
149
+ const onlineModels = await fetchModels('custom', apiKey, modelsUrl);
150
+ if (onlineModels && onlineModels.length > 0) {
151
+ fetchSpinner.succeed(chalk.green(`已获取 ${onlineModels.length} 个可用模型`));
152
+ modelChoices = onlineModels.map(id => ({ name: id, value: id }));
153
+ } else {
154
+ fetchSpinner.warn(chalk.yellow('无法获取在线列表,改为手动输入模型'));
155
+ }
156
+ }
157
+
158
+ let model;
159
+ let fastModel;
160
+ if (modelChoices.length > 0) {
161
+ model = await promptModelFromChoices({ chalk, choices: modelChoices, message: '选择主模型' });
162
+ if (model === '__BACK__') return null;
163
+ fastModel = await promptModelFromChoices({ chalk, choices: modelChoices, message: '选择快速模型 / Subagent 模型' });
164
+ if (fastModel === '__BACK__') return null;
165
+ } else {
166
+ model = await promptManualModel(chalk, '输入主模型名');
167
+ fastModel = await promptManualModel(chalk, '输入快速模型 / Subagent 模型名', model);
168
+ }
169
+
170
+ return {
171
+ provider: 'custom',
172
+ providerName: '自定义接口',
173
+ baseUrl,
174
+ modelsUrl: modelsUrl || undefined,
175
+ apiKey,
176
+ model,
177
+ fastModel,
178
+ };
179
+ }
180
+
84
181
  async function showStatus() {
85
182
  const chalk = (await import('chalk')).default;
86
183
  const boxen = (await import('boxen')).default;
@@ -176,14 +273,12 @@ program
176
273
  });
177
274
  if (network === '__BACK__') return;
178
275
 
179
- const cmd = network === 'vpn'
180
- ? 'npm install -g @anthropic-ai/claude-code'
181
- : 'npm install -g @anthropic-ai/claude-code --registry=https://registry.npmmirror.com';
276
+ const installCommand = buildClaudeInstallCommand(network);
182
277
 
183
278
  console.log(chalk.dim('\n安装中,实时输出:\n'));
184
279
 
185
280
  // 实时输出安装日志
186
- const result = spawnSync(cmd, { shell: true, stdio: 'inherit' });
281
+ const result = spawnSync(installCommand.command, installCommand.args, { stdio: 'inherit' });
187
282
 
188
283
  if (result.status === 0) {
189
284
  console.log(chalk.green('\n✔ Claude Code 安装成功!'));
@@ -237,7 +332,7 @@ program
237
332
  if (!overwrite) return;
238
333
  }
239
334
 
240
- let providerKey, provider, apiKey, model;
335
+ let providerKey, provider, apiKey, model, customConfig;
241
336
  let step = 'provider';
242
337
 
243
338
  while (true) {
@@ -251,6 +346,11 @@ program
251
346
  });
252
347
  if (providerKey === '__BACK__') return;
253
348
  provider = PROVIDERS[providerKey];
349
+ if (provider.custom) {
350
+ customConfig = await configureCustomProvider({ chalk, ora });
351
+ if (!customConfig) { step = 'provider'; continue; }
352
+ break;
353
+ }
254
354
  step = 'apikey';
255
355
  } else if (step === 'apikey') {
256
356
  const k = await input({
@@ -289,10 +389,10 @@ program
289
389
  const spinner = ora('写入配置...').start();
290
390
  let result, file;
291
391
  try {
292
- const fastModel = resolveFastModel(provider, model);
293
- const cfg = { provider: providerKey, model, fastModel, apiKey, baseUrl: provider.baseUrl };
392
+ const fastModel = customConfig?.fastModel || resolveFastModel(provider, model);
393
+ const cfg = customConfig || { provider: providerKey, model, fastModel, apiKey, baseUrl: provider.baseUrl };
294
394
  saveConfig(cfg);
295
- ({ result, file } = writeEnvToZshrc(provider.baseUrl, apiKey, model, fastModel));
395
+ ({ result, file } = writeEnvToZshrc(cfg.baseUrl, cfg.apiKey, cfg.model, cfg.fastModel));
296
396
  spinner.succeed(chalk.green(result === 'updated' ? `环境变量已更新 → ${file}` : `环境变量已写入 → ${file}`));
297
397
  } catch (e) {
298
398
  spinner.fail(chalk.red(`写入失败: ${e.message}`));
@@ -302,7 +402,7 @@ program
302
402
 
303
403
  console.log(boxen(
304
404
  chalk.bold('配置完成!\n\n') +
305
- chalk.dim('ANTHROPIC_BASE_URL ') + chalk.cyan(provider.baseUrl) + '\n' +
405
+ chalk.dim('ANTHROPIC_BASE_URL ') + chalk.cyan((customConfig || { baseUrl: provider.baseUrl }).baseUrl) + '\n' +
306
406
  chalk.dim('ANTHROPIC_API_KEY ') + chalk.cyan('已保存') + '\n\n' +
307
407
  chalk.white('下次直接输入 ') + chalk.cyan.bold('claude') + chalk.white(' 即可使用'),
308
408
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
@@ -314,7 +414,7 @@ program
314
414
 
315
415
  spawn('claude', [], {
316
416
  stdio: 'inherit',
317
- env: { ...process.env, ...buildClaudeEnv({ provider: providerKey, baseUrl: provider.baseUrl, apiKey, model, fastModel: resolveFastModel(provider, model) }) },
417
+ env: { ...process.env, ...buildClaudeEnv(customConfig || { provider: providerKey, baseUrl: provider.baseUrl, apiKey, model, fastModel: resolveFastModel(provider, model) }) },
318
418
  }).on('error', () => {
319
419
  console.log(chalk.yellow('\nClaude Code 未找到,请先运行: claw install-claude'));
320
420
  });
@@ -351,6 +451,18 @@ program
351
451
  if (providerKey === '__BACK__') return;
352
452
 
353
453
  const provider = PROVIDERS[providerKey];
454
+ if (provider.custom) {
455
+ const customConfig = await configureCustomProvider({ chalk, ora, existingConfig: config });
456
+ if (!customConfig) return;
457
+
458
+ const spinner = ora('切换中...').start();
459
+ saveConfig(customConfig);
460
+ const { file } = writeEnvToZshrc(customConfig.baseUrl, customConfig.apiKey, customConfig.model, customConfig.fastModel);
461
+ await new Promise(r => setTimeout(r, 300));
462
+ spinner.succeed(chalk.green(`已切换至 ${customConfig.providerName} · ${customConfig.model}`));
463
+ console.log(chalk.dim(`运行 source ${file} 生效,或重新开一个终端`));
464
+ return;
465
+ }
354
466
 
355
467
  // 切换厂商时询问是否更换 Key
356
468
  let apiKey = config.apiKey;
@@ -562,11 +674,14 @@ async function runMenu() {
562
674
  }
563
675
  }
564
676
 
677
+ function handleCliError(e) {
678
+ if (e?.name === 'ExitPromptError') return; // Ctrl+C
679
+ console.error(e);
680
+ process.exitCode = 1;
681
+ }
682
+
565
683
  if (process.argv.length === 2) {
566
- runMenu().catch((e) => {
567
- if (e?.name === 'ExitPromptError') return; // Ctrl+C
568
- console.error(e);
569
- });
684
+ runMenu().catch(handleCliError);
570
685
  } else {
571
- program.parse(process.argv);
686
+ program.parseAsync(process.argv).catch(handleCliError);
572
687
  }
package/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  module.exports = {
2
2
  ...require('./lib/config'),
3
+ ...require('./lib/install'),
3
4
  ...require('./lib/panel'),
4
5
  };
package/lib/config.js CHANGED
@@ -55,6 +55,11 @@ const PROVIDERS = {
55
55
  { name: 'MiMo V2.5 Pro(旗舰)', value: 'mimo-v2.5-pro' },
56
56
  ],
57
57
  },
58
+ custom: {
59
+ name: '自定义 Anthropic 兼容接口',
60
+ custom: true,
61
+ models: [],
62
+ },
58
63
  };
59
64
 
60
65
  function normalizeModelIds(providerKey, ids) {
@@ -68,18 +73,26 @@ function normalizeModelIds(providerKey, ids) {
68
73
  ];
69
74
  }
70
75
 
76
+ function parseModelIdsResponse(providerKey, data) {
77
+ const parsed = typeof data === 'string' ? JSON.parse(data) : data;
78
+ const list = parsed.data || parsed.models || [];
79
+ const ids = list.map(m => m.id || m.model || m.name).filter(Boolean);
80
+ return normalizeModelIds(providerKey, ids);
81
+ }
82
+
71
83
  // 联网拉取厂商支持的模型列表,失败返回 null
72
- function fetchModels(providerKey, apiKey) {
84
+ function fetchModels(providerKey, apiKey, modelsUrlOverride) {
73
85
  return new Promise((resolve) => {
74
86
  const provider = PROVIDERS[providerKey];
75
- if (!provider?.modelsUrl) return resolve(null);
87
+ const modelsUrl = modelsUrlOverride || provider?.modelsUrl;
88
+ if (!modelsUrl) return resolve(null);
76
89
 
77
90
  let url;
78
- try { url = new URL(provider.modelsUrl); } catch { return resolve(null); }
91
+ try { url = new URL(modelsUrl); } catch { return resolve(null); }
79
92
 
80
93
  const req = https.request({
81
94
  hostname: url.hostname,
82
- path: url.pathname,
95
+ path: url.pathname + url.search,
83
96
  method: 'GET',
84
97
  timeout: 6000,
85
98
  headers: {
@@ -91,13 +104,9 @@ function fetchModels(providerKey, apiKey) {
91
104
  res.on('data', (c) => data += c);
92
105
  res.on('end', () => {
93
106
  try {
94
- const parsed = JSON.parse(data);
95
- // OpenAI 格式: { data: [{ id, ... }] }
96
- // GLM 格式: { data: [{ id, ... }] } 类似
97
- const list = parsed.data || parsed.models || [];
98
- const ids = list.map(m => m.id || m.model || m.name).filter(Boolean);
107
+ const ids = parseModelIdsResponse(providerKey, data);
99
108
  if (ids.length === 0) return resolve(null);
100
- resolve(normalizeModelIds(providerKey, ids));
109
+ resolve(ids);
101
110
  } catch {
102
111
  resolve(null);
103
112
  }
@@ -257,6 +266,7 @@ module.exports = {
257
266
  resetConfig,
258
267
  validateConfig,
259
268
  normalizeModelIds,
269
+ parseModelIdsResponse,
260
270
  resolveFastModel,
261
271
  providerKeyFromBaseUrl,
262
272
  buildClaudeEnv,
package/lib/install.js ADDED
@@ -0,0 +1,9 @@
1
+ function buildClaudeInstallCommand(network) {
2
+ const args = ['install', '-g', '@anthropic-ai/claude-code'];
3
+ if (network === 'cn') {
4
+ args.push('--registry=https://registry.npmmirror.com');
5
+ }
6
+ return { command: 'npm', args };
7
+ }
8
+
9
+ module.exports = { buildClaudeInstallCommand };
package/lib/panel.js CHANGED
@@ -14,7 +14,7 @@ function isEnvActive(config, env) {
14
14
 
15
15
  function buildStatusView(config, options = {}) {
16
16
  const provider = PROVIDERS[config.provider];
17
- const providerName = provider?.name || config.provider;
17
+ const providerName = config.providerName || provider?.name || config.provider;
18
18
  const claudeInstalled = options.claudeInstalled === true;
19
19
  const env = options.env || {};
20
20
  const expectedEnv = buildClaudeEnv(config);
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "yingclaw",
3
- "version": "1.6.3",
3
+ "version": "1.7.0",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Qwen、MiniMax、GLM、MiMo",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "claw": "bin/cli.js"
8
8
  },
9
+ "files": [
10
+ "bin",
11
+ "lib",
12
+ "index.js",
13
+ "README.md"
14
+ ],
9
15
  "scripts": {
10
16
  "start": "node bin/cli.js",
11
17
  "test": "node --test"