openclawsetup 2.0.3 → 2.1.1

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
@@ -7,6 +7,7 @@ OpenClaw 智能安装向导 - 调用官方 `openclaw onboard` 交互界面,自
7
7
  - **真实体验**:调用官方 `openclaw onboard` 命令,用户看到完整的原版安装界面
8
8
  - **智能自动化**:自动选择推荐配置,无需手动操作
9
9
  - **可观看过程**:用户可以看到每一步的选择过程,了解发生了什么
10
+ - **随时接管**:按任意键立刻切换为手动操作
10
11
  - **手动模式**:`--manual` 参数可切换到完全手动模式
11
12
  - **三系统支持**:macOS、Linux、Windows
12
13
 
@@ -32,13 +33,13 @@ npx openclawsetup@latest
32
33
 
33
34
  ## 安装模式
34
35
 
35
- ### 智能模式(默认)
36
+ ### 智能模式(默认,自动应答)
36
37
 
37
38
  ```bash
38
39
  npx openclawsetup@latest
39
40
  ```
40
41
 
41
- 自动完成以下选择:
42
+ 自动完成以下选择(按任意键可接管):
42
43
  - ✓ 选择 QuickStart 模式
43
44
  - ✓ 跳过模型配置(后续用 `npx openclawapi` 配置)
44
45
  - ✓ 跳过渠道配置(后续用 `npx openclawdc` 或 `npx openclaw-chat-cn@latest feishu` 配置)
@@ -53,12 +54,22 @@ npx openclawsetup@latest --manual
53
54
 
54
55
  完全交互,自己选择所有配置项。
55
56
 
57
+ ### 强制自动模式
58
+
59
+ ```bash
60
+ npx openclawsetup@latest --auto
61
+ ```
62
+
63
+ 自动应答不可用时将直接退出(适合脚本化场景)。
64
+
56
65
  ## 命令行参数
57
66
 
58
67
  | 参数 | 说明 |
59
68
  |------|------|
60
69
  | `--manual` | 手动模式,不自动选择 |
61
- | `--with-model` | 安装时配置模型(默认跳过) |
70
+ | `--auto` | 强制自动模式(不可用则退出) |
71
+ | `--with-model` | 检测到模型配置时暂停自动选择 |
72
+ | `--with-channel` | 检测到渠道配置时暂停自动选择 |
62
73
  | `--update` | 检查并更新已安装的 OpenClaw |
63
74
  | `--reinstall` | 卸载后重新安装(清除配置) |
64
75
  | `--help, -h` | 显示帮助信息 |
@@ -132,7 +143,7 @@ openclaw doctor
132
143
  ## 工作原理
133
144
 
134
145
  1. 安装 `openclaw` npm 包
135
- 2. 使用 `script` 命令(Unix)创建伪终端,运行 `openclaw onboard --install-daemon`
146
+ 2. 使用 `node-pty` 创建跨平台伪终端(Windows 使用 ConPTY)
136
147
  3. 监听输出,识别交互提示后自动发送预设答案
137
148
  4. 用户看到完整的原版界面 + 自动选择过程
138
149
 
@@ -149,6 +160,8 @@ openclaw doctor
149
160
  | Daemon/Service | 安装 (y) |
150
161
  | UI 选择 | Web Dashboard |
151
162
 
163
+ > 使用 `--with-model` / `--with-channel` 时,自动模式会在对应步骤暂停并交给用户操作。
164
+
152
165
  ## 卸载
153
166
 
