openclawsetup 2.1.1 → 2.1.3

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
@@ -72,6 +72,7 @@ npx openclawsetup@latest --auto
72
72
  | `--with-channel` | 检测到渠道配置时暂停自动选择 |
73
73
  | `--update` | 检查并更新已安装的 OpenClaw |
74
74
  | `--reinstall` | 卸载后重新安装(清除配置) |
75
+ | `--uninstall` | 卸载 OpenClaw |
75
76
  | `--help, -h` | 显示帮助信息 |
76
77
 
77
78
  ## 使用示例
@@ -100,6 +101,9 @@ npx openclawsetup@latest --update
100
101
 
101
102
  # 卸载后重新安装(会清除配置)
102
103
  npx openclawsetup@latest --reinstall
104
+
105
+ # 直接卸载
106
+ npx openclawsetup@latest --uninstall
103
107
  ```
104
108
 
105
109
  ## 安装后
@@ -114,6 +118,12 @@ npx openclawapi@latest preset-claude
114
118
  npx openclawapi@latest
115
119
  ```
116
120
 
121
+ ## Dashboard 访问
122
+
123
+ 安装完成后会自动显示:
124
+ - 本机访问地址(含 token)
125
+ - 云服务器场景下的 SSH 隧道命令与说明
126
+
117
127
  ## 配置聊天渠道
118
128
 
