create-openclaw-bot 5.6.10 → 5.6.12

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
@@ -20,25 +20,25 @@ function loadSharedModule(modulePath, globalName) {
20
20
  return globalThis[globalName] || loaded || {};
21
21
  }
22
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');
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
42
 
43
43
  const {
44
44
  buildWorkspaceFileMap,
@@ -200,32 +200,32 @@ function resolveNative9RouterDesktopLaunch() {
200
200
  };
201
201
  }
202
202
 
203
- function build9RouterSmartRouteSyncScript(dbPath) {
204
- const safeDbPath = JSON.stringify(dbPath);
205
- const safeRouterBaseUrl = JSON.stringify(NINE_ROUTER_API_BASE_URL);
206
- const safeModelPriority = JSON.stringify(SMART_ROUTE_PROVIDER_MODELS);
207
- const safeProviderOrder = JSON.stringify(SMART_ROUTE_PROVIDER_ORDER);
208
- return `function bootstrap() {
209
- const fs = require('fs');
210
- const path = require('path');
211
- const dbPath = ${safeDbPath};
212
- const ROUTER=${safeRouterBaseUrl};
213
- const MODEL_PRIORITY=${safeModelPriority};
214
- const PREF=${safeProviderOrder};
215
- const sync = async () => {
216
- try {
217
- const response = await fetch(ROUTER + '/api/providers');
218
- if (!response.ok) return;
219
- const payload = await response.json();
220
- const rawConnections = Array.isArray(payload.connections)
221
- ? payload.connections
222
- : Array.isArray(payload.providerConnections)
223
- ? payload.providerConnections
224
- : [];
225
- const a = [...new Set(rawConnections
226
- .filter((item) => item && item.provider && item.isActive !== false && !item.disabled)
227
- .map((item) => item.provider))];
228
- let db = {};
203
+ function build9RouterSmartRouteSyncScript(dbPath) {
204
+ const safeDbPath = JSON.stringify(dbPath);
205
+ const safeRouterBaseUrl = JSON.stringify(NINE_ROUTER_API_BASE_URL);
206
+ const safeModelPriority = JSON.stringify(SMART_ROUTE_PROVIDER_MODELS);
207
+ const safeProviderOrder = JSON.stringify(SMART_ROUTE_PROVIDER_ORDER);
208
+ return `function bootstrap() {
209
+ const fs = require('fs');
210
+ const path = require('path');
211
+ const dbPath = ${safeDbPath};
212
+ const ROUTER=${safeRouterBaseUrl};
213
+ const MODEL_PRIORITY=${safeModelPriority};
214
+ const PREF=${safeProviderOrder};
215
+ const sync = async () => {
216
+ try {
217
+ const response = await fetch(ROUTER + '/api/providers');
218
+ if (!response.ok) return;
219
+ const payload = await response.json();
220
+ const rawConnections = Array.isArray(payload.connections)
221
+ ? payload.connections
222
+ : Array.isArray(payload.providerConnections)
223
+ ? payload.providerConnections
224
+ : [];
225
+ const a = [...new Set(rawConnections
226
+ .filter((item) => item && item.provider && item.isActive !== false && !item.disabled)
227
+ .map((item) => item.provider))];
228
+ let db = {};
229
229
  try {
230
230
  db = JSON.parse(fs.readFileSync(dbPath, 'utf8'));
231
231
  } catch {}
@@ -239,12 +239,12 @@ function build9RouterSmartRouteSyncScript(dbPath) {
239
239
  console.log('Removed smart-route (no active providers)');
240
240
  }
241
241
  };
242
- if (!a.length) {
243
- removeSmartRoute();
244
- return;
245
- }
246
- a.sort((x, y) => (PREF.indexOf(x) === -1 ? 99 : PREF.indexOf(x)) - (PREF.indexOf(y) === -1 ? 99 : PREF.indexOf(y)));
247
- const m = a.flatMap((provider) => MODEL_PRIORITY[provider] || []);
242
+ if (!a.length) {
243
+ removeSmartRoute();
244
+ return;
245
+ }
246
+ a.sort((x, y) => (PREF.indexOf(x) === -1 ? 99 : PREF.indexOf(x)) - (PREF.indexOf(y) === -1 ? 99 : PREF.indexOf(y)));
247
+ const m = a.flatMap((provider) => MODEL_PRIORITY[provider] || []);
248
248
  if (!m.length) {
249
249
  removeSmartRoute();
250
250
  return;
@@ -441,69 +441,69 @@ function resolveCommandOnPath(command) {
441
441
  }
442
442
  }
443
443
 
444
- async function writeNative9RouterSyncScript(projectDir) {
445
- const syncScriptPath = path.join(projectDir, '.openclaw', '9router-smart-route-sync.js');
446
- await fs.ensureDir(path.dirname(syncScriptPath));
447
- // Use native home data dir so sync script writes to same place 9router binary reads from
448
- const nativeDataDir = getNative9RouterDataDir();
449
- await fs.ensureDir(nativeDataDir);
450
- await fs.writeFile(syncScriptPath, build9RouterSmartRouteSyncScript(path.join(nativeDataDir, 'db.json')));
451
- return syncScriptPath;
452
- }
453
-
454
- async function writeNative9RouterPatchScript(projectDir) {
455
- const patchScriptPath = path.join(projectDir, '.openclaw', 'patch-9router.js');
456
- await fs.ensureDir(path.dirname(patchScriptPath));
457
- await fs.writeFile(patchScriptPath, build9RouterPatchScript());
458
- return patchScriptPath;
459
- }
460
-
461
- async function patchProject9RouterOpenClawConfig(projectDir) {
462
- const configPath = path.join(projectDir, '.openclaw', 'openclaw.json');
463
- if (!await fs.pathExists(configPath)) return false;
464
- const config = await fs.readJson(configPath);
465
- const provider = config?.models?.providers?.['9router'];
466
- if (!provider) return false;
467
- provider.baseUrl = get9RouterBaseUrl(detectProjectDeployMode(projectDir));
468
- provider.apiKey = NINE_ROUTER_PROXY_API_KEY;
469
- provider.api = 'openai-completions';
470
- provider.models = build9RouterProviderConfig(provider.baseUrl).models;
471
- await fs.writeJson(configPath, config, { spaces: 2 });
472
- return true;
473
- }
474
-
475
- async function patchProjectDocker9Router(projectDir) {
476
- const dockerDir = path.join(projectDir, 'docker', 'openclaw');
477
- const composePath = path.join(dockerDir, 'docker-compose.yml');
478
- if (!await fs.pathExists(composePath)) return false;
479
-
480
- await fs.ensureDir(dockerDir);
481
- await fs.writeFile(path.join(dockerDir, 'sync.js'), build9RouterSmartRouteSyncScript('/root/.9router/db.json'));
482
- await fs.writeFile(path.join(dockerDir, 'patch-9router.js'), build9RouterPatchScript());
483
- let compose = await fs.readFile(composePath, 'utf8');
484
- compose = compose.replace(
485
- /node -e "require\('fs'\)\.writeFileSync\('\/tmp\/sync\.js',Buffer\.from\('[^']*','base64'\)\.toString\(\)\)"/,
486
- "cp /opt/sync.js /tmp/sync.js"
487
- );
488
- compose = compose.replace(
489
- /(npm install -g [^\n]+\n)/,
490
- `$1 cp /opt/patch-9router.js /tmp/patch-9router.js\n`
491
- );
492
- if (!compose.includes('node /tmp/patch-9router.js || true')) {
493
- compose = compose.replace(
494
- /(\s*node \/tmp\/sync\.js > \/tmp\/sync\.log 2>&1 &\n)/,
495
- ` node /tmp/patch-9router.js || true\n$1`
496
- );
497
- }
498
- if (!compose.includes('./sync.js:/opt/sync.js:ro')) {
499
- compose = compose.replace(
500
- /(\s*-\s*9router-data:\/root\/\.9router\s*\n)/,
501
- `$1 - ./sync.js:/opt/sync.js:ro\n - ./patch-9router.js:/opt/patch-9router.js:ro\n`
502
- );
503
- }
504
- await fs.writeFile(composePath, compose, 'utf8');
505
- return true;
506
- }
444
+ async function writeNative9RouterSyncScript(projectDir) {
445
+ const syncScriptPath = path.join(projectDir, '.openclaw', '9router-smart-route-sync.js');
446
+ await fs.ensureDir(path.dirname(syncScriptPath));
447
+ // Use native home data dir so sync script writes to same place 9router binary reads from
448
+ const nativeDataDir = getNative9RouterDataDir();
449
+ await fs.ensureDir(nativeDataDir);
450
+ await fs.writeFile(syncScriptPath, build9RouterSmartRouteSyncScript(path.join(nativeDataDir, 'db.json')));
451
+ return syncScriptPath;
452
+ }
453
+
454
+ async function writeNative9RouterPatchScript(projectDir) {
455
+ const patchScriptPath = path.join(projectDir, '.openclaw', 'patch-9router.js');
456
+ await fs.ensureDir(path.dirname(patchScriptPath));
457
+ await fs.writeFile(patchScriptPath, build9RouterPatchScript());
458
+ return patchScriptPath;
459
+ }
460
+
461
+ async function patchProject9RouterOpenClawConfig(projectDir) {
462
+ const configPath = path.join(projectDir, '.openclaw', 'openclaw.json');
463
+ if (!await fs.pathExists(configPath)) return false;
464
+ const config = await fs.readJson(configPath);
465
+ const provider = config?.models?.providers?.['9router'];
466
+ if (!provider) return false;
467
+ provider.baseUrl = get9RouterBaseUrl(detectProjectDeployMode(projectDir));
468
+ provider.apiKey = NINE_ROUTER_PROXY_API_KEY;
469
+ provider.api = 'openai-completions';
470
+ provider.models = build9RouterProviderConfig(provider.baseUrl).models;
471
+ await fs.writeJson(configPath, config, { spaces: 2 });
472
+ return true;
473
+ }
474
+
475
+ async function patchProjectDocker9Router(projectDir) {
476
+ const dockerDir = path.join(projectDir, 'docker', 'openclaw');
477
+ const composePath = path.join(dockerDir, 'docker-compose.yml');
478
+ if (!await fs.pathExists(composePath)) return false;
479
+
480
+ await fs.ensureDir(dockerDir);
481
+ await fs.writeFile(path.join(dockerDir, 'sync.js'), build9RouterSmartRouteSyncScript('/root/.9router/db.json'));
482
+ await fs.writeFile(path.join(dockerDir, 'patch-9router.js'), build9RouterPatchScript());
483
+ let compose = await fs.readFile(composePath, 'utf8');
484
+ compose = compose.replace(
485
+ /node -e "require\('fs'\)\.writeFileSync\('\/tmp\/sync\.js',Buffer\.from\('[^']*','base64'\)\.toString\(\)\)"/,
486
+ "cp /opt/sync.js /tmp/sync.js"
487
+ );
488
+ compose = compose.replace(
489
+ /(npm install -g [^\n]+\n)/,
490
+ `$1 cp /opt/patch-9router.js /tmp/patch-9router.js\n`
491
+ );
492
+ if (!compose.includes('node /tmp/patch-9router.js || true')) {
493
+ compose = compose.replace(
494
+ /(\s*node \/tmp\/sync\.js > \/tmp\/sync\.log 2>&1 &\n)/,
495
+ ` node /tmp/patch-9router.js || true\n$1`
496
+ );
497
+ }
498
+ if (!compose.includes('./sync.js:/opt/sync.js:ro')) {
499
+ compose = compose.replace(
500
+ /(\s*-\s*9router-data:\/root\/\.9router\s*\n)/,
501
+ `$1 - ./sync.js:/opt/sync.js:ro\n - ./patch-9router.js:/opt/patch-9router.js:ro\n`
502
+ );
503
+ }
504
+ await fs.writeFile(composePath, compose, 'utf8');
505
+ return true;
506
+ }
507
507
 
508
508
  function getGatewayAllowedOrigins(port) {
509
509
  const normalizedPort = Number(port) || 18791;
@@ -854,7 +854,7 @@ function detectProjectBotName(projectDir) {
854
854
  return path.basename(projectDir);
855
855
  }
856
856
 
857
- function detectProjectUses9Router(projectDir) {
857
+ function detectProjectUses9Router(projectDir) {
858
858
  try {
859
859
  const configPath = path.join(projectDir, '.openclaw', 'openclaw.json');
860
860
  if (fs.existsSync(configPath)) {
@@ -866,25 +866,25 @@ function detectProjectUses9Router(projectDir) {
866
866
  } catch {
867
867
  // fallback below
868
868
  }
869
- return fs.existsSync(path.join(projectDir, '.9router'));
870
- }
871
-
872
- function detectProjectIsMultiBot(projectDir) {
873
- try {
874
- const configPath = path.join(projectDir, '.openclaw', 'openclaw.json');
875
- if (fs.existsSync(configPath)) {
876
- const config = fs.readJsonSync(configPath);
877
- return (config?.agents?.list?.length || 0) > 1;
878
- }
879
- } catch {
880
- // fallback below
881
- }
882
- return false;
883
- }
884
-
885
- function getNativePm2AppName(isMultiBot = false) {
886
- return isMultiBot ? 'openclaw-multibot' : 'openclaw';
887
- }
869
+ return fs.existsSync(path.join(projectDir, '.9router'));
870
+ }
871
+
872
+ function detectProjectIsMultiBot(projectDir) {
873
+ try {
874
+ const configPath = path.join(projectDir, '.openclaw', 'openclaw.json');
875
+ if (fs.existsSync(configPath)) {
876
+ const config = fs.readJsonSync(configPath);
877
+ return (config?.agents?.list?.length || 0) > 1;
878
+ }
879
+ } catch {
880
+ // fallback below
881
+ }
882
+ return false;
883
+ }
884
+
885
+ function getNativePm2AppName(isMultiBot = false) {
886
+ return isMultiBot ? 'openclaw-multibot' : 'openclaw';
887
+ }
888
888
 
889
889
  async function runUpgradeCommand() {
890
890
  const projectDir = findProjectDir();
@@ -894,133 +894,133 @@ async function runUpgradeCommand() {
894
894
  process.exit(1);
895
895
  }
896
896
 
897
- const deployMode = detectProjectDeployMode(projectDir);
898
- const osChoice = getDetectedOsChoice();
899
- const botName = detectProjectBotName(projectDir);
900
- const is9Router = detectProjectUses9Router(projectDir);
901
- const isMultiBot = detectProjectIsMultiBot(projectDir);
897
+ const deployMode = detectProjectDeployMode(projectDir);
898
+ const osChoice = getDetectedOsChoice();
899
+ const botName = detectProjectBotName(projectDir);
900
+ const is9Router = detectProjectUses9Router(projectDir);
901
+ const isMultiBot = detectProjectIsMultiBot(projectDir);
902
902
 
903
903
  console.log(chalk.cyan('\nRefreshing generated OpenClaw project artifacts...'));
904
904
  console.log(chalk.gray(` Project: ${projectDir}`));
905
905
  console.log(chalk.gray(` Mode: ${deployMode}`));
906
906
 
907
907
  await writeGeneratedArtifacts(projectDir, buildCliChromeDebugArtifacts());
908
- await writeGeneratedArtifacts(projectDir, buildCliUninstallArtifacts({
909
- deployMode,
910
- osChoice,
911
- projectDir,
912
- botName: (deployMode !== 'docker' && osChoice === 'vps')
913
- ? getNativePm2AppName(isMultiBot)
914
- : botName,
915
- }));
908
+ await writeGeneratedArtifacts(projectDir, buildCliUninstallArtifacts({
909
+ deployMode,
910
+ osChoice,
911
+ projectDir,
912
+ botName: (deployMode !== 'docker' && osChoice === 'vps')
913
+ ? getNativePm2AppName(isMultiBot)
914
+ : botName,
915
+ }));
916
916
  await writeGeneratedArtifacts(projectDir, buildCliUpgradeArtifacts());
917
917
 
918
- if (deployMode !== 'docker') {
919
- await writeGeneratedArtifacts(projectDir, buildCliStartBotArtifacts({
920
- projectDir,
921
- openclawHome: path.join(projectDir, '.openclaw'),
922
- is9Router,
923
- osChoice,
924
- isMultiBot,
925
- appName: getNativePm2AppName(isMultiBot),
926
- isVi: false,
927
- }));
928
- }
929
-
930
- if (is9Router) {
931
- await writeNative9RouterPatchScript(projectDir);
932
- await patchProject9RouterOpenClawConfig(projectDir);
933
- if (deployMode === 'docker') {
934
- await patchProjectDocker9Router(projectDir);
935
- } else {
936
- await writeNative9RouterSyncScript(projectDir);
937
- try {
938
- execFileSync(process.execPath, [path.join(projectDir, '.openclaw', 'patch-9router.js')], {
939
- cwd: projectDir,
940
- stdio: 'ignore',
941
- });
942
- } catch {
943
- // Best effort: start scripts also retry the patch before launch.
944
- }
945
- }
946
- }
947
-
948
- console.log(chalk.green('\nUpgrade artifacts refreshed successfully.'));
918
+ if (deployMode !== 'docker') {
919
+ await writeGeneratedArtifacts(projectDir, buildCliStartBotArtifacts({
920
+ projectDir,
921
+ openclawHome: path.join(projectDir, '.openclaw'),
922
+ is9Router,
923
+ osChoice,
924
+ isMultiBot,
925
+ appName: getNativePm2AppName(isMultiBot),
926
+ isVi: false,
927
+ }));
928
+ }
929
+
930
+ if (is9Router) {
931
+ await writeNative9RouterPatchScript(projectDir);
932
+ await patchProject9RouterOpenClawConfig(projectDir);
933
+ if (deployMode === 'docker') {
934
+ await patchProjectDocker9Router(projectDir);
935
+ } else {
936
+ await writeNative9RouterSyncScript(projectDir);
937
+ try {
938
+ execFileSync(process.execPath, [path.join(projectDir, '.openclaw', 'patch-9router.js')], {
939
+ cwd: projectDir,
940
+ stdio: 'ignore',
941
+ });
942
+ } catch {
943
+ // Best effort: start scripts also retry the patch before launch.
944
+ }
945
+ }
946
+ }
947
+
948
+ console.log(chalk.green('\nUpgrade artifacts refreshed successfully.'));
949
949
  if (deployMode === 'docker') {
950
950
  console.log(chalk.white(` Next: cd ${path.join(projectDir, 'docker', 'openclaw')} && docker compose up -d --build`));
951
951
  } else {
952
- console.log(chalk.white(` Next: run ${process.platform === 'win32' ? '.\\start-bot.bat' : './start-bot.sh'} from ${projectDir}`));
953
- }
954
- }
955
-
956
- function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
957
- const routerAppName = `${appName}-9router`;
958
- const syncAppName = `${appName}-9router-sync`;
959
- const routerLaunch = resolveNative9RouterDesktopLaunch();
960
- const normalizedProjectDir = projectDir.replace(/\\/g, '/');
961
- const normalizedSyncScriptPath = syncScriptPath ? syncScriptPath.replace(/\\/g, '/') : '';
962
- try {
963
- execSync(`pm2 delete ${routerAppName}`, {
964
- cwd: projectDir,
965
- stdio: 'ignore',
966
- shell: true
967
- });
968
- } catch {
969
- // ignore missing app
970
- }
971
- execFileSync('pm2', [
972
- 'start',
973
- routerLaunch.command,
974
- '--name',
975
- routerAppName,
976
- '--cwd',
977
- normalizedProjectDir,
978
- '--interpreter',
979
- 'none',
980
- '--',
981
- ...routerLaunch.args
982
- ], {
983
- cwd: projectDir,
984
- stdio: 'inherit',
985
- env: { ...process.env, ...routerLaunch.env }
986
- });
987
- if (syncScriptPath) {
988
- try {
989
- execSync(`pm2 delete ${syncAppName}`, {
990
- cwd: projectDir,
991
- stdio: 'ignore',
992
- shell: true
993
- });
994
- } catch {
995
- // ignore missing app
996
- }
997
- try {
998
- execFileSync('pm2', [
999
- 'start',
1000
- normalizedSyncScriptPath,
1001
- '--name',
1002
- syncAppName,
1003
- '--cwd',
1004
- normalizedProjectDir,
1005
- '--interpreter',
1006
- process.execPath,
1007
- '--no-autorestart',
1008
- ], {
1009
- cwd: projectDir,
1010
- stdio: 'inherit',
1011
- env: process.env
1012
- });
1013
- } catch (syncErr) {
1014
- console.log(chalk.yellow(isVi
1015
- ? `\n⚠️ Khong the tu dong khoi dong sync script qua PM2.`
1016
- : `\n⚠️ Could not auto-start 9router sync script via PM2.`));
1017
- }
1018
- }
1019
- runPm2Save({ projectDir, isVi });
1020
- console.log(chalk.green(`\n✅ ${isVi ? '9Router da duoc khoi dong qua PM2.' : '9Router is running via PM2.'}`));
1021
- console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${routerAppName}` : ` View logs: pm2 logs ${routerAppName}`));
1022
- }
1023
-
952
+ console.log(chalk.white(` Next: run ${process.platform === 'win32' ? '.\\start-bot.bat' : './start-bot.sh'} from ${projectDir}`));
953
+ }
954
+ }
955
+
956
+ function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
957
+ const routerAppName = `${appName}-9router`;
958
+ const syncAppName = `${appName}-9router-sync`;
959
+ const routerLaunch = resolveNative9RouterDesktopLaunch();
960
+ const normalizedProjectDir = projectDir.replace(/\\/g, '/');
961
+ const normalizedSyncScriptPath = syncScriptPath ? syncScriptPath.replace(/\\/g, '/') : '';
962
+ try {
963
+ execSync(`pm2 delete ${routerAppName}`, {
964
+ cwd: projectDir,
965
+ stdio: 'ignore',
966
+ shell: true
967
+ });
968
+ } catch {
969
+ // ignore missing app
970
+ }
971
+ execFileSync('pm2', [
972
+ 'start',
973
+ routerLaunch.command,
974
+ '--name',
975
+ routerAppName,
976
+ '--cwd',
977
+ normalizedProjectDir,
978
+ '--interpreter',
979
+ 'none',
980
+ '--',
981
+ ...routerLaunch.args
982
+ ], {
983
+ cwd: projectDir,
984
+ stdio: 'inherit',
985
+ env: { ...process.env, ...routerLaunch.env }
986
+ });
987
+ if (syncScriptPath) {
988
+ try {
989
+ execSync(`pm2 delete ${syncAppName}`, {
990
+ cwd: projectDir,
991
+ stdio: 'ignore',
992
+ shell: true
993
+ });
994
+ } catch {
995
+ // ignore missing app
996
+ }
997
+ try {
998
+ execFileSync('pm2', [
999
+ 'start',
1000
+ normalizedSyncScriptPath,
1001
+ '--name',
1002
+ syncAppName,
1003
+ '--cwd',
1004
+ normalizedProjectDir,
1005
+ '--interpreter',
1006
+ process.execPath,
1007
+ '--no-autorestart',
1008
+ ], {
1009
+ cwd: projectDir,
1010
+ stdio: 'inherit',
1011
+ env: process.env
1012
+ });
1013
+ } catch (syncErr) {
1014
+ console.log(chalk.yellow(isVi
1015
+ ? `\n⚠️ Khong the tu dong khoi dong sync script qua PM2.`
1016
+ : `\n⚠️ Could not auto-start 9router sync script via PM2.`));
1017
+ }
1018
+ }
1019
+ runPm2Save({ projectDir, isVi });
1020
+ console.log(chalk.green(`\n✅ ${isVi ? '9Router da duoc khoi dong qua PM2.' : '9Router is running via PM2.'}`));
1021
+ console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${routerAppName}` : ` View logs: pm2 logs ${routerAppName}`));
1022
+ }
1023
+
1024
1024
  async function ensureProjectRuntimeDirs(projectDir, isVi) {
1025
1025
  await fs.ensureDir(path.join(projectDir, '.openclaw'));
1026
1026
  await fs.ensureDir(getProject9RouterDataDir(projectDir));
@@ -1070,9 +1070,9 @@ function providerSupportsMemoryEmbeddings(providerKey) {
1070
1070
  return !!PROVIDERS[providerKey]?.supportsEmbeddings;
1071
1071
  }
1072
1072
 
1073
- function getCliSkillChoices({ providerKey, isVi }) {
1074
- const memoryRecommended = providerSupportsMemoryEmbeddings(providerKey);
1075
- return SKILLS
1073
+ function getCliSkillChoices({ providerKey, isVi }) {
1074
+ const memoryRecommended = providerSupportsMemoryEmbeddings(providerKey);
1075
+ return SKILLS
1076
1076
  .filter((skill) => skill.value !== 'memory' || providerSupportsMemoryEmbeddings(providerKey) || skill.id === 'memory')
1077
1077
  .map((skill) => {
1078
1078
  const value = skill.value || skill.id;
@@ -1087,677 +1087,677 @@ function getCliSkillChoices({ providerKey, isVi }) {
1087
1087
  value,
1088
1088
  checked: value === 'browser' || value === 'scheduler' || (value === 'memory' && memoryRecommended),
1089
1089
  };
1090
- });
1091
- }
1092
-
1093
- const CLI_BACK = '__openclaw_cli_back__';
1094
-
1095
- function getBackChoice(isVi) {
1096
- return {
1097
- name: isVi ? '← Quay lại' : '← Back',
1098
- value: CLI_BACK,
1099
- };
1100
- }
1101
-
1102
- function withBackHint(message, isVi) {
1103
- return `${message} ${isVi ? '(gõ "back" để quay lại)' : '(type "back" to go back)'}`;
1104
- }
1105
-
1106
- async function selectWithBack({ message, choices, defaultValue, allowBack = false, isVi = true }) {
1107
- const finalChoices = allowBack ? [...choices, getBackChoice(isVi)] : choices;
1108
- return select({
1109
- message,
1110
- choices: finalChoices,
1111
- ...(defaultValue ? { default: defaultValue } : {}),
1112
- });
1113
- }
1114
-
1115
- async function inputWithBack({ message, defaultValue = '', required = false, allowBack = false, isVi = true }) {
1116
- const value = await input({
1117
- message: allowBack ? withBackHint(message, isVi) : message,
1118
- default: defaultValue,
1119
- required,
1120
- });
1121
- if (allowBack && String(value || '').trim().toLowerCase() === 'back') {
1122
- return CLI_BACK;
1123
- }
1124
- return value;
1125
- }
1126
-
1127
- async function checkboxWithBack({ message, choices, isVi = true, allowBack = false }) {
1128
- const finalChoices = allowBack ? [...choices, getBackChoice(isVi)] : choices;
1129
- const value = await checkbox({
1130
- message,
1131
- choices: finalChoices,
1132
- });
1133
- return allowBack && value.includes(CLI_BACK) ? CLI_BACK : value;
1134
- }
1135
-
1136
- async function collectBotSetupStep({
1137
- isVi,
1138
- channelKey,
1139
- channel,
1140
- existingBots = [],
1141
- existingBotCount = 1,
1142
- existingGroupId = '',
1143
- }) {
1144
- let botCount = channelKey === 'telegram' ? existingBotCount : 1;
1145
- let groupId = existingGroupId;
1146
- const bots = [];
1147
-
1148
- if (channelKey === 'telegram') {
1149
- const botCountValue = await selectWithBack({
1150
- message: isVi ? 'Bạn muốn cài bao nhiêu Telegram bot?' : 'How many Telegram bots do you want to deploy?',
1151
- choices: [
1152
- { name: '1 bot (single)', value: '1' },
1153
- { name: '2 bots (Department Room)', value: '2' },
1154
- { name: '3 bots', value: '3' },
1155
- { name: '4 bots', value: '4' },
1156
- { name: '5 bots', value: '5' },
1157
- ],
1158
- defaultValue: String(existingBotCount || 1),
1159
- allowBack: true,
1160
- isVi,
1161
- });
1162
- if (botCountValue === CLI_BACK) {
1163
- return { back: true };
1164
- }
1165
- botCount = parseInt(botCountValue, 10);
1166
-
1167
- if (botCount > 1) {
1168
- const groupOption = await selectWithBack({
1169
- message: isVi ? 'Bạn có sẵn Telegram Group chưa?' : 'Do you already have a Telegram Group?',
1170
- choices: [
1171
- { 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' },
1172
- { name: isVi ? '🔗 Đã có group — nhập Group ID ngay' : '🔗 Already have a group — enter Group ID now', value: 'existing' }
1173
- ],
1174
- defaultValue: groupId ? 'existing' : 'create',
1175
- allowBack: true,
1176
- isVi,
1177
- });
1178
- if (groupOption === CLI_BACK) {
1179
- return { back: true };
1180
- }
1181
-
1182
- if (groupOption === 'existing') {
1183
- console.log(chalk.dim(isVi
1184
- ? '\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'
1185
- : '\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'));
1186
- const nextGroupId = await inputWithBack({
1187
- message: isVi ? 'Telegram Group ID (VD: -1001234567890):' : 'Telegram Group ID (e.g. -1001234567890):',
1188
- defaultValue: groupId,
1189
- allowBack: true,
1190
- isVi,
1191
- });
1192
- if (nextGroupId === CLI_BACK) {
1193
- return { back: true };
1194
- }
1195
- groupId = nextGroupId;
1196
- } else {
1197
- groupId = '';
1198
- }
1199
- } else {
1200
- groupId = '';
1201
- }
1202
-
1203
- for (let i = 0; i < botCount; i++) {
1204
- console.log(chalk.bold(`\n${isVi ? `─── Bot ${i + 1} / ${botCount} ───` : `─── Bot ${i + 1} / ${botCount} ───`}`));
1205
- const defaults = existingBots[i] || {};
1206
- const fields = [
1207
- {
1208
- key: 'name',
1209
- message: isVi ? `Tên Bot ${i + 1}:` : `Bot ${i + 1} name:`,
1210
- defaultValue: defaults.name || `Bot ${i + 1}`,
1211
- required: true,
1212
- },
1213
- {
1214
- key: 'slashCmd',
1215
- message: isVi ? `Slash command (VD: /bot${i + 1}):` : `Slash command (e.g. /bot${i + 1}):`,
1216
- defaultValue: defaults.slashCmd || `/bot${i + 1}`,
1217
- required: true,
1218
- },
1219
- {
1220
- key: 'desc',
1221
- message: isVi ? `Mô tả Bot ${i + 1} (VD: Trợ lý AI cá nhân):` : `Bot ${i + 1} description (e.g. Personal AI assistant):`,
1222
- defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1223
- required: true,
1224
- },
1225
- {
1226
- key: 'persona',
1227
- 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):`,
1228
- defaultValue: defaults.persona || '',
1229
- required: false,
1230
- },
1231
- {
1232
- key: 'token',
1233
- message: isVi ? 'Bot Token (từ @BotFather):' : 'Bot Token (from @BotFather):',
1234
- defaultValue: defaults.token || '',
1235
- required: true,
1236
- },
1237
- ];
1238
-
1239
- const draft = { ...defaults };
1240
- let fieldIndex = 0;
1241
- while (fieldIndex < fields.length) {
1242
- const field = fields[fieldIndex];
1243
- const value = await inputWithBack({
1244
- message: field.message,
1245
- defaultValue: draft[field.key] || field.defaultValue,
1246
- required: field.required,
1247
- allowBack: true,
1248
- isVi,
1249
- });
1250
- if (value === CLI_BACK) {
1251
- if (fieldIndex > 0) {
1252
- fieldIndex--;
1253
- continue;
1254
- }
1255
- return { back: true };
1256
- }
1257
- draft[field.key] = value;
1258
- fieldIndex++;
1259
- }
1260
- bots.push(draft);
1261
- }
1262
- } else if (channelKey !== 'zalo-personal') {
1263
- const defaults = existingBots[0] || {};
1264
- const fields = [
1265
- {
1266
- key: 'name',
1267
- message: isVi ? 'Tên Bot:' : 'Bot Name:',
1268
- defaultValue: defaults.name || 'Chat Bot',
1269
- required: true,
1270
- },
1271
- {
1272
- key: 'desc',
1273
- message: isVi ? 'Mô tả Bot:' : 'Bot Description:',
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 (VD: gọn gàng, thân thiện):' : 'Persona & rules (e.g. concise, friendly):',
1280
- defaultValue: defaults.persona || '',
1281
- required: false,
1282
- },
1283
- {
1284
- key: 'token',
1285
- message: isVi ? `Nhập ${channel.name} Token:` : `Enter ${channel.name} Token:`,
1286
- defaultValue: defaults.token || '',
1287
- required: true,
1288
- },
1289
- ];
1290
- const draft = { ...defaults, slashCmd: '' };
1291
- let fieldIndex = 0;
1292
- while (fieldIndex < fields.length) {
1293
- const field = fields[fieldIndex];
1294
- const value = await inputWithBack({
1295
- message: field.message,
1296
- defaultValue: draft[field.key] || field.defaultValue,
1297
- required: field.required,
1298
- allowBack: true,
1299
- isVi,
1300
- });
1301
- if (value === CLI_BACK) {
1302
- if (fieldIndex > 0) {
1303
- fieldIndex--;
1304
- continue;
1305
- }
1306
- return { back: true };
1307
- }
1308
- draft[field.key] = value;
1309
- fieldIndex++;
1310
- }
1311
- bots.push(draft);
1312
- } else {
1313
- bots.push({ name: 'Bot', slashCmd: '', desc: '', persona: '', token: '' });
1314
- }
1315
-
1316
- return {
1317
- back: false,
1318
- botCount,
1319
- groupId,
1320
- bots,
1321
- botToken: bots[0]?.token || '',
1322
- };
1323
- }
1324
-
1325
- async function collectBotSetupStepWithGroupBack(options) {
1326
- const {
1327
- isVi,
1328
- channelKey,
1329
- channel,
1330
- existingBots = [],
1331
- existingBotCount = 1,
1332
- existingGroupId = '',
1333
- } = options;
1334
-
1335
- let botCount = channelKey === 'telegram' ? existingBotCount : 1;
1336
- let groupId = existingGroupId;
1337
- const bots = [];
1338
-
1339
- if (channelKey === 'telegram') {
1340
- const botCountValue = await selectWithBack({
1341
- message: isVi ? 'Bạn muốn cài bao nhiêu Telegram bot?' : 'How many Telegram bots do you want to deploy?',
1342
- choices: [
1343
- { name: '1 bot (single)', value: '1' },
1344
- { name: '2 bots (Department Room)', value: '2' },
1345
- { name: '3 bots', value: '3' },
1346
- { name: '4 bots', value: '4' },
1347
- { name: '5 bots', value: '5' },
1348
- ],
1349
- defaultValue: String(existingBotCount || 1),
1350
- allowBack: true,
1351
- isVi,
1352
- });
1353
- if (botCountValue === CLI_BACK) {
1354
- return { back: true };
1355
- }
1356
- botCount = parseInt(botCountValue, 10);
1357
-
1358
- if (botCount > 1) {
1359
- while (true) {
1360
- const groupOption = await selectWithBack({
1361
- message: isVi ? 'Bạn có sẵn Telegram Group chưa?' : 'Do you already have a Telegram Group?',
1362
- choices: [
1363
- { 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' },
1364
- { name: isVi ? '🔗 Đã có group — nhập Group ID ngay' : '🔗 Already have a group — enter Group ID now', value: 'existing' }
1365
- ],
1366
- defaultValue: groupId ? 'existing' : 'create',
1367
- allowBack: true,
1368
- isVi,
1369
- });
1370
- if (groupOption === CLI_BACK) {
1371
- return { back: true };
1372
- }
1373
-
1374
- if (groupOption === 'existing') {
1375
- console.log(chalk.dim(isVi
1376
- ? '\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'
1377
- : '\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'));
1378
- const nextGroupId = await inputWithBack({
1379
- message: isVi ? 'Telegram Group ID (VD: -1001234567890):' : 'Telegram Group ID (e.g. -1001234567890):',
1380
- defaultValue: groupId,
1381
- allowBack: true,
1382
- isVi,
1383
- });
1384
- if (nextGroupId === CLI_BACK) {
1385
- continue;
1386
- }
1387
- groupId = nextGroupId;
1388
- break;
1389
- }
1390
-
1391
- groupId = '';
1392
- break;
1393
- }
1394
- } else {
1395
- groupId = '';
1396
- }
1397
-
1398
- for (let i = 0; i < botCount; i++) {
1399
- console.log(chalk.bold(`\n${isVi ? `─── Bot ${i + 1} / ${botCount} ───` : `─── Bot ${i + 1} / ${botCount} ───`}`));
1400
- const defaults = existingBots[i] || {};
1401
- const fields = [
1402
- {
1403
- key: 'name',
1404
- message: isVi ? `Tên Bot ${i + 1}:` : `Bot ${i + 1} name:`,
1405
- defaultValue: defaults.name || `Bot ${i + 1}`,
1406
- required: true,
1407
- },
1408
- {
1409
- key: 'slashCmd',
1410
- message: isVi ? `Slash command (VD: /bot${i + 1}):` : `Slash command (e.g. /bot${i + 1}):`,
1411
- defaultValue: defaults.slashCmd || `/bot${i + 1}`,
1412
- required: true,
1413
- },
1414
- {
1415
- key: 'desc',
1416
- message: isVi ? `Mô tả Bot ${i + 1} (VD: Trợ lý AI cá nhân):` : `Bot ${i + 1} description (e.g. Personal AI assistant):`,
1417
- defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1418
- required: true,
1419
- },
1420
- {
1421
- key: 'persona',
1422
- 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):`,
1423
- defaultValue: defaults.persona || '',
1424
- required: false,
1425
- },
1426
- {
1427
- key: 'token',
1428
- message: isVi ? 'Bot Token (từ @BotFather):' : 'Bot Token (from @BotFather):',
1429
- defaultValue: defaults.token || '',
1430
- required: true,
1431
- },
1432
- ];
1433
-
1434
- const draft = { ...defaults };
1435
- let fieldIndex = 0;
1436
- while (fieldIndex < fields.length) {
1437
- const field = fields[fieldIndex];
1438
- const value = await inputWithBack({
1439
- message: field.message,
1440
- defaultValue: draft[field.key] || field.defaultValue,
1441
- required: field.required,
1442
- allowBack: true,
1443
- isVi,
1444
- });
1445
- if (value === CLI_BACK) {
1446
- if (fieldIndex > 0) {
1447
- fieldIndex--;
1448
- continue;
1449
- }
1450
- return { back: true };
1451
- }
1452
- draft[field.key] = value;
1453
- fieldIndex++;
1454
- }
1455
- bots.push(draft);
1456
- }
1457
- } else if (channelKey !== 'zalo-personal') {
1458
- const defaults = existingBots[0] || {};
1459
- const fields = [
1460
- {
1461
- key: 'name',
1462
- message: isVi ? 'Tên Bot:' : 'Bot Name:',
1463
- defaultValue: defaults.name || 'Chat Bot',
1464
- required: true,
1465
- },
1466
- {
1467
- key: 'desc',
1468
- message: isVi ? 'Mô tả Bot:' : 'Bot Description:',
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 (VD: gọn gàng, thân thiện):' : 'Persona & rules (e.g. concise, friendly):',
1475
- defaultValue: defaults.persona || '',
1476
- required: false,
1477
- },
1478
- {
1479
- key: 'token',
1480
- message: isVi ? `Nhập ${channel.name} Token:` : `Enter ${channel.name} Token:`,
1481
- defaultValue: defaults.token || '',
1482
- required: true,
1483
- },
1484
- ];
1485
- const draft = { ...defaults, slashCmd: '' };
1486
- let fieldIndex = 0;
1487
- while (fieldIndex < fields.length) {
1488
- const field = fields[fieldIndex];
1489
- const value = await inputWithBack({
1490
- message: field.message,
1491
- defaultValue: draft[field.key] || field.defaultValue,
1492
- required: field.required,
1493
- allowBack: true,
1494
- isVi,
1495
- });
1496
- if (value === CLI_BACK) {
1497
- if (fieldIndex > 0) {
1498
- fieldIndex--;
1499
- continue;
1500
- }
1501
- return { back: true };
1502
- }
1503
- draft[field.key] = value;
1504
- fieldIndex++;
1505
- }
1506
- bots.push(draft);
1507
- } else {
1508
- bots.push({ name: 'Bot', slashCmd: '', desc: '', persona: '', token: '' });
1509
- }
1510
-
1511
- return {
1512
- back: false,
1513
- botCount,
1514
- groupId,
1515
- bots,
1516
- botToken: bots[0]?.token || '',
1517
- };
1518
- }
1519
-
1520
- async function collectProviderStep({
1521
- isVi,
1522
- existingProviderKey = '',
1523
- existingProviderKeyVal = '',
1524
- existingOllamaModel = 'gemma4:e2b',
1525
- }) {
1526
- const providerKey = await selectWithBack({
1527
- message: isVi ? 'Chọn AI Provider:' : 'Select AI Provider:',
1528
- choices: Object.entries(PROVIDERS).map(([k, v]) => ({ name: `${v.icon} ${v.name}`, value: k })),
1529
- defaultValue: existingProviderKey || undefined,
1530
- allowBack: true,
1531
- isVi,
1532
- });
1533
- if (providerKey === CLI_BACK) {
1534
- return { back: true };
1535
- }
1536
- const provider = PROVIDERS[providerKey];
1537
-
1538
- let providerKeyVal = existingProviderKey === providerKey ? existingProviderKeyVal : '';
1539
- if (!provider.isProxy && !provider.isLocal) {
1540
- const keyValue = await inputWithBack({
1541
- message: isVi ? `Nhập ${provider.envKey}:` : `Enter ${provider.envKey}:`,
1542
- defaultValue: providerKeyVal,
1543
- required: true,
1544
- allowBack: true,
1545
- isVi,
1546
- });
1547
- if (keyValue === CLI_BACK) {
1548
- return { back: true };
1549
- }
1550
- providerKeyVal = keyValue;
1551
- }
1552
-
1553
- let selectedOllamaModel = existingProviderKey === 'ollama' ? existingOllamaModel : 'gemma4:e2b';
1554
- if (providerKey === 'ollama') {
1555
- console.log(chalk.yellow(isVi
1556
- ? '\n💡 Gemma 4 (02/04/2026) — chọn kích thước phù hợp với RAM máy bạn:'
1557
- : '\n💡 Gemma 4 (April 2, 2026) — pick a size that fits your RAM:'));
1558
- const modelValue = await selectWithBack({
1559
- message: isVi ? 'Chọn model Ollama:' : 'Select Ollama model:',
1560
- choices: [
1561
- {
1562
- name: isVi
1563
- ? '🟢 gemma4:e2b — Nhẹ nhất (~4-6 GB RAM) — Laptop / test nhanh ★ Khuyên dùng'
1564
- : '🟢 gemma4:e2b — Lightest (~4-6 GB RAM) — Laptop / fastest test ★ Recommended',
1565
- value: 'gemma4:e2b'
1566
- },
1567
- {
1568
- name: isVi
1569
- ? '🟡 gemma4:e4b — Cân bằng (~8-10 GB RAM) — Dùng hằng ngày'
1570
- : '🟡 gemma4:e4b — Balanced (~8-10 GB RAM) — Daily use',
1571
- value: 'gemma4:e4b'
1572
- },
1573
- {
1574
- name: isVi
1575
- ? '🟠 gemma4:26b — Mạnh (~18-24 GB RAM/VRAM) — Máy mạnh'
1576
- : '🟠 gemma4:26b — Powerful (~18-24 GB RAM/VRAM) — High-end machine',
1577
- value: 'gemma4:26b'
1578
- },
1579
- {
1580
- name: isVi
1581
- ? '🔴 gemma4:31b — Mạnh nhất (~24+ GB RAM/VRAM) — GPU workstation'
1582
- : '🔴 gemma4:31b — Most powerful (~24+ GB RAM/VRAM) — GPU workstation',
1583
- value: 'gemma4:31b'
1584
- },
1585
- ],
1586
- defaultValue: selectedOllamaModel,
1587
- allowBack: true,
1588
- isVi,
1589
- });
1590
- if (modelValue === CLI_BACK) {
1591
- return { back: true };
1592
- }
1593
- selectedOllamaModel = modelValue;
1594
- }
1595
-
1596
- return {
1597
- back: false,
1598
- providerKey,
1599
- provider,
1600
- providerKeyVal,
1601
- selectedOllamaModel,
1602
- };
1603
- }
1604
-
1605
- async function collectSkillsStep({
1606
- isVi,
1607
- providerKey,
1608
- existingSelectedSkills = [],
1609
- existingBrowserMode = 'server',
1610
- existingTtsOpenaiKey = '',
1611
- existingTtsElevenKey = '',
1612
- existingSmtpHost = 'smtp.gmail.com',
1613
- existingSmtpPort = '587',
1614
- existingSmtpUser = '',
1615
- existingSmtpPass = '',
1616
- }) {
1617
- const selectedSkills = await checkboxWithBack({
1618
- message: isVi ? 'Bật tính năng bổ sung (Space để chọn):' : 'Enable extra skills (Space to select):',
1619
- choices: getCliSkillChoices({ providerKey, isVi }).map((choice) => ({
1620
- ...choice,
1621
- checked: existingSelectedSkills.includes(choice.value),
1622
- })),
1623
- allowBack: true,
1624
- isVi,
1625
- });
1626
- if (selectedSkills === CLI_BACK) {
1627
- return { back: true };
1628
- }
1629
-
1630
- let browserMode = existingBrowserMode;
1631
- if (selectedSkills.includes('browser')) {
1632
- const isLinux = process.platform === 'linux';
1633
- const browserValue = await selectWithBack({
1634
- message: isVi ? 'Chế độ Browser Automation:' : 'Browser Automation mode:',
1635
- choices: [
1636
- {
1637
- name: isVi
1638
- ? '🖥️ Dùng Chrome trên máy tính (Windows/Mac — Bypass Cloudflare tốt hơn)'
1639
- : '🖥️ Use Host Chrome (Windows/Mac — Better Cloudflare bypass)',
1640
- value: 'desktop'
1641
- },
1642
- {
1643
- name: isVi
1644
- ? '🐳 Headless Chromium trong Docker (Ubuntu Server / VPS — không cần GUI)'
1645
- : '🐳 Headless Chromium inside Docker (Ubuntu Server / VPS — No GUI)',
1646
- value: 'server'
1647
- }
1648
- ],
1649
- defaultValue: browserMode || (isLinux ? 'server' : 'desktop'),
1650
- allowBack: true,
1651
- isVi,
1652
- });
1653
- if (browserValue === CLI_BACK) {
1654
- return { back: true };
1655
- }
1656
- browserMode = browserValue;
1657
- } else {
1658
- browserMode = 'server';
1659
- }
1660
-
1661
- let ttsOpenaiKey = existingTtsOpenaiKey;
1662
- let ttsElevenKey = existingTtsElevenKey;
1663
- if (selectedSkills.includes('tts')) {
1664
- const openaiKey = await inputWithBack({
1665
- 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):',
1666
- defaultValue: ttsOpenaiKey,
1667
- allowBack: true,
1668
- isVi,
1669
- });
1670
- if (openaiKey === CLI_BACK) {
1671
- return { back: true };
1672
- }
1673
- ttsOpenaiKey = openaiKey;
1674
-
1675
- const elevenKey = await inputWithBack({
1676
- message: isVi ? 'Nhập ELEVENLABS_API_KEY (hoặc bỏ trống):' : 'Enter ELEVENLABS_API_KEY (or leave empty):',
1677
- defaultValue: ttsElevenKey,
1678
- allowBack: true,
1679
- isVi,
1680
- });
1681
- if (elevenKey === CLI_BACK) {
1682
- return { back: true };
1683
- }
1684
- ttsElevenKey = elevenKey;
1685
- } else {
1686
- ttsOpenaiKey = '';
1687
- ttsElevenKey = '';
1688
- }
1689
-
1690
- let smtpHost = existingSmtpHost;
1691
- let smtpPort = existingSmtpPort;
1692
- let smtpUser = existingSmtpUser;
1693
- let smtpPass = existingSmtpPass;
1694
- if (selectedSkills.includes('email')) {
1695
- const smtpHostValue = await inputWithBack({
1696
- message: isVi ? 'SMTP Host (VD: smtp.gmail.com):' : 'SMTP Host (e.g. smtp.gmail.com):',
1697
- defaultValue: smtpHost,
1698
- required: true,
1699
- allowBack: true,
1700
- isVi,
1701
- });
1702
- if (smtpHostValue === CLI_BACK) {
1703
- return { back: true };
1704
- }
1705
- smtpHost = smtpHostValue;
1706
-
1707
- const smtpPortValue = await inputWithBack({
1708
- message: 'SMTP Port:',
1709
- defaultValue: smtpPort,
1710
- required: true,
1711
- allowBack: true,
1712
- isVi,
1713
- });
1714
- if (smtpPortValue === CLI_BACK) {
1715
- return { back: true };
1716
- }
1717
- smtpPort = smtpPortValue;
1718
-
1719
- const smtpUserValue = await inputWithBack({
1720
- message: isVi ? 'SMTP Email:' : 'SMTP Email:',
1721
- defaultValue: smtpUser,
1722
- required: true,
1723
- allowBack: true,
1724
- isVi,
1725
- });
1726
- if (smtpUserValue === CLI_BACK) {
1727
- return { back: true };
1728
- }
1729
- smtpUser = smtpUserValue;
1730
-
1731
- const smtpPassValue = await inputWithBack({
1732
- message: isVi ? 'SMTP App Password:' : 'SMTP App Password:',
1733
- defaultValue: smtpPass,
1734
- required: true,
1735
- allowBack: true,
1736
- isVi,
1737
- });
1738
- if (smtpPassValue === CLI_BACK) {
1739
- return { back: true };
1740
- }
1741
- smtpPass = smtpPassValue;
1742
- } else {
1743
- smtpHost = 'smtp.gmail.com';
1744
- smtpPort = '587';
1745
- smtpUser = '';
1746
- smtpPass = '';
1747
- }
1748
-
1749
- return {
1750
- back: false,
1751
- selectedSkills,
1752
- browserMode,
1753
- ttsOpenaiKey,
1754
- ttsElevenKey,
1755
- smtpHost,
1756
- smtpPort,
1757
- smtpUser,
1758
- smtpPass,
1759
- };
1760
- }
1090
+ });
1091
+ }
1092
+
1093
+ const CLI_BACK = '__openclaw_cli_back__';
1094
+
1095
+ function getBackChoice(isVi) {
1096
+ return {
1097
+ name: isVi ? '← Quay lại' : '← Back',
1098
+ value: CLI_BACK,
1099
+ };
1100
+ }
1101
+
1102
+ function withBackHint(message, isVi) {
1103
+ return `${message} ${isVi ? '(gõ "back" để quay lại)' : '(type "back" to go back)'}`;
1104
+ }
1105
+
1106
+ async function selectWithBack({ message, choices, defaultValue, allowBack = false, isVi = true }) {
1107
+ const finalChoices = allowBack ? [...choices, getBackChoice(isVi)] : choices;
1108
+ return select({
1109
+ message,
1110
+ choices: finalChoices,
1111
+ ...(defaultValue ? { default: defaultValue } : {}),
1112
+ });
1113
+ }
1114
+
1115
+ async function inputWithBack({ message, defaultValue = '', required = false, allowBack = false, isVi = true }) {
1116
+ const value = await input({
1117
+ message: allowBack ? withBackHint(message, isVi) : message,
1118
+ default: defaultValue,
1119
+ required,
1120
+ });
1121
+ if (allowBack && String(value || '').trim().toLowerCase() === 'back') {
1122
+ return CLI_BACK;
1123
+ }
1124
+ return value;
1125
+ }
1126
+
1127
+ async function checkboxWithBack({ message, choices, isVi = true, allowBack = false }) {
1128
+ const finalChoices = allowBack ? [...choices, getBackChoice(isVi)] : choices;
1129
+ const value = await checkbox({
1130
+ message,
1131
+ choices: finalChoices,
1132
+ });
1133
+ return allowBack && value.includes(CLI_BACK) ? CLI_BACK : value;
1134
+ }
1135
+
1136
+ async function collectBotSetupStep({
1137
+ isVi,
1138
+ channelKey,
1139
+ channel,
1140
+ existingBots = [],
1141
+ existingBotCount = 1,
1142
+ existingGroupId = '',
1143
+ }) {
1144
+ let botCount = channelKey === 'telegram' ? existingBotCount : 1;
1145
+ let groupId = existingGroupId;
1146
+ const bots = [];
1147
+
1148
+ if (channelKey === 'telegram') {
1149
+ const botCountValue = await selectWithBack({
1150
+ message: isVi ? 'Bạn muốn cài bao nhiêu Telegram bot?' : 'How many Telegram bots do you want to deploy?',
1151
+ choices: [
1152
+ { name: '1 bot (single)', value: '1' },
1153
+ { name: '2 bots (Department Room)', value: '2' },
1154
+ { name: '3 bots', value: '3' },
1155
+ { name: '4 bots', value: '4' },
1156
+ { name: '5 bots', value: '5' },
1157
+ ],
1158
+ defaultValue: String(existingBotCount || 1),
1159
+ allowBack: true,
1160
+ isVi,
1161
+ });
1162
+ if (botCountValue === CLI_BACK) {
1163
+ return { back: true };
1164
+ }
1165
+ botCount = parseInt(botCountValue, 10);
1166
+
1167
+ if (botCount > 1) {
1168
+ const groupOption = await selectWithBack({
1169
+ message: isVi ? 'Bạn có sẵn Telegram Group chưa?' : 'Do you already have a Telegram Group?',
1170
+ choices: [
1171
+ { 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' },
1172
+ { name: isVi ? '🔗 Đã có group — nhập Group ID ngay' : '🔗 Already have a group — enter Group ID now', value: 'existing' }
1173
+ ],
1174
+ defaultValue: groupId ? 'existing' : 'create',
1175
+ allowBack: true,
1176
+ isVi,
1177
+ });
1178
+ if (groupOption === CLI_BACK) {
1179
+ return { back: true };
1180
+ }
1181
+
1182
+ if (groupOption === 'existing') {
1183
+ console.log(chalk.dim(isVi
1184
+ ? '\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'
1185
+ : '\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'));
1186
+ const nextGroupId = await inputWithBack({
1187
+ message: isVi ? 'Telegram Group ID (VD: -1001234567890):' : 'Telegram Group ID (e.g. -1001234567890):',
1188
+ defaultValue: groupId,
1189
+ allowBack: true,
1190
+ isVi,
1191
+ });
1192
+ if (nextGroupId === CLI_BACK) {
1193
+ return { back: true };
1194
+ }
1195
+ groupId = nextGroupId;
1196
+ } else {
1197
+ groupId = '';
1198
+ }
1199
+ } else {
1200
+ groupId = '';
1201
+ }
1202
+
1203
+ for (let i = 0; i < botCount; i++) {
1204
+ console.log(chalk.bold(`\n${isVi ? `─── Bot ${i + 1} / ${botCount} ───` : `─── Bot ${i + 1} / ${botCount} ───`}`));
1205
+ const defaults = existingBots[i] || {};
1206
+ const fields = [
1207
+ {
1208
+ key: 'name',
1209
+ message: isVi ? `Tên Bot ${i + 1}:` : `Bot ${i + 1} name:`,
1210
+ defaultValue: defaults.name || `Bot ${i + 1}`,
1211
+ required: true,
1212
+ },
1213
+ {
1214
+ key: 'slashCmd',
1215
+ message: isVi ? `Slash command (VD: /bot${i + 1}):` : `Slash command (e.g. /bot${i + 1}):`,
1216
+ defaultValue: defaults.slashCmd || `/bot${i + 1}`,
1217
+ required: true,
1218
+ },
1219
+ {
1220
+ key: 'desc',
1221
+ message: isVi ? `Mô tả Bot ${i + 1} (VD: Trợ lý AI cá nhân):` : `Bot ${i + 1} description (e.g. Personal AI assistant):`,
1222
+ defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1223
+ required: true,
1224
+ },
1225
+ {
1226
+ key: 'persona',
1227
+ 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):`,
1228
+ defaultValue: defaults.persona || '',
1229
+ required: false,
1230
+ },
1231
+ {
1232
+ key: 'token',
1233
+ message: isVi ? 'Bot Token (từ @BotFather):' : 'Bot Token (from @BotFather):',
1234
+ defaultValue: defaults.token || '',
1235
+ required: true,
1236
+ },
1237
+ ];
1238
+
1239
+ const draft = { ...defaults };
1240
+ let fieldIndex = 0;
1241
+ while (fieldIndex < fields.length) {
1242
+ const field = fields[fieldIndex];
1243
+ const value = await inputWithBack({
1244
+ message: field.message,
1245
+ defaultValue: draft[field.key] || field.defaultValue,
1246
+ required: field.required,
1247
+ allowBack: true,
1248
+ isVi,
1249
+ });
1250
+ if (value === CLI_BACK) {
1251
+ if (fieldIndex > 0) {
1252
+ fieldIndex--;
1253
+ continue;
1254
+ }
1255
+ return { back: true };
1256
+ }
1257
+ draft[field.key] = value;
1258
+ fieldIndex++;
1259
+ }
1260
+ bots.push(draft);
1261
+ }
1262
+ } else if (channelKey !== 'zalo-personal') {
1263
+ const defaults = existingBots[0] || {};
1264
+ const fields = [
1265
+ {
1266
+ key: 'name',
1267
+ message: isVi ? 'Tên Bot:' : 'Bot Name:',
1268
+ defaultValue: defaults.name || 'Chat Bot',
1269
+ required: true,
1270
+ },
1271
+ {
1272
+ key: 'desc',
1273
+ message: isVi ? 'Mô tả Bot:' : 'Bot Description:',
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 (VD: gọn gàng, thân thiện):' : 'Persona & rules (e.g. concise, friendly):',
1280
+ defaultValue: defaults.persona || '',
1281
+ required: false,
1282
+ },
1283
+ {
1284
+ key: 'token',
1285
+ message: isVi ? `Nhập ${channel.name} Token:` : `Enter ${channel.name} Token:`,
1286
+ defaultValue: defaults.token || '',
1287
+ required: true,
1288
+ },
1289
+ ];
1290
+ const draft = { ...defaults, slashCmd: '' };
1291
+ let fieldIndex = 0;
1292
+ while (fieldIndex < fields.length) {
1293
+ const field = fields[fieldIndex];
1294
+ const value = await inputWithBack({
1295
+ message: field.message,
1296
+ defaultValue: draft[field.key] || field.defaultValue,
1297
+ required: field.required,
1298
+ allowBack: true,
1299
+ isVi,
1300
+ });
1301
+ if (value === CLI_BACK) {
1302
+ if (fieldIndex > 0) {
1303
+ fieldIndex--;
1304
+ continue;
1305
+ }
1306
+ return { back: true };
1307
+ }
1308
+ draft[field.key] = value;
1309
+ fieldIndex++;
1310
+ }
1311
+ bots.push(draft);
1312
+ } else {
1313
+ bots.push({ name: 'Bot', slashCmd: '', desc: '', persona: '', token: '' });
1314
+ }
1315
+
1316
+ return {
1317
+ back: false,
1318
+ botCount,
1319
+ groupId,
1320
+ bots,
1321
+ botToken: bots[0]?.token || '',
1322
+ };
1323
+ }
1324
+
1325
+ async function collectBotSetupStepWithGroupBack(options) {
1326
+ const {
1327
+ isVi,
1328
+ channelKey,
1329
+ channel,
1330
+ existingBots = [],
1331
+ existingBotCount = 1,
1332
+ existingGroupId = '',
1333
+ } = options;
1334
+
1335
+ let botCount = channelKey === 'telegram' ? existingBotCount : 1;
1336
+ let groupId = existingGroupId;
1337
+ const bots = [];
1338
+
1339
+ if (channelKey === 'telegram') {
1340
+ const botCountValue = await selectWithBack({
1341
+ message: isVi ? 'Bạn muốn cài bao nhiêu Telegram bot?' : 'How many Telegram bots do you want to deploy?',
1342
+ choices: [
1343
+ { name: '1 bot (single)', value: '1' },
1344
+ { name: '2 bots (Department Room)', value: '2' },
1345
+ { name: '3 bots', value: '3' },
1346
+ { name: '4 bots', value: '4' },
1347
+ { name: '5 bots', value: '5' },
1348
+ ],
1349
+ defaultValue: String(existingBotCount || 1),
1350
+ allowBack: true,
1351
+ isVi,
1352
+ });
1353
+ if (botCountValue === CLI_BACK) {
1354
+ return { back: true };
1355
+ }
1356
+ botCount = parseInt(botCountValue, 10);
1357
+
1358
+ if (botCount > 1) {
1359
+ while (true) {
1360
+ const groupOption = await selectWithBack({
1361
+ message: isVi ? 'Bạn có sẵn Telegram Group chưa?' : 'Do you already have a Telegram Group?',
1362
+ choices: [
1363
+ { 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' },
1364
+ { name: isVi ? '🔗 Đã có group — nhập Group ID ngay' : '🔗 Already have a group — enter Group ID now', value: 'existing' }
1365
+ ],
1366
+ defaultValue: groupId ? 'existing' : 'create',
1367
+ allowBack: true,
1368
+ isVi,
1369
+ });
1370
+ if (groupOption === CLI_BACK) {
1371
+ return { back: true };
1372
+ }
1373
+
1374
+ if (groupOption === 'existing') {
1375
+ console.log(chalk.dim(isVi
1376
+ ? '\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'
1377
+ : '\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'));
1378
+ const nextGroupId = await inputWithBack({
1379
+ message: isVi ? 'Telegram Group ID (VD: -1001234567890):' : 'Telegram Group ID (e.g. -1001234567890):',
1380
+ defaultValue: groupId,
1381
+ allowBack: true,
1382
+ isVi,
1383
+ });
1384
+ if (nextGroupId === CLI_BACK) {
1385
+ continue;
1386
+ }
1387
+ groupId = nextGroupId;
1388
+ break;
1389
+ }
1390
+
1391
+ groupId = '';
1392
+ break;
1393
+ }
1394
+ } else {
1395
+ groupId = '';
1396
+ }
1397
+
1398
+ for (let i = 0; i < botCount; i++) {
1399
+ console.log(chalk.bold(`\n${isVi ? `─── Bot ${i + 1} / ${botCount} ───` : `─── Bot ${i + 1} / ${botCount} ───`}`));
1400
+ const defaults = existingBots[i] || {};
1401
+ const fields = [
1402
+ {
1403
+ key: 'name',
1404
+ message: isVi ? `Tên Bot ${i + 1}:` : `Bot ${i + 1} name:`,
1405
+ defaultValue: defaults.name || `Bot ${i + 1}`,
1406
+ required: true,
1407
+ },
1408
+ {
1409
+ key: 'slashCmd',
1410
+ message: isVi ? `Slash command (VD: /bot${i + 1}):` : `Slash command (e.g. /bot${i + 1}):`,
1411
+ defaultValue: defaults.slashCmd || `/bot${i + 1}`,
1412
+ required: true,
1413
+ },
1414
+ {
1415
+ key: 'desc',
1416
+ message: isVi ? `Mô tả Bot ${i + 1} (VD: Trợ lý AI cá nhân):` : `Bot ${i + 1} description (e.g. Personal AI assistant):`,
1417
+ defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1418
+ required: true,
1419
+ },
1420
+ {
1421
+ key: 'persona',
1422
+ 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):`,
1423
+ defaultValue: defaults.persona || '',
1424
+ required: false,
1425
+ },
1426
+ {
1427
+ key: 'token',
1428
+ message: isVi ? 'Bot Token (từ @BotFather):' : 'Bot Token (from @BotFather):',
1429
+ defaultValue: defaults.token || '',
1430
+ required: true,
1431
+ },
1432
+ ];
1433
+
1434
+ const draft = { ...defaults };
1435
+ let fieldIndex = 0;
1436
+ while (fieldIndex < fields.length) {
1437
+ const field = fields[fieldIndex];
1438
+ const value = await inputWithBack({
1439
+ message: field.message,
1440
+ defaultValue: draft[field.key] || field.defaultValue,
1441
+ required: field.required,
1442
+ allowBack: true,
1443
+ isVi,
1444
+ });
1445
+ if (value === CLI_BACK) {
1446
+ if (fieldIndex > 0) {
1447
+ fieldIndex--;
1448
+ continue;
1449
+ }
1450
+ return { back: true };
1451
+ }
1452
+ draft[field.key] = value;
1453
+ fieldIndex++;
1454
+ }
1455
+ bots.push(draft);
1456
+ }
1457
+ } else if (channelKey !== 'zalo-personal') {
1458
+ const defaults = existingBots[0] || {};
1459
+ const fields = [
1460
+ {
1461
+ key: 'name',
1462
+ message: isVi ? 'Tên Bot:' : 'Bot Name:',
1463
+ defaultValue: defaults.name || 'Chat Bot',
1464
+ required: true,
1465
+ },
1466
+ {
1467
+ key: 'desc',
1468
+ message: isVi ? 'Mô tả Bot:' : 'Bot Description:',
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 (VD: gọn gàng, thân thiện):' : 'Persona & rules (e.g. concise, friendly):',
1475
+ defaultValue: defaults.persona || '',
1476
+ required: false,
1477
+ },
1478
+ {
1479
+ key: 'token',
1480
+ message: isVi ? `Nhập ${channel.name} Token:` : `Enter ${channel.name} Token:`,
1481
+ defaultValue: defaults.token || '',
1482
+ required: true,
1483
+ },
1484
+ ];
1485
+ const draft = { ...defaults, slashCmd: '' };
1486
+ let fieldIndex = 0;
1487
+ while (fieldIndex < fields.length) {
1488
+ const field = fields[fieldIndex];
1489
+ const value = await inputWithBack({
1490
+ message: field.message,
1491
+ defaultValue: draft[field.key] || field.defaultValue,
1492
+ required: field.required,
1493
+ allowBack: true,
1494
+ isVi,
1495
+ });
1496
+ if (value === CLI_BACK) {
1497
+ if (fieldIndex > 0) {
1498
+ fieldIndex--;
1499
+ continue;
1500
+ }
1501
+ return { back: true };
1502
+ }
1503
+ draft[field.key] = value;
1504
+ fieldIndex++;
1505
+ }
1506
+ bots.push(draft);
1507
+ } else {
1508
+ bots.push({ name: 'Bot', slashCmd: '', desc: '', persona: '', token: '' });
1509
+ }
1510
+
1511
+ return {
1512
+ back: false,
1513
+ botCount,
1514
+ groupId,
1515
+ bots,
1516
+ botToken: bots[0]?.token || '',
1517
+ };
1518
+ }
1519
+
1520
+ async function collectProviderStep({
1521
+ isVi,
1522
+ existingProviderKey = '',
1523
+ existingProviderKeyVal = '',
1524
+ existingOllamaModel = 'gemma4:e2b',
1525
+ }) {
1526
+ const providerKey = await selectWithBack({
1527
+ message: isVi ? 'Chọn AI Provider:' : 'Select AI Provider:',
1528
+ choices: Object.entries(PROVIDERS).map(([k, v]) => ({ name: `${v.icon} ${v.name}`, value: k })),
1529
+ defaultValue: existingProviderKey || undefined,
1530
+ allowBack: true,
1531
+ isVi,
1532
+ });
1533
+ if (providerKey === CLI_BACK) {
1534
+ return { back: true };
1535
+ }
1536
+ const provider = PROVIDERS[providerKey];
1537
+
1538
+ let providerKeyVal = existingProviderKey === providerKey ? existingProviderKeyVal : '';
1539
+ if (!provider.isProxy && !provider.isLocal) {
1540
+ const keyValue = await inputWithBack({
1541
+ message: isVi ? `Nhập ${provider.envKey}:` : `Enter ${provider.envKey}:`,
1542
+ defaultValue: providerKeyVal,
1543
+ required: true,
1544
+ allowBack: true,
1545
+ isVi,
1546
+ });
1547
+ if (keyValue === CLI_BACK) {
1548
+ return { back: true };
1549
+ }
1550
+ providerKeyVal = keyValue;
1551
+ }
1552
+
1553
+ let selectedOllamaModel = existingProviderKey === 'ollama' ? existingOllamaModel : 'gemma4:e2b';
1554
+ if (providerKey === 'ollama') {
1555
+ console.log(chalk.yellow(isVi
1556
+ ? '\n💡 Gemma 4 (02/04/2026) — chọn kích thước phù hợp với RAM máy bạn:'
1557
+ : '\n💡 Gemma 4 (April 2, 2026) — pick a size that fits your RAM:'));
1558
+ const modelValue = await selectWithBack({
1559
+ message: isVi ? 'Chọn model Ollama:' : 'Select Ollama model:',
1560
+ choices: [
1561
+ {
1562
+ name: isVi
1563
+ ? '🟢 gemma4:e2b — Nhẹ nhất (~4-6 GB RAM) — Laptop / test nhanh ★ Khuyên dùng'
1564
+ : '🟢 gemma4:e2b — Lightest (~4-6 GB RAM) — Laptop / fastest test ★ Recommended',
1565
+ value: 'gemma4:e2b'
1566
+ },
1567
+ {
1568
+ name: isVi
1569
+ ? '🟡 gemma4:e4b — Cân bằng (~8-10 GB RAM) — Dùng hằng ngày'
1570
+ : '🟡 gemma4:e4b — Balanced (~8-10 GB RAM) — Daily use',
1571
+ value: 'gemma4:e4b'
1572
+ },
1573
+ {
1574
+ name: isVi
1575
+ ? '🟠 gemma4:26b — Mạnh (~18-24 GB RAM/VRAM) — Máy mạnh'
1576
+ : '🟠 gemma4:26b — Powerful (~18-24 GB RAM/VRAM) — High-end machine',
1577
+ value: 'gemma4:26b'
1578
+ },
1579
+ {
1580
+ name: isVi
1581
+ ? '🔴 gemma4:31b — Mạnh nhất (~24+ GB RAM/VRAM) — GPU workstation'
1582
+ : '🔴 gemma4:31b — Most powerful (~24+ GB RAM/VRAM) — GPU workstation',
1583
+ value: 'gemma4:31b'
1584
+ },
1585
+ ],
1586
+ defaultValue: selectedOllamaModel,
1587
+ allowBack: true,
1588
+ isVi,
1589
+ });
1590
+ if (modelValue === CLI_BACK) {
1591
+ return { back: true };
1592
+ }
1593
+ selectedOllamaModel = modelValue;
1594
+ }
1595
+
1596
+ return {
1597
+ back: false,
1598
+ providerKey,
1599
+ provider,
1600
+ providerKeyVal,
1601
+ selectedOllamaModel,
1602
+ };
1603
+ }
1604
+
1605
+ async function collectSkillsStep({
1606
+ isVi,
1607
+ providerKey,
1608
+ existingSelectedSkills = [],
1609
+ existingBrowserMode = 'server',
1610
+ existingTtsOpenaiKey = '',
1611
+ existingTtsElevenKey = '',
1612
+ existingSmtpHost = 'smtp.gmail.com',
1613
+ existingSmtpPort = '587',
1614
+ existingSmtpUser = '',
1615
+ existingSmtpPass = '',
1616
+ }) {
1617
+ const selectedSkills = await checkboxWithBack({
1618
+ message: isVi ? 'Bật tính năng bổ sung (Space để chọn):' : 'Enable extra skills (Space to select):',
1619
+ choices: getCliSkillChoices({ providerKey, isVi }).map((choice) => ({
1620
+ ...choice,
1621
+ checked: existingSelectedSkills.includes(choice.value),
1622
+ })),
1623
+ allowBack: true,
1624
+ isVi,
1625
+ });
1626
+ if (selectedSkills === CLI_BACK) {
1627
+ return { back: true };
1628
+ }
1629
+
1630
+ let browserMode = existingBrowserMode;
1631
+ if (selectedSkills.includes('browser')) {
1632
+ const isLinux = process.platform === 'linux';
1633
+ const browserValue = await selectWithBack({
1634
+ message: isVi ? 'Chế độ Browser Automation:' : 'Browser Automation mode:',
1635
+ choices: [
1636
+ {
1637
+ name: isVi
1638
+ ? '🖥️ Dùng Chrome trên máy tính (Windows/Mac — Bypass Cloudflare tốt hơn)'
1639
+ : '🖥️ Use Host Chrome (Windows/Mac — Better Cloudflare bypass)',
1640
+ value: 'desktop'
1641
+ },
1642
+ {
1643
+ name: isVi
1644
+ ? '🐳 Headless Chromium trong Docker (Ubuntu Server / VPS — không cần GUI)'
1645
+ : '🐳 Headless Chromium inside Docker (Ubuntu Server / VPS — No GUI)',
1646
+ value: 'server'
1647
+ }
1648
+ ],
1649
+ defaultValue: browserMode || (isLinux ? 'server' : 'desktop'),
1650
+ allowBack: true,
1651
+ isVi,
1652
+ });
1653
+ if (browserValue === CLI_BACK) {
1654
+ return { back: true };
1655
+ }
1656
+ browserMode = browserValue;
1657
+ } else {
1658
+ browserMode = 'server';
1659
+ }
1660
+
1661
+ let ttsOpenaiKey = existingTtsOpenaiKey;
1662
+ let ttsElevenKey = existingTtsElevenKey;
1663
+ if (selectedSkills.includes('tts')) {
1664
+ const openaiKey = await inputWithBack({
1665
+ 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):',
1666
+ defaultValue: ttsOpenaiKey,
1667
+ allowBack: true,
1668
+ isVi,
1669
+ });
1670
+ if (openaiKey === CLI_BACK) {
1671
+ return { back: true };
1672
+ }
1673
+ ttsOpenaiKey = openaiKey;
1674
+
1675
+ const elevenKey = await inputWithBack({
1676
+ message: isVi ? 'Nhập ELEVENLABS_API_KEY (hoặc bỏ trống):' : 'Enter ELEVENLABS_API_KEY (or leave empty):',
1677
+ defaultValue: ttsElevenKey,
1678
+ allowBack: true,
1679
+ isVi,
1680
+ });
1681
+ if (elevenKey === CLI_BACK) {
1682
+ return { back: true };
1683
+ }
1684
+ ttsElevenKey = elevenKey;
1685
+ } else {
1686
+ ttsOpenaiKey = '';
1687
+ ttsElevenKey = '';
1688
+ }
1689
+
1690
+ let smtpHost = existingSmtpHost;
1691
+ let smtpPort = existingSmtpPort;
1692
+ let smtpUser = existingSmtpUser;
1693
+ let smtpPass = existingSmtpPass;
1694
+ if (selectedSkills.includes('email')) {
1695
+ const smtpHostValue = await inputWithBack({
1696
+ message: isVi ? 'SMTP Host (VD: smtp.gmail.com):' : 'SMTP Host (e.g. smtp.gmail.com):',
1697
+ defaultValue: smtpHost,
1698
+ required: true,
1699
+ allowBack: true,
1700
+ isVi,
1701
+ });
1702
+ if (smtpHostValue === CLI_BACK) {
1703
+ return { back: true };
1704
+ }
1705
+ smtpHost = smtpHostValue;
1706
+
1707
+ const smtpPortValue = await inputWithBack({
1708
+ message: 'SMTP Port:',
1709
+ defaultValue: smtpPort,
1710
+ required: true,
1711
+ allowBack: true,
1712
+ isVi,
1713
+ });
1714
+ if (smtpPortValue === CLI_BACK) {
1715
+ return { back: true };
1716
+ }
1717
+ smtpPort = smtpPortValue;
1718
+
1719
+ const smtpUserValue = await inputWithBack({
1720
+ message: isVi ? 'SMTP Email:' : 'SMTP Email:',
1721
+ defaultValue: smtpUser,
1722
+ required: true,
1723
+ allowBack: true,
1724
+ isVi,
1725
+ });
1726
+ if (smtpUserValue === CLI_BACK) {
1727
+ return { back: true };
1728
+ }
1729
+ smtpUser = smtpUserValue;
1730
+
1731
+ const smtpPassValue = await inputWithBack({
1732
+ message: isVi ? 'SMTP App Password:' : 'SMTP App Password:',
1733
+ defaultValue: smtpPass,
1734
+ required: true,
1735
+ allowBack: true,
1736
+ isVi,
1737
+ });
1738
+ if (smtpPassValue === CLI_BACK) {
1739
+ return { back: true };
1740
+ }
1741
+ smtpPass = smtpPassValue;
1742
+ } else {
1743
+ smtpHost = 'smtp.gmail.com';
1744
+ smtpPort = '587';
1745
+ smtpUser = '';
1746
+ smtpPass = '';
1747
+ }
1748
+
1749
+ return {
1750
+ back: false,
1751
+ selectedSkills,
1752
+ browserMode,
1753
+ ttsOpenaiKey,
1754
+ ttsElevenKey,
1755
+ smtpHost,
1756
+ smtpPort,
1757
+ smtpUser,
1758
+ smtpPass,
1759
+ };
1760
+ }
1761
1761
 
