openclawsetup 2.8.9 → 2.8.11

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/cli.mjs +216 -44
  2. package/package.json +1 -1
package/bin/cli.mjs CHANGED
@@ -953,6 +953,76 @@ function isLikelyOpenClawProcess(output = '') {
953
953
  );
954
954
  }
955
955
 
956
+ /**
957
+ * 通过端口直接检测 Gateway 进程信息(不依赖 openclaw status 命令)
958
+ * 返回 { found, pid, processName, cmdline, isOpenClaw }
959
+ */
960
+ function detectGatewayProcess(port) {
961
+ const result = { found: false, pid: null, processName: '', cmdline: '', isOpenClaw: false };
962
+
963
+ if (platform() === 'win32') {
964
+ // Windows: netstat 找 PID,再用 wmic 查进程详情
965
+ const netstat = safeExec(`netstat -ano -p tcp | findstr LISTENING | findstr :${port}`, { timeout: 8000 });
966
+ if (!netstat.ok || !netstat.output) return result;
967
+
968
+ // 提取 PID(netstat 输出最后一列是 PID)
969
+ const pidMatch = netstat.output.match(/LISTENING\s+(\d+)/);
970
+ if (!pidMatch) return result;
971
+ result.pid = pidMatch[1];
972
+ result.found = true;
973
+
974
+ // 用 wmic 获取进程命令行
975
+ const wmic = safeExec(`wmic process where "ProcessId=${result.pid}" get CommandLine,Name /format:list`, { timeout: 5000 });
976
+ if (wmic.ok && wmic.output) {
977
+ const nameMatch = wmic.output.match(/Name=(.+)/i);
978
+ const cmdMatch = wmic.output.match(/CommandLine=(.+)/i);
979
+ result.processName = nameMatch ? nameMatch[1].trim() : '';
980
+ result.cmdline = cmdMatch ? cmdMatch[1].trim() : '';
981
+ }
982
+
983
+ // wmic 可能不可用(新版 Windows),回退用 tasklist
984
+ if (!result.processName) {
985
+ const tasklist = safeExec(`tasklist /FI "PID eq ${result.pid}" /FO CSV /NH`, { timeout: 5000 });
986
+ if (tasklist.ok && tasklist.output) {
987
+ const parts = tasklist.output.split(',');
988
+ if (parts.length > 0) {
989
+ result.processName = parts[0].replace(/"/g, '').trim();
990
+ }
991
+ }
992
+ }
993
+ } else {
994
+ // Linux/macOS: lsof 找 PID 和进程名
995
+ const lsof = safeExec(`lsof -nP -iTCP:${port} -sTCP:LISTEN -F pcn 2>/dev/null`, { timeout: 5000 });
996
+ if (lsof.ok && lsof.output) {
997
+ const pidMatch = lsof.output.match(/p(\d+)/);
998
+ const nameMatch = lsof.output.match(/c(.+)/);
999
+ if (pidMatch) {
1000
+ result.pid = pidMatch[1];
1001
+ result.found = true;
1002
+ result.processName = nameMatch ? nameMatch[1].trim() : '';
1003
+ }
1004
+ }
1005
+
1006
+ // 获取完整命令行
1007
+ if (result.pid) {
1008
+ const cmdline = safeExec(`cat /proc/${result.pid}/cmdline 2>/dev/null | tr '\\0' ' ' || ps -p ${result.pid} -o args= 2>/dev/null`, { timeout: 3000 });
1009
+ if (cmdline.ok && cmdline.output) {
1010
+ result.cmdline = cmdline.output.trim();
1011
+ }
1012
+ }
1013
+ }
1014
+
1015
+ // 判断是否是 OpenClaw 进程
1016
+ const combined = `${result.processName} ${result.cmdline}`.toLowerCase();
1017
+ result.isOpenClaw = combined.includes('openclaw') || combined.includes('clawdbot') || combined.includes('moltbot');
1018
+ // node 进程运行 gateway 也算
1019
+ if (!result.isOpenClaw && combined.includes('node') && combined.includes('gateway')) {
1020
+ result.isOpenClaw = true;
1021
+ }
1022
+
1023
+ return result;
1024
+ }
1025
+
956
1026
  function hasListeningState(output = '') {
957
1027
  if (!output) return false;
958
1028
  if (platform() !== 'win32') return Boolean(output.trim());
@@ -1216,9 +1286,13 @@ async function tryFixPortConflict(cliName, currentPort) {
1216
1286
  return { ok: false, newPort: currentPort, note: '未找到可用替代端口' };
1217
1287
  }
1218
1288
 
1219
- const setResult = safeExec(`${cliName} config set gateway.port ${availablePort}`);
1289
+ // 优先用 CLI 设置,超时则直接改配置文件
1290
+ const setResult = safeExec(`${cliName} config set gateway.port ${availablePort}`, { timeout: 10000 });
1220
1291
  if (!setResult.ok) {
1221
- return { ok: false, newPort: currentPort, note: `端口切换失败: ${setResult.stderr || setResult.error}` };
1292
+ const fileSet = setGatewayPortInConfig(availablePort);
1293
+ if (!fileSet.ok) {
1294
+ return { ok: false, newPort: currentPort, note: `端口切换失败: ${fileSet.error}` };
1295
+ }
1222
1296
  }
1223
1297
 
1224
1298
  const restartResult = await ensureGatewayRunning(cliName, 'restart');
@@ -1229,6 +1303,109 @@ async function tryFixPortConflict(cliName, currentPort) {
1229
1303
  return { ok: true, newPort: availablePort, note: `端口已切换到 ${availablePort}` };
1230
1304
  }
1231
1305
 
1306
+ /**
1307
+ * 直接修改 openclaw.json 中的 gateway.port(不依赖 openclaw CLI)
1308
+ */
1309
+ function setGatewayPortInConfig(newPort) {
1310
+ const config = getConfigInfo();
1311
+ if (!config.configPath || !existsSync(config.configPath)) {
1312
+ return { ok: false, error: '配置文件不存在' };
1313
+ }
1314
+ try {
1315
+ const raw = readFileSync(config.configPath, 'utf8');
1316
+ const json = JSON.parse(raw);
1317
+ if (!json.gateway) json.gateway = {};
1318
+ json.gateway.port = newPort;
1319
+ writeFileSync(config.configPath, JSON.stringify(json, null, 2), 'utf8');
1320
+ return { ok: true };
1321
+ } catch (e) {
1322
+ return { ok: false, error: e.message };
1323
+ }
1324
+ }
1325
+
1326
+ /**
1327
+ * 重置/更改端口:找可用端口 → 写配置 → 重启 Gateway
1328
+ */
1329
+ async function resetGatewayPort(cliName, requestedPort = null) {
1330
+ const config = getConfigInfo();
1331
+ const currentPort = config.port || DEFAULT_GATEWAY_PORT;
1332
+
1333
+ console.log(colors.cyan('\n🔧 Gateway 端口管理\n'));
1334
+ console.log(colors.gray(` 当前端口: ${currentPort}`));
1335
+
1336
+ // 确定目标端口
1337
+ let targetPort;
1338
+ if (requestedPort && requestedPort !== currentPort) {
1339
+ // 用户指定了端口,检查是否可用
1340
+ const inUse = await isPortInUse(requestedPort);
1341
+ if (inUse) {
1342
+ const processInfo = detectGatewayProcess(requestedPort);
1343
+ if (processInfo.found) {
1344
+ log.error(`端口 ${requestedPort} 已被占用: ${processInfo.processName || '未知'} (PID ${processInfo.pid})`);
1345
+ } else {
1346
+ log.error(`端口 ${requestedPort} 已被占用`);
1347
+ }
1348
+ return { ok: false, port: currentPort };
1349
+ }
1350
+ targetPort = requestedPort;
1351
+ } else {
1352
+ // 自动找可用端口
1353
+ console.log(colors.gray(' 正在扫描可用端口...'));
1354
+ targetPort = await findAvailablePort(DEFAULT_GATEWAY_PORT);
1355
+ if (!targetPort) {
1356
+ log.error('未找到可用端口');
1357
+ return { ok: false, port: currentPort };
1358
+ }
1359
+ }
1360
+
1361
+ console.log(colors.cyan(` 目标端口: ${targetPort}`));
1362
+
1363
+ // 先尝试用 CLI 设置(如果可用)
1364
+ const cliSet = safeExec(`${cliName} config set gateway.port ${targetPort}`, { timeout: 10000 });
1365
+ if (!cliSet.ok) {
1366
+ // CLI 不可用,直接改配置文件
1367
+ console.log(colors.gray(' CLI 设置超时,直接修改配置文件...'));
1368
+ const fileSet = setGatewayPortInConfig(targetPort);
1369
+ if (!fileSet.ok) {
1370
+ log.error(`配置写入失败: ${fileSet.error}`);
1371
+ return { ok: false, port: currentPort };
1372
+ }
1373
+ }
1374
+
1375
+ log.success(`端口已设置为 ${targetPort}`);
1376
+
1377
+ // 重启 Gateway
1378
+ console.log(colors.gray(' 正在重启 Gateway...'));
1379
+ await ensureGatewayRunning(cliName, 'restart');
1380
+
1381
+ // 等待并验证新端口
1382
+ const maxWait = platform() === 'win32' ? 15000 : 8000;
1383
+ const interval = 1500;
1384
+ let waited = 0;
1385
+ let portReady = false;
1386
+ while (waited < maxWait) {
1387
+ await sleep(interval);
1388
+ waited += interval;
1389
+ const check = getPortCheckOutput(targetPort);
1390
+ if (check.ok && hasListeningState(check.output)) {
1391
+ portReady = true;
1392
+ break;
1393
+ }
1394
+ }
1395
+
1396
+ if (portReady) {
1397
+ log.success(`Gateway 已在端口 ${targetPort} 上运行`);
1398
+ const token = getDashboardToken(config);
1399
+ if (token) {
1400
+ console.log(colors.cyan(`\n Dashboard: http://127.0.0.1:${targetPort}/?token=${token}`));
1401
+ }
1402
+ return { ok: true, port: targetPort };
1403
+ }
1404
+
1405
+ log.warn('Gateway 重启后端口未就绪,请手动检查');
1406
+ return { ok: false, port: targetPort };
1407
+ }
1408
+
1232
1409
  function summarizeIssue(level, title, detail, solution, fixCmd = '') {
1233
1410
  return { level, title, detail, solution, fixCmd };
1234
1411
  }
@@ -2573,61 +2750,44 @@ async function showStatusInfo(cliName) {
2573
2750
  const dashboardUrl = `http://127.0.0.1:${port}/?token=${token}`;
2574
2751
 
2575
2752
  console.log(colors.bold(colors.cyan('\n📊 OpenClaw 状态信息\n')));
2576
- console.log(colors.gray(' … 正在检查服务状态(超时自动跳过)...'));
2577
-
2578
- // 服务状态(兼容不同 CLI 版本,避免 Windows 下误判)
2579
- const statusAttempts = [
2580
- { cmd: `${cliName} status`, label: 'status' },
2581
- { cmd: `${cliName} gateway status`, label: 'gateway status' },
2582
- ];
2583
- let statusState = 'unknown';
2584
- let statusOutput = '';
2585
- let statusTimedOut = false;
2586
-
2587
- for (const attempt of statusAttempts) {
2588
- const statusResult = safeExec(attempt.cmd, { timeout: STATUS_CMD_TIMEOUT_MS });
2589
- if (!statusResult.ok && /timed out|etimedout|SIGTERM|killed/i.test(`${statusResult.error || ''} ${statusResult.stderr || ''}`)) {
2590
- statusTimedOut = true;
2591
- continue;
2592
- }
2593
- if (!statusResult.ok || !statusResult.output) continue;
2594
- statusOutput = statusResult.output;
2595
- const parsed = parseStatusOutput(statusResult.output);
2596
- if (parsed === 'running') {
2597
- statusState = 'running';
2598
- break;
2599
- }
2600
- if (parsed === 'stopped') {
2601
- statusState = 'stopped';
2602
- }
2603
- }
2753
+ console.log(colors.gray(' … 正在检查服务状态...'));
2604
2754
 
2605
- // 端口检查(作为状态命令兜底)
2755
+ // 1. 直接通过端口检测进程(不依赖 openclaw status 命令)
2756
+ const processInfo = detectGatewayProcess(port);
2606
2757
  const portResult = getPortCheckOutput(port, PORT_CHECK_TIMEOUT_MS);
2607
2758
  const portListening = Boolean(portResult.ok && portResult.output);
2608
2759
 
2609
- if (statusTimedOut && !portListening) {
2610
- console.log(colors.yellow(' 状态命令超时,已自动切换端口探测模式'));
2611
- }
2612
- if (portResolved.source === 'runtime' && Number(config?.port) !== Number(port)) {
2613
- console.log(colors.gray(` … 运行时端口为 ${port}(与本地配置文件不一致)`));
2760
+ // 2. 如果直接检测没结果,再尝试 openclaw status 命令
2761
+ let statusState = 'unknown';
2762
+ if (!processInfo.found && !portListening) {
2763
+ const statusProbe = probeGatewayStatus(cliName, STATUS_CMD_TIMEOUT_MS);
2764
+ statusState = statusProbe.state;
2614
2765
  }
2615
2766
 
2616
- if (statusState === 'running') {
2767
+ // 3. 综合判断并显示
2768
+ if (processInfo.found && processInfo.isOpenClaw) {
2617
2769
  console.log(colors.green(' ✓ Gateway 服务正在运行'));
2770
+ console.log(colors.gray(` 进程: ${processInfo.processName || 'node'} (PID ${processInfo.pid})`));
2771
+ } else if (processInfo.found && !processInfo.isOpenClaw) {
2772
+ console.log(colors.yellow(` ⚠ 端口 ${port} 被其他进程占用`));
2773
+ console.log(colors.gray(` 进程: ${processInfo.processName || '未知'} (PID ${processInfo.pid})`));
2774
+ if (processInfo.cmdline) {
2775
+ console.log(colors.gray(` 命令: ${processInfo.cmdline.substring(0, 120)}`));
2776
+ }
2777
+ console.log(colors.yellow(' → 请先选择「检查修复」解决端口冲突\n'));
2778
+ return;
2618
2779
  } else if (portListening) {
2619
- // 端口在监听 = Gateway 在运行,状态命令超时/不准确不影响判断
2780
+ // 端口在监听但无法获取进程详情(权限不足等)
2781
+ console.log(colors.green(' ✓ Gateway 服务正在运行'));
2782
+ } else if (statusState === 'running') {
2620
2783
  console.log(colors.green(' ✓ Gateway 服务正在运行'));
2621
2784
  } else {
2622
2785
  console.log(colors.red(' ✗ Gateway 服务未运行'));
2623
- if (statusOutput) {
2624
- console.log(colors.gray(` 状态输出: ${statusOutput.split('\n')[0].slice(0, 100)}`));
2625
- }
2626
2786
  console.log(colors.yellow(' → 请先选择「检查修复」自动修复此问题\n'));
2627
2787
  return;
2628
2788
  }
2629
2789
 
2630
- if (portResult.ok && portResult.output) {
2790
+ if (portListening) {
2631
2791
  console.log(colors.green(` ✓ 端口 ${port} 正在监听`));
2632
2792
  } else {
2633
2793
  console.log(colors.red(` ✗ 端口 ${port} 未监听`));
@@ -2748,9 +2908,10 @@ async function showInteractiveMenu(existing) {
2748
2908
  console.log(` ${colors.yellow('9')}. 配置技能`);
2749
2909
  console.log(` ${colors.yellow('10')}. 重新安装`);
2750
2910
  console.log(` ${colors.yellow('11')}. 完全卸载`);
2911
+ console.log(` ${colors.yellow('12')}. 重置/更改端口`);
2751
2912
  console.log(` ${colors.yellow('0')}. 退出`);
2752
2913
 
2753
- const choice = await askQuestion('\n请输入选项 (0-11): ');
2914
+ const choice = await askQuestion('\n请输入选项 (0-12): ');
2754
2915
 
2755
2916
  switch (choice.trim()) {
2756
2917
  case '1':
@@ -2830,12 +2991,23 @@ async function showInteractiveMenu(existing) {
2830
2991
  process.exit(0);
2831
2992
  }
2832
2993
  break;
2994
+ case '12': {
2995
+ const portInput = await askQuestion('输入新端口号(留空自动分配): ');
2996
+ const requestedPort = portInput.trim() ? Number(portInput.trim()) : null;
2997
+ if (requestedPort && (isNaN(requestedPort) || requestedPort < 1024 || requestedPort > 65535)) {
2998
+ log.error('端口号无效,请输入 1024-65535 之间的数字');
2999
+ } else {
3000
+ await resetGatewayPort(cliName, requestedPort);
3001
+ }
3002
+ await waitForEnter('\n按回车返回菜单...');
3003
+ break;
3004
+ }
2833
3005
  case '0':
2834
3006
  case '':
2835
3007
  console.log(colors.gray('\n再见!'));
2836
3008
  process.exit(0);
2837
3009
  default:
2838
- log.warn('无效选项,请输入 0-11');
3010
+ log.warn('无效选项,请输入 0-12');
2839
3011
  }
2840
3012
  }
2841
3013
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclawsetup",
3
- "version": "2.8.9",
3
+ "version": "2.8.11",
4
4
  "description": "OpenClaw 安装向导 - 智能安装、诊断、自动修复",
5
5
  "type": "module",
6
6
  "bin": {