yymaxapi 1.0.21 → 1.0.23

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 CHANGED
@@ -507,15 +507,15 @@ function getConfigPath() {
507
507
  // 将 WSL 配置复制到 Windows 侧,保持同步
508
508
  const winDest = path.join(openclawStateDir, path.basename(wp));
509
509
  try {
510
- const content = execFileSync('wsl', ['bash', '-c', `cat '${wp}'`], { encoding: 'utf8', timeout: 10000 });
510
+ const content = execFileSync('wsl', ['bash', '-c', `cat '${wp}'`], { encoding: 'utf8', timeout: 10000, stdio: 'pipe' });
511
511
  if (!fs.existsSync(openclawStateDir)) fs.mkdirSync(openclawStateDir, { recursive: true });
512
512
  fs.writeFileSync(winDest, content, 'utf8');
513
513
  if (!candidates.includes(winDest)) candidates.unshift(winDest);
514
- } catch {}
514
+ } catch { }
515
515
  break;
516
516
  }
517
517
  }
518
- } catch {}
518
+ } catch { }
519
519
  }
520
520
 
521
521
  const defaultConfig = preferMoltbot
@@ -618,7 +618,7 @@ function isWslAvailable() {
618
618
  function getWslHome() {
619
619
  if (_wslHomeCache !== undefined) return _wslHomeCache;
620
620
  try {
621
- _wslHomeCache = execFileSync('wsl', ['bash', '-c', 'echo $HOME'], { encoding: 'utf8', timeout: 5000 }).trim() || null;
621
+ _wslHomeCache = execFileSync('wsl', ['bash', '-c', 'echo $HOME'], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' }).trim() || null;
622
622
  } catch { _wslHomeCache = null; }
623
623
  return _wslHomeCache;
624
624
  }
@@ -630,12 +630,12 @@ function getWslCliBinary() {
630
630
  if (_wslCliBinaryCache !== undefined) return _wslCliBinaryCache;
631
631
  for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
632
632
  try {
633
- const result = execFileSync('wsl', ['bash', '-c', `which ${name} 2>/dev/null`], { encoding: 'utf8', timeout: 5000 }).trim();
633
+ const result = execFileSync('wsl', ['bash', '-lc', `which ${name} 2>/dev/null`], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' }).trim();
634
634
  if (result && result.startsWith('/')) {
635
635
  _wslCliBinaryCache = result;
636
636
  return _wslCliBinaryCache;
637
637
  }
638
- } catch {}
638
+ } catch { }
639
639
  }
640
640
  _wslCliBinaryCache = null;
641
641
  return null;
@@ -663,7 +663,7 @@ function detectGatewayEnv(port = 18789) {
663
663
  }
664
664
  }
665
665
  }
666
- } catch {}
666
+ } catch { }
667
667
  // Fallback: Windows 没有原生 OpenClaw CLI,但 WSL 里有 → 判定为 WSL
668
668
  const nativeCli = resolveCliBinary();
669
669
  if (!nativeCli) {
@@ -705,7 +705,7 @@ function syncConfigToWsl(windowsConfigPath) {
705
705
  if (!match) return;
706
706
  const wslSrc = `/mnt/${match[1].toLowerCase()}/${match[2]}`;
707
707
  const wslDest = `${wslHome}/.openclaw/openclaw.json`;
708
- execFileSync('wsl', ['bash', '-c', `mkdir -p "${wslHome}/.openclaw" && cp "${wslSrc}" "${wslDest}"`], { timeout: 10000, stdio: 'ignore' });
708
+ execFileSync('wsl', ['bash', '-c', `mkdir -p "${wslHome}/.openclaw" && cp "${wslSrc}" "${wslDest}"`], { timeout: 10000, stdio: 'pipe' });
709
709
  } catch { /* best-effort */ }
710
710
  }
711
711
 
@@ -880,7 +880,7 @@ function readAuthStore(authProfilesPath) {
880
880
  for (const [k, v] of Object.entries(raw)) {
881
881
  if (v && typeof v === 'object' && v.type) store.profiles[k] = v;
882
882
  }
883
- } catch {}
883
+ } catch { }
884
884
  return store;
885
885
  }
886
886
 
@@ -1037,7 +1037,7 @@ function resolveCliBinary() {
1037
1037
  if (fs.existsSync(override) && fs.statSync(override).isFile()) {
1038
1038
  return override;
1039
1039
  }
1040
- } catch {}
1040
+ } catch { }
1041
1041
  }
1042
1042
 
1043
1043
  // Validate that a found binary is a real gateway CLI, not yymaxapi itself
@@ -1078,7 +1078,7 @@ function resolveCliBinary() {
1078
1078
  if (fs.existsSync(full) && fs.statSync(full).isFile() && isRealCli(full)) {
1079
1079
  return full;
1080
1080
  }
1081
- } catch {}
1081
+ } catch { }
1082
1082
  }
1083
1083
  }
1084
1084
  }
@@ -1104,7 +1104,7 @@ function resolveCliBinary() {
1104
1104
  if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
1105
1105
  return candidate;
1106
1106
  }
1107
- } catch {}
1107
+ } catch { }
1108
1108
  }
1109
1109
 
1110
1110
  // Fallback: use login shell to find the binary (loads .zshrc/.bashrc PATH)