1762
1762
 
1763
1763
  // ─── Shared workspace file writer ─────────────────────────────────────────────
@@ -1833,228 +1833,228 @@ async function writeWorkspaceFiles({
1833
1833
  }
1834
1834
 
1835
1835
 
1836
- async function main() {
1837
- const cliSubcommand = getCliSubcommand();
1838
- if (cliSubcommand === 'upgrade') {
1839
- await runUpgradeCommand();
1840
- return;
1841
- }
1842
-
1843
- console.log(chalk.red('\n=================================='));
1844
- console.log(chalk.redBright(LOGO));
1845
- console.log(chalk.greenBright(' OpenClaw Auto Setup CLI '));
1846
- console.log(chalk.red('==================================\n'));
1847
-
1848
- let lang = 'vi';
1849
- let isVi = true;
1850
- const detectedPlatform = process.platform;
1851
- const detectedOS = detectedPlatform === 'win32' ? 'windows'
1852
- : detectedPlatform === 'darwin' ? 'macos'
1853
- : 'linux';
1854
- let osChoice = detectedOS === 'linux' ? 'vps' : detectedOS;
1855
- let deployMode = 'docker';
1856
- let channelKey = 'telegram';
1857
- let channel = CHANNELS[channelKey];
1858
- let botToken = '';
1859
- let botCount = 1;
1860
- let bots = [];
1861
- let groupId = '';
1862
- let userInfo = '';
1863
- let providerKey = '9router';
1864
- let provider = PROVIDERS[providerKey];
1865
- let providerKeyVal = '';
1866
- let selectedOllamaModel = 'gemma4:e2b';
1867
- let selectedSkills = [];
1868
- let tavilyKey = '';
1869
- let browserMode = 'server';
1870
- let ttsOpenaiKey = '';
1871
- let ttsElevenKey = '';
1872
- let smtpHost = 'smtp.gmail.com', smtpPort = '587', smtpUser = '', smtpPass = '';
1873
- let defaultDir = process.cwd();
1874
- if (!defaultDir.endsWith('openclaw-setup') && !defaultDir.endsWith('openclaw')) {
1875
- defaultDir = path.join(defaultDir, 'openclaw-setup');
1876
- }
1877
- let projectDir = defaultDir;
1878
-
1879
- let setupStep = 'language';
1880
- while (true) {
1881
- if (setupStep === 'language') {
1882
- lang = await select({
1883
- message: 'Select language / Chọn ngôn ngữ:',
1884
- choices: [
1885
- { name: 'Tiếng Việt', value: 'vi' },
1886
- { name: 'English', value: 'en' }
1887
- ],
1888
- default: lang
1889
- });
1890
- isVi = lang === 'vi';
1891
- setupStep = 'os';
1892
- continue;
1893
- }
1894
-
1895
- if (setupStep === 'os') {
1896
- const nextOsChoice = await selectWithBack({
1897
- message: isVi ? 'Bạn đang chạy trên hệ điều hành nào?' : 'What OS are you running on?',
1898
- choices: [
1899
- { name: isVi ? '🪟 Windows' : '🪟 Windows', value: 'windows' },
1900
- { name: isVi ? '🍎 macOS' : '🍎 macOS', value: 'macos' },
1901
- { name: isVi ? '🐧 Ubuntu Desktop' : '🐧 Ubuntu Desktop', value: 'ubuntu' },
1902
- { name: isVi ? '🖥️ VPS / Ubuntu Server' : '🖥️ VPS / Ubuntu Server', value: 'vps' },
1903
- ],
1904
- defaultValue: osChoice,
1905
- allowBack: true,
1906
- isVi,
1907
- });
1908
- if (nextOsChoice === CLI_BACK) {
1909
- setupStep = 'language';
1910
- continue;
1911
- }
1912
- osChoice = nextOsChoice;
1913
- setupStep = 'deploy';
1914
- continue;
1915
- }
1916
-
1917
- if (setupStep === 'deploy') {
1918
- const deployModeDefault = (osChoice === 'ubuntu' || osChoice === 'vps') ? 'native' : 'docker';
1919
- const nextDeployMode = await selectWithBack({
1920
- message: isVi ? 'Chọn cách chạy bot:' : 'How do you want to run the bot?',
1921
- choices: [
1922
- { 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' },
1923
- { 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' }
1924
- ],
1925
- defaultValue: deployMode || deployModeDefault,
1926
- allowBack: true,
1927
- isVi,
1928
- });
1929
- if (nextDeployMode === CLI_BACK) {
1930
- setupStep = 'os';
1931
- continue;
1932
- }
1933
- deployMode = nextDeployMode;
1934
- if (deployMode === 'docker' && !isDockerInstalled()) {
1935
- 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...'));
1936
- try {
1937
- const platform = process.platform;
1938
- if (platform === 'win32') {
1939
- execSync('winget install -e --id Docker.DockerDesktop --accept-source-agreements --accept-package-agreements', { stdio: 'inherit' });
1940
- 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.'));
1941
- process.exit(0);
1942
- } else if (platform === 'darwin') {
1943
- execSync('brew install --cask docker', { stdio: 'inherit' });
1944
- 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.'));
1945
- process.exit(0);
1946
- } else {
1947
- execSync('curl -fsSL https://get.docker.com | sh', { stdio: 'inherit', shell: true });
1948
- try { execSync('apt-get install -y docker-compose-plugin', { stdio: 'ignore', shell: true }); } catch {}
1949
- console.log(chalk.green(isVi ? '✅ Docker Engine + Compose plugin đã cài xong.' : '✅ Docker Engine + Compose plugin installed.'));
1950
- }
1951
- } catch {
1952
- 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/'));
1953
- process.exit(1);
1954
- }
1955
- }
1956
- setupStep = 'channel';
1957
- continue;
1958
- }
1959
-
1960
- if (setupStep === 'channel') {
1961
- const nextChannelKey = await selectWithBack({
1962
- message: isVi ? 'Chọn nền tảng bot:' : 'Select bot platform:',
1963
- choices: Object.entries(CHANNELS).map(([k, v]) => ({ name: v.icon + ' ' + v.name, value: k })),
1964
- defaultValue: channelKey,
1965
- allowBack: true,
1966
- isVi,
1967
- });
1968
- if (nextChannelKey === CLI_BACK) {
1969
- setupStep = 'deploy';
1970
- continue;
1971
- }
1972
- channelKey = nextChannelKey;
1973
- channel = CHANNELS[channelKey];
1974
- if (channelKey === 'zalo-bot') {
1975
- 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.')));
1976
- }
1977
- setupStep = 'botSetup';
1978
- continue;
1979
- }
1980
-
1981
- if (setupStep === 'botSetup') {
1982
- const botSetup = await collectBotSetupStepWithGroupBack({ isVi, channelKey, channel, existingBots: bots, existingBotCount: botCount, existingGroupId: groupId });
1983
- if (botSetup.back) {
1984
- setupStep = 'channel';
1985
- continue;
1986
- }
1987
- botCount = botSetup.botCount;
1988
- groupId = botSetup.groupId;
1989
- bots = botSetup.bots;
1990
- botToken = botSetup.botToken;
1991
- setupStep = 'userInfo';
1992
- continue;
1993
- }
1994
-
1995
- if (setupStep === 'userInfo') {
1996
- console.log(chalk.bold('\n' + (isVi ? '👤 Thông tin của bạn 👤' : '👤 About You 👤')));
1997
- 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 });
1998
- if (nextUserInfo === CLI_BACK) {
1999
- setupStep = 'botSetup';
2000
- continue;
2001
- }
2002
- userInfo = nextUserInfo;
2003
- setupStep = 'provider';
2004
- continue;
2005
- }
2006
-
2007
- if (setupStep === 'provider') {
2008
- const providerSetup = await collectProviderStep({ isVi, existingProviderKey: providerKey, existingProviderKeyVal: providerKeyVal, existingOllamaModel: selectedOllamaModel });
2009
- if (providerSetup.back) {
2010
- setupStep = 'userInfo';
2011
- continue;
2012
- }
2013
- providerKey = providerSetup.providerKey;
2014
- provider = providerSetup.provider;
2015
- providerKeyVal = providerSetup.providerKeyVal;
2016
- selectedOllamaModel = providerSetup.selectedOllamaModel;
2017
- setupStep = 'skills';
2018
- continue;
2019
- }
2020
-
2021
- if (setupStep === 'skills') {
2022
- const skillSetup = await collectSkillsStep({ isVi, providerKey, existingSelectedSkills: selectedSkills, existingBrowserMode: browserMode, existingTtsOpenaiKey: ttsOpenaiKey, existingTtsElevenKey: ttsElevenKey, existingSmtpHost: smtpHost, existingSmtpPort: smtpPort, existingSmtpUser: smtpUser, existingSmtpPass: smtpPass });
2023
- if (skillSetup.back) {
2024
- setupStep = 'provider';
2025
- continue;
2026
- }
2027
- selectedSkills = skillSetup.selectedSkills;
2028
- browserMode = skillSetup.browserMode;
2029
- ttsOpenaiKey = skillSetup.ttsOpenaiKey;
2030
- ttsElevenKey = skillSetup.ttsElevenKey;
2031
- smtpHost = skillSetup.smtpHost;
2032
- smtpPort = skillSetup.smtpPort;
2033
- smtpUser = skillSetup.smtpUser;
2034
- smtpPass = skillSetup.smtpPass;
2035
- setupStep = 'projectDir';
2036
- continue;
2037
- }
2038
-
2039
- if (setupStep === 'projectDir') {
2040
- const nextProjectDir = await inputWithBack({ message: isVi ? 'Thư mục cài đặt project:' : 'Project install directory:', defaultValue: projectDir, allowBack: true, isVi });
2041
- if (nextProjectDir === CLI_BACK) {
2042
- setupStep = 'skills';
2043
- continue;
2044
- }
2045
- projectDir = nextProjectDir;
2046
- break;
2047
- }
2048
- }
2049
-
2050
- const isMultiBot = botCount > 1 && channelKey === 'telegram';
2051
- const botName = bots[0].name;
2052
- const botDesc = bots[0].desc;
2053
- const botPersona = bots[0].persona;
2054
- const agentId = String(botName || 'chat').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'chat';
2055
- const modelsPrimary = providerKey === 'ollama' ? selectedOllamaModel : providerKey === '9router' ? 'smart-route' : provider.models?.[0]?.id || 'gpt-4o-mini';
2056
- const hasBrowserDesktop = selectedSkills.includes('browser') && browserMode === 'desktop';
2057
- const hasBrowserServer = selectedSkills.includes('browser') && browserMode === 'server';
1836
+ async function main() {
1837
+ const cliSubcommand = getCliSubcommand();
1838
+ if (cliSubcommand === 'upgrade') {
1839
+ await runUpgradeCommand();
1840
+ return;
1841
+ }
1842
+
1843
+ console.log(chalk.red('\n=================================='));
1844
+ console.log(chalk.redBright(LOGO));
1845
+ console.log(chalk.greenBright(' OpenClaw Auto Setup CLI '));
1846
+ console.log(chalk.red('==================================\n'));
1847
+
1848
+ let lang = 'vi';
1849
+ let isVi = true;
1850
+ const detectedPlatform = process.platform;
1851
+ const detectedOS = detectedPlatform === 'win32' ? 'windows'
1852
+ : detectedPlatform === 'darwin' ? 'macos'
1853
+ : 'linux';
1854
+ let osChoice = detectedOS === 'linux' ? 'vps' : detectedOS;
1855
+ let deployMode = 'docker';
1856
+ let channelKey = 'telegram';
1857
+ let channel = CHANNELS[channelKey];
1858
+ let botToken = '';
1859
+ let botCount = 1;
1860
+ let bots = [];
1861
+ let groupId = '';
1862
+ let userInfo = '';
1863
+ let providerKey = '9router';
1864
+ let provider = PROVIDERS[providerKey];
1865
+ let providerKeyVal = '';
1866
+ let selectedOllamaModel = 'gemma4:e2b';
1867
+ let selectedSkills = [];
1868
+ let tavilyKey = '';
1869
+ let browserMode = 'server';
1870
+ let ttsOpenaiKey = '';
1871
+ let ttsElevenKey = '';
1872
+ let smtpHost = 'smtp.gmail.com', smtpPort = '587', smtpUser = '', smtpPass = '';
1873
+ let defaultDir = process.cwd();
1874
+ if (!defaultDir.endsWith('openclaw-setup') && !defaultDir.endsWith('openclaw')) {
1875
+ defaultDir = path.join(defaultDir, 'openclaw-setup');
1876
+ }
1877
+ let projectDir = defaultDir;
1878
+
1879
+ let setupStep = 'language';
1880
+ while (true) {
1881
+ if (setupStep === 'language') {
1882
+ lang = await select({
1883
+ message: 'Select language / Chọn ngôn ngữ:',
1884
+ choices: [
1885
+ { name: 'Tiếng Việt', value: 'vi' },
1886
+ { name: 'English', value: 'en' }
1887
+ ],
1888
+ default: lang
1889
+ });
1890
+ isVi = lang === 'vi';
1891
+ setupStep = 'os';
1892
+ continue;
1893
+ }
1894
+
1895
+ if (setupStep === 'os') {
1896
+ const nextOsChoice = await selectWithBack({
1897
+ message: isVi ? 'Bạn đang chạy trên hệ điều hành nào?' : 'What OS are you running on?',
1898
+ choices: [
1899
+ { name: isVi ? '🪟 Windows' : '🪟 Windows', value: 'windows' },
1900
+ { name: isVi ? '🍎 macOS' : '🍎 macOS', value: 'macos' },
1901
+ { name: isVi ? '🐧 Ubuntu Desktop' : '🐧 Ubuntu Desktop', value: 'ubuntu' },
1902
+ { name: isVi ? '🖥️ VPS / Ubuntu Server' : '🖥️ VPS / Ubuntu Server', value: 'vps' },
1903
+ ],
1904
+ defaultValue: osChoice,
1905
+ allowBack: true,
1906
+ isVi,
1907
+ });
1908
+ if (nextOsChoice === CLI_BACK) {
1909
+ setupStep = 'language';
1910
+ continue;
1911
+ }
1912
+ osChoice = nextOsChoice;
1913
+ setupStep = 'deploy';
1914
+ continue;
1915
+ }
1916
+
1917
+ if (setupStep === 'deploy') {
1918
+ const deployModeDefault = (osChoice === 'ubuntu' || osChoice === 'vps') ? 'native' : 'docker';
1919
+ const nextDeployMode = await selectWithBack({
1920
+ message: isVi ? 'Chọn cách chạy bot:' : 'How do you want to run the bot?',
1921
+ choices: [
1922
+ { 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' },
1923
+ { 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' }
1924
+ ],
1925
+ defaultValue: deployMode || deployModeDefault,
1926
+ allowBack: true,
1927
+ isVi,
1928
+ });
1929
+ if (nextDeployMode === CLI_BACK) {
1930
+ setupStep = 'os';
1931
+ continue;
1932
+ }
1933
+ deployMode = nextDeployMode;
1934
+ if (deployMode === 'docker' && !isDockerInstalled()) {
1935
+ 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...'));
1936
+ try {
1937
+ const platform = process.platform;
1938
+ if (platform === 'win32') {
1939
+ execSync('winget install -e --id Docker.DockerDesktop --accept-source-agreements --accept-package-agreements', { stdio: 'inherit' });
1940
+ 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.'));
1941
+ process.exit(0);
1942
+ } else if (platform === 'darwin') {
1943
+ execSync('brew install --cask docker', { stdio: 'inherit' });
1944
+ 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.'));
1945
+ process.exit(0);
1946
+ } else {
1947
+ execSync('curl -fsSL https://get.docker.com | sh', { stdio: 'inherit', shell: true });
1948
+ try { execSync('apt-get install -y docker-compose-plugin', { stdio: 'ignore', shell: true }); } catch {}
1949
+ console.log(chalk.green(isVi ? '✅ Docker Engine + Compose plugin đã cài xong.' : '✅ Docker Engine + Compose plugin installed.'));
1950
+ }
1951
+ } catch {
1952
+ 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/'));
1953
+ process.exit(1);
1954
+ }
1955
+ }
1956
+ setupStep = 'channel';
1957
+ continue;
1958
+ }
1959
+
1960
+ if (setupStep === 'channel') {
1961
+ const nextChannelKey = await selectWithBack({
1962
+ message: isVi ? 'Chọn nền tảng bot:' : 'Select bot platform:',
1963
+ choices: Object.entries(CHANNELS).map(([k, v]) => ({ name: v.icon + ' ' + v.name, value: k })),
1964
+ defaultValue: channelKey,
1965
+ allowBack: true,
1966
+ isVi,
1967
+ });
1968
+ if (nextChannelKey === CLI_BACK) {
1969
+ setupStep = 'deploy';
1970
+ continue;
1971
+ }
1972
+ channelKey = nextChannelKey;
1973
+ channel = CHANNELS[channelKey];
1974
+ if (channelKey === 'zalo-bot') {
1975
+ 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.')));
1976
+ }
1977
+ setupStep = 'botSetup';
1978
+ continue;
1979
+ }
1980
+
1981
+ if (setupStep === 'botSetup') {
1982
+ const botSetup = await collectBotSetupStepWithGroupBack({ isVi, channelKey, channel, existingBots: bots, existingBotCount: botCount, existingGroupId: groupId });
1983
+ if (botSetup.back) {
1984
+ setupStep = 'channel';
1985
+ continue;
1986
+ }
1987
+ botCount = botSetup.botCount;
1988
+ groupId = botSetup.groupId;
1989
+ bots = botSetup.bots;
1990
+ botToken = botSetup.botToken;
1991
+ setupStep = 'userInfo';
1992
+ continue;
1993
+ }
1994
+
1995
+ if (setupStep === 'userInfo') {
1996
+ console.log(chalk.bold('\n' + (isVi ? '👤 Thông tin của bạn 👤' : '👤 About You 👤')));
1997
+ 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 });
1998
+ if (nextUserInfo === CLI_BACK) {
1999
+ setupStep = 'botSetup';
2000
+ continue;
2001
+ }
2002
+ userInfo = nextUserInfo;
2003
+ setupStep = 'provider';
2004
+ continue;
2005
+ }
2006
+
2007
+ if (setupStep === 'provider') {
2008
+ const providerSetup = await collectProviderStep({ isVi, existingProviderKey: providerKey, existingProviderKeyVal: providerKeyVal, existingOllamaModel: selectedOllamaModel });
2009
+ if (providerSetup.back) {
2010
+ setupStep = 'userInfo';
2011
+ continue;
2012
+ }
2013
+ providerKey = providerSetup.providerKey;
2014
+ provider = providerSetup.provider;
2015
+ providerKeyVal = providerSetup.providerKeyVal;
2016
+ selectedOllamaModel = providerSetup.selectedOllamaModel;
2017
+ setupStep = 'skills';
2018
+ continue;
2019
+ }
2020
+
2021
+ if (setupStep === 'skills') {
2022
+ const skillSetup = await collectSkillsStep({ isVi, providerKey, existingSelectedSkills: selectedSkills, existingBrowserMode: browserMode, existingTtsOpenaiKey: ttsOpenaiKey, existingTtsElevenKey: ttsElevenKey, existingSmtpHost: smtpHost, existingSmtpPort: smtpPort, existingSmtpUser: smtpUser, existingSmtpPass: smtpPass });
2023
+ if (skillSetup.back) {
2024
+ setupStep = 'provider';
2025
+ continue;
2026
+ }
2027
+ selectedSkills = skillSetup.selectedSkills;
2028
+ browserMode = skillSetup.browserMode;
2029
+ ttsOpenaiKey = skillSetup.ttsOpenaiKey;
2030
+ ttsElevenKey = skillSetup.ttsElevenKey;
2031
+ smtpHost = skillSetup.smtpHost;
2032
+ smtpPort = skillSetup.smtpPort;
2033
+ smtpUser = skillSetup.smtpUser;
2034
+ smtpPass = skillSetup.smtpPass;
2035
+ setupStep = 'projectDir';
2036
+ continue;
2037
+ }
2038
+
2039
+ if (setupStep === 'projectDir') {
2040
+ const nextProjectDir = await inputWithBack({ message: isVi ? 'Thư mục cài đặt project:' : 'Project install directory:', defaultValue: projectDir, allowBack: true, isVi });
2041
+ if (nextProjectDir === CLI_BACK) {
2042
+ setupStep = 'skills';
2043
+ continue;
2044
+ }
2045
+ projectDir = nextProjectDir;
2046
+ break;
2047
+ }
2048
+ }
2049
+
2050
+ const isMultiBot = botCount > 1 && channelKey === 'telegram';
2051
+ const botName = bots[0].name;
2052
+ const botDesc = bots[0].desc;
2053
+ const botPersona = bots[0].persona;
2054
+ const agentId = String(botName || 'chat').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'chat';
2055
+ const modelsPrimary = providerKey === 'ollama' ? selectedOllamaModel : providerKey === '9router' ? 'smart-route' : provider.models?.[0]?.id || 'gpt-4o-mini';
2056
+ const hasBrowserDesktop = selectedSkills.includes('browser') && browserMode === 'desktop';
2057
+ const hasBrowserServer = selectedSkills.includes('browser') && browserMode === 'server';
2058
2058
  console.log(chalk.cyan(`\n🚀 ${isVi ? 'Đang tạo thư mục và file cấu hình...' : 'Generating directories and configurations...'}`));
2059
2059
 
2060
2060
  await fs.ensureDir(projectDir);
@@ -2156,7 +2156,7 @@ async function main() {
2156
2156
  socatBridge,
2157
2157
  deviceApproveLoop,
2158
2158
  ].filter(Boolean),
2159
- volumeMount: '../../.openclaw:/root/project/.openclaw',
2159
+ volumeMount: '../../.openclaw:/root/project/.openclaw',
2160
2160
  singleComposeName: `oc-${agentId}`,
2161
2161
  multiComposeName: 'oc-multibot',
2162
2162
  singleAppContainerName: `openclaw-${agentId}`,
@@ -2215,13 +2215,13 @@ async function main() {
2215
2215
  } else if (providerKey === '9router') {
2216
2216
  authProfilesJson = {
2217
2217
  version: 1,
2218
- profiles: {
2219
- '9router-proxy': {
2220
- provider: '9router',
2221
- type: 'api_key',
2222
- key: NINE_ROUTER_PROXY_API_KEY,
2223
- },
2224
- },
2218
+ profiles: {
2219
+ '9router-proxy': {
2220
+ provider: '9router',
2221
+ type: 'api_key',
2222
+ key: NINE_ROUTER_PROXY_API_KEY,
2223
+ },
2224
+ },
2225
2225
  order: { '9router': ['9router-proxy'] },
2226
2226
  };
2227
2227
  }
@@ -2248,9 +2248,9 @@ async function main() {
2248
2248
  workspaceDir: `workspace-${agentSlug}`,
2249
2249
  };
2250
2250
  });
2251
- const telegramAccounts = Object.fromEntries(agentMetas.map((meta) => [meta.accountId, {
2252
- botToken: meta.token,
2253
- }]));
2251
+ const telegramAccounts = Object.fromEntries(agentMetas.map((meta) => [meta.accountId, {
2252
+ botToken: meta.token,
2253
+ }]));
2254
2254
  const telegramChannelConfig = {
2255
2255
  enabled: true,
2256
2256
  defaultAccount: 'default',
@@ -2293,14 +2293,14 @@ async function main() {
2293
2293
  model: { primary: modelsPrimary, fallbacks: [] },
2294
2294
  })),
