openclawsetup 1.0.14 → 1.0.16
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/README.md +25 -13
- package/bin/cli.mjs +147 -13
- package/install.sh +165 -36
- package/package.json +2 -2
- package//344/275/277/347/224/250/350/257/264/346/230/216.md +5 -1
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
- 自动创建配置文件和 workspace
|
|
10
10
|
- 自动配置服务持久化(开机自启动)
|
|
11
11
|
- 支持 macOS、Linux、Windows 三系统
|
|
12
|
+
- 安装/更新/卸载重装显示进度与预计剩余时间(基于网络测速,可能有误差)
|
|
13
|
+
- 云服务器自动检测公网 IP 并输出访问指引
|
|
12
14
|
- 无需任何交互,全自动完成
|
|
13
15
|
|
|
14
16
|
## 快速开始
|
|
@@ -17,18 +19,18 @@
|
|
|
17
19
|
|
|
18
20
|
**macOS / Linux:**
|
|
19
21
|
```bash
|
|
20
|
-
curl -fsSL https://unpkg.com/openclawsetup/install.sh | bash
|
|
22
|
+
curl -fsSL https://unpkg.com/openclawsetup@latest/install.sh | bash
|
|
21
23
|
```
|
|
22
24
|
|
|
23
25
|
**Windows PowerShell:**
|
|
24
26
|
```powershell
|
|
25
|
-
irm https://unpkg.com/openclawsetup/install.ps1 | iex
|
|
27
|
+
irm https://unpkg.com/openclawsetup@latest/install.ps1 | iex
|
|
26
28
|
```
|
|
27
29
|
|
|
28
30
|
### 方式二:npx(需要已安装 Node.js 18+)
|
|
29
31
|
|
|
30
32
|
```bash
|
|
31
|
-
npx openclawsetup
|
|
33
|
+
npx --yes openclawsetup@latest
|
|
32
34
|
```
|
|
33
35
|
|
|
34
36
|
如果提示 `npx: command not found`,说明未安装 Node.js,请使用方式一。
|
|
@@ -59,48 +61,58 @@ npx openclawsetup
|
|
|
59
61
|
|
|
60
62
|
```bash
|
|
61
63
|
# macOS/Linux - 一键脚本
|
|
62
|
-
curl -fsSL https://unpkg.com/openclawsetup/install.sh | bash
|
|
64
|
+
curl -fsSL https://unpkg.com/openclawsetup@latest/install.sh | bash
|
|
63
65
|
|
|
64
66
|
# 或者 npx(需要 Node.js)
|
|
65
|
-
npx openclawsetup
|
|
67
|
+
npx --yes openclawsetup@latest
|
|
66
68
|
```
|
|
67
69
|
|
|
68
70
|
### 指定端口
|
|
69
71
|
|
|
70
72
|
```bash
|
|
71
73
|
# macOS/Linux
|
|
72
|
-
OPENCLAW_PORT=8080 npx openclawsetup
|
|
74
|
+
OPENCLAW_PORT=8080 npx --yes openclawsetup@latest
|
|
73
75
|
|
|
74
76
|
# Windows PowerShell
|
|
75
|
-
$env:OPENCLAW_PORT=8080; npx openclawsetup
|
|
77
|
+
$env:OPENCLAW_PORT=8080; npx --yes openclawsetup@latest
|
|
76
78
|
|
|
77
79
|
# Windows CMD
|
|
78
|
-
set OPENCLAW_PORT=8080 && npx openclawsetup
|
|
80
|
+
set OPENCLAW_PORT=8080 && npx --yes openclawsetup@latest
|
|
79
81
|
```
|
|
80
82
|
|
|
81
83
|
### 指定 Token
|
|
82
84
|
|
|
83
85
|
```bash
|
|
84
86
|
# macOS/Linux
|
|
85
|
-
GATEWAY_TOKEN="my-secret-token" npx openclawsetup
|
|
87
|
+
GATEWAY_TOKEN="my-secret-token" npx --yes openclawsetup@latest
|
|
86
88
|
|
|
87
89
|
# Windows PowerShell
|
|
88
|
-
$env:GATEWAY_TOKEN="my-secret-token"; npx openclawsetup
|
|
90
|
+
$env:GATEWAY_TOKEN="my-secret-token"; npx --yes openclawsetup@latest
|
|
89
91
|
|
|
90
92
|
# Windows CMD
|
|
91
|
-
set GATEWAY_TOKEN=my-secret-token && npx openclawsetup
|
|
93
|
+
set GATEWAY_TOKEN=my-secret-token && npx --yes openclawsetup@latest
|
|
92
94
|
```
|
|
93
95
|
|
|
94
96
|
### 命令行参数方式
|
|
95
97
|
|
|
96
98
|
```bash
|
|
97
|
-
npx openclawsetup --port 8080 --token my-secret-token
|
|
99
|
+
npx --yes openclawsetup@latest --port 8080 --token my-secret-token
|
|
98
100
|
```
|
|
99
101
|
|
|
100
102
|
### 仅安装不启动
|
|
101
103
|
|
|
102
104
|
```bash
|
|
103
|
-
npx openclawsetup --skip-start
|
|
105
|
+
npx --yes openclawsetup@latest --skip-start
|
|
106
|
+
|
|
107
|
+
### 已安装用户:更新或重装
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# 检查并更新
|
|
111
|
+
npx --yes openclawsetup@latest --update
|
|
112
|
+
|
|
113
|
+
# 卸载后重新安装(会清除配置)
|
|
114
|
+
npx --yes openclawsetup@latest --reinstall
|
|
115
|
+
```
|
|
104
116
|
```
|
|
105
117
|
|
|
106
118
|
## 安装后
|
package/bin/cli.mjs
CHANGED
|
@@ -170,6 +170,128 @@ function safeExec(cmd, options = {}) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
const ETA_SAMPLE_BYTES = 256 * 1024;
|
|
174
|
+
const ETA_FACTOR = 2.0;
|
|
175
|
+
const ETA_OVERHEAD = 8;
|
|
176
|
+
|
|
177
|
+
async function fetchText(url, timeoutMs = 2000) {
|
|
178
|
+
const controller = new AbortController();
|
|
179
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
180
|
+
try {
|
|
181
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
182
|
+
if (!res.ok) return '';
|
|
183
|
+
return (await res.text()).trim();
|
|
184
|
+
} catch {
|
|
185
|
+
return '';
|
|
186
|
+
} finally {
|
|
187
|
+
clearTimeout(timer);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function extractIPv4(text) {
|
|
192
|
+
const match = text.match(/\b(\d{1,3}\.){3}\d{1,3}\b/);
|
|
193
|
+
return match ? match[0] : '';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function getPublicIP() {
|
|
197
|
+
const services = ['https://ifconfig.me/ip', 'https://icanhazip.com', 'https://ipinfo.io/ip'];
|
|
198
|
+
for (const url of services) {
|
|
199
|
+
const text = await fetchText(url);
|
|
200
|
+
const ip = extractIPv4(text);
|
|
201
|
+
if (ip && !ip.startsWith('127.')) return ip;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const result = safeExec('hostname -I 2>/dev/null || hostname -i 2>/dev/null');
|
|
205
|
+
if (result.ok && result.output) {
|
|
206
|
+
const ip = extractIPv4(result.output);
|
|
207
|
+
if (ip && !ip.startsWith('127.')) return ip;
|
|
208
|
+
}
|
|
209
|
+
return '';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getNpmDistInfo(pkgName) {
|
|
213
|
+
const tarballResult = safeExec(`npm view ${pkgName}@latest dist.tarball`);
|
|
214
|
+
const sizeResult = safeExec(`npm view ${pkgName}@latest dist.size`);
|
|
215
|
+
const tarball = tarballResult.ok ? tarballResult.output.trim() : '';
|
|
216
|
+
const size = sizeResult.ok ? parseInt(sizeResult.output.trim(), 10) : 0;
|
|
217
|
+
return { tarball, size: Number.isFinite(size) ? size : 0 };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function fetchContentLength(url, timeoutMs = 4000) {
|
|
221
|
+
const controller = new AbortController();
|
|
222
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
223
|
+
try {
|
|
224
|
+
const res = await fetch(url, { method: 'HEAD', signal: controller.signal });
|
|
225
|
+
const len = res.headers.get('content-length');
|
|
226
|
+
const size = len ? parseInt(len, 10) : 0;
|
|
227
|
+
return Number.isFinite(size) ? size : 0;
|
|
228
|
+
} catch {
|
|
229
|
+
return 0;
|
|
230
|
+
} finally {
|
|
231
|
+
clearTimeout(timer);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function measureDownloadSpeed(url, sampleBytes = ETA_SAMPLE_BYTES, timeoutMs = 5000) {
|
|
236
|
+
let received = 0;
|
|
237
|
+
const controller = new AbortController();
|
|
238
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
239
|
+
const start = Date.now();
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const res = await fetch(url, {
|
|
243
|
+
headers: { Range: `bytes=0-${sampleBytes - 1}` },
|
|
244
|
+
signal: controller.signal,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (!res.ok && res.status !== 206 && res.status !== 200) {
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const reader = res.body?.getReader();
|
|
252
|
+
if (!reader) return 0;
|
|
253
|
+
|
|
254
|
+
while (received < sampleBytes) {
|
|
255
|
+
const { done, value } = await reader.read();
|
|
256
|
+
if (done) break;
|
|
257
|
+
received += value.length;
|
|
258
|
+
if (received >= sampleBytes) {
|
|
259
|
+
controller.abort();
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
// ignore
|
|
265
|
+
} finally {
|
|
266
|
+
clearTimeout(timer);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const seconds = (Date.now() - start) / 1000;
|
|
270
|
+
if (seconds <= 0 || received <= 0) return 0;
|
|
271
|
+
return received / seconds;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function estimateInstallEta(pkgName) {
|
|
275
|
+
try {
|
|
276
|
+
const { tarball, size: distSize } = getNpmDistInfo(pkgName);
|
|
277
|
+
if (!tarball) return null;
|
|
278
|
+
|
|
279
|
+
let size = distSize;
|
|
280
|
+
if (!size) {
|
|
281
|
+
size = await fetchContentLength(tarball);
|
|
282
|
+
}
|
|
283
|
+
if (!size) return null;
|
|
284
|
+
|
|
285
|
+
const speed = await measureDownloadSpeed(tarball);
|
|
286
|
+
if (!speed) return null;
|
|
287
|
+
|
|
288
|
+
const estimate = Math.round((size * ETA_FACTOR) / speed + ETA_OVERHEAD);
|
|
289
|
+
return Math.max(5, estimate);
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
173
295
|
// 检查 Node.js 版本
|
|
174
296
|
function checkNodeVersion() {
|
|
175
297
|
const version = process.version;
|
|
@@ -236,10 +358,11 @@ async function checkAndUpdate(cliName) {
|
|
|
236
358
|
|
|
237
359
|
// 更新到最新版本
|
|
238
360
|
log.info('正在更新到最新版本...');
|
|
361
|
+
const etaSeconds = await estimateInstallEta(cliName);
|
|
239
362
|
const updateResult = await execWithProgress(
|
|
240
363
|
`npm install -g ${cliName}@latest`,
|
|
241
364
|
`正在更新 ${cliName}...`,
|
|
242
|
-
{ timeout: 300000 }
|
|
365
|
+
{ timeout: 300000, etaSeconds }
|
|
243
366
|
);
|
|
244
367
|
|
|
245
368
|
if (updateResult.ok) {
|
|
@@ -388,10 +511,15 @@ function execWithProgress(cmd, message, options = {}) {
|
|
|
388
511
|
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
389
512
|
let frameIndex = 0;
|
|
390
513
|
let seconds = 0;
|
|
514
|
+
const { etaSeconds, ...spawnOptions } = options;
|
|
515
|
+
const totalSeconds = Number.isFinite(etaSeconds) ? Math.max(1, Math.floor(etaSeconds)) : null;
|
|
391
516
|
|
|
392
517
|
// 显示进度动画
|
|
393
518
|
const spinner = setInterval(() => {
|
|
394
|
-
|
|
519
|
+
const remaining = totalSeconds !== null ? Math.max(0, totalSeconds - seconds) : null;
|
|
520
|
+
const etaText =
|
|
521
|
+
totalSeconds !== null ? colors.gray(`(剩余约${remaining}s)`) : colors.gray(`(${seconds}s)`);
|
|
522
|
+
process.stdout.write(`\r${colors.cyan(frames[frameIndex])} ${message} ${etaText}`);
|
|
395
523
|
frameIndex = (frameIndex + 1) % frames.length;
|
|
396
524
|
}, 100);
|
|
397
525
|
|
|
@@ -403,7 +531,7 @@ function execWithProgress(cmd, message, options = {}) {
|
|
|
403
531
|
// 执行命令
|
|
404
532
|
const child = spawn('sh', ['-c', cmd], {
|
|
405
533
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
406
|
-
...
|
|
534
|
+
...spawnOptions,
|
|
407
535
|
});
|
|
408
536
|
|
|
409
537
|
let stdout = '';
|
|
@@ -444,19 +572,21 @@ async function installOpenClaw() {
|
|
|
444
572
|
log.step(1, '安装 OpenClaw CLI...');
|
|
445
573
|
log.detail('正在从 npm 下载,请稍候...');
|
|
446
574
|
|
|
575
|
+
const etaSeconds = await estimateInstallEta('openclaw');
|
|
447
576
|
const result = await execWithProgress(
|
|
448
577
|
'npm install -g openclaw@latest',
|
|
449
578
|
'正在安装 openclaw...',
|
|
450
|
-
{ timeout: 300000 }
|
|
579
|
+
{ timeout: 300000, etaSeconds }
|
|
451
580
|
);
|
|
452
581
|
|
|
453
582
|
if (!result.ok) {
|
|
454
583
|
// 尝试 clawdbot 作为备选
|
|
455
584
|
log.warn('openclaw 安装失败,尝试 clawdbot...');
|
|
585
|
+
const fallbackEta = await estimateInstallEta('clawdbot');
|
|
456
586
|
const fallback = await execWithProgress(
|
|
457
587
|
'npm install -g clawdbot@latest',
|
|
458
588
|
'正在安装 clawdbot...',
|
|
459
|
-
{ timeout: 300000 }
|
|
589
|
+
{ timeout: 300000, etaSeconds: fallbackEta }
|
|
460
590
|
);
|
|
461
591
|
if (!fallback.ok) {
|
|
462
592
|
exitWithError('NPM_INSTALL_FAILED', result.error || result.stderr, [
|
|
@@ -707,7 +837,7 @@ function startService(cliName) {
|
|
|
707
837
|
}
|
|
708
838
|
|
|
709
839
|
// 显示安装完成信息
|
|
710
|
-
function showCompletionInfo(cliName, config, isVPS) {
|
|
840
|
+
function showCompletionInfo(cliName, config, isVPS, serverIP = '') {
|
|
711
841
|
console.log(colors.bold(colors.green('\n========================================')));
|
|
712
842
|
console.log(colors.bold(colors.green('✅ OpenClaw 安装完成!')));
|
|
713
843
|
console.log(colors.bold(colors.green('========================================')));
|
|
@@ -719,12 +849,13 @@ function showCompletionInfo(cliName, config, isVPS) {
|
|
|
719
849
|
|
|
720
850
|
// 根据环境显示不同的访问说明
|
|
721
851
|
if (isVPS) {
|
|
852
|
+
const ipDisplay = serverIP || '<服务器IP>';
|
|
722
853
|
console.log(colors.cyan('\n📡 Dashboard 访问 (云服务器):'));
|
|
723
854
|
console.log(colors.gray(' Gateway 默认绑定 127.0.0.1,外部无法直接访问(安全设计)'));
|
|
724
855
|
console.log('');
|
|
725
856
|
console.log(colors.yellow(' 方式一:SSH 隧道(推荐,安全)'));
|
|
726
857
|
console.log(colors.gray(' 在本地电脑执行以下命令,保持终端窗口打开:'));
|
|
727
|
-
console.log(` ssh -N -L ${config.port}:127.0.0.1:${config.port} root
|
|
858
|
+
console.log(` ssh -N -L ${config.port}:127.0.0.1:${config.port} root@${ipDisplay}`);
|
|
728
859
|
console.log(colors.gray(' 然后在本地浏览器访问:'));
|
|
729
860
|
console.log(` ${colors.green(`http://127.0.0.1:${config.port}/?token=${config.token}`)}`);
|
|
730
861
|
console.log('');
|
|
@@ -735,7 +866,7 @@ function showCompletionInfo(cliName, config, isVPS) {
|
|
|
735
866
|
console.log(colors.gray(' - 腾讯云:安全组 → 入站规则 → 添加 TCP ' + config.port));
|
|
736
867
|
console.log(colors.gray(' - 阿里云:安全组 → 配置规则 → 添加 TCP ' + config.port));
|
|
737
868
|
console.log(colors.gray(' 3. 重启 Gateway:' + cliName + ' gateway restart'));
|
|
738
|
-
console.log(colors.gray(' 4. 访问:http
|
|
869
|
+
console.log(colors.gray(' 4. 访问:http://' + ipDisplay + ':' + config.port + '/?token=...'));
|
|
739
870
|
} else {
|
|
740
871
|
console.log(colors.cyan('\nDashboard 访问:'));
|
|
741
872
|
console.log(` ${colors.yellow(`http://127.0.0.1:${config.port}/?token=${config.token}`)}`);
|
|
@@ -808,8 +939,10 @@ async function main() {
|
|
|
808
939
|
|
|
809
940
|
// 检测是否在 VPS 上
|
|
810
941
|
const isVPS = detectVPS();
|
|
942
|
+
const serverIP = isVPS ? await getPublicIP() : '';
|
|
811
943
|
const port = existing.port || 18789;
|
|
812
944
|
const token = existing.token || '<你的token>';
|
|
945
|
+
const ipDisplay = serverIP || '<服务器IP>';
|
|
813
946
|
|
|
814
947
|
// 显示访问说明
|
|
815
948
|
if (isVPS) {
|
|
@@ -818,7 +951,7 @@ async function main() {
|
|
|
818
951
|
console.log('');
|
|
819
952
|
console.log(colors.yellow(' 方式一:SSH 隧道(推荐,安全)'));
|
|
820
953
|
console.log(colors.gray(' 在本地电脑执行以下命令,保持终端窗口打开:'));
|
|
821
|
-
console.log(` ssh -N -L ${port}:127.0.0.1:${port} root
|
|
954
|
+
console.log(` ssh -N -L ${port}:127.0.0.1:${port} root@${ipDisplay}`);
|
|
822
955
|
console.log(colors.gray(' 然后在本地浏览器访问:'));
|
|
823
956
|
console.log(` ${colors.green(`http://127.0.0.1:${port}/?token=${token}`)}`);
|
|
824
957
|
console.log('');
|
|
@@ -829,7 +962,7 @@ async function main() {
|
|
|
829
962
|
console.log(colors.gray(' - 腾讯云:安全组 → 入站规则 → 添加 TCP ' + port));
|
|
830
963
|
console.log(colors.gray(' - 阿里云:安全组 → 配置规则 → 添加 TCP ' + port));
|
|
831
964
|
console.log(colors.gray(' 3. 重启 Gateway:' + existing.name + ' gateway restart'));
|
|
832
|
-
console.log(colors.gray(' 4. 访问:http
|
|
965
|
+
console.log(colors.gray(' 4. 访问:http://' + ipDisplay + ':' + port + '/?token=...'));
|
|
833
966
|
} else {
|
|
834
967
|
console.log(colors.cyan('\nDashboard 访问:'));
|
|
835
968
|
console.log(` ${colors.yellow(`http://127.0.0.1:${port}/?token=${token}`)}`);
|
|
@@ -857,7 +990,7 @@ async function main() {
|
|
|
857
990
|
|
|
858
991
|
if (options.reinstall) {
|
|
859
992
|
const result = await uninstallAndReinstall(existing, options);
|
|
860
|
-
showCompletionInfo(result.cliName, result.config, isVPS);
|
|
993
|
+
showCompletionInfo(result.cliName, result.config, isVPS, serverIP);
|
|
861
994
|
process.exit(0);
|
|
862
995
|
}
|
|
863
996
|
|
|
@@ -873,7 +1006,7 @@ async function main() {
|
|
|
873
1006
|
process.exit(0);
|
|
874
1007
|
} else if (choice === 'reinstall') {
|
|
875
1008
|
const result = await uninstallAndReinstall(existing, options);
|
|
876
|
-
showCompletionInfo(result.cliName, result.config, isVPS);
|
|
1009
|
+
showCompletionInfo(result.cliName, result.config, isVPS, serverIP);
|
|
877
1010
|
process.exit(0);
|
|
878
1011
|
} else {
|
|
879
1012
|
console.log(colors.gray('\n已退出'));
|
|
@@ -903,7 +1036,8 @@ async function main() {
|
|
|
903
1036
|
|
|
904
1037
|
// 检测是否在 VPS 上并显示完成信息
|
|
905
1038
|
const isVPS = detectVPS();
|
|
906
|
-
|
|
1039
|
+
const serverIP = isVPS ? await getPublicIP() : '';
|
|
1040
|
+
showCompletionInfo(cliName, config, isVPS, serverIP);
|
|
907
1041
|
}
|
|
908
1042
|
|
|
909
1043
|
main().catch((e) => {
|
package/install.sh
CHANGED
|
@@ -16,6 +16,115 @@ log_success() { echo -e "${GREEN}✓ $1${NC}"; }
|
|
|
16
16
|
log_warn() { echo -e "${YELLOW}⚠ $1${NC}"; }
|
|
17
17
|
log_error() { echo -e "${RED}✗ $1${NC}"; }
|
|
18
18
|
|
|
19
|
+
run_with_progress() {
|
|
20
|
+
local cmd="$1"
|
|
21
|
+
local message="$2"
|
|
22
|
+
local eta_total="$3"
|
|
23
|
+
local tty="/dev/tty"
|
|
24
|
+
local tmp=""
|
|
25
|
+
|
|
26
|
+
tmp=$(mktemp 2>/dev/null || mktemp -t openclawsetup 2>/dev/null || echo "/tmp/openclawsetup.$$")
|
|
27
|
+
|
|
28
|
+
local frames=('|' '/' '-' '\\')
|
|
29
|
+
local i=0
|
|
30
|
+
local start
|
|
31
|
+
start=$(date +%s)
|
|
32
|
+
|
|
33
|
+
bash -c "$cmd" >"$tmp" 2>&1 &
|
|
34
|
+
local pid=$!
|
|
35
|
+
|
|
36
|
+
while kill -0 "$pid" 2>/dev/null; do
|
|
37
|
+
local now
|
|
38
|
+
now=$(date +%s)
|
|
39
|
+
local seconds=$((now - start))
|
|
40
|
+
local frame="${frames[$i]}"
|
|
41
|
+
local suffix=""
|
|
42
|
+
|
|
43
|
+
if [[ "$eta_total" =~ ^[0-9]+$ ]] && [ "$eta_total" -gt 0 ]; then
|
|
44
|
+
local remaining=$((eta_total - seconds))
|
|
45
|
+
if [ "$remaining" -lt 0 ]; then
|
|
46
|
+
remaining=0
|
|
47
|
+
fi
|
|
48
|
+
suffix="(剩余约${remaining}s)"
|
|
49
|
+
else
|
|
50
|
+
suffix="(${seconds}s)"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
if [ -w "$tty" ]; then
|
|
54
|
+
printf "\r%s %s %s" "$frame" "$message" "$suffix" > "$tty"
|
|
55
|
+
else
|
|
56
|
+
printf "\r%s %s %s" "$frame" "$message" "$suffix" >&2
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
i=$(( (i + 1) % 4 ))
|
|
60
|
+
sleep 0.1
|
|
61
|
+
done
|
|
62
|
+
|
|
63
|
+
local status
|
|
64
|
+
set +e
|
|
65
|
+
wait "$pid"
|
|
66
|
+
status=$?
|
|
67
|
+
set -e
|
|
68
|
+
|
|
69
|
+
if [ -w "$tty" ]; then
|
|
70
|
+
printf "\r%s\r" "$(printf '%*s' 80 '')" > "$tty"
|
|
71
|
+
else
|
|
72
|
+
printf "\r%s\r" "$(printf '%*s' 80 '')" >&2
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
if [ $status -ne 0 ]; then
|
|
76
|
+
log_error "执行失败: $cmd"
|
|
77
|
+
if [ -s "$tmp" ]; then
|
|
78
|
+
echo -e "${YELLOW}---- 错误输出 (最后 20 行) ----${NC}" >&2
|
|
79
|
+
tail -n 20 "$tmp" >&2
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
rm -f "$tmp"
|
|
84
|
+
return $status
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
estimate_npm_eta() {
|
|
88
|
+
local pkg="$1"
|
|
89
|
+
local tarball=""
|
|
90
|
+
local size=""
|
|
91
|
+
local speed=""
|
|
92
|
+
local eta=""
|
|
93
|
+
local factor="2.0"
|
|
94
|
+
local overhead="8"
|
|
95
|
+
|
|
96
|
+
tarball=$(npm view "$pkg" dist.tarball 2>/dev/null | tail -n 1 || true)
|
|
97
|
+
size=$(npm view "$pkg" dist.size 2>/dev/null | tail -n 1 | tr -d '\r' || true)
|
|
98
|
+
|
|
99
|
+
if ! [[ "$size" =~ ^[0-9]+$ ]]; then
|
|
100
|
+
if [ -n "$tarball" ]; then
|
|
101
|
+
size=$(curl -sIL --connect-timeout 2 --max-time 4 "$tarball" 2>/dev/null \
|
|
102
|
+
| awk 'tolower($1)=="content-length:" {print $2}' \
|
|
103
|
+
| tail -n 1 | tr -d '\r' || true)
|
|
104
|
+
fi
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
if ! [[ "$size" =~ ^[0-9]+$ ]]; then
|
|
108
|
+
return 1
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
if [ -z "$tarball" ]; then
|
|
112
|
+
return 1
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
speed=$(curl -L --range 0-262143 -o /dev/null -s -w '%{speed_download}' \
|
|
116
|
+
--connect-timeout 2 --max-time 6 "$tarball" || true)
|
|
117
|
+
speed=${speed%.*}
|
|
118
|
+
|
|
119
|
+
if ! [[ "$speed" =~ ^[0-9]+$ ]] || [ "$speed" -le 0 ]; then
|
|
120
|
+
return 1
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
eta=$(awk -v size="$size" -v speed="$speed" -v factor="$factor" -v overhead="$overhead" \
|
|
124
|
+
'BEGIN { est=(size*factor)/speed + overhead; if (est<5) est=5; printf "%d", est }')
|
|
125
|
+
echo "$eta"
|
|
126
|
+
}
|
|
127
|
+
|
|
19
128
|
echo ""
|
|
20
129
|
echo -e "${CYAN}🦞 OpenClaw 一键安装${NC}"
|
|
21
130
|
echo ""
|
|
@@ -267,23 +376,54 @@ show_installed_info() {
|
|
|
267
376
|
|
|
268
377
|
# 显示菜单并获取选择
|
|
269
378
|
show_menu() {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
read -
|
|
379
|
+
local choice=""
|
|
380
|
+
local tty="/dev/tty"
|
|
381
|
+
|
|
382
|
+
# 菜单输出到交互终端,避免被 $() 捕获或被管道吞掉
|
|
383
|
+
if [ -w "$tty" ]; then
|
|
384
|
+
printf "%b\n" "${CYAN}\n请选择操作:${NC}" > "$tty"
|
|
385
|
+
printf " %b1%b. 检查并更新 OpenClaw\n" "${YELLOW}" "${NC}" > "$tty"
|
|
386
|
+
printf " %b2%b. 卸载后重新安装(会清除配置)\n" "${YELLOW}" "${NC}" > "$tty"
|
|
387
|
+
printf " %b0%b. 退出\n\n" "${YELLOW}" "${NC}" > "$tty"
|
|
388
|
+
printf "请输入选项编号: " > "$tty"
|
|
389
|
+
read -r choice < "$tty" || true
|
|
281
390
|
else
|
|
282
|
-
#
|
|
391
|
+
# 无交互终端时,输出到 stderr 并尝试从 stdin 读取
|
|
392
|
+
echo -e "${CYAN}\n请选择操作:${NC}" >&2
|
|
393
|
+
echo -e " ${YELLOW}1${NC}. 检查并更新 OpenClaw" >&2
|
|
394
|
+
echo -e " ${YELLOW}2${NC}. 卸载后重新安装(会清除配置)" >&2
|
|
395
|
+
echo -e " ${YELLOW}0${NC}. 退出" >&2
|
|
396
|
+
echo "" >&2
|
|
283
397
|
echo -n "请输入选项编号: " >&2
|
|
284
|
-
read choice
|
|
398
|
+
read -r choice || true
|
|
285
399
|
fi
|
|
286
|
-
|
|
400
|
+
|
|
401
|
+
MENU_CHOICE="$choice"
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# 已安装场景:展示信息 + 菜单
|
|
405
|
+
installed_flow() {
|
|
406
|
+
while true; do
|
|
407
|
+
show_installed_info
|
|
408
|
+
show_menu
|
|
409
|
+
|
|
410
|
+
case "$MENU_CHOICE" in
|
|
411
|
+
1)
|
|
412
|
+
update_openclaw
|
|
413
|
+
;;
|
|
414
|
+
2)
|
|
415
|
+
uninstall_openclaw
|
|
416
|
+
echo ""
|
|
417
|
+
echo -e "${GREEN}✓ 卸载完成,开始重新安装...${NC}"
|
|
418
|
+
echo ""
|
|
419
|
+
install_openclaw
|
|
420
|
+
;;
|
|
421
|
+
*)
|
|
422
|
+
echo -e "\033[90m\n已退出\033[0m"
|
|
423
|
+
return 0
|
|
424
|
+
;;
|
|
425
|
+
esac
|
|
426
|
+
done
|
|
287
427
|
}
|
|
288
428
|
|
|
289
429
|
# 更新 OpenClaw
|
|
@@ -296,7 +436,13 @@ update_openclaw() {
|
|
|
296
436
|
echo " 当前版本: $current_version"
|
|
297
437
|
|
|
298
438
|
log_info "正在更新到最新版本..."
|
|
299
|
-
|
|
439
|
+
local eta=""
|
|
440
|
+
eta=$(estimate_npm_eta "${cli_name}@latest" || true)
|
|
441
|
+
if ! run_with_progress "npm install -g ${cli_name}@latest" "正在更新 ${cli_name}..." "$eta"; then
|
|
442
|
+
log_error "更新失败"
|
|
443
|
+
log_warn "可手动更新: npm install -g ${cli_name}@latest"
|
|
444
|
+
return 1
|
|
445
|
+
fi
|
|
300
446
|
|
|
301
447
|
local new_version=$($cli_name --version 2>/dev/null || echo "未知")
|
|
302
448
|
if [ "$new_version" != "$current_version" ]; then
|
|
@@ -325,7 +471,9 @@ uninstall_openclaw() {
|
|
|
325
471
|
|
|
326
472
|
# 卸载 npm 包
|
|
327
473
|
log_info "卸载 npm 包..."
|
|
328
|
-
npm uninstall -g $cli_name
|
|
474
|
+
if ! run_with_progress "npm uninstall -g $cli_name" "正在卸载 ${cli_name}..."; then
|
|
475
|
+
log_warn "npm 卸载可能未完成"
|
|
476
|
+
fi
|
|
329
477
|
log_success "npm 包已卸载"
|
|
330
478
|
|
|
331
479
|
# 删除配置目录
|
|
@@ -394,26 +542,7 @@ main() {
|
|
|
394
542
|
# 3. 检测 OpenClaw 安装状态
|
|
395
543
|
if check_openclaw; then
|
|
396
544
|
# 已安装:显示信息和菜单
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
choice=$(show_menu)
|
|
400
|
-
|
|
401
|
-
case "$choice" in
|
|
402
|
-
1)
|
|
403
|
-
update_openclaw
|
|
404
|
-
;;
|
|
405
|
-
2)
|
|
406
|
-
uninstall_openclaw
|
|
407
|
-
echo ""
|
|
408
|
-
echo -e "${GREEN}✓ 卸载完成,开始重新安装...${NC}"
|
|
409
|
-
echo ""
|
|
410
|
-
install_openclaw
|
|
411
|
-
;;
|
|
412
|
-
*)
|
|
413
|
-
echo -e "\033[90m\n已退出\033[0m"
|
|
414
|
-
exit 0
|
|
415
|
-
;;
|
|
416
|
-
esac
|
|
545
|
+
installed_flow
|
|
417
546
|
else
|
|
418
547
|
# 未安装:直接安装
|
|
419
548
|
install_openclaw
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclawsetup",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "一键安装 OpenClaw - 自动完成基础部署,无需交互",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"openclawsetup": "
|
|
7
|
+
"openclawsetup": "bin/cli.mjs"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/cli.mjs"
|
|
@@ -36,6 +36,9 @@ $env:OPENCLAW_PORT=8080; npx --yes openclawsetup@latest
|
|
|
36
36
|
set OPENCLAW_PORT=8080 && npx --yes openclawsetup@latest
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
### 安装过程说明
|
|
40
|
+
安装/更新/卸载重装过程中会显示进度与预计剩余时间(基于网络测速,可能有误差)。
|
|
41
|
+
|
|
39
42
|
### 已安装用户:更新或重装
|
|
40
43
|
|
|
41
44
|
```bash
|
|
@@ -58,6 +61,7 @@ npx --yes openclawsetup@latest --reinstall
|
|
|
58
61
|
http://127.0.0.1:18789/?token=<你的token>
|
|
59
62
|
```
|
|
60
63
|
Token 在安装完成时会显示。
|
|
64
|
+
若为云服务器,会自动检测公网 IP 并展示 SSH 隧道命令;未显示时请手动替换 `<服务器IP>`。
|
|
61
65
|
|
|
62
66
|
3. **配置聊天渠道(可选)**
|
|
63
67
|
```bash
|
|
@@ -72,7 +76,7 @@ npx --yes openclawsetup@latest --reinstall
|
|
|
72
76
|
**原因**:未安装 Node.js
|
|
73
77
|
**解决**:使用一键脚本安装(会自动安装 Node.js)
|
|
74
78
|
```bash
|
|
75
|
-
curl -fsSL https://unpkg.com/openclawsetup/install.sh | bash
|
|
79
|
+
curl -fsSL https://unpkg.com/openclawsetup@latest/install.sh | bash
|
|
76
80
|
```
|
|
77
81
|
|
|
78
82
|
---
|