yymaxapi 1.0.24 → 1.0.26

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 +332 -67
  2. package/package.json +1 -1
package/bin/yymaxapi.js CHANGED
@@ -235,8 +235,10 @@ const CLAUDE_MODELS = PRESETS.models.claude;
235
235
  const CODEX_MODELS = PRESETS.models.codex;
236
236
  const API_CONFIG = PRESETS.apiConfig;
237
237
 
238
- // 备份文件名
238
+ // 备份文件名(兼容旧版单文件备份)
239
239
  const BACKUP_FILENAME = 'openclaw-default.json.bak';
240
+ const BACKUP_DIR_NAME = 'backups';
241
+ const MAX_BACKUPS = 10;
240
242
  const EXTRA_BIN_DIRS = [
241
243
  path.join(os.homedir(), '.npm-global', 'bin'),
242
244
  path.join(os.homedir(), '.local', 'bin'),
@@ -286,18 +288,31 @@ async function multiSampleTest(url, samples = SPEED_SAMPLES) {
286
288
  const latencyScore = Math.max(0, 100 - avg / 10);
287
289
  const stabilityScore = Math.max(0, 100 - stddev * 2);
288
290
  const reachScore = (okPings.length / samples) * 100;
289
- const score = Math.round(
291
+ let score = Math.round(
290
292
  latencyScore * SCORE_WEIGHTS.latency +
291
293
  stabilityScore * SCORE_WEIGHTS.stability +
292
294
  reachScore * SCORE_WEIGHTS.reachability
293
295
  );
294
- return { success: true, latency: Math.round(avg), min: Math.round(min), stddev: Math.round(stddev), samples: okPings.length, total: samples, score };
296
+
297
+ // HTTP 健康检查:TCP 通了再确认服务真的在响应
298
+ let healthy = false;
299
+ try {
300
+ const healthUrl = `${url.replace(/\/+$/, '')}/health`;
301
+ const res = await httpGetJson(healthUrl, {}, 5000);
302
+ healthy = res.status >= 200 && res.status < 500;
303
+ } catch { /* 健康检查失败不影响评分,但会降分 */ }
304
+ if (!healthy) {
305
+ score = Math.max(0, score - 15);
306
+ }
307
+
308
+ return { success: true, latency: Math.round(avg), min: Math.round(min), stddev: Math.round(stddev), samples: okPings.length, total: samples, score, healthy };
295
309
  }
296
310
 
297
311
  function formatSpeedResult(r) {
298
312
  if (r.success) {
299
313
  const bar = r.score >= 70 ? chalk.green('■') : r.score >= 40 ? chalk.yellow('■') : chalk.red('■');
300
- return ` ${bar} ${chalk.gray(r.name)} ${chalk.green(r.latency + 'ms')} ${chalk.gray(`(±${r.stddev}ms)`)} ${chalk.cyan(`评分:${r.score}`)}`;
314
+ const healthTag = r.healthy === false ? chalk.red(' [服务异常]') : '';
315
+ return ` ${bar} ${chalk.gray(r.name)} ${chalk.green(r.latency + 'ms')} ${chalk.gray(`(±${r.stddev}ms)`)} ${chalk.cyan(`评分:${r.score}`)}${healthTag}`;
301
316
  }
302
317
  return ` ${chalk.red('□')} ${chalk.gray(r.name)} ${chalk.red(r.error)}`;
303
318
  }
@@ -354,54 +369,68 @@ function httpGetJson(url, headers = {}, timeout = 10000) {
354
369
 
355
370
  async function validateApiKey(nodeUrl, apiKey) {
356
371
  const verifyUrl = `${nodeUrl.replace(/\/+$/, '')}/user/api/v1/me`;
372
+ const maxRetries = 3;
357
373
  const spinner = ora({ text: '正在验证 API Key...', spinner: 'dots' }).start();
358
- try {
359
- const res = await httpGetJson(verifyUrl, { Authorization: `Bearer ${apiKey}` });
360
- if (res.status === 200 && res.data) {
361
- spinner.succeed('API Key 验证成功');
362
- const d = res.data;
363
- // 基本信息
364
- if (d.service_type) console.log(chalk.gray(` 服务类型: ${d.service_type}`));
365
- if (d.billing_mode) console.log(chalk.gray(` 计费模式: ${d.billing_mode}`));
366
- if (d.status && d.status !== 'active') console.log(chalk.yellow(` 状态: ${d.status}`));
367
- // 配额信息
368
- if (d.total_quota !== undefined || d.remaining_quota !== undefined) {
369
- const total = d.total_quota || 0;
370
- const remaining = d.remaining_quota !== undefined ? d.remaining_quota : total;
371
- const used = total - remaining;
372
- const pct = total > 0 ? Math.round((used / total) * 100) : 0;
373
- const barLen = 20;
374
- const filled = Math.round(barLen * pct / 100);
375
- const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(barLen - filled));
376
- console.log(chalk.gray(` 总配额: ${total}`) + (d.max_requests ? chalk.gray(` | 最大请求: ${d.max_requests}`) : ''));
377
- console.log(chalk.gray(` 已用/剩余: ${used} / ${remaining}`));
378
- console.log(` [${bar}] ${pct}%`);
379
- }
380
- // 每日配额
381
- if (d.daily_quota !== undefined || d.daily_remaining !== undefined) {
382
- const dTotal = d.daily_quota || 0;
383
- const dRemain = d.daily_remaining !== undefined ? d.daily_remaining : dTotal;
384
- const dUsed = d.daily_used !== undefined ? d.daily_used : (dTotal - dRemain);
385
- console.log(chalk.gray(` 今日配额: ${dTotal} | 已用: ${dUsed} | 剩余: ${dRemain}`));
386
- }
387
- // 有效期
388
- if (d.activated_at) console.log(chalk.gray(` 激活时间: ${new Date(d.activated_at).toLocaleDateString()}`));
389
- if (d.expires_at) {
390
- const exp = new Date(d.expires_at);
391
- const daysLeft = Math.ceil((exp - Date.now()) / 86400000);
392
- const expColor = daysLeft <= 7 ? chalk.red : daysLeft <= 30 ? chalk.yellow : chalk.gray;
393
- console.log(expColor(` 有效期至: ${exp.toLocaleDateString()} (${daysLeft > 0 ? `剩余 ${daysLeft} 天` : '已过期'})`));
394
- }
395
- return { valid: true, data: d };
396
- } else {
374
+
375
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
376
+ try {
377
+ const res = await httpGetJson(verifyUrl, { Authorization: `Bearer ${apiKey}` });
378
+ if (res.status === 200 && res.data) {
379
+ spinner.succeed('API Key 验证成功');
380
+ const d = res.data;
381
+ // 基本信息
382
+ if (d.service_type) console.log(chalk.gray(` 服务类型: ${d.service_type}`));
383
+ if (d.billing_mode) console.log(chalk.gray(` 计费模式: ${d.billing_mode}`));
384
+ if (d.status && d.status !== 'active') console.log(chalk.yellow(` ⚠ 状态: ${d.status}`));
385
+ // 配额信息
386
+ if (d.total_quota !== undefined || d.remaining_quota !== undefined) {
387
+ const total = d.total_quota || 0;
388
+ const remaining = d.remaining_quota !== undefined ? d.remaining_quota : total;
389
+ const used = total - remaining;
390
+ const pct = total > 0 ? Math.round((used / total) * 100) : 0;
391
+ const barLen = 20;
392
+ const filled = Math.round(barLen * pct / 100);
393
+ const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(barLen - filled));
394
+ console.log(chalk.gray(` 总配额: ${total}`) + (d.max_requests ? chalk.gray(` | 最大请求: ${d.max_requests}`) : ''));
395
+ console.log(chalk.gray(` 已用/剩余: ${used} / ${remaining}`));
396
+ console.log(` [${bar}] ${pct}%`);
397
+ }
398
+ // 每日配额
399
+ if (d.daily_quota !== undefined || d.daily_remaining !== undefined) {
400
+ const dTotal = d.daily_quota || 0;
401
+ const dRemain = d.daily_remaining !== undefined ? d.daily_remaining : dTotal;
402
+ const dUsed = d.daily_used !== undefined ? d.daily_used : (dTotal - dRemain);
403
+ console.log(chalk.gray(` 今日配额: ${dTotal} | 已用: ${dUsed} | 剩余: ${dRemain}`));
404
+ }
405
+ // 有效期
406
+ if (d.activated_at) console.log(chalk.gray(` 激活时间: ${new Date(d.activated_at).toLocaleDateString()}`));
407
+ if (d.expires_at) {
408
+ const exp = new Date(d.expires_at);
409
+ const daysLeft = Math.ceil((exp - Date.now()) / 86400000);
410
+ const expColor = daysLeft <= 7 ? chalk.red : daysLeft <= 30 ? chalk.yellow : chalk.gray;
411
+ console.log(expColor(` 有效期至: ${exp.toLocaleDateString()} (${daysLeft > 0 ? `剩余 ${daysLeft} 天` : '已过期'})`));
412
+ }
413
+ return { valid: true, data: d };
414
+ } else {
415
+ spinner.fail('API Key 验证失败');
416
+ console.log(chalk.red(` HTTP ${res.status}`));
417
+ return { valid: false, status: res.status };
418
+ }
419
+ } catch (err) {
420
+ const isNetworkError = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN', '请求超时'].some(
421
+ k => (err.message || '').includes(k) || (err.code || '') === k
422
+ );
423
+ if (isNetworkError && attempt < maxRetries) {
424
+ const delay = attempt * 2;
425
+ spinner.text = `网络错误,${delay}s 后重试 (${attempt}/${maxRetries})...`;
426
+ await new Promise(r => setTimeout(r, delay * 1000));
427
+ spinner.text = '正在验证 API Key...';
428
+ continue;
429
+ }
397
430
  spinner.fail('API Key 验证失败');
398
- console.log(chalk.red(` HTTP ${res.status}`));
399
- return { valid: false, status: res.status };
431
+ console.log(chalk.gray(` ${err.message}`));
432
+ return { valid: false, error: err.message };
400
433
  }
401
- } catch (err) {
402
- spinner.fail('API Key 验证失败');
403
- console.log(chalk.gray(` ${err.message}`));
404
- return { valid: false, error: err.message };
405
434
  }
406
435
  }
407
436
 
@@ -573,6 +602,92 @@ function writeConfig(configPath, config) {
573
602
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
574
603
  }
575
604
 
605
+ // ============ 多工具配置同步 ============
606
+
607
+ function writeClaudeCodeSettings(baseUrl, apiKey) {
608
+ // ~/.claude/settings.json
609
+ const claudeDir = path.join(os.homedir(), '.claude');
610
+ const settingsPath = path.join(claudeDir, 'settings.json');
611
+ try {
612
+ let settings = {};
613
+ if (fs.existsSync(settingsPath)) {
614
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch { settings = {}; }
615
+ }
616
+ settings.apiBaseUrl = baseUrl.replace(/\/+$/, '');
617
+ if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
618
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
619
+ } catch { /* 非关键,静默失败 */ }
620
+
621
+ // ~/.claude.json — 跳过 onboarding
622
+ const claudeJsonPath = path.join(os.homedir(), '.claude.json');
623
+ try {
624
+ let claudeJson = {};
625
+ if (fs.existsSync(claudeJsonPath)) {
626
+ try { claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')); } catch { claudeJson = {}; }
627
+ }
628
+ if (!claudeJson.hasCompletedOnboarding) {
629
+ claudeJson.hasCompletedOnboarding = true;
630
+ fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2), 'utf8');
631
+ }
632
+ } catch { /* 非关键,静默失败 */ }
633
+ }
634
+
635
+ function writeCodexConfig(baseUrl, apiKey) {
636
+ const codexDir = path.join(os.homedir(), '.codex');
637
+ if (!fs.existsSync(codexDir)) fs.mkdirSync(codexDir, { recursive: true });
638
+
639
+ // ~/.codex/config.toml — 用 section marker 管理
640
+ const configPath = path.join(codexDir, 'config.toml');
641
+ const marker = '# >>> maxapi codex >>>';
642
+ const markerEnd = '# <<< maxapi codex <<<';
643
+ try {
644
+ let existing = '';
645
+ if (fs.existsSync(configPath)) {
646
+ existing = fs.readFileSync(configPath, 'utf8');
647
+ // 移除旧的 maxapi section
648
+ const re = new RegExp(`${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${markerEnd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n?`, 'g');
649
+ existing = existing.replace(re, '').trim();
650
+ }
651
+ const section = [
652
+ marker,
653
+ `model = "o4-mini"`,
654
+ `provider = "openai"`,
655
+ ``,
656
+ `[providers.openai]`,
657
+ `api_key = "${apiKey}"`,
658
+ `base_url = "${baseUrl.replace(/\/+$/, '')}"`,
659
+ markerEnd
660
+ ].join('\n');
661
+ const content = existing ? `${existing}\n\n${section}\n` : `${section}\n`;
662
+ fs.writeFileSync(configPath, content, 'utf8');
663
+ } catch { /* 非关键,静默失败 */ }
664
+
665
+ // ~/.codex/auth.json
666
+ const authPath = path.join(codexDir, 'auth.json');
667
+ try {
668
+ let auth = {};
669
+ if (fs.existsSync(authPath)) {
670
+ try { auth = JSON.parse(fs.readFileSync(authPath, 'utf8')); } catch { auth = {}; }
671
+ }
672
+ auth.OPENAI_API_KEY = apiKey;
673
+ fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
674
+ } catch { /* 非关键,静默失败 */ }
675
+ }
676
+
677
+ function syncExternalTools(type, baseUrl, apiKey) {
678
+ const synced = [];
679
+ try {
680
+ if (type === 'claude') {
681
+ writeClaudeCodeSettings(baseUrl, apiKey);
682
+ synced.push('Claude Code settings');
683
+ } else if (type === 'codex') {
684
+ writeCodexConfig(baseUrl, apiKey);
685
+ synced.push('Codex CLI config');
686
+ }
687
+ } catch { /* ignore */ }
688
+ return synced;
689
+ }
690
+
576
691
  function syncClawdbotConfigs(paths, config) {
577
692
  if (!paths.syncTargets || paths.syncTargets.length === 0) return;
578
693
  for (const target of paths.syncTargets) {
@@ -1443,12 +1558,59 @@ async function tryAutoStartGateway(port, allowAutoDaemon) {
1443
1558
 
1444
1559
  // ============ 备份/恢复 ============
1445
1560
  function backupOriginalConfig(configPath, configDir) {
1446
- const backupPath = path.join(configDir, BACKUP_FILENAME);
1447
- if (!fs.existsSync(backupPath) && fs.existsSync(configPath)) {
1448
- fs.copyFileSync(configPath, backupPath);
1449
- return true;
1561
+ // 兼容旧版:首次运行仍创建 .bak
1562
+ const legacyBackup = path.join(configDir, BACKUP_FILENAME);
1563
+ if (!fs.existsSync(legacyBackup) && fs.existsSync(configPath)) {
1564
+ fs.copyFileSync(configPath, legacyBackup);
1450
1565
  }
1451
- return false;
1566
+ return false; // 不再在首次运行时显示提示,由 createTimestampedBackup 管理
1567
+ }
1568
+
1569
+ function createTimestampedBackup(configPath, configDir, label = '') {
1570
+ if (!fs.existsSync(configPath)) return null;
1571
+ const backupDir = path.join(configDir, BACKUP_DIR_NAME);
1572
+ const indexPath = path.join(backupDir, 'index.json');
1573
+ if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
1574
+
1575
+ let index = [];
1576
+ if (fs.existsSync(indexPath)) {
1577
+ try { index = JSON.parse(fs.readFileSync(indexPath, 'utf8')); } catch { index = []; }
1578
+ }
1579
+
1580
+ const id = Date.now().toString(36);
1581
+ const timestamp = new Date().toISOString();
1582
+ const backupFile = `${id}.json`;
1583
+ fs.copyFileSync(configPath, path.join(backupDir, backupFile));
1584
+
1585
+ const stat = fs.statSync(configPath);
1586
+ index.push({ id, timestamp, file: backupFile, label: label || '', size: stat.size });
1587
+
1588
+ // 保留最近 MAX_BACKUPS 个
1589
+ while (index.length > MAX_BACKUPS) {
1590
+ const old = index.shift();
1591
+ const oldPath = path.join(backupDir, old.file);
1592
+ try { if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath); } catch { }
1593
+ }
1594
+
1595
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf8');
1596
+ return id;
1597
+ }
1598
+
1599
+ function listBackups(configDir) {
1600
+ const indexPath = path.join(configDir, BACKUP_DIR_NAME, 'index.json');
1601
+ if (!fs.existsSync(indexPath)) return [];
1602
+ try { return JSON.parse(fs.readFileSync(indexPath, 'utf8')); } catch { return []; }
1603
+ }
1604
+
1605
+ function restoreFromBackup(configPath, configDir, backupId) {
1606
+ const backupDir = path.join(configDir, BACKUP_DIR_NAME);
1607
+ const index = listBackups(configDir);
1608
+ const entry = index.find(e => e.id === backupId);
1609
+ if (!entry) return false;
1610
+ const backupFile = path.join(backupDir, entry.file);
1611
+ if (!fs.existsSync(backupFile)) return false;
1612
+ fs.copyFileSync(backupFile, configPath);
1613
+ return true;
1452
1614
  }
1453
1615
 
1454
1616
  function restoreDefaultConfig(configPath, configDir) {
@@ -1680,6 +1842,7 @@ async function quickSetup(paths, args = {}) {
1680
1842
  }
1681
1843
 
1682
1844
  const ws = ora({ text: '正在写入配置...', spinner: 'dots' }).start();
1845
+ createTimestampedBackup(paths.openclawConfig, paths.configDir, 'quick-setup');
1683
1846
  ensureGatewaySettings(config);
1684
1847
  writeConfigWithSync(paths, config);
1685
1848
  updateAuthProfiles(paths.authProfiles, providerName, apiKey);
@@ -1710,21 +1873,21 @@ async function presetClaude(paths, args = {}) {
1710
1873
 
1711
1874
  const sorted = speedResult.ranked || [];
1712
1875
  if (sorted.length > 0) {
1876
+ const defaultEp = ENDPOINTS[0];
1713
1877
  const { selectedIndex } = await inquirer.prompt([{
1714
1878
  type: 'list',
1715
1879
  name: 'selectedIndex',
1716
1880
  message: '选择节点:',
1717
1881
  choices: [
1718
- { name: `* 使用推荐节点 (${sorted[0].name}, ${sorted[0].latency}ms, 评分:${sorted[0].score})`, value: -1 },
1719
- new inquirer.Separator(' ---- 或手动选择 ----'),
1882
+ { name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
1883
+ new inquirer.Separator(' ---- 或按测速结果选择 ----'),
1720
1884
  ...sorted.map((e, i) => ({
1721
1885
  name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
1722
1886
  value: i
1723
1887
  }))
1724
1888
  ]
1725
1889
  }]);
1726
- const primaryIndex = selectedIndex === -1 ? 0 : selectedIndex;
1727
- selectedEndpoint = sorted[primaryIndex];
1890
+ selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
1728
1891
  if (speedResult.usedFallback) {
1729
1892
  console.log(chalk.yellow(`\n⚠ 当前使用备用节点\n`));
1730
1893
  }
@@ -1863,9 +2026,11 @@ async function presetClaude(paths, args = {}) {
1863
2026
  }
1864
2027
 
1865
2028
  const writeSpinner = ora({ text: '正在写入配置...', spinner: 'dots' }).start();
2029
+ createTimestampedBackup(paths.openclawConfig, paths.configDir, 'claude');
1866
2030
  ensureGatewaySettings(config);
1867
2031
  writeConfigWithSync(paths, config);
1868
2032
  updateAuthProfiles(paths.authProfiles, providerName, apiKey);
2033
+ const extSynced = syncExternalTools('claude', baseUrl, apiKey);
1869
2034
  writeSpinner.succeed('配置写入完成');
1870
2035
 
1871
2036
  console.log(chalk.green('\n✅ Claude 节点配置完成!'));
@@ -1873,6 +2038,7 @@ async function presetClaude(paths, args = {}) {
1873
2038
  console.log(chalk.cyan(` ${providerName}${tag}: ${buildFullUrl(selectedEndpoint.url, 'claude')}`));
1874
2039
  console.log(chalk.gray(` 模型: ${modelName}`));
1875
2040
  console.log(chalk.gray(' API Key: 已设置'));
2041
+ if (extSynced.length > 0) console.log(chalk.gray(` 同步: ${extSynced.join(', ')}`));
1876
2042
 
1877
2043
  const shouldTestGateway = args.test !== undefined
1878
2044
  ? !['false', '0', 'no'].includes(String(args.test).toLowerCase())
@@ -1904,21 +2070,21 @@ async function presetCodex(paths, args = {}) {
1904
2070
 
1905
2071
  const sorted = speedResult.ranked || [];
1906
2072
  if (sorted.length > 0) {
2073
+ const defaultEp = ENDPOINTS[0];
1907
2074
  const { selectedIndex } = await inquirer.prompt([{
1908
2075
  type: 'list',
1909
2076
  name: 'selectedIndex',
1910
2077
  message: '选择节点:',
1911
2078
  choices: [
1912
- { name: `* 使用推荐节点 (${sorted[0].name}, ${sorted[0].latency}ms, 评分:${sorted[0].score})`, value: -1 },
1913
- new inquirer.Separator(' ---- 或手动选择 ----'),
2079
+ { name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
2080
+ new inquirer.Separator(' ---- 或按测速结果选择 ----'),
1914
2081
  ...sorted.map((e, i) => ({
1915
2082
  name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
1916
2083
  value: i
1917
2084
  }))
1918
2085
  ]
1919
2086
  }]);
1920
- const primaryIndex = selectedIndex === -1 ? 0 : selectedIndex;
1921
- selectedEndpoint = sorted[primaryIndex];
2087
+ selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
1922
2088
  if (speedResult.usedFallback) {
1923
2089
  console.log(chalk.yellow(`\n⚠ 当前使用备用节点\n`));
1924
2090
  }
@@ -2053,9 +2219,11 @@ async function presetCodex(paths, args = {}) {
2053
2219
  }
2054
2220
 
2055
2221
  const writeSpinner2 = ora({ text: '正在写入配置...', spinner: 'dots' }).start();
2222
+ createTimestampedBackup(paths.openclawConfig, paths.configDir, 'codex');
2056
2223
  ensureGatewaySettings(config);
2057
2224
  writeConfigWithSync(paths, config);
2058
2225
  updateAuthProfiles(paths.authProfiles, providerName, apiKey);
2226
+ const extSynced2 = syncExternalTools('codex', baseUrl, apiKey);
2059
2227
  writeSpinner2.succeed('配置写入完成');
2060
2228
 
2061
2229
  console.log(chalk.green('\n✅ Codex 节点配置完成!'));
@@ -2063,6 +2231,7 @@ async function presetCodex(paths, args = {}) {
2063
2231
  console.log(chalk.cyan(` ${providerName}${tag}: ${baseUrl}`));
2064
2232
  console.log(chalk.gray(` 模型: ${modelName}`));
2065
2233
  console.log(chalk.gray(' API Key: 已设置'));
2234
+ if (extSynced2.length > 0) console.log(chalk.gray(` 同步: ${extSynced2.join(', ')}`));
2066
2235
 
2067
2236
  const shouldTestGateway = args.test !== undefined
2068
2237
  ? !['false', '0', 'no'].includes(String(args.test).toLowerCase())
@@ -2078,6 +2247,63 @@ async function presetCodex(paths, args = {}) {
2078
2247
  }
2079
2248
  }
2080
2249
 
2250
+ // ============ 一键激活(自动识别服务类型) ============
2251
+ async function autoActivate(paths) {
2252
+ console.log(chalk.cyan.bold('\n🚀 一键激活(自动识别服务类型)\n'));
2253
+
2254
+ const apiKey = await promptApiKey('请输入 API Key:', '');
2255
+ if (!apiKey) { console.log(chalk.gray('已取消')); return; }
2256
+
2257
+ // 用第一个节点验证,获取 service_type
2258
+ const nodeUrl = ENDPOINTS[0] ? ENDPOINTS[0].url : '';
2259
+ if (!nodeUrl) { console.log(chalk.red('没有可用节点')); return; }
2260
+
2261
+ console.log('');
2262
+ const validation = await validateApiKey(nodeUrl, apiKey);
2263
+
2264
+ if (!validation.valid) {
2265
+ const { continueAnyway } = await inquirer.prompt([{
2266
+ type: 'confirm', name: 'continueAnyway',
2267
+ message: 'API Key 验证失败,是否手动选择服务类型继续?', default: false
2268
+ }]);
2269
+ if (!continueAnyway) { console.log(chalk.gray('已取消')); return; }
2270
+ }
2271
+
2272
+ const serviceType = (validation.data && validation.data.service_type || '').toLowerCase();
2273
+ let targetType = '';
2274
+
2275
+ if (serviceType.includes('claude') || serviceType.includes('anthropic')) {
2276
+ targetType = 'claude';
2277
+ console.log(chalk.cyan(`\n识别为 Claude 服务,进入 Claude 配置流程...\n`));
2278
+ } else if (serviceType.includes('codex') || serviceType.includes('gpt') || serviceType.includes('openai')) {
2279
+ targetType = 'codex';
2280
+ console.log(chalk.cyan(`\n识别为 Codex 服务,进入 Codex 配置流程...\n`));
2281
+ }
2282
+
2283
+ if (!targetType) {
2284
+ // 无法自动识别,让用户选
2285
+ const hasClaude = CLAUDE_MODELS && CLAUDE_MODELS.length > 0;
2286
+ const hasCodex = CODEX_MODELS && CODEX_MODELS.length > 0;
2287
+ const typeChoices = [];
2288
+ if (hasClaude) typeChoices.push({ name: 'Claude', value: 'claude' });
2289
+ if (hasCodex) typeChoices.push({ name: 'Codex (GPT)', value: 'codex' });
2290
+ if (typeChoices.length === 0) { targetType = 'claude'; }
2291
+ else if (typeChoices.length === 1) { targetType = typeChoices[0].value; }
2292
+ else {
2293
+ const { picked } = await inquirer.prompt([{
2294
+ type: 'list', name: 'picked', message: '无法自动识别服务类型,请选择:', choices: typeChoices
2295
+ }]);
2296
+ targetType = picked;
2297
+ }
2298
+ }
2299
+
2300
+ if (targetType === 'claude') {
2301
+ await presetClaude(paths, { 'api-key': apiKey });
2302
+ } else {
2303
+ await presetCodex(paths, { 'api-key': apiKey });
2304
+ }
2305
+ }
2306
+
2081
2307
  // ============ 主程序 ============
2082
2308
  async function main() {
2083
2309
  console.clear();
@@ -2126,6 +2352,7 @@ async function main() {
2126
2352
  loop: false,
2127
2353
  choices: [
2128
2354
  new inquirer.Separator(' -- 配置模型 --'),
2355
+ { name: ' 一键激活(自动识别)', value: 'auto_activate' },
2129
2356
  { name: ' 激活 Claude', value: 'activate_claude' },
2130
2357
  { name: ' 激活 Codex (GPT)', value: 'activate_codex' },
2131
2358
  new inquirer.Separator(' -- 工具 --'),
@@ -2148,6 +2375,9 @@ async function main() {
2148
2375
 
2149
2376
  try {
2150
2377
  switch (action) {
2378
+ case 'auto_activate':
2379
+ await autoActivate(paths);
2380
+ break;
2151
2381
  case 'activate_claude':
2152
2382
  await presetClaude(paths, {});
2153
2383
  break;
@@ -3412,17 +3642,45 @@ async function installOrUpdateOpenClaw() {
3412
3642
 
3413
3643
  // ============ 恢复默认配置 ============
3414
3644
  async function restore(paths) {
3415
- const backupPath = path.join(paths.configDir, BACKUP_FILENAME);
3645
+ const backups = listBackups(paths.configDir);
3646
+ const legacyBackup = path.join(paths.configDir, BACKUP_FILENAME);
3647
+ const hasLegacy = fs.existsSync(legacyBackup);
3416
3648
 
3417
- if (!fs.existsSync(backupPath)) {
3649
+ if (backups.length === 0 && !hasLegacy) {
3418
3650
  console.log(chalk.yellow('⚠️ 没有找到备份文件'));
3419
3651
  return;
3420
3652
  }
3421
3653
 
3654
+ const choices = [];
3655
+ for (const b of [...backups].reverse()) {
3656
+ const date = new Date(b.timestamp);
3657
+ const dateStr = date.toLocaleString();
3658
+ const sizeKB = b.size ? `${Math.round(b.size / 1024)}KB` : '';
3659
+ const label = b.label ? ` [${b.label}]` : '';
3660
+ choices.push({ name: `${dateStr}${label} ${chalk.gray(sizeKB)}`, value: b.id });
3661
+ }
3662
+ if (hasLegacy) {
3663
+ choices.push({ name: `初始备份 (首次运行时创建)`, value: '__legacy__' });
3664
+ }
3665
+ choices.push(new inquirer.Separator(''));
3666
+ choices.push({ name: '取消', value: '__cancel__' });
3667
+
3668
+ const { selected } = await inquirer.prompt([{
3669
+ type: 'list',
3670
+ name: 'selected',
3671
+ message: '选择要恢复的备份:',
3672
+ choices
3673
+ }]);
3674
+
3675
+ if (selected === '__cancel__') {
3676
+ console.log(chalk.gray('已取消'));
3677
+ return;
3678
+ }
3679
+
3422
3680
  const { confirm } = await inquirer.prompt([{
3423
3681
  type: 'confirm',
3424
3682
  name: 'confirm',
3425
- message: '确定要恢复默认配置吗?当前配置将被覆盖。',
3683
+ message: '确定要恢复此备份吗?当前配置将被覆盖。',
3426
3684
  default: false
3427
3685
  }]);
3428
3686
 
@@ -3431,8 +3689,15 @@ async function restore(paths) {
3431
3689
  return;
3432
3690
  }
3433
3691
 
3434
- if (restoreDefaultConfig(paths.openclawConfig, paths.configDir)) {
3435
- console.log(chalk.green('\n✅ 已恢复默认配置'));
3692
+ let ok = false;
3693
+ if (selected === '__legacy__') {
3694
+ ok = restoreDefaultConfig(paths.openclawConfig, paths.configDir);
3695
+ } else {
3696
+ ok = restoreFromBackup(paths.openclawConfig, paths.configDir, selected);
3697
+ }
3698
+
3699
+ if (ok) {
3700
+ console.log(chalk.green('\n✅ 已恢复配置'));
3436
3701
  } else {
3437
3702
  console.log(chalk.red('\n❌ 恢复失败'));
3438
3703
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yymaxapi",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "bin/yymaxapi.js",
6
6
  "bin": {