2295
2295
  },
2296
- ...(providerKey === '9router' ? {
2297
- models: {
2298
- mode: 'merge',
2299
- providers: {
2300
- '9router': build9RouterProviderConfig(get9RouterBaseUrl(deployMode)),
2301
- },
2302
- },
2303
- } : provider.isLocal ? {
2296
+ ...(providerKey === '9router' ? {
2297
+ models: {
2298
+ mode: 'merge',
2299
+ providers: {
2300
+ '9router': build9RouterProviderConfig(get9RouterBaseUrl(deployMode)),
2301
+ },
2302
+ },
2303
+ } : provider.isLocal ? {
2304
2304
  models: {
2305
2305
  mode: 'merge',
2306
2306
  providers: {
@@ -2355,9 +2355,9 @@ async function main() {
2355
2355
  sharedConfig.skills = { entries: skillEntries };
2356
2356
  }
2357
2357
 
2358
- await fs.writeJson(path.join(rootClawDir, 'openclaw.json'), sharedConfig, { spaces: 2 });
2359
- await fs.writeFile(
2360
- path.join(projectDir, TELEGRAM_SETUP_GUIDE_FILENAME),
2358
+ await fs.writeJson(path.join(rootClawDir, 'openclaw.json'), sharedConfig, { spaces: 2 });
2359
+ await fs.writeFile(
2360
+ path.join(projectDir, TELEGRAM_SETUP_GUIDE_FILENAME),
2361
2361
  buildTelegramPostInstallChecklist({ isVi, bots, groupId }),
2362
2362
  'utf8',
2363
2363
  );
@@ -2379,7 +2379,7 @@ async function main() {
2379
2379
  const safeRootClawDir = rootClawDir.replace(/\\/g, '/');
2380
2380
  const pm2Apps = [
2381
2381
  ' {',
2382
- ` name: 'openclaw-multibot',`,
2382
+ ` name: 'openclaw-multibot',`,
2383
2383
  ` script: 'openclaw',`,
2384
2384
  ` args: 'gateway run',`,
2385
2385
  ` cwd: '${projectDir.replace(/\\/g, '/')}',`,
@@ -2456,6 +2456,7 @@ async function main() {
2456
2456
  const loopBotName = isMultiBot ? (bots[bIndex]?.name || `Bot ${bIndex+1}`) : botName;
2457
2457
  const loopBotDesc = isMultiBot ? (bots[bIndex]?.desc || '') : botDesc;
2458
2458
  const loopBotPersona = isMultiBot ? (bots[bIndex]?.persona || '') : botPersona;
2459
+ const loopBotToken = isMultiBot ? (bots[bIndex]?.token || '') : botToken;
2459
2460
  const teamRoster = bots.slice(0, numBotsToConfigure).map((peer, idx) => ({
2460
2461
  idx,
2461
2462
  name: peer?.name || `Bot ${idx + 1}`,
@@ -2492,14 +2493,14 @@ async function main() {
2492
2493
  model: { primary: modelsPrimary, fallbacks: [] }
2493
2494
  }]
2494
2495
  },
2495
- ...(providerKey === '9router' ? {
2496
- models: {
2497
- mode: 'merge',
2498
- providers: {
2499
- '9router': build9RouterProviderConfig(get9RouterBaseUrl(deployMode))
2500
- }
2501
- }
2502
- } : provider.isLocal ? {
2496
+ ...(providerKey === '9router' ? {
2497
+ models: {
2498
+ mode: 'merge',
2499
+ providers: {
2500
+ '9router': build9RouterProviderConfig(get9RouterBaseUrl(deployMode))
2501
+ }
2502
+ }
2503
+ } : provider.isLocal ? {
2503
2504
  models: {
2504
2505
  mode: 'merge',
2505
2506
  providers: {
@@ -2544,56 +2545,56 @@ async function main() {
2544
2545
  botConfig.skills = { entries: skillEntries };
2545
2546
  }
2546
2547
 
2547
- if (channelKey === 'telegram') {
2548
- const telegramConfig = {
2549
- enabled: true,
2550
- dmPolicy: 'open',
2551
- allowFrom: ['*'],
2552
- defaultAccount: 'default',
2553
- replyToMode: 'first',
2554
- reactionLevel: 'minimal',
2555
- actions: {
2556
- sendMessage: true,
2557
- reactions: true,
2558
- },
2559
- accounts: {
2560
- default: {
2561
- botToken: loopBotToken || '<your_bot_token>',
2562
- },
2563
- },
2564
- };
2565
- if (isMultiBot) {
2566
- telegramConfig.groupPolicy = groupId ? 'allowlist' : 'open';
2567
- telegramConfig.groupAllowFrom = ['*'];
2568
- telegramConfig.groups = {
2569
- [groupId || '*']: { enabled: true, requireMention: false }
2548
+ if (channelKey === 'telegram') {
2549
+ const telegramConfig = {
2550
+ enabled: true,
2551
+ dmPolicy: 'open',
2552
+ allowFrom: ['*'],
2553
+ defaultAccount: 'default',
2554
+ replyToMode: 'first',
2555
+ reactionLevel: 'minimal',
2556
+ actions: {
2557
+ sendMessage: true,
2558
+ reactions: true,
2559
+ },
2560
+ accounts: {
2561
+ default: {
2562
+ botToken: loopBotToken || '<your_bot_token>',
2563
+ },
2564
+ },
2565
+ };
2566
+ if (isMultiBot) {
2567
+ telegramConfig.groupPolicy = groupId ? 'allowlist' : 'open';
2568
+ telegramConfig.groupAllowFrom = ['*'];
2569
+ telegramConfig.groups = {
2570
+ [groupId || '*']: { enabled: true, requireMention: false }
2570
2571
  };
2571
2572
  }
2572
2573
  botConfig.channels['telegram'] = telegramConfig;
2573
- } else if (hasZaloPersonal(channelKey)) {
2574
- botConfig.channels['zalouser'] = {
2575
- enabled: true,
2576
- defaultAccount: 'default',
2577
- dmPolicy: 'open',
2578
- allowFrom: ['*'],
2579
- groupPolicy: 'allowlist',
2580
- groupAllowFrom: ['*'],
2581
- historyLimit: 50,
2582
- groups: {
2583
- '*': {
2584
- enabled: true,
2585
- requireMention: false,
2586
- },
2587
- },
2588
- autoReply: true,
2589
- };
2574
+ } else if (hasZaloPersonal(channelKey)) {
2575
+ botConfig.channels['zalouser'] = {
2576
+ enabled: true,
2577
+ defaultAccount: 'default',
2578
+ dmPolicy: 'open',
2579
+ allowFrom: ['*'],
2580
+ groupPolicy: 'allowlist',
2581
+ groupAllowFrom: ['*'],
2582
+ historyLimit: 50,
2583
+ groups: {
2584
+ '*': {
2585
+ enabled: true,
2586
+ requireMention: false,
2587
+ },
2588
+ },
2589
+ autoReply: true,
2590
+ };
2590
2591
  } else if (channelKey === 'zalo-bot') {
2591
2592
  botConfig.channels['zalo'] = { enabled: true, provider: 'official_account' };
2592
2593
  }
2593
2594
 
2594
- await fs.writeJson(path.join(loopBotDir, '.openclaw', 'openclaw.json'), botConfig, { spaces: 2 });
2595
-
2596
- // ── Workspace files: use shared writeWorkspaceFiles() ──────────────────────
2595
+ await fs.writeJson(path.join(loopBotDir, '.openclaw', 'openclaw.json'), botConfig, { spaces: 2 });
2596
+
2597
+ // ── Workspace files: use shared writeWorkspaceFiles() ──────────────────────
2597
2598
  const dockerWorkspaceDir = path.join(loopBotDir, '.openclaw', loopWorkspaceDir);
2598
2599
  const dockerOwnAliases = [loopBotName, bots[bIndex]?.slashCmd || '', `bot ${bIndex + 1}`].filter(Boolean);
2599
2600
  const dockerOtherAgents = teamRoster
@@ -2624,8 +2625,8 @@ async function main() {
2624
2625
  // Append per-bot reply rules to AGENTS.md
2625
2626
  const otherBotNames = teamRoster.filter((p) => p.idx !== bIndex).map((p) => p.name);
2626
2627
  const extraAgentsMd = isVi
2627
- ? `\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.`
2628
- : `\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.`;
2628
+ ? `\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.`
2629
+ : `\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.`;
2629
2630
  await fs.appendFile(path.join(dockerWorkspaceDir, 'AGENTS.md'), extraAgentsMd);
2630
2631
  }
2631
2632
  } // END FOR LOOP
@@ -2635,14 +2636,14 @@ async function main() {
2635
2636
  await writeGeneratedArtifacts(projectDir, buildCliChromeDebugArtifacts());
2636
2637
 
2637
2638
  // ── Uninstall scripts ───────────────────────────────────────────────────────
2638
- await writeGeneratedArtifacts(projectDir, buildCliUninstallArtifacts({
2639
- deployMode,
2640
- osChoice: detectedOS,
2641
- projectDir,
2642
- botName: (deployMode !== 'docker' && detectedOS === 'vps')
2643
- ? getNativePm2AppName(isMultiBot)
2644
- : botName,
2645
- }));
2639
+ await writeGeneratedArtifacts(projectDir, buildCliUninstallArtifacts({
2640
+ deployMode,
2641
+ osChoice: detectedOS,
2642
+ projectDir,
2643
+ botName: (deployMode !== 'docker' && detectedOS === 'vps')
2644
+ ? getNativePm2AppName(isMultiBot)
2645
+ : botName,
2646
+ }));
2646
2647
 
2647
2648
  // ── Upgrade scripts ─────────────────────────────────────────────────────────
2648
2649
  await writeGeneratedArtifacts(projectDir, buildCliUpgradeArtifacts());
@@ -2650,33 +2651,33 @@ async function main() {
2650
2651
  // ── start-bot.bat / start-bot.sh — one-click restart scripts ─────────────
2651
2652
  // Generated for native deployments only (docker has docker compose up)
2652
2653
  if (deployMode !== 'docker') {
2653
- await writeGeneratedArtifacts(projectDir, buildCliStartBotArtifacts({
2654
- projectDir,
2655
- openclawHome: path.join(projectDir, '.openclaw'),
2656
- is9Router: providerKey === '9router',
2657
- osChoice,
2658
- isMultiBot,
2659
- appName: getNativePm2AppName(isMultiBot),
2660
- isVi,
2661
- }));
2662
-
2663
- console.log(chalk.cyan(
2664
- process.platform === 'win32'
2665
- ? (isVi
2666
- ? `\n🚀 start-bot.bat / start-bot.sh đã tạo — double-click để restart bot.`
2667
- : `\n🚀 start-bot.bat / start-bot.sh created — double-click to restart the bot.`)
2668
- : (isVi
2669
- ? `\n🚀 start-bot.sh đã tạo — chạy ./start-bot.sh để restart bot.`
2670
- : `\n🚀 start-bot.sh created — run ./start-bot.sh to restart the bot.`)
2671
- ));
2672
- }
2673
-
2674
- console.log(chalk.green(`✅ ${isVi ? 'Tạo cấu hình thành công!' : 'Configs created successfully!'}`));
2675
-
2676
- installLatestOpenClaw({ isVi, osChoice });
2677
-
2678
- // 7. Auto Run
2679
- const autoRun = deployMode === 'docker' ? await confirm({
2654
+ await writeGeneratedArtifacts(projectDir, buildCliStartBotArtifacts({
2655
+ projectDir,
2656
+ openclawHome: path.join(projectDir, '.openclaw'),
2657
+ is9Router: providerKey === '9router',
2658
+ osChoice,
2659
+ isMultiBot,
2660
+ appName: getNativePm2AppName(isMultiBot),
2661
+ isVi,
2662
+ }));
2663
+
2664
+ console.log(chalk.cyan(
2665
+ process.platform === 'win32'
2666
+ ? (isVi
2667
+ ? `\n🚀 start-bot.bat / start-bot.sh đã tạo — double-click để restart bot.`
2668
+ : `\n🚀 start-bot.bat / start-bot.sh created — double-click to restart the bot.`)
2669
+ : (isVi
2670
+ ? `\n🚀 start-bot.sh đã tạo — chạy ./start-bot.sh để restart bot.`
2671
+ : `\n🚀 start-bot.sh created — run ./start-bot.sh to restart the bot.`)
2672
+ ));
2673
+ }
2674
+
2675
+ console.log(chalk.green(`✅ ${isVi ? 'Tạo cấu hình thành công!' : 'Configs created successfully!'}`));
2676
+
2677
+ installLatestOpenClaw({ isVi, osChoice });
2678
+
2679
+ // 7. Auto Run
2680
+ const autoRun = deployMode === 'docker' ? await confirm({
2680
2681
  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?',
2681
2682
  default: true
2682
2683
  }) : false;
@@ -2756,7 +2757,7 @@ async function main() {
2756
2757
  });
2757
2758
 
2758
2759
  }
2759
- if (deployMode === 'docker') {
2760
+ if (deployMode === 'docker') {
2760
2761
 
2761
2762
  // ── Auto-install openclaw binary if not present ──────────────────────────
2762
2763
  const isOpenClawInstalled = () => { try { execSync('openclaw --version', { stdio: 'ignore' }); return true; } catch { return false; } };
@@ -2804,19 +2805,19 @@ async function main() {
2804
2805
  }
2805
2806
  }
2806
2807
 
2807
- let native9RouterSyncScriptPath = null;
2808
- if (providerKey === '9router') {
2809
- await writeNative9RouterPatchScript(projectDir);
2810
- native9RouterSyncScriptPath = await writeNative9RouterSyncScript(projectDir);
2811
- try {
2812
- execFileSync(process.execPath, [path.join(projectDir, '.openclaw', 'patch-9router.js')], {
2813
- cwd: projectDir,
2814
- stdio: 'ignore',
2815
- });
2816
- } catch {
2817
- // Start scripts retry this patch before launching 9router.
2818
- }
2819
- }
2808
+ let native9RouterSyncScriptPath = null;
2809
+ if (providerKey === '9router') {
2810
+ await writeNative9RouterPatchScript(projectDir);
2811
+ native9RouterSyncScriptPath = await writeNative9RouterSyncScript(projectDir);
2812
+ try {
2813
+ execFileSync(process.execPath, [path.join(projectDir, '.openclaw', 'patch-9router.js')], {
2814
+ cwd: projectDir,
2815
+ stdio: 'ignore',
2816
+ });
2817
+ } catch {
2818
+ // Start scripts retry this patch before launching 9router.
2819
+ }
2820
+ }
2820
2821
 
2821
2822
  await ensureProjectRuntimeDirs(projectDir, isVi);
2822
2823
 
@@ -2832,26 +2833,26 @@ async function main() {
2832
2833
  }
2833
2834
  }
2834
2835
 
2835
- if (isMultiBot && channelKey === 'telegram') {
2836
- if (providerKey === '9router') {
2837
- startNative9RouterPm2({ isVi, projectDir, appName: getNativePm2AppName(true), syncScriptPath: native9RouterSyncScriptPath });
2838
- }
2839
- execSync('pm2 start ecosystem.config.js && pm2 save', {
2840
- cwd: projectDir,
2841
- stdio: 'inherit',
2842
- shell: true
2843
- });
2844
- 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.'}`));
2845
- console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${getNativePm2AppName(true)}` : ` View logs: pm2 logs ${getNativePm2AppName(true)}`));
2846
- printNativeDashboardAccessInfo({ isVi, providerKey, projectDir });
2847
- if (channelKey === 'zalo-personal') {
2848
- printZaloPersonalLoginInfo({ isVi, deployMode: 'native', projectDir });
2849
- }
2850
- } else {
2851
- const appName = getNativePm2AppName(false);
2852
- if (providerKey === '9router') {
2853
- startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath: native9RouterSyncScriptPath });
2854
- }
2836
+ if (isMultiBot && channelKey === 'telegram') {
2837
+ if (providerKey === '9router') {
2838
+ startNative9RouterPm2({ isVi, projectDir, appName: getNativePm2AppName(true), syncScriptPath: native9RouterSyncScriptPath });
2839
+ }
2840
+ execSync('pm2 start ecosystem.config.js && pm2 save', {
2841
+ cwd: projectDir,
2842
+ stdio: 'inherit',
2843
+ shell: true
2844
+ });
2845
+ 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.'}`));
2846
+ console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${getNativePm2AppName(true)}` : ` View logs: pm2 logs ${getNativePm2AppName(true)}`));
2847
+ printNativeDashboardAccessInfo({ isVi, providerKey, projectDir });
2848
+ if (channelKey === 'zalo-personal') {
2849
+ printZaloPersonalLoginInfo({ isVi, deployMode: 'native', projectDir });
2850
+ }
2851
+ } else {
2852
+ const appName = getNativePm2AppName(false);
2853
+ if (providerKey === '9router') {
2854
+ startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath: native9RouterSyncScriptPath });
2855
+ }
2855
2856
  if (channelKey === 'zalo-personal') {
2856
2857
  await runNativeZaloPersonalLoginFlow({ isVi, projectDir });
2857
2858
  }
@@ -2886,10 +2887,10 @@ async function main() {
2886
2887
  cwd: projectDir,
2887
2888
  env: getProjectRuntimeEnv(projectDir, native9RouterLaunch.env)
2888
2889
  }).unref();
2889
- const routerHealth = await waitFor9RouterApiReady();
2890
- if (native9RouterSyncScriptPath) {
2891
- spawnBackgroundProcess(process.execPath, [native9RouterSyncScriptPath], {
2892
- cwd: projectDir
2890
+ const routerHealth = await waitFor9RouterApiReady();
2891
+ if (native9RouterSyncScriptPath) {
2892
+ spawnBackgroundProcess(process.execPath, [native9RouterSyncScriptPath], {
2893
+ cwd: projectDir
2893
2894
  }).unref();
2894
2895
  }
2895
2896
  console.log(chalk.gray(isVi
@@ -2935,9 +2936,9 @@ async function main() {
2935
2936
  console.log(chalk.yellow(`\n📋 ${isVi ? 'Xem huong dan sau cai:' : 'Read post-install guide:'} ${path.join(projectDir, TELEGRAM_SETUP_GUIDE_FILENAME)}`));
2936
2937
  }
2937
2938
  }
2938
- }
2939
-
2940
-
2939
+ }
2940
+
2941
+
2941
2942
 
2942
2943
  main().catch(err => {
2943
2944
  console.error(chalk.red('Error:'), err);