@@ -1136,7 +1136,7 @@ function resolveCliBinary() {
1136
1136
  const full = path.join(npmBin, name);
1137
1137
  try {
1138
1138
  if (fs.existsSync(full) && fs.statSync(full).isFile() && isRealCli(full)) return full;
1139
- } catch {}
1139
+ } catch { }
1140
1140
  }
1141
1141
  }
1142
1142
 
@@ -1155,14 +1155,14 @@ function resolveCliBinary() {
1155
1155
  extraSearchDirs.push(path.join(nvmVersionsDir, entry, 'bin'));
1156
1156
  }
1157
1157
  }
1158
- } catch {}
1158
+ } catch { }
1159
1159
 
1160
1160
  for (const dir of extraSearchDirs) {
1161
1161
  for (const name of candidates) {
1162
1162
  const full = path.join(dir, name);
1163
1163
  try {
1164
1164
  if (fs.existsSync(full) && fs.statSync(full).isFile() && isRealCli(full)) return full;
1165
- } catch {}
1165
+ } catch { }
1166
1166
  }
1167
1167
  }
1168
1168
 
@@ -1204,7 +1204,7 @@ function findCompatibleNode(minMajor = 22) {
1204
1204
  for (const entry of entries) {
1205
1205
  candidates.push(path.join(nvmVersionsDir, entry, 'bin', 'node'));
1206
1206
  }
1207
- } catch {}
1207
+ } catch { }
1208
1208
  }
1209
1209
 
1210
1210
  const seen = new Set();
@@ -1220,7 +1220,7 @@ function findCompatibleNode(minMajor = 22) {
1220
1220
  if (major && major >= minMajor) {
1221
1221
  return { path: candidate, version: version.trim(), major };
1222
1222
  }
1223
- } catch {}
1223
+ } catch { }
1224
1224
  }
1225
1225
 
1226
1226
  return null;
@@ -1350,7 +1350,7 @@ function installMacGatewayDaemon() {
1350
1350
  const domain = `gui/${uid}`;
1351
1351
  const service = `${domain}/${label}`;
1352
1352
 
1353
- try { execFileSync('launchctl', ['bootout', service], { stdio: 'ignore' }); } catch {}
1353
+ try { execFileSync('launchctl', ['bootout', service], { stdio: 'ignore' }); } catch { }
1354
1354
  execFileSync('launchctl', ['bootstrap', domain, plistPath], { stdio: 'ignore' });
1355
1355
  execFileSync('launchctl', ['kickstart', '-k', service], { stdio: 'ignore' });
1356
1356
 
@@ -1550,14 +1550,14 @@ async function quickSetup(paths, args = {}) {
1550
1550
  const { relayType } = initialType
1551
1551
  ? { relayType: initialType }
1552
1552
  : await inquirer.prompt([{
1553
- type: 'list',
1554
- name: 'relayType',
1555
- message: '选择类型:',
1556
- choices: [
1557
- { name: 'Claude', value: 'claude' },
1558
- { name: 'Codex / GPT', value: 'codex' }
1559
- ]
1560
- }]);
1553
+ type: 'list',
1554
+ name: 'relayType',
1555
+ message: '选择类型:',
1556
+ choices: [
1557
+ { name: 'Claude', value: 'claude' },
1558
+ { name: 'Codex / GPT', value: 'codex' }
1559
+ ]
1560
+ }]);
1561
1561
 
1562
1562
  const type = relayType;
1563
1563
  const typeLabel = type === 'claude' ? 'Claude' : 'Codex';
@@ -1877,11 +1877,11 @@ async function presetClaude(paths, args = {}) {
1877
1877
  const shouldTestGateway = args.test !== undefined
1878
1878
  ? !['false', '0', 'no'].includes(String(args.test).toLowerCase())
1879
1879
  : await inquirer.prompt([{
1880
- type: 'confirm',
1881
- name: 'testGateway',
1882
- message: '是否立即通过 OpenClaw Gateway 测试?',
1883
- default: true
1884
- }]).then(r => r.testGateway);
1880
+ type: 'confirm',
1881
+ name: 'testGateway',
1882
+ message: '是否立即通过 OpenClaw Gateway 测试?',
1883
+ default: true
1884
+ }]).then(r => r.testGateway);
1885
1885
 
1886
1886
  if (shouldTestGateway) {
1887
1887
  await testConnection(paths, args);
@@ -2067,11 +2067,11 @@ async function presetCodex(paths, args = {}) {
2067
2067
  const shouldTestGateway = args.test !== undefined
2068
2068
  ? !['false', '0', 'no'].includes(String(args.test).toLowerCase())
2069
2069
  : await inquirer.prompt([{
2070
- type: 'confirm',
2071
- name: 'testGateway',
2072
- message: '是否立即通过 OpenClaw Gateway 测试?',
2073
- default: true
2074
- }]).then(r => r.testGateway);
2070
+ type: 'confirm',
2071
+ name: 'testGateway',
2072
+ message: '是否立即通过 OpenClaw Gateway 测试?',
2073
+ default: true
2074
+ }]).then(r => r.testGateway);
2075
2075
 
