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.
- package/bin/yymaxapi.js +332 -67
- 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);
|
|
@@ -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: `*
|
|
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
|
-
|
|
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: `*
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
3435
|
-
|
|
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
|
}
|