yymaxapi 1.0.103 → 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
@@ -321,6 +321,90 @@ async function promptClaudeModelSelection(args = {}, message = '选择 Claude
321
321
  return CLAUDE_MODELS.find(model => model.id === selected) || fallback;
322
322
  }
323
323
 
324
+ function normalizeHermesModelType(value) {
325
+ const normalized = String(value || '').trim().toLowerCase();
326
+ if (!normalized) return '';
327
+ if (['gpt', 'codex', 'openai'].includes(normalized)) return 'codex';
328
+ if (['claude', 'anthropic'].includes(normalized)) return 'claude';
329
+ return '';
330
+ }
331
+
332
+ function resolveHermesModelType(modelId, preferredType = '') {
333
+ const requestedType = normalizeHermesModelType(preferredType);
334
+ const inClaude = CLAUDE_MODELS.some(model => model.id === modelId);
335
+ const inCodex = CODEX_MODELS.some(model => model.id === modelId);
336
+
337
+ if (requestedType === 'claude' && (inClaude || !inCodex)) return 'claude';
338
+ if (requestedType === 'codex' && (inCodex || !inClaude)) return 'codex';
339
+ if (inCodex && !inClaude) return 'codex';
340
+ if (inClaude && !inCodex) return 'claude';
341
+ return requestedType || 'claude';
342
+ }
343
+
344
+ async function promptHermesModelSelection(args = {}, message = '选择 Hermes 默认模型:') {
345
+ const requestedModel = (args['model-id'] || args.model || '').toString().trim();
346
+ const requestedType = normalizeHermesModelType(
347
+ args.primary || args.type || args.protocol || args.provider || args.runtime
348
+ );
349
+ const explicitClaudeModel = (args['claude-model'] || '').toString().trim();
350
+ const explicitCodexModel = (args['codex-model'] || '').toString().trim();
351
+
352
+ let selectedType = requestedType;
353
+ if (!selectedType && explicitClaudeModel) selectedType = 'claude';
354
+ if (!selectedType && explicitCodexModel) selectedType = 'codex';
355
+ if (!selectedType && requestedModel) {
356
+ selectedType = resolveHermesModelType(requestedModel);
357
+ }
358
+
359
+ if (!selectedType) {
360
+ if (CLAUDE_MODELS.length > 0 && CODEX_MODELS.length === 0) {
361
+ selectedType = 'claude';
362
+ } else if (CODEX_MODELS.length > 0 && CLAUDE_MODELS.length === 0) {
363
+ selectedType = 'codex';
364
+ } else {
365
+ const { selectedType: pickedType } = await inquirer.prompt([{
366
+ type: 'list',
367
+ name: 'selectedType',
368
+ message: '选择 Hermes 模型类型:',
369
+ choices: [
370
+ { name: 'Claude', value: 'claude' },
371
+ { name: 'GPT', value: 'codex' }
372
+ ],
373
+ default: 'claude'
374
+ }]);
375
+ selectedType = pickedType;
376
+ }
377
+ }
378
+
379
+ const modelList = selectedType === 'codex' ? CODEX_MODELS : CLAUDE_MODELS;
380
+ const fallbackModel = modelList[0] || (selectedType === 'codex' ? getDefaultCodexModel() : getDefaultClaudeModel());
381
+ const explicitModelId = selectedType === 'codex'
382
+ ? (explicitCodexModel || requestedModel)
383
+ : (explicitClaudeModel || requestedModel);
384
+
385
+ if (explicitModelId) {
386
+ const model = modelList.find(item => item.id === explicitModelId) || { id: explicitModelId, name: explicitModelId };
387
+ return { type: selectedType, model };
388
+ }
389
+
390
+ if (modelList.length <= 1) {
391
+ return { type: selectedType, model: fallbackModel };
392
+ }
393
+
394
+ const { selected } = await inquirer.prompt([{
395
+ type: 'list',
396
+ name: 'selected',
397
+ message,
398
+ choices: modelList.map(model => ({ name: model.name, value: model.id })),
399
+ default: fallbackModel.id
400
+ }]);
401
+
402
+ return {
403
+ type: selectedType,
404
+ model: modelList.find(model => model.id === selected) || fallbackModel
405
+ };
406
+ }
407
+
324
408
  async function promptOpencodeDefaultModelSelection(args = {}, message = '选择 Opencode 默认模型:') {
325
409
  const fallbackClaude = getDefaultClaudeModel();
326
410
  const fallbackCodex = getDefaultCodexModel();
@@ -523,6 +607,77 @@ async function testFallbackEndpoints() {
523
607
  return testAllEndpoints(FALLBACK_EPS, { label: '检测备用节点', autoFallback: false });
524
608
  }
525
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
+
526
681
  // ============ API Key 验证 ============
527
682
  function httpGetJson(url, headers = {}, timeout = 10000) {
528
683
  return new Promise((resolve, reject) => {
@@ -883,11 +1038,13 @@ function writeClaudeCodeSettings(baseUrl, apiKey, modelId = getDefaultClaudeMode
883
1038
  if (fs.existsSync(settingsPath)) {
884
1039
  try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch { settings = {}; }
885
1040
  }
886
- settings.apiBaseUrl = baseUrl.replace(/\/+$/, '');
1041
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, '');
1042
+ settings.apiBaseUrl = normalizedBaseUrl;
887
1043
  if (!settings.env) settings.env = {};
888
- 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;
889
1047
  delete settings.availableModels;
890
- delete settings.env.ANTHROPIC_BASE_URL;
891
1048
  delete settings.env.ANTHROPIC_MODEL;
892
1049
  if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
893
1050
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
@@ -1075,12 +1232,16 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKe
1075
1232
  return configPath;
1076
1233
  }
1077
1234
 
1078
- function writeHermesConfig(baseUrl, apiKey, modelId = getDefaultClaudeModel().id) {
1235
+ function writeHermesConfig(baseUrl, apiKey, modelId = getDefaultClaudeModel().id, options = {}) {
1079
1236
  const dataDir = getHermesDataDir();
1080
1237
  const configPath = path.join(dataDir, 'config.yaml');
1081
1238
  const envPath = getHermesEnvPath();
1082
1239
  const existingConfigPath = getHermesConfigPath();
1083
1240
  const wslMirror = getHermesWslMirrorInfo();
1241
+ const modelType = options.type || options.provider || '';
1242
+ const resolvedType = resolveHermesModelType(modelId, modelType);
1243
+ const runtimeProvider = resolvedType === 'codex' ? 'custom' : 'anthropic';
1244
+ const apiMode = resolvedType === 'codex' ? 'codex_responses' : 'anthropic_messages';
1084
1245
 
1085
1246
  let existingConfigRaw = readTextIfExists(existingConfigPath);
1086
1247
  if (!existingConfigRaw && wslMirror.sourceConfigPath) {
@@ -1092,15 +1253,25 @@ function writeHermesConfig(baseUrl, apiKey, modelId = getDefaultClaudeModel().id
1092
1253
  existingEnvRaw = readWslTextFile(wslMirror.envPath);
1093
1254
  }
1094
1255
 
1095
- const normalizedBaseUrl = trimClaudeMessagesSuffix(String(baseUrl || '').trim()).replace(/\/+$/, '');
1096
- const nextConfigRaw = upsertSimpleYamlConfig(existingConfigRaw, {
1097
- provider: 'anthropic',
1098
- model: modelId,
1099
- base_url: normalizedBaseUrl
1100
- });
1101
- const nextEnvRaw = upsertEnvFile(existingEnvRaw, {
1102
- ANTHROPIC_API_KEY: String(apiKey || '').trim()
1256
+ const normalizedBaseUrl = (
1257
+ resolvedType === 'codex'
1258
+ ? trimOpenAiEndpointSuffix(String(baseUrl || '').trim())
1259
+ : trimClaudeMessagesSuffix(String(baseUrl || '').trim())
1260
+ ).replace(/\/+$/, '');
1261
+ const existingConfig = parseSimpleYamlConfig(existingConfigRaw);
1262
+ const existingModelConfig = existingConfig.model && !Array.isArray(existingConfig.model) && typeof existingConfig.model === 'object'
1263
+ ? existingConfig.model
1264
+ : {};
1265
+ const nextConfigRaw = upsertHermesModelConfig(existingConfigRaw, {
1266
+ ...existingModelConfig,
1267
+ default: modelId,
1268
+ provider: runtimeProvider,
1269
+ base_url: normalizedBaseUrl,
1270
+ api_mode: apiMode
1103
1271
  });
1272
+ const envEntries = {};
1273
+ envEntries[resolvedType === 'codex' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY'] = String(apiKey || '').trim();
1274
+ const nextEnvRaw = upsertEnvFile(existingEnvRaw, envEntries);
1104
1275
 
1105
1276
  if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
1106
1277
  fs.writeFileSync(configPath, nextConfigRaw, 'utf8');
@@ -1120,6 +1291,9 @@ function writeHermesConfig(baseUrl, apiKey, modelId = getDefaultClaudeModel().id
1120
1291
  wslConfigPath: wslMirror.configPath,
1121
1292
  wslEnvPath: wslMirror.envPath,
1122
1293
  modelId,
1294
+ modelType: resolvedType,
1295
+ provider: runtimeProvider,
1296
+ apiMode,
1123
1297
  baseUrl: normalizedBaseUrl
1124
1298
  };
1125
1299
  }
@@ -3508,24 +3682,58 @@ function readWslTextFile(filePath) {
3508
3682
  return result.ok ? (result.output || result.stdout || '') : '';
3509
3683
  }
3510
3684
 
3685
+ function parseYamlScalar(value) {
3686
+ let normalized = String(value ?? '').trim();
3687
+ if (
3688
+ (normalized.startsWith('"') && normalized.endsWith('"'))
3689
+ || (normalized.startsWith('\'') && normalized.endsWith('\''))
3690
+ ) {
3691
+ try {
3692
+ normalized = JSON.parse(normalized);
3693
+ } catch {
3694
+ normalized = normalized.slice(1, -1);
3695
+ }
3696
+ }
3697
+ return normalized;
3698
+ }
3699
+
3511
3700
  function parseSimpleYamlConfig(text) {
3512
3701
  const config = {};
3702
+ let currentObjectKey = '';
3703
+
3513
3704
  for (const line of String(text || '').split(/\r?\n/)) {
3514
- const match = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*?)\s*$/);
3515
- if (!match) continue;
3516
- let value = match[2].trim();
3517
- if (
3518
- (value.startsWith('"') && value.endsWith('"'))
3519
- || (value.startsWith('\'') && value.endsWith('\''))
3520
- ) {
3521
- try {
3522
- value = JSON.parse(value);
3523
- } catch {
3524
- value = value.slice(1, -1);
3705
+ if (!line.trim() || line.trim().startsWith('#')) continue;
3706
+
3707
+ const topLevelMatch = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*?)\s*$/);
3708
+ if (topLevelMatch && !line.startsWith(' ')) {
3709
+ const key = topLevelMatch[1];
3710
+ const rawValue = topLevelMatch[2];
3711
+ if (rawValue === '') {
3712
+ if (!config[key] || typeof config[key] !== 'object' || Array.isArray(config[key])) {
3713
+ config[key] = {};
3714
+ }
3715
+ currentObjectKey = key;
3716
+ } else {
3717
+ config[key] = parseYamlScalar(rawValue);
3718
+ currentObjectKey = '';
3719
+ }
3720
+ continue;
3721
+ }
3722
+
3723
+ if (currentObjectKey) {
3724
+ const nestedMatch = line.match(/^\s{2}([A-Za-z0-9_-]+)\s*:\s*(.*?)\s*$/);
3725
+ if (nestedMatch) {
3726
+ if (!config[currentObjectKey] || typeof config[currentObjectKey] !== 'object' || Array.isArray(config[currentObjectKey])) {
3727
+ config[currentObjectKey] = {};
3728
+ }
3729
+ config[currentObjectKey][nestedMatch[1]] = parseYamlScalar(nestedMatch[2]);
3730
+ continue;
3525
3731
  }
3526
3732
  }
3527
- config[match[1]] = value;
3733
+
3734
+ currentObjectKey = '';
3528
3735
  }
3736
+
3529
3737
  return config;
3530
3738
  }
3531
3739
 
@@ -3533,31 +3741,71 @@ function serializeYamlScalar(value) {
3533
3741
  return JSON.stringify(String(value ?? ''));
3534
3742
  }
3535
3743
 
3536
- function upsertSimpleYamlConfig(text, entries) {
3537
- const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
3538
- const normalized = lines.length === 1 && lines[0] === '' ? [] : [...lines];
3539
- const seen = new Set();
3744
+ function buildHermesModelConfigBlock(entries) {
3745
+ const ordered = {};
3746
+ for (const key of ['default', 'provider', 'base_url', 'api_mode']) {
3747
+ if (Object.prototype.hasOwnProperty.call(entries, key)) {
3748
+ ordered[key] = entries[key];
3749
+ }
3750
+ }
3751
+ for (const [key, value] of Object.entries(entries)) {
3752
+ if (!Object.prototype.hasOwnProperty.call(ordered, key)) {
3753
+ ordered[key] = value;
3754
+ }
3755
+ }
3540
3756
 
3541
- for (let i = 0; i < normalized.length; i += 1) {
3542
- const match = normalized[i].match(/^([A-Za-z0-9_-]+)\s*:\s*.*$/);
3543
- if (!match) continue;
3544
- const key = match[1];
3545
- if (!Object.prototype.hasOwnProperty.call(entries, key)) continue;
3546
- normalized[i] = `${key}: ${serializeYamlScalar(entries[key])}`;
3547
- seen.add(key);
3757
+ const lines = ['model:'];
3758
+ for (const [key, value] of Object.entries(ordered)) {
3759
+ if (value === undefined || value === null || value === '') continue;
3760
+ lines.push(` ${key}: ${serializeYamlScalar(value)}`);
3548
3761
  }
3762
+ return lines.join('\n');
3763
+ }
3549
3764
 
3550
- for (const key of Object.keys(entries)) {
3551
- if (!seen.has(key)) {
3552
- normalized.push(`${key}: ${serializeYamlScalar(entries[key])}`);
3765
+ function upsertHermesModelConfig(text, modelEntries) {
3766
+ let normalized = String(text || '').replace(/\r\n/g, '\n');
3767
+ normalized = normalized.replace(/^(provider|base_url|api_mode):\s*.*\n?/gm, '');
3768
+
3769
+ const lines = normalized.split('\n');
3770
+ const output = [];
3771
+ let replaced = false;
3772
+
3773
+ for (let i = 0; i < lines.length; i += 1) {
3774
+ const line = lines[i];
3775
+ if (/^model:\s*(.*)$/.test(line)) {
3776
+ replaced = true;
3777
+ if (output.length > 0 && output[output.length - 1] !== '') {
3778
+ output.push('');
3779
+ }
3780
+ output.push(buildHermesModelConfigBlock(modelEntries));
3781
+
3782
+ i += 1;
3783
+ while (i < lines.length) {
3784
+ const nextLine = lines[i];
3785
+ if (/^\s{2,}/.test(nextLine)) {
3786
+ i += 1;
3787
+ continue;
3788
+ }
3789
+ if (nextLine === '') {
3790
+ break;
3791
+ }
3792
+ i -= 1;
3793
+ break;
3794
+ }
3795
+ continue;
3553
3796
  }
3797
+ output.push(line);
3554
3798
  }
3555
3799
 
3556
- while (normalized.length > 0 && normalized[normalized.length - 1] === '') {
3557
- normalized.pop();
3800
+ while (output.length > 0 && output[output.length - 1] === '') {
3801
+ output.pop();
3802
+ }
3803
+ if (!replaced) {
3804
+ if (output.length > 0) output.push('');
3805
+ output.push(buildHermesModelConfigBlock(modelEntries));
3558
3806
  }
3559
3807
 
3560
- return normalized.join('\n') + '\n';
3808
+ return output.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
3561
3809
  }
3562
3810
 
3563
3811
  function parseEnvFile(text) {
@@ -3620,6 +3868,32 @@ function readHermesYamlConfig(configPath = getHermesConfigPath()) {
3620
3868
  };
3621
3869
  }
3622
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
+
3623
3897
  function getOpencodeConfigPath() {
3624
3898
  const home = os.homedir();
3625
3899
  return process.platform === 'win32'
@@ -3638,11 +3912,20 @@ function getCodexCliPaths() {
3638
3912
  function readClaudeCodeCliConfig() {
3639
3913
  const settingsPath = getClaudeCodeSettingsPath();
3640
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();
3641
3919
  return {
3642
3920
  settingsPath,
3643
- modelId: settings.model || settings.env?.ANTHROPIC_MODEL || process.env.ANTHROPIC_MODEL || getDefaultClaudeModel().id,
3644
- baseUrl: settings.env?.ANTHROPIC_BASE_URL || settings.apiBaseUrl || process.env.ANTHROPIC_BASE_URL || '',
3645
- 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,
3646
3929
  configured: fs.existsSync(settingsPath)
3647
3930
  };
3648
3931
  }
@@ -3697,6 +3980,23 @@ function summarizeCliTestOutput(text) {
3697
3980
  return lines[0].slice(0, 160);
3698
3981
  }
3699
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
+
3700
4000
  function buildReadableAgentError(text, fallback = '') {
3701
4001
  const cleaned = cleanCliTestOutput(text);
3702
4002
  if (!cleaned) return fallback;
@@ -3712,6 +4012,11 @@ function looksLikeCliTestError(text) {
3712
4012
  'all models failed',
3713
4013
  'auth_permanent',
3714
4014
  'auth issue',
4015
+ 'authentication_error',
4016
+ 'failed to authenticate',
4017
+ 'invalid bearer token',
4018
+ 'invalid api key',
4019
+ 'fix external api key',
3715
4020
  'unauthorized',
3716
4021
  'forbidden',
3717
4022
  'missing environment variable',
@@ -3739,17 +4044,28 @@ function testClaudeCodeCliConnection() {
3739
4044
 
3740
4045
  const config = readClaudeCodeCliConfig();
3741
4046
  if (!config.configured) return { name: 'Claude Code CLI', status: 'skipped', detail: '未检测到 ~/.claude/settings.json' };
3742
- 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
+ }
3743
4057
 
3744
4058
  const env = {
3745
4059
  ...process.env,
3746
4060
  PATH: extendPathEnv(null),
3747
- ANTHROPIC_BASE_URL: config.baseUrl,
3748
- ANTHROPIC_AUTH_TOKEN: config.apiKey,
3749
- ANTHROPIC_MODEL: config.modelId,
3750
4061
  NODE_TLS_REJECT_UNAUTHORIZED: '0',
3751
4062
  NODE_NO_WARNINGS: '1'
3752
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;
3753
4069
 
3754
4070
  return runCliTestCandidates('Claude Code CLI', [
3755
4071
  `${shellQuote(cliBinary)} -p --model ${shellQuote(config.modelId)} --output-format text ${shellQuote('请只回复 OK')}`,
@@ -3801,14 +4117,62 @@ function testCodexCliConnection() {
3801
4117
  ], env);
3802
4118
  }
3803
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
+
3804
4167
  async function testAdditionalCliConnections(args = {}, options = {}) {
3805
4168
  if (args['no-app-test'] || args.noAppTest) return;
3806
4169
 
3807
- 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)));
3808
4171
  const results = [];
3809
4172
  if (requested.has('claude')) results.push(testClaudeCodeCliConnection());
3810
4173
  if (requested.has('opencode')) results.push(testOpencodeCliConnection());
3811
4174
  if (requested.has('codex')) results.push(testCodexCliConnection());
4175
+ if (requested.has('hermes')) results.push(testHermesCliConnection());
3812
4176
  if (results.length === 0) return;
3813
4177
 
3814
4178
  console.log(chalk.cyan('\n附加测试: 其他 CLI 连接...'));
@@ -4332,6 +4696,27 @@ function trimClaudeMessagesSuffix(baseUrl) {
4332
4696
  return trimmed;
4333
4697
  }
4334
4698
 
4699
+ function trimOpenAiEndpointSuffix(baseUrl) {
4700
+ const trimmed = baseUrl.trim();
4701
+ const suffixes = [
4702
+ '/v1/responses',
4703
+ '/responses',
4704
+ '/v1/chat/completions',
4705
+ '/chat/completions',
4706
+ '/v1/models',
4707
+ '/models',
4708
+ '/v1'
4709
+ ];
4710
+
4711
+ for (const suffix of suffixes) {
4712
+ if (trimmed.endsWith(suffix)) {
4713
+ return trimmed.slice(0, -suffix.length);
4714
+ }
4715
+ }
4716
+
4717
+ return trimmed;
4718
+ }
4719
+
4335
4720
  async function quickSetup(paths, args = {}) {
4336
4721
  console.log(chalk.cyan.bold('\n🚀 快速配置向导\n'));
4337
4722
 
@@ -4474,47 +4859,15 @@ async function quickSetup(paths, args = {}) {
4474
4859
  }
4475
4860
 
4476
4861
  async function presetClaude(paths, args = {}) {
4477
- console.log(chalk.cyan.bold('\n🚀 Claude 快速配置(自动测速推荐节点)\n'));
4862
+ console.log(chalk.cyan.bold('\n🚀 Claude 快速配置(默认节点直连)\n'));
4478
4863
 
4479
4864
  const apiConfig = API_CONFIG.claude;
4480
4865
  const providerPrefix = (args['provider-prefix'] || args.prefix || apiConfig.providerName).toString().trim() || apiConfig.providerName;
4481
-
4482
- const shouldTest = !(args['no-test'] || args.noTest);
4483
- let selectedEndpoint = ENDPOINTS[0];
4484
-
4485
- if (shouldTest) {
4486
- console.log(chalk.cyan('📡 开始测速 Claude 节点...\n'));
4487
- const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
4488
-
4489
- const sorted = speedResult.ranked || [];
4490
- if (sorted.length > 0) {
4491
- const defaultEp = ENDPOINTS[0];
4492
- const { selectedIndex } = await inquirer.prompt([{
4493
- type: 'list',
4494
- name: 'selectedIndex',
4495
- message: '选择节点:',
4496
- choices: [
4497
- { name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
4498
- new inquirer.Separator(' ---- 或按测速结果选择 ----'),
4499
- ...sorted.map((e, i) => ({
4500
- name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
4501
- value: i
4502
- }))
4503
- ]
4504
- }]);
4505
- selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
4506
- if (speedResult.usedFallback) {
4507
- console.log(chalk.yellow(`\n⚠ 当前使用备用节点\n`));
4508
- }
4509
- } else {
4510
- console.log(chalk.red('\n⚠️ 所有节点(含备用)均不可达'));
4511
- const { proceed } = await inquirer.prompt([{
4512
- type: 'confirm', name: 'proceed',
4513
- message: '仍要写入默认节点配置吗?', default: false
4514
- }]);
4515
- if (!proceed) { console.log(chalk.gray('已取消')); return; }
4516
- }
4517
- }
4866
+ const selectedEndpoint = await resolveEndpointSelection(args, {
4867
+ speedIntro: '📡 开始测速 Claude 节点...\n',
4868
+ proceedMessage: '仍要写入默认节点配置吗?'
4869
+ });
4870
+ if (!selectedEndpoint) return;
4518
4871
 
4519
4872
  const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
4520
4873
  const repairResult = applyConfigRepairsWithSync(config, paths);
@@ -4526,6 +4879,8 @@ async function presetClaude(paths, args = {}) {
4526
4879
 
4527
4880
  const apiKeyEnvFallbacks = [
4528
4881
  'OPENCLAW_CLAUDE_KEY',
4882
+ 'ANTHROPIC_API_KEY',
4883
+ 'ANTHROPIC_AUTH_TOKEN',
4529
4884
  'CLAUDE_API_KEY',
4530
4885
  'OPENCLAW_API_KEY'
4531
4886
  ];
@@ -4657,48 +5012,16 @@ async function presetClaude(paths, args = {}) {
4657
5012
  }
4658
5013
 
4659
5014
  async function presetCodex(paths, args = {}) {
4660
- console.log(chalk.cyan.bold('\n🚀 Codex 快速配置(自动测速推荐节点)\n'));
5015
+ console.log(chalk.cyan.bold('\n🚀 Codex 快速配置(默认节点直连)\n'));
4661
5016
 
4662
5017
  const apiConfig = API_CONFIG.codex;
4663
5018
  const providerPrefix = (args['provider-prefix'] || args.prefix || apiConfig.providerName).toString().trim() || apiConfig.providerName;
4664
5019
  const providerName = (args['provider-name'] || args.provider || providerPrefix).toString().trim() || apiConfig.providerName;
4665
-
4666
- const shouldTest = !(args['no-test'] || args.noTest);
4667
- let selectedEndpoint = ENDPOINTS[0];
4668
-
4669
- if (shouldTest) {
4670
- console.log(chalk.cyan('📡 开始测速 Codex 节点...\n'));
4671
- const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
4672
-
4673
- const sorted = speedResult.ranked || [];
4674
- if (sorted.length > 0) {
4675
- const defaultEp = ENDPOINTS[0];
4676
- const { selectedIndex } = await inquirer.prompt([{
4677
- type: 'list',
4678
- name: 'selectedIndex',
4679
- message: '选择节点:',
4680
- choices: [
4681
- { name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
4682
- new inquirer.Separator(' ---- 或按测速结果选择 ----'),
4683
- ...sorted.map((e, i) => ({
4684
- name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
4685
- value: i
4686
- }))
4687
- ]
4688
- }]);
4689
- selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
4690
- if (speedResult.usedFallback) {
4691
- console.log(chalk.yellow(`\n⚠ 当前使用备用节点\n`));
4692
- }
4693
- } else {
4694
- console.log(chalk.red('\n⚠️ 所有节点(含备用)均不可达'));
4695
- const { proceed } = await inquirer.prompt([{
4696
- type: 'confirm', name: 'proceed',
4697
- message: '仍要写入默认节点配置吗?', default: false
4698
- }]);
4699
- if (!proceed) { console.log(chalk.gray('已取消')); return; }
4700
- }
4701
- }
5020
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5021
+ speedIntro: '📡 开始测速 Codex 节点...\n',
5022
+ proceedMessage: '仍要写入默认节点配置吗?'
5023
+ });
5024
+ if (!selectedEndpoint) return;
4702
5025
 
4703
5026
  const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
4704
5027
  const repairResult = applyConfigRepairsWithSync(config, paths);
@@ -4852,6 +5175,8 @@ async function autoActivate(paths, args = {}) {
4852
5175
  const apiKeyEnvFallbacks = [
4853
5176
  'OPENCLAW_CLAUDE_KEY',
4854
5177
  'OPENCLAW_CODEX_KEY',
5178
+ 'ANTHROPIC_API_KEY',
5179
+ 'ANTHROPIC_AUTH_TOKEN',
4855
5180
  'CLAUDE_API_KEY',
4856
5181
  'OPENAI_API_KEY',
4857
5182
  'OPENCLAW_API_KEY'
@@ -5062,40 +5387,10 @@ async function autoActivate(paths, args = {}) {
5062
5387
  async function activateClaudeCode(paths, args = {}) {
5063
5388
  console.log(chalk.cyan.bold('\n🔧 配置 Claude Code CLI\n'));
5064
5389
  const selectedModel = await promptClaudeModelSelection(args);
5065
-
5066
- // ---- 测速选节点 ----
5067
- const shouldTest = !(args['no-test'] || args.noTest);
5068
- let selectedEndpoint = ENDPOINTS[0];
5069
-
5070
- if (shouldTest) {
5071
- console.log(chalk.cyan('📡 开始测速节点...\n'));
5072
- const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
5073
- const sorted = speedResult.ranked || [];
5074
- if (sorted.length > 0) {
5075
- const defaultEp = ENDPOINTS[0];
5076
- const { selectedIndex } = await inquirer.prompt([{
5077
- type: 'list',
5078
- name: 'selectedIndex',
5079
- message: '选择节点:',
5080
- choices: [
5081
- { name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
5082
- new inquirer.Separator(' ---- 或按测速结果选择 ----'),
5083
- ...sorted.map((e, i) => ({
5084
- name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
5085
- value: i
5086
- }))
5087
- ]
5088
- }]);
5089
- selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
5090
- } else {
5091
- console.log(chalk.red('\n⚠️ 所有节点均不可达'));
5092
- const { proceed } = await inquirer.prompt([{
5093
- type: 'confirm', name: 'proceed',
5094
- message: '仍要使用默认节点配置吗?', default: false
5095
- }]);
5096
- if (!proceed) { console.log(chalk.gray('已取消')); return; }
5097
- }
5098
- }
5390
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5391
+ proceedMessage: '仍要使用默认节点配置吗?'
5392
+ });
5393
+ if (!selectedEndpoint) return;
5099
5394
 
5100
5395
  // ---- API Key ----
5101
5396
  const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
@@ -5103,7 +5398,7 @@ async function activateClaudeCode(paths, args = {}) {
5103
5398
  if (directKey) {
5104
5399
  apiKey = directKey;
5105
5400
  } else {
5106
- 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 || '';
5107
5402
  apiKey = await promptApiKey('请输入 API Key:', envKey);
5108
5403
  }
5109
5404
  if (!apiKey) { console.log(chalk.gray('已取消')); return; }
@@ -5131,7 +5426,7 @@ async function activateClaudeCode(paths, args = {}) {
5131
5426
  console.log(chalk.gray(' API Key: 已设置'));
5132
5427
  console.log(chalk.gray('\n 已写入:'));
5133
5428
  console.log(chalk.gray(' • ~/.claude/settings.json'));
5134
- console.log(chalk.gray(' • 最小字段: apiBaseUrl + env.ANTHROPIC_AUTH_TOKEN'));
5429
+ console.log(chalk.gray(' • 最小字段: apiBaseUrl + env.ANTHROPIC_API_KEY + env.ANTHROPIC_BASE_URL'));
5135
5430
  console.log(chalk.yellow('\n 提示: 如果 Claude Code 已在运行,重新打开一个会话即可读取新配置'));
5136
5431
 
5137
5432
  if (await confirmImmediateTest(args, '是否立即测试 Claude Code CLI 连接?')) {
@@ -5143,40 +5438,10 @@ async function activateClaudeCode(paths, args = {}) {
5143
5438
  async function activateOpencode(paths, args = {}) {
5144
5439
  console.log(chalk.cyan.bold('\n🔧 配置 Opencode\n'));
5145
5440
  const defaultModel = await promptOpencodeDefaultModelSelection(args);
5146
-
5147
- // ---- 测速选节点 ----
5148
- const shouldTest = !(args['no-test'] || args.noTest);
5149
- let selectedEndpoint = ENDPOINTS[0];
5150
-
5151
- if (shouldTest) {
5152
- console.log(chalk.cyan('📡 开始测速节点...\n'));
5153
- const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
5154
- const sorted = speedResult.ranked || [];
5155
- if (sorted.length > 0) {
5156
- const defaultEp = ENDPOINTS[0];
5157
- const { selectedIndex } = await inquirer.prompt([{
5158
- type: 'list',
5159
- name: 'selectedIndex',
5160
- message: '选择节点:',
5161
- choices: [
5162
- { name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
5163
- new inquirer.Separator(' ---- 或按测速结果选择 ----'),
5164
- ...sorted.map((e, i) => ({
5165
- name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
5166
- value: i
5167
- }))
5168
- ]
5169
- }]);
5170
- selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
5171
- } else {
5172
- console.log(chalk.red('\n⚠️ 所有节点均不可达'));
5173
- const { proceed } = await inquirer.prompt([{
5174
- type: 'confirm', name: 'proceed',
5175
- message: '仍要使用默认节点配置吗?', default: false
5176
- }]);
5177
- if (!proceed) { console.log(chalk.gray('已取消')); return; }
5178
- }
5179
- }
5441
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5442
+ proceedMessage: '仍要使用默认节点配置吗?'
5443
+ });
5444
+ if (!selectedEndpoint) return;
5180
5445
 
5181
5446
  // ---- API Key ----
5182
5447
  const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
@@ -5184,7 +5449,7 @@ async function activateOpencode(paths, args = {}) {
5184
5449
  if (directKey) {
5185
5450
  apiKey = directKey;
5186
5451
  } else {
5187
- 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 || '';
5188
5453
  apiKey = await promptApiKey('请输入 API Key:', envKey);
5189
5454
  }
5190
5455
  if (!apiKey) { console.log(chalk.gray('已取消')); return; }
@@ -5240,39 +5505,10 @@ async function activateCodex(paths, args = {}) {
5240
5505
  }
5241
5506
  const modelConfig = CODEX_MODELS.find(m => m.id === modelId) || { id: modelId, name: modelId };
5242
5507
 
5243
- // ---- 测速选节点 ----
5244
- const shouldTest = !(args['no-test'] || args.noTest);
5245
- let selectedEndpoint = ENDPOINTS[0];
5246
-
5247
- if (shouldTest) {
5248
- console.log(chalk.cyan('📡 开始测速节点...\n'));
5249
- const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
5250
- const sorted = speedResult.ranked || [];
5251
- if (sorted.length > 0) {
5252
- const defaultEp = ENDPOINTS[0];
5253
- const { selectedIndex } = await inquirer.prompt([{
5254
- type: 'list',
5255
- name: 'selectedIndex',
5256
- message: '选择节点:',
5257
- choices: [
5258
- { name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
5259
- new inquirer.Separator(' ---- 或按测速结果选择 ----'),
5260
- ...sorted.map((e, i) => ({
5261
- name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
5262
- value: i
5263
- }))
5264
- ]
5265
- }]);
5266
- selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
5267
- } else {
5268
- console.log(chalk.red('\n⚠️ 所有节点均不可达'));
5269
- const { proceed } = await inquirer.prompt([{
5270
- type: 'confirm', name: 'proceed',
5271
- message: '仍要使用默认节点配置吗?', default: false
5272
- }]);
5273
- if (!proceed) { console.log(chalk.gray('已取消')); return; }
5274
- }
5275
- }
5508
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5509
+ proceedMessage: '仍要使用默认节点配置吗?'
5510
+ });
5511
+ if (!selectedEndpoint) return;
5276
5512
 
5277
5513
  // ---- API Key ----
5278
5514
  const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
@@ -5319,47 +5555,22 @@ async function activateCodex(paths, args = {}) {
5319
5555
  // ============ 单独配置 Hermes ============
5320
5556
  async function activateHermes(paths, args = {}) {
5321
5557
  console.log(chalk.cyan.bold('\n🔧 配置 Hermes\n'));
5322
- const selectedModel = await promptClaudeModelSelection(args, '选择 Hermes 默认 Claude 模型:');
5323
-
5324
- const shouldTest = !(args['no-test'] || args.noTest);
5325
- let selectedEndpoint = ENDPOINTS[0];
5326
-
5327
- if (shouldTest) {
5328
- console.log(chalk.cyan('📡 开始测速节点...\n'));
5329
- const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
5330
- const sorted = speedResult.ranked || [];
5331
- if (sorted.length > 0) {
5332
- const defaultEp = ENDPOINTS[0];
5333
- const { selectedIndex } = await inquirer.prompt([{
5334
- type: 'list',
5335
- name: 'selectedIndex',
5336
- message: '选择节点:',
5337
- choices: [
5338
- { name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
5339
- new inquirer.Separator(' ---- 或按测速结果选择 ----'),
5340
- ...sorted.map((e, i) => ({
5341
- name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
5342
- value: i
5343
- }))
5344
- ]
5345
- }]);
5346
- selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
5347
- } else {
5348
- console.log(chalk.red('\n⚠️ 所有节点均不可达'));
5349
- const { proceed } = await inquirer.prompt([{
5350
- type: 'confirm', name: 'proceed',
5351
- message: '仍要使用默认节点配置吗?', default: false
5352
- }]);
5353
- if (!proceed) { console.log(chalk.gray('已取消')); return; }
5354
- }
5355
- }
5558
+ const selection = await promptHermesModelSelection(args, '选择 Hermes 默认模型:');
5559
+ const selectedModel = selection.model;
5560
+ const selectedType = selection.type;
5561
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5562
+ proceedMessage: '仍要使用默认节点配置吗?'
5563
+ });
5564
+ if (!selectedEndpoint) return;
5356
5565
 
5357
5566
  const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
5358
5567
  let apiKey;
5359
5568
  if (directKey) {
5360
5569
  apiKey = directKey;
5361
5570
  } else {
5362
- const envKey = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '';
5571
+ const envKey = selectedType === 'codex'
5572
+ ? (process.env.OPENAI_API_KEY || process.env.OPENAI_AUTH_TOKEN || '')
5573
+ : (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '');
5363
5574
  apiKey = await promptApiKey('请输入 API Key:', envKey);
5364
5575
  }
5365
5576
  if (!apiKey) { console.log(chalk.gray('已取消')); return; }
@@ -5374,15 +5585,17 @@ async function activateHermes(paths, args = {}) {
5374
5585
  if (!continueAnyway) { console.log(chalk.gray('已取消')); return; }
5375
5586
  }
5376
5587
 
5377
- const claudeBaseUrl = buildFullUrl(selectedEndpoint.url, 'claude');
5588
+ const typeLabel = selectedType === 'codex' ? 'GPT' : 'Claude';
5589
+ const hermesBaseUrl = buildFullUrl(selectedEndpoint.url, selectedType === 'codex' ? 'codex' : 'claude');
5378
5590
  const writeSpinner = ora({ text: '正在写入 Hermes 配置...', spinner: 'dots' }).start();
5379
- const hermesPaths = writeHermesConfig(claudeBaseUrl, apiKey, selectedModel.id);
5591
+ const hermesPaths = writeHermesConfig(hermesBaseUrl, apiKey, selectedModel.id, { type: selectedType });
5380
5592
  writeSpinner.succeed('Hermes 配置写入完成');
5381
5593
 
5382
5594
  console.log(chalk.green('\n✅ Hermes 配置完成!'));
5383
5595
  console.log(chalk.cyan(` Base URL: ${hermesPaths.baseUrl}`));
5384
5596
  console.log(chalk.gray(` 模型: ${selectedModel.name} (${selectedModel.id})`));
5385
- console.log(chalk.gray(' Provider: anthropic'));
5597
+ console.log(chalk.gray(` 协议: ${typeLabel}`));
5598
+ console.log(chalk.gray(` Provider: ${hermesPaths.provider}`));
5386
5599
  console.log(chalk.gray(' API Key: 已设置'));
5387
5600
  console.log(chalk.gray('\n 已写入:'));
5388
5601
  console.log(chalk.gray(` • ${hermesPaths.configPath}`));
@@ -5390,8 +5603,12 @@ async function activateHermes(paths, args = {}) {
5390
5603
  if (hermesPaths.wslConfigPath && hermesPaths.wslEnvPath) {
5391
5604
  console.log(chalk.gray(' • 已额外同步到 WSL ~/.hermes'));
5392
5605
  }
5393
- console.log(chalk.yellow('\n 提示: Hermes Desktop 的“测试连接”按钮当前可能仍按 /v1/models 检测;按钮不绿,不代表 runtime 不能正常对话'));
5394
- 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
+ }
5395
5612
  }
5396
5613
 
5397
5614
  // ============ 主程序 ============
@@ -5433,6 +5650,10 @@ async function main() {
5433
5650
  await testConnection(paths, args);
5434
5651
  return;
5435
5652
  }
5653
+ if (args._.includes('speed-test') || args._.includes('speedtest')) {
5654
+ await speedTestNodes();
5655
+ return;
5656
+ }
5436
5657
 
5437
5658
  while (true) {
5438
5659
  // 显示当前状态
@@ -5459,6 +5680,7 @@ async function main() {
5459
5680
  new inquirer.Separator(' -- 工具 --'),
5460
5681
  { name: ' 切换 OpenClaw 模型', value: 'switch_model' },
5461
5682
  { name: ` 权限管理${getToolsProfileTag(paths)}`, value: 'tools_profile' },
5683
+ { name: ' 测速节点', value: 'speed_test_nodes' },
5462
5684
  { name: ' 测试连接', value: 'test_connection' },
5463
5685
  { name: ' 查看配置', value: 'view_config' },
5464
5686
  { name: ' 恢复默认', value: 'restore' },
@@ -5482,6 +5704,9 @@ async function main() {
5482
5704
  case 'test_connection':
5483
5705
  await testConnection(paths, {});
5484
5706
  break;
5707
+ case 'speed_test_nodes':
5708
+ await speedTestNodes();
5709
+ break;
5485
5710
  case 'switch_model':
5486
5711
  await switchModel(paths);
5487
5712
  break;
@@ -6686,6 +6911,8 @@ function testGatewayViaAgent(model, agentId = '') {
6686
6911
  }
6687
6912
  const nodeInfo = findCompatibleNode(nodeMajor);
6688
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;
6689
6916
  delete env.CLAUDE_API_KEY;
6690
6917
  delete env.OPENCLAW_CLAUDE_KEY;
6691
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.103",
3
+ "version": "1.0.108",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "bin/yymaxapi.js",
6
6
  "bin": {