2076
2076
  if (shouldTestGateway) {
2077
2077
  await testConnection(paths, args);
@@ -2405,7 +2405,7 @@ async function testConnection(paths, args = {}) {
2405
2405
 
2406
2406
  // 步骤1: 先重启 Gateway 使配置生效
2407
2407
  console.log(chalk.cyan('步骤 1/2: 重启 Gateway 使配置生效...'));
2408
- await restartGateway();
2408
+ const restartOk = await restartGateway();
2409
2409
 
2410
2410
  // 等待 Gateway 启动
2411
2411
  const gwSpinner = ora({ text: '等待 Gateway 启动...', spinner: 'dots' }).start();
@@ -2428,6 +2428,12 @@ async function testConnection(paths, args = {}) {
2428
2428
 
2429
2429
  gwSpinner.succeed('Gateway 已启动');
2430
2430
 
2431
+ if (!restartOk) {
2432
+ console.log(chalk.yellow('⚠️ Gateway 未能通过常规方式重启,当前使用的可能是之前的 Gateway 进程'));
2433
+ console.log(chalk.yellow(' 新配置可能未生效。如 bot 不回复,请手动重启 Gateway:'));
2434
+ console.log(chalk.gray(' openclaw gateway restart 或 clawdbot gateway restart'));
2435
+ }
2436
+
2431
2437
  // 步骤2: 通过 Gateway 端点测试(优先使用 CLI agent)
2432
2438
  console.log(chalk.cyan(`\n步骤 2/2: 测试 Gateway API 端点...`));
2433
2439
 
@@ -2557,10 +2563,10 @@ async function restartGateway() {
2557
2563
  const wslCmds = wslCli
2558
2564
  ? [`wsl -- bash -c "${wslCli} gateway restart"`]
2559
2565
  : [
2560
- 'wsl -- bash -lc "openclaw gateway restart"',
2561
- 'wsl -- bash -lc "clawdbot gateway restart"',
2562
- 'wsl -- bash -lc "moltbot gateway restart"',
2563
- ];
2566
+ 'wsl -- bash -lc "openclaw gateway restart"',
2567
+ 'wsl -- bash -lc "clawdbot gateway restart"',
2568
+ 'wsl -- bash -lc "moltbot gateway restart"',
2569
+ ];
2564
2570
  let tried = 0;
2565
2571
  const tryNext = () => {
2566
2572
  if (tried >= wslCmds.length) {
@@ -2569,11 +2575,15 @@ async function restartGateway() {
2569
2575
  return;
2570
2576
  }
2571
2577
  const cmd = wslCmds[tried++];
2572
- exec(cmd, { timeout: 30000 }, (error) => {
2573
- if (error) { tryNext(); }
2574
- else {
2578
+ exec(cmd, { timeout: 30000 }, (error, stdout, stderr) => {
2579
+ const errStr = (stderr || '').toLowerCase();
2580
+ const wslGarbled = errStr.includes('wsl:') && errStr.includes('localhost') && errStr.includes('nat');
2581
+
2582
+ if (error || errStr.includes('/usr/bin/env:') || wslGarbled) {
2583
+ tryNext();
2584
+ } else {
2575
2585
  console.log(chalk.green('Gateway 已重启 (WSL)'));
2576
- resolve();
2586
+ resolve(true);
2577
2587
  }
2578
2588
  });
2579
2589
  };
@@ -2584,6 +2594,85 @@ async function restartGateway() {
2584
2594
  return restartGatewayNative();
2585
2595
  }
2586
2596
 
2597
+ // 强制重启 Gateway:杀掉旧进程,再用已找到的 CLI 路径启动新 Gateway
2598
+ async function forceRestartGateway(resolved, nodeInfo, useNode, env, gatewayPort = 18789) {
2599
+ console.log(chalk.yellow('\n🔄 尝试强制重启 Gateway(杀旧进程 → 启动新进程)...'));
2600
+
2601
+ // 1. 杀掉旧 Gateway 进程
2602
+ try {
2603
+ if (process.platform === 'win32') {
2604
+ // Windows: 通过 taskkill 杀掉占用端口的 node 进程
2605
+ const findPid = safeExec(`netstat -ano | findstr ":${gatewayPort}"`, { timeout: 5000 });
2606
+ if (findPid.ok && findPid.output) {
2607
+ const lines = findPid.output.split('\n').filter(l => l.includes('LISTENING'));
2608
+ for (const line of lines) {
2609
+ const pid = line.trim().split(/\s+/).pop();
2610
+ if (pid && /^\d+$/.test(pid)) {
2611
+ safeExec(`taskkill /F /PID ${pid}`, { timeout: 5000 });
2612
+ }
2613
+ }
2614
+ }
2615
+ } else {
2616
+ // Linux/macOS: pkill gateway 相关进程
2617
+ for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
2618
+ safeExec(`pkill -f '${name}.*gateway' 2>/dev/null || true`);
2619
+ }
2620
+ // 备用:通过端口找进程并杀掉
2621
+ const lsof = safeExec(`lsof -ti :${gatewayPort} 2>/dev/null`);
2622
+ if (lsof.ok && lsof.output) {
2623
+ for (const pid of lsof.output.trim().split('\n').filter(Boolean)) {
2624
+ if (/^\d+$/.test(pid.trim())) {
2625
+ safeExec(`kill -9 ${pid.trim()} 2>/dev/null || true`);
2626
+ }
2627
+ }
2628
+ }
2629
+ }
2630
+ console.log(chalk.gray(' 已尝试清理旧 Gateway 进程'));
2631
+ } catch { /* ignore */ }
2632
+
2633
+ // 2. 等待端口释放
2634
+ await new Promise(resolve => setTimeout(resolve, 2000));
2635
+
2636
+ // 3. 用已找到的 CLI 路径启动新 Gateway
2637
+ const startCmds = [];
2638
+
2639
+ // 优先用已解析的完整路径
2640
+ if (resolved) {
2641
+ if (useNode && nodeInfo) {
2642
+ startCmds.push(`"${nodeInfo.path}" "${resolved}" gateway`);
2643
+ } else {
2644
+ startCmds.push(`"${resolved}" gateway`);
2645
+ }
2646
+ }
2647
+
2648
+ // Fallback: login shell 方式(加载 nvm 等 PATH)
2649
+ if (process.platform !== 'win32') {
2650
+ for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
2651
+ startCmds.push(`bash -lc '${name} gateway'`);
2652
+ }
2653
+ } else {
2654
+ startCmds.push('openclaw gateway', 'clawdbot gateway', 'moltbot gateway');
2655
+ }
2656
+
2657
+ for (const cmd of [...new Set(startCmds)].filter(Boolean)) {
2658
+ console.log(chalk.gray(` 尝试启动: ${cmd}`));
2659
+ if (spawnDetached(cmd, env)) {
2660
+ // 等待新 Gateway 启动
2661
+ if (await waitForGateway(gatewayPort, '127.0.0.1', 12000)) {
2662
+ console.log(chalk.green('✅ Gateway 已强制重启成功'));
2663
+ console.log(chalk.gray(' 现在可以在 Web/Telegram/Discord 等渠道测试对话了'));
2664
+ return true;
2665
+ }
2666
+ }
2667
+ }
2668
+
2669
+ console.log(chalk.red('❌ 强制重启也失败了'));
2670
+ console.log(chalk.gray(' 请手动重启 Gateway:'));
2671
+ console.log(chalk.gray(' 1. 关闭旧进程: pkill -f "gateway" 或 taskkill'));
2672
+ console.log(chalk.gray(' 2. 启动新进程: openclaw gateway / clawdbot gateway / moltbot gateway'));
2673
+ return false;
2674
+ }
2675
+
2587
2676
  async function restartGatewayNative() {
2588
2677
  const { cliBinary: resolved, nodeMajor } = getCliMeta();
2589
2678
  const nodeInfo = findCompatibleNode(nodeMajor);
@@ -2593,16 +2682,16 @@ async function restartGatewayNative() {
2593
2682
  // 尝试多种命令
2594
2683
  const commands = resolved
2595
2684
  ? [
2596
- useNode ? `"${nodeInfo.path}" "${resolved}" gateway restart` : `"${resolved}" gateway restart`
2597
- ]
2685
+ useNode ? `"${nodeInfo.path}" "${resolved}" gateway restart` : `"${resolved}" gateway restart`
2686
+ ]
2598
2687
  : [
2599
- 'openclaw gateway restart',
2600
- 'clawdbot gateway restart',
2601
- 'moltbot gateway restart',
2602
- 'npx openclaw gateway restart',
2603
- 'npx clawdbot gateway restart',
2604
- 'npx moltbot gateway restart'
2605
- ];
2688
+ 'openclaw gateway restart',
2689
+ 'clawdbot gateway restart',
2690
+ 'moltbot gateway restart',
2691
+ 'npx openclaw gateway restart',
2692
+ 'npx clawdbot gateway restart',
2693
+ 'npx moltbot gateway restart'
2694
+ ];
2606
2695
 
2607
2696
  return new Promise((resolve) => {
2608
2697
  let tried = 0;
@@ -2621,8 +2710,8 @@ async function restartGatewayNative() {
2621
2710
  const which = safeExec(process.platform === 'win32' ? `where ${name} 2>nul` : `/bin/zsh -lc "command -v ${name}" 2>/dev/null || /bin/bash -lc "command -v ${name}" 2>/dev/null`);
2622
2711
  if (which.ok && which.output) console.log(chalk.gray(` [诊断] ${name} -> ${which.output.split('\n')[0].trim()}`));
2623
2712
  }
2624
- console.log(chalk.gray(` [诊断] 等待 Gateway 启动...`));
2625
- resolve();
2713
+ // 尝试强制重启
2714
+ forceRestartGateway(resolved, nodeInfo, useNode, env).then(resolve);
2626
2715
  return;
2627
2716
  }
2628
2717
 
@@ -2635,7 +2724,7 @@ async function restartGatewayNative() {
2635
2724
  } else {
2636
2725
  console.log(chalk.green(`✅ Gateway 已重启`));
2637
2726
  console.log(chalk.gray(` 现在可以在 Web/Telegram/Discord 等渠道测试对话了`));
2638
- resolve();
2727
+ resolve(true);
2639
2728
  }
2640
2729
  });
2641
2730
  };
@@ -2650,13 +2739,13 @@ function testGatewayApi(port, token, model, endpoint = '/v1/responses') {
2650
2739
  const isChatCompletions = endpoint.includes('chat/completions');
2651
2740
  const postData = isChatCompletions
2652
2741
  ? JSON.stringify({
2653
- model: model,
2654
- messages: [{ role: 'user', content: '你是什么模型?' }]
2655
- })
2742
+ model: model,
2743
+ messages: [{ role: 'user', content: '你是什么模型?' }]
2744
+ })
2656
2745
  : JSON.stringify({
2657
- model: model,
2658
- input: '你是什么模型?'
2659
- });
2746
+ model: model,
2747
+ input: '你是什么模型?'
2748
+ });
2660
2749
 
