yymaxapi 1.0.103 → 1.0.104

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.
Files changed (2) hide show
  1. package/bin/yymaxapi.js +245 -43
  2. package/package.json +1 -1
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();
@@ -1075,12 +1159,16 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKe
1075
1159
  return configPath;
1076
1160
  }
1077
1161
 
1078
- function writeHermesConfig(baseUrl, apiKey, modelId = getDefaultClaudeModel().id) {
1162
+ function writeHermesConfig(baseUrl, apiKey, modelId = getDefaultClaudeModel().id, options = {}) {
1079
1163
  const dataDir = getHermesDataDir();
1080
1164
  const configPath = path.join(dataDir, 'config.yaml');
1081
1165
  const envPath = getHermesEnvPath();
1082
1166
  const existingConfigPath = getHermesConfigPath();
1083
1167
  const wslMirror = getHermesWslMirrorInfo();
1168
+ const modelType = options.type || options.provider || '';
1169
+ const resolvedType = resolveHermesModelType(modelId, modelType);
1170
+ const runtimeProvider = resolvedType === 'codex' ? 'custom' : 'anthropic';
1171
+ const apiMode = resolvedType === 'codex' ? 'codex_responses' : 'anthropic_messages';
1084
1172
 
1085
1173
  let existingConfigRaw = readTextIfExists(existingConfigPath);
1086
1174
  if (!existingConfigRaw && wslMirror.sourceConfigPath) {
@@ -1092,15 +1180,25 @@ function writeHermesConfig(baseUrl, apiKey, modelId = getDefaultClaudeModel().id
1092
1180
  existingEnvRaw = readWslTextFile(wslMirror.envPath);
1093
1181
  }
1094
1182
 
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()
1183
+ const normalizedBaseUrl = (
1184
+ resolvedType === 'codex'
1185
+ ? trimOpenAiEndpointSuffix(String(baseUrl || '').trim())
1186
+ : trimClaudeMessagesSuffix(String(baseUrl || '').trim())
1187
+ ).replace(/\/+$/, '');
1188
+ const existingConfig = parseSimpleYamlConfig(existingConfigRaw);
1189
+ const existingModelConfig = existingConfig.model && !Array.isArray(existingConfig.model) && typeof existingConfig.model === 'object'
1190
+ ? existingConfig.model
1191
+ : {};
1192
+ const nextConfigRaw = upsertHermesModelConfig(existingConfigRaw, {
1193
+ ...existingModelConfig,
1194
+ default: modelId,
1195
+ provider: runtimeProvider,
1196
+ base_url: normalizedBaseUrl,
1197
+ api_mode: apiMode
1103
1198
  });
1199
+ const envEntries = {};
1200
+ envEntries[resolvedType === 'codex' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY'] = String(apiKey || '').trim();
1201
+ const nextEnvRaw = upsertEnvFile(existingEnvRaw, envEntries);
1104
1202
 
1105
1203
  if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
1106
1204
  fs.writeFileSync(configPath, nextConfigRaw, 'utf8');
@@ -1120,6 +1218,9 @@ function writeHermesConfig(baseUrl, apiKey, modelId = getDefaultClaudeModel().id
1120
1218
  wslConfigPath: wslMirror.configPath,
1121
1219
  wslEnvPath: wslMirror.envPath,
1122
1220
  modelId,
1221
+ modelType: resolvedType,
1222
+ provider: runtimeProvider,
1223
+ apiMode,
1123
1224
  baseUrl: normalizedBaseUrl
1124
1225
  };
1125
1226
  }
@@ -3508,24 +3609,58 @@ function readWslTextFile(filePath) {
3508
3609
  return result.ok ? (result.output || result.stdout || '') : '';
3509
3610
  }
3510
3611
 
3612
+ function parseYamlScalar(value) {
3613
+ let normalized = String(value ?? '').trim();
3614
+ if (
3615
+ (normalized.startsWith('"') && normalized.endsWith('"'))
3616
+ || (normalized.startsWith('\'') && normalized.endsWith('\''))
3617
+ ) {
3618
+ try {
3619
+ normalized = JSON.parse(normalized);
3620
+ } catch {
3621
+ normalized = normalized.slice(1, -1);
3622
+ }
3623
+ }
3624
+ return normalized;
3625
+ }
3626
+
3511
3627
  function parseSimpleYamlConfig(text) {
3512
3628
  const config = {};
3629
+ let currentObjectKey = '';
3630
+
3513
3631
  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);
3632
+ if (!line.trim() || line.trim().startsWith('#')) continue;
3633
+
3634
+ const topLevelMatch = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*?)\s*$/);
3635
+ if (topLevelMatch && !line.startsWith(' ')) {
3636
+ const key = topLevelMatch[1];
3637
+ const rawValue = topLevelMatch[2];
3638
+ if (rawValue === '') {
3639
+ if (!config[key] || typeof config[key] !== 'object' || Array.isArray(config[key])) {
3640
+ config[key] = {};
3641
+ }
3642
+ currentObjectKey = key;
3643
+ } else {
3644
+ config[key] = parseYamlScalar(rawValue);
3645
+ currentObjectKey = '';
3525
3646
  }
3647
+ continue;
3526
3648
  }
3527
- config[match[1]] = value;
3649
+
3650
+ if (currentObjectKey) {
3651
+ const nestedMatch = line.match(/^\s{2}([A-Za-z0-9_-]+)\s*:\s*(.*?)\s*$/);
3652
+ if (nestedMatch) {
3653
+ if (!config[currentObjectKey] || typeof config[currentObjectKey] !== 'object' || Array.isArray(config[currentObjectKey])) {
3654
+ config[currentObjectKey] = {};
3655
+ }
3656
+ config[currentObjectKey][nestedMatch[1]] = parseYamlScalar(nestedMatch[2]);
3657
+ continue;
3658
+ }
3659
+ }
3660
+
3661
+ currentObjectKey = '';
3528
3662
  }
