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.
- package/bin/cli.mjs +216 -44
- 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
|
-
|
|
1289
|
+
// 优先用 CLI 设置,超时则直接改配置文件
|
|
1290
|
+
const setResult = safeExec(`${cliName} config set gateway.port ${availablePort}`, { timeout: 10000 });
|
|
1220
1291
|
if (!setResult.ok) {
|
|
1221
|
-
|
|
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
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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 (
|
|
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-
|
|
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-
|
|
3010
|
+
log.warn('无效选项,请输入 0-12');
|
|
2839
3011
|
}
|
|
2840
3012
|
}
|
|
2841
3013
|
}
|