119
129
  ```bash
@@ -143,11 +153,11 @@ openclaw doctor
143
153
  ## 工作原理
144
154
 
145
155
  1. 安装 `openclaw` npm 包
146
- 2. 使用 `node-pty` 创建跨平台伪终端(Windows 使用 ConPTY)
147
- 3. 监听输出,识别交互提示后自动发送预设答案
148
- 4. 用户看到完整的原版界面 + 自动选择过程
156
+ 2. 优先检测官方 `openclaw onboard` 的非交互参数(可用时直接自动完成)
157
+ 3. 如不支持,则使用 `node-pty` 创建伪终端进行自动应答(按任意键接管)
158
+ 4. 若自动应答不可用,退回手动模式
149
159
 
150
- ## 自动应答规则
160
+ ## 自动应答规则(兜底)
151
161
 
152
162
  | 提示类型 | 自动选择 |
153
163
  |---------|---------|
package/bin/cli.mjs CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { execSync, spawnSync } from 'child_process';
14
- import { existsSync, accessSync, constants as fsConstants, rmSync } from 'fs';
14
+ import { existsSync, accessSync, constants as fsConstants, rmSync, readFileSync } from 'fs';
15
15
  import { homedir, platform } from 'os';
16
16
  import { join } from 'path';
17
17
  import { createInterface } from 'readline';
@@ -51,6 +51,7 @@ function parseArgs() {
51
51
  return {
52
52
  update: args.includes('--update'),
53
53
  reinstall: args.includes('--reinstall'),
54
+ uninstall: args.includes('--uninstall'),
54
55
  manual: args.includes('--manual'),
55
56
  auto: args.includes('--auto'),
56
57
  withModel: args.includes('--with-model'),
@@ -67,6 +68,7 @@ ${colors.cyan('用法:')}
67
68
  npx openclawsetup 带中文指引的安装
68
69
  npx openclawsetup --update 更新已安装的 OpenClaw
69
70
  npx openclawsetup --reinstall 卸载后重新安装
71
+ npx openclawsetup --uninstall 卸载 OpenClaw
70
72
  npx openclawsetup --manual 完全手动模式
71
73
  npx openclawsetup --auto 强制自动模式
72
74
 
@@ -139,6 +141,65 @@ function detectExistingInstall() {
139
141
  return { installed: false };
140
142
  }
141
143
 
144
+ function getConfigInfo() {
145
+ const home = homedir();
146
+ const configs = [
147
+ { dir: join(home, '.openclaw'), file: 'openclaw.json' },
148
+ { dir: join(home, '.clawdbot'), file: 'clawdbot.json' },
149
+ ];
150
+
151
+ for (const cfg of configs) {
152
+ const configPath = join(cfg.dir, cfg.file);
153
+ if (!existsSync(configPath)) continue;
154
+ try {
155
+ const raw = readFileSync(configPath, 'utf8');
156
+ const json = JSON.parse(raw);
157
+ const token = json.token || json.gatewayToken || '';
158
+ const port = Number(json.port || json.gatewayPort || 18789);
159
+ const bind = json.bind || json.gatewayBind || '';
160
+ return { configDir: cfg.dir, configPath, token, port, bind, raw };
161
+ } catch {
162
+ try {
163
+ const raw = readFileSync(configPath, 'utf8');
164
+ const tokenMatch = raw.match(/"token"\\s*:\\s*"([^"]+)"/i);
165
+ const portMatch = raw.match(/"port"\\s*:\\s*(\\d+)/i);
166
+ const bindMatch = raw.match(/"bind"\\s*:\\s*"([^"]+)"/i);
167
+ return {
168
+ configDir: cfg.dir,
169
+ configPath,
170
+ token: tokenMatch ? tokenMatch[1] : '',
171
+ port: portMatch ? Number(portMatch[1]) : 18789,
172
+ bind: bindMatch ? bindMatch[1] : '',
173
+ raw,
174
+ };
175
+ } catch {
176
+ return { configDir: cfg.dir, configPath, token: '', port: 18789, bind: '', raw: '' };
177
+ }
178
+ }
179
+ }
180
+ return { configDir: '', configPath: '', token: '', port: 18789, bind: '', raw: '' };
181
+ }
182
+
183
+ function detectVps() {
184
+ return existsSync('/etc/cloud') || existsSync('/var/lib/cloud') || existsSync('/sys/hypervisor');
185
+ }
186
+
187
+ function getServerIp() {
188
+ const candidates = [
189
+ 'curl -s --connect-timeout 2 ifconfig.me',
190
+ 'curl -s --connect-timeout 2 icanhazip.com',
191
+ 'curl -s --connect-timeout 2 ipinfo.io/ip',
192
+ "hostname -I 2>/dev/null | awk '{print $1}'",
193
+ ];
194
+ for (const cmd of candidates) {
195
+ const result = safeExec(cmd);
196
+ if (result.ok && result.output) {
197
+ return result.output.trim();
198
+ }
199
+ }
200
+ return '';
201
+ }
202
+
142
203
  // ============ 安装指引 ============
143
204
 
144
205
  function showInstallGuide() {
@@ -184,11 +245,82 @@ function stripAnsi(input) {
184
245
  .replace(/\r/g, '\n');
185
246
  }
186
247
 
187
- function getOnboardCommand(cliName) {
248
+ function getOnboardHelp(cliName) {
249
+ const candidates = [
250
+ `${cliName} onboard --help`,
251
+ `${cliName} onboard -h`,
252
+ `${cliName} help onboard`,
253
+ ];
254
+
255
+ for (const cmd of candidates) {
256
+ const result = safeExec(cmd, { stdio: 'pipe' });
257
+ const output = result.ok ? result.output : '';
258
+ if (output && output.length > 20) {
259
+ return output;
260
+ }
261
+ }
262
+ return '';
263
+ }
264
+
265
+ function buildOnboardArgsFromHelp(helpText, options) {
266
+ const help = helpText.toLowerCase();
267
+ const args = [];
268
+ const enabled = [];
269
+
270
+ const pickFlag = (flags) => flags.find((flag) => help.includes(flag));
271
+
272
+ const yesFlag = pickFlag(['--yes', '--assume-yes', '--accept', '--agree']);
273
+ if (yesFlag) {
274
+ args.push(yesFlag);
275
+ enabled.push('yes');
276
+ }
277
+
278
+ const installDaemonFlag = pickFlag(['--install-daemon', '--daemon']);
279
+ if (installDaemonFlag) {
280
+ args.push(installDaemonFlag);
281
+ }
282
+
283
+ if (help.includes('--quickstart')) {
284
+ args.push('--quickstart');
285
+ enabled.push('quickstart');
286
+ } else if (help.includes('--mode') && (help.includes('quickstart') || help.includes('quick start'))) {
287
+ args.push('--mode', 'quickstart');
288
+ enabled.push('quickstart');
289
+ }
290
+
291
+ if (!options.withModel) {
292
+ const skipModelFlag = pickFlag(['--skip-model', '--no-model', '--skip-provider']);
293
+ if (skipModelFlag) {
294
+ args.push(skipModelFlag);
295
+ enabled.push('skip-model');
296
+ }
297
+ }
298
+
299
+ if (!options.withChannel) {
300
+ const skipChannelFlag = pickFlag(['--skip-channel', '--no-channel', '--skip-channels']);
301
+ if (skipChannelFlag) {
302
+ args.push(skipChannelFlag);
303
+ enabled.push('skip-channel');
304
+ }
305
+ }
306
+
307
+ if (help.includes('--ui') && (help.includes('web') || help.includes('dashboard'))) {
308
+ args.push('--ui', 'web');
309
+ enabled.push('ui-web');
310
+ } else if (help.includes('--web')) {
311
+ args.push('--web');
312
+ enabled.push('ui-web');
313
+ }
314
+
315
+ const autoCapable = enabled.length > 0 || Boolean(yesFlag);
316
+ return { args, enabled, autoCapable };
317
+ }
318
+
319
+ function getOnboardCommand(cliName, onboardArgs = ['onboard', '--install-daemon']) {
188
320
  if (platform() === 'win32') {
189
- return { file: 'cmd.exe', args: ['/c', cliName, 'onboard', '--install-daemon'] };
321
+ return { file: 'cmd.exe', args: ['/c', cliName, ...onboardArgs] };
190
322
  }
191
- return { file: cliName, args: ['onboard', '--install-daemon'] };
323
+ return { file: cliName, args: onboardArgs };
192
324
  }
193
325
 
194
326
  function waitForEnter(message) {
@@ -276,21 +408,31 @@ async function runOnboard(cliName) {
276
408
  let usedAuto = false;
277
409
 
278
410
  if (preferAuto) {
279
- const autoResult = await runOnboardAuto(cliName, options);
280
- if (autoResult.ok) {
411
+ const flagResult = runOnboardFlags(cliName, options);
412
+ if (flagResult.ran) {
281
413
  usedAuto = true;
282
414
  console.log(colors.gray('\n' + '-'.repeat(60)));
283
- if (autoResult.exitCode !== 0) {
284
- log.warn(`onboard 退出码: ${autoResult.exitCode}`);
415
+ if (!flagResult.ok) {
416
+ log.warn(`onboard 退出码: ${flagResult.exitCode}`);
285
417
  log.hint('如果配置未完成,可以手动运行: ' + cliName + ' onboard');
286
418
  }
287
- } else if (options.auto) {
288
- log.error('自动模式不可用,已退出');
289
- log.hint(autoResult.reason || '请尝试 --manual');
290
- process.exit(1);
291
419
  } else {
292
- log.warn('自动模式不可用,已切换为手动模式');
293
- log.hint(autoResult.reason || '未能启用自动应答');
420
+ const autoResult = await runOnboardAuto(cliName, options);
421
+ if (autoResult.ok) {
422
+ usedAuto = true;
423
+ console.log(colors.gray('\n' + '-'.repeat(60)));
424
+ if (autoResult.exitCode !== 0) {
425
+ log.warn(`onboard 退出码: ${autoResult.exitCode}`);
426
+ log.hint('如果配置未完成,可以手动运行: ' + cliName + ' onboard');
427
+ }
428
+ } else if (options.auto) {
429
+ log.error('自动模式不可用,已退出');
430
+ log.hint(autoResult.reason || '请尝试 --manual');
431
+ process.exit(1);
432
+ } else {
433
+ log.warn('自动模式不可用,已切换为手动模式');
434
+ log.hint(autoResult.reason || '未能启用自动应答');
435
+ }
294
436
  }
295
437
  }
296
438
 
@@ -311,6 +453,30 @@ function runOnboardManual(cliName) {
311
453
  });
312
454
  }
313
455
 
456
+ function runOnboardFlags(cliName, options) {
457
+ const help = getOnboardHelp(cliName);
458
+ if (!help) {
459
+ return { ran: false, reason: '未检测到 onboard 帮助信息' };
460
+ }
461
+
462
+ const { args, enabled, autoCapable } = buildOnboardArgsFromHelp(help, options);
463
+ if (!autoCapable) {
464
+ return { ran: false, reason: '未检测到可用的非交互参数' };
465
+ }
466
+
467
+ log.info('检测到官方非交互参数,优先使用原生方式安装');
468
+ if (enabled.length) {
469
+ log.hint(`已启用: ${enabled.join(', ')}`);
470
+ }
471
+
472
+ const { file, args: spawnArgs } = getOnboardCommand(cliName, ['onboard', ...args]);
473
+ const result = spawnSync(file, spawnArgs, {
474
+ stdio: 'inherit',
475
+ });
476
+
477
+ return { ran: true, ok: result.status === 0, exitCode: result.status ?? 1 };
478
+ }
479
+
314
480
  function createAutoResponder(term, options) {
315
481
  const lastSent = new Map();
316
482
  let autoStopped = false;
@@ -344,7 +510,11 @@ function createAutoResponder(term, options) {
344
510
  const tail = text.slice(-800);
345
511
 
346
512
  if (tail.includes('do you want to continue') || tail.includes('continue?') || tail.includes('是否继续')) {
347
- send('confirm', 'y\r');
513
+ if (tail.includes('yes') && tail.includes('no')) {
514
+ send('confirm-select', '\x1b[A\x1b[D\r');
515
+ } else {
516
+ send('confirm', 'y\r');
517
+ }
348
518
  return;
349
519
  }
350
520
 
@@ -567,6 +737,8 @@ function showCompletionInfo(cliName) {
567
737
  console.log(colors.cyan('\n下一步 - 配置 AI 模型(必须):'));
568
738
  console.log(` ${colors.yellow('npx openclawapi@latest preset-claude')}`);
569
739
 
740
+ showDashboardAccessInfo();
741
+
570
742
  console.log(colors.cyan('\n常用命令:'));
571
743
  console.log(` 查看状态: ${colors.yellow(`${cliName} status`)}`);
572
744
  console.log(` 查看日志: ${colors.yellow(`${cliName} gateway logs`)}`);
@@ -580,6 +752,78 @@ function showCompletionInfo(cliName) {
580
752
  console.log('');
581
753
  }
582
754
 
755
+ function showDashboardAccessInfo() {
756
+ const config = getConfigInfo();
757
+ const port = config.port || 18789;
758
+ const token = config.token || '<你的token>';
759
+ const dashboardUrl = `http://127.0.0.1:${port}/?token=${token}`;
760
+
761
+ if (detectVps()) {
762
+ const serverIp = getServerIp() || '<服务器IP>';
763
+ const user = process.env.USER || 'root';
764
+
765
+ console.log(colors.cyan('\n📡 Dashboard 访问(云服务器):'));
766
+ console.log(colors.gray(' Gateway 默认绑定 127.0.0.1,外部无法直接访问(安全设计)'));
767
+ console.log('');
768
+ console.log(colors.yellow(' 方式一:SSH 隧道(推荐,安全)'));
769
+ console.log(colors.gray(' 在本地电脑执行以下命令,保持终端窗口打开:'));
770
+ console.log(` ${colors.green(`ssh -N -L ${port}:127.0.0.1:${port} ${user}@${serverIp}`)}`);
771
+ console.log(colors.gray(' 然后在本地浏览器访问:'));
772
+ console.log(` ${colors.green(dashboardUrl)}`);
773
+ console.log('');
774
+ console.log(colors.yellow(' 方式二:直接暴露端口(不推荐,有安全风险)'));
775
+ console.log(colors.gray(' 1. 修改配置文件 ~/.openclaw/openclaw.json'));
776
+ console.log(colors.gray(' 将 "bind": "loopback" 改为 "bind": "all"'));
777
+ console.log(colors.gray(` 2. 在云服务器控制台开放端口 ${port}`));
778
+ console.log(colors.gray(' 3. 重启 Gateway:openclaw gateway restart'));
779
+ console.log(colors.gray(` 4. 访问:http://${serverIp}:${port}/?token=...`));
780
+ } else {
781
+ console.log(colors.cyan('\nDashboard 访问:'));
782
+ console.log(` ${colors.yellow(dashboardUrl)}`);
783
+ }
784
+ }
785
+
786
+ async function uninstallOpenClaw(existing) {
787
+ const cliName = existing?.name || 'openclaw';
788
+ const useSudo = needsSudo();
789
+
790
+ console.log(colors.cyan('\n开始卸载...'));
791
+
792
+ safeExec(`${cliName} gateway stop`);
793
+
794
+ if (useSudo) {
795
+ console.log(colors.yellow('\n请运行以下命令卸载:'));
796
+ console.log(colors.green(` sudo npm uninstall -g ${cliName}`));
797
+ console.log(colors.green(' rm -rf ~/.openclaw ~/.clawdbot\n'));
798
+ await waitForEnter('卸载完成后按回车继续...');
799
+ } else {
800
+ spawnSync('npm', ['uninstall', '-g', cliName], { stdio: 'inherit', shell: true });
801
+ }
802
+
803
+ const config = getConfigInfo();
804
+ if (config.configDir && existsSync(config.configDir)) {
805
+ rmSync(config.configDir, { recursive: true, force: true });
806
+ }
807
+ const otherDir = config.configDir?.endsWith('.openclaw') ? join(homedir(), '.clawdbot') : join(homedir(), '.openclaw');
808
+ if (existsSync(otherDir)) {
809
+ rmSync(otherDir, { recursive: true, force: true });
810
+ }
811
+
812
+ if (platform() === 'darwin') {
813
+ const plist = join(homedir(), 'Library/LaunchAgents/com.openclaw.gateway.plist');
814
+ safeExec(`launchctl unload "${plist}"`);
815
+ if (existsSync(plist)) rmSync(plist, { force: true });
816
+ } else if (platform() === 'linux') {
817
+ safeExec('systemctl --user stop openclaw');
818
+ safeExec('systemctl --user disable openclaw');
819
+ const service = join(homedir(), '.config/systemd/user/openclaw.service');
820
+ if (existsSync(service)) rmSync(service, { force: true });
821
+ safeExec('systemctl --user daemon-reload');
822
+ }
823
+
824
+ log.success('卸载完成');
825
+ }
826
+
583
827
  // ============ 主函数 ============