154
167
  ```bash
package/bin/cli.mjs CHANGED
@@ -1,58 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * OpenClaw 智能安装向导
3
+ * OpenClaw 安装向导
4
4
  *
5
- * 调用官方 openclaw onboard 交互界面,自动完成推荐配置
6
- * 用户可以看到完整的原版安装过程,但无需手动选择
5
+ * 调用官方 openclaw onboard,自动选择推荐配置
6
+ * 用户可随时按任意键接管,切换为手动操作
7
7
  *
8
8
  * 用法:
9
- * npx openclawsetup # 智能安装(自动选择推荐配置)
10
- * npx openclawsetup --manual # 手动模式(完全交互)
9
+ * npx openclawsetup # 带中文指引的安装
11
10
  * npx openclawsetup --update # 更新已安装的 OpenClaw
12
11
  */
13
12
 
14
- import { execSync, spawn } from 'child_process';
15
- import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync, accessSync, constants as fsConstants } from 'fs';
13
+ import { execSync, spawnSync } from 'child_process';
14
+ import { existsSync, accessSync, constants as fsConstants, rmSync } from 'fs';
16
15
  import { homedir, platform } from 'os';
17
16
  import { join } from 'path';
18
17
  import { createInterface } from 'readline';
19
18
 
20
- // ============ 配置 ============
21
-
22
- // 自动应答规则:识别关键词 -> 自动输入
23
- // 顺序很重要:先匹配的先执行
24
- const AUTO_RESPONSES = [
25
- // 安全确认
26
- { match: /continue\?|accept|agree|proceed|\(y\/n\)|\[y\/N\]|\[Y\/n\]/i, response: 'y', delay: 300, desc: '安全确认' },
27
-
28
- // Setup 模式选择 - QuickStart
29
- { match: /quick\s*start|setup\s*mode|choose.*mode/i, response: '1', delay: 500, desc: '选择 QuickStart' },
30
-
31
- // Model Provider - 跳过(用户后续用 openclawapi 配置)
32
- { match: /provider|anthropic|openai|select.*model|choose.*model/i, response: 's', delay: 500, desc: '跳过模型配置' },
33
-
34
- // API Key - 跳过
35
- { match: /api\s*key|enter.*key|paste.*key|setup.*token/i, response: '', delay: 300, desc: '跳过 API Key', skip: true },
36
-
37
- // Channel 配置 - 跳过
38
- { match: /channel|telegram|discord|whatsapp|slack|skip.*channel/i, response: 's', delay: 500, desc: '跳过渠道配置' },
39
-
40
- // Skills - 跳过
41
- { match: /skill|install.*skill|skip.*skill/i, response: 's', delay: 500, desc: '跳过 Skills' },
42
-
43
- // Daemon/Service - 安装
44
- { match: /daemon|service|background|auto.*start|launchd|systemd/i, response: 'y', delay: 300, desc: '安装后台服务' },
45
-
46
- // UI 选择 - Web Dashboard
47
- { match: /interface|dashboard|tui|web.*ui|control.*ui/i, response: '1', delay: 500, desc: '选择 Web Dashboard' },
48
-
49
- // 名称/称呼 - 使用默认
50
- { match: /name|call\s*you|address/i, response: '', delay: 300, desc: '使用默认名称' },
51
-
52
- // 通用确认
53
- { match: /press\s*enter|continue|next|\[enter\]/i, response: '', delay: 200, desc: '继续' },
54
- ];
55
-
56
19
  // ============ 工具函数 ============
57
20
 
58
21
  const colors = {
@@ -62,7 +25,7 @@ const colors = {
62
25
  cyan: (s) => `\x1b[36m${s}\x1b[0m`,
63
26
  bold: (s) => `\x1b[1m${s}\x1b[0m`,
64
27
  gray: (s) => `\x1b[90m${s}\x1b[0m`,
65
- dim: (s) => `\x1b[2m${s}\x1b[0m`,
28
+ bgYellow: (s) => `\x1b[43m\x1b[30m${s}\x1b[0m`,
66
29
  };
67
30
 
68
31
  const log = {
@@ -71,30 +34,9 @@ const log = {
71
34
  warn: (msg) => console.log(colors.yellow(`⚠ ${msg}`)),
72
35
  error: (msg) => console.log(colors.red(`✗ ${msg}`)),
73
36
  hint: (msg) => console.log(colors.gray(` 提示: ${msg}`)),
74
- auto: (msg) => console.log(colors.dim(` [自动] ${msg}`)),
75
- };
76
-
77
- const ERROR_CODES = {
78
- NODE_VERSION: { code: 1, message: 'Node.js 版本过低' },
79
- NPM_INSTALL_FAILED: { code: 3, message: 'npm 安装失败' },
80
- ONBOARD_FAILED: { code: 4, message: 'onboard 执行失败' },
81
- NOT_TTY: { code: 5, message: '需要交互式终端' },
37
+ guide: (msg) => console.log(colors.bgYellow(` 📖 ${msg} `)),
82
38
  };
83
39
 
84
- function exitWithError(errorType, details = '', solutions = []) {
85
- const err = ERROR_CODES[errorType] || { code: 99, message: '未知错误' };
86
- console.log(colors.bold(colors.red('\n========================================')));
87
- console.log(colors.bold(colors.red(`❌ 错误: ${err.message}`)));
88
- console.log(colors.bold(colors.red('========================================')));
89
- if (details) console.log(colors.gray(` ${details}`));
90
- if (solutions.length > 0) {
91
- console.log(colors.cyan('\n解决方案:'));
92
- solutions.forEach((s, i) => console.log(` ${i + 1}. ${s}`));
93
- }
94
- console.log(colors.gray(`\n错误码: ${err.code}`));
95
- process.exit(err.code);
96
- }
97
-
98
40
  function safeExec(cmd, options = {}) {
99
41
  try {
100
42
  const output = execSync(cmd, { encoding: 'utf8', stdio: 'pipe', ...options });
@@ -104,96 +46,62 @@ function safeExec(cmd, options = {}) {
104
46
  }
105
47
  }
106
48
 
107
- function checkNodeVersion() {
108
- const version = process.version;
109
- const major = parseInt(version.slice(1).split('.')[0], 10);
110
- if (major < 18) {
111
- exitWithError('NODE_VERSION', `当前版本: ${version},需要 Node.js 18+`, [
112
- '升级 Node.js: https://nodejs.org/',
113
- '使用 nvm: nvm install 22',
114
- ]);
115
- }
116
- return true;
117
- }
118
-
119
49
  function parseArgs() {
120
50
  const args = process.argv.slice(2);
121
51
  return {
122
- manual: args.includes('--manual'),
123
52
  update: args.includes('--update'),
124
53
  reinstall: args.includes('--reinstall'),
54
+ manual: args.includes('--manual'),
55
+ auto: args.includes('--auto'),
56
+ withModel: args.includes('--with-model'),
57
+ withChannel: args.includes('--with-channel'),
125
58
  help: args.includes('--help') || args.includes('-h'),
126
- skipModel: !args.includes('--with-model'), // 默认跳过模型配置
127
59
  };
128
60
  }
129
61
 
130
62
  function showHelp() {
131
63
  console.log(`
132
- ${colors.bold('OpenClaw 智能安装向导')}
64
+ ${colors.bold('OpenClaw 安装向导')}
133
65
 
134
66
  ${colors.cyan('用法:')}
135
- npx openclawsetup 智能安装(自动选择推荐配置)
136
- npx openclawsetup --manual 手动模式(完全交互,自己选择)
67
+ npx openclawsetup 带中文指引的安装
137
68
  npx openclawsetup --update 更新已安装的 OpenClaw
138
69
  npx openclawsetup --reinstall 卸载后重新安装
70
+ npx openclawsetup --manual 完全手动模式
71
+ npx openclawsetup --auto 强制自动模式
139
72
 
140
- ${colors.cyan('选项:')}
141
- --manual 完全手动模式,不自动选择
142
- --with-model 安装时配置模型(默认跳过,后续用 openclawapi 配置)
143
- --update 检查并更新
144
- --reinstall 重新安装(清除配置)
145
- --help, -h 显示帮助
73
+ ${colors.cyan('说明:')}
74
+ 本工具会调用官方 openclaw onboard 命令
75
+ 自动选择推荐配置,按任意键可随时接管
146
76
 
147
- ${colors.cyan('智能安装会自动:')}
148
- ✓ 选择 QuickStart 模式
149
- ✓ 跳过模型配置(后续用 npx openclawapi 配置)
150
- ✓ 跳过渠道配置(后续用 npx openclawdc 或 npx openclaw-chat-cn@latest feishu 配置)
151
- ✓ 安装后台服务(开机自启)
152
- ✓ 选择 Web Dashboard
77
+ ${colors.cyan('高级选项:')}
78
+ --with-model 检测到模型配置时暂停自动选择
79
+ --with-channel 检测到渠道配置时暂停自动选择
153
80
 
154
81
  ${colors.cyan('安装后配置模型:')}
155
82
  npx openclawapi@latest preset-claude
156
83
  `);
157
84
  }
158
85
 
159
- // ============ 检测已安装 ============
160
-
161
- function detectExistingInstall() {
162
- const home = homedir();
163
- const openclawDir = join(home, '.openclaw');
164
- const clawdbotDir = join(home, '.clawdbot');
165
-
166
- let result = { installed: false };
86
+ // ============ 环境检测 ============
167
87
 
168
- if (existsSync(openclawDir) || existsSync(clawdbotDir)) {
169
- result = {
170
- installed: true,
171
- configDir: existsSync(openclawDir) ? openclawDir : clawdbotDir,
172
- name: existsSync(openclawDir) ? 'openclaw' : 'clawdbot',
173
- };
174
- } else {
175
- const openclawResult = safeExec('openclaw --version');
176
- if (openclawResult.ok) {
177
- result = { installed: true, name: 'openclaw', version: openclawResult.output };
178
- } else {
179
- const clawdbotResult = safeExec('clawdbot --version');
180
- if (clawdbotResult.ok) {
181
- result = { installed: true, name: 'clawdbot', version: clawdbotResult.output };
182
- }
183
- }
88
+ function checkNodeVersion() {
89
+ const version = process.version;
90
+ const major = parseInt(version.slice(1).split('.')[0], 10);
91
+ if (major < 18) {
92
+ log.error(`Node.js 版本过低: ${version},需要 18+`);
93
+ console.log(colors.cyan('\n解决方案:'));
94
+ console.log(' 1. 访问 https://nodejs.org/ 下载最新版本');
95
+ console.log(' 2. 或使用 nvm: nvm install 22');
96
+ process.exit(1);
184
97
  }
185
-
186
- return result;
98
+ return true;
187
99
  }
188
100
 
189
- // ============ 安装 OpenClaw ============
190
-
191
- // 检测是否需要 sudo
192
101
  function needsSudo() {
193
102
  const os = platform();
194
103
  if (os === 'win32' || os === 'darwin') return false;
195
104
 
196
- // Linux: 检查 /usr/lib/node_modules 是否可写
197
105
  try {
198
106
  const testDir = '/usr/lib/node_modules';
199
107
  if (existsSync(testDir)) {
@@ -206,178 +114,408 @@ function needsSudo() {
206
114
  return true;
207
115
  }
208
116
 
209
- // 获取 npm 安装命令
210
- function getNpmInstallCmd(pkg) {
211
- const useSudo = needsSudo();
212
- if (useSudo) {
213
- return { cmd: 'sudo', args: ['npm', 'install', '-g', pkg] };
117
+ function detectExistingInstall() {
118
+ const home = homedir();
119
+ const openclawDir = join(home, '.openclaw');
120
+ const clawdbotDir = join(home, '.clawdbot');
121
+
122
+ if (existsSync(openclawDir)) {
123
+ return { installed: true, configDir: openclawDir, name: 'openclaw' };
124
+ }
125
+ if (existsSync(clawdbotDir)) {
126
+ return { installed: true, configDir: clawdbotDir, name: 'clawdbot' };
127
+ }
128
+
129
+ const openclawResult = safeExec('openclaw --version');
130
+ if (openclawResult.ok) {
131
+ return { installed: true, name: 'openclaw', version: openclawResult.output };
214
132
  }
215
- return { cmd: 'npm', args: ['install', '-g', pkg] };
133
+
134
+ const clawdbotResult = safeExec('clawdbot --version');
135
+ if (clawdbotResult.ok) {
136
+ return { installed: true, name: 'clawdbot', version: clawdbotResult.output };
137
+ }
138
+
139
+ return { installed: false };
216
140
  }
217
141
 
218
- async function installOpenClaw() {
219
- log.info('\n[1/2] 安装 OpenClaw CLI...\n');
142
+ // ============ 安装指引 ============
143
+
144
+ function showInstallGuide() {
145
+ console.log(colors.bold(colors.cyan('\n' + '='.repeat(60))));
146
+ console.log(colors.bold(colors.cyan(' 📖 OpenClaw 安装指引')));
147
+ console.log(colors.bold(colors.cyan('='.repeat(60))));
148
+
149
+ console.log(colors.yellow('\n接下来会运行官方 openclaw onboard 命令'));
150
+ console.log(colors.yellow('自动模式会按以下推荐选择(可随时接管):\n'));
151
+
152
+ console.log(colors.cyan('┌─────────────────────────────────────────────────────────┐'));
153
+ console.log(colors.cyan('│') + colors.bold(' 步骤 1: 安全确认') + colors.cyan(' │'));
154
+ console.log(colors.cyan('│') + ' 看到 "Do you want to continue?" 时 ' + colors.cyan('│'));
155
+ console.log(colors.cyan('│') + colors.green(' → 输入 y 然后回车') + ' ' + colors.cyan('│'));
156
+ console.log(colors.cyan('├─────────────────────────────────────────────────────────┤'));
157
+ console.log(colors.cyan('│') + colors.bold(' 步骤 2: Setup 模式') + colors.cyan(' │'));
158
+ console.log(colors.cyan('│') + ' 看到 "Quick Start" 和 "Advanced" 选项时 ' + colors.cyan('│'));
159
+ console.log(colors.cyan('│') + colors.green(' → 选择 Quick Start(通常是第 1 个)') + ' ' + colors.cyan('│'));
160
+ console.log(colors.cyan('├─────────────────────────────────────────────────────────┤'));
161
+ console.log(colors.cyan('│') + colors.bold(' 步骤 3: Model Provider') + colors.cyan(' │'));
162
+ console.log(colors.cyan('│') + ' 看到选择 AI 模型提供商时 ' + colors.cyan('│'));
163
+ console.log(colors.cyan('│') + colors.green(' → 选择 Skip 或按 s 跳过') + ' ' + colors.cyan('│'));
164
+ console.log(colors.cyan('│') + colors.gray(' (后续用 npx openclawapi 单独配置更方便)') + ' ' + colors.cyan('│'));
165
+ console.log(colors.cyan('├─────────────────────────────────────────────────────────┤'));
166
+ console.log(colors.cyan('│') + colors.bold(' 步骤 4: Channel 配置') + colors.cyan(' │'));
167
+ console.log(colors.cyan('│') + ' 看到选择聊天渠道(Telegram/Discord 等)时 ' + colors.cyan('│'));
168
+ console.log(colors.cyan('│') + colors.green(' → 选择 Skip 或按 s 跳过') + ' ' + colors.cyan('│'));
169
+ console.log(colors.cyan('│') + colors.gray(' (后续用 npx openclawdc 等单独配置)') + ' ' + colors.cyan('│'));
170
+ console.log(colors.cyan('├─────────────────────────────────────────────────────────┤'));
171
+ console.log(colors.cyan('│') + colors.bold(' 步骤 5: Daemon/Service') + colors.cyan(' │'));
172
+ console.log(colors.cyan('│') + ' 看到是否安装后台服务时 ' + colors.cyan('│'));
173
+ console.log(colors.cyan('│') + colors.green(' → 输入 y 确认安装(开机自启)') + ' ' + colors.cyan('│'));
174
+ console.log(colors.cyan('└─────────────────────────────────────────────────────────┘'));
175
+
176
+ console.log(colors.gray('\n其他选项可以直接回车使用默认值'));
177
+ console.log(colors.gray('如果不确定,选择 Skip 或直接回车通常是安全的\n'));
178
+ }
179
+
180
+ function stripAnsi(input) {
181
+ return input
182
+ .replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '')
183
+ .replace(/\x1b\][^\x07]*\x07/g, '')
184
+ .replace(/\r/g, '\n');
185
+ }
186
+
187
+ function getOnboardCommand(cliName) {
188
+ if (platform() === 'win32') {
189
+ return { file: 'cmd.exe', args: ['/c', cliName, 'onboard', '--install-daemon'] };
190
+ }
191
+ return { file: cliName, args: ['onboard', '--install-daemon'] };
192
+ }
193
+
194
+ function waitForEnter(message) {
195
+ return new Promise((resolve) => {
196
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
197
+ rl.question(colors.yellow(message), () => {
198
+ rl.close();
199
+ resolve();
200
+ });
201
+ });
202
+ }
203
+
204
+ // ============ 安装 OpenClaw ============
220
205
 
206
+ async function installOpenClaw() {
221
207
  const useSudo = needsSudo();
208
+
209
+ console.log(colors.bold(colors.cyan('\n[1/2] 安装 OpenClaw CLI\n')));
210
+
222
211
  if (useSudo) {
223
- log.hint('检测到需要 sudo 权限,可能需要输入密码\n');
212
+ log.hint('Linux 系统需要 sudo 权限安装全局包');
213
+ console.log(colors.yellow('\n请运行以下命令:'));
214
+ console.log(colors.green(' sudo npm install -g openclaw@latest\n'));
215
+
216
+ await waitForEnter('安装完成后按回车继续...');
217
+
218
+ // 验证安装
219
+ const check = safeExec('openclaw --version');
220
+ if (!check.ok) {
221
+ log.error('未检测到 openclaw 命令,请确认安装成功');
222
+ process.exit(1);
223
+ }
224
+ log.success(`OpenClaw 已安装: ${check.output}`);
225
+ return 'openclaw';
224
226
  }
225
227
 
226
- return new Promise((resolve, reject) => {
227
- const { cmd, args } = getNpmInstallCmd('openclaw@latest');
228
- const child = spawn(cmd, args, {
229
- stdio: 'inherit',
230
- shell: true,
231
- });
228
+ // macOS 或有权限的 Linux:直接安装
229
+ console.log(colors.gray('正在安装 openclaw...\n'));
232
230
 
233
- child.on('close', (code) => {
234
- if (code === 0) {
235
- log.success('OpenClaw CLI 安装完成');
236
- resolve('openclaw');
237
- } else {
238
- // 尝试 clawdbot
239
- log.warn('openclaw 安装失败,尝试 clawdbot...');
240
- const { cmd: cmd2, args: args2 } = getNpmInstallCmd('clawdbot@latest');
241
- const fallback = spawn(cmd2, args2, {
242
- stdio: 'inherit',
243
- shell: true,
244
- });
245
- fallback.on('close', (code2) => {
246
- if (code2 === 0) {
247
- log.success('clawdbot 安装完成');
248
- resolve('clawdbot');
249
- } else {
250
- reject(new Error('npm 安装失败'));
251
- }
252
- });
253
- }
254
- });
231
+ const result = spawnSync('npm', ['install', '-g', 'openclaw@latest'], {
232
+ stdio: 'inherit',
233
+ shell: true,
255
234
  });
256
- }
257
235
 
258
- // ============ 自动化 Onboard ============
236
+ if (result.status === 0) {
237
+ log.success('OpenClaw CLI 安装完成');
238
+ return 'openclaw';
239
+ }
259
240
 
260
- async function runOnboardWithAutoResponse(cliName, options) {
261
- log.info('\n[2/2] 运行配置向导...\n');
241
+ // 尝试 clawdbot
242
+ log.warn('openclaw 安装失败,尝试 clawdbot...');
243
+ const fallback = spawnSync('npm', ['install', '-g', 'clawdbot@latest'], {
244
+ stdio: 'inherit',
245
+ shell: true,
246
+ });
262
247
 
263
- if (options.manual) {
264
- log.hint('手动模式:请自行完成所有选择\n');
265
- } else {
266
- console.log(colors.dim(' 智能模式:自动选择推荐配置,您可以观看安装过程\n'));
248
+ if (fallback.status === 0) {
249
+ log.success('clawdbot 安装完成');
250
+ return 'clawdbot';
267
251
  }
268
252
 
269
- return new Promise((resolve, reject) => {
270
- // 使用 script 命令来伪造 TTY(Unix)或直接 spawn(可能不完美)
271
- const os = platform();
272
- let child;
273
-
274
- // 设置终端尺寸环境变量
275
- const termEnv = {
276
- ...process.env,
277
- FORCE_COLOR: '1',
278
- TERM: 'xterm-256color',
279
- COLUMNS: process.stdout.columns?.toString() || '120',
280
- LINES: process.stdout.rows?.toString() || '40',
281
- };
282
-
283
- if (os === 'win32') {
284
- // Windows: 直接 spawn,可能没有完整的交互体验
285
- child = spawn(cliName, ['onboard', '--install-daemon'], {
286
- stdio: options.manual ? 'inherit' : ['pipe', 'pipe', 'pipe'],
287
- shell: true,
288
- env: termEnv,
289
- });
290
- } else if (os === 'darwin') {
291
- // macOS: script -q /dev/null command args...
292
- child = spawn('script', ['-q', '/dev/null', cliName, 'onboard', '--install-daemon'], {
293
- stdio: options.manual ? 'inherit' : ['pipe', 'pipe', 'pipe'],
294
- env: termEnv,
295
- });
253
+ log.error('安装失败');
254
+ console.log(colors.cyan('\n解决方案:'));
255
+ console.log(' 1. 检查网络连接');
256
+ console.log(' 2. 手动安装: npm install -g openclaw@latest');
257
+ process.exit(1);
258
+ }
259
+
260
+ // ============ 运行 Onboard ============
261
+
262
+ async function runOnboard(cliName) {
263
+ console.log(colors.bold(colors.cyan('\n[2/2] 运行配置向导\n')));
264
+
265
+ // 显示指引
266
+ showInstallGuide();
267
+
268
+ await waitForEnter('准备好了吗?按回车开始配置...');
269
+
270
+ console.log(colors.gray('\n' + '-'.repeat(60)));
271
+ console.log(colors.gray('以下是官方 openclaw onboard 界面:'));
272
+ console.log(colors.gray('-'.repeat(60) + '\n'));
273
+
274
+ const options = parseArgs();
275
+ const preferAuto = !options.manual;
276
+ let usedAuto = false;
277
+
278
+ if (preferAuto) {
279
+ const autoResult = await runOnboardAuto(cliName, options);
280
+ if (autoResult.ok) {
281
+ usedAuto = true;
282
+ console.log(colors.gray('\n' + '-'.repeat(60)));
283
+ if (autoResult.exitCode !== 0) {
284
+ log.warn(`onboard 退出码: ${autoResult.exitCode}`);
285
+ log.hint('如果配置未完成,可以手动运行: ' + cliName + ' onboard');
286
+ }
287
+ } else if (options.auto) {
288
+ log.error('自动模式不可用,已退出');
289
+ log.hint(autoResult.reason || '请尝试 --manual');
290
+ process.exit(1);
296
291
  } else {
297
- // Linux: script -q -c "command args..." /dev/null
298
- // 使用 stty 设置终端尺寸
299
- const cols = process.stdout.columns || 120;
300
- const rows = process.stdout.rows || 40;
301
- const cmd = `stty cols ${cols} rows ${rows} 2>/dev/null; ${cliName} onboard --install-daemon`;
302
- child = spawn('script', ['-q', '-c', cmd, '/dev/null'], {
303
- stdio: options.manual ? 'inherit' : ['pipe', 'pipe', 'pipe'],
304
- env: termEnv,
305
- });
292
+ log.warn('自动模式不可用,已切换为手动模式');
293
+ log.hint(autoResult.reason || '未能启用自动应答');
306
294
  }
295
+ }
307
296
 
308
- if (options.manual) {
309
- // 手动模式:完全交互
310
- child.on('close', (code) => {
311
- if (code === 0) {
312
- resolve();
313
- } else {
314
- reject(new Error(`onboard 退出码: ${code}`));
315
- }
316
- });
297
+ if (!usedAuto) {
298
+ const manualResult = runOnboardManual(cliName);
299
+ console.log(colors.gray('\n' + '-'.repeat(60)));
300
+ if (manualResult.status !== 0) {
301
+ log.warn(`onboard 退出码: ${manualResult.status}`);
302
+ log.hint('如果配置未完成,可以手动运行: ' + cliName + ' onboard');
303
+ }
304
+ }
305
+ }
306
+
307
+ function runOnboardManual(cliName) {
308
+ const { file, args } = getOnboardCommand(cliName);
309
+ return spawnSync(file, args, {
310
+ stdio: 'inherit',
311
+ });
312
+ }
313
+
314
+ function createAutoResponder(term, options) {
315
+ const lastSent = new Map();
316
+ let autoStopped = false;
317
+ let pausedNoticeShown = false;
318
+
319
+ const cooldownMs = 1200;
320
+ const shouldRespond = (id) => {
321
+ const now = Date.now();
322
+ const last = lastSent.get(id) || 0;
323
+ if (now - last < cooldownMs) return false;
324
+ lastSent.set(id, now);
325
+ return true;
326
+ };
327
+
328
+ const stopAuto = (reason) => {
329
+ if (autoStopped) return;
330
+ autoStopped = true;
331
+ log.warn(reason);
332
+ log.hint('已进入手动模式');
333
+ };
334
+
335
+ const send = (id, payload) => {
336
+ if (!shouldRespond(id)) return false;
337
+ term.write(payload);
338
+ return true;
339
+ };
340
+
341
+ return (rawText) => {
342
+ if (autoStopped) return;
343
+ const text = rawText.toLowerCase();
344
+ const tail = text.slice(-800);
345
+
346
+ if (tail.includes('do you want to continue') || tail.includes('continue?') || tail.includes('是否继续')) {
347
+ send('confirm', 'y\r');
317
348
  return;
318
349
  }
319
350
 
320
- // 智能模式:监听输出并自动应答
321
- let buffer = '';
322
- let lastResponseTime = 0;
323
- const RESPONSE_COOLDOWN = 1000; // 防止重复响应
324
-
325
- const processOutput = (data) => {
326
- const text = data.toString();
327
- process.stdout.write(text); // 显示原版输出
328
- buffer += text;
329
-
330
- // 检查是否需要自动应答
331
- const now = Date.now();
332
- if (now - lastResponseTime < RESPONSE_COOLDOWN) return;
333
-
334
- for (const rule of AUTO_RESPONSES) {
335
- if (rule.match.test(buffer)) {
336
- if (rule.skip) {
337
- // 跳过这个提示,不发送任何内容
338
- buffer = '';
339
- return;
340
- }
341
-
342
- lastResponseTime = now;
343
- setTimeout(() => {
344
- if (child.stdin && !child.stdin.destroyed) {
345
- log.auto(rule.desc);
346
- child.stdin.write(rule.response + '\n');
347
- }
348
- }, rule.delay);
349
-
350
- buffer = ''; // 清空缓冲区,避免重复匹配
351
- break;
352
- }
351
+ if ((tail.includes('quick start') || tail.includes('quickstart')) && (tail.includes('advanced') || tail.includes('custom'))) {
352
+ send('setup', '\r');
353
+ return;
354
+ }
355
+
356
+ const modelPrompt =
357
+ tail.includes('model provider') ||
358
+ tail.includes('model providers') ||
359
+ tail.includes('select a model') ||
360
+ tail.includes('模型') && tail.includes('提供商');
361
+
362
+ if (modelPrompt) {
363
+ if (options.withModel) {
364
+ stopAuto('检测到模型配置提示,已暂停自动选择(--with-model)');
365
+ return;
353
366
  }
367
+ send('model-skip', tail.includes('skip') || tail.includes('跳过') ? 's\r' : '\r');
368
+ return;
369
+ }
370
+
371
+ const apiKeyPrompt =
372
+ tail.includes('api key') ||
373
+ tail.includes('apikey') ||
374
+ (tail.includes('key') && tail.includes('请输入'));
354
375
 
355
- // 防止缓冲区过大
356
- if (buffer.length > 2000) {
357
- buffer = buffer.slice(-1000);
376
+ if (apiKeyPrompt) {
377
+ if (options.withModel) {
378
+ stopAuto('检测到 API Key 配置,已暂停自动选择(--with-model)');
379
+ return;
358
380
  }
359
- };
381
+ send('api-skip', tail.includes('skip') || tail.includes('跳过') ? 's\r' : '\r');
382
+ return;
383
+ }
360
384
 
361
- if (child.stdout) {
362
- child.stdout.on('data', processOutput);
385
+ const channelPrompt =
386
+ tail.includes('channel') ||
387
+ tail.includes('discord') ||
388
+ tail.includes('telegram') ||
389
+ tail.includes('feishu') ||
390
+ (tail.includes('渠道') && (tail.includes('选择') || tail.includes('配置')));
391
+
392
+ if (channelPrompt) {
393
+ if (options.withChannel) {
394
+ stopAuto('检测到渠道配置提示,已暂停自动选择(--with-channel)');
395
+ return;
396
+ }
397
+ send('channel-skip', 's\r');
398
+ return;
363
399
  }
364
- if (child.stderr) {
365
- child.stderr.on('data', processOutput);
400
+
401
+ if (tail.includes('skills') || tail.includes('skill')) {
402
+ send('skills-skip', 's\r');
403
+ return;
366
404
  }
367
405
 
368
- child.on('close', (code) => {
369
- console.log(''); // 换行
370
- if (code === 0) {
371
- resolve();
372
- } else {
373
- // 非零退出码不一定是错误,可能是用户中断
374
- log.warn(`onboard 退出码: ${code}`);
375
- resolve();
376
- }
406
+ const daemonPrompt =
407
+ tail.includes('daemon') ||
408
+ tail.includes('service') && (tail.includes('install') || tail.includes('enable')) ||
409
+ (tail.includes('后台') && tail.includes('服务')) ||
410
+ tail.includes('开机') && tail.includes('自启');
411
+
412
+ if (daemonPrompt) {
413
+ send('daemon-yes', 'y\r');
414
+ return;
415
+ }
416
+
417
+ const uiPrompt =
418
+ tail.includes('dashboard') ||
419
+ tail.includes('web ui') ||
420
+ tail.includes('web dashboard') ||
421
+ (tail.includes('ui') && tail.includes('选择')) ||
422
+ (tail.includes('界面') && tail.includes('选择'));
423
+
424
+ if (uiPrompt) {
425
+ send('ui-web', tail.includes('web') || tail.includes('dashboard') ? '\r' : 'w\r');
426
+ return;
427
+ }
428
+
429
+ if (tail.includes('port') && (tail.includes('gateway') || tail.includes('端口'))) {
430
+ send('port-default', '\r');
431
+ return;
432
+ }
433
+
434
+ if (!pausedNoticeShown && (options.withModel || options.withChannel)) {
435
+ pausedNoticeShown = true;
436
+ log.hint('检测到 --with-model / --with-channel,将在相关步骤暂停自动选择');
437
+ }
438
+ };
439
+ }
440
+
441
+ async function runOnboardAuto(cliName, options) {
442
+ let ptyModule;
443
+ try {
444
+ ptyModule = await import('node-pty');
445
+ } catch (e) {
446
+ return { ok: false, reason: 'node-pty 依赖不可用,请尝试 --manual' };
447
+ }
448
+
449
+ const pty = ptyModule.default ?? ptyModule;
450
+ const spawn = pty?.spawn;
451
+ if (typeof spawn !== 'function') {
452
+ return { ok: false, reason: 'node-pty 未正确加载,请尝试 --manual' };
453
+ }
454
+
455
+ const { file, args } = getOnboardCommand(cliName);
456
+ const cols = process.stdout.columns || 80;
457
+ const rows = process.stdout.rows || 24;
458
+ const env = { ...process.env, FORCE_COLOR: '1' };
459
+
460
+ let term;
461
+ try {
462
+ term = spawn(file, args, {
463
+ name: 'xterm-256color',
464
+ cols,
465
+ rows,
466
+ cwd: process.cwd(),
467
+ env,
377
468
  });
469
+ } catch (e) {
470
+ return { ok: false, reason: `启动 PTY 失败: ${e.message}` };
471
+ }
472
+
473
+ const autoResponder = createAutoResponder(term, options);
474
+ let buffer = '';
475
+ let manualOverride = false;
476
+
477
+ log.info('自动模式已开启,按任意键可接管');
478
+
479
+ const stdin = process.stdin;
480
+ const canReadInput = Boolean(stdin.isTTY);
481
+ const restoreRaw = canReadInput ? stdin.isRaw : false;
482
+
483
+ const onUserData = (data) => {
484
+ if (!manualOverride) {
485
+ manualOverride = true;
486
+ log.warn('已接管,自动模式停止');
487
+ }
488
+ term.write(data.toString('utf8'));
489
+ };
490
+
491
+ if (canReadInput && typeof stdin.setRawMode === 'function') {
492
+ stdin.setRawMode(true);
493
+ }
494
+ if (canReadInput) {
495
+ stdin.on('data', onUserData);
496
+ stdin.resume();
497
+ }
498
+
499
+ term.onData((data) => {
500
+ process.stdout.write(data);
501
+ if (manualOverride) return;
502
+ buffer += stripAnsi(data);
503
+ if (buffer.length > 4000) {
504
+ buffer = buffer.slice(-4000);
505
+ }
506
+ autoResponder(buffer);
507
+ });
378
508
 
379
- child.on('error', (err) => {
380
- reject(err);
509
+ return await new Promise((resolve) => {
510
+ term.onExit(({ exitCode }) => {
511
+ if (canReadInput) {
512
+ stdin.off('data', onUserData);
513
+ if (typeof stdin.setRawMode === 'function') {
514
+ stdin.setRawMode(Boolean(restoreRaw));
515
+ }
516
+ stdin.pause();
517
+ }
518
+ resolve({ ok: true, exitCode });
381
519
  });
382
520
  });
383
521
  }
@@ -385,53 +523,48 @@ async function runOnboardWithAutoResponse(cliName, options) {
385
523
  // ============ 更新 ============
386
524
 
387
525
  async function updateOpenClaw(cliName) {
388
- log.info('\n检查更新...\n');
526
+ console.log(colors.bold(colors.cyan('\n检查更新...\n')));
389
527
 
390
528
  const currentResult = safeExec(`${cliName} --version`);
391
529
  const currentVersion = currentResult.ok ? currentResult.output : '未知';
392
- console.log(` 当前版本: ${colors.yellow(currentVersion)}`);
530
+ console.log(`当前版本: ${colors.yellow(currentVersion)}`);
393
531
 
394
- return new Promise((resolve) => {
395
- const child = spawn('npm', ['install', '-g', `${cliName}@latest`], {
532
+ const useSudo = needsSudo();
533
+
534
+ if (useSudo) {
535
+ console.log(colors.yellow('\n请运行以下命令更新:'));
536
+ console.log(colors.green(` sudo npm install -g ${cliName}@latest`));
537
+ console.log(colors.green(` ${cliName} gateway restart\n`));
538
+ } else {
539
+ console.log(colors.gray('\n正在更新...\n'));
540
+ spawnSync('npm', ['install', '-g', `${cliName}@latest`], {
396
541
  stdio: 'inherit',
397
542
  shell: true,
398
543
  });
399
544
 
400
- child.on('close', (code) => {
401
- if (code === 0) {
402
- const newResult = safeExec(`${cliName} --version`);
403
- const newVersion = newResult.ok ? newResult.output : '未知';
404
- if (newVersion !== currentVersion) {
405
- log.success(`更新完成: ${currentVersion} → ${newVersion}`);
406
- } else {
407
- log.success(`已是最新版本: ${newVersion}`);
408
- }
545
+ const newResult = safeExec(`${cliName} --version`);
546
+ const newVersion = newResult.ok ? newResult.output : '未知';
409
547
 
410
- // 重启 Gateway
411
- log.info('\n重启 Gateway...');
412
- const restart = safeExec(`${cliName} gateway restart`);
413
- if (restart.ok) {
414
- log.success('Gateway 已重启');
415
- } else {
416
- log.hint(`手动重启: ${cliName} gateway restart`);
417
- }
418
- } else {
419
- log.error('更新失败');
420
- log.hint(`手动更新: npm install -g ${cliName}@latest`);
421
- }
422
- resolve();
423
- });
424
- });
548
+ if (newVersion !== currentVersion) {
549
+ log.success(`更新完成: ${currentVersion} → ${newVersion}`);
550
+ } else {
551
+ log.success(`已是最新版本: ${newVersion}`);
552
+ }
553
+
554
+ console.log(colors.gray('\n重启 Gateway...'));
555
+ safeExec(`${cliName} gateway restart`);
556
+ log.success('Gateway 已重启');
557
+ }
425
558
  }
426
559
 
427
560
  // ============ 完成信息 ============
428
561
 
429
562
  function showCompletionInfo(cliName) {
430
- console.log(colors.bold(colors.green('\n========================================')));
431
- console.log(colors.bold(colors.green('✅ OpenClaw 安装完成!')));
432
- console.log(colors.bold(colors.green('========================================')));
563
+ console.log(colors.bold(colors.green('\n' + '='.repeat(60))));
564
+ console.log(colors.bold(colors.green(' ✅ OpenClaw 安装完成!')));
565
+ console.log(colors.bold(colors.green('='.repeat(60))));
433
566
 
434
- console.log(colors.cyan('\n下一步 - 配置 AI 模型:'));
567
+ console.log(colors.cyan('\n下一步 - 配置 AI 模型(必须):'));
435
568
  console.log(` ${colors.yellow('npx openclawapi@latest preset-claude')}`);
436
569
 
437
570
  console.log(colors.cyan('\n常用命令:'));
@@ -440,7 +573,7 @@ function showCompletionInfo(cliName) {
440
573
  console.log(` 重启服务: ${colors.yellow(`${cliName} gateway restart`)}`);
441
574
  console.log(` 诊断问题: ${colors.yellow(`${cliName} doctor`)}`);
442
575
 
443
- console.log(colors.cyan('\n配置聊天渠道:'));
576
+ console.log(colors.cyan('\n配置聊天渠道(可选):'));
444
577
  console.log(` Discord: ${colors.yellow('npx openclawdc')}`);
445
578
  console.log(` 飞书: ${colors.yellow('npx openclaw-chat-cn@latest feishu')}`);
446
579
 
@@ -457,7 +590,7 @@ async function main() {
457
590
  process.exit(0);
458
591
  }
459
592
 
460
- console.log(colors.bold(colors.cyan('\n🦞 OpenClaw 智能安装向导\n')));
593
+ console.log(colors.bold(colors.cyan('\n🦞 OpenClaw 安装向导\n')));
461
594
 
462
595
  // 检查 Node 版本
463
596
  checkNodeVersion();
@@ -477,10 +610,18 @@ async function main() {
477
610
  if (options.reinstall) {
478
611
  log.info('\n卸载现有安装...');
479
612
  safeExec(`${existing.name} gateway stop`);
480
- safeExec(`npm uninstall -g ${existing.name}`);
481
- if (existing.configDir && existsSync(existing.configDir)) {
482
- rmSync(existing.configDir, { recursive: true, force: true });
483
- log.success('配置已清除');
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('卸载完成');
484
625
  }
485
626
  } else {
486
627
  console.log(colors.cyan('\n已安装,可选操作:'));
@@ -492,22 +633,14 @@ async function main() {
492
633
  }
493
634
  }
494
635
 
495
- try {
496
- // 安装 CLI
497
- const cliName = await installOpenClaw();
498
-
499
- // 运行 onboard
500
- await runOnboardWithAutoResponse(cliName, options);
636
+ // 安装 CLI
637
+ const cliName = await installOpenClaw();
501
638
 
502
- // 显示完成信息
503
- showCompletionInfo(cliName);
639
+ // 运行 onboard
640
+ await runOnboard(cliName);
504
641
 
505
- } catch (err) {
506
- exitWithError('ONBOARD_FAILED', err.message, [
507
- '检查网络连接',
508
- '手动安装: npm install -g openclaw && openclaw onboard --install-daemon',
509
- ]);
510
- }
642
+ // 显示完成信息
643
+ showCompletionInfo(cliName);
511
644
  }
512
645
 
513
646
  main().catch((e) => {
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "openclawsetup",
3
- "version": "2.0.3",
4
- "description": "OpenClaw 智能安装向导 - 调用官方交互界面,自动选择推荐配置",
3
+ "version": "2.1.1",
4
+ "description": "OpenClaw 安装向导 - 带中文指引的官方安装流程",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "openclawsetup": "bin/cli.mjs"
8
8
  },
9
+ "optionalDependencies": {
10
+ "node-pty": "^1.0.0"
11
+ },
9
12
  "scripts": {
10
13
  "start": "node bin/cli.mjs"
11
14
  },
@@ -10,7 +10,7 @@
10
10
  npx openclawsetup@latest
11
11
  ```
12
12
 
13
- 你会看到官方的 `openclaw onboard` 界面,但所有选项会自动选择推荐配置。
13
+ 你会看到官方的 `openclaw onboard` 界面,但所有选项会自动选择推荐配置(按任意键可接管)。
14
14
 
15
15
  ### 方式二:手动模式
16
16
 
@@ -20,7 +20,15 @@ npx openclawsetup@latest
20
20
  npx openclawsetup@latest --manual
21
21
  ```
22
22
 
23
- ### 方式三:一键脚本
23
+ ### 方式三:强制自动模式
24
+
25
+ ```bash
26
+ npx openclawsetup@latest --auto
27
+ ```
28
+
29
+ 自动应答不可用时将直接退出(适合脚本化场景)。
30
+
31
+ ### 方式四:一键脚本
24
32
 
25
33
  自动检测并安装 Node.js,无需任何前置条件。
26
34
 
@@ -49,6 +57,10 @@ irm https://unpkg.com/openclawsetup@latest/install.ps1 | iex
49
57
  | Daemon/Service | 安装 (y) |
50
58
  | UI 选择 | Web Dashboard |
51
59
 
60
+ > 需要在安装过程中配置模型或渠道时,可使用:
61
+ > - `--with-model`:检测到模型配置步骤时暂停自动选择
62
+ > - `--with-channel`:检测到渠道配置步骤时暂停自动选择
63
+
52
64
  **为什么跳过模型和渠道?**
53
65
  - 模型配置:后续用 `npx openclawapi` 单独配置,更灵活
54
66
  - 渠道配置:后续用 `npx openclawdc`(Discord)或 `npx openclaw-chat-cn@latest feishu`(飞书)配置
@@ -142,13 +154,17 @@ curl -fsSL https://unpkg.com/openclawsetup@latest/install.sh | bash
142
154
 
143
155
  ### 自动选择没有生效
144
156
  **现象**:安装过程中需要手动输入
145
- **原因**:可能是终端环境问题
157
+ **原因**:可能是终端环境问题或自动应答依赖不可用
146
158
  **解决**:
147
159
  1. 使用手动模式:
148
160
  ```bash
149
161
  npx openclawsetup@latest --manual
150
162
  ```
151
- 2. 或直接运行官方命令:
163
+ 2. 需要脚本化时使用强制自动模式(不可用会退出):
164
+ ```bash
165
+ npx openclawsetup@latest --auto
166
+ ```
167
+ 3. 或直接运行官方命令:
152
168
  ```bash
153
169
  npm install -g openclaw@latest
154
170
  openclaw onboard --install-daemon