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.
- package/bin/yymaxapi.js +354 -60
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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.
|
|
399
|
-
return { valid: false,
|
|
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
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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 -
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
3406
|
-
|
|
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
|
}
|