create-openclaw-bot 5.7.9 → 5.8.0

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/dist/cli.js CHANGED
@@ -1,2810 +1,112 @@
1
- #!/usr/bin/env node
2
-
3
- import { input, select, checkbox, confirm } from '@inquirer/prompts';
4
- import fs from 'fs-extra';
5
- import path from 'path';
6
- import os from 'os';
7
- import chalk from 'chalk';
8
- import { spawn, execSync, execFileSync } from 'child_process';
9
- import { createRequire } from 'module';
10
-
11
- // ─── Shared generators (dual-mode IIFE + CJS) ────────────────────────────────
12
- // These modules export via module.exports when required from Node.js
13
- const _require = createRequire(import.meta.url);
14
-
15
- function loadSharedModule(modulePath, globalName) {
16
- const loaded = _require(modulePath);
17
- if (loaded && Object.keys(loaded).length > 0) {
18
- return loaded;
19
- }
20
- return globalThis[globalName] || loaded || {};
21
- }
22
-
23
- const {
24
- OPENCLAW_NPM_SPEC,
25
- OPENCLAW_RUNTIME_PACKAGES,
26
- NINE_ROUTER_PROXY_API_KEY,
27
- NINE_ROUTER_API_BASE_URL,
28
- SMART_ROUTE_PROVIDER_MODELS,
29
- SMART_ROUTE_PROVIDER_ORDER,
30
- TELEGRAM_RELAY_PLUGIN_SPEC,
31
- TELEGRAM_SETUP_GUIDE_FILENAME,
32
- buildRelayPluginInstallCommand,
33
- buildTelegramPostInstallChecklist,
34
- get9RouterBaseUrl,
35
- build9RouterProviderConfig,
36
- } = loadSharedModule('./setup/shared/common-gen.js', '__openclawCommon');
37
-
38
- const {
39
- buildDockerArtifacts,
40
- build9RouterPatchScript,
41
- } = loadSharedModule('./setup/shared/docker-gen.js', '__openclawDockerGen');
42
-
43
- const {
44
- buildWorkspaceFileMap,
45
- } = loadSharedModule('./setup/shared/workspace-gen.js', '__openclawWorkspace');
46
-
47
- const dataExport = loadSharedModule('./setup/data/index.js', '__openclawData');
48
-
49
- const {
50
- PROVIDERS: _PROVIDERS,
51
- SKILLS: _SKILLS,
52
- CHANNELS: _CHANNELS,
53
- OLLAMA_MODELS,
54
- } = dataExport;
55
-
56
- const {
57
- buildCliChromeDebugArtifacts,
58
- buildCliUninstallArtifacts,
59
- buildCliUpgradeArtifacts,
60
- buildCliStartBotArtifacts,
61
- } = loadSharedModule('./setup/shared/install-gen.js', '__openclawInstall');
62
-
63
- const {
64
- buildOpenclawJson,
65
- buildExecApprovalsJson,
66
- buildEnvFileContent,
67
- buildSkillsEntries: _buildSkillsEntries,
68
- } = loadSharedModule('./setup/shared/bot-config-gen.js', '__openclawBotConfig');
69
-
70
- function installRelayPluginForProject(projectDir, isVi) {
71
- try {
72
- execSync(`openclaw plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC}`, { cwd: projectDir, stdio: 'ignore' });
73
- return true;
74
- } catch {
75
- // silent fallback
76
- }
77
- console.log(chalk.yellow(isVi
78
- ? `\n⚠️ Chua the tu dong cai plugin. Sau khi bot chay, chay thu cong:\n openclaw plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC}`
79
- : `\n⚠️ Could not auto-install plugin. After the bot starts, run manually:\n openclaw plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC}`));
80
- return false;
81
- }
82
-
83
- function isOpenClawInstalled() {
84
- try {
85
- execSync('openclaw --version', { stdio: 'ignore' });
86
- return true;
87
- } catch {
88
- return false;
89
- }
90
- }
91
-
92
- function isPm2Installed() {
93
- try {
94
- execSync('pm2 --version', { stdio: 'ignore' });
95
- return true;
96
- } catch {
97
- return false;
98
- }
99
- }
100
-
101
- function is9RouterInstalled() {
102
- try {
103
- execSync('9router --help', { stdio: 'ignore' });
104
- return true;
105
- } catch {
106
- return false;
107
- }
108
- }
109
-
110
- function shouldReuseInstalledGlobals() {
111
- return ['1', 'true', 'yes', 'on'].includes(String(process.env.OPENCLAW_SETUP_REUSE_GLOBALS || '').trim().toLowerCase());
112
- }
113
-
114
- function getUserNpmPrefixInfo() {
115
- if (process.platform === 'win32') {
116
- return null;
117
- }
118
-
119
- const prefixDir = path.join(os.homedir(), '.local');
120
- return {
121
- prefixDir,
122
- binDir: path.join(prefixDir, 'bin')
123
- };
124
- }
125
-
126
- function ensureBinDirOnPath(binDir) {
127
- const delimiter = path.delimiter;
128
- const pathParts = String(process.env.PATH || '').split(delimiter).filter(Boolean);
129
- if (!pathParts.includes(binDir)) {
130
- process.env.PATH = [binDir, ...pathParts].join(delimiter);
131
- }
132
- }
133
-
134
- function quoteWindowsCmdArg(arg) {
135
- const value = String(arg);
136
- if (!/[\s"]/u.test(value)) {
137
- return value;
138
- }
139
- return `"${value.replace(/"/g, '""')}"`;
140
- }
141
-
142
- function quotePowerShellSingle(value) {
143
- return `'${String(value).replace(/'/g, "''")}'`;
144
- }
145
-
146
- function resolveWindowsCommand(command) {
147
- try {
148
- const output = execSync(`where.exe ${command}`, {
149
- stdio: ['ignore', 'pipe', 'ignore'],
150
- encoding: 'utf8',
151
- shell: true,
152
- env: process.env
153
- });
154
- const firstMatch = output
155
- .split(/\r?\n/)
156
- .map((line) => line.trim())
157
- .find(Boolean);
158
- return firstMatch || command;
159
- } catch {
160
- return command;
161
- }
162
- }
163
-
164
- function spawnBackgroundProcess(command, args, options = {}) {
165
- const { cwd, env = {} } = options;
166
- const mergedEnv = { ...process.env, ...env };
167
-
168
- if (process.platform === 'win32') {
169
- const resolvedCommand = resolveWindowsCommand(command);
170
- const argList = args.map((arg) => quotePowerShellSingle(arg)).join(', ');
171
- const startProcessScript = [
172
- `$filePath = ${quotePowerShellSingle(resolvedCommand)}`,
173
- `$workingDir = ${quotePowerShellSingle(cwd || process.cwd())}`,
174
- `$argList = @(${argList})`,
175
- "Start-Process -WindowStyle Hidden -FilePath $filePath -WorkingDirectory $workingDir -ArgumentList $argList"
176
- ].join('; ');
177
-
178
- return spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', startProcessScript], {
179
- cwd,
180
- detached: true,
181
- stdio: 'ignore',
182
- windowsHide: true,
183
- env: mergedEnv
184
- });
185
- }
186
-
187
- return spawn(command, args, {
188
- cwd,
189
- detached: true,
190
- stdio: 'ignore',
191
- windowsHide: true,
192
- env: mergedEnv
193
- });
194
- }
195
-
196
- function resolveNative9RouterDesktopLaunch() {
197
- const routerBin = resolveCommandOnPath('9router');
198
- return {
199
- command: routerBin,
200
- args: ['-n', '-H', '0.0.0.0', '-p', '20128', '--skip-update'],
201
- env: {
202
- PORT: '20128',
203
- HOSTNAME: '0.0.0.0',
204
- // Ensures 9router stores data in the user home dir, matching where sync script writes db.json
205
- DATA_DIR: getNative9RouterDataDir(),
206
- }
207
- };
208
- }
209
-
210
- function build9RouterSmartRouteSyncScript(dbPath) {
211
- const safeDbPath = JSON.stringify(dbPath);
212
- const safeRouterBaseUrl = JSON.stringify(NINE_ROUTER_API_BASE_URL);
213
- const safeModelPriority = JSON.stringify(SMART_ROUTE_PROVIDER_MODELS);
214
- const safeProviderOrder = JSON.stringify(SMART_ROUTE_PROVIDER_ORDER);
215
- return `function bootstrap() {
216
- const fs = require('fs');
217
- const path = require('path');
218
- const dbPath = ${safeDbPath};
219
- const ROUTER=${safeRouterBaseUrl};
220
- const MODEL_PRIORITY=${safeModelPriority};
221
- const PREF=${safeProviderOrder};
222
- const sync = async () => {
223
- try {
224
- const response = await fetch(ROUTER + '/api/providers');
225
- if (!response.ok) return;
226
- const payload = await response.json();
227
- const rawConnections = Array.isArray(payload.connections)
228
- ? payload.connections
229
- : Array.isArray(payload.providerConnections)
230
- ? payload.providerConnections
231
- : [];
232
- const a = [...new Set(rawConnections
233
- .filter((item) => item && item.provider && item.isActive !== false && !item.disabled)
234
- .map((item) => item.provider))];
235
- let db = {};
236
- try {
237
- db = JSON.parse(fs.readFileSync(dbPath, 'utf8'));
238
- } catch {}
239
- if (!db.combos) db.combos = [];
240
- const removeSmartRoute = () => {
241
- const next = db.combos.filter((combo) => combo.id !== 'smart-route');
242
- if (next.length !== db.combos.length) {
243
- db.combos = next;
244
- fs.mkdirSync(path.dirname(dbPath), { recursive: true });
245
- fs.writeFileSync(dbPath, JSON.stringify(db, null, 2));
246
- console.log('Removed smart-route (no active providers)');
247
- }
248
- };
249
- if (!a.length) {
250
- removeSmartRoute();
251
- return;
252
- }
253
- a.sort((x, y) => (PREF.indexOf(x) === -1 ? 99 : PREF.indexOf(x)) - (PREF.indexOf(y) === -1 ? 99 : PREF.indexOf(y)));
254
- const m = a.flatMap((provider) => MODEL_PRIORITY[provider] || []);
255
- if (!m.length) {
256
- removeSmartRoute();
257
- return;
258
- }
259
- const nextCombos = db.combos.filter((combo) => combo.id !== 'smart-route');
260
- nextCombos.push({ id: 'smart-route', name: 'smart-route', alias: 'smart-route', models: m });
261
- db.combos = nextCombos;
262
- fs.mkdirSync(path.dirname(dbPath), { recursive: true });
263
- fs.writeFileSync(dbPath, JSON.stringify(db, null, 2));
264
- } catch {}
265
- };
266
- setTimeout(sync, 5000);
267
- setInterval(sync, 30000);
268
- }
269
- bootstrap();
270
- `;
271
- }
272
-
273
- function getProjectOpenClawHome(projectDir) {
274
- return path.join(projectDir, '.openclaw');
275
- }
276
-
277
- function getProject9RouterDataDir(projectDir) {
278
- return path.join(projectDir, '.9router');
279
- }
280
-
281
- function getProjectRuntimeEnv(projectDir, extraEnv = {}) {
282
- return {
283
- ...process.env,
284
- OPENCLAW_HOME: getProjectOpenClawHome(projectDir),
285
- OPENCLAW_STATE_DIR: getProjectOpenClawHome(projectDir),
286
- DATA_DIR: getProject9RouterDataDir(projectDir),
287
- ...extraEnv,
288
- };
289
- }
290
-
291
- function hasZaloPersonal(channelKey) {
292
- return channelKey === 'zalo-personal';
293
- }
294
-
295
- function getNative9RouterDataDir() {
296
- if (process.platform === 'win32') {
297
- return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), '9router');
298
- }
299
-
300
- return path.join(os.homedir(), '.9router');
301
- }
302
-
303
- async function waitFor9RouterApiReady({ port = 20128, timeoutMs = 15000 } = {}) {
304
- const deadline = Date.now() + timeoutMs;
305
- const candidates = [
306
- `http://127.0.0.1:${port}/api/settings/require-login`,
307
- `http://127.0.0.1:${port}/api/version`
308
- ];
309
-
310
- while (Date.now() < deadline) {
311
- for (const url of candidates) {
312
- try {
313
- const response = await fetch(url, { signal: AbortSignal.timeout(2500) });
314
- if (response.ok) {
315
- return { ok: true, url };
316
- }
317
- } catch {
318
- // keep polling until timeout
319
- }
320
- }
321
-
322
- await new Promise((resolve) => setTimeout(resolve, 1200));
323
- }
324
-
325
- return { ok: false, url: candidates[0] };
326
- }
327
-
328
- function appendLineIfMissing(filePath, line) {
329
- let content = '';
330
- if (fs.existsSync(filePath)) {
331
- content = fs.readFileSync(filePath, 'utf8');
332
- }
333
-
334
- if (!content.includes(line)) {
335
- const prefix = content && !content.endsWith('\n') ? '\n' : '';
336
- fs.appendFileSync(filePath, `${prefix}${line}\n`);
337
- }
338
- }
339
-
340
- function ensureUserWritableGlobalNpm({ isVi, osChoice }) {
341
- if (process.platform === 'win32') {
342
- return true;
343
- }
344
-
345
- const npmInfo = getUserNpmPrefixInfo();
346
- if (!npmInfo) {
347
- return true;
348
- }
349
-
350
- try {
351
- fs.ensureDirSync(npmInfo.binDir);
352
- process.env.npm_config_prefix = npmInfo.prefixDir;
353
- ensureBinDirOnPath(npmInfo.binDir);
354
-
355
- execSync(`npm config set prefix "${npmInfo.prefixDir.replace(/"/g, '\\"')}"`, {
356
- stdio: 'ignore',
357
- shell: true,
358
- env: process.env
359
- });
360
-
361
- appendLineIfMissing(path.join(os.homedir(), '.profile'), 'export PATH="$HOME/.local/bin:$PATH"');
362
- appendLineIfMissing(
363
- path.join(os.homedir(), osChoice === 'macos' ? '.zshrc' : '.bashrc'),
364
- 'export PATH="$HOME/.local/bin:$PATH"'
365
- );
366
- return true;
367
- } catch {
368
- console.log(chalk.yellow(isVi
369
- ? '⚠️ Không thể cấu hình npm global prefix trong ~/.local. Tiếp tục thử cài đặt trực tiếp.'
370
- : '⚠️ Could not configure npm global prefix in ~/.local. Falling back to direct install.'));
371
- return false;
372
- }
373
- }
374
-
375
- const userNpmInfo = getUserNpmPrefixInfo();
376
- if (userNpmInfo) {
377
- ensureBinDirOnPath(userNpmInfo.binDir);
378
- }
379
-
380
- function installGlobalPackage(pkg, { isVi, osChoice, displayName }) {
381
- const installCommands = [];
382
-
383
- if (osChoice === 'windows') {
384
- installCommands.push(`npm install -g ${pkg}`);
385
- } else {
386
- ensureUserWritableGlobalNpm({ isVi, osChoice });
387
- installCommands.push(`npm install -g ${pkg}`);
388
- const npmInfo = getUserNpmPrefixInfo();
389
- if (npmInfo) {
390
- installCommands.push(`npm install -g --prefix "${npmInfo.prefixDir.replace(/"/g, '\\"')}" ${pkg}`);
391
- }
392
- }
393
-
394
- for (const cmd of installCommands) {
395
- try {
396
- execSync(cmd, { stdio: 'inherit', shell: true, env: process.env });
397
- return true;
398
- } catch {
399
- // try next candidate
400
- }
401
- }
402
-
403
- console.log(chalk.yellow(isVi
404
- ? `⚠️ Không thể tự cài ${displayName}. Chạy thủ công: ${osChoice === 'windows' ? `npm install -g ${pkg}` : `npm config set prefix ~/.local && npm install -g ${pkg}`}`
405
- : `⚠️ Could not auto-install ${displayName}. Run manually: ${osChoice === 'windows' ? `npm install -g ${pkg}` : `npm config set prefix ~/.local && npm install -g ${pkg}`}`));
406
- return false;
407
- }
408
-
409
- function installLatestOpenClaw({ isVi, osChoice }) {
410
- if (shouldReuseInstalledGlobals() && isOpenClawInstalled()) {
411
- console.log(chalk.green(isVi
412
- ? '\n♻️ Dang dung lai openclaw da cai san de test nhanh.'
413
- : '\n♻️ Reusing the installed openclaw for a faster test run.'));
414
- return;
415
- }
416
-
417
- console.log(chalk.cyan(isVi
418
- ? `\n📦 Dang cai/cap nhat ${OPENCLAW_NPM_SPEC}...`
419
- : `\n📦 Installing/updating ${OPENCLAW_NPM_SPEC}...`));
420
-
421
- if (!installGlobalPackage(OPENCLAW_NPM_SPEC, { isVi, osChoice, displayName: 'openclaw' })) {
422
- process.exit(1);
423
- }
424
-
425
- console.log(chalk.green(isVi
426
- ? `✅ openclaw da duoc ghim dung ban ${OPENCLAW_NPM_SPEC}!`
427
- : `✅ openclaw is now pinned to ${OPENCLAW_NPM_SPEC}!`));
428
- }
429
-
430
- // ─── Shared from docker-gen.js ──────────────────────────────────────────────
431
- // build9RouterSmartRouteSyncScript, indentBlock, build9RouterComposeEntrypointScript
432
- // are imported from setup/shared/docker-gen.js — do NOT re-define here.
433
-
434
- function resolveCommandOnPath(command) {
435
- if (process.platform === 'win32') {
436
- return resolveWindowsCommand(command);
437
- }
438
-
439
- try {
440
- return execSync(`command -v ${command}`, {
441
- stdio: ['ignore', 'pipe', 'ignore'],
442
- encoding: 'utf8',
443
- shell: true,
444
- env: process.env
445
- }).trim() || command;
446
- } catch {
447
- return command;
448
- }
449
- }
450
-
451
- async function writeNative9RouterSyncScript(projectDir) {
452
- const syncScriptPath = path.join(projectDir, '.openclaw', '9router-smart-route-sync.js');
453
- await fs.ensureDir(path.dirname(syncScriptPath));
454
- // Use native home data dir so sync script writes to same place 9router binary reads from
455
- const nativeDataDir = getNative9RouterDataDir();
456
- await fs.ensureDir(nativeDataDir);
457
- await fs.writeFile(syncScriptPath, build9RouterSmartRouteSyncScript(path.join(nativeDataDir, 'db.json')));
458
- return syncScriptPath;
459
- }
460
-
461
- async function writeNative9RouterPatchScript(projectDir) {
462
- const patchScriptPath = path.join(projectDir, '.openclaw', 'patch-9router.js');
463
- await fs.ensureDir(path.dirname(patchScriptPath));
464
- await fs.writeFile(patchScriptPath, build9RouterPatchScript());
465
- return patchScriptPath;
466
- }
467
-
468
- async function patchProject9RouterOpenClawConfig(projectDir) {
469
- const configPath = path.join(projectDir, '.openclaw', 'openclaw.json');
470
- if (!await fs.pathExists(configPath)) return false;
471
- const config = await fs.readJson(configPath);
472
- const provider = config?.models?.providers?.['9router'];
473
- if (!provider) return false;
474
- provider.baseUrl = get9RouterBaseUrl(detectProjectDeployMode(projectDir));
475
- provider.apiKey = NINE_ROUTER_PROXY_API_KEY;
476
- provider.api = 'openai-completions';
477
- provider.models = build9RouterProviderConfig(provider.baseUrl).models;
478
- await fs.writeJson(configPath, config, { spaces: 2 });
479
- return true;
480
- }
481
-
482
- async function patchProjectDocker9Router(projectDir) {
483
- const dockerDir = path.join(projectDir, 'docker', 'openclaw');
484
- const composePath = path.join(dockerDir, 'docker-compose.yml');
485
- if (!await fs.pathExists(composePath)) return false;
486
-
487
- await fs.ensureDir(dockerDir);
488
- await fs.writeFile(path.join(dockerDir, 'sync.js'), build9RouterSmartRouteSyncScript('/root/.9router/db.json'));
489
- await fs.writeFile(path.join(dockerDir, 'patch-9router.js'), build9RouterPatchScript());
490
- let compose = await fs.readFile(composePath, 'utf8');
491
- compose = compose.replace(
492
- /node -e "require\('fs'\)\.writeFileSync\('\/tmp\/sync\.js',Buffer\.from\('[^']*','base64'\)\.toString\(\)\)"/,
493
- "cp /opt/sync.js /tmp/sync.js"
494
- );
495
- compose = compose.replace(
496
- /\s*node -e "require\('fs'\)\.writeFileSync\('\/tmp\/patch-9router\.js',Buffer\.from\('[^']*','base64'\)\.toString\(\)\)"\n/,
497
- ''
498
- );
499
- compose = compose.replace(
500
- /(npm install -g [^\n]+\n)/,
501
- `$1 cp /opt/patch-9router.js /tmp/patch-9router.js\n`
502
- );
503
- if (!compose.includes('node /tmp/patch-9router.js || true')) {
504
- compose = compose.replace(
505
- /(\s*node \/tmp\/sync\.js > \/tmp\/sync\.log 2>&1 &\n)/,
506
- ` node /tmp/patch-9router.js || true\n$1`
507
- );
508
- }
509
- if (!compose.includes('./sync.js:/opt/sync.js:ro')) {
510
- compose = compose.replace(
511
- /(\s*-\s*9router-data:\/root\/\.9router\s*\n)/,
512
- `$1 - ./sync.js:/opt/sync.js:ro\n - ./patch-9router.js:/opt/patch-9router.js:ro\n`
513
- );
514
- }
515
- await fs.writeFile(composePath, compose, 'utf8');
516
- return true;
517
- }
518
-
519
- function getGatewayAllowedOrigins(port) {
520
- const normalizedPort = Number(port) || 18791;
521
- const origins = new Set([
522
- `http://localhost:${normalizedPort}`,
523
- `http://127.0.0.1:${normalizedPort}`,
524
- `http://0.0.0.0:${normalizedPort}`,
525
- ]);
526
- Object.values(os.networkInterfaces() || {}).forEach((entries) => {
527
- (entries || []).forEach((entry) => {
528
- if (!entry || entry.internal || entry.family !== 'IPv4' || !entry.address) return;
529
- origins.add(`http://${entry.address}:${normalizedPort}`);
530
- });
531
- });
532
- return Array.from(origins);
533
- }
534
-
535
- function getReachableDashboardHosts(port) {
536
- const normalizedPort = Number(port) || 18791;
537
- const hosts = [];
538
- const seen = new Set();
539
- const pushHost = (host) => {
540
- if (!host || seen.has(host)) return;
541
- seen.add(host);
542
- hosts.push(`http://${host}:${normalizedPort}`);
543
- };
544
- pushHost('127.0.0.1');
545
- pushHost('localhost');
546
- Object.values(os.networkInterfaces() || {}).forEach((entries) => {
547
- (entries || []).forEach((entry) => {
548
- if (!entry || entry.internal || entry.family !== 'IPv4' || !entry.address) return;
549
- pushHost(entry.address);
550
- });
551
- });
552
- return hosts;
553
- }
554
-
555
- function rewriteDashboardUrlHost(urlText, fallbackPort, targetBaseUrl) {
556
- try {
557
- const parsed = new URL(urlText || `http://127.0.0.1:${fallbackPort}`);
558
- const target = new URL(targetBaseUrl);
559
- parsed.protocol = target.protocol;
560
- parsed.host = target.host;
561
- if (!parsed.pathname || parsed.pathname === '/') {
562
- parsed.pathname = target.pathname;
563
- }
564
- return parsed.toString();
565
- } catch {
566
- return urlText || targetBaseUrl;
567
- }
568
- }
569
-
570
- function extractFirstHttpUrl(text) {
571
- const match = String(text || '').match(/https?:\/\/[^\s"'`]+/);
572
- return match ? match[0] : null;
573
- }
574
-
575
- function getTokenizedDashboardUrl(projectDir) {
576
- try {
577
- const output = execSync('openclaw dashboard', {
578
- cwd: projectDir,
579
- env: process.env,
580
- encoding: 'utf8',
581
- shell: true,
582
- stdio: ['ignore', 'pipe', 'pipe'],
583
- timeout: 15000
584
- });
585
- return extractFirstHttpUrl(output);
586
- } catch (error) {
587
- const combined = `${error?.stdout || ''}\n${error?.stderr || ''}`;
588
- return extractFirstHttpUrl(combined);
589
- }
590
- }
591
-
592
- function printNativeDashboardAccessInfo({ isVi, providerKey, projectDir, gatewayPort = 18791 }) {
593
- const tokenizedUrl = getTokenizedDashboardUrl(projectDir);
594
- const gatewayUrls = getReachableDashboardHosts(gatewayPort);
595
- const dashboardUrl = gatewayUrls[0] || `http://127.0.0.1:${gatewayPort}`;
596
-
597
- console.log(chalk.yellow(`\nDashboard OpenClaw: ${dashboardUrl}`));
598
-
599
- if (tokenizedUrl) {
600
- const rewrittenTokenUrl = rewriteDashboardUrlHost(tokenizedUrl, gatewayPort, dashboardUrl);
601
- console.log(chalk.green(isVi
602
- ? ` Link da kem token: ${rewrittenTokenUrl}`
603
- : ` Tokenized link: ${rewrittenTokenUrl}`));
604
- } else {
605
- console.log(chalk.gray(isVi
606
- ? ' Neu dashboard doi Gateway Token, chay: openclaw dashboard'
607
- : ' If the dashboard asks for a Gateway Token, run: openclaw dashboard'));
608
- }
609
- console.log(chalk.gray(` Other reachable URLs: ${gatewayUrls.join(', ')}`));
610
-
611
- if (providerKey === '9router') {
612
- const routerUrls = getReachableDashboardHosts(20128).map((baseUrl) => `${baseUrl}/dashboard`);
613
- console.log(chalk.yellow(`\n9Router Dashboard: ${routerUrls[0] || 'http://127.0.0.1:20128/dashboard'}`));
614
- console.log(chalk.gray(` Other reachable URLs: ${routerUrls.join(', ')}`));
615
- }
616
- }
617
-
618
- function printZaloPersonalLoginInfo({ isVi, deployMode, projectDir }) {
619
- const nativeCmd = 'openclaw channels login --channel zalouser --verbose';
620
- const dockerCmd = 'docker compose run --rm --no-deps ai-bot openclaw channels login --channel zalouser --verbose';
621
- const dockerStopCmd = 'docker compose stop ai-bot';
622
- const dockerStartCmd = 'docker compose up -d --force-recreate ai-bot';
623
- const dockerStatusCmd = 'docker compose exec ai-bot openclaw channels status --probe';
624
- const dockerReloginCmd = 'docker compose run --rm --no-deps ai-bot openclaw channels logout --channel zalouser && docker compose run --rm --no-deps ai-bot openclaw channels login --channel zalouser --verbose';
625
- const cmd = deployMode === 'native' ? nativeCmd : dockerCmd;
626
- const qrPath = deployMode === 'native'
627
- ? path.join(os.tmpdir(), 'openclaw', 'openclaw-zalouser-qr-default.png')
628
- : '/tmp/openclaw/openclaw-zalouser-qr-default.png';
629
- const copyCmd = deployMode === 'native'
630
- ? (process.platform === 'win32'
631
- ? `Copy-Item "${qrPath}" "${path.join(projectDir, 'zalo-login-qr.png')}"`
632
- : `cp "${qrPath}" "${path.join(projectDir, 'zalo-login-qr.png')}"`)
633
- : `docker cp openclaw-bot:${qrPath} ./zalo-qr.png`;
634
-
635
- console.log(chalk.yellow(`\n📱 ${isVi ? 'Đăng nhập Zalo Personal (1 lần):' : 'Zalo Personal login (one time):'}`));
636
- if (deployMode === 'docker') {
637
- console.log(chalk.white(isVi
638
- ? ` 1. cd ${projectDir}/docker/openclaw`
639
- : ` 1. cd ${projectDir}/docker/openclaw`));
640
- console.log(chalk.white(isVi
641
- ? ` 2. ${dockerStopCmd}`
642
- : ` 2. ${dockerStopCmd}`));
643
- console.log(chalk.white(isVi
644
- ? ` 3. ${cmd}`
645
- : ` 3. ${cmd}`));
646
- console.log(chalk.white(isVi
647
- ? ` 4. Tìm file QR trong container: ${qrPath}`
648
- : ` 4. Find QR image in container: ${qrPath}`));
649
- console.log(chalk.gray(isVi
650
- ? ` → Mở Docker Desktop > container openclaw-bot > tab Files > tìm file trên`
651
- : ` → Open Docker Desktop > container openclaw-bot > Files tab > find file above`));
652
- console.log(chalk.gray(isVi
653
- ? ` → Hoặc chạy: ${copyCmd}`
654
- : ` → Or run: ${copyCmd}`));
655
- console.log(chalk.white(isVi
656
- ? ' 5. Mở app Zalo > Quét QR > quét mã trong file QR'
657
- : ' 5. Open Zalo app > Scan QR > scan the QR image'));
658
- console.log(chalk.white(isVi
659
- ? ' 6. Đợi thấy "Login successful" trong terminal'
660
- : ' 6. Wait for "Login successful" in terminal'));
661
- console.log(chalk.white(isVi
662
- ? ` 7. ${dockerStartCmd}`
663
- : ` 7. ${dockerStartCmd}`));
664
- console.log(chalk.white(isVi
665
- ? ` 8. ${dockerStatusCmd} # phải thấy: running`
666
- : ` 8. ${dockerStatusCmd} # should show: running`));
667
- console.log(chalk.gray(isVi
668
- ? ` Nếu probe báo chưa auth: ${dockerReloginCmd}`
669
- : ` If the probe says unauthenticated: ${dockerReloginCmd}`));
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'child_process';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ function isLocalRepo() {
11
+ const pathsToTry = [
12
+ path.resolve(__dirname, '..'),
13
+ path.resolve(__dirname, '../..')
14
+ ];
15
+ for (const rootDir of pathsToTry) {
16
+ const pkgPath = path.join(rootDir, 'package.json');
17
+ if (fs.existsSync(pkgPath)) {
18
+ try {
19
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
20
+ if (pkg.name === 'create-openclaw-bot' && fs.existsSync(path.join(rootDir, 'src', 'cli', 'cli.src.js'))) {
21
+ return true;
22
+ }
23
+ } catch {}
24
+ }
25
+ }
26
+ return false;
27
+ }
28
+
29
+ const args = process.argv.slice(2);
30
+
31
+ if (isLocalRepo()) {
32
+ const { startLocalInstaller } = await import('./server/local-server.js');
33
+
34
+ if (args[0] === 'legacy' || args.includes('--legacy')) {
35
+ await import('./legacy-cli.js');
670
36
  } else {
671
- console.log(chalk.white(` cd ${projectDir} ${process.platform === 'win32' ? ';' : '&&'} ${cmd}`));
672
- console.log(chalk.gray(isVi
673
- ? ` → File QR sẽ tạo tại: ${qrPath}`
674
- : ` → QR file will be created at: ${qrPath}`));
675
- if (process.platform === 'win32') {
676
- console.log(chalk.gray(isVi
677
- ? ` → Copy QR ra project: ${copyCmd}`
678
- : ` → Copy QR to project: ${copyCmd}`));
679
- }
680
- }
681
- }
682
-
683
- async function waitForFile(filePath, timeoutMs = 15000, intervalMs = 500) {
684
- const deadline = Date.now() + timeoutMs;
685
- while (Date.now() < deadline) {
686
- if (await fs.pathExists(filePath)) {
687
- return true;
688
- }
689
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
690
- }
691
- return fs.pathExists(filePath);
692
- }
693
-
694
- async function writeGeneratedArtifacts(targetDir, artifacts = []) {
695
- for (const artifact of artifacts.filter(Boolean)) {
696
- const artifactPath = path.join(targetDir, artifact.name);
697
- await fs.writeFile(artifactPath, artifact.content, 'utf8');
698
- if (artifact.executable || artifact.name.endsWith('.sh')) {
699
- try { await fs.chmod(artifactPath, 0o755); } catch (_) {}
700
- }
701
- }
702
- }
703
-
704
- function extractZaloPairingCode(text) {
705
- const value = String(text || '');
706
- const explicitCommandMatch = value.match(/openclaw pairing approve zalouser\s+([A-Z0-9-]+)/i);
707
- if (explicitCommandMatch) {
708
- return explicitCommandMatch[1].trim();
709
- }
710
-
711
- const pairingBlockMatch = value.match(/Pairing code:\s*`{0,3}\s*([A-Z0-9-]{6,})/i);
712
- if (pairingBlockMatch) {
713
- return pairingBlockMatch[1].trim();
714
- }
715
-
716
- return null;
717
- }
718
-
719
- function approveZaloPairingCode({ pairingCode, projectDir, isVi }) {
720
- try {
721
- execSync(`openclaw pairing approve zalouser ${pairingCode}`, {
722
- cwd: projectDir,
723
- stdio: 'inherit',
724
- shell: true,
725
- env: process.env
726
- });
727
- console.log(chalk.green(isVi
728
- ? `✅ Da tu dong approve pairing code Zalo: ${pairingCode}`
729
- : `✅ Automatically approved the Zalo pairing code: ${pairingCode}`));
730
- return true;
731
- } catch {
732
- console.log(chalk.yellow(isVi
733
- ? `⚠️ Khong the tu dong approve pairing code ${pairingCode}. Ban co the chay thu cong: openclaw pairing approve zalouser ${pairingCode}`
734
- : `⚠️ Could not auto-approve pairing code ${pairingCode}. You can run it manually: openclaw pairing approve zalouser ${pairingCode}`));
735
- return false;
736
- }
737
- }
738
-
739
- async function runNativeZaloPersonalLoginFlow({ isVi, projectDir }) {
740
- const qrSourcePath = path.join(os.tmpdir(), 'openclaw', 'openclaw-zalouser-qr-default.png');
741
- const qrProjectPath = path.join(projectDir, 'zalo-login-qr.png');
742
- console.log(chalk.yellow(`\n📱 ${isVi ? 'Đang tạo QR đăng nhập Zalo Personal...' : 'Generating the Zalo Personal login QR...'}`));
743
- const loginStartedAt = Date.now();
744
-
745
- try {
746
- await fs.remove(qrSourcePath);
747
- } catch {
748
- // ignore stale tmp QR cleanup failures
749
- }
750
-
751
- try {
752
- await fs.remove(qrProjectPath);
753
- } catch {
754
- // ignore stale project QR cleanup failures
755
- }
756
-
757
- const child = spawn('openclaw', ['channels', 'login', '--channel', 'zalouser', '--verbose'], {
758
- cwd: projectDir,
759
- stdio: ['inherit', 'pipe', 'pipe'],
760
- shell: process.platform === 'win32'
761
- });
762
-
763
- let loginSucceeded = false;
764
- let approvedPairingCode = null;
765
- let outputBuffer = '';
766
- const successPattern = /login successful|logged in successfully|channel login successful/i;
767
- const forwardChunk = (chunk, target) => {
768
- const text = chunk.toString();
769
- outputBuffer = `${outputBuffer}${text}`.slice(-8000);
770
- if (successPattern.test(text)) {
771
- loginSucceeded = true;
772
- }
773
- const pairingCode = extractZaloPairingCode(outputBuffer);
774
- if (pairingCode && pairingCode !== approvedPairingCode) {
775
- if (approveZaloPairingCode({ pairingCode, projectDir, isVi })) {
776
- approvedPairingCode = pairingCode;
777
- }
778
- }
779
- target.write(text);
780
- };
781
-
782
- child.stdout?.on('data', (chunk) => forwardChunk(chunk, process.stdout));
783
- child.stderr?.on('data', (chunk) => forwardChunk(chunk, process.stderr));
784
-
785
- let qrCopied = false;
786
- const copyQrIfReady = async () => {
787
- if (qrCopied) return;
788
- if (await waitForFile(qrSourcePath, 500, 250)) {
789
- const qrStats = await fs.stat(qrSourcePath).catch(() => null);
790
- if (!qrStats || qrStats.mtimeMs < loginStartedAt) {
791
- return;
792
- }
793
- await fs.copy(qrSourcePath, qrProjectPath, { overwrite: true });
794
- qrCopied = true;
795
- console.log(chalk.green(isVi
796
- ? `✅ QR đã được copy vào: ${qrProjectPath}`
797
- : `✅ QR copied to: ${qrProjectPath}`));
798
- }
799
- };
800
-
801
- const watcher = setInterval(() => {
802
- copyQrIfReady().catch(() => {});
803
- }, 750);
804
-
805
- const exitCode = await new Promise((resolve) => {
806
- child.on('close', (code) => resolve(code ?? 0));
807
- child.on('error', () => resolve(1));
808
- });
809
- clearInterval(watcher);
810
- await copyQrIfReady();
811
-
812
- if (exitCode !== 0 && !loginSucceeded) {
813
- console.log(chalk.yellow(isVi
814
- ? '⚠️ Chưa hoàn tất đăng nhập Zalo trong lúc setup. Bạn có thể chạy lại lệnh login thủ công sau.'
815
- : '⚠️ Zalo login was not completed during setup. You can run the login command manually afterwards.'));
816
- printZaloPersonalLoginInfo({ isVi, deployMode: 'native', projectDir });
817
- } else if (loginSucceeded && exitCode !== 0) {
818
- console.log(chalk.green(isVi
819
- ? '✅ Đăng nhập Zalo đã thành công dù CLI trả về trạng thái không chuẩn.'
820
- : '✅ Zalo login succeeded even though the CLI returned a non-standard exit status.'));
821
- }
822
- }
823
-
824
- function runPm2Save({ projectDir, isVi }) {
825
- try {
826
- execSync('pm2 save', {
827
- cwd: projectDir,
828
- stdio: 'inherit',
829
- shell: true,
830
- env: process.env
831
- });
832
- } catch {
833
- console.log(chalk.yellow(isVi
834
- ? '⚠️ PM2 save khong hoan tat. Bot van co the dang chay, nhung hay thu chay lai `pm2 save` sau.'
835
- : '⚠️ PM2 save did not complete. The app may still be running, but try `pm2 save` again afterwards.'));
836
- }
837
- }
838
-
839
- function getDetectedOsChoice() {
840
- const detectedPlatform = process.platform;
841
- return detectedPlatform === 'win32' ? 'windows'
842
- : detectedPlatform === 'darwin' ? 'macos'
843
- : 'vps';
844
- }
845
-
846
- function getCliSubcommand() {
847
- return String(process.argv[2] || '').trim().toLowerCase();
848
- }
849
-
850
- function findProjectDir(startDir = process.cwd()) {
851
- let currentDir = path.resolve(startDir);
852
-
853
- while (true) {
854
- if (
855
- fs.existsSync(path.join(currentDir, '.openclaw'))
856
- || fs.existsSync(path.join(currentDir, 'docker', 'openclaw'))
857
- ) {
858
- return currentDir;
859
- }
860
-
861
- const isDockerOpenClawDir =
862
- path.basename(currentDir).toLowerCase() === 'openclaw'
863
- && path.basename(path.dirname(currentDir)).toLowerCase() === 'docker';
864
- if (isDockerOpenClawDir) {
865
- const projectDir = path.dirname(path.dirname(currentDir));
866
- if (
867
- fs.existsSync(path.join(projectDir, '.openclaw'))
868
- || fs.existsSync(path.join(currentDir, 'docker-compose.yml'))
869
- ) {
870
- return projectDir;
871
- }
872
- }
873
-
874
- const parentDir = path.dirname(currentDir);
875
- if (parentDir === currentDir) {
876
- return null;
877
- }
878
- currentDir = parentDir;
879
- }
880
- }
881
-
882
- function detectProjectDeployMode(projectDir) {
883
- const dockerDir = path.join(projectDir, 'docker', 'openclaw');
884
- if (
885
- fs.existsSync(path.join(dockerDir, 'docker-compose.yml'))
886
- || fs.existsSync(path.join(dockerDir, '.env'))
887
- ) {
888
- return 'docker';
889
- }
890
- return 'native';
891
- }
892
-
893
- function detectProjectBotName(projectDir) {
894
- try {
895
- const configPath = path.join(projectDir, '.openclaw', 'openclaw.json');
896
- if (fs.existsSync(configPath)) {
897
- const config = fs.readJsonSync(configPath);
898
- const firstAgentId = config?.agents?.list?.[0]?.id;
899
- if (firstAgentId) {
900
- return firstAgentId;
901
- }
902
- }
903
- } catch {
904
- // fallback below
905
- }
906
- return path.basename(projectDir);
907
- }
908
-
909
- function detectProjectUses9Router(projectDir) {
910
- try {
911
- const configPath = path.join(projectDir, '.openclaw', 'openclaw.json');
912
- if (fs.existsSync(configPath)) {
913
- const config = fs.readJsonSync(configPath);
914
- if (config?.models?.providers?.['9router']) {
915
- return true;
916
- }
917
- }
918
- } catch {
919
- // fallback below
920
- }
921
- return fs.existsSync(path.join(projectDir, '.9router'));
922
- }
923
-
924
- function detectProjectIsMultiBot(projectDir) {
925
- try {
926
- const configPath = path.join(projectDir, '.openclaw', 'openclaw.json');
927
- if (fs.existsSync(configPath)) {
928
- const config = fs.readJsonSync(configPath);
929
- return (config?.agents?.list?.length || 0) > 1;
930
- }
931
- } catch {
932
- // fallback below
933
- }
934
- return false;
935
- }
936
-
937
- function getNativePm2AppName(isMultiBot = false) {
938
- return isMultiBot ? 'openclaw-multibot' : 'openclaw';
939
- }
940
-
941
- async function runUpgradeCommand() {
942
- const projectDir = findProjectDir();
943
- if (!projectDir) {
944
- console.error(chalk.red('Error: no OpenClaw project found in the current directory tree.'));
945
- console.error(chalk.yellow('Run this inside the bot project folder that contains .openclaw or docker/openclaw.'));
946
- process.exit(1);
947
- }
948
-
949
- const deployMode = detectProjectDeployMode(projectDir);
950
- const osChoice = getDetectedOsChoice();
951
- const botName = detectProjectBotName(projectDir);
952
- const is9Router = detectProjectUses9Router(projectDir);
953
- const isMultiBot = detectProjectIsMultiBot(projectDir);
954
-
955
- console.log(chalk.cyan('\nRefreshing generated OpenClaw project artifacts...'));
956
- console.log(chalk.gray(` Project: ${projectDir}`));
957
- console.log(chalk.gray(` Mode: ${deployMode}`));
958
-
959
- await writeGeneratedArtifacts(projectDir, buildCliChromeDebugArtifacts());
960
- await writeGeneratedArtifacts(projectDir, buildCliUninstallArtifacts({
961
- deployMode,
962
- osChoice,
963
- projectDir,
964
- botName: (deployMode !== 'docker' && osChoice === 'vps')
965
- ? getNativePm2AppName(isMultiBot)
966
- : botName,
967
- }));
968
- await writeGeneratedArtifacts(projectDir, buildCliUpgradeArtifacts());
969
-
970
- if (deployMode !== 'docker') {
971
- await writeGeneratedArtifacts(projectDir, buildCliStartBotArtifacts({
972
- projectDir,
973
- openclawHome: path.join(projectDir, '.openclaw'),
974
- is9Router,
975
- osChoice,
976
- isMultiBot,
977
- appName: getNativePm2AppName(isMultiBot),
978
- isVi: false,
979
- }));
980
- }
981
-
982
- if (is9Router) {
983
- await writeNative9RouterPatchScript(projectDir);
984
- await patchProject9RouterOpenClawConfig(projectDir);
985
- if (deployMode === 'docker') {
986
- await patchProjectDocker9Router(projectDir);
987
- } else {
988
- await writeNative9RouterSyncScript(projectDir);
989
- try {
990
- execFileSync(process.execPath, [path.join(projectDir, '.openclaw', 'patch-9router.js')], {
991
- cwd: projectDir,
992
- stdio: 'ignore',
993
- });
994
- } catch {
995
- // Best effort: start scripts also retry the patch before launch.
996
- }
997
- }
998
- }
999
-
1000
- console.log(chalk.green('\nUpgrade artifacts refreshed successfully.'));
1001
- if (deployMode === 'docker') {
1002
- console.log(chalk.white(` Next: cd ${path.join(projectDir, 'docker', 'openclaw')} && docker compose up -d --build`));
1003
- } else {
1004
- console.log(chalk.white(` Next: run ${process.platform === 'win32' ? '.\\start-bot.bat' : './start-bot.sh'} from ${projectDir}`));
1005
- }
1006
- }
1007
-
1008
- function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
1009
- const routerAppName = `${appName}-9router`;
1010
- const syncAppName = `${appName}-9router-sync`;
1011
- const routerLaunch = resolveNative9RouterDesktopLaunch();
1012
- const normalizedProjectDir = projectDir.replace(/\\/g, '/');
1013
- const normalizedSyncScriptPath = syncScriptPath ? syncScriptPath.replace(/\\/g, '/') : '';
1014
- try {
1015
- execSync(`pm2 delete ${routerAppName}`, {
1016
- cwd: projectDir,
1017
- stdio: 'ignore',
1018
- shell: true
1019
- });
1020
- } catch {
1021
- // ignore missing app
1022
- }
1023
- execFileSync('pm2', [
1024
- 'start',
1025
- routerLaunch.command,
1026
- '--name',
1027
- routerAppName,
1028
- '--cwd',
1029
- normalizedProjectDir,
1030
- '--interpreter',
1031
- 'none',
1032
- '--',
1033
- ...routerLaunch.args
1034
- ], {
1035
- cwd: projectDir,
1036
- stdio: 'inherit',
1037
- env: { ...process.env, ...routerLaunch.env }
1038
- });
1039
- if (syncScriptPath) {
1040
- try {
1041
- execSync(`pm2 delete ${syncAppName}`, {
1042
- cwd: projectDir,
1043
- stdio: 'ignore',
1044
- shell: true
1045
- });
1046
- } catch {
1047
- // ignore missing app
1048
- }
1049
- try {
1050
- execFileSync('pm2', [
1051
- 'start',
1052
- normalizedSyncScriptPath,
1053
- '--name',
1054
- syncAppName,
1055
- '--cwd',
1056
- normalizedProjectDir,
1057
- '--interpreter',
1058
- process.execPath,
1059
- '--no-autorestart',
1060
- ], {
1061
- cwd: projectDir,
1062
- stdio: 'inherit',
1063
- env: process.env
1064
- });
1065
- } catch (syncErr) {
1066
- console.log(chalk.yellow(isVi
1067
- ? `\n⚠️ Khong the tu dong khoi dong sync script qua PM2.`
1068
- : `\n⚠️ Could not auto-start 9router sync script via PM2.`));
1069
- }
1070
- }
1071
- runPm2Save({ projectDir, isVi });
1072
- console.log(chalk.green(`\n✅ ${isVi ? '9Router da duoc khoi dong qua PM2.' : '9Router is running via PM2.'}`));
1073
- console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${routerAppName}` : ` View logs: pm2 logs ${routerAppName}`));
1074
- }
1075
-
1076
- async function ensureProjectRuntimeDirs(projectDir, isVi) {
1077
- await fs.ensureDir(path.join(projectDir, '.openclaw'));
1078
- await fs.ensureDir(getProject9RouterDataDir(projectDir));
1079
- console.log(chalk.green(`\n✅ ${isVi
1080
- ? 'Da chuan bi runtime directories local trong project.'
1081
- : 'Prepared project-local runtime directories.'}`));
1082
- }
1083
-
1084
- // buildTelegramPostInstallChecklist is imported from setup/shared/common-gen.js
1085
-
1086
- // ─── Docker Auto-Detection ───────────────────────────────────────────────────
1087
- function isDockerInstalled() {
1088
- try {
1089
- execSync('docker --version', { stdio: 'ignore' });
1090
- return true;
1091
- } catch { return false; }
1092
- }
1093
-
1094
-
1095
-
1096
- const LOGO = `
1097
- ████████╗██╗ ██╗ █████╗ ███╗ ██╗███╗ ███╗██╗███╗ ██╗██╗ ██╗██╗ ██╗ ██████╗ ██╗ ███████╗
1098
- ╚══██╔══╝██║ ██║██╔══██╗████╗ ██║████╗ ████║██║████╗ ██║██║ ██║██║ ██║██╔═══██╗██║ ██╔════╝
1099
- ██║ ██║ ██║███████║██╔██╗ ██║██╔████╔██║██║██╔██╗ ██║███████║███████║██║ ██║██║ █████╗
1100
- ██║ ██║ ██║██╔══██║██║╚██╗██║██║╚██╔╝██║██║██║╚██╗██║██╔══██║██╔══██║██║ ██║██║ ██╔══╝
1101
- ██║ ╚██████╔╝██║ ██║██║ ╚████║██║ ╚═╝ ██║██║██║ ╚████║██║ ██║██║ ██║╚██████╔╝███████╗███████╗
1102
- ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝
1103
- `;
1104
-
1105
- // ── Data constants from setup/data/index.js (single source of truth) ──────────
1106
- const CHANNELS = _CHANNELS;
1107
- const PROVIDERS = _PROVIDERS;
1108
- const SKILLS = _SKILLS;
1109
-
1110
- function providerSupportsMemoryEmbeddings(providerKey) {
1111
- const providerCapabilities = {
1112
- '9router': { supportsEmbeddings: true },
1113
- openai: { supportsEmbeddings: true },
1114
- anthropic: { supportsEmbeddings: false },
1115
- ollama: { supportsEmbeddings: false },
1116
- google: { supportsEmbeddings: true },
1117
- gemini: { supportsEmbeddings: true },
1118
- };
1119
- if (providerCapabilities[providerKey]) {
1120
- return providerCapabilities[providerKey].supportsEmbeddings;
1121
- }
1122
- return !!PROVIDERS[providerKey]?.supportsEmbeddings;
1123
- }
1124
-
1125
- function getCliSkillChoices({ providerKey, isVi }) {
1126
- const memoryRecommended = providerSupportsMemoryEmbeddings(providerKey);
1127
- return SKILLS
1128
- .filter((skill) => skill.value !== 'memory' || providerSupportsMemoryEmbeddings(providerKey) || skill.id === 'memory')
1129
- .map((skill) => {
1130
- const value = skill.value || skill.id;
1131
- let name = `${skill.icon || ''} ${isVi ? (skill.nameVi || skill.name) : (skill.nameEn || skill.name)}`.trim();
1132
- if (value === 'memory') {
1133
- name = isVi
1134
- ? (memoryRecommended ? '🧠 Long-term Memory ⭐(Khuyên dùng)' : '🧠 Long-term Memory')
1135
- : (memoryRecommended ? '🧠 Long-term Memory ⭐(Recommended)' : '🧠 Long-term Memory');
1136
- }
1137
- return {
1138
- name,
1139
- value,
1140
- checked: value === 'browser' || value === 'scheduler' || (value === 'memory' && memoryRecommended),
1141
- };
1142
- });
1143
- }
1144
-
1145
- const CLI_BACK = '__openclaw_cli_back__';
1146
-
1147
- function getBackChoice(isVi) {
1148
- return {
1149
- name: isVi ? '← Quay lại' : '← Back',
1150
- value: CLI_BACK,
1151
- };
1152
- }
1153
-
1154
- function withBackHint(message, isVi) {
1155
- return `${message} ${isVi ? '(gõ "back" để quay lại)' : '(type "back" to go back)'}`;
1156
- }
1157
-
1158
- async function selectWithBack({ message, choices, defaultValue, allowBack = false, isVi = true }) {
1159
- const finalChoices = allowBack ? [...choices, getBackChoice(isVi)] : choices;
1160
- return select({
1161
- message,
1162
- choices: finalChoices,
1163
- ...(defaultValue ? { default: defaultValue } : {}),
1164
- });
1165
- }
1166
-
1167
- async function inputWithBack({ message, defaultValue = '', required = false, allowBack = false, isVi = true }) {
1168
- const value = await input({
1169
- message: allowBack ? withBackHint(message, isVi) : message,
1170
- default: defaultValue,
1171
- required,
1172
- });
1173
- if (allowBack && String(value || '').trim().toLowerCase() === 'back') {
1174
- return CLI_BACK;
1175
- }
1176
- return value;
1177
- }
1178
-
1179
- async function checkboxWithBack({ message, choices, isVi = true, allowBack = false }) {
1180
- const finalChoices = allowBack ? [...choices, getBackChoice(isVi)] : choices;
1181
- const value = await checkbox({
1182
- message,
1183
- choices: finalChoices,
1184
- });
1185
- return allowBack && value.includes(CLI_BACK) ? CLI_BACK : value;
1186
- }
1187
-
1188
- async function collectBotSetupStep({
1189
- isVi,
1190
- channelKey,
1191
- channel,
1192
- existingBots = [],
1193
- existingBotCount = 1,
1194
- existingGroupId = '',
1195
- }) {
1196
- let botCount = channelKey === 'telegram' ? existingBotCount : 1;
1197
- let groupId = existingGroupId;
1198
- const bots = [];
1199
-
1200
- if (channelKey === 'telegram') {
1201
- const botCountValue = await selectWithBack({
1202
- message: isVi ? 'Bạn muốn cài bao nhiêu Telegram bot?' : 'How many Telegram bots do you want to deploy?',
1203
- choices: [
1204
- { name: '1 bot (single)', value: '1' },
1205
- { name: '2 bots (Department Room)', value: '2' },
1206
- { name: '3 bots', value: '3' },
1207
- { name: '4 bots', value: '4' },
1208
- { name: '5 bots', value: '5' },
1209
- ],
1210
- defaultValue: String(existingBotCount || 1),
1211
- allowBack: true,
1212
- isVi,
1213
- });
1214
- if (botCountValue === CLI_BACK) {
1215
- return { back: true };
1216
- }
1217
- botCount = parseInt(botCountValue, 10);
1218
-
1219
- if (botCount > 1) {
1220
- const groupOption = await selectWithBack({
1221
- message: isVi ? 'Bạn có sẵn Telegram Group chưa?' : 'Do you already have a Telegram Group?',
1222
- choices: [
1223
- { name: isVi ? '✨ Tôi sẽ tạo sau (nhập Group ID vào .env sau khi setup)' : '✨ I\'ll create later (add Group ID to .env after setup)', value: 'create' },
1224
- { name: isVi ? '🔗 Đã có group — nhập Group ID ngay' : '🔗 Already have a group — enter Group ID now', value: 'existing' }
1225
- ],
1226
- defaultValue: groupId ? 'existing' : 'create',
1227
- allowBack: true,
1228
- isVi,
1229
- });
1230
- if (groupOption === CLI_BACK) {
1231
- return { back: true };
1232
- }
1233
-
1234
- if (groupOption === 'existing') {
1235
- console.log(chalk.dim(isVi
1236
- ? '\n 📌 Cách lấy Group ID:\n 1. Mở Telegram → tìm @userinfobot\n 2. Bấm Start để bắt đầu → chọn nút "Group" trên màn hình → chọn Group bạn muốn thêm bot vào\n 3. Bot sẽ trả về "Chat ID" — đó là Group ID (bắt đầu bằng -100)\n 👉 https://t.me/userinfobot\n'
1237
- : '\n 📌 How to get Group ID:\n 1. Open Telegram → find @userinfobot\n 2. Click Start → select "Group" button on the screen → select the group you want to add the bot to\n 3. The bot replies with "Chat ID" — that is your Group ID (starts with -100)\n 👉 https://t.me/userinfobot\n'));
1238
- const nextGroupId = await inputWithBack({
1239
- message: isVi ? 'Telegram Group ID (VD: -1001234567890):' : 'Telegram Group ID (e.g. -1001234567890):',
1240
- defaultValue: groupId,
1241
- allowBack: true,
1242
- isVi,
1243
- });
1244
- if (nextGroupId === CLI_BACK) {
1245
- return { back: true };
1246
- }
1247
- groupId = nextGroupId;
1248
- } else {
1249
- groupId = '';
1250
- }
1251
- } else {
1252
- groupId = '';
1253
- }
1254
-
1255
- for (let i = 0; i < botCount; i++) {
1256
- console.log(chalk.bold(`\n${isVi ? `─── Bot ${i + 1} / ${botCount} ───` : `─── Bot ${i + 1} / ${botCount} ───`}`));
1257
- const defaults = existingBots[i] || {};
1258
- const fields = [
1259
- {
1260
- key: 'name',
1261
- message: isVi ? `Tên Bot ${i + 1}:` : `Bot ${i + 1} name:`,
1262
- defaultValue: defaults.name || `Bot ${i + 1}`,
1263
- required: true,
1264
- },
1265
- {
1266
- key: 'slashCmd',
1267
- message: isVi ? `Slash command (VD: /bot${i + 1}):` : `Slash command (e.g. /bot${i + 1}):`,
1268
- defaultValue: defaults.slashCmd || `/bot${i + 1}`,
1269
- required: true,
1270
- },
1271
- {
1272
- key: 'desc',
1273
- message: isVi ? `Mô tả Bot ${i + 1} (VD: Trợ lý AI cá nhân):` : `Bot ${i + 1} description (e.g. Personal AI assistant):`,
1274
- defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1275
- required: true,
1276
- },
1277
- {
1278
- key: 'persona',
1279
- message: isVi ? `Tính cách & quy tắc Bot ${i + 1} (VD: siêu thân thiện, nhiều emoji):` : `Bot ${i + 1} persona & rules (e.g. friendly, uses emojis):`,
1280
- defaultValue: defaults.persona || '',
1281
- required: false,
1282
- },
1283
- {
1284
- key: 'token',
1285
- message: isVi ? 'Bot Token (từ @BotFather):' : 'Bot Token (from @BotFather):',
1286
- defaultValue: defaults.token || '',
1287
- required: true,
1288
- },
1289
- ];
1290
-
1291
- const draft = { ...defaults };
1292
- let fieldIndex = 0;
1293
- while (fieldIndex < fields.length) {
1294
- const field = fields[fieldIndex];
1295
- const value = await inputWithBack({
1296
- message: field.message,
1297
- defaultValue: draft[field.key] || field.defaultValue,
1298
- required: field.required,
1299
- allowBack: true,
1300
- isVi,
1301
- });
1302
- if (value === CLI_BACK) {
1303
- if (fieldIndex > 0) {
1304
- fieldIndex--;
1305
- continue;
1306
- }
1307
- return { back: true };
1308
- }
1309
- draft[field.key] = value;
1310
- fieldIndex++;
1311
- }
1312
- bots.push(draft);
1313
- }
1314
- } else if (channelKey !== 'zalo-personal') {
1315
- const defaults = existingBots[0] || {};
1316
- const fields = [
1317
- {
1318
- key: 'name',
1319
- message: isVi ? 'Tên Bot:' : 'Bot Name:',
1320
- defaultValue: defaults.name || 'Chat Bot',
1321
- required: true,
1322
- },
1323
- {
1324
- key: 'desc',
1325
- message: isVi ? 'Mô tả Bot:' : 'Bot Description:',
1326
- defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1327
- required: true,
1328
- },
1329
- {
1330
- key: 'persona',
1331
- message: isVi ? 'Tính cách & quy tắc (VD: gọn gàng, thân thiện):' : 'Persona & rules (e.g. concise, friendly):',
1332
- defaultValue: defaults.persona || '',
1333
- required: false,
1334
- },
1335
- {
1336
- key: 'token',
1337
- message: isVi ? `Nhập ${channel.name} Token:` : `Enter ${channel.name} Token:`,
1338
- defaultValue: defaults.token || '',
1339
- required: true,
1340
- },
1341
- ];
1342
- const draft = { ...defaults, slashCmd: '' };
1343
- let fieldIndex = 0;
1344
- while (fieldIndex < fields.length) {
1345
- const field = fields[fieldIndex];
1346
- const value = await inputWithBack({
1347
- message: field.message,
1348
- defaultValue: draft[field.key] || field.defaultValue,
1349
- required: field.required,
1350
- allowBack: true,
1351
- isVi,
1352
- });
1353
- if (value === CLI_BACK) {
1354
- if (fieldIndex > 0) {
1355
- fieldIndex--;
1356
- continue;
1357
- }
1358
- return { back: true };
1359
- }
1360
- draft[field.key] = value;
1361
- fieldIndex++;
1362
- }
1363
- bots.push(draft);
1364
- } else {
1365
- bots.push({ name: 'Bot', slashCmd: '', desc: '', persona: '', token: '' });
1366
- }
1367
-
1368
- return {
1369
- back: false,
1370
- botCount,
1371
- groupId,
1372
- bots,
1373
- botToken: bots[0]?.token || '',
1374
- };
1375
- }
1376
-
1377
- async function collectBotSetupStepWithGroupBack(options) {
1378
- const {
1379
- isVi,
1380
- channelKey,
1381
- channel,
1382
- existingBots = [],
1383
- existingBotCount = 1,
1384
- existingGroupId = '',
1385
- } = options;
1386
-
1387
- let botCount = channelKey === 'telegram' ? existingBotCount : 1;
1388
- let groupId = existingGroupId;
1389
- const bots = [];
1390
-
1391
- if (channelKey === 'telegram') {
1392
- const botCountValue = await selectWithBack({
1393
- message: isVi ? 'Bạn muốn cài bao nhiêu Telegram bot?' : 'How many Telegram bots do you want to deploy?',
1394
- choices: [
1395
- { name: '1 bot (single)', value: '1' },
1396
- { name: '2 bots (Department Room)', value: '2' },
1397
- { name: '3 bots', value: '3' },
1398
- { name: '4 bots', value: '4' },
1399
- { name: '5 bots', value: '5' },
1400
- ],
1401
- defaultValue: String(existingBotCount || 1),
1402
- allowBack: true,
1403
- isVi,
1404
- });
1405
- if (botCountValue === CLI_BACK) {
1406
- return { back: true };
1407
- }
1408
- botCount = parseInt(botCountValue, 10);
1409
-
1410
- if (botCount > 1) {
1411
- while (true) {
1412
- const groupOption = await selectWithBack({
1413
- message: isVi ? 'Bạn có sẵn Telegram Group chưa?' : 'Do you already have a Telegram Group?',
1414
- choices: [
1415
- { name: isVi ? '✨ Tôi sẽ tạo sau (nhập Group ID vào .env sau khi setup)' : '✨ I\'ll create later (add Group ID to .env after setup)', value: 'create' },
1416
- { name: isVi ? '🔗 Đã có group — nhập Group ID ngay' : '🔗 Already have a group — enter Group ID now', value: 'existing' }
1417
- ],
1418
- defaultValue: groupId ? 'existing' : 'create',
1419
- allowBack: true,
1420
- isVi,
1421
- });
1422
- if (groupOption === CLI_BACK) {
1423
- return { back: true };
1424
- }
1425
-
1426
- if (groupOption === 'existing') {
1427
- console.log(chalk.dim(isVi
1428
- ? '\n 📌 Cách lấy Group ID:\n 1. Mở Telegram → tìm @userinfobot\n 2. Bấm Start để bắt đầu → chọn nút "Group" trên màn hình → chọn Group bạn muốn thêm bot vào\n 3. Bot sẽ trả về "Chat ID" — đó là Group ID (bắt đầu bằng -100)\n 👉 https://t.me/userinfobot\n'
1429
- : '\n 📌 How to get Group ID:\n 1. Open Telegram → find @userinfobot\n 2. Click Start → select "Group" button on the screen → select the group you want to add the bot to\n 3. The bot replies with "Chat ID" — that is your Group ID (starts with -100)\n 👉 https://t.me/userinfobot\n'));
1430
- const nextGroupId = await inputWithBack({
1431
- message: isVi ? 'Telegram Group ID (VD: -1001234567890):' : 'Telegram Group ID (e.g. -1001234567890):',
1432
- defaultValue: groupId,
1433
- allowBack: true,
1434
- isVi,
1435
- });
1436
- if (nextGroupId === CLI_BACK) {
1437
- continue;
1438
- }
1439
- groupId = nextGroupId;
1440
- break;
1441
- }
1442
-
1443
- groupId = '';
1444
- break;
1445
- }
1446
- } else {
1447
- groupId = '';
1448
- }
1449
-
1450
- for (let i = 0; i < botCount; i++) {
1451
- console.log(chalk.bold(`\n${isVi ? `─── Bot ${i + 1} / ${botCount} ───` : `─── Bot ${i + 1} / ${botCount} ───`}`));
1452
- const defaults = existingBots[i] || {};
1453
- const fields = [
1454
- {
1455
- key: 'name',
1456
- message: isVi ? `Tên Bot ${i + 1}:` : `Bot ${i + 1} name:`,
1457
- defaultValue: defaults.name || `Bot ${i + 1}`,
1458
- required: true,
1459
- },
1460
- {
1461
- key: 'slashCmd',
1462
- message: isVi ? `Slash command (VD: /bot${i + 1}):` : `Slash command (e.g. /bot${i + 1}):`,
1463
- defaultValue: defaults.slashCmd || `/bot${i + 1}`,
1464
- required: true,
1465
- },
1466
- {
1467
- key: 'desc',
1468
- message: isVi ? `Mô tả Bot ${i + 1} (VD: Trợ lý AI cá nhân):` : `Bot ${i + 1} description (e.g. Personal AI assistant):`,
1469
- defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1470
- required: true,
1471
- },
1472
- {
1473
- key: 'persona',
1474
- message: isVi ? `Tính cách & quy tắc Bot ${i + 1} (VD: siêu thân thiện, nhiều emoji):` : `Bot ${i + 1} persona & rules (e.g. friendly, uses emojis):`,
1475
- defaultValue: defaults.persona || '',
1476
- required: false,
1477
- },
1478
- {
1479
- key: 'token',
1480
- message: isVi ? 'Bot Token (từ @BotFather):' : 'Bot Token (from @BotFather):',
1481
- defaultValue: defaults.token || '',
1482
- required: true,
1483
- },
1484
- ];
1485
-
1486
- const draft = { ...defaults };
1487
- let fieldIndex = 0;
1488
- while (fieldIndex < fields.length) {
1489
- const field = fields[fieldIndex];
1490
- const value = await inputWithBack({
1491
- message: field.message,
1492
- defaultValue: draft[field.key] || field.defaultValue,
1493
- required: field.required,
1494
- allowBack: true,
1495
- isVi,
1496
- });
1497
- if (value === CLI_BACK) {
1498
- if (fieldIndex > 0) {
1499
- fieldIndex--;
1500
- continue;
1501
- }
1502
- return { back: true };
1503
- }
1504
- draft[field.key] = value;
1505
- fieldIndex++;
1506
- }
1507
- bots.push(draft);
1508
- }
1509
- } else if (channelKey !== 'zalo-personal') {
1510
- const defaults = existingBots[0] || {};
1511
- const fields = [
1512
- {
1513
- key: 'name',
1514
- message: isVi ? 'Tên Bot:' : 'Bot Name:',
1515
- defaultValue: defaults.name || 'Chat Bot',
1516
- required: true,
1517
- },
1518
- {
1519
- key: 'desc',
1520
- message: isVi ? 'Mô tả Bot:' : 'Bot Description:',
1521
- defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1522
- required: true,
1523
- },
1524
- {
1525
- key: 'persona',
1526
- message: isVi ? 'Tính cách & quy tắc (VD: gọn gàng, thân thiện):' : 'Persona & rules (e.g. concise, friendly):',
1527
- defaultValue: defaults.persona || '',
1528
- required: false,
1529
- },
1530
- {
1531
- key: 'token',
1532
- message: isVi ? `Nhập ${channel.name} Token:` : `Enter ${channel.name} Token:`,
1533
- defaultValue: defaults.token || '',
1534
- required: true,
1535
- },
1536
- ];
1537
- const draft = { ...defaults, slashCmd: '' };
1538
- let fieldIndex = 0;
1539
- while (fieldIndex < fields.length) {
1540
- const field = fields[fieldIndex];
1541
- const value = await inputWithBack({
1542
- message: field.message,
1543
- defaultValue: draft[field.key] || field.defaultValue,
1544
- required: field.required,
1545
- allowBack: true,
1546
- isVi,
1547
- });
1548
- if (value === CLI_BACK) {
1549
- if (fieldIndex > 0) {
1550
- fieldIndex--;
1551
- continue;
1552
- }
1553
- return { back: true };
1554
- }
1555
- draft[field.key] = value;
1556
- fieldIndex++;
1557
- }
1558
- bots.push(draft);
1559
- } else {
1560
- bots.push({ name: 'Bot', slashCmd: '', desc: '', persona: '', token: '' });
1561
- }
1562
-
1563
- return {
1564
- back: false,
1565
- botCount,
1566
- groupId,
1567
- bots,
1568
- botToken: bots[0]?.token || '',
1569
- };
1570
- }
1571
-
1572
- async function collectProviderStep({
1573
- isVi,
1574
- existingProviderKey = '',
1575
- existingProviderKeyVal = '',
1576
- existingOllamaModel = 'gemma4:e2b',
1577
- }) {
1578
- const providerKey = await selectWithBack({
1579
- message: isVi ? 'Chọn AI Provider:' : 'Select AI Provider:',
1580
- choices: Object.entries(PROVIDERS).map(([k, v]) => ({ name: `${v.icon} ${v.name}`, value: k })),
1581
- defaultValue: existingProviderKey || undefined,
1582
- allowBack: true,
1583
- isVi,
1584
- });
1585
- if (providerKey === CLI_BACK) {
1586
- return { back: true };
1587
- }
1588
- const provider = PROVIDERS[providerKey];
1589
-
1590
- let providerKeyVal = existingProviderKey === providerKey ? existingProviderKeyVal : '';
1591
- if (!provider.isProxy && !provider.isLocal) {
1592
- const keyValue = await inputWithBack({
1593
- message: isVi ? `Nhập ${provider.envKey}:` : `Enter ${provider.envKey}:`,
1594
- defaultValue: providerKeyVal,
1595
- required: true,
1596
- allowBack: true,
1597
- isVi,
1598
- });
1599
- if (keyValue === CLI_BACK) {
1600
- return { back: true };
1601
- }
1602
- providerKeyVal = keyValue;
1603
- }
1604
-
1605
- let selectedOllamaModel = existingProviderKey === 'ollama' ? existingOllamaModel : 'gemma4:e2b';
1606
- if (providerKey === 'ollama') {
1607
- console.log(chalk.yellow(isVi
1608
- ? '\n💡 Gemma 4 (02/04/2026) — chọn kích thước phù hợp với RAM máy bạn:'
1609
- : '\n💡 Gemma 4 (April 2, 2026) — pick a size that fits your RAM:'));
1610
- const modelValue = await selectWithBack({
1611
- message: isVi ? 'Chọn model Ollama:' : 'Select Ollama model:',
1612
- choices: [
1613
- {
1614
- name: isVi
1615
- ? '🟢 gemma4:e2b — Nhẹ nhất (~4-6 GB RAM) — Laptop / test nhanh ★ Khuyên dùng'
1616
- : '🟢 gemma4:e2b — Lightest (~4-6 GB RAM) — Laptop / fastest test ★ Recommended',
1617
- value: 'gemma4:e2b'
1618
- },
1619
- {
1620
- name: isVi
1621
- ? '🟡 gemma4:e4b — Cân bằng (~8-10 GB RAM) — Dùng hằng ngày'
1622
- : '🟡 gemma4:e4b — Balanced (~8-10 GB RAM) — Daily use',
1623
- value: 'gemma4:e4b'
1624
- },
1625
- {
1626
- name: isVi
1627
- ? '🟠 gemma4:26b — Mạnh (~18-24 GB RAM/VRAM) — Máy mạnh'
1628
- : '🟠 gemma4:26b — Powerful (~18-24 GB RAM/VRAM) — High-end machine',
1629
- value: 'gemma4:26b'
1630
- },
1631
- {
1632
- name: isVi
1633
- ? '🔴 gemma4:31b — Mạnh nhất (~24+ GB RAM/VRAM) — GPU workstation'
1634
- : '🔴 gemma4:31b — Most powerful (~24+ GB RAM/VRAM) — GPU workstation',
1635
- value: 'gemma4:31b'
1636
- },
1637
- ],
1638
- defaultValue: selectedOllamaModel,
1639
- allowBack: true,
1640
- isVi,
1641
- });
1642
- if (modelValue === CLI_BACK) {
1643
- return { back: true };
1644
- }
1645
- selectedOllamaModel = modelValue;
1646
- }
1647
-
1648
- return {
1649
- back: false,
1650
- providerKey,
1651
- provider,
1652
- providerKeyVal,
1653
- selectedOllamaModel,
1654
- };
1655
- }
1656
-
1657
- async function collectSkillsStep({
1658
- isVi,
1659
- providerKey,
1660
- existingSelectedSkills = [],
1661
- existingBrowserMode = 'server',
1662
- existingTtsOpenaiKey = '',
1663
- existingTtsElevenKey = '',
1664
- existingSmtpHost = 'smtp.gmail.com',
1665
- existingSmtpPort = '587',
1666
- existingSmtpUser = '',
1667
- existingSmtpPass = '',
1668
- }) {
1669
- const selectedSkills = await checkboxWithBack({
1670
- message: isVi ? 'Bật tính năng bổ sung (Space để chọn):' : 'Enable extra skills (Space to select):',
1671
- choices: getCliSkillChoices({ providerKey, isVi }).map((choice) => ({
1672
- ...choice,
1673
- checked: existingSelectedSkills.includes(choice.value),
1674
- })),
1675
- allowBack: true,
1676
- isVi,
1677
- });
1678
- if (selectedSkills === CLI_BACK) {
1679
- return { back: true };
1680
- }
1681
-
1682
- let browserMode = existingBrowserMode;
1683
- if (selectedSkills.includes('browser')) {
1684
- const isLinux = process.platform === 'linux';
1685
- const browserValue = await selectWithBack({
1686
- message: isVi ? 'Chế độ Browser Automation:' : 'Browser Automation mode:',
1687
- choices: [
1688
- {
1689
- name: isVi
1690
- ? '🖥️ Dùng Chrome trên máy tính (Windows/Mac — Bypass Cloudflare tốt hơn)'
1691
- : '🖥️ Use Host Chrome (Windows/Mac — Better Cloudflare bypass)',
1692
- value: 'desktop'
1693
- },
1694
- {
1695
- name: isVi
1696
- ? '🐳 Headless Chromium trong Docker (Ubuntu Server / VPS — không cần GUI)'
1697
- : '🐳 Headless Chromium inside Docker (Ubuntu Server / VPS — No GUI)',
1698
- value: 'server'
1699
- }
1700
- ],
1701
- defaultValue: browserMode || (isLinux ? 'server' : 'desktop'),
1702
- allowBack: true,
1703
- isVi,
1704
- });
1705
- if (browserValue === CLI_BACK) {
1706
- return { back: true };
1707
- }
1708
- browserMode = browserValue;
1709
- } else {
1710
- browserMode = 'server';
1711
- }
1712
-
1713
- let ttsOpenaiKey = existingTtsOpenaiKey;
1714
- let ttsElevenKey = existingTtsElevenKey;
1715
- if (selectedSkills.includes('tts')) {
1716
- const openaiKey = await inputWithBack({
1717
- message: isVi ? 'Nhập OPENAI_API_KEY (cho TTS, bỏ trống nếu dùng ElevenLabs):' : 'Enter OPENAI_API_KEY (for TTS, leave empty for ElevenLabs):',
1718
- defaultValue: ttsOpenaiKey,
1719
- allowBack: true,
1720
- isVi,
1721
- });
1722
- if (openaiKey === CLI_BACK) {
1723
- return { back: true };
1724
- }
1725
- ttsOpenaiKey = openaiKey;
1726
-
1727
- const elevenKey = await inputWithBack({
1728
- message: isVi ? 'Nhập ELEVENLABS_API_KEY (hoặc bỏ trống):' : 'Enter ELEVENLABS_API_KEY (or leave empty):',
1729
- defaultValue: ttsElevenKey,
1730
- allowBack: true,
1731
- isVi,
1732
- });
1733
- if (elevenKey === CLI_BACK) {
1734
- return { back: true };
1735
- }
1736
- ttsElevenKey = elevenKey;
1737
- } else {
1738
- ttsOpenaiKey = '';
1739
- ttsElevenKey = '';
1740
- }
1741
-
1742
- let smtpHost = existingSmtpHost;
1743
- let smtpPort = existingSmtpPort;
1744
- let smtpUser = existingSmtpUser;
1745
- let smtpPass = existingSmtpPass;
1746
- if (selectedSkills.includes('email')) {
1747
- const smtpHostValue = await inputWithBack({
1748
- message: isVi ? 'SMTP Host (VD: smtp.gmail.com):' : 'SMTP Host (e.g. smtp.gmail.com):',
1749
- defaultValue: smtpHost,
1750
- required: true,
1751
- allowBack: true,
1752
- isVi,
1753
- });
1754
- if (smtpHostValue === CLI_BACK) {
1755
- return { back: true };
1756
- }
1757
- smtpHost = smtpHostValue;
1758
-
1759
- const smtpPortValue = await inputWithBack({
1760
- message: 'SMTP Port:',
1761
- defaultValue: smtpPort,
1762
- required: true,
1763
- allowBack: true,
1764
- isVi,
1765
- });
1766
- if (smtpPortValue === CLI_BACK) {
1767
- return { back: true };
1768
- }
1769
- smtpPort = smtpPortValue;
1770
-
1771
- const smtpUserValue = await inputWithBack({
1772
- message: isVi ? 'SMTP Email:' : 'SMTP Email:',
1773
- defaultValue: smtpUser,
1774
- required: true,
1775
- allowBack: true,
1776
- isVi,
1777
- });
1778
- if (smtpUserValue === CLI_BACK) {
1779
- return { back: true };
1780
- }
1781
- smtpUser = smtpUserValue;
1782
-
1783
- const smtpPassValue = await inputWithBack({
1784
- message: isVi ? 'SMTP App Password:' : 'SMTP App Password:',
1785
- defaultValue: smtpPass,
1786
- required: true,
1787
- allowBack: true,
1788
- isVi,
1789
- });
1790
- if (smtpPassValue === CLI_BACK) {
1791
- return { back: true };
1792
- }
1793
- smtpPass = smtpPassValue;
1794
- } else {
1795
- smtpHost = 'smtp.gmail.com';
1796
- smtpPort = '587';
1797
- smtpUser = '';
1798
- smtpPass = '';
1799
- }
1800
-
1801
- return {
1802
- back: false,
1803
- selectedSkills,
1804
- browserMode,
1805
- ttsOpenaiKey,
1806
- ttsElevenKey,
1807
- smtpHost,
1808
- smtpPort,
1809
- smtpUser,
1810
- smtpPass,
1811
- };
1812
- }
1813
-
1814
-
1815
- // ─── Shared workspace file writer ─────────────────────────────────────────────
1816
- // Used by both native and docker flows to write .md + browser files consistently.
1817
- async function writeWorkspaceFiles({
1818
- workspaceDir, // absolute path to the workspace dir
1819
- isVi,
1820
- botName,
1821
- botDesc,
1822
- persona = '',
1823
- selectedSkills = [],
1824
- deployMode = 'native', // 'native' | 'docker'
1825
- isServer = false, // true = headless Docker server mode
1826
- isDesktop = false, // true = desktop/host Chrome mode
1827
- isMultiBot = false,
1828
- ownAliases = [],
1829
- otherAgents = [], // [{ name, agentId }]
1830
- teamRoster = [],
1831
- userInfo = '',
1832
- agentWorkspaceDir = 'workspace',
1833
- isRelayBot = false,
1834
- replyToDirectMessages = true,
1835
- channelKey = 'telegram',
1836
- }) {
1837
- const skillListStr = SKILLS
1838
- .filter((s) => selectedSkills.includes(s.value))
1839
- .map((s) => {
1840
- const label = s.name.replace(/^[^ ]+ /, '');
1841
- return isRelayBot
1842
- ? `- ${label}${s.slug ? ` (${s.slug})` : ' (native)'}`
1843
- : `- **${label}**${s.slug ? ` (${s.slug})` : ' (native)'}`;
1844
- })
1845
- .join('\n') || (isVi ? '- _(Chưa có skill nào)_' : '- _(No skills installed)_');
1846
-
1847
- const workspacePath = `.openclaw/${agentWorkspaceDir}/`;
1848
- const teamRosterFormatted = teamRoster
1849
- .map((peer, idx) => {
1850
- const agentId = peer.agentId || String(peer.name || `Bot ${idx + 1}`).toLowerCase().replace(/[^a-z0-9]+/g, '-');
1851
- const desc = peer.desc || (isVi ? 'Tro ly AI ca nhan' : 'Personal AI assistant');
1852
- const accountId = peer.accountId ? `, accountId: ${peer.accountId}` : '';
1853
- const slashCmd = peer.slashCmd ? `, slash: ${peer.slashCmd}` : '';
1854
- return `- \`${agentId}\`: ${peer.name || `Bot ${idx + 1}`} - ${desc}${accountId}${slashCmd}`;
1855
- })
1856
- .join('\n');
1857
-
1858
- const files = buildWorkspaceFileMap({
1859
- isVi,
1860
- variant: isRelayBot ? 'relay' : 'single',
1861
- botName,
1862
- botDesc,
1863
- ownAliases,
1864
- otherAgents,
1865
- replyToDirectMessages,
1866
- skillListStr,
1867
- workspacePath,
1868
- agentWorkspaceDir,
1869
- persona,
1870
- userInfo,
1871
- hasBrowser: isDesktop || isServer,
1872
- soulVariant: isRelayBot ? 'cli-simple' : 'cli-rich',
1873
- userVariant: isRelayBot ? 'cli-multi' : 'cli-single',
1874
- memoryVariant: isRelayBot ? 'cli-multi' : 'cli-single',
1875
- browserDocVariant: isServer ? 'cli-server' : 'cli-desktop',
1876
- browserToolVariant: 'cli',
1877
- includeBrowserTool: isDesktop,
1878
- teamRosterFormatted,
1879
- hasScheduler: selectedSkills.includes('scheduler'),
1880
- hasZaloMod: channelKey === 'zalo-personal',
1881
- });
1882
-
1883
- await fs.ensureDir(workspaceDir);
1884
- for (const [name, content] of Object.entries(files)) {
1885
- await fs.writeFile(path.join(workspaceDir, name), content, 'utf8');
1886
- }
1887
- }
1888
-
1889
-
1890
- async function main() {
1891
- const cliSubcommand = getCliSubcommand();
1892
- if (cliSubcommand === 'upgrade') {
1893
- await runUpgradeCommand();
1894
- return;
1895
- }
1896
-
1897
- console.log(chalk.red('\n=================================='));
1898
- console.log(chalk.redBright(LOGO));
1899
- console.log(chalk.greenBright(' OpenClaw Auto Setup CLI '));
1900
- console.log(chalk.red('==================================\n'));
1901
-
1902
- let lang = 'vi';
1903
- let isVi = true;
1904
- const detectedPlatform = process.platform;
1905
- const detectedOS = detectedPlatform === 'win32' ? 'windows'
1906
- : detectedPlatform === 'darwin' ? 'macos'
1907
- : 'linux';
1908
- let osChoice = detectedOS === 'linux' ? 'vps' : detectedOS;
1909
- let deployMode = 'docker';
1910
- let channelKey = 'telegram';
1911
- let channel = CHANNELS[channelKey];
1912
- let botToken = '';
1913
- let botCount = 1;
1914
- let bots = [];
1915
- let groupId = '';
1916
- let userInfo = '';
1917
- let providerKey = '9router';
1918
- let provider = PROVIDERS[providerKey];
1919
- let providerKeyVal = '';
1920
- let selectedOllamaModel = 'gemma4:e2b';
1921
- let selectedSkills = [];
1922
- let tavilyKey = '';
1923
- let browserMode = 'server';
1924
- let ttsOpenaiKey = '';
1925
- let ttsElevenKey = '';
1926
- let smtpHost = 'smtp.gmail.com', smtpPort = '587', smtpUser = '', smtpPass = '';
1927
- let defaultDir = process.cwd();
1928
- if (!defaultDir.endsWith('openclaw-setup') && !defaultDir.endsWith('openclaw')) {
1929
- defaultDir = path.join(defaultDir, 'openclaw-setup');
1930
- }
1931
- let projectDir = defaultDir;
1932
-
1933
- let setupStep = 'language';
1934
- while (true) {
1935
- if (setupStep === 'language') {
1936
- lang = await select({
1937
- message: 'Select language / Chọn ngôn ngữ:',
1938
- choices: [
1939
- { name: 'Tiếng Việt', value: 'vi' },
1940
- { name: 'English', value: 'en' }
1941
- ],
1942
- default: lang
1943
- });
1944
- isVi = lang === 'vi';
1945
- setupStep = 'os';
1946
- continue;
1947
- }
1948
-
1949
- if (setupStep === 'os') {
1950
- const nextOsChoice = await selectWithBack({
1951
- message: isVi ? 'Bạn đang chạy trên hệ điều hành nào?' : 'What OS are you running on?',
1952
- choices: [
1953
- { name: isVi ? '🪟 Windows' : '🪟 Windows', value: 'windows' },
1954
- { name: isVi ? '🍎 macOS' : '🍎 macOS', value: 'macos' },
1955
- { name: isVi ? '🐧 Ubuntu Desktop' : '🐧 Ubuntu Desktop', value: 'ubuntu' },
1956
- { name: isVi ? '🖥️ VPS / Ubuntu Server' : '🖥️ VPS / Ubuntu Server', value: 'vps' },
1957
- ],
1958
- defaultValue: osChoice,
1959
- allowBack: true,
1960
- isVi,
1961
- });
1962
- if (nextOsChoice === CLI_BACK) {
1963
- setupStep = 'language';
1964
- continue;
1965
- }
1966
- osChoice = nextOsChoice;
1967
- setupStep = 'deploy';
1968
- continue;
1969
- }
1970
-
1971
- if (setupStep === 'deploy') {
1972
- const deployModeDefault = (osChoice === 'ubuntu' || osChoice === 'vps') ? 'native' : 'docker';
1973
- const nextDeployMode = await selectWithBack({
1974
- message: isVi ? 'Chọn cách chạy bot:' : 'How do you want to run the bot?',
1975
- choices: [
1976
- { name: isVi ? '🐳 Docker (Khuyên dùng cho Windows / macOS — dễ cài, chạy ngay)' : '🐳 Docker (Recommended for Windows / macOS — easy setup, runs immediately)', value: 'docker' },
1977
- { name: isVi ? '⚡ Native / PM2 (Khuyên dùng cho Ubuntu / VPS — ít RAM, ổn định hơn)' : '⚡ Native / PM2 (Recommended for Ubuntu / VPS — less RAM, more stable)', value: 'native' }
1978
- ],
1979
- defaultValue: deployMode || deployModeDefault,
1980
- allowBack: true,
1981
- isVi,
1982
- });
1983
- if (nextDeployMode === CLI_BACK) {
1984
- setupStep = 'os';
1985
- continue;
1986
- }
1987
- deployMode = nextDeployMode;
1988
- if (deployMode === 'docker' && !isDockerInstalled()) {
1989
- console.log(chalk.cyan(isVi ? '\n🐳 Docker chưa được cài — đang tự động cài Docker Engine + Compose plugin...' : '\n🐳 Docker not found — auto-installing Docker Engine + Compose plugin...'));
1990
- try {
1991
- const platform = process.platform;
1992
- if (platform === 'win32') {
1993
- execSync('winget install -e --id Docker.DockerDesktop --accept-source-agreements --accept-package-agreements', { stdio: 'inherit' });
1994
- console.log(chalk.green(isVi ? '✅ Docker Desktop đã cài xong. Vui lòng mở Docker Desktop, đợi khởi động (icon tray chuyển xanh) rồi chạy lại lệnh này.' : '✅ Docker Desktop installed. Open Docker Desktop, wait for it to start (tray icon turns green), then re-run this command.'));
1995
- process.exit(0);
1996
- } else if (platform === 'darwin') {
1997
- execSync('brew install --cask docker', { stdio: 'inherit' });
1998
- console.log(chalk.green(isVi ? '✅ Docker Desktop cài xong qua Homebrew. Mở Docker Desktop, đợi khởi động rồi chạy lại lệnh này.' : '✅ Docker Desktop installed via Homebrew. Open Docker Desktop, wait for it to start, then re-run this command.'));
1999
- process.exit(0);
2000
- } else {
2001
- execSync('curl -fsSL https://get.docker.com | sh', { stdio: 'inherit', shell: true });
2002
- try { execSync('apt-get install -y docker-compose-plugin', { stdio: 'ignore', shell: true }); } catch {}
2003
- console.log(chalk.green(isVi ? '✅ Docker Engine + Compose plugin đã cài xong.' : '✅ Docker Engine + Compose plugin installed.'));
2004
- }
2005
- } catch {
2006
- console.log(chalk.red(isVi ? '❌ Không thể tự cài Docker. Tải thủ công: https://www.docker.com/products/docker-desktop/' : '❌ Could not auto-install Docker. Download manually: https://www.docker.com/products/docker-desktop/'));
2007
- process.exit(1);
2008
- }
2009
- }
2010
- setupStep = 'channel';
2011
- continue;
2012
- }
2013
-
2014
- if (setupStep === 'channel') {
2015
- const nextChannelKey = await selectWithBack({
2016
- message: isVi ? 'Chọn nền tảng bot:' : 'Select bot platform:',
2017
- choices: Object.entries(CHANNELS).map(([k, v]) => ({ name: v.icon + ' ' + v.name, value: k })),
2018
- defaultValue: channelKey,
2019
- allowBack: true,
2020
- isVi,
2021
- });
2022
- if (nextChannelKey === CLI_BACK) {
2023
- setupStep = 'deploy';
2024
- continue;
2025
- }
2026
- channelKey = nextChannelKey;
2027
- channel = CHANNELS[channelKey];
2028
- if (channelKey === 'zalo-bot') {
2029
- console.log(chalk.yellow('\n⚠️ ' + (isVi ? 'LƯU Ý: Zalo OA Bot yêu cầu phải thiết lập Webhook Public (qua VPS/ngrok có HTTPS). Hãy dùng Zalo Personal nếu bạn chưa có Webhook.' : 'NOTE: Zalo OA requires a Public Webhook (via VPS/ngrok with HTTPS). Use Zalo Personal if you do not have one.')));
2030
- }
2031
- setupStep = 'botSetup';
2032
- continue;
2033
- }
2034
-
2035
- if (setupStep === 'botSetup') {
2036
- const botSetup = await collectBotSetupStepWithGroupBack({ isVi, channelKey, channel, existingBots: bots, existingBotCount: botCount, existingGroupId: groupId });
2037
- if (botSetup.back) {
2038
- setupStep = 'channel';
2039
- continue;
2040
- }
2041
- botCount = botSetup.botCount;
2042
- groupId = botSetup.groupId;
2043
- bots = botSetup.bots;
2044
- botToken = botSetup.botToken;
2045
- setupStep = 'userInfo';
2046
- continue;
2047
- }
2048
-
2049
- if (setupStep === 'userInfo') {
2050
- console.log(chalk.bold('\n' + (isVi ? '👤 Thông tin của bạn 👤' : '👤 About You 👤')));
2051
- const nextUserInfo = await inputWithBack({ message: isVi ? '👤 Thông tin về bạn (tên bạn, ngôn ngữ, múi giờ, sở thích...):' : '👤 About you (your name, language, timezone, interests...):', defaultValue: userInfo, required: true, allowBack: true, isVi });
2052
- if (nextUserInfo === CLI_BACK) {
2053
- setupStep = 'botSetup';
2054
- continue;
2055
- }
2056
- userInfo = nextUserInfo;
2057
- setupStep = 'provider';
2058
- continue;
2059
- }
2060
-
2061
- if (setupStep === 'provider') {
2062
- const providerSetup = await collectProviderStep({ isVi, existingProviderKey: providerKey, existingProviderKeyVal: providerKeyVal, existingOllamaModel: selectedOllamaModel });
2063
- if (providerSetup.back) {
2064
- setupStep = 'userInfo';
2065
- continue;
2066
- }
2067
- providerKey = providerSetup.providerKey;
2068
- provider = providerSetup.provider;
2069
- providerKeyVal = providerSetup.providerKeyVal;
2070
- selectedOllamaModel = providerSetup.selectedOllamaModel;
2071
- setupStep = 'skills';
2072
- continue;
2073
- }
2074
-
2075
- if (setupStep === 'skills') {
2076
- const skillSetup = await collectSkillsStep({ isVi, providerKey, existingSelectedSkills: selectedSkills, existingBrowserMode: browserMode, existingTtsOpenaiKey: ttsOpenaiKey, existingTtsElevenKey: ttsElevenKey, existingSmtpHost: smtpHost, existingSmtpPort: smtpPort, existingSmtpUser: smtpUser, existingSmtpPass: smtpPass });
2077
- if (skillSetup.back) {
2078
- setupStep = 'provider';
2079
- continue;
2080
- }
2081
- selectedSkills = skillSetup.selectedSkills;
2082
- browserMode = skillSetup.browserMode;
2083
- ttsOpenaiKey = skillSetup.ttsOpenaiKey;
2084
- ttsElevenKey = skillSetup.ttsElevenKey;
2085
- smtpHost = skillSetup.smtpHost;
2086
- smtpPort = skillSetup.smtpPort;
2087
- smtpUser = skillSetup.smtpUser;
2088
- smtpPass = skillSetup.smtpPass;
2089
- setupStep = 'projectDir';
2090
- continue;
2091
- }
2092
-
2093
- if (setupStep === 'projectDir') {
2094
- const nextProjectDir = await inputWithBack({ message: isVi ? 'Thư mục cài đặt project:' : 'Project install directory:', defaultValue: projectDir, allowBack: true, isVi });
2095
- if (nextProjectDir === CLI_BACK) {
2096
- setupStep = 'skills';
2097
- continue;
2098
- }
2099
- projectDir = nextProjectDir;
2100
- break;
2101
- }
2102
- }
2103
-
2104
- const isMultiBot = botCount > 1 && channelKey === 'telegram';
2105
- const botName = bots[0].name;
2106
- const botDesc = bots[0].desc;
2107
- const botPersona = bots[0].persona;
2108
- const agentId = String(botName || 'chat').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'chat';
2109
- const modelsPrimary = providerKey === 'ollama' ? selectedOllamaModel : providerKey === '9router' ? 'smart-route' : provider.models?.[0]?.id || 'gpt-4o-mini';
2110
- const hasBrowserDesktop = selectedSkills.includes('browser') && browserMode === 'desktop';
2111
- const hasBrowserServer = selectedSkills.includes('browser') && browserMode === 'server';
2112
- console.log(chalk.cyan(`\n🚀 ${isVi ? 'Đang tạo thư mục và file cấu hình...' : 'Generating directories and configurations...'}`));
2113
-
2114
- await fs.ensureDir(projectDir);
2115
-
2116
-
2117
- // ─── Helper: build .env content per bot ──────────────────────────────────
2118
-
2119
- function buildEnvContentForBot(botIndex) {
2120
- const tok = bots[botIndex]?.token || botToken;
2121
- return buildEnvFileContent({
2122
- provider,
2123
- providerKeyVal,
2124
- channelKey,
2125
- botToken: tok,
2126
- isMultiBot,
2127
- groupId,
2128
- selectedSkills,
2129
- ttsOpenaiKey,
2130
- ttsElevenKey,
2131
- smtpHost,
2132
- smtpPort,
2133
- smtpUser,
2134
- smtpPass,
2135
- isSharedEnv: false,
2136
- });
2137
- }
2138
-
2139
- function buildSharedEnvContentForBots() {
2140
- return buildEnvFileContent({
2141
- provider,
2142
- providerKeyVal,
2143
- channelKey,
2144
- botToken: '',
2145
- isMultiBot,
2146
- groupId,
2147
- selectedSkills,
2148
- ttsOpenaiKey,
2149
- ttsElevenKey,
2150
- smtpHost,
2151
- smtpPort,
2152
- smtpUser,
2153
- smtpPass,
2154
- isSharedEnv: true,
2155
- });
2156
- }
2157
-
2158
- // ─── Create directories and write .env files ─────────────────────────────
2159
- if (isMultiBot) {
2160
- await fs.ensureDir(path.join(projectDir, '.openclaw'));
2161
- if (deployMode === 'docker') {
2162
- await fs.ensureDir(path.join(projectDir, 'docker', 'openclaw'));
2163
- await fs.writeFile(path.join(projectDir, 'docker', 'openclaw', '.env'), buildSharedEnvContentForBots());
2164
- } else {
2165
- await fs.writeFile(path.join(projectDir, '.env'), buildSharedEnvContentForBots());
2166
- }
2167
- } else {
2168
- await fs.ensureDir(path.join(projectDir, '.openclaw'));
2169
- await fs.ensureDir(path.join(projectDir, 'docker', 'openclaw'));
2170
- const envFilePath = deployMode === 'docker'
2171
- ? path.join(projectDir, 'docker', 'openclaw', '.env')
2172
- : path.join(projectDir, '.env');
2173
- await fs.writeFile(envFilePath, buildEnvContentForBot(0));
2174
- }
2175
-
2176
-
2177
- // ── Docker artifacts: Dockerfile + docker-compose via shared buildDockerArtifacts() ──────
2178
- const skillSlugs = SKILLS
2179
- .filter(s => selectedSkills.includes(s.value) && s.slug)
2180
- .map(s => s.slug);
2181
- const skillInstallCmd = skillSlugs.length > 0
2182
- ? skillSlugs.map(s => `ensure_skill ${s}`).join('\n')
2183
- : '';
2184
- const relayInstallCmd = (isMultiBot && channelKey === 'telegram')
2185
- ? buildRelayPluginInstallCommand('openclaw')
2186
- : '';
2187
- const deviceApproveLoop = 'while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done >/dev/null 2>&1 &';
2188
-
2189
- // buildDockerArtifacts joins runtimeCommandParts with spaces, then appends 'openclaw gateway run'
2190
- // Each part should be a standalone command fragment (no trailing &&)
2191
- const { dockerfile, compose } = buildDockerArtifacts({
2192
- openClawNpmSpec: OPENCLAW_NPM_SPEC,
2193
- openClawRuntimePackages: OPENCLAW_RUNTIME_PACKAGES,
2194
- is9Router: providerKey === '9router',
2195
- isLocal: provider.isLocal,
2196
- isMultiBot,
2197
- hasBrowser: hasBrowserDesktop || hasBrowserServer,
2198
- selectedModel: modelsPrimary,
2199
- agentId,
2200
- runtimeCommandParts: [
2201
- relayInstallCmd,
2202
- skillInstallCmd,
2203
- deviceApproveLoop,
2204
- ].filter(Boolean),
2205
- volumeMount: '../..:/root/project',
2206
- singleComposeName: `oc-${agentId}`,
2207
- multiComposeName: 'oc-multibot',
2208
- singleAppContainerName: `openclaw-${agentId}`,
2209
- multiAppContainerName: 'openclaw-multibot',
2210
- singleRouterContainerName: `9router-${agentId}`,
2211
- multiRouterContainerName: '9router-multibot',
2212
- singleOllamaContainerName: `ollama-${agentId}`,
2213
- multiOllamaContainerName: 'ollama-multibot',
2214
- plainSingleExtraHosts: hasBrowserDesktop,
2215
- multiOllamaNumParallel: 2,
2216
- singleOllamaNumParallel: 1,
2217
- emitBrowserInstall: hasBrowserServer || hasBrowserDesktop,
2218
-
2219
- });
2220
-
2221
- const dockerDir = path.join(projectDir, 'docker', 'openclaw');
2222
- await fs.ensureDir(dockerDir);
2223
- await fs.writeFile(path.join(dockerDir, 'Dockerfile'), dockerfile);
2224
- await fs.ensureDir(dockerDir);
2225
- await fs.writeFile(path.join(dockerDir, 'docker-compose.yml'), compose);
2226
-
2227
- let authProfilesJson = {};
2228
- if (provider.isLocal) {
2229
- // Ollama: must register provider with any non-empty API key
2230
- authProfilesJson = {
2231
- version: 1,
2232
- profiles: {
2233
- 'ollama:default': {
2234
- provider: 'ollama',
2235
- type: 'api_key',
2236
- key: 'ollama-local',
2237
- url: 'http://ollama:11434',
2238
- },
2239
- },
2240
- order: { ollama: ['ollama:default'] },
2241
- };
2242
- } else if (providerKey && providerKey !== '9router') {
2243
- const authProviderName = 'openai';
2244
- const authProfileId = `${authProviderName}:default`;
2245
- const authKeyValue = providerKeyVal;
2246
-
2247
- authProfilesJson = {
2248
- version: 1,
2249
- profiles: {
2250
- [authProfileId]: {
2251
- provider: authProviderName,
2252
- type: 'api_key',
2253
- key: authKeyValue,
2254
- },
2255
- },
2256
- order: { [authProviderName]: [authProfileId] },
2257
- };
2258
-
2259
- if (providerKey !== 'openai' && provider.baseURL) {
2260
- authProfilesJson.profiles[authProfileId].url = provider.baseURL;
2261
- }
2262
- } else if (providerKey === '9router') {
2263
- authProfilesJson = {
2264
- version: 1,
2265
- profiles: {
2266
- '9router-proxy': {
2267
- provider: '9router',
2268
- type: 'api_key',
2269
- key: NINE_ROUTER_PROXY_API_KEY,
2270
- },
2271
- },
2272
- order: { '9router': ['9router-proxy'] },
2273
- };
2274
- }
2275
-
2276
- // modelsPrimary already declared above
2277
-
2278
-
2279
- if (isMultiBot) {
2280
- const rootClawDir = path.join(projectDir, '.openclaw');
2281
- const teamRoster = bots.slice(0, botCount).map((peer, idx) => ({
2282
- idx,
2283
- name: peer?.name || `Bot ${idx + 1}`,
2284
- desc: peer?.desc || (isVi ? 'Tro ly AI ca nhan' : 'Personal AI assistant'),
2285
- persona: peer?.persona || '',
2286
- slashCmd: peer?.slashCmd || '',
2287
- token: peer?.token || '',
2288
- }));
2289
- const agentMetas = teamRoster.map((peer) => {
2290
- const agentSlug = peer.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || `bot${peer.idx + 1}`;
2291
- return {
2292
- ...peer,
2293
- agentId: agentSlug,
2294
- accountId: peer.idx === 0 ? 'default' : agentSlug,
2295
- workspaceDir: `workspace-${agentSlug}`,
2296
- };
2297
- });
2298
- const sharedConfig = buildOpenclawJson({
2299
- channelKey,
2300
- deployMode,
2301
- providerKey,
2302
- provider,
2303
- model: modelsPrimary,
2304
- isMultiBot: true,
2305
- agentMetas,
2306
- groupId,
2307
- selectedSkills,
2308
- skills: SKILLS,
2309
- hasBrowserDesktop,
2310
- hasBrowserServer,
2311
- gatewayPort: 18791,
2312
- gatewayAllowedOrigins: getGatewayAllowedOrigins(18791),
2313
- osChoice,
2314
- });
2315
-
2316
- await fs.writeJson(path.join(rootClawDir, 'openclaw.json'), sharedConfig, { spaces: 2 });
2317
- await fs.writeFile(
2318
- path.join(projectDir, TELEGRAM_SETUP_GUIDE_FILENAME),
2319
- buildTelegramPostInstallChecklist({ isVi, bots, groupId }),
2320
- 'utf8',
2321
- );
2322
- // Generate ecosystem.config.js for PM2 native multi-bot
2323
- if (deployMode === 'native') {
2324
- // Also write config to ~/.openclaw/ — openclaw binary on Linux/Mac reads from home dir
2325
- const homeClawDir = path.join(os.homedir(), '.openclaw');
2326
- await fs.ensureDir(homeClawDir);
2327
- const homeConfig = JSON.parse(JSON.stringify(sharedConfig));
2328
- for (const agent of (homeConfig.agents && homeConfig.agents.list || [])) {
2329
- // workspace is relative to projectDir (.openclaw/workspace-X); agentDir is relative to rootClawDir (agents/X/agent)
2330
- if (agent.workspace && !path.isAbsolute(agent.workspace)) agent.workspace = path.join(projectDir, agent.workspace);
2331
- if (agent.agentDir && !path.isAbsolute(agent.agentDir)) agent.agentDir = path.join(rootClawDir, agent.agentDir);
2332
- }
2333
- await fs.writeJson(path.join(homeClawDir, 'openclaw.json'), homeConfig, { spaces: 2 });
2334
- if (Object.keys(authProfilesJson).length > 0) {
2335
- await fs.writeJson(path.join(homeClawDir, 'auth-profiles.json'), authProfilesJson, { spaces: 2 });
2336
- }
2337
- const safeRootClawDir = rootClawDir.replace(/\\/g, '/');
2338
- const pm2Apps = [
2339
- ' {',
2340
- ` name: 'openclaw-multibot',`,
2341
- ` script: 'openclaw',`,
2342
- ` args: 'gateway run',`,
2343
- ` cwd: '${projectDir.replace(/\\/g, '/')}',`,
2344
- ` interpreter: 'none',`,
2345
- ` autorestart: true,`,
2346
- ` watch: false,`,
2347
- ` env: {`,
2348
- ` NODE_ENV: 'production',`,
2349
- ` OPENCLAW_HOME: '\',`,
2350
- ` OPENCLAW_STATE_DIR: '\',`,
2351
- ` }`,
2352
- ' }',
2353
- ].join('\n');
2354
- const ecosystemContent = [
2355
- '// PM2 ecosystem — run: pm2 start ecosystem.config.js',
2356
- 'module.exports = {',
2357
- ' apps: [',
2358
- pm2Apps,
2359
- ' ]',
2360
- '};',
2361
- '',
2362
- ].join('\n');
2363
- await fs.writeFile(path.join(projectDir, 'ecosystem.config.js'), ecosystemContent);
2364
- }
2365
- if (Object.keys(authProfilesJson).length > 0) {
2366
- await fs.writeJson(path.join(rootClawDir, 'auth-profiles.json'), authProfilesJson, { spaces: 2 });
2367
- }
2368
-
2369
- const execApprovalsConfig = buildExecApprovalsJson({ agentMetas });
2370
- await fs.writeJson(path.join(rootClawDir, 'exec-approvals.json'), execApprovalsConfig, { spaces: 2 });
2371
-
2372
- const teamMdRoster = agentMetas.map((meta) => ({
2373
- name: meta.name, desc: meta.desc, agentId: meta.agentId,
2374
- accountId: meta.accountId, slashCmd: meta.slashCmd, persona: meta.persona,
2375
- }));
2376
-
2377
- for (const meta of agentMetas) {
2378
- const workspaceDir = path.join(rootClawDir, meta.workspaceDir);
2379
- await fs.ensureDir(path.join(rootClawDir, 'agents', meta.agentId, 'agent'));
2380
- const ownAliases = [meta.name, meta.slashCmd, `bot ${meta.idx + 1}`].filter(Boolean);
2381
- const otherAgents = agentMetas
2382
- .filter((peer) => peer.agentId !== meta.agentId)
2383
- .map((peer) => ({ name: peer.name, agentId: peer.agentId }));
2384
-
2385
- // agentYaml & auth still needed, keep non-workspace writes here
2386
- // .yaml removed — OpenClaw reads config exclusively from openclaw.json
2387
- if (Object.keys(authProfilesJson).length > 0) {
2388
- await fs.writeJson(path.join(rootClawDir, 'agents', meta.agentId, 'agent', 'auth-profiles.json'), authProfilesJson, { spaces: 2 });
2389
- }
2390
-
2391
- // ── Workspace files via shared helper ────────────────────────────────
2392
- await writeWorkspaceFiles({
2393
- workspaceDir,
2394
- isVi, botName: meta.name, botDesc: meta.desc, persona: meta.persona,
2395
- selectedSkills, deployMode,
2396
- isDesktop: hasBrowserDesktop, isServer: hasBrowserServer,
2397
- isMultiBot: true, ownAliases, otherAgents,
2398
- teamRoster: teamMdRoster, userInfo,
2399
- agentWorkspaceDir: meta.workspaceDir,
2400
- isRelayBot: true,
2401
- replyToDirectMessages: true,
2402
- channelKey,
2403
- });
2404
- }
2405
- } else {
2406
- const numBotsToConfigure = 1;
2407
- for (let bIndex = 0; bIndex < numBotsToConfigure; bIndex++) {
2408
- const loopBotName = isMultiBot ? (bots[bIndex]?.name || `Bot ${bIndex+1}`) : botName;
2409
- const loopBotDesc = isMultiBot ? (bots[bIndex]?.desc || '') : botDesc;
2410
- const loopBotPersona = isMultiBot ? (bots[bIndex]?.persona || '') : botPersona;
2411
- const loopBotToken = isMultiBot ? (bots[bIndex]?.token || '') : botToken;
2412
- const teamRoster = bots.slice(0, numBotsToConfigure).map((peer, idx) => ({
2413
- idx,
2414
- name: peer?.name || `Bot ${idx + 1}`,
2415
- desc: peer?.desc || (isVi ? 'Tro ly AI ca nhan' : 'Personal AI assistant'),
2416
- persona: peer?.persona || '',
2417
- slashCmd: peer?.slashCmd || '',
2418
- }));
2419
- const ownAliases = [loopBotName, bots[bIndex]?.slashCmd || '', `bot ${bIndex + 1}`].filter(Boolean);
2420
- const otherBotNames = teamRoster.filter((peer) => peer.idx !== bIndex).map((peer) => peer.name);
2421
- const loopAgentId = loopBotName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || `bot${bIndex + 1}`;
2422
- const loopWorkspaceDir = `workspace-${loopAgentId}`;
2423
- const loopBotDir = isMultiBot ? path.join(projectDir, `bot${bIndex+1}`) : projectDir;
2424
-
2425
- await fs.ensureDir(path.join(loopBotDir, '.openclaw', 'agents', loopAgentId, 'agent'));
2426
- if (Object.keys(authProfilesJson).length > 0) {
2427
- await fs.writeJson(path.join(loopBotDir, '.openclaw', 'auth-profiles.json'), authProfilesJson, { spaces: 2 });
2428
- await fs.writeJson(path.join(loopBotDir, '.openclaw', 'agents', loopAgentId, 'agent', 'auth-profiles.json'), authProfilesJson, { spaces: 2 });
2429
- }
2430
-
2431
-
2432
- const loopGatewayPort = 18791 + (isMultiBot ? bIndex : 0);
2433
- const botConfig = buildOpenclawJson({
2434
- channelKey,
2435
- deployMode,
2436
- providerKey,
2437
- provider,
2438
- model: modelsPrimary,
2439
- isMultiBot: false,
2440
- agentMetas: [{
2441
- agentId: loopAgentId,
2442
- name: loopBotName,
2443
- token: loopBotToken,
2444
- workspaceDir: loopWorkspaceDir,
2445
- }],
2446
- groupId,
2447
- selectedSkills,
2448
- skills: SKILLS,
2449
- hasBrowserDesktop,
2450
- hasBrowserServer,
2451
- gatewayPort: loopGatewayPort,
2452
- gatewayAllowedOrigins: getGatewayAllowedOrigins(loopGatewayPort),
2453
- osChoice,
2454
- selectedModel: selectedOllamaModel,
2455
- });
2456
-
2457
- await fs.writeJson(path.join(loopBotDir, '.openclaw', 'openclaw.json'), botConfig, { spaces: 2 });
2458
-
2459
- // ── Workspace files: use shared writeWorkspaceFiles() ──────────────────────
2460
- const dockerWorkspaceDir = path.join(loopBotDir, '.openclaw', loopWorkspaceDir);
2461
- const dockerOwnAliases = [loopBotName, bots[bIndex]?.slashCmd || '', `bot ${bIndex + 1}`].filter(Boolean);
2462
- const dockerOtherAgents = teamRoster
2463
- .filter((peer) => peer.idx !== bIndex)
2464
- .map((peer) => ({ name: peer.name, agentId: peer.name.replace(/\s+/g, '-').toLowerCase() }));
2465
-
2466
- await writeWorkspaceFiles({
2467
- workspaceDir: dockerWorkspaceDir,
2468
- isVi,
2469
- botName: loopBotName,
2470
- botDesc: loopBotDesc,
2471
- persona: loopBotPersona,
2472
- selectedSkills,
2473
- deployMode,
2474
- isDesktop: hasBrowserDesktop,
2475
- isServer: hasBrowserServer,
2476
- isMultiBot,
2477
- ownAliases: dockerOwnAliases,
2478
- otherAgents: dockerOtherAgents,
2479
- teamRoster,
2480
- userInfo,
2481
- agentWorkspaceDir: loopWorkspaceDir,
2482
- isRelayBot: isMultiBot,
2483
- replyToDirectMessages: true,
2484
- channelKey,
2485
- });
2486
-
2487
- if (isMultiBot) {
2488
- // Append per-bot reply rules to AGENTS.md
2489
- const otherBotNames = teamRoster.filter((p) => p.idx !== bIndex).map((p) => p.name);
2490
- const extraAgentsMd = isVi
2491
- ? `\n\n## Khi nao nen tra loi\n- Neu metadata khong noi ro day la group/supergroup, mac dinh xem la chat rieng/DM va tra loi binh thuong.\n- Trong group, chi tra loi khi tin nhan co alias cua ban: ${dockerOwnAliases.map((a) => `\`${a}\``).join(', ')} hoac username Telegram cua ban.\n- Quy tac im lang khi tin nhan khong goi ban chi ap dung cho group chat, khong ap dung cho DM/chat rieng.\n- Neu group message chi goi ro bot khac ${otherBotNames.length ? otherBotNames.map((n) => `\`${n}\``).join(', ') : '`bot khac`'} thi khong cuop loi.\n- Trước MỖI phản hồi Telegram nhìn thấy bởi user, nếu turn hiện tại có tool/action reaction native thì ưu tiên dùng nó để thả \`👍\` trước, trừ khi \`👍\` đã có sẵn.\n- Khong duoc noi la minh da tha like neu turn hien tai chua goi duoc tool reaction hoac reaction khong hien that tren chat.\n- Khi can phoi hop noi bo, dung dung agent id ky thuat trong \`AGENTS.md\`, khong dung ten hien thi.\n- Khi hoi ve vai tro cac bot, dung \`AGENTS.md\` lam nguon su that.`
2492
- : `\n\n## When To Reply\n- If metadata does not clearly say this is a group/supergroup, treat it as a private DM and reply normally.\n- In group chats, only reply when the message contains one of your aliases: ${dockerOwnAliases.map((a) => `\`${a}\``).join(', ')} or your Telegram username.\n- The stay-silent rule for messages not addressed to you applies only to group chats, never to DMs/private chats.\n- If a group message is clearly calling another bot such as ${otherBotNames.length ? otherBotNames.map((n) => `\`${n}\``).join(', ') : '`another bot`'}, do not hijack it.\n- Before EVERY user-visible Telegram reply, if the current turn exposes a native reaction tool/action, use it to add \`👍\` first unless \`👍\` is already present.\n- Never claim that you already reacted unless the current turn actually executed the reaction and it is visible on the chat.\n- When you need internal coordination, use the exact technical agent id from \`AGENTS.md\`, not the display name.\n- Use \`AGENTS.md\` as the source of truth for team roles.`;
2493
- await fs.appendFile(path.join(dockerWorkspaceDir, 'AGENTS.md'), extraAgentsMd);
2494
- }
2495
- } // END FOR LOOP
2496
- }
2497
-
2498
- // ── Chrome Debug scripts — via shared builder (same content as wizard ZIP) ─
2499
- await writeGeneratedArtifacts(projectDir, buildCliChromeDebugArtifacts());
2500
-
2501
- // ── Uninstall scripts ───────────────────────────────────────────────────────
2502
- await writeGeneratedArtifacts(projectDir, buildCliUninstallArtifacts({
2503
- deployMode,
2504
- osChoice: detectedOS,
2505
- projectDir,
2506
- botName: (deployMode !== 'docker' && detectedOS === 'vps')
2507
- ? getNativePm2AppName(isMultiBot)
2508
- : botName,
2509
- }));
2510
-
2511
- // ── Upgrade scripts ─────────────────────────────────────────────────────────
2512
- await writeGeneratedArtifacts(projectDir, buildCliUpgradeArtifacts());
2513
-
2514
- // ── start-bot.bat / start-bot.sh — one-click restart scripts ─────────────
2515
- // Generated for native deployments only (docker has docker compose up)
2516
- if (deployMode !== 'docker') {
2517
- await writeGeneratedArtifacts(projectDir, buildCliStartBotArtifacts({
2518
- projectDir,
2519
- openclawHome: path.join(projectDir, '.openclaw'),
2520
- is9Router: providerKey === '9router',
2521
- osChoice,
2522
- isMultiBot,
2523
- appName: getNativePm2AppName(isMultiBot),
2524
- isVi,
2525
- }));
2526
-
2527
- console.log(chalk.cyan(
2528
- process.platform === 'win32'
2529
- ? (isVi
2530
- ? `\n🚀 start-bot.bat / start-bot.sh đã tạo — double-click để restart bot.`
2531
- : `\n🚀 start-bot.bat / start-bot.sh created — double-click to restart the bot.`)
2532
- : (isVi
2533
- ? `\n🚀 start-bot.sh đã tạo — chạy ./start-bot.sh để restart bot.`
2534
- : `\n🚀 start-bot.sh created — run ./start-bot.sh to restart the bot.`)
2535
- ));
2536
- }
2537
-
2538
- console.log(chalk.green(`✅ ${isVi ? 'Tạo cấu hình thành công!' : 'Configs created successfully!'}`));
2539
-
2540
- installLatestOpenClaw({ isVi, osChoice });
2541
-
2542
- // 7. Auto Run
2543
- const autoRun = deployMode === 'docker' ? await confirm({
2544
- message: isVi ? 'Bạn có muốn tự động build Docker và khởi động Bot luôn không?' : 'Do you want to run Docker compose and start the bot now?',
2545
- default: true
2546
- }) : false;
2547
-
2548
- if (deployMode === 'docker' && autoRun) {
2549
- console.log(chalk.yellow(`\n🐳 ${isVi ? 'Đang khởi động Docker (có thể mất vài phút)...' : 'Starting Docker (might take a few minutes)...'}`));
2550
- const dockerPath = path.join(projectDir, 'docker', 'openclaw');
2551
-
2552
- // Auto-detect Docker Compose V2 (plugin) vs V1 (standalone docker-compose).
2553
- // On Ubuntu 24.04 installed via `apt install docker.io`, the Compose V2 plugin
2554
- // is NOT included — `docker compose` subcommand may not exist or may be broken.
2555
- // We test both and use whichever actually works.
2556
- let composeCmd, composeArgs;
2557
- const detectCompose = () => {
2558
- // Test V2 plugin: 'docker compose up --help' exits 0 if plugin works
2559
- try {
2560
- execSync('docker compose up --help', { stdio: 'ignore' });
2561
- return { cmd: 'docker', args: ['compose', 'up', '--detach', '--build'] };
2562
- } catch { /* V2 not available or broken */ }
2563
- // Test V1 standalone: 'docker-compose up --help'
2564
- try {
2565
- execSync('docker-compose up --help', { stdio: 'ignore' });
2566
- return { cmd: 'docker-compose', args: ['up', '--detach', '--build'] };
2567
- } catch { /* V1 also not available */ }
2568
- return null;
2569
- };
2570
- const detected = detectCompose();
2571
- if (!detected) {
2572
- console.log(chalk.red(isVi
2573
- ? '\n❌ Không tìm thấy Docker Compose!\n Cài bằng lệnh: sudo apt-get install docker-compose-plugin'
2574
- : '\n❌ Docker Compose not found!\n Install: sudo apt-get install docker-compose-plugin'));
2575
- process.exit(1);
2576
- }
2577
- composeCmd = detected.cmd;
2578
- composeArgs = detected.args;
2579
-
2580
- const child = spawn(composeCmd, composeArgs, {
2581
- cwd: dockerPath,
2582
- stdio: 'inherit'
2583
- });
2584
-
2585
- child.on('close', (code) => {
2586
- if (code === 0) {
2587
- console.log(chalk.green(`\n🎉 ${isVi ? 'Setup hoàn tất! Bot đang chạy.' : 'Setup complete! Bot is running.'}`));
2588
-
2589
- if (providerKey === '9router') {
2590
- console.log(chalk.yellow(`\n🔀 ${isVi
2591
- ? '9Router Dashboard: http://localhost:20128/dashboard'
2592
- : '9Router Dashboard: http://localhost:20128/dashboard'}`));
2593
- console.log(chalk.gray(isVi
2594
- ? ' → Mở dashboard → đăng nhập OAuth để kết nối các Provider (iFlow, Gemini CLI, Claude Code...)'
2595
- : ' → Open dashboard → OAuth login to connect Providers (iFlow, Gemini CLI, Claude Code...)'));
2596
- console.log(chalk.gray(isVi
2597
- ? ' → Sau khi kết nối provider, bot sẽ tự động hoạt động qua combo "smart-route"'
2598
- : ' → After connecting providers, bot works automatically via "smart-route" combo'));
2599
- }
2600
-
2601
- if (channelKey === 'telegram') {
2602
- console.log(chalk.cyan(`\n💬 ${isVi
2603
- ? 'Nhắn tin cho bot trên Telegram là dùng được ngay!'
2604
- : 'Just message your bot on Telegram to start chatting!'}`));
2605
- if (isMultiBot) {
2606
- console.log(chalk.yellow(`\n${isVi ? '📋 Đọc hướng dẫn setup bot trong Group:' : '📋 Read setup guide in Group:'} ${TELEGRAM_SETUP_GUIDE_FILENAME}`));
2607
- console.log(chalk.gray(isVi
2608
- ? ' → Chạy scripts/telegram-post-install-check.mjs để lấy link thật, kiểm tra group/privacy, rồi mới add bot và Disable privacy mode.'
2609
- : ' → Run scripts/telegram-post-install-check.mjs to get the real links, verify group/privacy, then add the bots and disable privacy mode.'));
2610
- }
2611
- } else if (channelKey === 'zalo-personal') {
2612
- printZaloPersonalLoginInfo({ isVi, deployMode: 'docker', projectDir });
2613
- }
2614
- } else {
2615
- console.log(chalk.red(`\n❌ Docker exited with code ${code}`));
2616
- console.log(chalk.yellow(isVi
2617
- ? `\n💡 Nếu lỗi "unknown shorthand flag", chạy: sudo apt-get install docker-compose-plugin\n Rồi thử lại: cd ${dockerPath} && docker compose up -d --build`
2618
- : `\n💡 If "unknown shorthand flag" error, run: sudo apt-get install docker-compose-plugin\n Then retry: cd ${dockerPath} && docker compose up -d --build`));
2619
- }
2620
- });
2621
-
2622
- }
2623
- if (deployMode === 'docker') {
2624
-
2625
- // ── Auto-install openclaw binary if not present ──────────────────────────
2626
- const isOpenClawInstalled = () => { try { execSync('openclaw --version', { stdio: 'ignore' }); return true; } catch { return false; } };
2627
- if (!isOpenClawInstalled()) {
2628
- console.log(chalk.cyan(isVi
2629
- ? `\n📦 Đang cài openclaw binary (${OPENCLAW_NPM_SPEC})...`
2630
- : `\n📦 Installing openclaw binary (${OPENCLAW_NPM_SPEC})...`));
2631
- try {
2632
- execSync(`npm install -g ${OPENCLAW_NPM_SPEC}`, { stdio: 'inherit' });
2633
- console.log(chalk.green(isVi ? '✅ openclaw đã cài xong!' : '✅ openclaw installed!'));
2634
- } catch {
2635
- console.log(chalk.yellow(isVi
2636
- ? `⚠️ Không tự cài được. Chạy thủ công: sudo npm install -g ${OPENCLAW_NPM_SPEC}`
2637
- : `⚠️ Could not auto-install. Run manually: sudo npm install -g ${OPENCLAW_NPM_SPEC}`));
2638
- }
2639
- }
2640
-
2641
- if (isMultiBot && channelKey === 'telegram') {
2642
- console.log(chalk.yellow(`\n${isVi ? '📋 Xem hướng dẫn sau cài:' : '📋 Read post-install guide:'} ${path.join(projectDir, TELEGRAM_SETUP_GUIDE_FILENAME)}`));
2643
- }
2644
- } else {
2645
- if (!isOpenClawInstalled()) {
2646
- console.log(chalk.cyan(isVi
2647
- ? `\n📦 Dang cai openclaw binary (${OPENCLAW_NPM_SPEC})...`
2648
- : `\n📦 Installing openclaw binary (${OPENCLAW_NPM_SPEC})...`));
2649
- if (!installGlobalPackage(OPENCLAW_NPM_SPEC, { isVi, osChoice, displayName: 'openclaw' })) {
2650
- process.exit(1);
2651
- }
2652
- console.log(chalk.green(isVi ? '✅ openclaw da cai xong!' : '✅ openclaw installed!'));
2653
- }
2654
-
2655
- if (providerKey === '9router') {
2656
- if (shouldReuseInstalledGlobals() && is9RouterInstalled()) {
2657
- console.log(chalk.green(isVi
2658
- ? '\n♻️ Dang dung lai 9Router da cai san de test nhanh.'
2659
- : '\n♻️ Reusing the installed 9Router for a faster test run.'));
2660
- } else if (!is9RouterInstalled()) {
2661
- console.log(chalk.cyan(isVi
2662
- ? '\n📦 Dang cai 9Router binary (npm install -g 9router)...'
2663
- : '\n📦 Installing 9Router binary (npm install -g 9router)...'));
2664
- if (!installGlobalPackage('9router@latest', { isVi, osChoice, displayName: '9Router' })) {
2665
- process.exit(1);
2666
- }
2667
- console.log(chalk.green(isVi ? '✅ 9Router da cai xong!' : '✅ 9Router installed!'));
2668
- }
2669
- }
2670
-
2671
- let native9RouterSyncScriptPath = null;
2672
- if (providerKey === '9router') {
2673
- await writeNative9RouterPatchScript(projectDir);
2674
- native9RouterSyncScriptPath = await writeNative9RouterSyncScript(projectDir);
2675
- try {
2676
- execFileSync(process.execPath, [path.join(projectDir, '.openclaw', 'patch-9router.js')], {
2677
- cwd: projectDir,
2678
- stdio: 'ignore',
2679
- });
2680
- } catch {
2681
- // Start scripts retry this patch before launching 9router.
2682
- }
2683
- }
2684
-
2685
- await ensureProjectRuntimeDirs(projectDir, isVi);
2686
-
2687
- if (isMultiBot && channelKey === 'telegram') {
2688
- installRelayPluginForProject(projectDir, isVi);
2689
- }
2690
-
2691
- if (osChoice === 'vps') {
2692
- if (!isPm2Installed()) {
2693
- console.log(chalk.cyan(isVi ? '\n📦 Dang cai PM2...' : '\n📦 Installing PM2...'));
2694
- if (!installGlobalPackage('pm2@latest', { isVi, osChoice, displayName: 'PM2' })) {
2695
- process.exit(1);
2696
- }
2697
- }
2698
-
2699
- if (isMultiBot && channelKey === 'telegram') {
2700
- if (providerKey === '9router') {
2701
- startNative9RouterPm2({ isVi, projectDir, appName: getNativePm2AppName(true), syncScriptPath: native9RouterSyncScriptPath });
2702
- }
2703
- execSync('pm2 start ecosystem.config.js && pm2 save', {
2704
- cwd: projectDir,
2705
- stdio: 'inherit',
2706
- shell: true
2707
- });
2708
- console.log(chalk.green(`\n🎉 ${isVi ? 'Setup hoan tat! Multi-bot native dang chay qua PM2.' : 'Setup complete! Native multi-bot is running via PM2.'}`));
2709
- console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${getNativePm2AppName(true)}` : ` View logs: pm2 logs ${getNativePm2AppName(true)}`));
2710
- printNativeDashboardAccessInfo({ isVi, providerKey, projectDir });
2711
- if (channelKey === 'zalo-personal') {
2712
- printZaloPersonalLoginInfo({ isVi, deployMode: 'native', projectDir });
2713
- }
2714
- } else {
2715
- const appName = getNativePm2AppName(false);
2716
- if (providerKey === '9router') {
2717
- startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath: native9RouterSyncScriptPath });
2718
- }
2719
- if (channelKey === 'zalo-personal') {
2720
- await runNativeZaloPersonalLoginFlow({ isVi, projectDir });
2721
- }
2722
- execFileSync('pm2', [
2723
- 'start',
2724
- 'openclaw',
2725
- '--name',
2726
- appName,
2727
- '--cwd',
2728
- projectDir.replace(/\\/g, '/'),
2729
- '--',
2730
- 'gateway',
2731
- 'run'
2732
- ], {
2733
- cwd: projectDir,
2734
- stdio: 'inherit',
2735
- env: getProjectRuntimeEnv(projectDir)
2736
- });
2737
- runPm2Save({ projectDir, isVi });
2738
- console.log(chalk.green(`\n🎉 ${isVi ? 'Setup hoan tat! Bot native dang chay qua PM2.' : 'Setup complete! Native bot is running via PM2.'}`));
2739
- console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${appName}` : ` View logs: pm2 logs ${appName}`));
2740
- printNativeDashboardAccessInfo({ isVi, providerKey, projectDir });
2741
- if (channelKey === 'zalo-personal') {
2742
- printZaloPersonalLoginInfo({ isVi, deployMode: 'native', projectDir });
2743
- }
2744
- }
2745
- } else {
2746
- if (providerKey === '9router') {
2747
- console.log(chalk.yellow(`\n${isVi ? 'Khoi dong 9Router native (background)...' : 'Starting native 9Router (background)...'}`));
2748
- const native9RouterLaunch = resolveNative9RouterDesktopLaunch();
2749
- spawnBackgroundProcess(native9RouterLaunch.command, native9RouterLaunch.args, {
2750
- cwd: projectDir,
2751
- env: getProjectRuntimeEnv(projectDir, native9RouterLaunch.env)
2752
- }).unref();
2753
- const routerHealth = await waitFor9RouterApiReady();
2754
- if (native9RouterSyncScriptPath) {
2755
- spawnBackgroundProcess(process.execPath, [native9RouterSyncScriptPath], {
2756
- cwd: projectDir
2757
- }).unref();
2758
- }
2759
- console.log(chalk.gray(isVi
2760
- ? ' 9Router dashboard: http://localhost:20128/dashboard'
2761
- : ' 9Router dashboard: http://localhost:20128/dashboard'));
2762
- if (!routerHealth.ok) {
2763
- console.log(chalk.yellow(isVi
2764
- ? ` ⚠️ 9Router da mo cong 20128 nhung admin API chua san sang. Kiem tra them: ${routerHealth.url}`
2765
- : ` ⚠️ 9Router opened port 20128 but the admin API is not ready yet. Check: ${routerHealth.url}`));
2766
- }
2767
- }
2768
- if (hasZaloPersonal(channelKey)) {
2769
- await runNativeZaloPersonalLoginFlow({ isVi, projectDir });
2770
- }
2771
- console.log(chalk.yellow(`\n${isVi ? 'Khoi dong native bot (foreground)...' : 'Starting native bot (foreground)...'}`));
2772
- const isZaloPersonal = hasZaloPersonal(channelKey);
2773
- const child = spawn('openclaw', ['gateway', 'run'], {
2774
- cwd: projectDir,
2775
- stdio: isZaloPersonal ? ['inherit', 'pipe', 'pipe'] : 'inherit',
2776
- shell: process.platform === 'win32'
2777
- });
2778
- if (isZaloPersonal) {
2779
- let approvedPairingCode = null;
2780
- const onGatewayChunk = (chunk, target) => {
2781
- const text = chunk.toString();
2782
- target.write(text);
2783
- const pairingCode = extractZaloPairingCode(text);
2784
- if (pairingCode && pairingCode !== approvedPairingCode) {
2785
- if (approveZaloPairingCode({ pairingCode, projectDir, isVi })) {
2786
- approvedPairingCode = pairingCode;
2787
- }
2788
- }
2789
- };
2790
- child.stdout?.on('data', (chunk) => onGatewayChunk(chunk, process.stdout));
2791
- child.stderr?.on('data', (chunk) => onGatewayChunk(chunk, process.stderr));
2792
- }
2793
- child.on('close', (code) => process.exit(code ?? 0));
2794
- return;
2795
- }
2796
-
2797
- console.log(chalk.cyan(`\n👉 ${isVi ? 'Native runtime da duoc cai san va khoi dong.' : 'Native runtime is installed and started.'}`));
2798
- if (isMultiBot && channelKey === 'telegram') {
2799
- console.log(chalk.yellow(`\n📋 ${isVi ? 'Xem huong dan sau cai:' : 'Read post-install guide:'} ${path.join(projectDir, TELEGRAM_SETUP_GUIDE_FILENAME)}`));
2800
- }
2801
- }
2802
- }
2803
-
2804
-
2805
-
2806
- main().catch(err => {
2807
- console.error(chalk.red('Error:'), err);
2808
- process.exit(1);
2809
- });
2810
-
37
+ const noOpen = args.includes('--no-open');
38
+ const hostArg = args.find((arg) => arg.startsWith('--host='));
39
+ const portArg = args.find((arg) => arg.startsWith('--port='));
40
+ const projectDirArg = args.find((arg) => arg.startsWith('--project-dir='));
41
+
42
+ await startLocalInstaller({
43
+ openBrowser: !noOpen,
44
+ host: hostArg ? hostArg.slice('--host='.length) : '127.0.0.1',
45
+ preferredPort: portArg ? Number(portArg.slice('--port='.length)) : 51789,
46
+ projectDir: projectDirArg ? projectDirArg.slice('--project-dir='.length) : process.cwd(),
47
+ });
48
+ }
49
+ } else {
50
+ console.log('\n============================================================');
51
+ console.log(' 🦞 OpenClaw Setup — Auto-downloader & Installer');
52
+ console.log('============================================================\n');
53
+
54
+ const targetDirName = 'openclaw-setup';
55
+ const targetPath = path.join(process.cwd(), targetDirName);
56
+
57
+ const runCmd = (cmd, cmdArgs, opts = {}) => {
58
+ return new Promise((resolve, reject) => {
59
+ const child = spawn(cmd, cmdArgs, {
60
+ shell: true,
61
+ stdio: 'inherit',
62
+ ...opts
63
+ });
64
+ child.on('close', (code) => {
65
+ if (code === 0) resolve();
66
+ else reject(new Error(`${cmd} exited with code ${code}`));
67
+ });
68
+ child.on('error', reject);
69
+ });
70
+ };
71
+
72
+ try {
73
+ // Check if git is installed
74
+ try {
75
+ await new Promise((resolve, reject) => {
76
+ const child = spawn('git', ['--version'], { shell: true, stdio: 'ignore' });
77
+ child.on('close', (code) => {
78
+ if (code === 0) resolve();
79
+ else reject(new Error());
80
+ });
81
+ child.on('error', reject);
82
+ });
83
+ } catch {
84
+ throw new Error('Git is not installed or not found in PATH. Please install Git and try again.');
85
+ }
86
+
87
+ if (!fs.existsSync(targetPath)) {
88
+ console.log(`[1/3] Cloning OpenClaw setup repository to: ${targetPath}...`);
89
+ await runCmd('git', ['clone', 'https://github.com/tuanminhhole/openclaw-setup.git', targetDirName]);
90
+ } else {
91
+ console.log(`[1/3] OpenClaw setup folder already exists at: ${targetPath}`);
92
+ console.log(' Updating repository to latest version...');
93
+ try {
94
+ await runCmd('git', ['pull'], { cwd: targetPath });
95
+ } catch (err) {
96
+ console.warn(' Warning: Failed to git pull (continuing anyway):', err.message);
97
+ }
98
+ }
99
+
100
+ console.log('\n[2/3] Installing dependencies...');
101
+ await runCmd('npm', ['install'], { cwd: targetPath });
102
+
103
+ console.log('\n[3/3] Starting OpenClaw Setup UI...');
104
+ const originalCwd = process.cwd();
105
+ const cliPath = path.join('dist', 'cli.js');
106
+ await runCmd('node', [cliPath, ...args, `--project-dir=${originalCwd}`], { cwd: targetPath });
107
+
108
+ } catch (error) {
109
+ console.error('\n❌ Error during setup:', error.message);
110
+ process.exit(1);
111
+ }
112
+ }