yingclaw 2.2.2 → 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;
@@ -787,7 +752,7 @@ program
787
752
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
788
753
  ));
789
754
 
790
- if (process.platform === 'darwin') {
755
+ if (process.platform === 'darwin' || process.platform === 'win32') {
791
756
  const shouldReopen = await confirm({ message: '是否现在重启 Claude 桌面应用使默认配置生效?', default: true });
792
757
  if (shouldReopen) {
793
758
  const openSpinner = ora('正在重启 Claude 桌面应用...').start();
@@ -795,8 +760,15 @@ program
795
760
  await openClaudeDesktop();
796
761
  openSpinner.succeed(chalk.green('Claude 桌面应用已重启(已恢复 1P 模式)'));
797
762
  } catch (e) {
798
- openSpinner.fail(chalk.red(`打开失败: ${e.message}`));
799
- console.log(chalk.dim('请手动完全退出 Claude 后重新打开。'));
763
+ openSpinner.fail(chalk.red(`自动重启失败: ${e.message}`));
764
+ if (process.platform === 'win32') {
765
+ console.log(chalk.yellow('\n请手动操作(仅关闭窗口不够,进程还在系统托盘):'));
766
+ console.log(chalk.dim(' 1. 任务栏右下角找 Claude 图标 → 右键 → 退出'));
767
+ console.log(chalk.dim(' 2. 或在任务管理器中结束所有 Claude.exe 进程'));
768
+ console.log(chalk.dim(' 3. 然后重新打开 Claude'));
769
+ } else {
770
+ console.log(chalk.dim('请手动完全退出 Claude 后重新打开。'));
771
+ }
800
772
  }
801
773
  }
802
774
  } else {
@@ -850,6 +822,52 @@ program
850
822
  }
851
823
  });
852
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
+
853
871
  program
854
872
  .command('update')
855
873
  .description('检查并更新 yingclaw 到最新版本')
@@ -990,6 +1008,7 @@ async function runAdvancedMenu(chalk, hasConfig) {
990
1008
  const action = await select({ loop: false,
991
1009
  message: chalk.cyan('高级选项'),
992
1010
  choices: [
1011
+ { name: '🩺 诊断(一键自检并给出修复建议)', value: 'doctor' },
993
1012
  { name: '🔁 重新检测 API', value: 'recheck', disabled: !hasConfig && ADVANCED_DISABLED_HINT },
994
1013
  { name: '↩️ 恢复 Claude Code 终端默认', value: 'code-reset' },
995
1014
  { name: '↩️ 恢复 Claude 桌面默认', value: 'desktop-reset' },
@@ -1098,6 +1117,7 @@ async function runMenu() {
1098
1117
  status: 'status',
1099
1118
  reset: 'reset',
1100
1119
  update: 'update',
1120
+ doctor: 'doctor',
1101
1121
  };
1102
1122
 
1103
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.2",
3
+ "version": "2.3.0",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Kimi、Qwen、MiniMax、GLM、MiMo",
5
5
  "main": "index.js",
6
6
  "bin": {