yingclaw 1.5.2 → 1.6.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
@@ -50,7 +50,19 @@ claw status # 查看当前配置,验证 Key 是否有效
50
50
 
51
51
  ## 原理
52
52
 
53
- 各厂商均原生支持 Anthropic API 格式,只需设置 `ANTHROPIC_BASE_URL` `ANTHROPIC_API_KEY` 两个环境变量即可。本工具自动完成配置,无需手动修改任何文件。
53
+ 各厂商均原生支持 Anthropic API 格式。本工具会自动写入 Claude Code 所需的环境变量,包括:
54
+
55
+ - `ANTHROPIC_BASE_URL`
56
+ - `ANTHROPIC_AUTH_TOKEN`
57
+ - `ANTHROPIC_API_KEY`
58
+ - `ANTHROPIC_MODEL`
59
+ - `ANTHROPIC_DEFAULT_OPUS_MODEL`
60
+ - `ANTHROPIC_DEFAULT_SONNET_MODEL`
61
+ - `ANTHROPIC_DEFAULT_HAIKU_MODEL`
62
+ - `CLAUDE_CODE_SUBAGENT_MODEL`
63
+ - `CLAUDE_CODE_EFFORT_LEVEL`
64
+
65
+ 以 DeepSeek 为例,主模型默认使用 `deepseek-v4-pro[1m]`,Haiku/Subagent 快速模型使用 `deepseek-v4-flash`。如果在线模型列表获取失败,会回退到内置默认列表。
54
66
 
55
67
  ## License
56
68
 
package/bin/cli.js CHANGED
@@ -2,10 +2,22 @@
2
2
 
3
3
  const { Command } = require('commander');
4
4
  const { select, input, confirm } = require('@inquirer/prompts');
5
- const { loadConfig, saveConfig, writeEnvToZshrc, fetchModels, PROVIDERS } = require('../lib/config');
5
+ const {
6
+ loadConfig,
7
+ saveConfig,
8
+ writeEnvToZshrc,
9
+ fetchModels,
10
+ resetConfig,
11
+ validateConfig,
12
+ resolveFastModel,
13
+ buildClaudeEnv,
14
+ classifyValidationStatus,
15
+ PROVIDERS,
16
+ } = require('../lib/config');
6
17
  const { execSync, spawn, spawnSync } = require('child_process');
7
18
  const https = require('https');
8
19
  const pkg = require('../package.json');
20
+ const { buildStatusView } = require('../lib/panel');
9
21
 
10
22
  const program = new Command();
11
23
 
@@ -46,9 +58,7 @@ async function validateKey(config) {
46
58
  'content-length': body.length,
47
59
  },
48
60
  }, (res) => {
49
- // 401/403 = Key 无效;2xx/400/500 都说明 Key 被识别了
50
- if (res.statusCode === 401 || res.statusCode === 403) resolve(false);
51
- else resolve(true);
61
+ resolve(classifyValidationStatus(res.statusCode));
52
62
  });
53
63
  req.on('error', () => resolve(null));
54
64
  req.on('timeout', () => { req.destroy(); resolve(null); });
@@ -57,6 +67,20 @@ async function validateKey(config) {
57
67
  });
58
68
  }
59
69
 
