yymaxapi 1.0.104 → 1.0.109

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');
@@ -3318,6 +3391,15 @@ async function confirmImmediateTest(args = {}, message = '是否立即测试连
3318
3391
  return shouldTest;
3319
3392
  }
3320
3393
 
3394
+ async function pauseBeforeReturningToMenu(args = {}, message = '测试完成,按回车返回主菜单') {
3395
+ if (!args.fromMenu) return;
3396
+ await inquirer.prompt([{
3397
+ type: 'input',
3398
+ name: 'continue',
3399
+ message
3400
+ }]);
3401
+ }
3402
+
3321
3403
  function extendPathEnv(preferredNodePath) {
3322
3404
  const current = process.env.PATH || '';
3323
3405
  const parts = current.split(path.delimiter).filter(Boolean);
@@ -3560,7 +3642,7 @@ function readTextIfExists(filePath) {
3560
3642
  }
3561
3643
 
3562
3644
  function getHermesDataDir() {
3563
- const envDir = String(process.env.HERMES_DATA_DIR || '').trim();
3645
+ const envDir = String(process.env.HERMES_HOME || process.env.HERMES_DATA_DIR || '').trim();
3564
3646
  return envDir || path.join(os.homedir(), '.hermes');
3565
3647
  }
3566
3648
 
@@ -3795,6 +3877,32 @@ function readHermesYamlConfig(configPath = getHermesConfigPath()) {
3795
3877
  };
3796
3878
  }
3797
3879
 
