yingclaw 2.2.3 → 2.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.
package/README.md CHANGED
@@ -4,6 +4,8 @@ Claude Code × 国产大模型,一键接入。
4
4
 
5
5
  支持 DeepSeek、Kimi、阿里云百炼(Qwen)、MiniMax、智谱 GLM、小米 MiMo,也支持自定义 Anthropic 兼容接口,无需梯子即可使用 Claude Code。
6
6
 
7
+ ![yingclaw 交互菜单](https://raw.githubusercontent.com/DengShiyingA/yingclaw/main/screenshot.png)
8
+
7
9
  ## 安装
8
10
 
9
11
  ```bash
@@ -112,7 +114,7 @@ CLAUDE_CODE_EFFORT_LEVEL
112
114
  **自定义接口**需支持 Anthropic `/v1/messages` 格式;工具会根据 Base URL 自动尝试获取模型列表,失败则手动输入。
113
115
 
114
116
  ## 卸载
115
-
117
+
116
118
  ```bash
117
119
  npm uninstall -g yingclaw
118
120
  claw reset # 可选:清除已写入的环境变量和桌面配置
package/bin/cli.js CHANGED
@@ -11,10 +11,10 @@ const {
11
11
  fetchModelsFromBaseUrl,
12
12
  resetConfig,
13
13
  validateConfig,
14
+ validateKey,
14
15
  normalizeAnthropicBaseUrl,
15
16
  resolveFastModel,
16
17
  buildClaudeEnv,
17
- classifyValidationStatus,
18
18
  PROVIDERS,
19
19
  CLAUDE_ENV_KEYS,
20
20
  } = require('../lib/config');
@@ -23,6 +23,7 @@ const pkg = require('../package.json');
23
23
  const { buildMenuStatusLines, buildStatusView } = require('../lib/panel');
24
24
  const { buildClaudeInstallCommand } = require('../lib/install');
25
25
  const { clearClaudeDesktopConfig, isDesktopConfigured, openClaudeDesktop, writeClaudeDesktopConfig } = require('../lib/desktop');
26
+ const { runDoctorChecks, summarize, STATUS_OK, STATUS_FAIL, STATUS_WARN, STATUS_INFO } = require('../lib/doctor');
26
27
 
27
28
  const program = new Command();
28
29
 
@@ -38,42 +39,6 @@ async function getBanner() {
38
39
  );
39
40
  }
40
41
 