2661
2750
  const options = {
2662
2751
  hostname: '127.0.0.1',
@@ -2768,6 +2857,18 @@ function testGatewayViaAgent(model) {
2768
2857
  exec(cmd, execOpts, (error, stdout, stderr) => {
2769
2858
  // 过滤 stderr 中的 Node.js DeprecationWarning 噪音
2770
2859
  const cleanStderr = (stderr || '').replace(/\(node:\d+\) \[DEP\d+\] DeprecationWarning:.*(\n.*trace-deprecation.*)?/g, '').trim();
2860
+ const errStr = cleanStderr.toLowerCase();
2861
+
2862
+ const wslGarbled = errStr.includes('wsl:') && errStr.includes('localhost') && errStr.includes('nat');
2863
+
2864
+ if (gwEnv === 'wsl' && (errStr.includes('/usr/bin/env: ‘node’: no such file') || wslGarbled)) {
2865
+ resolve({
2866
+ success: false,
2867
+ usedCli: true,
2868
+ error: 'WSL 子系统中未安装 Node.js。请使用 `wsl -u root apt install nodejs` 修复环境。\nGateway 可能暂无法在 WSL 内使用 CLI 诊断。'
2869
+ });
2870
+ return;
2871
+ }
2771
2872
 
2772
2873
  if (error) {
2773
2874
  // 即使 exec 报错,stdout 中可能仍有有效 JSON(如 CLI 输出了结果但 exit code 非零)
@@ -2865,9 +2966,9 @@ function sanitizeModelReply(message, options = {}) {
2865
2966
 
2866
2967
  const tailPattern = tokens.length
2867
2968
  ? new RegExp(
2868
- `[((][^))]*(?:${tokens.join('|')}|[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+)[^))]*[))]\\s*$`,
2869
- 'i'
2870
- )
2969
+ `[((][^))]*(?:${tokens.join('|')}|[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+)[^))]*[))]\\s*$`,
2970
+ 'i'
2971
+ )
2871
2972
  : /[((][^))]*[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+[^))]*[))]\s*$/i;
2872
2973
 
2873
2974
  text = text
@@ -3308,7 +3409,64 @@ async function restore(paths) {
3308
3409
  }
3309
3410
  }
3310
3411
 
3412
+ // ============ 环境自检 & 自动安装 ============
3413
+ function ensureGit() {
3414
+ const hasGit = safeExec('git --version');
3415
+ if (hasGit.ok) return;
3416
+
3417
+ console.log(chalk.yellow('\n⚠ 未检测到 Git,正在尝试自动安装...\n'));
3418
+ const platform = os.platform();
3419
+
3420
+ if (platform === 'win32') {
3421
+ // Windows: winget 优先
3422
+ const hasWinget = safeExec('winget --version');
3423
+ if (hasWinget.ok) {
3424
+ console.log(chalk.cyan('→ 通过 winget 安装 Git...'));
3425
+ const result = safeExec('winget install Git.Git --accept-source-agreements --accept-package-agreements --silent', { stdio: 'inherit', timeout: 300000 });
3426
+ if (result.ok) {
3427
+ console.log(chalk.green('✓ Git 安装成功,请重新打开终端后再次运行'));
3428
+ process.exit(0);
3429
+ }
3430
+ }
3431
+ console.log(chalk.red('✗ 自动安装失败,请手动安装 Git: https://git-scm.com'));
3432
+ process.exit(1);
3433
+ }
3434
+
3435
+ if (platform === 'darwin') {
3436
+ // macOS: xcode-select
3437
+ console.log(chalk.cyan('→ 通过 Xcode Command Line Tools 安装 Git...'));
3438
+ const result = safeExec('xcode-select --install', { stdio: 'inherit', timeout: 600000 });
3439
+ if (!result.ok && !result.stderr?.includes('already installed')) {
3440
+ console.log(chalk.yellow(' 请在弹出窗口中点击「安装」,完成后重新运行'));
3441
+ }
3442
+ process.exit(0);
3443
+ }
3444
+
3445
+ // Linux / WSL
3446
+ const pmCmds = [
3447
+ { check: 'apt-get', install: 'sudo apt-get update -qq && sudo apt-get install -y -qq git' },
3448
+ { check: 'dnf', install: 'sudo dnf install -y git' },
3449
+ { check: 'yum', install: 'sudo yum install -y git' },
3450
+ { check: 'pacman', install: 'sudo pacman -S --noconfirm git' },
3451
+ { check: 'apk', install: 'sudo apk add git' },
3452
+ { check: 'zypper', install: 'sudo zypper install -y git' },
3453
+ ];
3454
+ for (const pm of pmCmds) {
3455
+ if (safeExec(`command -v ${pm.check}`).ok) {
3456
+ console.log(chalk.cyan(`→ 通过 ${pm.check} 安装 Git...`));
3457
+ const result = safeExec(pm.install, { stdio: 'inherit', timeout: 300000 });
3458
+ if (result.ok && safeExec('git --version').ok) {
3459
+ console.log(chalk.green('✓ Git 安装成功'));
3460
+ return;
3461
+ }
3462
+ }
3463
+ }
3464
+ console.log(chalk.red('✗ 自动安装失败,请手动安装 Git: https://git-scm.com'));
3465
+ process.exit(1);
3466
+ }
3467
+
3311
3468
  // 启动
3469
+ ensureGit();
3312
3470
  main().catch(error => {
3313
3471
  exitWithError('CONFIG_FAILED', error.message, [
3314
3472
  '检查网络连接',
package/install.ps1 ADDED
@@ -0,0 +1,126 @@
1
+ # ============================================================
2
+ # maxapi 一键环境安装脚本(Windows PowerShell)
3
+ # 用法(Node.js 不存在时,先装 Node.js 再 npx):
4
+ # .\install.ps1
5
+ # $env:PACKAGE='llmaxapi'; .\install.ps1
6
+ # ============================================================
7
+
8
+ $ErrorActionPreference = "Stop"
9
+ $PACKAGE = if ($env:PACKAGE) { $env:PACKAGE } else { "yymaxapi" }
10
+ $MIN_NODE_MAJOR = 18
11
+
12
+ # ---------- 颜色输出 ----------
13
+ function Write-Ok($msg) { Write-Host " $msg" -ForegroundColor Green }
14
+ function Write-Info($msg) { Write-Host " $msg" -ForegroundColor Cyan }
15
+ function Write-Warn($msg) { Write-Host " $msg" -ForegroundColor Yellow }
16
+ function Write-Fail($msg) { Write-Host " $msg" -ForegroundColor Red; exit 1 }
17
+
18
+ Write-Host ""
19
+ Write-Host " maxapi 一键环境安装 ($PACKAGE)" -ForegroundColor Cyan
20
+ Write-Host " ========================================" -ForegroundColor Cyan
21
+ Write-Host ""
22
+
23
+ # ---------- 工具函数 ----------
24
+ function Test-Command($cmd) {
25
+ return [bool](Get-Command $cmd -ErrorAction SilentlyContinue)
26
+ }
27
+
28
+ function Get-NodeMajor {
29
+ if (-not (Test-Command "node")) { return 0 }
30
+ $ver = (node -v) -replace '^v','' -split '\.'
31
+ return [int]$ver[0]
32
+ }
33
+
34
+ function Refresh-Path {
35
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" +
36
+ [System.Environment]::GetEnvironmentVariable("Path", "User")
37
+ }
38
+
39
+ function Test-HasWinget {
40
+ return [bool](Get-Command "winget" -ErrorAction SilentlyContinue)
41
+ }
42
+
43
+ # ---------- 安装 Git ----------
44
+ function Install-GitIfNeeded {
45
+ if (Test-Command "git") {
46
+ Write-Ok "Git $(git --version) 已安装"
47
+ return
48
+ }
49
+ Write-Info "安装 Git..."
50
+ if (Test-HasWinget) {
51
+ winget install Git.Git --accept-source-agreements --accept-package-agreements --silent
52
+ Refresh-Path
53
+ } else {
54
+ Write-Warn "未检测到 winget,尝试下载 Git 安装包..."
55
+ $gitUrl = "https://github.com/git-for-windows/git/releases/latest/download/Git-64-bit.exe"
56
+ $gitInstaller = "$env:TEMP\git-installer.exe"
57
+ Invoke-WebRequest -Uri $gitUrl -OutFile $gitInstaller -UseBasicParsing
58
+ Start-Process -FilePath $gitInstaller -ArgumentList "/VERYSILENT","/NORESTART" -Wait
59
+ Remove-Item $gitInstaller -Force -ErrorAction SilentlyContinue
60
+ Refresh-Path
61
+ }
62
+ if (Test-Command "git") { Write-Ok "Git 安装成功" }
63
+ else { Write-Fail "Git 安装失败,请手动安装: https://git-scm.com" }
64
+ }
65
+
66
+ # ---------- 安装 Node.js ----------
67
+ function Install-NodeIfNeeded {
68
+ if ((Get-NodeMajor) -ge $MIN_NODE_MAJOR) {
69
+ Write-Ok "Node.js $(node -v) 已安装"
70
+ return
71
+ }
72
+ if (Test-Command "node") {
73
+ Write-Warn "Node.js $(node -v) 版本过低,需要 >= $MIN_NODE_MAJOR"
74
+ }
75
+ Write-Info "安装 Node.js LTS..."
76
+ if (Test-HasWinget) {
77
+ winget install OpenJS.NodeJS.LTS --accept-source-agreements --accept-package-agreements --silent
78
+ Refresh-Path
79
+ } else {
80
+ Write-Info "未检测到 winget,尝试下载 Node.js 安装包..."
81
+ $arch = if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" }
82
+ $nodeUrl = "https://nodejs.org/dist/latest-v22.x/node-v22.0.0-${arch}.msi"
83
+ # 获取实际最新 LTS 版本
84
+ try {
85
+ $versions = Invoke-RestMethod "https://nodejs.org/dist/index.json" -UseBasicParsing
86
+ $lts = ($versions | Where-Object { $_.lts -ne $false } | Select-Object -First 1).version
87
+ $nodeUrl = "https://nodejs.org/dist/${lts}/node-${lts}-${arch}.msi"
88
+ } catch {
89
+ Write-Warn "无法获取最新版本号,使用默认版本"
90
+ }
91
+ $nodeInstaller = "$env:TEMP\node-installer.msi"
92
+ Invoke-WebRequest -Uri $nodeUrl -OutFile $nodeInstaller -UseBasicParsing
93
+ Start-Process msiexec.exe -ArgumentList "/i","$nodeInstaller","/qn","/norestart" -Wait
94
+ Remove-Item $nodeInstaller -Force -ErrorAction SilentlyContinue
95
+ Refresh-Path
96
+ }
97
+ if ((Get-NodeMajor) -ge $MIN_NODE_MAJOR) {
98
+ Write-Ok "Node.js $(node -v) 安装成功"
99
+ } else {
100
+ Write-Fail "Node.js 安装失败,请手动安装: https://nodejs.org"
101
+ }
102
+ }
103
+
104
+ # ========== 主流程 ==========
105
+
106
+ # 修复执行策略(仅当前用户)
107
+ try {
108
+ Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force -ErrorAction SilentlyContinue
109
+ } catch {}
110
+
111
+ Install-GitIfNeeded
112
+ Install-NodeIfNeeded
113
+
114
+ Write-Host ""
115
+ Write-Host " ========================================" -ForegroundColor Green
116
+ Write-Host " 环境安装完成!" -ForegroundColor Green
117
+ Write-Host " ========================================" -ForegroundColor Green
118
+ Write-Host ""
119
+ Write-Ok "Git: $(git --version)"
120
+ Write-Ok "Node: $(node -v)"
121
+ Write-Ok "npm: $(npm -v)"
122
+ Write-Host ""
123
+
124
+ Write-Info "正在启动 ${PACKAGE}..."
125
+ Write-Host ""
126
+ & npx.cmd "${PACKAGE}@latest"
package/install.sh ADDED
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================
3
+ # maxapi 一键环境安装脚本(macOS / Linux / WSL)
4
+ # 用法(Node.js 不存在时,先装 Node.js 再 npx):
5
+ # bash install.sh
6
+ # bash install.sh llmaxapi
7
+ # bash install.sh --no-run
8
+ # ============================================================
9
+ set -euo pipefail
10
+
11
+ # ---------- 配置 ----------
12
+ PACKAGE="${1:-yymaxapi}"
13
+ NO_RUN=false
14
+ MIN_NODE_MAJOR=18
15
+
16
+ for arg in "$@"; do
17
+ case "$arg" in
18
+ --no-run) NO_RUN=true ;;
19
+ yymaxapi|llmaxapi) PACKAGE="$arg" ;;
20
+ esac
21
+ done
22
+
23
+ # ---------- 颜色 ----------
24
+ RED='\033[0;31m'; GREEN='\033[0;32m'
25
+ YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
26
+
27
+ ok() { echo -e "${GREEN}✓${NC} $1"; }
28
+ info() { echo -e "${CYAN}→${NC} $1"; }
29
+ warn() { echo -e "${YELLOW}⚠${NC} $1"; }
30
+ fail() { echo -e "${RED}✗${NC} $1"; exit 1; }
31
+
32
+ # ---------- 平台检测 ----------
33
+ OS="$(uname -s)"
34
+ ARCH="$(uname -m)"
35
+ IS_WSL=false
36
+ grep -qiE '(microsoft|wsl)' /proc/version 2>/dev/null && IS_WSL=true
37
+
38
+ echo ""
39
+ echo -e "${CYAN}╔══════════════════════════════════════╗${NC}"
40
+ echo -e "${CYAN}║ maxapi 一键环境安装 (${PACKAGE}) ${NC}"
41
+ echo -e "${CYAN}╚══════════════════════════════════════╝${NC}"
42
+ echo ""
43
+ info "系统: ${OS} ${ARCH}$($IS_WSL && echo ' (WSL)' || true)"
44
+ echo ""
45
+
46
+ # ---------- 工具函数 ----------
47
+ has_cmd() { command -v "$1" &>/dev/null; }
48
+
49
+ node_version_ok() {
50
+ if ! has_cmd node; then return 1; fi
51
+ local ver
52
+ ver="$(node -v | tr -d 'v' | cut -d. -f1)"
53
+ [ "$ver" -ge "$MIN_NODE_MAJOR" ] 2>/dev/null
54
+ }
55
+
56
+ detect_pkg_manager() {
57
+ if has_cmd apt-get; then echo "apt"; return; fi
58
+ if has_cmd dnf; then echo "dnf"; return; fi
59
+ if has_cmd yum; then echo "yum"; return; fi
60
+ if has_cmd pacman; then echo "pacman"; return; fi
61
+ if has_cmd apk; then echo "apk"; return; fi
62
+ if has_cmd zypper; then echo "zypper"; return; fi
63
+ if has_cmd brew; then echo "brew"; return; fi
64
+ echo "unknown"
65
+ }
66
+
67
+ # ---------- 安装 Git ----------
68
+ install_git() {
69
+ if has_cmd git; then
70
+ ok "Git $(git --version | awk '{print $3}') 已安装"
71
+ return
72
+ fi
73
+ info "安装 Git..."
74
+ case "$OS" in
75
+ Darwin)
76
+ if has_cmd brew; then
77
+ brew install git
78
+ else
79
+ info "通过 Xcode Command Line Tools 安装..."
80
+ xcode-select --install 2>/dev/null || true
81
+ warn "请在弹出窗口中点击「安装」,完成后重新运行此脚本"
82
+ exit 0
83
+ fi ;;
84
+ Linux)
85
+ local pm; pm="$(detect_pkg_manager)"
86
+ case "$pm" in
87
+ apt) sudo apt-get update -qq && sudo apt-get install -y -qq git ;;
88
+ dnf) sudo dnf install -y git ;;
89
+ yum) sudo yum install -y git ;;
90
+ pacman) sudo pacman -S --noconfirm git ;;
91
+ apk) sudo apk add git ;;
92
+ zypper) sudo zypper install -y git ;;
93
+ *) fail "无法自动安装 Git,请手动安装: https://git-scm.com" ;;
94
+ esac ;;
95
+ *) fail "不支持的操作系统: $OS" ;;
96
+ esac
97
+ has_cmd git && ok "Git 安装成功" || fail "Git 安装失败"
98
+ }
99
+
100
+ # ---------- 安装 Node.js ----------
101
+ install_node() {
102
+ if node_version_ok; then
103
+ ok "Node.js $(node -v) 已安装"
104
+ return
105
+ fi
106
+
107
+ if has_cmd node; then
108
+ warn "Node.js $(node -v) 版本过低,需要 >= ${MIN_NODE_MAJOR}"
109
+ fi
110
+
111
+ info "安装 Node.js (via fnm)..."
112
+
113
+ # fnm: 快速 Node 版本管理器,单二进制,跨平台
114
+ if ! has_cmd fnm; then
115
+ curl -fsSL https://fnm.vercel.app/install | bash -s -- --skip-shell
116
+ fi
117
+
118
+ # 加载 fnm 到当前 shell
119
+ export FNM_DIR="${FNM_DIR:-$HOME/.local/share/fnm}"
120
+ if [ -d "$FNM_DIR" ]; then
121
+ export PATH="$FNM_DIR:$PATH"
122
+ elif [ -d "$HOME/.fnm" ]; then
123
+ export PATH="$HOME/.fnm:$PATH"
124
+ fi
125
+ eval "$(fnm env)" 2>/dev/null || true
126
+
127
+ fnm install --lts
128
+ fnm use lts-latest
129
+ eval "$(fnm env)" 2>/dev/null || true
130
+
131
+ if node_version_ok; then
132
+ ok "Node.js $(node -v) 安装成功"
133
+ else
134
+ fail "Node.js 安装失败,请手动安装: https://nodejs.org"
135
+ fi
136
+
137
+ # 提示用户添加 fnm 到 shell profile
138
+ local shell_profile=""
139
+ case "${SHELL:-}" in
140
+ */zsh) shell_profile="$HOME/.zshrc" ;;
141
+ */bash) shell_profile="$HOME/.bashrc" ;;
142
+ */fish) shell_profile="$HOME/.config/fish/config.fish" ;;
143
+ esac
144
+
145
+ if [ -n "$shell_profile" ] && ! grep -q 'fnm env' "$shell_profile" 2>/dev/null; then
146
+ warn "建议将以下内容添加到 ${shell_profile}:"
147
+ echo ' eval "$(fnm env --use-on-cd --shell '"${SHELL##*/}"')"'
148
+ fi
149
+ }
150
+
151
+ # ---------- 安装 curl(极少数 Linux 缺失) ----------
152
+ install_curl() {
153
+ if has_cmd curl; then return; fi
154
+ info "安装 curl..."
155
+ local pm; pm="$(detect_pkg_manager)"
156
+ case "$pm" in
157
+ apt) sudo apt-get update -qq && sudo apt-get install -y -qq curl ;;
158
+ dnf) sudo dnf install -y curl ;;
159
+ yum) sudo yum install -y curl ;;
160
+ pacman) sudo pacman -S --noconfirm curl ;;
161
+ apk) sudo apk add curl ;;
162
+ *) fail "请先手动安装 curl" ;;
163
+ esac
164
+ }
165
+
166
+ # ========== 主流程 ==========
167
+ install_curl
168
+ install_git
169
+ install_node
170
+
171
+ echo ""
172
+ echo -e "${GREEN}╔══════════════════════════════════════╗${NC}"
173
+ echo -e "${GREEN}║ 环境安装完成! ${NC}"
174
+ echo -e "${GREEN}╚══════════════════════════════════════╝${NC}"
175
+ echo ""
176
+ ok "Git: $(git --version | awk '{print $3}')"
177
+ ok "Node: $(node -v)"
178
+ ok "npm: $(npm -v)"
179
+ echo ""
180
+
181
+ if [ "$NO_RUN" = true ]; then
182
+ info "跳过运行 ${PACKAGE}(--no-run)"
183
+ info "手动运行: npx ${PACKAGE}@latest"
184
+ else
185
+ info "正在启动 ${PACKAGE}..."
186
+ echo ""
187
+ npx "${PACKAGE}@latest"
188
+ fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yymaxapi",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "bin/yymaxapi.js",
6
6
  "bin": {
@@ -26,7 +26,9 @@
26
26
  "bin/",
27
27
  "lib/",
28
28
  "README.md",
29
- "config/API节点设置.md"
29
+ "config/API节点设置.md",
30
+ "install.sh",
31
+ "install.ps1"
30
32
  ],
31
33
  "dependencies": {
32
34
  "inquirer": "^8.2.5",