yymaxapi 1.0.102 → 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 +491 -156
  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,6 +1159,72 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKe
1075
1159
  return configPath;
1076
1160
  }
1077
1161
 
1162
+ function writeHermesConfig(baseUrl, apiKey, modelId = getDefaultClaudeModel().id, options = {}) {
1163
+ const dataDir = getHermesDataDir();
1164
+ const configPath = path.join(dataDir, 'config.yaml');
1165
+ const envPath = getHermesEnvPath();
1166
+ const existingConfigPath = getHermesConfigPath();
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';
1172
+
1173
+ let existingConfigRaw = readTextIfExists(existingConfigPath);
1174
+ if (!existingConfigRaw && wslMirror.sourceConfigPath) {
1175
+ existingConfigRaw = readWslTextFile(wslMirror.sourceConfigPath);
1176
+ }
1177
+
1178
+ let existingEnvRaw = readTextIfExists(envPath);
1179
+ if (!existingEnvRaw && wslMirror.envPath) {
1180
+ existingEnvRaw = readWslTextFile(wslMirror.envPath);
1181
+ }
1182
+
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
1198
+ });
1199
+ const envEntries = {};
1200
+ envEntries[resolvedType === 'codex' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY'] = String(apiKey || '').trim();
1201
+ const nextEnvRaw = upsertEnvFile(existingEnvRaw, envEntries);
1202
+
1203
+ if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
1204
+ fs.writeFileSync(configPath, nextConfigRaw, 'utf8');
1205
+ fs.writeFileSync(envPath, nextEnvRaw, 'utf8');
1206
+
1207
+ if (wslMirror.configPath) {
1208
+ try { syncFileToWsl(configPath, wslMirror.configPath); } catch { /* best-effort */ }
1209
+ }
1210
+ if (wslMirror.envPath) {
1211
+ try { syncFileToWsl(envPath, wslMirror.envPath); } catch { /* best-effort */ }
1212
+ }
1213
+
1214
+ return {
1215
+ dataDir,
1216
+ configPath,
1217
+ envPath,
1218
+ wslConfigPath: wslMirror.configPath,
1219
+ wslEnvPath: wslMirror.envPath,
1220
+ modelId,
1221
+ modelType: resolvedType,
1222
+ provider: runtimeProvider,
1223
+ apiMode,
1224
+ baseUrl: normalizedBaseUrl
1225
+ };
1226
+ }
1227
+
1078
1228
  function syncExternalTools(type, baseUrl, apiKey, extra = {}) {
1079
1229
  const synced = [];
1080
1230
  try {
@@ -3400,6 +3550,251 @@ function getClaudeCodeSettingsPath() {
3400
3550
  return path.join(os.homedir(), '.claude', 'settings.json');
3401
3551
  }
3402
3552
 
3553
+ function readTextIfExists(filePath) {
3554
+ if (!filePath || !fs.existsSync(filePath)) return '';
3555
+ try {
3556
+ return fs.readFileSync(filePath, 'utf8');
3557
+ } catch {
3558
+ return '';
3559
+ }
3560
+ }
3561
+
3562
+ function getHermesDataDir() {
3563
+ const envDir = String(process.env.HERMES_DATA_DIR || '').trim();
3564
+ return envDir || path.join(os.homedir(), '.hermes');
3565
+ }
3566
+
3567
+ function getHermesConfigPath() {
3568
+ const dataDir = getHermesDataDir();
3569
+ const candidates = [
3570
+ path.join(dataDir, 'config.yaml'),
3571
+ path.join(dataDir, 'config.yml')
3572
+ ];
3573
+ return candidates.find(filePath => fs.existsSync(filePath)) || candidates[0];
3574
+ }
3575
+
3576
+ function getHermesEnvPath() {
3577
+ return path.join(getHermesDataDir(), '.env');
3578
+ }
3579
+
3580
+ function getHermesWslMirrorInfo() {
3581
+ if (process.platform !== 'win32' || !isWslAvailable()) {
3582
+ return {
3583
+ dataDir: null,
3584
+ sourceConfigPath: null,
3585
+ configPath: null,
3586
+ envPath: null
3587
+ };
3588
+ }
3589
+
3590
+ const wslHome = getWslHome() || '/root';
3591
+ const dataDir = path.posix.join(wslHome, '.hermes');
3592
+ const sourceConfigPath = findExistingWslFile([
3593
+ path.posix.join(dataDir, 'config.yaml'),
3594
+ path.posix.join(dataDir, 'config.yml')
3595
+ ]);
3596
+
3597
+ return {
3598
+ dataDir,
3599
+ sourceConfigPath,
3600
+ configPath: path.posix.join(dataDir, 'config.yaml'),
3601
+ envPath: path.posix.join(dataDir, '.env')
3602
+ };
3603
+ }
3604
+
3605
+ function readWslTextFile(filePath) {
3606
+ if (process.platform !== 'win32' || !filePath) return '';
3607
+ const quoted = shellQuote(filePath);
3608
+ const result = safeExec(`wsl -- bash -lc "cat ${quoted} 2>/dev/null"`, { timeout: 10000 });
3609
+ return result.ok ? (result.output || result.stdout || '') : '';
3610
+ }
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
+
3627
+ function parseSimpleYamlConfig(text) {
3628
+ const config = {};
3629
+ let currentObjectKey = '';
3630
+
3631
+ for (const line of String(text || '').split(/\r?\n/)) {
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 = '';
3646
+ }
3647
+ continue;
3648
+ }
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 = '';
3662
+ }
3663
+
3664
+ return config;
3665
+ }
3666
+
3667
+ function serializeYamlScalar(value) {
3668
+ return JSON.stringify(String(value ?? ''));
3669
+ }
3670
+
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
+ }
3683
+
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)}`);
3688
+ }
3689
+ return lines.join('\n');
3690
+ }
3691
+
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;
3723
+ }
3724
+ output.push(line);
3725
+ }
3726
+
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));
3733
+ }
3734
+
3735
+ return output.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
3736
+ }
3737
+
3738
+ function parseEnvFile(text) {
3739
+ const entries = {};
3740
+ for (const line of String(text || '').split(/\r?\n/)) {
3741
+ const trimmed = line.trim();
3742
+ if (!trimmed || trimmed.startsWith('#')) continue;
3743
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
3744
+ if (!match) continue;
3745
+ let value = match[2].trim();
3746
+ if (
3747
+ (value.startsWith('"') && value.endsWith('"'))
3748
+ || (value.startsWith('\'') && value.endsWith('\''))
3749
+ ) {
3750
+ try {
3751
+ value = JSON.parse(value);
3752
+ } catch {
3753
+ value = value.slice(1, -1);
3754
+ }
3755
+ }
3756
+ entries[match[1]] = value;
3757
+ }
3758
+ return entries;
3759
+ }
3760
+
3761
+ function upsertEnvFile(text, entries) {
3762
+ const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
3763
+ const normalized = lines.length === 1 && lines[0] === '' ? [] : [...lines];
3764
+ const seen = new Set();
3765
+
3766
+ for (let i = 0; i < normalized.length; i += 1) {
3767
+ const match = normalized[i].match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
3768
+ if (!match) continue;
3769
+ const key = match[1];
3770
+ if (!Object.prototype.hasOwnProperty.call(entries, key)) continue;
3771
+ normalized[i] = `${key}=${JSON.stringify(String(entries[key] ?? ''))}`;
3772
+ seen.add(key);
3773
+ }
3774
+
3775
+ for (const key of Object.keys(entries)) {
3776
+ if (!seen.has(key)) {
3777
+ normalized.push(`${key}=${JSON.stringify(String(entries[key] ?? ''))}`);
3778
+ }
3779
+ }
3780
+
3781
+ while (normalized.length > 0 && normalized[normalized.length - 1] === '') {
3782
+ normalized.pop();
3783
+ }
3784
+
3785
+ return normalized.join('\n') + '\n';
3786
+ }
3787
+
3788
+ function readHermesYamlConfig(configPath = getHermesConfigPath()) {
3789
+ const raw = readTextIfExists(configPath);
3790
+ return {
3791
+ configPath,
3792
+ configured: !!raw || fs.existsSync(configPath),
3793
+ config: parseSimpleYamlConfig(raw),
3794
+ raw
3795
+ };
3796
+ }
3797
+
3403
3798
  function getOpencodeConfigPath() {
3404
3799
  const home = os.homedir();
3405
3800
  return process.platform === 'win32'
@@ -4112,6 +4507,27 @@ function trimClaudeMessagesSuffix(baseUrl) {
4112
4507
  return trimmed;
4113
4508
  }
4114
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
+
4115
4531
  async function quickSetup(paths, args = {}) {
4116
4532
  console.log(chalk.cyan.bold('\n🚀 快速配置向导\n'));
4117
4533
 
@@ -5096,183 +5512,94 @@ async function activateCodex(paths, args = {}) {
5096
5512
  }
5097
5513
  }
5098
5514
 
5099
- // ============ yycode 精简模式(零交互一键配置) ============
5100
- async function yycodeQuickSetup(paths) {
5101
- console.log(chalk.cyan.bold('\n yycode 一键配置\n'));
5515
+ // ============ 单独配置 Hermes ============
5516
+ async function activateHermes(paths, args = {}) {
5517
+ console.log(chalk.cyan.bold('\n🔧 配置 Hermes\n'));
5518
+ const selection = await promptHermesModelSelection(args, '选择 Hermes 默认模型:');
5519
+ const selectedModel = selection.model;
5520
+ const selectedType = selection.type;
5102
5521
 
5103
- const claudeApiConfig = API_CONFIG.claude;
5104
- const codexApiConfig = API_CONFIG.codex;
5105
- const claudeProviderName = claudeApiConfig.providerName;
5106
- const codexProviderName = codexApiConfig.providerName;
5522
+ const shouldTest = !(args['no-test'] || args.noTest);
5523
+ let selectedEndpoint = ENDPOINTS[0];
5107
5524
 
5108
- // ---- 探测已有 key ----
5109
- let apiKey = '';
5110
- let keySource = '';
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
+ }
5111
5554
 
5112
- // 1. CLI 参数
5113
- const args = parseArgs(process.argv.slice(2));
5114
5555
  const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
5556
+ let apiKey;
5115
5557
  if (directKey) {
5116
5558
  apiKey = directKey;
5117
- keySource = '命令行参数';
5118
- }
5119
-
5120
- // 2. 环境变量
5121
- if (!apiKey) {
5122
- const envKeys = ['OPENCLAW_CLAUDE_KEY', 'OPENCLAW_CODEX_KEY', 'CLAUDE_API_KEY', 'OPENAI_API_KEY', 'OPENCLAW_API_KEY'];
5123
- for (const k of envKeys) {
5124
- if (process.env[k] && process.env[k].trim()) {
5125
- apiKey = process.env[k].trim();
5126
- keySource = `环境变量 ${k}`;
5127
- break;
5128
- }
5129
- }
5130
- }
5131
-
5132
- // 3. 已有 OpenClaw 配置(云翼 Claude Code 密钥)
5133
- if (!apiKey) {
5134
- try {
5135
- const config = readConfig(paths.openclawConfig);
5136
- if (config && config.models && config.models.providers) {
5137
- // 优先取 claude-yunyi 的 key
5138
- const preferredOrder = [claudeProviderName, codexProviderName];
5139
- for (const name of preferredOrder) {
5140
- const p = config.models.providers[name];
5141
- if (p && p.apiKey && p.apiKey.trim()) {
5142
- apiKey = p.apiKey.trim();
5143
- keySource = `已有配置 (${name})`;
5144
- break;
5145
- }
5146
- }
5147
- // 其他 provider 的 key
5148
- if (!apiKey) {
5149
- for (const [name, p] of Object.entries(config.models.providers)) {
5150
- if (p.apiKey && p.apiKey.trim()) {
5151
- apiKey = p.apiKey.trim();
5152
- keySource = `已有配置 (${name})`;
5153
- break;
5154
- }
5155
- }
5156
- }
5157
- }
5158
- } catch { /* ignore */ }
5159
- }
5160
-
5161
- // 4. 都没有,提示输入
5162
- if (apiKey) {
5163
- const masked = apiKey.length > 8 ? apiKey.slice(0, 5) + '***' + apiKey.slice(-3) : '***';
5164
- console.log(chalk.green(`✓ 已检测到 API Key: ${masked} (来源: ${keySource})`));
5165
5559
  } else {
5166
- apiKey = await promptApiKey('请输入 API Key:', '');
5167
- if (!apiKey) { console.log(chalk.gray('已取消')); return; }
5168
- }
5169
-
5170
- // ---- 静默测速选最快节点 ----
5171
- const speedSpinner = ora({ text: '正在测速选择最快节点...', spinner: 'dots' }).start();
5172
- let selectedEndpoint = ENDPOINTS[0];
5173
- try {
5174
- const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
5175
- if (speedResult.ranked && speedResult.ranked.length > 0) {
5176
- selectedEndpoint = speedResult.ranked[0];
5177
- }
5178
- speedSpinner.succeed(`节点: ${selectedEndpoint.name}`);
5179
- } catch {
5180
- speedSpinner.succeed(`节点: ${selectedEndpoint.name} (默认)`);
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 || '');
5563
+ apiKey = await promptApiKey('请输入 API Key:', envKey);
5181
5564
  }
5565
+ if (!apiKey) { console.log(chalk.gray('已取消')); return; }
5182
5566
 
5183
- // ---- 验证 key ----
5567
+ console.log('');
5184
5568
  const validation = await validateApiKey(selectedEndpoint.url, apiKey);
5185
5569
  if (!validation.valid) {
5186
- console.log(chalk.yellow('⚠ API Key 验证失败,仍将写入配置'));
5570
+ const { continueAnyway } = await inquirer.prompt([{
5571
+ type: 'confirm', name: 'continueAnyway',
5572
+ message: 'API Key 验证失败,是否仍然继续写入配置?', default: false
5573
+ }]);
5574
+ if (!continueAnyway) { console.log(chalk.gray('已取消')); return; }
5187
5575
  }
5188
5576
 
5189
- // ---- 写入两套配置 ----
5190
- const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
5191
-
5192
- // 增量写入: 只添加/更新云翼 provider,保留其他已有配置
5577
+ const typeLabel = selectedType === 'codex' ? 'GPT' : 'Claude';
5578
+ const hermesBaseUrl = buildFullUrl(selectedEndpoint.url, selectedType === 'codex' ? 'codex' : 'claude');
5579
+ const writeSpinner = ora({ text: '正在写入 Hermes 配置...', spinner: 'dots' }).start();
5580
+ const hermesPaths = writeHermesConfig(hermesBaseUrl, apiKey, selectedModel.id, { type: selectedType });
5581
+ writeSpinner.succeed('Hermes 配置写入完成');
5193
5582
 
5194
- // Claude
5195
- const claudeBaseUrl = buildFullUrl(selectedEndpoint.url, 'claude');
5196
- const claudeModelId = 'claude-sonnet-4-6';
5197
- const claudeModel = CLAUDE_MODELS.find(m => m.id === claudeModelId) || { id: claudeModelId, name: 'Claude Sonnet 4.6' };
5198
- const claudeModelKey = `${claudeProviderName}/${claudeModelId}`;
5199
-
5200
- config.models.providers[claudeProviderName] = {
5201
- baseUrl: claudeBaseUrl,
5202
- auth: DEFAULT_AUTH_MODE,
5203
- api: claudeApiConfig.api,
5204
- headers: {},
5205
- authHeader: false,
5206
- apiKey: apiKey.trim(),
5207
- models: [{ id: claudeModel.id, name: claudeModel.name, contextWindow: claudeApiConfig.contextWindow, maxTokens: claudeApiConfig.maxTokens }]
5208
- };
5209
- config.auth.profiles[`${claudeProviderName}:default`] = { provider: claudeProviderName, mode: 'api_key' };
5210
- config.agents.defaults.models[claudeModelKey] = { alias: claudeProviderName };
5211
-
5212
- // Codex 侧
5213
- const codexBaseUrl = buildFullUrl(selectedEndpoint.url, 'codex');
5214
- const codexModelId = CODEX_MODELS[0]?.id || 'gpt-5.4';
5215
- const codexModel = CODEX_MODELS.find(m => m.id === codexModelId) || { id: codexModelId, name: 'GPT 5.4' };
5216
- const codexModelKey = `${codexProviderName}/${codexModelId}`;
5217
-
5218
- config.models.providers[codexProviderName] = {
5219
- baseUrl: codexBaseUrl,
5220
- auth: DEFAULT_AUTH_MODE,
5221
- api: codexApiConfig.api,
5222
- headers: {},
5223
- authHeader: codexApiConfig.api.startsWith('openai'),
5224
- apiKey: apiKey.trim(),
5225
- models: [{ id: codexModel.id, name: codexModel.name, contextWindow: codexApiConfig.contextWindow, maxTokens: codexApiConfig.maxTokens }]
5226
- };
5227
- config.auth.profiles[`${codexProviderName}:default`] = { provider: codexProviderName, mode: 'api_key' };
5228
- config.agents.defaults.models[codexModelKey] = { alias: codexProviderName };
5229
-
5230
- // 默认主力: Codex, 备用: Claude
5231
- config.agents.defaults.model.primary = codexModelKey;
5232
- config.agents.defaults.model.fallbacks = [claudeModelKey];
5233
- const yunyiLayoutResult = applyManagedYunyiOpenClawLayout(config, {
5234
- force: true,
5235
- endpointUrl: selectedEndpoint.url,
5236
- apiKey
5237
- });
5238
-
5239
- // ---- 写入 ----
5240
- const writeSpinner = ora({ text: '正在写入配置...', spinner: 'dots' }).start();
5241
- createTimestampedBackup(paths.openclawConfig, paths.configDir, 'yycode');
5242
- ensureGatewaySettings(config);
5243
- cleanupConflictingEnvVars(config, codexBaseUrl, apiKey);
5244
- writeConfigWithSync(paths, config);
5245
- updateAuthProfilesWithSync(paths, claudeProviderName, apiKey);
5246
- updateAuthProfilesWithSync(paths, codexProviderName, apiKey);
5247
- if (yunyiLayoutResult.applied) {
5248
- syncManagedYunyiAuthProfiles(paths, config);
5583
+ console.log(chalk.green('\n✅ Hermes 配置完成!'));
5584
+ console.log(chalk.cyan(` Base URL: ${hermesPaths.baseUrl}`));
5585
+ console.log(chalk.gray(` 模型: ${selectedModel.name} (${selectedModel.id})`));
5586
+ console.log(chalk.gray(` 协议: ${typeLabel}`));
5587
+ console.log(chalk.gray(` Provider: ${hermesPaths.provider}`));
5588
+ console.log(chalk.gray(' API Key: 已设置'));
5589
+ console.log(chalk.gray('\n 已写入:'));
5590
+ console.log(chalk.gray(` • ${hermesPaths.configPath}`));
5591
+ console.log(chalk.gray(` • ${hermesPaths.envPath}`));
5592
+ if (hermesPaths.wslConfigPath && hermesPaths.wslEnvPath) {
5593
+ console.log(chalk.gray(' • 已额外同步到 WSL ~/.hermes'));
5249
5594
  }
5250
- try { syncExternalTools('claude', claudeBaseUrl, apiKey, { codexBaseUrl, claudeModelId, opencodeDefaultModelKey: getExternalModelKey('codex', codexModelId) }); } catch { /* ignore */ }
5251
- try { syncExternalTools('codex', codexBaseUrl, apiKey, { modelId: codexModelId }); } catch { /* ignore */ }
5252
- writeSpinner.succeed('配置写入完成');
5253
-
5254
- // ---- 结果 ----
5255
- console.log(chalk.green('\n✅ 配置完成!'));
5256
- console.log(chalk.cyan(` Claude Code: ${claudeModel.name}`));
5257
- console.log(chalk.cyan(` Codex CLI: ${codexModel.name}`));
5258
- printYunyiOpenClawSwitchHint(yunyiLayoutResult);
5259
- console.log('');
5595
+ console.log(chalk.yellow('\n 提示: Hermes Desktop 的“测试连接”按钮当前可能仍按 /v1/models 检测;按钮不绿,不代表 runtime 不能正常对话'));
5596
+ console.log(chalk.yellow(' 建议: 重启 Hermes Desktop,或手动执行 hermes server --port 8787 验证 Hermes runtime'));
5260
5597
  }
5261
5598
 
5262
-
5263
5599
  // ============ 主程序 ============
5264
5600
  async function main() {
5265
5601
  console.clear();
5266
5602
 
5267
- // yycode 精简模式:检测到 yycode CLI 时直接走零交互流程
5268
- const isYYCode = path.basename(process.argv[1] || '').replace(/\.js$/, '') === 'yycode';
5269
- if (isYYCode) {
5270
- const paths = getConfigPath();
5271
- backupOriginalConfig(paths.openclawConfig, paths.configDir);
5272
- await yycodeQuickSetup(paths);
5273
- return;
5274
- }
5275
-
5276
5603
  console.log(chalk.cyan.bold('\n🔧 OpenClaw API 配置工具\n'));
5277
5604
 
5278
5605
  const paths = getConfigPath();
@@ -5296,6 +5623,10 @@ async function main() {
5296
5623
  await activateClaudeCode(paths, args);
5297
5624
  return;
5298
5625
  }
5626
+ if (args.preset === 'hermes' || args._.includes('preset-hermes') || args._.includes('hermes-preset')) {
5627
+ await activateHermes(paths, args);
5628
+ return;
5629
+ }
5299
5630
  if (args.preset === 'codex' || args._.includes('preset-codex') || args._.includes('codex-preset')) {
5300
5631
  await autoActivate(paths, { ...args, primary: 'codex' });
5301
5632
  return;
@@ -5324,6 +5655,7 @@ async function main() {
5324
5655
  new inquirer.Separator(' -- 一键配置 --'),
5325
5656
  { name: ' 配置 OpenClaw(Claude + Codex)', value: 'auto_activate' },
5326
5657
  { name: ' 配置 Claude Code', value: 'activate_claude_code' },
5658
+ { name: ' 配置 Hermes', value: 'activate_hermes' },
5327
5659
  { name: ' 配置 Opencode', value: 'activate_opencode' },
5328
5660
  { name: ' 配置 Codex CLI', value: 'activate_codex' },
5329
5661
  new inquirer.Separator(' -- 工具 --'),
@@ -5367,6 +5699,9 @@ async function main() {
5367
5699
  case 'activate_claude_code':
5368
5700
  await activateClaudeCode(paths);
5369
5701
  break;
5702
+ case 'activate_hermes':
5703
+ await activateHermes(paths);
5704
+ break;
5370
5705
  case 'activate_opencode':
5371
5706
  await activateOpencode(paths);
5372
5707
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yymaxapi",
3
- "version": "1.0.102",
3
+ "version": "1.0.104",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "bin/yymaxapi.js",
6
6
  "bin": {