41
- async function validateKey(config) {
42
- let url;
43
- try {
44
- url = `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
45
- new URL(url);
46
- } catch {
47
- return null;
48
- }
49
-
50
- const controller = new AbortController();
51
- const timeout = setTimeout(() => controller.abort(), 8000);
52
-
53
- try {
54
- const res = await fetch(url, {
55
- method: 'POST',
56
- signal: controller.signal,
57
- headers: {
58
- 'content-type': 'application/json',
59
- authorization: `Bearer ${config.apiKey}`,
60
- 'x-api-key': config.apiKey,
61
- 'anthropic-version': '2023-06-01',
62
- },
63
- body: JSON.stringify({
64
- model: config.model,
65
- max_tokens: 1,
66
- messages: [{ role: 'user', content: 'hi' }],
67
- }),
68
- });
69
- return classifyValidationStatus(res.status);
70
- } catch {
71
- return null;
72
- } finally {
73
- clearTimeout(timeout);
74
- }
75
- }
76
-
77
42
  function getConfigValidationMessage(config) {
78
43
  const validation = validateConfig(config);
79
44
  return validation.valid ? null : validation.message;
@@ -857,6 +822,52 @@ program
857
822
  }
858
823
  });
859
824
 
825
+ program
826
+ .command('doctor')
827
+ .description('诊断当前环境,列出所有问题和修复建议')
828
+ .action(async () => {
829
+ const chalk = (await import('chalk')).default;
830
+ const ora = (await import('ora')).default;
831
+ const boxen = (await import('boxen')).default;
832
+
833
+ console.log(await getBanner());
834
+
835
+ const spinner = ora('运行诊断检查...').start();
836
+ const checks = await runDoctorChecks();
837
+ spinner.stop();
838
+
839
+ const icons = {
840
+ [STATUS_OK]: chalk.green('✓'),
841
+ [STATUS_FAIL]: chalk.red('✗'),
842
+ [STATUS_WARN]: chalk.yellow('⚠'),
843
+ [STATUS_INFO]: chalk.cyan('ℹ'),
844
+ };
845
+
846
+ console.log();
847
+ for (const c of checks) {
848
+ console.log(` ${icons[c.status]} ${chalk.bold(c.name)} ${chalk.dim(c.message)}`);
849
+ if (c.fix) console.log(` ${chalk.dim('→')} ${chalk.cyan(c.fix)}`);
850
+ }
851
+
852
+ const counts = summarize(checks);
853
+ const summaryBits = [];
854
+ if (counts.fail) summaryBits.push(chalk.red(`${counts.fail} 个错误`));
855
+ if (counts.warn) summaryBits.push(chalk.yellow(`${counts.warn} 个警告`));
856
+ if (counts.ok) summaryBits.push(chalk.green(`${counts.ok} 个通过`));
857
+
858
+ const allOk = counts.fail === 0 && counts.warn === 0;
859
+ console.log(boxen(
860
+ (allOk ? chalk.bold.green('✓ 一切正常') : chalk.bold('诊断完成')) +
861
+ (summaryBits.length ? '\n\n' + summaryBits.join(' · ') : ''),
862
+ {
863
+ padding: { top: 0, bottom: 0, left: 2, right: 2 },
864
+ borderStyle: 'round',
865
+ borderColor: counts.fail ? 'red' : counts.warn ? 'yellow' : 'green',
866
+ margin: { top: 1, bottom: 1 },
867
+ }
868
+ ));
869
+ });
870
+
860
871
  program
861
872
  .command('update')
862
873
  .description('检查并更新 yingclaw 到最新版本')
@@ -997,6 +1008,7 @@ async function runAdvancedMenu(chalk, hasConfig) {
997
1008
  const action = await select({ loop: false,
998
1009
  message: chalk.cyan('高级选项'),
999
1010
  choices: [
1011
+ { name: '🩺 诊断(一键自检并给出修复建议)', value: 'doctor' },
1000
1012
  { name: '🔁 重新检测 API', value: 'recheck', disabled: !hasConfig && ADVANCED_DISABLED_HINT },
1001
1013
  { name: '↩️ 恢复 Claude Code 终端默认', value: 'code-reset' },
1002
1014
  { name: '↩️ 恢复 Claude 桌面默认', value: 'desktop-reset' },
@@ -1105,6 +1117,7 @@ async function runMenu() {
1105
1117
  status: 'status',
1106
1118
  reset: 'reset',
1107
1119
  update: 'update',
1120
+ doctor: 'doctor',
1108
1121
  };
1109
1122
 
1110
1123
  // 执行子命令(用 spawn 隔离,避免 commander 对 program 的副作用)
package/lib/config.js CHANGED
@@ -283,6 +283,42 @@ function classifyValidationStatus(statusCode) {
283
283
  return null;
284
284
  }
285
285
 
286
+ // 发一次最小的 /v1/messages 请求验证 Key(true=有效, false=无效, null=网络/服务异常)
287
+ async function validateKey(config, options = {}) {
288
+ let url;
289
+ try {
290
+ url = `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
291
+ new URL(url);
292
+ } catch {
293
+ return null;
294
+ }
295
+
296
+ const controller = new AbortController();
297
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs || 8000);
298
+ try {
299
+ const res = await fetch(url, {
300
+ method: 'POST',
301
+ signal: controller.signal,
302
+ headers: {
303
+ 'content-type': 'application/json',
304
+ authorization: `Bearer ${config.apiKey}`,
305
+ 'x-api-key': config.apiKey,
306
+ 'anthropic-version': '2023-06-01',
307
+ },
308
+ body: JSON.stringify({
309
+ model: config.model,
310
+ max_tokens: 1,
311
+ messages: [{ role: 'user', content: 'hi' }],
312
+ }),
313
+ });
314
+ return classifyValidationStatus(res.status);
315
+ } catch {
316
+ return null;
317
+ } finally {
318
+ clearTimeout(timeout);
319
+ }
320
+ }
321
+
286
322
  function shellQuote(value) {
287
323
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
288
324
  }