3663
+
3529
3664
  return config;
3530
3665
  }
3531
3666
 
@@ -3533,31 +3668,71 @@ function serializeYamlScalar(value) {
3533
3668
  return JSON.stringify(String(value ?? ''));
3534
3669
  }
3535
3670
 
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();
3671
+ function buildHermesModelConfigBlock(entries) {
3672
+ const ordered = {};
3673
+ for (const key of ['default', 'provider', 'base_url', 'api_mode']) {
3674
+ if (Object.prototype.hasOwnProperty.call(entries, key)) {
3675
+ ordered[key] = entries[key];
3676
+ }
3677
+ }
3678
+ for (const [key, value] of Object.entries(entries)) {
3679
+ if (!Object.prototype.hasOwnProperty.call(ordered, key)) {
3680
+ ordered[key] = value;
3681
+ }
3682
+ }
3540
3683
 
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);
3684
+ const lines = ['model:'];
3685
+ for (const [key, value] of Object.entries(ordered)) {
3686
+ if (value === undefined || value === null || value === '') continue;
3687
+ lines.push(` ${key}: ${serializeYamlScalar(value)}`);
3548
3688
  }
3689
+ return lines.join('\n');
3690
+ }
3549
3691
 
3550
- for (const key of Object.keys(entries)) {
3551
- if (!seen.has(key)) {
3552
- normalized.push(`${key}: ${serializeYamlScalar(entries[key])}`);
3692
+ function upsertHermesModelConfig(text, modelEntries) {
3693
+ let normalized = String(text || '').replace(/\r\n/g, '\n');
3694
+ normalized = normalized.replace(/^(provider|base_url|api_mode):\s*.*\n?/gm, '');
3695
+
3696
+ const lines = normalized.split('\n');
3697
+ const output = [];
3698
+ let replaced = false;
3699
+
3700
+ for (let i = 0; i < lines.length; i += 1) {
3701
+ const line = lines[i];
3702
+ if (/^model:\s*(.*)$/.test(line)) {
3703
+ replaced = true;
3704
+ if (output.length > 0 && output[output.length - 1] !== '') {
3705
+ output.push('');
3706
+ }
3707
+ output.push(buildHermesModelConfigBlock(modelEntries));
3708
+
3709
+ i += 1;
3710
+ while (i < lines.length) {
3711
+ const nextLine = lines[i];
3712
+ if (/^\s{2,}/.test(nextLine)) {
3713
+ i += 1;
3714
+ continue;
3715
+ }
3716
+ if (nextLine === '') {
3717
+ break;
3718
+ }
3719
+ i -= 1;
3720
+ break;
3721
+ }
3722
+ continue;
3553
3723
  }
3724
+ output.push(line);
3554
3725
  }
3555
3726
 
3556
- while (normalized.length > 0 && normalized[normalized.length - 1] === '') {
3557
- normalized.pop();
3727
+ while (output.length > 0 && output[output.length - 1] === '') {
3728
+ output.pop();
3729
+ }
3730
+ if (!replaced) {
3731
+ if (output.length > 0) output.push('');
3732
+ output.push(buildHermesModelConfigBlock(modelEntries));
3558
3733
  }
3559
3734
 
3560
- return normalized.join('\n') + '\n';
3735
+ return output.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
3561
3736
  }
3562
3737
 
3563
3738
  function parseEnvFile(text) {
@@ -4332,6 +4507,27 @@ function trimClaudeMessagesSuffix(baseUrl) {
4332
4507
  return trimmed;
4333
4508
  }
4334
4509
 
4510
+ function trimOpenAiEndpointSuffix(baseUrl) {
4511
+ const trimmed = baseUrl.trim();
4512
+ const suffixes = [
4513
+ '/v1/responses',
4514
+ '/responses',
4515
+ '/v1/chat/completions',
4516
+ '/chat/completions',
4517
+ '/v1/models',
4518
+ '/models',
4519
+ '/v1'
4520
+ ];
4521
+
4522
+ for (const suffix of suffixes) {
4523
+ if (trimmed.endsWith(suffix)) {
4524
+ return trimmed.slice(0, -suffix.length);
4525
+ }
4526
+ }
4527
+
4528
+ return trimmed;
4529
+ }
4530
+
4335
4531
  async function quickSetup(paths, args = {}) {
4336
4532
  console.log(chalk.cyan.bold('\n🚀 快速配置向导\n'));
4337
4533
 
@@ -5319,7 +5515,9 @@ async function activateCodex(paths, args = {}) {
5319
5515
  // ============ 单独配置 Hermes ============
5320
5516
  async function activateHermes(paths, args = {}) {
5321
5517
  console.log(chalk.cyan.bold('\n🔧 配置 Hermes\n'));
5322
- const selectedModel = await promptClaudeModelSelection(args, '选择 Hermes 默认 Claude 模型:');
5518
+ const selection = await promptHermesModelSelection(args, '选择 Hermes 默认模型:');
5519
+ const selectedModel = selection.model;
5520
+ const selectedType = selection.type;
5323
5521
 
5324
5522
  const shouldTest = !(args['no-test'] || args.noTest);
5325
5523
  let selectedEndpoint = ENDPOINTS[0];
@@ -5359,7 +5557,9 @@ async function activateHermes(paths, args = {}) {
5359
5557
  if (directKey) {
5360
5558
  apiKey = directKey;
5361
5559
  } else {
5362
- const envKey = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '';
5560
+ const envKey = selectedType === 'codex'
5561
+ ? (process.env.OPENAI_API_KEY || process.env.OPENAI_AUTH_TOKEN || '')
5562
+ : (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '');
5363
5563
  apiKey = await promptApiKey('请输入 API Key:', envKey);
5364
5564
  }
5365
5565
  if (!apiKey) { console.log(chalk.gray('已取消')); return; }
@@ -5374,15 +5574,17 @@ async function activateHermes(paths, args = {}) {
5374
5574
  if (!continueAnyway) { console.log(chalk.gray('已取消')); return; }
5375
5575
  }
5376
5576
 
5377
- const claudeBaseUrl = buildFullUrl(selectedEndpoint.url, 'claude');
5577
+ const typeLabel = selectedType === 'codex' ? 'GPT' : 'Claude';
5578
+ const hermesBaseUrl = buildFullUrl(selectedEndpoint.url, selectedType === 'codex' ? 'codex' : 'claude');
5378
5579
  const writeSpinner = ora({ text: '正在写入 Hermes 配置...', spinner: 'dots' }).start();
5379
- const hermesPaths = writeHermesConfig(claudeBaseUrl, apiKey, selectedModel.id);
5580
+ const hermesPaths = writeHermesConfig(hermesBaseUrl, apiKey, selectedModel.id, { type: selectedType });
5380
5581
  writeSpinner.succeed('Hermes 配置写入完成');
5381
5582
 
5382
5583
  console.log(chalk.green('\n✅ Hermes 配置完成!'));
5383
5584
  console.log(chalk.cyan(` Base URL: ${hermesPaths.baseUrl}`));
5384
5585
  console.log(chalk.gray(` 模型: ${selectedModel.name} (${selectedModel.id})`));
5385
- console.log(chalk.gray(' Provider: anthropic'));
5586
+ console.log(chalk.gray(` 协议: ${typeLabel}`));
5587
+ console.log(chalk.gray(` Provider: ${hermesPaths.provider}`));
5386
5588
  console.log(chalk.gray(' API Key: 已设置'));
5387
5589
  console.log(chalk.gray('\n 已写入:'));
5388
5590
  console.log(chalk.gray(` • ${hermesPaths.configPath}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yymaxapi",
3
- "version": "1.0.103",
3
+ "version": "1.0.104",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "bin/yymaxapi.js",
6
6
  "bin": {