yingclaw 1.6.2 → 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 +13 -2
- package/bin/cli.js +130 -15
- package/index.js +1 -0
- package/lib/config.js +32 -10
- package/lib/install.js +9 -0
- package/lib/panel.js +1 -1
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Claude Code × 国产大模型,一键接入。
|
|
4
4
|
|
|
5
|
-
支持 DeepSeek、阿里云百炼(Qwen)、MiniMax、智谱 GLM、小米 MiMo
|
|
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
|
-
选择厂商 →
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
567
|
-
if (e?.name === 'ExitPromptError') return; // Ctrl+C
|
|
568
|
-
console.error(e);
|
|
569
|
-
});
|
|
684
|
+
runMenu().catch(handleCliError);
|
|
570
685
|
} else {
|
|
571
|
-
program.
|
|
686
|
+
program.parseAsync(process.argv).catch(handleCliError);
|
|
572
687
|
}
|
package/index.js
CHANGED
package/lib/config.js
CHANGED
|
@@ -12,8 +12,8 @@ const PROVIDERS = {
|
|
|
12
12
|
modelsUrl: 'https://api.deepseek.com/v1/models',
|
|
13
13
|
fastModel: 'deepseek-v4-flash',
|
|
14
14
|
models: [
|
|
15
|
-
{ name: 'DeepSeek V4 Flash(快速)', value: 'deepseek-v4-flash' },
|
|
16
15
|
{ name: 'DeepSeek V4 Pro(强力)', value: 'deepseek-v4-pro[1m]' },
|
|
16
|
+
{ name: 'DeepSeek V4 Flash(快速)', value: 'deepseek-v4-flash' },
|
|
17
17
|
],
|
|
18
18
|
},
|
|
19
19
|
qwen: {
|
|
@@ -55,20 +55,44 @@ 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
|
|
|
65
|
+
function normalizeModelIds(providerKey, ids) {
|
|
66
|
+
if (providerKey !== 'deepseek') return ids;
|
|
67
|
+
|
|
68
|
+
const mapped = ids.map((id) => id === 'deepseek-v4-pro' ? 'deepseek-v4-pro[1m]' : id);
|
|
69
|
+
const preferred = ['deepseek-v4-pro[1m]', 'deepseek-v4-flash'];
|
|
70
|
+
return [
|
|
71
|
+
...preferred.filter((id) => mapped.includes(id)),
|
|
72
|
+
...mapped.filter((id) => !preferred.includes(id)),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
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
|
+
|
|
60
83
|
// 联网拉取厂商支持的模型列表,失败返回 null
|
|
61
|
-
function fetchModels(providerKey, apiKey) {
|
|
84
|
+
function fetchModels(providerKey, apiKey, modelsUrlOverride) {
|
|
62
85
|
return new Promise((resolve) => {
|
|
63
86
|
const provider = PROVIDERS[providerKey];
|
|
64
|
-
|
|
87
|
+
const modelsUrl = modelsUrlOverride || provider?.modelsUrl;
|
|
88
|
+
if (!modelsUrl) return resolve(null);
|
|
65
89
|
|
|
66
90
|
let url;
|
|
67
|
-
try { url = new URL(
|
|
91
|
+
try { url = new URL(modelsUrl); } catch { return resolve(null); }
|
|
68
92
|
|
|
69
93
|
const req = https.request({
|
|
70
94
|
hostname: url.hostname,
|
|
71
|
-
path: url.pathname,
|
|
95
|
+
path: url.pathname + url.search,
|
|
72
96
|
method: 'GET',
|
|
73
97
|
timeout: 6000,
|
|
74
98
|
headers: {
|
|
@@ -80,11 +104,7 @@ function fetchModels(providerKey, apiKey) {
|
|
|
80
104
|
res.on('data', (c) => data += c);
|
|
81
105
|
res.on('end', () => {
|
|
82
106
|
try {
|
|
83
|
-
const
|
|
84
|
-
// OpenAI 格式: { data: [{ id, ... }] }
|
|
85
|
-
// GLM 格式: { data: [{ id, ... }] } 类似
|
|
86
|
-
const list = parsed.data || parsed.models || [];
|
|
87
|
-
const ids = list.map(m => m.id || m.model || m.name).filter(Boolean);
|
|
107
|
+
const ids = parseModelIdsResponse(providerKey, data);
|
|
88
108
|
if (ids.length === 0) return resolve(null);
|
|
89
109
|
resolve(ids);
|
|
90
110
|
} catch {
|
|
@@ -245,6 +265,8 @@ module.exports = {
|
|
|
245
265
|
fetchModels,
|
|
246
266
|
resetConfig,
|
|
247
267
|
validateConfig,
|
|
268
|
+
normalizeModelIds,
|
|
269
|
+
parseModelIdsResponse,
|
|
248
270
|
resolveFastModel,
|
|
249
271
|
providerKeyFromBaseUrl,
|
|
250
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.
|
|
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"
|