yingclaw 1.7.12 → 1.8.2

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/bin/cli.js CHANGED
@@ -196,6 +196,7 @@ async function configureCustomProvider({ chalk, ora, existingConfig }) {
196
196
  apiKey,
197
197
  model,
198
198
  fastModel,
199
+ availableModels: onlineResult?.models || (model ? [model, fastModel].filter(Boolean) : []),
199
200
  };
200
201
  }
201
202
 
@@ -283,7 +284,7 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
283
284
  if (!overwrite) return;
284
285
  }
285
286
 
286
- let providerKey, provider, apiKey, model, customConfig;
287
+ let providerKey, provider, apiKey, model, customConfig, availableModels;
287
288
  let step = 'provider';
288
289
 
289
290
  while (true) {
@@ -319,9 +320,11 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
319
320
  if (onlineModels && onlineModels.length > 0) {
320
321
  fetchSpinner.succeed(chalk.green(`已获取 ${onlineModels.length} 个可用模型`));
321
322
  modelChoices = onlineModels.map(id => ({ name: id, value: id }));
323
+ availableModels = onlineModels;
322
324
  } else {
323
325
  fetchSpinner.warn(chalk.yellow('无法获取在线列表,使用内置默认列表'));
324
326
  modelChoices = provider.models;
327
+ availableModels = provider.models.map(m => m.value);
325
328
  }
326
329
 
327
330
  const m = await select({ loop: false,
@@ -342,7 +345,7 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
342
345
  let cfg;
343
346
  try {
344
347
  const fastModel = customConfig?.fastModel || resolveFastModel(provider, model);
345
- cfg = customConfig || { provider: providerKey, model, fastModel, apiKey, baseUrl: provider.baseUrl };
348
+ cfg = customConfig || { provider: providerKey, model, fastModel, apiKey, baseUrl: provider.baseUrl, availableModels };
346
349
  saveConfig(cfg);
347
350
  if (writeCodeEnv) {
348
351
  ({ file } = writeEnvToZshrc(cfg.baseUrl, cfg.apiKey, cfg.model, cfg.fastModel));
@@ -589,12 +592,15 @@ program
589
592
  const fetchSpinner = ora('正在获取可用模型...').start();
590
593
  const onlineModels = await fetchModels(providerKey, apiKey);
591
594
  let modelChoices;
595
+ let availableModels;
592
596
  if (onlineModels && onlineModels.length > 0) {
593
597
  fetchSpinner.succeed(chalk.green(`已获取 ${onlineModels.length} 个可用模型`));
594
598
  modelChoices = onlineModels.map(id => ({ name: id, value: id }));
599
+ availableModels = onlineModels;
595
600
  } else {
596
601
  fetchSpinner.warn(chalk.yellow('无法获取在线列表,使用内置默认列表'));
597
602
  modelChoices = provider.models;
603
+ availableModels = provider.models.map(m => m.value);
598
604
  }
599
605
 
600
606
  const model = await select({ loop: false,
@@ -608,7 +614,7 @@ program
608
614
 
609
615
  const spinner = ora('切换中...').start();
610
616
  const fastModel = resolveFastModel(provider, model);
611
- const newConfig = { ...config, provider: providerKey, model, fastModel, baseUrl: provider.baseUrl, apiKey };
617
+ const newConfig = { ...config, provider: providerKey, model, fastModel, baseUrl: provider.baseUrl, apiKey, availableModels };
612
618
  saveConfig(newConfig);
613
619
  await new Promise(r => setTimeout(r, 300));
614
620
  spinner.succeed(chalk.green(`API 连接已切换至 ${provider.name} · ${model}`));
@@ -676,8 +682,8 @@ program
676
682
  if (shouldOpen) {
677
683
  const openSpinner = ora('正在打开 Claude 桌面应用...').start();
678
684
  try {
679
- openClaudeDesktop();
680
- openSpinner.succeed(chalk.green('Claude 桌面应用已打开'));
685
+ await openClaudeDesktop();
686
+ openSpinner.succeed(chalk.green('Claude 桌面应用已重新打开(旧实例已退出,新配置生效)'));
681
687
  } catch (e) {
682
688
  openSpinner.fail(chalk.red(`打开失败: ${e.message}`));
683
689
  console.log(chalk.dim(getDesktopOpenHint()));
@@ -802,62 +808,113 @@ async function renderStatusBar(apiStatus) {
802
808
  return config ? ` ${cfgPart}` : ` ${claudeIcon} ${claudeText} ${cfgPart}`;
803
809
  }
804
810
 
811
+ // 缓存上次校验的 config 哈希和结果,避免每次回菜单都重检
812
+ let lastCheckedHash = null;
813
+ let lastCheckResult; // undefined / true / false / null
814
+
815
+ function configHash(cfg) {
816
+ if (!cfg) return null;
817
+ return JSON.stringify({ p: cfg.provider, b: cfg.baseUrl, k: cfg.apiKey, m: cfg.model });
818
+ }
819
+
820
+ async function maybeCheckApi(config, forceRecheck) {
821
+ const hash = configHash(config);
822
+ if (!forceRecheck && hash === lastCheckedHash && lastCheckResult !== undefined) {
823
+ return lastCheckResult;
824
+ }
825
+ const result = await validateKey(config);
826
+ lastCheckedHash = hash;
827
+ lastCheckResult = result;
828
+ return result;
829
+ }
830
+
831
+ const ADVANCED_DISABLED_HINT = '需先配置 API 连接';
832
+
833
+ async function runAdvancedMenu(chalk, hasConfig) {
834
+ const action = await select({ loop: false,
835
+ message: chalk.cyan('高级选项'),
836
+ choices: [
837
+ { name: '🔁 重新检测 API', value: 'recheck', disabled: !hasConfig && ADVANCED_DISABLED_HINT },
838
+ { name: '↩️ 恢复 Claude Code 终端默认', value: 'code-reset' },
839
+ { name: '↩️ 恢复 Claude 桌面默认', value: 'desktop-reset' },
840
+ { name: '🗑 清除所有 yingclaw 配置', value: 'reset' },
841
+ { name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
842
+ ],
843
+ });
844
+ return action;
845
+ }
846
+
805
847
  async function runMenu() {
806
848
  const chalk = (await import('chalk')).default;
807
849
  const ora = (await import('ora')).default;
808
850
 
851
+ let forceRecheck = false;
852
+
809
853
  while (true) {
810
854
  console.clear();
811
855
  console.log(await getBanner());
812
856
 
813
857
  const config = loadConfig();
814
- let apiStatus; // undefined = skipped, true/false/null = checked
815
858
  const configProblem = config ? getConfigValidationMessage(config) : null;
859
+ let apiStatus;
860
+
816
861
  if (config && !configProblem) {
817
- const spinner = ora('正在检测 API 是否通畅...').start();
818
- apiStatus = await validateKey(config);
819
- if (apiStatus === true) spinner.succeed('API 连接正常');
820
- else if (apiStatus === false) spinner.fail('API Key 无效或已过期');
821
- else spinner.warn('网络异常,无法连接 API');
862
+ const hash = configHash(config);
863
+ if (forceRecheck || hash !== lastCheckedHash || lastCheckResult === undefined) {
864
+ const spinner = ora('正在检测 API...').start();
865
+ apiStatus = await maybeCheckApi(config, true);
866
+ if (apiStatus === true) spinner.succeed('API 连接正常');
867
+ else if (apiStatus === false) spinner.fail('API Key 无效或已过期');
868
+ else spinner.warn('网络异常,无法连接 API');
869
+ } else {
870
+ apiStatus = lastCheckResult;
871
+ }
872
+ forceRecheck = false;
822
873
  }
823
874
 
824
875
  if (configProblem) {
825
876
  console.log(chalk.red(` ● 配置无效:${configProblem}`));
826
- console.log(chalk.dim(' 请先选择“配置 API 连接”重新配置'));
877
+ console.log(chalk.dim(' 请先选择"配置 API 连接"重新配置'));
827
878
  } else {
828
879
  console.log(await renderStatusBar(apiStatus));
829
880
  }
830
881
  console.log();
831
882
 
883
+ const disabledHint = (!config || configProblem) && '需先配置 API 连接';
884
+
832
885
  const action = await select({ loop: false,
833
886
  message: chalk.cyan('选择操作'),
834
887
  choices: [
835
- { name: '🤖 启动 Claude Code', value: 'launch', disabled: (!config || configProblem) && '需先配置 API 连接' },
888
+ { name: '🤖 启动 Claude Code', value: 'launch', disabled: disabledHint },
836
889
  { name: '📦 安装 Claude Code', value: 'install' },
837
890
  { name: config ? '🔑 重新配置 API 连接' : '🔑 配置 API 连接', value: 'config' },
838
- { name: '💻 接入 Claude Code 终端', value: 'code', disabled: (!config || configProblem) && '需先配置 API 连接' },
839
- { name: '🔄 切换厂商或模型', value: 'switch', disabled: (!config || configProblem) && '需先配置 API 连接' },
840
- { name: '🖥 接入 Claude 桌面应用', value: 'desktop', disabled: (!config || configProblem) && '需先配置 API 连接' },
841
- { name: '↩️ 恢复 Claude Code 终端默认', value: 'code-reset' },
842
- { name: '↩️ 恢复 Claude 桌面默认', value: 'desktop-reset' },
891
+ { name: '🔄 切换厂商或模型', value: 'switch', disabled: disabledHint },
892
+ { name: '💻 接入 Claude Code 终端', value: 'code', disabled: disabledHint },
893
+ { name: '🖥 接入 Claude 桌面应用', value: 'desktop', disabled: disabledHint },
843
894
  { name: '📊 查看当前配置', value: 'status', disabled: !config && '需先配置 API 连接' },
844
- { name: '🔁 重新检测 API', value: 'recheck', disabled: (!config || configProblem) && '需先配置 API 连接' },
845
- { name: '🗑 清除所有 yingclaw 配置', value: 'reset' },
895
+ { name: '🛠 高级 ', value: 'advanced' },
846
896
  { name: '退出', value: 'exit' },
847
897
  ],
848
898
  });
849
899
 
850
900
  if (action === 'exit') return;
851
901
 
852
- if (action === 'recheck') {
853
- // 仅刷新菜单,下次循环会重新检测
902
+ let resolvedAction = action;
903
+ if (action === 'advanced') {
904
+ const adv = await runAdvancedMenu(chalk, !!config && !configProblem);
905
+ if (adv === '__BACK__') continue;
906
+ resolvedAction = adv;
907
+ }
908
+
909
+ if (resolvedAction === 'recheck') {
910
+ lastCheckResult = undefined;
911
+ forceRecheck = true;
854
912
  continue;
855
913
  }
856
914
 
857
- if (action === 'launch') {
915
+ if (resolvedAction === 'launch') {
858
916
  const cfg = loadConfig();
859
917
  if (!cfg || getConfigValidationMessage(cfg)) continue;
860
- // 启动 claude 后等它退出,然后回菜单
861
918
  await new Promise((resolve) => {
862
919
  const child = spawn('claude', [], {
863
920
  stdio: 'inherit',
@@ -875,7 +932,6 @@ async function runMenu() {
875
932
  const cmdMap = {
876
933
  install: 'install-claude',
877
934
  config: 'config',
878
- setup: 'setup',
879
935
  code: 'code',
880
936
  'code-reset': 'code-reset',
881
937
  switch: 'switch',
@@ -885,14 +941,19 @@ async function runMenu() {
885
941
  reset: 'reset',
886
942
  };
887
943
 
888
- // 执行子命令
944
+ // 执行子命令(用 spawn 隔离,避免 commander 对 program 的副作用)
889
945
  await new Promise((resolve) => {
890
- const child = spawn(process.execPath, [__filename, cmdMap[action]], { stdio: 'inherit' });
946
+ const child = spawn(process.execPath, [__filename, cmdMap[resolvedAction]], { stdio: 'inherit' });
891
947
  child.on('exit', resolve);
892
948
  child.on('error', resolve);
893
949
  });
894
950
 
895
- // 操作结束,提示返回菜单
951
+ // 改 config 的命令需要刷新缓存
952
+ if (['config', 'switch', 'reset', 'code-reset'].includes(resolvedAction)) {
953
+ lastCheckResult = undefined;
954
+ lastCheckedHash = null;
955
+ }
956
+
896
957
  console.log();
897
958
  const next = await select({ loop: false,
898
959
  message: chalk.cyan('下一步'),
package/lib/desktop.js CHANGED
@@ -40,12 +40,17 @@ function readJsonFile(file) {
40
40
  }
41
41
  }
42
42
 
43
- function uniqueModels(model, fastModel) {
44
- return [...new Set([model, fastModel].filter(Boolean))];
43
+ function collectModels(config) {
44
+ // 优先用 config 中保存的全量模型列表(setup/switch 时联网拉取的)
45
+ // 退化:主模型 + 快速模型
46
+ const list = Array.isArray(config.availableModels) && config.availableModels.length > 0
47
+ ? [config.model, config.fastModel, ...config.availableModels]
48
+ : [config.model, config.fastModel];
49
+ return [...new Set(list.filter(Boolean))];
45
50
  }
46
51
 
47
52
  function buildClaudeDesktopEnterpriseConfig(config, options = {}) {
48
- const models = uniqueModels(config.model, config.fastModel);
53
+ const models = collectModels(config);
49
54
  const baseUrl = normalizeAnthropicBaseUrl(config.baseUrl);
50
55
  if (!baseUrl.startsWith('https://')) {
51
56
  throw new Error('Claude 桌面应用要求 Gateway Base URL 使用 HTTPS');
@@ -121,24 +126,37 @@ function clearClaudeDesktopConfig(options = {}) {
121
126
 
122
127
  function buildClaudeDesktopOpenCommands(platform = process.platform) {
123
128
  if (platform !== 'darwin') return null;
129
+ // 先 graceful quit,再 force kill 兜底,最后 open
124
130
  return [
131
+ { command: 'osascript', args: ['-e', 'tell application "Claude" to quit'], optional: true, waitAfter: 600 },
132
+ { command: 'pkill', args: ['-x', 'Claude'], optional: true, waitAfter: 400 },
125
133
  { command: 'open', args: ['-a', 'Claude'] },
126
134
  ];
127
135
  }
128
136
 
129
- function openClaudeDesktop(options = {}) {
137
+ async function sleep(ms) {
138
+ return new Promise((r) => setTimeout(r, ms));
139
+ }
140
+
141
+ async function openClaudeDesktop(options = {}) {
130
142
  const platform = options.platform || process.platform;
131
143
  const commands = buildClaudeDesktopOpenCommands(platform);
132
144
  if (!commands) return { result: 'unsupported' };
133
145
 
134
146
  const runner = options.runner || spawnSync;
135
- const [openCommand] = commands;
136
- const result = runner(openCommand.command, openCommand.args, { stdio: 'ignore', windowsHide: true });
137
- if (result.status !== 0) {
138
- throw new Error('Claude 桌面应用打开失败');
147
+ const isMocked = options.runner !== undefined;
148
+
149
+ for (const { command, args, optional, waitAfter } of commands) {
150
+ const result = runner(command, args, { stdio: 'ignore', windowsHide: true });
151
+ if (result.status !== 0 && !optional) {
152
+ throw new Error('Claude 桌面应用打开失败');
153
+ }
154
+ if (waitAfter && !isMocked) {
155
+ await sleep(waitAfter);
156
+ }
139
157
  }
140
158
 
141
- return { result: 'opened' };
159
+ return { result: 'reopened' };
142
160
  }
143
161
 
144
162
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yingclaw",
3
- "version": "1.7.12",
3
+ "version": "1.8.2",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Kimi、Qwen、MiniMax、GLM、MiMo",
5
5
  "main": "index.js",
6
6
  "bin": {