yymaxapi 1.0.104 → 1.0.108

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
@@ -8,8 +8,14 @@
8
8
  ```bash
9
9
  npx yymaxapi@latest
10
10
  ```
11
- 按提示选择节点和模型即可。
12
- 主菜单已简化为:激活 Claude / 激活 Codex / 测试连接 / 查看配置 / 恢复 / 退出。
11
+ 按提示选择模型并输入 Key 即可,默认会直接使用预设默认节点。
12
+ 主菜单已简化为:一键配置 / 配置外部工具 / 测速节点 / 测试连接 / 查看配置 / 恢复 / 退出。
13
+
14
+ **需要手动测速时**
15
+ ```bash
16
+ npx yymaxapi@latest speed-test
17
+ ```
18
+ 或在具体配置命令后追加 `--speed-test`,按测速结果手动选点。
13
19
 
14
20
  **方式二:一键配置 Claude**
15
21
  ```bash
package/bin/yymaxapi.js CHANGED
@@ -607,6 +607,77 @@ async function testFallbackEndpoints() {
607
607
  return testAllEndpoints(FALLBACK_EPS, { label: '检测备用节点', autoFallback: false });
608
608
  }
609
609
 
610
+ function shouldRunEndpointSpeedTest(args = {}) {
611
+ const raw = args['speed-test'] !== undefined ? args['speed-test'] : args.speedTest;
612
+ if (raw === undefined) return false;
613
+ if (typeof raw === 'string') {
614
+ return !['false', '0', 'no'].includes(raw.toLowerCase());
615
+ }
616
+ return !!raw;
617
+ }
618
+
619
+ async function resolveEndpointSelection(args = {}, options = {}) {
620
+ const defaultEndpoint = options.defaultEndpoint || ENDPOINTS[0];
621
+ if (!defaultEndpoint) return null;
622
+ if (!shouldRunEndpointSpeedTest(args)) return defaultEndpoint;
623
+
624
+ const speedIntro = options.speedIntro || '📡 开始测速节点...\n';
625
+ const proceedMessage = options.proceedMessage || '仍要写入默认节点配置吗?';
626
+ const chooseMessage = options.chooseMessage || '选择节点:';
627
+
628
+ console.log(chalk.cyan(`${speedIntro}`));
629
+ const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
630
+ const sorted = speedResult.ranked || [];
631
+
632
+ if (sorted.length > 0) {
633
+ const { selectedIndex } = await inquirer.prompt([{
634
+ type: 'list',
635
+ name: 'selectedIndex',
636
+ message: chooseMessage,
637
+ choices: [
638
+ { name: `* 使用默认节点 (${defaultEndpoint.name})`, value: -1 },
639
+ new inquirer.Separator(' ---- 或按测速结果选择 ----'),
640
+ ...sorted.map((endpoint, index) => ({
641
+ name: `${endpoint.name} - ${endpoint.latency}ms (评分:${endpoint.score})`,
642
+ value: index
643
+ }))
644
+ ]
645
+ }]);
646
+
647
+ const selectedEndpoint = selectedIndex === -1 ? defaultEndpoint : sorted[selectedIndex];
648
+ if (speedResult.usedFallback) {
649
+ console.log(chalk.yellow('\n⚠ 当前使用备用节点\n'));
650
+ }
651
+ return selectedEndpoint;
652
+ }
653
+
654
+ console.log(chalk.red('\n⚠️ 所有节点(含备用)均不可达'));
655
+ const { proceed } = await inquirer.prompt([{
656
+ type: 'confirm',
657
+ name: 'proceed',
658
+ message: proceedMessage,
659
+ default: false
660
+ }]);
661
+ if (!proceed) {
662
+ console.log(chalk.gray('已取消'));
663
+ return null;
664
+ }
665
+ return defaultEndpoint;
666
+ }
667
+
668
+ async function speedTestNodes() {
669
+ console.log(chalk.cyan.bold('\n📡 节点测速\n'));
670
+ const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
671
+ const defaultEndpoint = ENDPOINTS[0];
672
+
673
+ if (defaultEndpoint) {
674
+ console.log(chalk.gray(`\n默认配置当前直连: ${defaultEndpoint.name} (${defaultEndpoint.url})`));
675
+ }
676
+ if (speedResult.best && defaultEndpoint && speedResult.best.url !== defaultEndpoint.url) {
677
+ console.log(chalk.yellow('如需按测速结果选点,可在对应配置命令后追加 --speed-test'));
678
+ }
679
+ }
680
+
610
681
  // ============ API Key 验证 ============
611
682
  function httpGetJson(url, headers = {}, timeout = 10000) {
612
683
  return new Promise((resolve, reject) => {
@@ -967,11 +1038,13 @@ function writeClaudeCodeSettings(baseUrl, apiKey, modelId = getDefaultClaudeMode
967
1038
  if (fs.existsSync(settingsPath)) {
968
1039
  try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch { settings = {}; }
969
1040
  }
970
- settings.apiBaseUrl = baseUrl.replace(/\/+$/, '');
1041
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, '');
1042
+ settings.apiBaseUrl = normalizedBaseUrl;
971
1043
  if (!settings.env) settings.env = {};
972
- settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
1044
+ settings.env.ANTHROPIC_API_KEY = apiKey;
1045
+ settings.env.ANTHROPIC_BASE_URL = normalizedBaseUrl;
1046
+ delete settings.env.ANTHROPIC_AUTH_TOKEN;
973
1047
  delete settings.availableModels;
974
- delete settings.env.ANTHROPIC_BASE_URL;
975
1048
  delete settings.env.ANTHROPIC_MODEL;
976
1049
  if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
977
1050
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
@@ -3795,6 +3868,32 @@ function readHermesYamlConfig(configPath = getHermesConfigPath()) {
3795
3868
  };
3796
3869
  }
3797
3870
 
3871
+ function readHermesCliConfig() {
3872
+ const yamlConfig = readHermesYamlConfig();
3873
+ const envPath = getHermesEnvPath();
3874
+ const envEntries = parseEnvFile(readTextIfExists(envPath));
3875
+ const modelConfig = yamlConfig.config?.model && typeof yamlConfig.config.model === 'object'
3876
+ ? yamlConfig.config.model
3877
+ : {};
3878
+ const modelId = String(modelConfig.default || '').trim() || getDefaultClaudeModel().id;
3879
+ const provider = String(modelConfig.provider || '').trim().toLowerCase();
3880
+ const type = resolveHermesModelType(modelId, provider);
3881
+ const apiKey = type === 'codex'
3882
+ ? (envEntries.OPENAI_API_KEY || process.env.OPENAI_API_KEY || process.env.OPENAI_AUTH_TOKEN || '')
3883
+ : (envEntries.ANTHROPIC_API_KEY || envEntries.ANTHROPIC_TOKEN || process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_TOKEN || process.env.CLAUDE_CODE_OAUTH_TOKEN || '');
3884
+
3885
+ return {
3886
+ configPath: yamlConfig.configPath,
3887
+ envPath,
3888
+ configured: yamlConfig.configured || fs.existsSync(envPath),
3889
+ modelId,
3890
+ provider,
3891
+ type,
3892
+ baseUrl: String(modelConfig.base_url || '').trim(),
3893
+ apiKey
3894
+ };
3895
+ }
3896
+
3798
3897
  function getOpencodeConfigPath() {
3799
3898
  const home = os.homedir();
3800
3899
  return process.platform === 'win32'
@@ -3813,11 +3912,20 @@ function getCodexCliPaths() {
3813
3912
  function readClaudeCodeCliConfig() {
3814
3913
  const settingsPath = getClaudeCodeSettingsPath();
3815
3914
  const settings = readJsonIfExists(settingsPath) || {};
3915
+ const settingsEnv = settings.env && typeof settings.env === 'object' ? settings.env : {};
3916
+ const settingsApiKey = String(settingsEnv.ANTHROPIC_API_KEY || '').trim();
3917
+ const settingsLegacyAuthToken = String(settingsEnv.ANTHROPIC_AUTH_TOKEN || '').trim();
3918
+ const settingsBaseUrl = String(settingsEnv.ANTHROPIC_BASE_URL || settings.apiBaseUrl || '').trim();
3816
3919
  return {
3817
3920
  settingsPath,
3818
- modelId: settings.model || settings.env?.ANTHROPIC_MODEL || process.env.ANTHROPIC_MODEL || getDefaultClaudeModel().id,
3819
- baseUrl: settings.env?.ANTHROPIC_BASE_URL || settings.apiBaseUrl || process.env.ANTHROPIC_BASE_URL || '',
3820
- apiKey: settings.env?.ANTHROPIC_AUTH_TOKEN || process.env.ANTHROPIC_AUTH_TOKEN || '',
3921
+ modelId: settings.model || settingsEnv.ANTHROPIC_MODEL || process.env.ANTHROPIC_MODEL || getDefaultClaudeModel().id,
3922
+ baseUrl: settingsBaseUrl || process.env.ANTHROPIC_BASE_URL || '',
3923
+ apiKey: settingsApiKey || settingsLegacyAuthToken || process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '',
3924
+ settingsBaseUrl,
3925
+ settingsApiKey,
3926
+ settingsLegacyAuthToken,
3927
+ hasLegacyAuthTokenInSettings: !!settingsLegacyAuthToken,
3928
+ usesLegacyAuthTokenOnly: !settingsApiKey && !!settingsLegacyAuthToken,
3821
3929
  configured: fs.existsSync(settingsPath)
3822
3930
  };
3823
3931
  }
@@ -3872,6 +3980,23 @@ function summarizeCliTestOutput(text) {
3872
3980
  return lines[0].slice(0, 160);
3873
3981
  }
3874
3982
 
3983
+ function extractHermesCliReply(text) {
3984
+ const lines = cleanCliTestOutput(text)
3985
+ .split('\n')
3986
+ .map(line => line.trim())
3987
+ .filter(Boolean)
3988
+ .filter(line => !/^session_id:/i.test(line))
3989
+ .filter(line => !/^resume this session/i.test(line))
3990
+ .filter(line => !/^session:/i.test(line))
3991
+ .filter(line => !/^duration:/i.test(line))
3992
+ .filter(line => !/^messages:/i.test(line))
3993
+ .filter(line => !/^[╭╰│─]+/.test(line));
3994
+
3995
+ const okLine = lines.find(line => /^OK$/i.test(line));
3996
+ if (okLine) return okLine;
3997
+ return lines[0] || '';
3998
+ }
3999
+
3875
4000
  function buildReadableAgentError(text, fallback = '') {
3876
4001
  const cleaned = cleanCliTestOutput(text);
3877
4002
  if (!cleaned) return fallback;
@@ -3887,6 +4012,11 @@ function looksLikeCliTestError(text) {
3887
4012
  'all models failed',
3888
4013
  'auth_permanent',
3889
4014
  'auth issue',
4015
+ 'authentication_error',
4016
+ 'failed to authenticate',
4017
+ 'invalid bearer token',
4018
+ 'invalid api key',
4019
+ 'fix external api key',
3890
4020
  'unauthorized',
3891
4021
  'forbidden',
3892
4022
  'missing environment variable',
@@ -3914,17 +4044,28 @@ function testClaudeCodeCliConnection() {
3914
4044
 
3915
4045
  const config = readClaudeCodeCliConfig();
3916
4046
  if (!config.configured) return { name: 'Claude Code CLI', status: 'skipped', detail: '未检测到 ~/.claude/settings.json' };
3917
- if (!config.baseUrl || !config.apiKey) return { name: 'Claude Code CLI', status: 'failed', detail: 'Claude Code 配置缺少 Base URL 或 API Key' };
4047
+ if (config.usesLegacyAuthTokenOnly) {
4048
+ return {
4049
+ name: 'Claude Code CLI',
4050
+ status: 'failed',
4051
+ detail: '检测到旧版 Claude Code 配置字段 ANTHROPIC_AUTH_TOKEN,请重新运行 yymaxapi 以改写为 ANTHROPIC_API_KEY'
4052
+ };
4053
+ }
4054
+ if (!config.settingsBaseUrl || !config.settingsApiKey) {
4055
+ return { name: 'Claude Code CLI', status: 'failed', detail: 'Claude Code 配置缺少 settings.json 中的 Base URL 或 API Key' };
4056
+ }
3918
4057
 
3919
4058
  const env = {
3920
4059
  ...process.env,
3921
4060
  PATH: extendPathEnv(null),
3922
- ANTHROPIC_BASE_URL: config.baseUrl,
3923
- ANTHROPIC_AUTH_TOKEN: config.apiKey,
3924
- ANTHROPIC_MODEL: config.modelId,
3925
4061
  NODE_TLS_REJECT_UNAUTHORIZED: '0',
3926
4062
  NODE_NO_WARNINGS: '1'
3927
4063
  };
4064
+ delete env.ANTHROPIC_BASE_URL;
4065
+ delete env.ANTHROPIC_API_KEY;
4066
+ delete env.ANTHROPIC_AUTH_TOKEN;
4067
+ delete env.ANTHROPIC_MODEL;
4068
+ delete env.CLAUDE_API_KEY;
3928
4069
 
3929
4070
  return runCliTestCandidates('Claude Code CLI', [
3930
4071
  `${shellQuote(cliBinary)} -p --model ${shellQuote(config.modelId)} --output-format text ${shellQuote('请只回复 OK')}`,
@@ -3976,14 +4117,62 @@ function testCodexCliConnection() {
3976
4117
  ], env);
3977
4118
  }
3978
4119
 
4120
+ function testHermesCliConnection() {
4121
+ const cliBinary = resolveCommandBinary('hermes');
4122
+ if (!cliBinary) return { name: 'Hermes CLI', status: 'skipped', detail: '未安装 hermes 命令' };
4123
+
4124
+ const config = readHermesCliConfig();
4125
+ if (!config.configured) return { name: 'Hermes CLI', status: 'skipped', detail: '未检测到 ~/.hermes/config.yaml 或 ~/.hermes/.env' };
4126
+ if (!config.apiKey) return { name: 'Hermes CLI', status: 'failed', detail: 'Hermes 配置缺少 API Key' };
4127
+
4128
+ const env = {
4129
+ ...process.env,
4130
+ PATH: extendPathEnv(null),
4131
+ NODE_TLS_REJECT_UNAUTHORIZED: '0',
4132
+ NODE_NO_WARNINGS: '1'
4133
+ };
4134
+
4135
+ if (config.type === 'codex') {
4136
+ env.OPENAI_API_KEY = config.apiKey;
4137
+ delete env.ANTHROPIC_API_KEY;
4138
+ delete env.ANTHROPIC_TOKEN;
4139
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
4140
+ } else {
4141
+ env.ANTHROPIC_API_KEY = config.apiKey;
4142
+ if (config.baseUrl) env.ANTHROPIC_BASE_URL = config.baseUrl;
4143
+ delete env.ANTHROPIC_TOKEN;
4144
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
4145
+ }
4146
+
4147
+ const providerArg = config.provider === 'anthropic' ? ' --provider anthropic' : '';
4148
+ const result = safeExec(`${shellQuote(cliBinary)} chat -Q -q ${shellQuote('请只回复 OK')}${providerArg}`, {
4149
+ timeout: 120000,
4150
+ env,
4151
+ maxBuffer: 1024 * 1024
4152
+ });
4153
+ const combined = cleanCliTestOutput(`${result.output || ''}\n${result.stdout || ''}\n${result.stderr || ''}`);
4154
+ const reply = extractHermesCliReply(combined);
4155
+
4156
+ if (result.ok && /^OK$/i.test(reply) && !looksLikeCliTestError(combined)) {
4157
+ return { name: 'Hermes CLI', status: 'success', detail: 'OK' };
4158
+ }
4159
+
4160
+ return {
4161
+ name: 'Hermes CLI',
4162
+ status: 'failed',
4163
+ detail: reply || summarizeCliTestOutput(combined) || result.error || '命令执行失败'
4164
+ };
4165
+ }
4166
+
3979
4167
  async function testAdditionalCliConnections(args = {}, options = {}) {
3980
4168
  if (args['no-app-test'] || args.noAppTest) return;
3981
4169
 
3982
- const requested = new Set((options.only || ['claude', 'opencode', 'codex']).map(item => String(item)));
4170
+ const requested = new Set((options.only || ['claude', 'opencode', 'codex', 'hermes']).map(item => String(item)));
3983
4171
  const results = [];
3984
4172
  if (requested.has('claude')) results.push(testClaudeCodeCliConnection());
3985
4173
  if (requested.has('opencode')) results.push(testOpencodeCliConnection());
3986
4174
  if (requested.has('codex')) results.push(testCodexCliConnection());
4175
+ if (requested.has('hermes')) results.push(testHermesCliConnection());
3987
4176
  if (results.length === 0) return;
3988
4177
 
3989
4178
  console.log(chalk.cyan('\n附加测试: 其他 CLI 连接...'));
@@ -4670,47 +4859,15 @@ async function quickSetup(paths, args = {}) {
4670
4859
  }
4671
4860
 
4672
4861
  async function presetClaude(paths, args = {}) {
4673
- console.log(chalk.cyan.bold('\n🚀 Claude 快速配置(自动测速推荐节点)\n'));
4862
+ console.log(chalk.cyan.bold('\n🚀 Claude 快速配置(默认节点直连)\n'));
4674
4863
 
4675
4864
  const apiConfig = API_CONFIG.claude;
4676
4865
  const providerPrefix = (args['provider-prefix'] || args.prefix || apiConfig.providerName).toString().trim() || apiConfig.providerName;
4677
-
4678
- const shouldTest = !(args['no-test'] || args.noTest);
4679
- let selectedEndpoint = ENDPOINTS[0];
4680
-
4681
- if (shouldTest) {
4682
- console.log(chalk.cyan('📡 开始测速 Claude 节点...\n'));
4683
- const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
4684
-
4685
- const sorted = speedResult.ranked || [];
4686
- if (sorted.length > 0) {
4687
- const defaultEp = ENDPOINTS[0];
4688
- const { selectedIndex } = await inquirer.prompt([{
4689
- type: 'list',
4690
- name: 'selectedIndex',
4691
- message: '选择节点:',
4692
- choices: [
4693
- { name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
4694
- new inquirer.Separator(' ---- 或按测速结果选择 ----'),
4695
- ...sorted.map((e, i) => ({
4696
- name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
4697
- value: i
4698
- }))
4699
- ]
4700
- }]);
4701
- selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
4702
- if (speedResult.usedFallback) {
4703
- console.log(chalk.yellow(`\n⚠ 当前使用备用节点\n`));
4704
- }
4705
- } else {
4706
- console.log(chalk.red('\n⚠️ 所有节点(含备用)均不可达'));
4707
- const { proceed } = await inquirer.prompt([{
4708
- type: 'confirm', name: 'proceed',
4709
- message: '仍要写入默认节点配置吗?', default: false
4710
- }]);
4711
- if (!proceed) { console.log(chalk.gray('已取消')); return; }
4712
- }
4713
- }
4866
+ const selectedEndpoint = await resolveEndpointSelection(args, {
4867
+ speedIntro: '📡 开始测速 Claude 节点...\n',
4868
+ proceedMessage: '仍要写入默认节点配置吗?'
4869
+ });
4870
+ if (!selectedEndpoint) return;
4714
4871
 
4715
4872
  const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
4716
4873
  const repairResult = applyConfigRepairsWithSync(config, paths);
@@ -4722,6 +4879,8 @@ async function presetClaude(paths, args = {}) {
4722
4879
 
4723
4880
  const apiKeyEnvFallbacks = [
4724
4881
  'OPENCLAW_CLAUDE_KEY',
4882
+ 'ANTHROPIC_API_KEY',
4883
+ 'ANTHROPIC_AUTH_TOKEN',
4725
4884
  'CLAUDE_API_KEY',
4726
4885
  'OPENCLAW_API_KEY'
4727
4886
  ];
@@ -4853,48 +5012,16 @@ async function presetClaude(paths, args = {}) {
4853
5012
  }
4854
5013
 
4855
5014
  async function presetCodex(paths, args = {}) {
4856
- console.log(chalk.cyan.bold('\n🚀 Codex 快速配置(自动测速推荐节点)\n'));
5015
+ console.log(chalk.cyan.bold('\n🚀 Codex 快速配置(默认节点直连)\n'));
4857
5016
 
4858
5017
  const apiConfig = API_CONFIG.codex;
4859
5018
  const providerPrefix = (args['provider-prefix'] || args.prefix || apiConfig.providerName).toString().trim() || apiConfig.providerName;
4860
5019
  const providerName = (args['provider-name'] || args.provider || providerPrefix).toString().trim() || apiConfig.providerName;
4861
-
4862
- const shouldTest = !(args['no-test'] || args.noTest);
4863
- let selectedEndpoint = ENDPOINTS[0];
4864
-
4865
- if (shouldTest) {
4866
- console.log(chalk.cyan('📡 开始测速 Codex 节点...\n'));
4867
- const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
4868
-
4869
- const sorted = speedResult.ranked || [];
4870
- if (sorted.length > 0) {
4871
- const defaultEp = ENDPOINTS[0];
4872
- const { selectedIndex } = await inquirer.prompt([{
4873
- type: 'list',
4874
- name: 'selectedIndex',
4875
- message: '选择节点:',
4876
- choices: [
4877
- { name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
4878
- new inquirer.Separator(' ---- 或按测速结果选择 ----'),
4879
- ...sorted.map((e, i) => ({
4880
- name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
4881
- value: i
4882
- }))
4883
- ]
4884
- }]);
4885
- selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
4886
- if (speedResult.usedFallback) {
4887
- console.log(chalk.yellow(`\n⚠ 当前使用备用节点\n`));
4888
- }
4889
- } else {
4890
- console.log(chalk.red('\n⚠️ 所有节点(含备用)均不可达'));
4891
- const { proceed } = await inquirer.prompt([{
4892
- type: 'confirm', name: 'proceed',
4893
- message: '仍要写入默认节点配置吗?', default: false
4894
- }]);
4895
- if (!proceed) { console.log(chalk.gray('已取消')); return; }
4896
- }
4897
- }
5020
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5021
+ speedIntro: '📡 开始测速 Codex 节点...\n',
5022
+ proceedMessage: '仍要写入默认节点配置吗?'
5023
+ });
5024
+ if (!selectedEndpoint) return;
4898
5025
 
4899
5026
  const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
4900
5027
  const repairResult = applyConfigRepairsWithSync(config, paths);
@@ -5048,6 +5175,8 @@ async function autoActivate(paths, args = {}) {
5048
5175
  const apiKeyEnvFallbacks = [
5049
5176
  'OPENCLAW_CLAUDE_KEY',
5050
5177
  'OPENCLAW_CODEX_KEY',
5178
+ 'ANTHROPIC_API_KEY',
5179
+ 'ANTHROPIC_AUTH_TOKEN',
5051
5180
  'CLAUDE_API_KEY',
5052
5181
  'OPENAI_API_KEY',
5053
5182
  'OPENCLAW_API_KEY'
@@ -5258,40 +5387,10 @@ async function autoActivate(paths, args = {}) {
5258
5387
  async function activateClaudeCode(paths, args = {}) {
5259
5388
  console.log(chalk.cyan.bold('\n🔧 配置 Claude Code CLI\n'));
5260
5389
  const selectedModel = await promptClaudeModelSelection(args);
5261
-
5262
- // ---- 测速选节点 ----
5263
- const shouldTest = !(args['no-test'] || args.noTest);
5264
- let selectedEndpoint = ENDPOINTS[0];
5265
-
5266
- if (shouldTest) {
5267
- console.log(chalk.cyan('📡 开始测速节点...\n'));
5268
- const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
5269
- const sorted = speedResult.ranked || [];
5270
- if (sorted.length > 0) {
5271
- const defaultEp = ENDPOINTS[0];
5272
- const { selectedIndex } = await inquirer.prompt([{
5273
- type: 'list',
5274
- name: 'selectedIndex',
5275
- message: '选择节点:',
5276
- choices: [
5277
- { name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
5278
- new inquirer.Separator(' ---- 或按测速结果选择 ----'),
5279
- ...sorted.map((e, i) => ({
5280
- name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
5281
- value: i
5282
- }))
5283
- ]
5284
- }]);
5285
- selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
5286
- } else {
5287
- console.log(chalk.red('\n⚠️ 所有节点均不可达'));
5288
- const { proceed } = await inquirer.prompt([{
5289
- type: 'confirm', name: 'proceed',
5290
- message: '仍要使用默认节点配置吗?', default: false
5291
- }]);
5292
- if (!proceed) { console.log(chalk.gray('已取消')); return; }
5293
- }
5294
- }
5390
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5391
+ proceedMessage: '仍要使用默认节点配置吗?'
5392
+ });
5393
+ if (!selectedEndpoint) return;
5295
5394
 
5296
5395
  // ---- API Key ----
5297
5396
  const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
@@ -5299,7 +5398,7 @@ async function activateClaudeCode(paths, args = {}) {
5299
5398
  if (directKey) {
5300
5399
  apiKey = directKey;
5301
5400
  } else {
5302
- const envKey = process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '';
5401
+ const envKey = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '';
5303
5402
  apiKey = await promptApiKey('请输入 API Key:', envKey);
5304
5403
  }
5305
5404
  if (!apiKey) { console.log(chalk.gray('已取消')); return; }
@@ -5327,7 +5426,7 @@ async function activateClaudeCode(paths, args = {}) {
5327
5426
  console.log(chalk.gray(' API Key: 已设置'));
5328
5427
  console.log(chalk.gray('\n 已写入:'));
5329
5428
  console.log(chalk.gray(' • ~/.claude/settings.json'));
5330
- console.log(chalk.gray(' • 最小字段: apiBaseUrl + env.ANTHROPIC_AUTH_TOKEN'));
5429
+ console.log(chalk.gray(' • 最小字段: apiBaseUrl + env.ANTHROPIC_API_KEY + env.ANTHROPIC_BASE_URL'));
5331
5430
  console.log(chalk.yellow('\n 提示: 如果 Claude Code 已在运行,重新打开一个会话即可读取新配置'));
5332
5431
 
5333
5432
  if (await confirmImmediateTest(args, '是否立即测试 Claude Code CLI 连接?')) {
@@ -5339,40 +5438,10 @@ async function activateClaudeCode(paths, args = {}) {
5339
5438
  async function activateOpencode(paths, args = {}) {
5340
5439
  console.log(chalk.cyan.bold('\n🔧 配置 Opencode\n'));
5341
5440
  const defaultModel = await promptOpencodeDefaultModelSelection(args);
5342
-
5343
- // ---- 测速选节点 ----
5344
- const shouldTest = !(args['no-test'] || args.noTest);
5345
- let selectedEndpoint = ENDPOINTS[0];
5346
-
5347
- if (shouldTest) {
5348
- console.log(chalk.cyan('📡 开始测速节点...\n'));
5349
- const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
5350
- const sorted = speedResult.ranked || [];
5351
- if (sorted.length > 0) {
5352
- const defaultEp = ENDPOINTS[0];
5353
- const { selectedIndex } = await inquirer.prompt([{
5354
- type: 'list',
5355
- name: 'selectedIndex',
5356
- message: '选择节点:',
5357
- choices: [
5358
- { name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
5359
- new inquirer.Separator(' ---- 或按测速结果选择 ----'),
5360
- ...sorted.map((e, i) => ({
5361
- name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
5362
- value: i
5363
- }))
5364
- ]
5365
- }]);
5366
- selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
5367
- } else {
5368
- console.log(chalk.red('\n⚠️ 所有节点均不可达'));
5369
- const { proceed } = await inquirer.prompt([{
5370
- type: 'confirm', name: 'proceed',
5371
- message: '仍要使用默认节点配置吗?', default: false
5372
- }]);
5373
- if (!proceed) { console.log(chalk.gray('已取消')); return; }
5374
- }
5375
- }
5441
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5442
+ proceedMessage: '仍要使用默认节点配置吗?'
5443
+ });
5444
+ if (!selectedEndpoint) return;
5376
5445
 
5377
5446
  // ---- API Key ----
5378
5447
  const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
@@ -5380,7 +5449,7 @@ async function activateOpencode(paths, args = {}) {
5380
5449
  if (directKey) {
5381
5450
  apiKey = directKey;
5382
5451
  } else {
5383
- const envKey = process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '';
5452
+ const envKey = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '';
5384
5453
  apiKey = await promptApiKey('请输入 API Key:', envKey);
5385
5454
  }
5386
5455
  if (!apiKey) { console.log(chalk.gray('已取消')); return; }
@@ -5436,39 +5505,10 @@ async function activateCodex(paths, args = {}) {
5436
5505
  }
5437
5506
  const modelConfig = CODEX_MODELS.find(m => m.id === modelId) || { id: modelId, name: modelId };
5438
5507
 
5439
- // ---- 测速选节点 ----
5440
- const shouldTest = !(args['no-test'] || args.noTest);
5441
- let selectedEndpoint = ENDPOINTS[0];
5442
-
5443
- if (shouldTest) {
5444
- console.log(chalk.cyan('📡 开始测速节点...\n'));
5445
- const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
5446
- const sorted = speedResult.ranked || [];
5447
- if (sorted.length > 0) {
5448
- const defaultEp = ENDPOINTS[0];
5449
- const { selectedIndex } = await inquirer.prompt([{
5450
- type: 'list',
5451
- name: 'selectedIndex',
5452
- message: '选择节点:',
5453
- choices: [
5454
- { name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
5455
- new inquirer.Separator(' ---- 或按测速结果选择 ----'),
5456
- ...sorted.map((e, i) => ({
5457
- name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
5458
- value: i
5459
- }))
5460
- ]
5461
- }]);
5462
- selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
5463
- } else {
5464
- console.log(chalk.red('\n⚠️ 所有节点均不可达'));
5465
- const { proceed } = await inquirer.prompt([{
5466
- type: 'confirm', name: 'proceed',
5467
- message: '仍要使用默认节点配置吗?', default: false
5468
- }]);
5469
- if (!proceed) { console.log(chalk.gray('已取消')); return; }
5470
- }
5471
- }
5508
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5509
+ proceedMessage: '仍要使用默认节点配置吗?'
5510
+ });
5511
+ if (!selectedEndpoint) return;
5472
5512
 
5473
5513
  // ---- API Key ----
5474
5514
  const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
@@ -5518,39 +5558,10 @@ async function activateHermes(paths, args = {}) {
5518
5558
  const selection = await promptHermesModelSelection(args, '选择 Hermes 默认模型:');
5519
5559
  const selectedModel = selection.model;
5520
5560
  const selectedType = selection.type;
5521
-
5522
- const shouldTest = !(args['no-test'] || args.noTest);
5523
- let selectedEndpoint = ENDPOINTS[0];
5524
-
5525
- if (shouldTest) {
5526
- console.log(chalk.cyan('📡 开始测速节点...\n'));
5527
- const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
5528
- const sorted = speedResult.ranked || [];
5529
- if (sorted.length > 0) {
5530
- const defaultEp = ENDPOINTS[0];
5531
- const { selectedIndex } = await inquirer.prompt([{
5532
- type: 'list',
5533
- name: 'selectedIndex',
5534
- message: '选择节点:',
5535
- choices: [
5536
- { name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
5537
- new inquirer.Separator(' ---- 或按测速结果选择 ----'),
5538
- ...sorted.map((e, i) => ({
5539
- name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
5540
- value: i
5541
- }))
5542
- ]
5543
- }]);
5544
- selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
5545
- } else {
5546
- console.log(chalk.red('\n⚠️ 所有节点均不可达'));
5547
- const { proceed } = await inquirer.prompt([{
5548
- type: 'confirm', name: 'proceed',
5549
- message: '仍要使用默认节点配置吗?', default: false
5550
- }]);
5551
- if (!proceed) { console.log(chalk.gray('已取消')); return; }
5552
- }
5553
- }
5561
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5562
+ proceedMessage: '仍要使用默认节点配置吗?'
5563
+ });
5564
+ if (!selectedEndpoint) return;
5554
5565
 
5555
5566
  const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
5556
5567
  let apiKey;
@@ -5592,8 +5603,12 @@ async function activateHermes(paths, args = {}) {
5592
5603
  if (hermesPaths.wslConfigPath && hermesPaths.wslEnvPath) {
5593
5604
  console.log(chalk.gray(' • 已额外同步到 WSL ~/.hermes'));
5594
5605
  }
5595
- console.log(chalk.yellow('\n 提示: Hermes Desktop 的“测试连接”按钮当前可能仍按 /v1/models 检测;按钮不绿,不代表 runtime 不能正常对话'));
5596
- console.log(chalk.yellow(' 建议: 重启 Hermes Desktop,或手动执行 hermes server --port 8787 验证 Hermes runtime'));
5606
+ console.log(chalk.yellow('\n 提示: Hermes doctor 当前会固定检查官方 Anthropic /v1/models,不会读取 model.base_url;第三方 Anthropic 中转可能被误报为 invalid API key'));
5607
+ console.log(chalk.yellow(' 建议: yymaxapi 的 Hermes CLI 运行时测试,或 `hermes chat -Q -q "请只回复 OK"` 的结果为准'));
5608
+
5609
+ if (await confirmImmediateTest(args, '是否立即测试 Hermes CLI 连接?')) {
5610
+ await testAdditionalCliConnections(args, { only: ['hermes'] });
5611
+ }
5597
5612
  }
5598
5613
 
5599
5614
  // ============ 主程序 ============
@@ -5635,6 +5650,10 @@ async function main() {
5635
5650
  await testConnection(paths, args);
5636
5651
  return;
5637
5652
  }
5653
+ if (args._.includes('speed-test') || args._.includes('speedtest')) {
5654
+ await speedTestNodes();
5655
+ return;
5656
+ }
5638
5657
 
5639
5658
  while (true) {
5640
5659
  // 显示当前状态
@@ -5661,6 +5680,7 @@ async function main() {
5661
5680
  new inquirer.Separator(' -- 工具 --'),
5662
5681
  { name: ' 切换 OpenClaw 模型', value: 'switch_model' },
5663
5682
  { name: ` 权限管理${getToolsProfileTag(paths)}`, value: 'tools_profile' },
5683
+ { name: ' 测速节点', value: 'speed_test_nodes' },
5664
5684
  { name: ' 测试连接', value: 'test_connection' },
5665
5685
  { name: ' 查看配置', value: 'view_config' },
5666
5686
  { name: ' 恢复默认', value: 'restore' },
@@ -5684,6 +5704,9 @@ async function main() {
5684
5704
  case 'test_connection':
5685
5705
  await testConnection(paths, {});
5686
5706
  break;
5707
+ case 'speed_test_nodes':
5708
+ await speedTestNodes();
5709
+ break;
5687
5710
  case 'switch_model':
5688
5711
  await switchModel(paths);
5689
5712
  break;
@@ -6888,6 +6911,8 @@ function testGatewayViaAgent(model, agentId = '') {
6888
6911
  }
6889
6912
  const nodeInfo = findCompatibleNode(nodeMajor);
6890
6913
  const env = { ...process.env, PATH: extendPathEnv(nodeInfo ? nodeInfo.path : null), NODE_NO_WARNINGS: '1' };
6914
+ delete env.ANTHROPIC_API_KEY;
6915
+ delete env.ANTHROPIC_AUTH_TOKEN;
6891
6916
  delete env.CLAUDE_API_KEY;
6892
6917
  delete env.OPENCLAW_CLAUDE_KEY;
6893
6918
  delete env.OPENCLAW_API_KEY;
@@ -155,7 +155,8 @@ npx yymaxapi@latest
155
155
  {
156
156
  "apiBaseUrl": "https://yunyi.rdzhvip.com/claude",
157
157
  "env": {
158
- "ANTHROPIC_AUTH_TOKEN": "<你的云翼 API Key>"
158
+ "ANTHROPIC_API_KEY": "<你的云翼 API Key>",
159
+ "ANTHROPIC_BASE_URL": "https://yunyi.rdzhvip.com/claude"
159
160
  }
160
161
  }
161
162
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yymaxapi",
3
- "version": "1.0.104",
3
+ "version": "1.0.108",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "bin/yymaxapi.js",
6
6
  "bin": {