yymaxapi 1.0.23 → 1.0.25

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 +354 -60
  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);
@@ -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())
@@ -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;
@@ -2561,7 +2791,7 @@ async function restartGateway() {
2561
2791
  const wslCli = getWslCliBinary();
2562
2792
  return new Promise((resolve) => {
2563
2793
  const wslCmds = wslCli
2564
- ? [`wsl -- bash -c "${wslCli} gateway restart"`]
2794
+ ? [`wsl -- bash -lc "${wslCli} gateway restart"`]
2565
2795
  : [
2566
2796
  'wsl -- bash -lc "openclaw gateway restart"',
2567
2797
  'wsl -- bash -lc "clawdbot gateway restart"',
@@ -2612,6 +2842,13 @@ async function forceRestartGateway(resolved, nodeInfo, useNode, env, gatewayPort
2612
2842
  }
2613
2843
  }
2614
2844
  }
2845
+ // Windows + WSL: 也清理 WSL 内的 gateway 进程
2846
+ if (isWslAvailable()) {
2847
+ for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
2848
+ safeExec(`wsl -- bash -c "pkill -f '${name}.*gateway' 2>/dev/null || true"`, { timeout: 5000 });
2849
+ }
2850
+ safeExec(`wsl -- bash -c "lsof -ti :${gatewayPort} 2>/dev/null | xargs -r kill -9 2>/dev/null || true"`, { timeout: 5000 });
2851
+ }
2615
2852
  } else {
2616
2853
  // Linux/macOS: pkill gateway 相关进程
2617
2854
  for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
@@ -2651,7 +2888,18 @@ async function forceRestartGateway(resolved, nodeInfo, useNode, env, gatewayPort
2651
2888
  startCmds.push(`bash -lc '${name} gateway'`);
2652
2889
  }
2653
2890
  } else {
2891
+ // Windows: 先尝试原生命令
2654
2892
  startCmds.push('openclaw gateway', 'clawdbot gateway', 'moltbot gateway');
2893
+ // Windows + WSL: 也尝试通过 WSL 启动 gateway
2894
+ if (isWslAvailable()) {
2895
+ const wslCli = getWslCliBinary();
2896
+ if (wslCli) {
2897
+ startCmds.push(`wsl -- bash -lc "${wslCli} gateway"`);
2898
+ }
2899
+ for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
2900
+ startCmds.push(`wsl -- bash -lc "${name} gateway"`);
2901
+ }
2902
+ }
2655
2903
  }
2656
2904
 
2657
2905
  for (const cmd of [...new Set(startCmds)].filter(Boolean)) {
@@ -2693,6 +2941,17 @@ async function restartGatewayNative() {
2693
2941
  'npx moltbot gateway restart'
2694
2942
  ];
2695
2943
 
2944
+ // Windows + WSL: 追加 WSL 命令作为额外回退
2945
+ if (process.platform === 'win32' && isWslAvailable()) {
2946
+ const wslCli = getWslCliBinary();
2947
+ if (wslCli) {
2948
+ commands.push(`wsl -- bash -lc "${wslCli} gateway restart"`);
2949
+ }
2950
+ for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
2951
+ commands.push(`wsl -- bash -lc "${name} gateway restart"`);
2952
+ }
2953
+ }
2954
+
2696
2955
  return new Promise((resolve) => {
2697
2956
  let tried = 0;
2698
2957
 
@@ -3383,17 +3642,45 @@ async function installOrUpdateOpenClaw() {
3383
3642
 
3384
3643
  // ============ 恢复默认配置 ============
3385
3644
  async function restore(paths) {
3386
- 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);
3387
3648
 
3388
- if (!fs.existsSync(backupPath)) {
3649
+ if (backups.length === 0 && !hasLegacy) {
3389
3650
  console.log(chalk.yellow('⚠️ 没有找到备份文件'));
3390
3651
  return;
3391
3652
  }
3392
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
+
3393
3680
  const { confirm } = await inquirer.prompt([{
3394
3681
  type: 'confirm',
3395
3682
  name: 'confirm',
3396
- message: '确定要恢复默认配置吗?当前配置将被覆盖。',
3683
+ message: '确定要恢复此备份吗?当前配置将被覆盖。',
3397
3684
  default: false
3398
3685
  }]);
3399
3686
 
@@ -3402,8 +3689,15 @@ async function restore(paths) {
3402
3689
  return;
3403
3690
  }
3404
3691
 
3405
- if (restoreDefaultConfig(paths.openclawConfig, paths.configDir)) {
3406
- 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✅ 已恢复配置'));
3407
3701
  } else {
3408
3702
  console.log(chalk.red('\n❌ 恢复失败'));
3409
3703
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yymaxapi",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "bin/yymaxapi.js",
6
6
  "bin": {