openclawsetup 1.0.13 → 1.0.15
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 +147 -13
- package/install.sh +166 -36
- package/package.json +1 -1
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,22 +376,54 @@ show_installed_info() {
|
|
|
267
376
|
|
|
268
377
|
# 显示菜单并获取选择
|
|
269
378
|
show_menu() {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
280
390
|
else
|
|
281
|
-
#
|
|
282
|
-
echo -
|
|
283
|
-
|
|
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
|
|
397
|
+
echo -n "请输入选项编号: " >&2
|
|
398
|
+
read -r choice || true
|
|
284
399
|
fi
|
|
285
|
-
|
|
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
|
|
286
427
|
}
|
|
287
428
|
|
|
288
429
|
# 更新 OpenClaw
|
|
@@ -295,7 +436,13 @@ update_openclaw() {
|
|
|
295
436
|
echo " 当前版本: $current_version"
|
|
296
437
|
|
|
297
438
|
log_info "正在更新到最新版本..."
|
|
298
|
-
|
|
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
|
|
299
446
|
|
|
300
447
|
local new_version=$($cli_name --version 2>/dev/null || echo "未知")
|
|
301
448
|
if [ "$new_version" != "$current_version" ]; then
|
|
@@ -324,7 +471,9 @@ uninstall_openclaw() {
|
|
|
324
471
|
|
|
325
472
|
# 卸载 npm 包
|
|
326
473
|
log_info "卸载 npm 包..."
|
|
327
|
-
npm uninstall -g $cli_name
|
|
474
|
+
if ! run_with_progress "npm uninstall -g $cli_name" "正在卸载 ${cli_name}..."; then
|
|
475
|
+
log_warn "npm 卸载可能未完成"
|
|
476
|
+
fi
|
|
328
477
|
log_success "npm 包已卸载"
|
|
329
478
|
|
|
330
479
|
# 删除配置目录
|
|
@@ -393,26 +542,7 @@ main() {
|
|
|
393
542
|
# 3. 检测 OpenClaw 安装状态
|
|
394
543
|
if check_openclaw; then
|
|
395
544
|
# 已安装:显示信息和菜单
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
choice=$(show_menu)
|
|
399
|
-
|
|
400
|
-
case "$choice" in
|
|
401
|
-
1)
|
|
402
|
-
update_openclaw
|
|
403
|
-
;;
|
|
404
|
-
2)
|
|
405
|
-
uninstall_openclaw
|
|
406
|
-
echo ""
|
|
407
|
-
echo -e "${GREEN}✓ 卸载完成,开始重新安装...${NC}"
|
|
408
|
-
echo ""
|
|
409
|
-
install_openclaw
|
|
410
|
-
;;
|
|
411
|
-
*)
|
|
412
|
-
echo -e "\033[90m\n已退出\033[0m"
|
|
413
|
-
exit 0
|
|
414
|
-
;;
|
|
415
|
-
esac
|
|
545
|
+
installed_flow
|
|
416
546
|
else
|
|
417
547
|
# 未安装:直接安装
|
|
418
548
|
install_openclaw
|