yingclaw 1.6.3 → 1.7.1

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,14 @@ 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
+
36
+ 工具会根据 Base URL 自动尝试获取模型列表;如果获取失败,则手动输入主模型和快速模型。
28
37
 
29
38
  **第三步:以后直接用**
30
39
  ```bash
@@ -40,6 +49,7 @@ claude
40
49
  | MiniMax | M2.7、M2.7 Turbo、M2.5 |
41
50
  | 智谱 GLM | GLM-4.7、GLM-5.1、GLM-5 Turbo、GLM-4.5 Air |
42
51
  | 小米 MiMo | MiMo V2.5 Pro |
52
+ | 自定义接口 | 自动获取或手动输入 |
43
53
 
44
54
  ## 其他命令
45
55
 
package/bin/cli.js CHANGED
@@ -7,6 +7,7 @@ const {
7
7
  saveConfig,
8
8
  writeEnvToZshrc,
9
9
  fetchModels,
10
+ fetchModelsFromBaseUrl,
10
11
  resetConfig,
11
12
  validateConfig,
12
13
  resolveFastModel,
@@ -18,6 +19,7 @@ const { execSync, spawn, spawnSync } = require('child_process');
18
19
  const https = require('https');
19
20
  const pkg = require('../package.json');
20
21
  const { buildMenuStatusLines, buildStatusView } = require('../lib/panel');
22
+ const { buildClaudeInstallCommand } = require('../lib/install');
21
23
 
22
24
  const program = new Command();
23
25
 
@@ -81,6 +83,96 @@ function isClaudeInstalled() {
81
83
  }
82
84
  }
83
85
 
86
+ function isValidUrl(value) {
87
+ try {
88
+ new URL(value);
89
+ return true;
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+
95
+ async function promptModelFromChoices({ chalk, choices, message, backLabel = '↩ 返回上一步', allowManual = true }) {
96
+ const selected = await select({ loop: false,
97
+ message: chalk.cyan(message),
98
+ choices: [
99
+ ...choices,
100
+ ...(allowManual ? [{ name: '手动输入模型名', value: '__MANUAL__' }] : []),
101
+ { name: chalk.dim(backLabel), value: '__BACK__' },
102
+ ],
103
+ });
104
+ if (selected === '__BACK__') return '__BACK__';
105
+ if (selected !== '__MANUAL__') return selected;
106
+
107
+ return input({
108
+ message: chalk.cyan('输入模型名'),
109
+ validate: (v) => v.trim().length > 0 ? true : '模型名不能为空',
110
+ }).then(v => v.trim());
111
+ }
112
+
113
+ async function promptManualModel(chalk, message, defaultValue) {
114
+ return input({
115
+ message: chalk.cyan(message),
116
+ default: defaultValue,
117
+ validate: (v) => v.trim().length > 0 ? true : '模型名不能为空',
118
+ }).then(v => v.trim());
119
+ }
120
+
121
+ async function configureCustomProvider({ chalk, ora, existingConfig }) {
122
+ const baseUrl = await input({
123
+ message: chalk.cyan('Anthropic Base URL'),
124
+ default: existingConfig?.provider === 'custom' ? existingConfig.baseUrl : undefined,
125
+ validate: (v) => v.trim().length > 0 && isValidUrl(v.trim()) ? true : '请输入有效 URL',
126
+ }).then(v => v.trim().replace(/\/+$/, ''));
127
+
128
+ let apiKey = existingConfig?.provider === 'custom' ? existingConfig.apiKey : '';
129
+ if (apiKey) {
130
+ const keepKey = await confirm({ message: '沿用当前 API Key?', default: true });
131
+ if (!keepKey) apiKey = '';
132
+ }
133
+ if (!apiKey) {
134
+ apiKey = await input({
135
+ message: chalk.cyan('API Key'),
136
+ transformer: (v) => v ? chalk.dim('•'.repeat(v.length)) : '',
137
+ validate: (v) => v.trim().length > 0 ? true : 'API Key 不能为空',
138
+ }).then(v => v.trim());
139
+ }
140
+
141
+ let modelChoices = [];
142
+ let modelsUrl;
143
+ const fetchSpinner = ora('正在自动获取可用模型...').start();
144
+ const onlineResult = await fetchModelsFromBaseUrl('custom', apiKey, baseUrl);
145
+ if (onlineResult) {
146
+ modelsUrl = onlineResult.modelsUrl;
147
+ fetchSpinner.succeed(chalk.green(`已获取 ${onlineResult.models.length} 个可用模型`));
148
+ modelChoices = onlineResult.models.map(id => ({ name: id, value: id }));
149
+ } else {
150
+ fetchSpinner.warn(chalk.yellow('无法自动获取模型列表,改为手动输入模型'));
151
+ }
152
+
153
+ let model;
154
+ let fastModel;
155
+ if (modelChoices.length > 0) {
156
+ model = await promptModelFromChoices({ chalk, choices: modelChoices, message: '选择主模型' });
157
+ if (model === '__BACK__') return null;
158
+ fastModel = await promptModelFromChoices({ chalk, choices: modelChoices, message: '选择快速模型 / Subagent 模型' });
159
+ if (fastModel === '__BACK__') return null;
160
+ } else {
161
+ model = await promptManualModel(chalk, '输入主模型名');
162
+ fastModel = await promptManualModel(chalk, '输入快速模型 / Subagent 模型名', model);
163
+ }
164
+
165
+ return {
166
+ provider: 'custom',
167
+ providerName: '自定义接口',
168
+ baseUrl,
169
+ modelsUrl: modelsUrl || undefined,
170
+ apiKey,
171
+ model,
172
+ fastModel,
173
+ };
174
+ }
175
+
84
176
  async function showStatus() {
85
177
  const chalk = (await import('chalk')).default;
86
178
  const boxen = (await import('boxen')).default;
@@ -176,14 +268,12 @@ program
176
268
  });
177
269
  if (network === '__BACK__') return;
178
270
 
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';
271
+ const installCommand = buildClaudeInstallCommand(network);
182
272
 
183
273
  console.log(chalk.dim('\n安装中,实时输出:\n'));
184
274
 
185
275
  // 实时输出安装日志
186
- const result = spawnSync(cmd, { shell: true, stdio: 'inherit' });
276
+ const result = spawnSync(installCommand.command, installCommand.args, { stdio: 'inherit' });
187
277
 
188
278
  if (result.status === 0) {
189
279
  console.log(chalk.green('\n✔ Claude Code 安装成功!'));
@@ -237,7 +327,7 @@ program
237
327
  if (!overwrite) return;
238
328
  }
239
329
 
240
- let providerKey, provider, apiKey, model;
330
+ let providerKey, provider, apiKey, model, customConfig;
241
331
  let step = 'provider';
242
332
 
243
333
  while (true) {
@@ -251,6 +341,11 @@ program
251
341
  });
252
342
  if (providerKey === '__BACK__') return;
253
343
  provider = PROVIDERS[providerKey];
344
+ if (provider.custom) {
345
+ customConfig = await configureCustomProvider({ chalk, ora });
346
+ if (!customConfig) { step = 'provider'; continue; }
347
+ break;
348
+ }
254
349
  step = 'apikey';
255
350
  } else if (step === 'apikey') {
256
351
  const k = await input({
@@ -289,10 +384,10 @@ program
289
384
  const spinner = ora('写入配置...').start();
290
385
  let result, file;
291
386
  try {
292
- const fastModel = resolveFastModel(provider, model);
293
- const cfg = { provider: providerKey, model, fastModel, apiKey, baseUrl: provider.baseUrl };
387
+ const fastModel = customConfig?.fastModel || resolveFastModel(provider, model);
388
+ const cfg = customConfig || { provider: providerKey, model, fastModel, apiKey, baseUrl: provider.baseUrl };
294
389
  saveConfig(cfg);
295
- ({ result, file } = writeEnvToZshrc(provider.baseUrl, apiKey, model, fastModel));
390
+ ({ result, file } = writeEnvToZshrc(cfg.baseUrl, cfg.apiKey, cfg.model, cfg.fastModel));
296
391
  spinner.succeed(chalk.green(result === 'updated' ? `环境变量已更新 → ${file}` : `环境变量已写入 → ${file}`));
297
392
  } catch (e) {
298
393
  spinner.fail(chalk.red(`写入失败: ${e.message}`));
@@ -302,7 +397,7 @@ program
302
397
 
303
398
  console.log(boxen(
304
399
  chalk.bold('配置完成!\n\n') +
305
- chalk.dim('ANTHROPIC_BASE_URL ') + chalk.cyan(provider.baseUrl) + '\n' +
400
+ chalk.dim('ANTHROPIC_BASE_URL ') + chalk.cyan((customConfig || { baseUrl: provider.baseUrl }).baseUrl) + '\n' +
306
401
  chalk.dim('ANTHROPIC_API_KEY ') + chalk.cyan('已保存') + '\n\n' +
307
402
  chalk.white('下次直接输入 ') + chalk.cyan.bold('claude') + chalk.white(' 即可使用'),
308
403
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
@@ -314,7 +409,7 @@ program
314
409
 
315
410
  spawn('claude', [], {
316
411
  stdio: 'inherit',
317
- env: { ...process.env, ...buildClaudeEnv({ provider: providerKey, baseUrl: provider.baseUrl, apiKey, model, fastModel: resolveFastModel(provider, model) }) },
412
+ env: { ...process.env, ...buildClaudeEnv(customConfig || { provider: providerKey, baseUrl: provider.baseUrl, apiKey, model, fastModel: resolveFastModel(provider, model) }) },
318
413
  }).on('error', () => {
319
414
  console.log(chalk.yellow('\nClaude Code 未找到,请先运行: claw install-claude'));
320
415
  });
@@ -351,6 +446,18 @@ program
351
446
  if (providerKey === '__BACK__') return;
352
447
 
353
448
  const provider = PROVIDERS[providerKey];
449
+ if (provider.custom) {
450
+ const customConfig = await configureCustomProvider({ chalk, ora, existingConfig: config });
451
+ if (!customConfig) return;
452
+
453
+ const spinner = ora('切换中...').start();
454
+ saveConfig(customConfig);
455
+ const { file } = writeEnvToZshrc(customConfig.baseUrl, customConfig.apiKey, customConfig.model, customConfig.fastModel);
456
+ await new Promise(r => setTimeout(r, 300));
457
+ spinner.succeed(chalk.green(`已切换至 ${customConfig.providerName} · ${customConfig.model}`));
458
+ console.log(chalk.dim(`运行 source ${file} 生效,或重新开一个终端`));
459
+ return;
460
+ }
354
461
 
355
462
  // 切换厂商时询问是否更换 Key
356
463
  let apiKey = config.apiKey;
@@ -562,11 +669,14 @@ async function runMenu() {
562
669
  }
563
670
  }
564
671
 
672
+ function handleCliError(e) {
673
+ if (e?.name === 'ExitPromptError') return; // Ctrl+C
674
+ console.error(e);
675
+ process.exitCode = 1;
676
+ }
677
+
565
678
  if (process.argv.length === 2) {
566
- runMenu().catch((e) => {
567
- if (e?.name === 'ExitPromptError') return; // Ctrl+C
568
- console.error(e);
569
- });
679
+ runMenu().catch(handleCliError);
570
680
  } else {
571
- program.parse(process.argv);
681
+ program.parseAsync(process.argv).catch(handleCliError);
572
682
  }
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,63 @@ 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
+
83
+ function buildModelUrlCandidates(baseUrl) {
84
+ let url;
85
+ try { url = new URL(baseUrl); } catch { return []; }
86
+
87
+ const pathname = url.pathname.replace(/\/+$/, '');
88
+ const candidates = [];
89
+ const add = (pathPart) => {
90
+ const normalized = pathPart.startsWith('/') ? pathPart : `/${pathPart}`;
91
+ candidates.push(`${url.origin}${normalized}`);
92
+ };
93
+
94
+ add(`${pathname}/v1/models`);
95
+
96
+ if (pathname.endsWith('/anthropic')) {
97
+ const withoutAnthropic = pathname.slice(0, -'/anthropic'.length);
98
+ add(`${withoutAnthropic}/v1/models`);
99
+ }
100
+
101
+ add('/v1/models');
102
+
103
+ return [...new Set(candidates)];
104
+ }
105
+
106
+ async function fetchModelsFromBaseUrl(providerKey, apiKey, baseUrl, fetcher = fetchModels) {
107
+ const effectiveProviderKey = providerKey === 'custom'
108
+ ? providerKeyFromBaseUrl(baseUrl) || providerKey
109
+ : providerKey;
110
+
111
+ for (const modelsUrl of buildModelUrlCandidates(baseUrl)) {
112
+ const models = await fetcher(effectiveProviderKey, apiKey, modelsUrl);
113
+ if (models && models.length > 0) {
114
+ return { modelsUrl, models: normalizeModelIds(effectiveProviderKey, models) };
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+
71
120
  // 联网拉取厂商支持的模型列表,失败返回 null
72
- function fetchModels(providerKey, apiKey) {
121
+ function fetchModels(providerKey, apiKey, modelsUrlOverride) {
73
122
  return new Promise((resolve) => {
74
123
  const provider = PROVIDERS[providerKey];
75
- if (!provider?.modelsUrl) return resolve(null);
124
+ const modelsUrl = modelsUrlOverride || provider?.modelsUrl;
125
+ if (!modelsUrl) return resolve(null);
76
126
 
77
127
  let url;
78
- try { url = new URL(provider.modelsUrl); } catch { return resolve(null); }
128
+ try { url = new URL(modelsUrl); } catch { return resolve(null); }
79
129
 
80
130
  const req = https.request({
81
131
  hostname: url.hostname,
82
- path: url.pathname,
132
+ path: url.pathname + url.search,
83
133
  method: 'GET',
84
134
  timeout: 6000,
85
135
  headers: {
@@ -91,13 +141,9 @@ function fetchModels(providerKey, apiKey) {
91
141
  res.on('data', (c) => data += c);
92
142
  res.on('end', () => {
93
143
  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);
144
+ const ids = parseModelIdsResponse(providerKey, data);
99
145
  if (ids.length === 0) return resolve(null);
100
- resolve(normalizeModelIds(providerKey, ids));
146
+ resolve(ids);
101
147
  } catch {
102
148
  resolve(null);
103
149
  }
@@ -257,6 +303,9 @@ module.exports = {
257
303
  resetConfig,
258
304
  validateConfig,
259
305
  normalizeModelIds,
306
+ parseModelIdsResponse,
307
+ buildModelUrlCandidates,
308
+ fetchModelsFromBaseUrl,
260
309
  resolveFastModel,
261
310
  providerKeyFromBaseUrl,
262
311
  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.1",
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"