584
828
 
585
829
  async function main() {
@@ -607,27 +851,20 @@ async function main() {
607
851
  process.exit(0);
608
852
  }
609
853
 
854
+ if (options.uninstall) {
855
+ await uninstallOpenClaw(existing);
856
+ process.exit(0);
857
+ }
858
+
610
859
  if (options.reinstall) {
611
- log.info('\n卸载现有安装...');
612
- safeExec(`${existing.name} gateway stop`);
613
-
614
- if (needsSudo()) {
615
- console.log(colors.yellow('\n请运行以下命令卸载:'));
616
- console.log(colors.green(` sudo npm uninstall -g ${existing.name}`));
617
- console.log(colors.green(` rm -rf ~/.openclaw ~/.clawdbot\n`));
618
- await waitForEnter('卸载完成后按回车继续...');
619
- } else {
620
- spawnSync('npm', ['uninstall', '-g', existing.name], { stdio: 'inherit', shell: true });
621
- if (existing.configDir && existsSync(existing.configDir)) {
622
- rmSync(existing.configDir, { recursive: true, force: true });
623
- }
624
- log.success('卸载完成');
625
- }
860
+ await uninstallOpenClaw(existing);
626
861
  } else {
627
862
  console.log(colors.cyan('\n已安装,可选操作:'));
628
863
  console.log(` 更新: ${colors.yellow('npx openclawsetup --update')}`);
629
864
  console.log(` 重装: ${colors.yellow('npx openclawsetup --reinstall')}`);
865
+ console.log(` 卸载: ${colors.yellow('npx openclawsetup --uninstall')}`);
630
866
  console.log(` 配置模型: ${colors.yellow('npx openclawapi@latest preset-claude')}`);
867
+ showDashboardAccessInfo();
631
868
  console.log('');
632
869
  process.exit(0);
633
870
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclawsetup",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "OpenClaw 安装向导 - 带中文指引的官方安装流程",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,7 @@ npx openclawsetup@latest
11
11
  ```
12
12
 
13
13
  你会看到官方的 `openclaw onboard` 界面,但所有选项会自动选择推荐配置(按任意键可接管)。
14
+ 默认优先使用官方提供的非交互参数,若版本不支持则使用自动应答兜底。
14
15
 
15
16
  ### 方式二:手动模式
16
17
 
@@ -73,6 +74,9 @@ npx openclawsetup@latest --update
73
74
 
74
75
  # 卸载后重新安装(会清除配置)
75
76
  npx openclawsetup@latest --reinstall
77
+
78
+ # 直接卸载
79
+ npx openclawsetup@latest --uninstall
76
80
  ```
77
81
 
78
82
  ## 安装完成后
@@ -83,10 +87,9 @@ npx openclawsetup@latest --reinstall
83
87
  ```
84
88
 
85
89
  2. **访问 Dashboard**
86
- ```
87
- http://127.0.0.1:18789/?token=<你的token>
88
- ```
89
- Token 在安装完成时会显示。
90
+ 安装完成后会自动显示:
91
+ - 本机访问地址(含 token
92
+ - 云服务器场景下的 SSH 隧道命令与说明
90
93
 
91
94
  3. **配置聊天渠道(可选)**
92
95
  ```bash
@@ -154,7 +157,7 @@ curl -fsSL https://unpkg.com/openclawsetup@latest/install.sh | bash
154
157
 
155
158
  ### 自动选择没有生效
156
159
  **现象**:安装过程中需要手动输入
157
- **原因**:可能是终端环境问题或自动应答依赖不可用
160
+ **原因**:可能是终端环境问题、无 TTY,或自动应答依赖不可用(node-pty 未安装成功)
158
161
  **解决**:
159
162
  1. 使用手动模式:
160
163
  ```bash
@@ -164,7 +167,13 @@ curl -fsSL https://unpkg.com/openclawsetup@latest/install.sh | bash
164
167
  ```bash
165
168
  npx openclawsetup@latest --auto
166
169
  ```
167
- 3. 或直接运行官方命令:
170
+ 3. 在 Linux 服务器上补齐编译依赖后再试(Ubuntu 示例):
171
+ ```bash
172
+ sudo apt-get update
173
+ sudo apt-get install -y build-essential python3 make g++ pkg-config
174
+ npx openclawsetup@latest --auto
175
+ ```
176
+ 4. 或直接运行官方命令:
168
177
  ```bash
169
178
  npm install -g openclaw@latest
170
179
  openclaw onboard --install-daemon