@@ -405,6 +441,7 @@ module.exports = {
405
441
  buildClaudeEnv,
406
442
  buildEnvBlock,
407
443
  classifyValidationStatus,
444
+ validateKey,
408
445
  PROVIDERS,
409
446
  CONFIG_FILE,
410
447
  CLAUDE_ENV_KEYS,
package/lib/doctor.js ADDED
@@ -0,0 +1,192 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const { execSync, spawnSync } = require('child_process');
5
+ const {
6
+ loadConfig,
7
+ validateConfig,
8
+ validateKey,
9
+ buildClaudeEnv,
10
+ normalizeAnthropicBaseUrl,
11
+ PROVIDERS,
12
+ CLAUDE_ENV_KEYS,
13
+ } = require('./config');
14
+ const { isDesktopConfigured } = require('./desktop');
15
+
16
+ const STATUS_OK = 'ok';
17
+ const STATUS_FAIL = 'fail';
18
+ const STATUS_WARN = 'warn';
19
+ const STATUS_INFO = 'info';
20
+
21
+ async function runDoctorChecks(options = {}) {
22
+ const checks = [];
23
+ const platform = options.platform || process.platform;
24
+ const env = options.env || process.env;
25
+
26
+ // 1. Node 版本
27
+ const nodeVersion = process.versions.node;
28
+ const nodeMajor = parseInt(nodeVersion.split('.')[0], 10);
29
+ checks.push({
30
+ name: 'Node 版本',
31
+ status: nodeMajor >= 18 ? STATUS_OK : STATUS_FAIL,
32
+ message: `v${nodeVersion}${nodeMajor >= 18 ? '' : '(< 18)'}`,
33
+ fix: nodeMajor < 18 ? '从 https://nodejs.org 下载 LTS 版本(≥18)' : null,
34
+ });
35
+
36
+ // 2. Claude Code
37
+ let claudeVersion = null;
38
+ try {
39
+ claudeVersion = execSync('claude --version', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
40
+ } catch {}
41
+ checks.push({
42
+ name: 'Claude Code',
43
+ status: claudeVersion ? STATUS_OK : STATUS_WARN,
44
+ message: claudeVersion || '未安装',
45
+ fix: claudeVersion ? null : '运行 claw install-claude',
46
+ });
47
+
48
+ // 3. 配置文件
49
+ const config = loadConfig();
50
+ if (!config) {
51
+ checks.push({
52
+ name: '配置文件',
53
+ status: STATUS_FAIL,
54
+ message: '~/.clawai.json 不存在',
55
+ fix: '运行 claw config',
56
+ });
57
+ return checks;
58
+ }
59
+ const validation = validateConfig(config);
60
+ if (!validation.valid) {
61
+ checks.push({
62
+ name: '配置文件',
63
+ status: STATUS_FAIL,
64
+ message: `~/.clawai.json 无效:${validation.message}`,
65
+ fix: '运行 claw config 重新配置',
66
+ });
67
+ return checks;
68
+ }
69
+ const provider = PROVIDERS[config.provider];
70
+ checks.push({
71
+ name: '配置文件',
72
+ status: STATUS_OK,
73
+ message: `${provider?.name || config.provider} · ${config.model}`,
74
+ });
75
+
76
+ // 4. shell rc / Windows 环境变量已写入
77
+ if (platform === 'win32') {
78
+ const result = checkWindowsEnvVars(options);
79
+ checks.push({
80
+ name: 'Windows 用户环境变量',
81
+ status: result.allWritten ? STATUS_OK : STATUS_WARN,
82
+ message: result.allWritten ? '已写入' : `${result.missing.length} 个变量未写入`,
83
+ fix: result.allWritten ? null : '运行 claw code',
84
+ });
85
+ } else {
86
+ const rcCheck = checkShellRcBlock(options);
87
+ checks.push({
88
+ name: 'shell 配置文件',
89
+ status: rcCheck.found ? STATUS_OK : STATUS_WARN,
90
+ message: rcCheck.found ? `${rcCheck.file} 已写入 yingclaw 块` : '未找到 yingclaw 配置块',
91
+ fix: rcCheck.found ? null : '运行 claw code',
92
+ });
93
+ }
94
+
95
+ // 5. 当前终端环境变量是否生效
96
+ const expected = buildClaudeEnv(config);
97
+ const inactive = Object.entries(expected).filter(([k, v]) => env[k] !== v);
98
+ checks.push({
99
+ name: '当前终端环境变量',
100
+ status: inactive.length === 0 ? STATUS_OK : STATUS_WARN,
101
+ message: inactive.length === 0 ? '已生效' : `${inactive.length}/${Object.keys(expected).length} 未生效`,
102
+ fix: inactive.length === 0
103
+ ? null
104
+ : (platform === 'win32' ? '重新打开 PowerShell / CMD' : '运行 source ~/.zshrc 或重开终端'),
105
+ });
106
+
107
+ // 6. API Key(顺便确认网络可达)
108
+ const valid = await validateKey(config, { timeoutMs: 6000 });
109
+ if (valid === true) {
110
+ checks.push({ name: 'API Key', status: STATUS_OK, message: '校验通过' });
111
+ } else if (valid === false) {
112
+ checks.push({
113
+ name: 'API Key',
114
+ status: STATUS_FAIL,
115
+ message: '无效或已过期(401/403)',
116
+ fix: '运行 claw config 重新输入 Key',
117
+ });
118
+ } else {
119
+ checks.push({
120
+ name: 'API Key',
121
+ status: STATUS_WARN,
122
+ message: '无法连接(网络 / 服务异常)',
123
+ fix: '检查网络 / VPN,或确认 Base URL 没拼错',
124
+ });
125
+ }
126
+
127
+ // 7. Claude 桌面应用接入状态
128
+ const desktopConfigured = isDesktopConfigured();
129
+ checks.push({
130
+ name: 'Claude 桌面应用',
131
+ status: STATUS_INFO,
132
+ message: desktopConfigured ? '已通过 yingclaw 接入' : '未接入(如需运行 claw desktop)',
133
+ });
134
+
135
+ // 8. DeepSeek 旧模型名提醒
136
+ if (config.provider === 'deepseek' && (
137
+ config.model === 'deepseek-v4-pro' ||
138
+ config.model === 'deepseek-v4-flash' ||
139
+ config.fastModel === 'deepseek-v4-flash'
140
+ )) {
141
+ checks.push({
142
+ name: '模型名',
143
+ status: STATUS_WARN,
144
+ message: '使用旧 DeepSeek 模型名',
145
+ fix: '运行 claw switch 升级到 [1m] 长上下文版本',
146
+ });
147
+ }
148
+
149
+ return checks;
150
+ }
151
+
152
+ function checkShellRcBlock(options = {}) {
153
+ const homeDir = options.homeDir || os.homedir();
154
+ const rcFiles = options.rcFiles || [
155
+ path.join(homeDir, '.zshrc'),
156
+ path.join(homeDir, '.bashrc'),
157
+ path.join(homeDir, '.bash_profile'),
158
+ ];
159
+ for (const file of rcFiles) {
160
+ if (!fs.existsSync(file)) continue;
161
+ const content = fs.readFileSync(file, 'utf8');
162
+ if (content.includes('# clawai-start')) return { found: true, file };
163
+ }
164
+ return { found: false };
165
+ }
166
+
167
+ function checkWindowsEnvVars(options = {}) {
168
+ const runner = options.runner || spawnSync;
169
+ const missing = [];
170
+ for (const key of CLAUDE_ENV_KEYS) {
171
+ const result = runner('reg', ['query', 'HKCU\\Environment', '/V', key], { stdio: 'pipe', encoding: 'utf8' });
172
+ if (result.status !== 0) missing.push(key);
173
+ }
174
+ return { allWritten: missing.length === 0, missing };
175
+ }
176
+
177
+ function summarize(checks) {
178
+ const counts = { ok: 0, fail: 0, warn: 0, info: 0 };
179
+ for (const c of checks) counts[c.status] = (counts[c.status] || 0) + 1;
180
+ return counts;
181
+ }
182
+
183
+ module.exports = {
184
+ runDoctorChecks,
185
+ checkShellRcBlock,
186
+ checkWindowsEnvVars,
187
+ summarize,
188
+ STATUS_OK,
189
+ STATUS_FAIL,
190
+ STATUS_WARN,
191
+ STATUS_INFO,
192
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yingclaw",
3
- "version": "2.2.3",
3
+ "version": "2.3.0",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Kimi、Qwen、MiniMax、GLM、MiMo",
5
5
  "main": "index.js",
6
6
  "bin": {