70
+ function getConfigValidationMessage(config) {
71
+ const validation = validateConfig(config);
72
+ return validation.valid ? null : validation.message;
73
+ }
74
+
75
+ function isClaudeInstalled() {
76
+ try {
77
+ execSync('claude --version', { stdio: 'pipe' });
78
+ return true;
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+
60
84
  async function showStatus() {
61
85
  const chalk = (await import('chalk')).default;
62
86
  const boxen = (await import('boxen')).default;
@@ -67,8 +91,13 @@ async function showStatus() {
67
91
  console.log(chalk.red('\n未配置,请先运行: claw setup\n'));
68
92
  return;
69
93
  }
94
+ const configProblem = getConfigValidationMessage(config);
95
+ if (configProblem) {
96
+ console.log(chalk.red(`\n配置无效:${configProblem}`));
97
+ console.log(chalk.dim('请运行 claw setup 重新配置。\n'));
98
+ return;
99
+ }
70
100
 
71
- const provider = PROVIDERS[config.provider];
72
101
  const spinner = ora('验证 API Key...').start();
73
102
  const valid = await validateKey(config);
74
103
 
@@ -76,12 +105,34 @@ async function showStatus() {
76
105
  else if (valid === false) spinner.fail('API Key 无效或已过期');
77
106
  else spinner.warn('网络异常,无法验证');
78
107
 
79
- const lines = [
80
- `${chalk.dim('厂商 ')} ${chalk.white.bold(provider?.name || config.provider)}`,
81
- `${chalk.dim('模型 ')} ${chalk.yellow(config.model)}`,
82
- `${chalk.dim('API Key ')} ${chalk.dim(config.apiKey.slice(0, 10) + '...')} ${valid === true ? chalk.green('✔') : valid === false ? chalk.red('✘') : chalk.yellow('?')}`,
83
- `${chalk.dim('Base URL')} ${chalk.cyan(config.baseUrl)}`,
84
- ];
108
+ const view = buildStatusView(config, {
109
+ apiStatus: valid,
110
+ claudeInstalled: isClaudeInstalled(),
111
+ env: process.env,
112
+ });
113
+
114
+ const lines = view.lines.map(({ label, value }) => {
115
+ const coloredValue = label === '厂商'
116
+ ? chalk.white.bold(value)
117
+ : label.includes('模型')
118
+ ? chalk.yellow(value)
119
+ : label === 'Base URL'
120
+ ? chalk.cyan(value)
121
+ : label === '当前终端' && value === '未生效'
122
+ ? chalk.yellow(value)
123
+ : value;
124
+ return `${chalk.dim(label + ':')} ${coloredValue}`;
125
+ });
126
+
127
+ for (const warning of view.warnings) {
128
+ if (warning.includes('建议运行')) {
129
+ const [summary, action] = warning.split(',建议运行 ');
130
+ lines.push(`${chalk.yellow('提示:')} ${chalk.yellow(summary)}`);
131
+ lines.push(`${chalk.yellow('建议:')} ${chalk.yellow(`运行 ${action}`)}`);
132
+ } else {
133
+ lines.push(`${chalk.yellow('提示:')} ${chalk.yellow(warning)}`);
134
+ }
135
+ }
85
136
 
86
137
  console.log(boxen(lines.join('\n'), {
87
138
  title: chalk.bold('当前配置'),
@@ -179,7 +230,7 @@ program
179
230
  chalk.bold('当前配置\n\n') +
180
231
  chalk.dim('厂商 ') + chalk.white(existingProvider?.name || existing.provider) + '\n' +
181
232
  chalk.dim('模型 ') + chalk.yellow(existing.model) + '\n' +
182
- chalk.dim('Key ') + chalk.dim(existing.apiKey.slice(0, 10) + '...'),
233
+ chalk.dim('Key ') + chalk.dim(existing.apiKey ? '已保存' : '缺失'),
183
234
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'yellow', margin: { top: 1, bottom: 1 } }
184
235
  ));
185
236
  const overwrite = await confirm({ message: '覆盖现有配置?', default: false });
@@ -238,9 +289,10 @@ program
238
289
  const spinner = ora('写入配置...').start();
239
290
  let result, file;
240
291
  try {
241
- const cfg = { provider: providerKey, model, apiKey, baseUrl: provider.baseUrl };
292
+ const fastModel = resolveFastModel(provider, model);
293
+ const cfg = { provider: providerKey, model, fastModel, apiKey, baseUrl: provider.baseUrl };
242
294
  saveConfig(cfg);
243
- ({ result, file } = writeEnvToZshrc(provider.baseUrl, apiKey));
295
+ ({ result, file } = writeEnvToZshrc(provider.baseUrl, apiKey, model, fastModel));
244
296
  spinner.succeed(chalk.green(result === 'updated' ? `环境变量已更新 → ${file}` : `环境变量已写入 → ${file}`));
245
297
  } catch (e) {
246
298
  spinner.fail(chalk.red(`写入失败: ${e.message}`));
@@ -251,7 +303,7 @@ program
251
303
  console.log(boxen(
252
304
  chalk.bold('配置完成!\n\n') +
253
305
  chalk.dim('ANTHROPIC_BASE_URL ') + chalk.cyan(provider.baseUrl) + '\n' +
254
- chalk.dim('ANTHROPIC_API_KEY ') + chalk.cyan(apiKey.slice(0, 10) + '...') + '\n\n' +
306
+ chalk.dim('ANTHROPIC_API_KEY ') + chalk.cyan('已保存') + '\n\n' +
255
307
  chalk.white('下次直接输入 ') + chalk.cyan.bold('claude') + chalk.white(' 即可使用'),
256
308
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
257
309
  ));
@@ -262,7 +314,7 @@ program
262
314
 
263
315
  spawn('claude', [], {
264
316
  stdio: 'inherit',
265
- env: { ...process.env, ANTHROPIC_BASE_URL: provider.baseUrl, ANTHROPIC_API_KEY: apiKey },
317
+ env: { ...process.env, ...buildClaudeEnv({ provider: providerKey, baseUrl: provider.baseUrl, apiKey, model, fastModel: resolveFastModel(provider, model) }) },
266
318
  }).on('error', () => {
267
319
  console.log(chalk.yellow('\nClaude Code 未找到,请先运行: claw install-claude'));
268
320
  });
@@ -282,6 +334,12 @@ program
282
334
  console.log(chalk.red('\n未配置,请先运行: claw setup\n'));
283
335
  return;
284
336
  }
337
+ const configProblem = getConfigValidationMessage(config);
338
+ if (configProblem) {
339
+ console.log(chalk.red(`\n配置无效:${configProblem}`));
340
+ console.log(chalk.dim('请运行 claw setup 重新配置。\n'));
341
+ return;
342
+ }
285
343
 
286
344
  const providerKey = await select({ loop: false,
287
345
  message: chalk.cyan('选择 AI 厂商'),
@@ -330,9 +388,10 @@ program
330
388
  if (model === '__BACK__') return;
331
389
 
332
390
  const spinner = ora('切换中...').start();
333
- const newConfig = { ...config, provider: providerKey, model, baseUrl: provider.baseUrl, apiKey };
391
+ const fastModel = resolveFastModel(provider, model);
392
+ const newConfig = { ...config, provider: providerKey, model, fastModel, baseUrl: provider.baseUrl, apiKey };
334
393
  saveConfig(newConfig);
335
- const { file } = writeEnvToZshrc(provider.baseUrl, apiKey);
394
+ const { file } = writeEnvToZshrc(provider.baseUrl, apiKey, model, fastModel);
336
395
  await new Promise(r => setTimeout(r, 300));
337
396
  spinner.succeed(chalk.green(`已切换至 ${provider.name} · ${model}`));
338
397
  console.log(chalk.dim(`运行 source ${file} 生效,或重新开一个终端`));
@@ -343,19 +402,57 @@ program
343
402
  .description('查看当前配置和 Key 有效性')
344
403
  .action(showStatus);
345
404
 
405
+ program
406
+ .command('reset')
407
+ .description('恢复默认(清除所有 clawai 配置)')
408
+ .action(async () => {
409
+ const chalk = (await import('chalk')).default;
410
+ const ora = (await import('ora')).default;
411
+ const boxen = (await import('boxen')).default;
412
+
413
+ console.log(await getBanner());
414
+
415
+ const yes = await confirm({
416
+ message: chalk.red('确定要清除所有 clawai 配置吗?此操作不可撤销'),
417
+ default: false,
418
+ });
419
+ if (!yes) {
420
+ console.log(chalk.dim('已取消'));
421
+ return;
422
+ }
423
+
424
+ const spinner = ora('清除中...').start();
425
+ const cleared = resetConfig();
426
+ await new Promise(r => setTimeout(r, 300));
427
+
428
+ if (cleared.length === 0) {
429
+ spinner.warn(chalk.yellow('没有找到任何配置,无需清除'));
430
+ } else {
431
+ spinner.succeed(chalk.green('已恢复默认'));
432
+ console.log(boxen(
433
+ chalk.bold('已清除以下文件中的 clawai 配置:\n\n') +
434
+ cleared.map(f => chalk.cyan(' • ' + f)).join('\n') +
435
+ '\n\n' + chalk.dim('注:当前终端的环境变量还在内存中,重开终端或 unset 才彻底清除'),
436
+ { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
437
+ ));
438
+ }
439
+ });
440
+
346
441
  async function renderStatusBar(apiStatus) {
347
442
  const chalk = (await import('chalk')).default;
348
443
  const config = loadConfig();
349
- const claudeInstalled = (() => {
350
- try { execSync('claude --version', { stdio: 'pipe' }); return true; } catch { return false; }
351
- })();
444
+ const claudeInstalled = isClaudeInstalled();
352
445
 
353
446
  const claudeIcon = claudeInstalled ? chalk.green('●') : chalk.red('●');
354
447
  const claudeText = chalk.dim('Claude');
355
448
 
356
449
  let cfgPart;
357
450
  if (config) {
358
- const provName = PROVIDERS[config.provider]?.name || config.provider;
451
+ const view = buildStatusView(config, {
452
+ apiStatus,
453
+ claudeInstalled,
454
+ env: process.env,
455
+ });
359
456
  let dot;
360
457
  if (apiStatus === true) dot = chalk.green('●');
361
458
  else if (apiStatus === false) dot = chalk.red('●');
@@ -365,7 +462,9 @@ async function renderStatusBar(apiStatus) {
365
462
  : apiStatus === false ? chalk.red(' API ✘')
366
463
  : apiStatus === null ? chalk.yellow(' 网络异常')
367
464
  : '';
368
- cfgPart = `${dot} ${chalk.white(provName)}${chalk.dim(' · ')}${chalk.yellow(config.model)}${apiTag}`;
465
+ const envTag = view.envActive ? chalk.green(' env ') : chalk.yellow(' env 未生效');
466
+ const legacyTag = view.warnings.length > 0 ? chalk.yellow(' 旧模型') : '';
467
+ cfgPart = `${dot} ${chalk.white(view.providerName)}${apiTag}${envTag}${legacyTag}\n ${chalk.dim('主 ')}${chalk.yellow(view.mainModel)}${chalk.dim(' 快 ')}${chalk.yellow(view.fastModel)}`;
369
468
  } else {
370
469
  cfgPart = chalk.red('●') + ' ' + chalk.dim('未配置');
371
470
  }
@@ -383,7 +482,8 @@ async function runMenu() {
383
482
 
384
483
  const config = loadConfig();
385
484
  let apiStatus; // undefined = skipped, true/false/null = checked
386
- if (config) {
485
+ const configProblem = config ? getConfigValidationMessage(config) : null;
486
+ if (config && !configProblem) {
387
487
  const spinner = ora('正在检测 API 是否通畅...').start();
388
488
  apiStatus = await validateKey(config);
389
489
  if (apiStatus === true) spinner.succeed('API 连接正常');
@@ -391,18 +491,24 @@ async function runMenu() {
391
491
  else spinner.warn('网络异常,无法连接 API');
392
492
  }
393
493
 
394
- console.log(await renderStatusBar(apiStatus));
494
+ if (configProblem) {
495
+ console.log(chalk.red(` ● 配置无效:${configProblem}`));
496
+ console.log(chalk.dim(' 请先选择“首次配置 API Key 和模型”重新配置'));
497
+ } else {
498
+ console.log(await renderStatusBar(apiStatus));
499
+ }
395
500
  console.log();
396
501
 
397
502
  const action = await select({ loop: false,
398
503
  message: chalk.cyan('选择操作'),
399
504
  choices: [
400
- { name: '🤖 启动 Claude Code', value: 'launch', disabled: !config && '需先完成配置' },
505
+ { name: '🤖 启动 Claude Code', value: 'launch', disabled: (!config || configProblem) && '需先完成配置' },
401
506
  { name: '📦 安装 Claude Code', value: 'install' },
402
507
  { name: config ? '⚙️ 重新配置(输入新的 API Key)' : '⚙️ 首次配置 API Key 和模型', value: 'setup' },
403
- { name: '🔄 切换厂商/模型(保留当前 Key)', value: 'switch', disabled: !config && '需先完成配置' },
508
+ { name: '🔄 切换厂商或模型', value: 'switch', disabled: (!config || configProblem) && '需先完成配置' },
404
509
  { name: '📊 查看当前配置', value: 'status', disabled: !config && '需先完成配置' },
405
- { name: '🔁 重新检测 API', value: 'recheck', disabled: !config && '需先完成配置' },
510
+ { name: '🔁 重新检测 API', value: 'recheck', disabled: (!config || configProblem) && '需先完成配置' },
511
+ { name: '🗑 恢复默认(清除所有配置)', value: 'reset', disabled: !config && '没有可清除的配置' },
406
512
  { name: '退出', value: 'exit' },
407
513
  ],
408
514
  });
@@ -416,12 +522,12 @@ async function runMenu() {
416
522
 
417
523
  if (action === 'launch') {
418
524
  const cfg = loadConfig();
419
- if (!cfg) continue;
525
+ if (!cfg || getConfigValidationMessage(cfg)) continue;
420
526
  // 启动 claude 后等它退出,然后回菜单
421
527
  await new Promise((resolve) => {
422
528
  const child = spawn('claude', [], {
423
529
  stdio: 'inherit',
424
- env: { ...process.env, ANTHROPIC_BASE_URL: cfg.baseUrl, ANTHROPIC_API_KEY: cfg.apiKey },
530
+ env: { ...process.env, ...buildClaudeEnv(cfg) },
425
531
  });
426
532
  child.on('error', () => {
427
533
  console.log(chalk.yellow('\nClaude Code 未找到,请先选择"安装 Claude Code"'));
@@ -437,6 +543,7 @@ async function runMenu() {
437
543
  setup: 'setup',
438
544
  switch: 'switch',
439
545
  status: 'status',
546
+ reset: 'reset',
440
547
  };
441
548
 
442
549
  // 执行子命令
package/index.js ADDED
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ ...require('./lib/config'),
3
+ ...require('./lib/panel'),
4
+ };
package/lib/config.js CHANGED
@@ -10,9 +10,10 @@ const PROVIDERS = {
10
10
  name: 'DeepSeek',
11
11
  baseUrl: 'https://api.deepseek.com/anthropic',
12
12
  modelsUrl: 'https://api.deepseek.com/v1/models',
13
+ fastModel: 'deepseek-v4-flash',
13
14
  models: [
14
15
  { name: 'DeepSeek V4 Flash(快速)', value: 'deepseek-v4-flash' },
15
- { name: 'DeepSeek V4 Pro(强力)', value: 'deepseek-v4-pro' },
16
+ { name: 'DeepSeek V4 Pro(强力)', value: 'deepseek-v4-pro[1m]' },
16
17
  ],
17
18
  },
18
19
  qwen: {
@@ -105,14 +106,85 @@ function loadConfig() {
105
106
  }
106
107
  }
107
108
 
109
+ function validateConfig(config) {
110
+ if (!config || typeof config !== 'object') {
111
+ return { valid: false, message: '未找到配置' };
112
+ }
113
+ for (const key of ['provider', 'baseUrl', 'apiKey', 'model']) {
114
+ if (typeof config[key] !== 'string' || config[key].trim().length === 0) {
115
+ return { valid: false, message: `配置缺少 ${key}` };
116
+ }
117
+ }
118
+ if (!PROVIDERS[config.provider]) {
119
+ return { valid: false, message: `未知厂商 ${config.provider}` };
120
+ }
121
+ return { valid: true };
122
+ }
123
+
108
124
  function saveConfig(config) {
109
125
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
126
+ try {
127
+ fs.chmodSync(CONFIG_FILE, 0o600);
128
+ } catch {}
129
+ }
130
+
131
+ function providerKeyFromBaseUrl(baseUrl) {
132
+ return Object.entries(PROVIDERS).find(([, provider]) => provider.baseUrl === baseUrl)?.[0];
133
+ }
134
+
135
+ function resolveFastModel(provider, model) {
136
+ if (/flash|turbo|haiku|air|lite/i.test(model)) return model;
137
+ return provider?.fastModel || model;
138
+ }
139
+
140
+ function buildClaudeEnv({ provider, baseUrl, apiKey, model, fastModel }) {
141
+ const resolvedFastModel = fastModel || resolveFastModel(PROVIDERS[provider], model);
142
+ return {
143
+ ANTHROPIC_BASE_URL: baseUrl,
144
+ ANTHROPIC_AUTH_TOKEN: apiKey,
145
+ ANTHROPIC_API_KEY: apiKey,
146
+ ANTHROPIC_MODEL: model,
147
+ ANTHROPIC_DEFAULT_OPUS_MODEL: model,
148
+ ANTHROPIC_DEFAULT_SONNET_MODEL: model,
149
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: resolvedFastModel,
150
+ CLAUDE_CODE_SUBAGENT_MODEL: resolvedFastModel,
151
+ CLAUDE_CODE_EFFORT_LEVEL: 'max',
152
+ };
153
+ }
154
+
155
+ function classifyValidationStatus(statusCode) {
156
+ if (statusCode >= 200 && statusCode < 300) return true;
157
+ if (statusCode === 401 || statusCode === 403) return false;
158
+ return null;
159
+ }
160
+
161
+ function shellQuote(value) {
162
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
163
+ }
164
+
165
+ // 构造完整的 clawai 环境变量块
166
+ function buildEnvBlock(baseUrl, apiKey, model, fastModel) {
167
+ const provider = providerKeyFromBaseUrl(baseUrl);
168
+ const env = buildClaudeEnv({ provider, baseUrl, apiKey, model, fastModel });
169
+ return [
170
+ '',
171
+ '# clawai-start',
172
+ `export ANTHROPIC_BASE_URL=${shellQuote(env.ANTHROPIC_BASE_URL)}`,
173
+ `export ANTHROPIC_AUTH_TOKEN=${shellQuote(env.ANTHROPIC_AUTH_TOKEN)}`,
174
+ `export ANTHROPIC_API_KEY=${shellQuote(env.ANTHROPIC_API_KEY)}`,
175
+ `export ANTHROPIC_MODEL=${shellQuote(env.ANTHROPIC_MODEL)}`,
176
+ `export ANTHROPIC_DEFAULT_OPUS_MODEL=${shellQuote(env.ANTHROPIC_DEFAULT_OPUS_MODEL)}`,
177
+ `export ANTHROPIC_DEFAULT_SONNET_MODEL=${shellQuote(env.ANTHROPIC_DEFAULT_SONNET_MODEL)}`,
178
+ `export ANTHROPIC_DEFAULT_HAIKU_MODEL=${shellQuote(env.ANTHROPIC_DEFAULT_HAIKU_MODEL)}`,
179
+ `export CLAUDE_CODE_SUBAGENT_MODEL=${shellQuote(env.CLAUDE_CODE_SUBAGENT_MODEL)}`,
180
+ `export CLAUDE_CODE_EFFORT_LEVEL=${shellQuote(env.CLAUDE_CODE_EFFORT_LEVEL)}`,
181
+ '# clawai-end',
182
+ '',
183
+ ].join('\n');
110
184
  }
111
185
 
112
186
  // 写入或更新 shell 配置文件中的环境变量块
113
- function writeEnvToZshrc(baseUrl, apiKey) {
114
- // 检测用户 shell,自动选配置文件
115
- // macOS bash 默认读 ~/.bash_profile(登录 shell),Linux 读 ~/.bashrc
187
+ function writeEnvToZshrc(baseUrl, apiKey, model, fastModel) {
116
188
  const shell = process.env.SHELL || '';
117
189
  let rcFile;
118
190
  if (shell.includes('bash')) {
@@ -123,22 +195,61 @@ function writeEnvToZshrc(baseUrl, apiKey) {
123
195
  rcFile = path.join(os.homedir(), '.zshrc');
124
196
  }
125
197
 
126
- // Key 加引号,避免特殊字符(& = 空格)破坏 shell 语法
127
- const safeKey = `"${apiKey.replace(/"/g, '\\"')}"`;
128
- const block = `\n# clawai\nexport ANTHROPIC_BASE_URL=${baseUrl}\nexport ANTHROPIC_API_KEY=${safeKey}\n`;
198
+ const block = buildEnvBlock(baseUrl, apiKey, model, fastModel);
129
199
  const current = fs.existsSync(rcFile) ? fs.readFileSync(rcFile, 'utf8') : '';
130
200
 
131
- if (current.includes('# clawai')) {
132
- const updated = current.replace(
133
- /# clawai\nexport ANTHROPIC_BASE_URL=[^\n]*\nexport ANTHROPIC_API_KEY=[^\n]*/,
134
- `# clawai\nexport ANTHROPIC_BASE_URL=${baseUrl}\nexport ANTHROPIC_API_KEY=${safeKey}`
135
- );
136
- fs.writeFileSync(rcFile, updated);
137
- return { result: 'updated', file: rcFile };
138
- } else {
139
- fs.appendFileSync(rcFile, block);
140
- return { result: 'added', file: rcFile };
201
+ // 兼容旧版(# clawai 单行块)和新版(# clawai-start...# clawai-end 多行块)
202
+ const cleaned = current
203
+ .replace(/\n?# clawai-start[\s\S]*?# clawai-end\n?/g, '')
204
+ .replace(/\n?# clawai\nexport ANTHROPIC_BASE_URL=[^\n]*\nexport ANTHROPIC_API_KEY=[^\n]*\n?/g, '');
205
+
206
+ fs.writeFileSync(rcFile, cleaned + block);
207
+ return { result: cleaned !== current ? 'updated' : 'added', file: rcFile };
208
+ }
209
+
210
+ // 清除所有 clawai 配置(配置文件 + shell 环境变量块)
211
+ function resetConfig() {
212
+ const cleared = [];
213
+
214
+ // 删配置文件
215
+ if (fs.existsSync(CONFIG_FILE)) {
216
+ fs.unlinkSync(CONFIG_FILE);
217
+ cleared.push(CONFIG_FILE);
218
+ }
219
+
220
+ // 清理所有可能的 rc 文件中的 clawai 块
221
+ const rcFiles = [
222
+ path.join(os.homedir(), '.zshrc'),
223
+ path.join(os.homedir(), '.bashrc'),
224
+ path.join(os.homedir(), '.bash_profile'),
225
+ ];
226
+ for (const f of rcFiles) {
227
+ if (!fs.existsSync(f)) continue;
228
+ const content = fs.readFileSync(f, 'utf8');
229
+ if (!content.includes('# clawai')) continue;
230
+ const cleaned = content
231
+ .replace(/\n?# clawai-start[\s\S]*?# clawai-end\n?/g, '')
232
+ .replace(/\n?# clawai\nexport ANTHROPIC_BASE_URL=[^\n]*\nexport ANTHROPIC_API_KEY=[^\n]*\n?/g, '');
233
+ if (cleaned === content) continue;
234
+ fs.writeFileSync(f, cleaned);
235
+ cleared.push(f);
141
236
  }
237
+
238
+ return cleared;
142
239
  }
143
240
 
144
- module.exports = { loadConfig, saveConfig, writeEnvToZshrc, fetchModels, PROVIDERS, CONFIG_FILE };
241
+ module.exports = {
242
+ loadConfig,
243
+ saveConfig,
244
+ writeEnvToZshrc,
245
+ fetchModels,
246
+ resetConfig,
247
+ validateConfig,
248
+ resolveFastModel,
249
+ providerKeyFromBaseUrl,
250
+ buildClaudeEnv,
251
+ buildEnvBlock,
252
+ classifyValidationStatus,
253
+ PROVIDERS,
254
+ CONFIG_FILE,
255
+ };
package/lib/panel.js ADDED
@@ -0,0 +1,49 @@
1
+ const { buildClaudeEnv, PROVIDERS } = require('./config');
2
+
3
+ function apiStatusText(apiStatus) {
4
+ if (apiStatus === true) return 'API 正常';
5
+ if (apiStatus === false) return 'API Key 无效';
6
+ if (apiStatus === null) return '网络/服务异常';
7
+ return '未检测';
8
+ }
9
+
10
+ function isEnvActive(config, env) {
11
+ const expected = buildClaudeEnv(config);
12
+ return Object.entries(expected).every(([key, value]) => env[key] === value);
13
+ }
14
+
15
+ function buildStatusView(config, options = {}) {
16
+ const provider = PROVIDERS[config.provider];
17
+ const providerName = provider?.name || config.provider;
18
+ const claudeInstalled = options.claudeInstalled === true;
19
+ const env = options.env || {};
20
+ const expectedEnv = buildClaudeEnv(config);
21
+ const mainModel = expectedEnv.ANTHROPIC_MODEL;
22
+ const fastModel = expectedEnv.CLAUDE_CODE_SUBAGENT_MODEL;
23
+ const envActive = isEnvActive(config, env);
24
+ const warnings = [];
25
+
26
+ if (config.provider === 'deepseek' && config.model === 'deepseek-v4-pro') {
27
+ warnings.push('检测到旧 DeepSeek 模型名,建议运行 claw switch 更新到 deepseek-v4-pro[1m]');
28
+ }
29
+
30
+ return {
31
+ providerName,
32
+ mainModel,
33
+ fastModel,
34
+ envActive,
35
+ warnings,
36
+ lines: [
37
+ { label: '厂商', value: providerName },
38
+ { label: '主模型', value: mainModel },
39
+ { label: '快速模型', value: fastModel },
40
+ { label: 'API Key', value: '已保存' },
41
+ { label: 'API 状态', value: apiStatusText(options.apiStatus) },
42
+ { label: 'Claude Code', value: claudeInstalled ? '已安装' : '未检测到' },
43
+ { label: '当前终端', value: envActive ? '已生效' : '未生效' },
44
+ { label: 'Base URL', value: config.baseUrl },
45
+ ],
46
+ };
47
+ }
48
+
49
+ module.exports = { buildStatusView, apiStatusText, isEnvActive };
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "yingclaw",
3
- "version": "1.5.2",
3
+ "version": "1.6.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
9
  "scripts": {
10
- "start": "node bin/cli.js"
10
+ "start": "node bin/cli.js",
11
+ "test": "node --test"
11
12
  },
12
13
  "keywords": [
13
14
  "claude",