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 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
- process.stdout.write(`\r${colors.cyan(frames[frameIndex])} ${message} ${colors.gray(`(${seconds}s)`)}`);
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
- ...options,
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@<服务器IP>`);
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://<服务器IP>:' + config.port + '/?token=...'));
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@<服务器IP>`);
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://<服务器IP>:' + port + '/?token=...'));
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
- showCompletionInfo(cliName, config, isVPS);
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
- # 菜单输出到 stderr,避免被 $() 捕获
271
- echo -e "${CYAN}\n请选择操作:${NC}" >&2
272
- echo -e " ${YELLOW}1${NC}. 检查并更新 OpenClaw" >&2
273
- echo -e " ${YELLOW}2${NC}. 卸载后重新安装(会清除配置)" >&2
274
- echo -e " ${YELLOW}0${NC}. 退出" >&2
275
- echo "" >&2
276
-
277
- # /dev/tty 读取,支持管道环境
278
- if [ -t 0 ]; then
279
- # 标准输入是终端
280
- read -p "请输入选项编号: " choice
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
- # 管道环境,从 /dev/tty 读取
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 < /dev/tty
398
+ read -r choice || true
285
399
  fi
286
- echo "$choice"
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
- npm install -g ${cli_name}@latest
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 2>/dev/null || true
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
- show_installed_info
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.14",
3
+ "version": "1.0.16",
4
4
  "description": "一键安装 OpenClaw - 自动完成基础部署,无需交互",
5
5
  "type": "module",
6
6
  "bin": {
7
- "openclawsetup": "./bin/cli.mjs"
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
  ---