3880
+ function readHermesCliConfig() {
3881
+ const yamlConfig = readHermesYamlConfig();
3882
+ const envPath = getHermesEnvPath();
3883
+ const envEntries = parseEnvFile(readTextIfExists(envPath));
3884
+ const modelConfig = yamlConfig.config?.model && typeof yamlConfig.config.model === 'object'
3885
+ ? yamlConfig.config.model
3886
+ : {};
3887
+ const modelId = String(modelConfig.default || '').trim() || getDefaultClaudeModel().id;
3888
+ const provider = String(modelConfig.provider || '').trim().toLowerCase();
3889
+ const type = resolveHermesModelType(modelId, provider);
3890
+ const apiKey = type === 'codex'
3891
+ ? (envEntries.OPENAI_API_KEY || process.env.OPENAI_API_KEY || process.env.OPENAI_AUTH_TOKEN || '')
3892
+ : (envEntries.ANTHROPIC_API_KEY || envEntries.ANTHROPIC_TOKEN || process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_TOKEN || process.env.CLAUDE_CODE_OAUTH_TOKEN || '');
3893
+
3894
+ return {
3895
+ configPath: yamlConfig.configPath,
3896
+ envPath,
3897
+ configured: yamlConfig.configured || fs.existsSync(envPath),
3898
+ modelId,
3899
+ provider,
3900
+ type,
3901
+ baseUrl: String(modelConfig.base_url || '').trim(),
3902
+ apiKey
3903
+ };
3904
+ }
3905
+
3798
3906
  function getOpencodeConfigPath() {
3799
3907
  const home = os.homedir();
3800
3908
  return process.platform === 'win32'
@@ -3813,11 +3921,20 @@ function getCodexCliPaths() {
3813
3921
  function readClaudeCodeCliConfig() {
3814
3922
  const settingsPath = getClaudeCodeSettingsPath();
3815
3923
  const settings = readJsonIfExists(settingsPath) || {};
3924
+ const settingsEnv = settings.env && typeof settings.env === 'object' ? settings.env : {};
3925
+ const settingsApiKey = String(settingsEnv.ANTHROPIC_API_KEY || '').trim();
3926
+ const settingsLegacyAuthToken = String(settingsEnv.ANTHROPIC_AUTH_TOKEN || '').trim();
3927
+ const settingsBaseUrl = String(settingsEnv.ANTHROPIC_BASE_URL || settings.apiBaseUrl || '').trim();
3816
3928
  return {
3817
3929
  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 || '',
3930
+ modelId: settings.model || settingsEnv.ANTHROPIC_MODEL || process.env.ANTHROPIC_MODEL || getDefaultClaudeModel().id,
3931
+ baseUrl: settingsBaseUrl || process.env.ANTHROPIC_BASE_URL || '',
3932
+ apiKey: settingsApiKey || settingsLegacyAuthToken || process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '',
3933
+ settingsBaseUrl,
3934
+ settingsApiKey,
3935
+ settingsLegacyAuthToken,
3936
+ hasLegacyAuthTokenInSettings: !!settingsLegacyAuthToken,
3937
+ usesLegacyAuthTokenOnly: !settingsApiKey && !!settingsLegacyAuthToken,
3821
3938
  configured: fs.existsSync(settingsPath)
3822
3939
  };
3823
3940
  }
@@ -3872,6 +3989,23 @@ function summarizeCliTestOutput(text) {
3872
3989
  return lines[0].slice(0, 160);
3873
3990
  }
3874
3991
 
3992
+ function extractHermesCliReply(text) {
3993
+ const lines = cleanCliTestOutput(text)
3994
+ .split('\n')
3995
+ .map(line => line.trim())
3996
+ .filter(Boolean)
3997
+ .filter(line => !/^session_id:/i.test(line))
3998
+ .filter(line => !/^resume this session/i.test(line))
3999
+ .filter(line => !/^session:/i.test(line))
4000
+ .filter(line => !/^duration:/i.test(line))
4001
+ .filter(line => !/^messages:/i.test(line))
4002
+ .filter(line => !/^[╭╰│─]+/.test(line));
4003
+
4004
+ const okLine = lines.find(line => /^OK$/i.test(line));
4005
+ if (okLine) return okLine;
4006
+ return lines[0] || '';
4007
+ }
4008
+
3875
4009
  function buildReadableAgentError(text, fallback = '') {
3876
4010
  const cleaned = cleanCliTestOutput(text);
3877
4011
  if (!cleaned) return fallback;
@@ -3887,6 +4021,11 @@ function looksLikeCliTestError(text) {
3887
4021
  'all models failed',
3888
4022
  'auth_permanent',
3889
4023
  'auth issue',
4024
+ 'authentication_error',
4025
+ 'failed to authenticate',
4026
+ 'invalid bearer token',
4027
+ 'invalid api key',
4028
+ 'fix external api key',
3890
4029
  'unauthorized',
3891
4030
  'forbidden',
3892
4031
  'missing environment variable',
@@ -3914,17 +4053,28 @@ function testClaudeCodeCliConnection() {
3914
4053
 
3915
4054
  const config = readClaudeCodeCliConfig();
3916
4055
  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' };
4056
+ if (config.usesLegacyAuthTokenOnly) {
4057
+ return {
4058
+ name: 'Claude Code CLI',
4059
+ status: 'failed',
4060
+ detail: '检测到旧版 Claude Code 配置字段 ANTHROPIC_AUTH_TOKEN,请重新运行 yymaxapi 以改写为 ANTHROPIC_API_KEY'
4061
+ };
4062
+ }
4063
+ if (!config.settingsBaseUrl || !config.settingsApiKey) {
4064
+ return { name: 'Claude Code CLI', status: 'failed', detail: 'Claude Code 配置缺少 settings.json 中的 Base URL 或 API Key' };
4065
+ }
3918
4066
 
3919
4067
  const env = {
3920
4068
  ...process.env,
3921
4069
  PATH: extendPathEnv(null),
3922
- ANTHROPIC_BASE_URL: config.baseUrl,
3923
- ANTHROPIC_AUTH_TOKEN: config.apiKey,
3924
- ANTHROPIC_MODEL: config.modelId,
3925
4070
  NODE_TLS_REJECT_UNAUTHORIZED: '0',
3926
4071
  NODE_NO_WARNINGS: '1'
3927
4072
  };
4073
+ delete env.ANTHROPIC_BASE_URL;
4074
+ delete env.ANTHROPIC_API_KEY;
4075
+ delete env.ANTHROPIC_AUTH_TOKEN;
4076
+ delete env.ANTHROPIC_MODEL;
4077
+ delete env.CLAUDE_API_KEY;
3928
4078
 
3929
4079
  return runCliTestCandidates('Claude Code CLI', [
3930
4080
  `${shellQuote(cliBinary)} -p --model ${shellQuote(config.modelId)} --output-format text ${shellQuote('请只回复 OK')}`,
@@ -3976,14 +4126,62 @@ function testCodexCliConnection() {
3976
4126
  ], env);
3977
4127
  }
3978
4128
 
4129
+ function testHermesCliConnection() {
4130
+ const cliBinary = resolveCommandBinary('hermes');
4131
+ if (!cliBinary) return { name: 'Hermes CLI', status: 'skipped', detail: '未安装 hermes 命令' };
4132
+
4133
+ const config = readHermesCliConfig();
4134
+ if (!config.configured) return { name: 'Hermes CLI', status: 'skipped', detail: '未检测到 ~/.hermes/config.yaml 或 ~/.hermes/.env' };
4135
+ if (!config.apiKey) return { name: 'Hermes CLI', status: 'failed', detail: 'Hermes 配置缺少 API Key' };
4136
+
4137
+ const env = {
4138
+ ...process.env,
4139
+ PATH: extendPathEnv(null),
4140
+ NODE_TLS_REJECT_UNAUTHORIZED: '0',
4141
+ NODE_NO_WARNINGS: '1'
4142
+ };
4143
+
4144
+ if (config.type === 'codex') {
4145
+ env.OPENAI_API_KEY = config.apiKey;
4146
+ delete env.ANTHROPIC_API_KEY;
4147
+ delete env.ANTHROPIC_TOKEN;
4148
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
4149
+ } else {
4150
+ env.ANTHROPIC_API_KEY = config.apiKey;
4151
+ if (config.baseUrl) env.ANTHROPIC_BASE_URL = config.baseUrl;
4152
+ delete env.ANTHROPIC_TOKEN;
4153
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
4154
+ }
4155
+
4156
+ const providerArg = config.provider === 'anthropic' ? ' --provider anthropic' : '';
4157
+ const result = safeExec(`${shellQuote(cliBinary)} chat -Q -q ${shellQuote('请只回复 OK')}${providerArg}`, {
4158
+ timeout: 120000,
4159
+ env,
4160
+ maxBuffer: 1024 * 1024
4161
+ });
4162
+ const combined = cleanCliTestOutput(`${result.output || ''}\n${result.stdout || ''}\n${result.stderr || ''}`);
4163
+ const reply = extractHermesCliReply(combined);
4164
+
4165
+ if (result.ok && /^OK$/i.test(reply) && !looksLikeCliTestError(combined)) {
4166
+ return { name: 'Hermes CLI', status: 'success', detail: 'OK' };
4167
+ }
4168
+
4169
+ return {
4170
+ name: 'Hermes CLI',
4171
+ status: 'failed',
4172
+ detail: reply || summarizeCliTestOutput(combined) || result.error || '命令执行失败'
4173
+ };
4174
+ }
4175
+
3979
4176
  async function testAdditionalCliConnections(args = {}, options = {}) {
3980
4177
  if (args['no-app-test'] || args.noAppTest) return;
3981
4178
 
3982
- const requested = new Set((options.only || ['claude', 'opencode', 'codex']).map(item => String(item)));
4179
+ const requested = new Set((options.only || ['claude', 'opencode', 'codex', 'hermes']).map(item => String(item)));
3983
4180
  const results = [];
3984
4181
  if (requested.has('claude')) results.push(testClaudeCodeCliConnection());
3985
4182
  if (requested.has('opencode')) results.push(testOpencodeCliConnection());
3986
4183
  if (requested.has('codex')) results.push(testCodexCliConnection());
4184
+ if (requested.has('hermes')) results.push(testHermesCliConnection());
3987
4185
  if (results.length === 0) return;
3988
4186
 
3989
4187
  console.log(chalk.cyan('\n附加测试: 其他 CLI 连接...'));
@@ -4670,47 +4868,15 @@ async function quickSetup(paths, args = {}) {
4670
4868
  }
4671
4869
 
4672
4870
  async function presetClaude(paths, args = {}) {
4673
- console.log(chalk.cyan.bold('\n🚀 Claude 快速配置(自动测速推荐节点)\n'));
4871
+ console.log(chalk.cyan.bold('\n🚀 Claude 快速配置(默认节点直连)\n'));
4674
4872
 
4675
4873
  const apiConfig = API_CONFIG.claude;
4676
4874
  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
- }
4875
+ const selectedEndpoint = await resolveEndpointSelection(args, {
4876
+ speedIntro: '📡 开始测速 Claude 节点...\n',
4877
+ proceedMessage: '仍要写入默认节点配置吗?'
4878
+ });
4879
+ if (!selectedEndpoint) return;
4714
4880
 
4715
4881
  const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
4716
4882
  const repairResult = applyConfigRepairsWithSync(config, paths);
@@ -4722,6 +4888,8 @@ async function presetClaude(paths, args = {}) {
4722
4888
 
4723
4889
  const apiKeyEnvFallbacks = [
4724
4890
  'OPENCLAW_CLAUDE_KEY',
4891
+ 'ANTHROPIC_API_KEY',
4892
+ 'ANTHROPIC_AUTH_TOKEN',
4725
4893
  'CLAUDE_API_KEY',
4726
4894
  'OPENCLAW_API_KEY'
4727
4895
  ];
@@ -4853,48 +5021,16 @@ async function presetClaude(paths, args = {}) {
4853
5021
  }
4854
5022
 
4855
5023
  async function presetCodex(paths, args = {}) {
4856
- console.log(chalk.cyan.bold('\n🚀 Codex 快速配置(自动测速推荐节点)\n'));
5024
+ console.log(chalk.cyan.bold('\n🚀 Codex 快速配置(默认节点直连)\n'));
4857
5025
 
4858
5026
  const apiConfig = API_CONFIG.codex;
4859
5027
  const providerPrefix = (args['provider-prefix'] || args.prefix || apiConfig.providerName).toString().trim() || apiConfig.providerName;
4860
5028
  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
- }
5029
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5030
+ speedIntro: '📡 开始测速 Codex 节点...\n',
5031
+ proceedMessage: '仍要写入默认节点配置吗?'
5032
+ });
5033
+ if (!selectedEndpoint) return;
4898
5034
 
4899
5035
  const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
4900
5036
  const repairResult = applyConfigRepairsWithSync(config, paths);
@@ -5048,6 +5184,8 @@ async function autoActivate(paths, args = {}) {
5048
5184
  const apiKeyEnvFallbacks = [
5049
5185
  'OPENCLAW_CLAUDE_KEY',
5050
5186
  'OPENCLAW_CODEX_KEY',
5187
+ 'ANTHROPIC_API_KEY',
5188
+ 'ANTHROPIC_AUTH_TOKEN',
5051
5189
  'CLAUDE_API_KEY',
5052
5190
  'OPENAI_API_KEY',
5053
5191
  'OPENCLAW_API_KEY'
@@ -5258,40 +5396,10 @@ async function autoActivate(paths, args = {}) {
5258
5396
  async function activateClaudeCode(paths, args = {}) {
5259
5397
  console.log(chalk.cyan.bold('\n🔧 配置 Claude Code CLI\n'));
5260
5398
  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
- }
5399
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5400
+ proceedMessage: '仍要使用默认节点配置吗?'
5401
+ });
5402
+ if (!selectedEndpoint) return;
5295
5403
 
5296
5404
  // ---- API Key ----
5297
5405
  const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
@@ -5299,7 +5407,7 @@ async function activateClaudeCode(paths, args = {}) {
5299
5407
  if (directKey) {
5300
5408
  apiKey = directKey;
5301
5409
  } else {
5302
- const envKey = process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '';
5410
+ const envKey = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '';
5303
5411
  apiKey = await promptApiKey('请输入 API Key:', envKey);
5304
5412
  }
5305
5413
  if (!apiKey) { console.log(chalk.gray('已取消')); return; }
@@ -5327,11 +5435,12 @@ async function activateClaudeCode(paths, args = {}) {
5327
5435
  console.log(chalk.gray(' API Key: 已设置'));
5328
5436
  console.log(chalk.gray('\n 已写入:'));
5329
5437
  console.log(chalk.gray(' • ~/.claude/settings.json'));
5330
- console.log(chalk.gray(' • 最小字段: apiBaseUrl + env.ANTHROPIC_AUTH_TOKEN'));
5438
+ console.log(chalk.gray(' • 最小字段: apiBaseUrl + env.ANTHROPIC_API_KEY + env.ANTHROPIC_BASE_URL'));
5331
5439
  console.log(chalk.yellow('\n 提示: 如果 Claude Code 已在运行,重新打开一个会话即可读取新配置'));
5332
5440
 
5333
5441
  if (await confirmImmediateTest(args, '是否立即测试 Claude Code CLI 连接?')) {
5334
5442
  await testAdditionalCliConnections(args, { only: ['claude'] });
5443
+ await pauseBeforeReturningToMenu(args);
5335
5444
  }
5336
5445
  }
5337
5446
 
@@ -5339,40 +5448,10 @@ async function activateClaudeCode(paths, args = {}) {
5339
5448
  async function activateOpencode(paths, args = {}) {
5340
5449
  console.log(chalk.cyan.bold('\n🔧 配置 Opencode\n'));
5341
5450
  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
- }
5451
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5452
+ proceedMessage: '仍要使用默认节点配置吗?'
5453
+ });
5454
+ if (!selectedEndpoint) return;
5376
5455
 
5377
5456
  // ---- API Key ----
5378
5457
  const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
@@ -5380,7 +5459,7 @@ async function activateOpencode(paths, args = {}) {
5380
5459
  if (directKey) {
5381
5460
  apiKey = directKey;
5382
5461
  } else {
5383
- const envKey = process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '';
5462
+ const envKey = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '';
5384
5463
  apiKey = await promptApiKey('请输入 API Key:', envKey);
5385
5464
  }
5386
5465
  if (!apiKey) { console.log(chalk.gray('已取消')); return; }
@@ -5416,6 +5495,7 @@ async function activateOpencode(paths, args = {}) {
5416
5495
 
5417
5496
  if (await confirmImmediateTest(args, '是否立即测试 Opencode CLI 连接?')) {
5418
5497
  await testAdditionalCliConnections(args, { only: ['opencode'] });
5498
+ await pauseBeforeReturningToMenu(args);
5419
5499
  }
5420
5500
  }
5421
5501
 
@@ -5436,39 +5516,10 @@ async function activateCodex(paths, args = {}) {
5436
5516
  }
5437
5517
  const modelConfig = CODEX_MODELS.find(m => m.id === modelId) || { id: modelId, name: modelId };
5438
5518
 
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
- }
5519
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5520
+ proceedMessage: '仍要使用默认节点配置吗?'
5521
+ });
5522
+ if (!selectedEndpoint) return;
5472
5523
 
5473
5524
  // ---- API Key ----
5474
5525
  const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
@@ -5509,6 +5560,7 @@ async function activateCodex(paths, args = {}) {
5509
5560
 
5510
5561
  if (await confirmImmediateTest(args, '是否立即测试 Codex CLI 连接?')) {
5511
5562
  await testAdditionalCliConnections(args, { only: ['codex'] });
5563
+ await pauseBeforeReturningToMenu(args);
5512
5564
  }
5513
5565
  }
5514
5566
 
@@ -5518,39 +5570,10 @@ async function activateHermes(paths, args = {}) {
5518
5570
  const selection = await promptHermesModelSelection(args, '选择 Hermes 默认模型:');
5519
5571
  const selectedModel = selection.model;
5520
5572
  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
- }
5573
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5574
+ proceedMessage: '仍要使用默认节点配置吗?'
5575
+ });
5576
+ if (!selectedEndpoint) return;
5554
5577
 
5555
5578
  const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
5556
5579
  let apiKey;
@@ -5592,8 +5615,13 @@ async function activateHermes(paths, args = {}) {
5592
5615
  if (hermesPaths.wslConfigPath && hermesPaths.wslEnvPath) {
5593
5616
  console.log(chalk.gray(' • 已额外同步到 WSL ~/.hermes'));
5594
5617
  }
5595
- console.log(chalk.yellow('\n 提示: Hermes Desktop 的“测试连接”按钮当前可能仍按 /v1/models 检测;按钮不绿,不代表 runtime 不能正常对话'));
5596
- console.log(chalk.yellow(' 建议: 重启 Hermes Desktop,或手动执行 hermes server --port 8787 验证 Hermes runtime'));
5618
+ console.log(chalk.yellow('\n 提示: Hermes doctor 当前会固定检查官方 Anthropic /v1/models,不会读取 model.base_url;第三方 Anthropic 中转可能被误报为 invalid API key'));
5619
+ console.log(chalk.yellow(' 建议: yymaxapi 的 Hermes CLI 运行时测试,或 `hermes chat -Q -q "请只回复 OK"` 的结果为准'));
5620
+
5621
+ if (await confirmImmediateTest(args, '是否立即测试 Hermes CLI 连接?')) {
5622
+ await testAdditionalCliConnections(args, { only: ['hermes'] });
5623
+ await pauseBeforeReturningToMenu(args);
5624
+ }
5597
5625
  }
5598
5626
 
5599
5627
  // ============ 主程序 ============
@@ -5635,6 +5663,10 @@ async function main() {
5635
5663
  await testConnection(paths, args);
5636
5664
  return;
5637
5665
  }
5666
+ if (args._.includes('speed-test') || args._.includes('speedtest')) {
5667
+ await speedTestNodes();
5668
+ return;
5669
+ }
5638
5670
 
5639
5671
  while (true) {
5640
5672
  // 显示当前状态
@@ -5661,6 +5693,7 @@ async function main() {
5661
5693
  new inquirer.Separator(' -- 工具 --'),
5662
5694
  { name: ' 切换 OpenClaw 模型', value: 'switch_model' },
5663
5695
  { name: ` 权限管理${getToolsProfileTag(paths)}`, value: 'tools_profile' },
5696
+ { name: ' 测速节点', value: 'speed_test_nodes' },
5664
5697
  { name: ' 测试连接', value: 'test_connection' },
5665
5698
  { name: ' 查看配置', value: 'view_config' },
5666
5699
  { name: ' 恢复默认', value: 'restore' },
@@ -5684,6 +5717,9 @@ async function main() {
5684
5717
  case 'test_connection':
5685
5718
  await testConnection(paths, {});
5686
5719
  break;
5720
+ case 'speed_test_nodes':
5721
+ await speedTestNodes();
5722
+ break;
5687
5723
  case 'switch_model':
5688
5724
  await switchModel(paths);
5689
5725
  break;
@@ -5697,16 +5733,16 @@ async function main() {
5697
5733
  await restore(paths);
5698
5734
  break;
5699
5735
  case 'activate_claude_code':
5700
- await activateClaudeCode(paths);
5736
+ await activateClaudeCode(paths, { fromMenu: true });
5701
5737
  break;
5702
5738
  case 'activate_hermes':
5703
- await activateHermes(paths);
5739
+ await activateHermes(paths, { fromMenu: true });
5704
5740
  break;
5705
5741
  case 'activate_opencode':
5706
- await activateOpencode(paths);
5742
+ await activateOpencode(paths, { fromMenu: true });
5707
5743
  break;
5708
5744
  case 'activate_codex':
5709
- await activateCodex(paths);
5745
+ await activateCodex(paths, { fromMenu: true });
5710
5746
  break;
5711
5747
  }
5712
5748
  } catch (error) {
@@ -6888,6 +6924,8 @@ function testGatewayViaAgent(model, agentId = '') {
6888
6924
  }
6889
6925
  const nodeInfo = findCompatibleNode(nodeMajor);
6890
6926
  const env = { ...process.env, PATH: extendPathEnv(nodeInfo ? nodeInfo.path : null), NODE_NO_WARNINGS: '1' };
6927
+ delete env.ANTHROPIC_API_KEY;
6928
+ delete env.ANTHROPIC_AUTH_TOKEN;
6891
6929
  delete env.CLAUDE_API_KEY;
6892
6930
  delete env.OPENCLAW_CLAUDE_KEY;
6893
6931
  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.109",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "bin/yymaxapi.js",
6
6
  "bin": {