create-openclaw-bot 5.7.1 → 5.7.4

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
@@ -8,7 +8,7 @@ import chalk from 'chalk';
8
8
  import { spawn, execSync, execFileSync } from 'child_process';
9
9
  import { createRequire } from 'module';
10
10
 
11
- // ─── Shared generators (dual-mode IIFE + CJS) ────────────────────────────────
11
+ // ─── Shared generators (dual-mode IIFE + CJS) ────────────────────────────────
12
12
  // These modules export via module.exports when required from Node.js
13
13
  const _require = createRequire(import.meta.url);
14
14
 
@@ -75,8 +75,8 @@ function installRelayPluginForProject(projectDir, isVi) {
75
75
  // silent fallback
76
76
  }
77
77
  console.log(chalk.yellow(isVi
78
- ? `\n⚠️ Chua the tu dong cai plugin. Sau khi bot chay, chay thu cong:\n openclaw plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC}`
79
- : `\n⚠️ Could not auto-install plugin. After the bot starts, run manually:\n openclaw plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC}`));
78
+ ? `\n⚠️ Chua the tu dong cai plugin. Sau khi bot chay, chay thu cong:\n openclaw plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC}`
79
+ : `\n⚠️ Could not auto-install plugin. After the bot starts, run manually:\n openclaw plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC}`));
80
80
  return false;
81
81
  }
82
82
 
@@ -366,8 +366,8 @@ function ensureUserWritableGlobalNpm({ isVi, osChoice }) {
366
366
  return true;
367
367
  } catch {
368
368
  console.log(chalk.yellow(isVi
369
- ? '⚠️ Không thể cấu hình npm global prefix trong ~/.local. Tiếp tục thử cài đặt trực tiếp.'
370
- : '⚠️ Could not configure npm global prefix in ~/.local. Falling back to direct install.'));
369
+ ? '⚠️ Không thể cấu hình npm global prefix trong ~/.local. Tiếp tục thử cài đặt trực tiếp.'
370
+ : '⚠️ Could not configure npm global prefix in ~/.local. Falling back to direct install.'));
371
371
  return false;
372
372
  }
373
373
  }
@@ -401,35 +401,35 @@ function installGlobalPackage(pkg, { isVi, osChoice, displayName }) {
401
401
  }
402
402
 
403
403
  console.log(chalk.yellow(isVi
404
- ? `⚠️ Không thể tự cài ${displayName}. Chạy thủ công: ${osChoice === 'windows' ? `npm install -g ${pkg}` : `npm config set prefix ~/.local && npm install -g ${pkg}`}`
405
- : `⚠️ Could not auto-install ${displayName}. Run manually: ${osChoice === 'windows' ? `npm install -g ${pkg}` : `npm config set prefix ~/.local && npm install -g ${pkg}`}`));
404
+ ? `⚠️ Không thể tự cài ${displayName}. Chạy thủ công: ${osChoice === 'windows' ? `npm install -g ${pkg}` : `npm config set prefix ~/.local && npm install -g ${pkg}`}`
405
+ : `⚠️ Could not auto-install ${displayName}. Run manually: ${osChoice === 'windows' ? `npm install -g ${pkg}` : `npm config set prefix ~/.local && npm install -g ${pkg}`}`));
406
406
  return false;
407
407
  }
408
408
 
409
409
  function installLatestOpenClaw({ isVi, osChoice }) {
410
410
  if (shouldReuseInstalledGlobals() && isOpenClawInstalled()) {
411
411
  console.log(chalk.green(isVi
412
- ? '\n♻️ Dang dung lai openclaw da cai san de test nhanh.'
413
- : '\n♻️ Reusing the installed openclaw for a faster test run.'));
412
+ ? '\n♻️ Dang dung lai openclaw da cai san de test nhanh.'
413
+ : '\n♻️ Reusing the installed openclaw for a faster test run.'));
414
414
  return;
415
415
  }
416
416
 
417
417
  console.log(chalk.cyan(isVi
418
- ? `\n📦 Dang cai/cap nhat ${OPENCLAW_NPM_SPEC}...`
419
- : `\n📦 Installing/updating ${OPENCLAW_NPM_SPEC}...`));
418
+ ? `\n📦 Dang cai/cap nhat ${OPENCLAW_NPM_SPEC}...`
419
+ : `\n📦 Installing/updating ${OPENCLAW_NPM_SPEC}...`));
420
420
 
421
421
  if (!installGlobalPackage(OPENCLAW_NPM_SPEC, { isVi, osChoice, displayName: 'openclaw' })) {
422
422
  process.exit(1);
423
423
  }
424
424
 
425
425
  console.log(chalk.green(isVi
426
- ? `✅ openclaw da duoc ghim dung ban ${OPENCLAW_NPM_SPEC}!`
427
- : `✅ openclaw is now pinned to ${OPENCLAW_NPM_SPEC}!`));
426
+ ? `✅ openclaw da duoc ghim dung ban ${OPENCLAW_NPM_SPEC}!`
427
+ : `✅ openclaw is now pinned to ${OPENCLAW_NPM_SPEC}!`));
428
428
  }
429
429
 
430
- // ─── Shared from docker-gen.js ──────────────────────────────────────────────
430
+ // ─── Shared from docker-gen.js ──────────────────────────────────────────────
431
431
  // build9RouterSmartRouteSyncScript, indentBlock, build9RouterComposeEntrypointScript
432
- // are imported from setup/shared/docker-gen.js do NOT re-define here.
432
+ // are imported from setup/shared/docker-gen.js — do NOT re-define here.
433
433
 
434
434
  function resolveCommandOnPath(command) {
435
435
  if (process.platform === 'win32') {
@@ -613,26 +613,54 @@ function printNativeDashboardAccessInfo({ isVi, providerKey, projectDir, gateway
613
613
 
614
614
  function printZaloPersonalLoginInfo({ isVi, deployMode, projectDir }) {
615
615
  const nativeCmd = 'openclaw channels login --channel zalouser --verbose';
616
- const dockerCmd = 'docker compose exec -it ai-bot openclaw channels login --channel zalouser --verbose';
616
+ const dockerCmd = 'docker exec -it openclaw-bot openclaw channels login --channel zalouser --verbose';
617
617
  const cmd = deployMode === 'native' ? nativeCmd : dockerCmd;
618
618
  const qrPath = deployMode === 'native'
619
619
  ? path.join(os.tmpdir(), 'openclaw', 'openclaw-zalouser-qr-default.png')
620
620
  : '/tmp/openclaw/openclaw-zalouser-qr-default.png';
621
- const projectQrPath = path.join(projectDir, 'zalo-login-qr.png');
622
621
  const copyCmd = deployMode === 'native'
623
622
  ? (process.platform === 'win32'
624
- ? `Copy-Item "${qrPath}" "${projectQrPath}"`
625
- : `cp "${qrPath}" "${projectQrPath}"`)
626
- : `docker compose cp ai-bot:${qrPath} ./zalo-login-qr.png`;
623
+ ? `Copy-Item "${qrPath}" "${path.join(projectDir, 'zalo-login-qr.png')}"`
624
+ : `cp "${qrPath}" "${path.join(projectDir, 'zalo-login-qr.png')}"`)
625
+ : `docker cp openclaw-bot:${qrPath} ./zalo-qr.png`;
627
626
 
628
- console.log(chalk.yellow(`\n📱 ${isVi ? 'Đăng nhập Zalo Personal (1 lần):' : 'Zalo Personal login (one time):'}`));
629
- console.log(chalk.white(` cd ${projectDir}${deployMode === 'native' ? '' : '/docker/openclaw'} ${process.platform === 'win32' ? ';' : '&&'} ${cmd}`));
630
- console.log(chalk.gray(isVi
631
- ? ` OpenClaw sẽ tạo file QR tại: ${qrPath}`
632
- : ` OpenClaw will generate a QR image at: ${qrPath}`));
633
- console.log(chalk.gray(isVi
634
- ? ` Nếu cần copy QR ra thư mục project, dùng: ${copyCmd}`
635
- : ` If needed, copy the QR into the project folder with: ${copyCmd}`));
627
+ console.log(chalk.yellow(`\n📱 ${isVi ? 'Đăng nhập Zalo Personal (1 lần):' : 'Zalo Personal login (one time):'}`));
628
+ if (deployMode === 'docker') {
629
+ console.log(chalk.white(isVi
630
+ ? ` 1. cd ${projectDir}/docker/openclaw`
631
+ : ` 1. cd ${projectDir}/docker/openclaw`));
632
+ console.log(chalk.white(isVi
633
+ ? ` 2. ${cmd}`
634
+ : ` 2. ${cmd}`));
635
+ console.log(chalk.white(isVi
636
+ ? ` 3. Tìm file QR trong container: ${qrPath}`
637
+ : ` 3. Find QR image in container: ${qrPath}`));
638
+ console.log(chalk.gray(isVi
639
+ ? ` → Mở Docker Desktop > container openclaw-bot > tab Files > tìm file trên`
640
+ : ` → Open Docker Desktop > container openclaw-bot > Files tab > find file above`));
641
+ console.log(chalk.gray(isVi
642
+ ? ` → Hoặc chạy: ${copyCmd}`
643
+ : ` → Or run: ${copyCmd}`));
644
+ console.log(chalk.white(isVi
645
+ ? ' 4. Mở app Zalo > Quét QR > quét mã trong file QR'
646
+ : ' 4. Open Zalo app > Scan QR > scan the QR image'));
647
+ console.log(chalk.white(isVi
648
+ ? ' 5. Đợi thấy "Login successful" trong terminal'
649
+ : ' 5. Wait for "Login successful" in terminal'));
650
+ console.log(chalk.white(isVi
651
+ ? ' 6. docker compose restart'
652
+ : ' 6. docker compose restart'));
653
+ } else {
654
+ console.log(chalk.white(` cd ${projectDir} ${process.platform === 'win32' ? ';' : '&&'} ${cmd}`));
655
+ console.log(chalk.gray(isVi
656
+ ? ` → File QR sẽ tạo tại: ${qrPath}`
657
+ : ` → QR file will be created at: ${qrPath}`));
658
+ if (process.platform === 'win32') {
659
+ console.log(chalk.gray(isVi
660
+ ? ` → Copy QR ra project: ${copyCmd}`
661
+ : ` → Copy QR to project: ${copyCmd}`));
662
+ }
663
+ }
636
664
  }
637
665
 
638
666
  async function waitForFile(filePath, timeoutMs = 15000, intervalMs = 500) {
@@ -680,13 +708,13 @@ function approveZaloPairingCode({ pairingCode, projectDir, isVi }) {
680
708
  env: process.env
681
709
  });
682
710
  console.log(chalk.green(isVi
683
- ? `✅ Da tu dong approve pairing code Zalo: ${pairingCode}`
684
- : `✅ Automatically approved the Zalo pairing code: ${pairingCode}`));
711
+ ? `✅ Da tu dong approve pairing code Zalo: ${pairingCode}`
712
+ : `✅ Automatically approved the Zalo pairing code: ${pairingCode}`));
685
713
  return true;
686
714
  } catch {
687
715
  console.log(chalk.yellow(isVi
688
- ? `⚠️ Khong the tu dong approve pairing code ${pairingCode}. Ban co the chay thu cong: openclaw pairing approve zalouser ${pairingCode}`
689
- : `⚠️ Could not auto-approve pairing code ${pairingCode}. You can run it manually: openclaw pairing approve zalouser ${pairingCode}`));
716
+ ? `⚠️ Khong the tu dong approve pairing code ${pairingCode}. Ban co the chay thu cong: openclaw pairing approve zalouser ${pairingCode}`
717
+ : `⚠️ Could not auto-approve pairing code ${pairingCode}. You can run it manually: openclaw pairing approve zalouser ${pairingCode}`));
690
718
  return false;
691
719
  }
692
720
  }
@@ -694,7 +722,7 @@ function approveZaloPairingCode({ pairingCode, projectDir, isVi }) {
694
722
  async function runNativeZaloPersonalLoginFlow({ isVi, projectDir }) {
695
723
  const qrSourcePath = path.join(os.tmpdir(), 'openclaw', 'openclaw-zalouser-qr-default.png');
696
724
  const qrProjectPath = path.join(projectDir, 'zalo-login-qr.png');
697
- console.log(chalk.yellow(`\n📱 ${isVi ? 'Đang tạo QR đăng nhập Zalo Personal...' : 'Generating the Zalo Personal login QR...'}`));
725
+ console.log(chalk.yellow(`\n📱 ${isVi ? 'Đang tạo QR đăng nhập Zalo Personal...' : 'Generating the Zalo Personal login QR...'}`));
698
726
  const loginStartedAt = Date.now();
699
727
 
700
728
  try {
@@ -748,8 +776,8 @@ async function runNativeZaloPersonalLoginFlow({ isVi, projectDir }) {
748
776
  await fs.copy(qrSourcePath, qrProjectPath, { overwrite: true });
749
777
  qrCopied = true;
750
778
  console.log(chalk.green(isVi
751
- ? `✅ QR đã được copy vào: ${qrProjectPath}`
752
- : `✅ QR copied to: ${qrProjectPath}`));
779
+ ? `✅ QR đã được copy vào: ${qrProjectPath}`
780
+ : `✅ QR copied to: ${qrProjectPath}`));
753
781
  }
754
782
  };
755
783
 
@@ -766,13 +794,13 @@ async function runNativeZaloPersonalLoginFlow({ isVi, projectDir }) {
766
794
 
767
795
  if (exitCode !== 0 && !loginSucceeded) {
768
796
  console.log(chalk.yellow(isVi
769
- ? '⚠️ Chưa hoàn tất đăng nhập Zalo trong lúc setup. Bạn thể chạy lại lệnh login thủ công sau.'
770
- : '⚠️ Zalo login was not completed during setup. You can run the login command manually afterwards.'));
797
+ ? '⚠️ Chưa hoàn tất đăng nhập Zalo trong lúc setup. Bạn có thể chạy lại lệnh login thủ công sau.'
798
+ : '⚠️ Zalo login was not completed during setup. You can run the login command manually afterwards.'));
771
799
  printZaloPersonalLoginInfo({ isVi, deployMode: 'native', projectDir });
772
800
  } else if (loginSucceeded && exitCode !== 0) {
773
801
  console.log(chalk.green(isVi
774
- ? ' Đăng nhập Zalo đã thành công CLI trả về trạng thái không chuẩn.'
775
- : ' Zalo login succeeded even though the CLI returned a non-standard exit status.'));
802
+ ? '✅ Đăng nhập Zalo đã thành công dù CLI trả về trạng thái không chuẩn.'
803
+ : '✅ Zalo login succeeded even though the CLI returned a non-standard exit status.'));
776
804
  }
777
805
  }
778
806
 
@@ -786,8 +814,8 @@ function runPm2Save({ projectDir, isVi }) {
786
814
  });
787
815
  } catch {
788
816
  console.log(chalk.yellow(isVi
789
- ? '⚠️ PM2 save khong hoan tat. Bot van co the dang chay, nhung hay thu chay lai `pm2 save` sau.'
790
- : '⚠️ PM2 save did not complete. The app may still be running, but try `pm2 save` again afterwards.'));
817
+ ? '⚠️ PM2 save khong hoan tat. Bot van co the dang chay, nhung hay thu chay lai `pm2 save` sau.'
818
+ : '⚠️ PM2 save did not complete. The app may still be running, but try `pm2 save` again afterwards.'));
791
819
  }
792
820
  }
793
821
 
@@ -1019,26 +1047,26 @@ function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
1019
1047
  });
1020
1048
  } catch (syncErr) {
1021
1049
  console.log(chalk.yellow(isVi
1022
- ? `\n⚠️ Khong the tu dong khoi dong sync script qua PM2.`
1023
- : `\n⚠️ Could not auto-start 9router sync script via PM2.`));
1050
+ ? `\n⚠️ Khong the tu dong khoi dong sync script qua PM2.`
1051
+ : `\n⚠️ Could not auto-start 9router sync script via PM2.`));
1024
1052
  }
1025
1053
  }
1026
1054
  runPm2Save({ projectDir, isVi });
1027
- console.log(chalk.green(`\n ${isVi ? '9Router da duoc khoi dong qua PM2.' : '9Router is running via PM2.'}`));
1055
+ console.log(chalk.green(`\n✅ ${isVi ? '9Router da duoc khoi dong qua PM2.' : '9Router is running via PM2.'}`));
1028
1056
  console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${routerAppName}` : ` View logs: pm2 logs ${routerAppName}`));
1029
1057
  }
1030
1058
 
1031
1059
  async function ensureProjectRuntimeDirs(projectDir, isVi) {
1032
1060
  await fs.ensureDir(path.join(projectDir, '.openclaw'));
1033
1061
  await fs.ensureDir(getProject9RouterDataDir(projectDir));
1034
- console.log(chalk.green(`\n ${isVi
1062
+ console.log(chalk.green(`\n✅ ${isVi
1035
1063
  ? 'Da chuan bi runtime directories local trong project.'
1036
1064
  : 'Prepared project-local runtime directories.'}`));
1037
1065
  }
1038
1066
 
1039
1067
  // buildTelegramPostInstallChecklist is imported from setup/shared/common-gen.js
1040
1068
 
1041
- // ─── Docker Auto-Detection ───────────────────────────────────────────────────
1069
+ // ─── Docker Auto-Detection ───────────────────────────────────────────────────
1042
1070
  function isDockerInstalled() {
1043
1071
  try {
1044
1072
  execSync('docker --version', { stdio: 'ignore' });
@@ -1049,15 +1077,15 @@ function isDockerInstalled() {
1049
1077
 
1050
1078
 
1051
1079
  const LOGO = `
1052
- ████████╗██╗ ██╗ █████╗ ███╗ ██╗███╗ ███╗██╗███╗ ██╗██╗ ██╗██╗ ██╗ ██████╗ ██╗ ███████╗
1053
- ╚══██╔══╝██║ ██║██╔══██╗████╗ ██║████╗ ████║██║████╗ ██║██║ ██║██║ ██║██╔═══██╗██║ ██╔════╝
1054
- ██║ ██║ ██║███████║██╔██╗ ██║██╔████╔██║██║██╔██╗ ██║███████║███████║██║ ██║██║ █████╗
1055
- ██║ ██║ ██║██╔══██║██║╚██╗██║██║╚██╔╝██║██║██║╚██╗██║██╔══██║██╔══██║██║ ██║██║ ██╔══╝
1056
- ██║ ╚██████╔╝██║ ██║██║ ╚████║██║ ╚═╝ ██║██║██║ ╚████║██║ ██║██║ ██║╚██████╔╝███████╗███████╗
1057
- ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝
1080
+ ████████╗██╗ ██╗ █████╗ ███╗ ██╗███╗ ███╗██╗███╗ ██╗██╗ ██╗██╗ ██╗ ██████╗ ██╗ ███████╗
1081
+ ╚══██╔══╝██║ ██║██╔══██╗████╗ ██║████╗ ████║██║████╗ ██║██║ ██║██║ ██║██╔═══██╗██║ ██╔════╝
1082
+ ██║ ██║ ██║███████║██╔██╗ ██║██╔████╔██║██║██╔██╗ ██║███████║███████║██║ ██║██║ █████╗
1083
+ ██║ ██║ ██║██╔══██║██║╚██╗██║██║╚██╔╝██║██║██║╚██╗██║██╔══██║██╔══██║██║ ██║██║ ██╔══╝
1084
+ ██║ ╚██████╔╝██║ ██║██║ ╚████║██║ ╚═╝ ██║██║██║ ╚████║██║ ██║██║ ██║╚██████╔╝███████╗███████╗
1085
+ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝
1058
1086
  `;
1059
1087
 
1060
- // ── Data constants from setup/data/index.js (single source of truth) ──────────
1088
+ // ── Data constants from setup/data/index.js (single source of truth) ──────────
1061
1089
  const CHANNELS = _CHANNELS;
1062
1090
  const PROVIDERS = _PROVIDERS;
1063
1091
  const SKILLS = _SKILLS;
@@ -1086,8 +1114,8 @@ function getCliSkillChoices({ providerKey, isVi }) {
1086
1114
  let name = `${skill.icon || ''} ${isVi ? (skill.nameVi || skill.name) : (skill.nameEn || skill.name)}`.trim();
1087
1115
  if (value === 'memory') {
1088
1116
  name = isVi
1089
- ? (memoryRecommended ? '🧠 Long-term Memory (Khuyên dùng)' : '🧠 Long-term Memory')
1090
- : (memoryRecommended ? '🧠 Long-term Memory (Recommended)' : '🧠 Long-term Memory');
1117
+ ? (memoryRecommended ? '🧠 Long-term Memory ⭐(Khuyên dùng)' : '🧠 Long-term Memory')
1118
+ : (memoryRecommended ? '🧠 Long-term Memory ⭐(Recommended)' : '🧠 Long-term Memory');
1091
1119
  }
1092
1120
  return {
1093
1121
  name,
@@ -1101,13 +1129,13 @@ const CLI_BACK = '__openclaw_cli_back__';
1101
1129
 
1102
1130
  function getBackChoice(isVi) {
1103
1131
  return {
1104
- name: isVi ? ' Quay lại' : ' Back',
1132
+ name: isVi ? '← Quay lại' : '← Back',
1105
1133
  value: CLI_BACK,
1106
1134
  };
1107
1135
  }
1108
1136
 
1109
1137
  function withBackHint(message, isVi) {
1110
- return `${message} ${isVi ? '( "back" để quay lại)' : '(type "back" to go back)'}`;
1138
+ return `${message} ${isVi ? '(gõ "back" để quay lại)' : '(type "back" to go back)'}`;
1111
1139
  }
1112
1140
 
1113
1141
  async function selectWithBack({ message, choices, defaultValue, allowBack = false, isVi = true }) {
@@ -1154,7 +1182,7 @@ async function collectBotSetupStep({
1154
1182
 
1155
1183
  if (channelKey === 'telegram') {
1156
1184
  const botCountValue = await selectWithBack({
1157
- message: isVi ? 'Bạn muốn cài bao nhiêu Telegram bot?' : 'How many Telegram bots do you want to deploy?',
1185
+ message: isVi ? 'Bạn muốn cài bao nhiêu Telegram bot?' : 'How many Telegram bots do you want to deploy?',
1158
1186
  choices: [
1159
1187
  { name: '1 bot (single)', value: '1' },
1160
1188
  { name: '2 bots (Department Room)', value: '2' },
@@ -1173,10 +1201,10 @@ async function collectBotSetupStep({
1173
1201
 
1174
1202
  if (botCount > 1) {
1175
1203
  const groupOption = await selectWithBack({
1176
- message: isVi ? 'Bạn sẵn Telegram Group chưa?' : 'Do you already have a Telegram Group?',
1204
+ message: isVi ? 'Bạn có sẵn Telegram Group chưa?' : 'Do you already have a Telegram Group?',
1177
1205
  choices: [
1178
- { 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' },
1179
- { name: isVi ? '🔗 Đã group nhập Group ID ngay' : '🔗 Already have a group enter Group ID now', value: 'existing' }
1206
+ { 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' },
1207
+ { name: isVi ? '🔗 Đã có group — nhập Group ID ngay' : '🔗 Already have a group — enter Group ID now', value: 'existing' }
1180
1208
  ],
1181
1209
  defaultValue: groupId ? 'existing' : 'create',
1182
1210
  allowBack: true,
@@ -1188,8 +1216,8 @@ async function collectBotSetupStep({
1188
1216
 
1189
1217
  if (groupOption === 'existing') {
1190
1218
  console.log(chalk.dim(isVi
1191
- ? '\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" đó Group ID (bắt đầu bằng -100)\n 👉 https://t.me/userinfobot\n'
1192
- : '\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'));
1219
+ ? '\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'
1220
+ : '\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'));
1193
1221
  const nextGroupId = await inputWithBack({
1194
1222
  message: isVi ? 'Telegram Group ID (VD: -1001234567890):' : 'Telegram Group ID (e.g. -1001234567890):',
1195
1223
  defaultValue: groupId,
@@ -1208,12 +1236,12 @@ async function collectBotSetupStep({
1208
1236
  }
1209
1237
 
1210
1238
  for (let i = 0; i < botCount; i++) {
1211
- console.log(chalk.bold(`\n${isVi ? `─── Bot ${i + 1} / ${botCount} ───` : `─── Bot ${i + 1} / ${botCount} ───`}`));
1239
+ console.log(chalk.bold(`\n${isVi ? `─── Bot ${i + 1} / ${botCount} ───` : `─── Bot ${i + 1} / ${botCount} ───`}`));
1212
1240
  const defaults = existingBots[i] || {};
1213
1241
  const fields = [
1214
1242
  {
1215
1243
  key: 'name',
1216
- message: isVi ? `Tên Bot ${i + 1}:` : `Bot ${i + 1} name:`,
1244
+ message: isVi ? `Tên Bot ${i + 1}:` : `Bot ${i + 1} name:`,
1217
1245
  defaultValue: defaults.name || `Bot ${i + 1}`,
1218
1246
  required: true,
1219
1247
  },
@@ -1225,19 +1253,19 @@ async function collectBotSetupStep({
1225
1253
  },
1226
1254
  {
1227
1255
  key: 'desc',
1228
- message: isVi ? ` tả Bot ${i + 1} (VD: Trợ AI nhân):` : `Bot ${i + 1} description (e.g. Personal AI assistant):`,
1229
- defaultValue: defaults.desc || (isVi ? 'Trợ AI nhân' : 'Personal AI assistant'),
1256
+ message: isVi ? `Mô tả Bot ${i + 1} (VD: Trợ lý AI cá nhân):` : `Bot ${i + 1} description (e.g. Personal AI assistant):`,
1257
+ defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1230
1258
  required: true,
1231
1259
  },
1232
1260
  {
1233
1261
  key: 'persona',
1234
- 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):`,
1262
+ 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):`,
1235
1263
  defaultValue: defaults.persona || '',
1236
1264
  required: false,
1237
1265
  },
1238
1266
  {
1239
1267
  key: 'token',
1240
- message: isVi ? 'Bot Token (từ @BotFather):' : 'Bot Token (from @BotFather):',
1268
+ message: isVi ? 'Bot Token (từ @BotFather):' : 'Bot Token (from @BotFather):',
1241
1269
  defaultValue: defaults.token || '',
1242
1270
  required: true,
1243
1271
  },
@@ -1271,25 +1299,25 @@ async function collectBotSetupStep({
1271
1299
  const fields = [
1272
1300
  {
1273
1301
  key: 'name',
1274
- message: isVi ? 'Tên Bot:' : 'Bot Name:',
1302
+ message: isVi ? 'Tên Bot:' : 'Bot Name:',
1275
1303
  defaultValue: defaults.name || 'Chat Bot',
1276
1304
  required: true,
1277
1305
  },
1278
1306
  {
1279
1307
  key: 'desc',
1280
- message: isVi ? ' tả Bot:' : 'Bot Description:',
1281
- defaultValue: defaults.desc || (isVi ? 'Trợ AI nhân' : 'Personal AI assistant'),
1308
+ message: isVi ? 'Mô tả Bot:' : 'Bot Description:',
1309
+ defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1282
1310
  required: true,
1283
1311
  },
1284
1312
  {
1285
1313
  key: 'persona',
1286
- message: isVi ? 'Tính cách & quy tắc (VD: gọn gàng, thân thiện):' : 'Persona & rules (e.g. concise, friendly):',
1314
+ message: isVi ? 'Tính cách & quy tắc (VD: gọn gàng, thân thiện):' : 'Persona & rules (e.g. concise, friendly):',
1287
1315
  defaultValue: defaults.persona || '',
1288
1316
  required: false,
1289
1317
  },
1290
1318
  {
1291
1319
  key: 'token',
1292
- message: isVi ? `Nhập ${channel.name} Token:` : `Enter ${channel.name} Token:`,
1320
+ message: isVi ? `Nhập ${channel.name} Token:` : `Enter ${channel.name} Token:`,
1293
1321
  defaultValue: defaults.token || '',
1294
1322
  required: true,
1295
1323
  },
@@ -1345,7 +1373,7 @@ async function collectBotSetupStepWithGroupBack(options) {
1345
1373
 
1346
1374
  if (channelKey === 'telegram') {
1347
1375
  const botCountValue = await selectWithBack({
1348
- message: isVi ? 'Bạn muốn cài bao nhiêu Telegram bot?' : 'How many Telegram bots do you want to deploy?',
1376
+ message: isVi ? 'Bạn muốn cài bao nhiêu Telegram bot?' : 'How many Telegram bots do you want to deploy?',
1349
1377
  choices: [
1350
1378
  { name: '1 bot (single)', value: '1' },
1351
1379
  { name: '2 bots (Department Room)', value: '2' },
@@ -1365,10 +1393,10 @@ async function collectBotSetupStepWithGroupBack(options) {
1365
1393
  if (botCount > 1) {
1366
1394
  while (true) {
1367
1395
  const groupOption = await selectWithBack({
1368
- message: isVi ? 'Bạn sẵn Telegram Group chưa?' : 'Do you already have a Telegram Group?',
1396
+ message: isVi ? 'Bạn có sẵn Telegram Group chưa?' : 'Do you already have a Telegram Group?',
1369
1397
  choices: [
1370
- { 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' },
1371
- { name: isVi ? '🔗 Đã group nhập Group ID ngay' : '🔗 Already have a group enter Group ID now', value: 'existing' }
1398
+ { 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' },
1399
+ { name: isVi ? '🔗 Đã có group — nhập Group ID ngay' : '🔗 Already have a group — enter Group ID now', value: 'existing' }
1372
1400
  ],
1373
1401
  defaultValue: groupId ? 'existing' : 'create',
1374
1402
  allowBack: true,
@@ -1380,8 +1408,8 @@ async function collectBotSetupStepWithGroupBack(options) {
1380
1408
 
1381
1409
  if (groupOption === 'existing') {
1382
1410
  console.log(chalk.dim(isVi
1383
- ? '\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" đó Group ID (bắt đầu bằng -100)\n 👉 https://t.me/userinfobot\n'
1384
- : '\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'));
1411
+ ? '\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'
1412
+ : '\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'));
1385
1413
  const nextGroupId = await inputWithBack({
1386
1414
  message: isVi ? 'Telegram Group ID (VD: -1001234567890):' : 'Telegram Group ID (e.g. -1001234567890):',
1387
1415
  defaultValue: groupId,
@@ -1403,12 +1431,12 @@ async function collectBotSetupStepWithGroupBack(options) {
1403
1431
  }
1404
1432
 
1405
1433
  for (let i = 0; i < botCount; i++) {
1406
- console.log(chalk.bold(`\n${isVi ? `─── Bot ${i + 1} / ${botCount} ───` : `─── Bot ${i + 1} / ${botCount} ───`}`));
1434
+ console.log(chalk.bold(`\n${isVi ? `─── Bot ${i + 1} / ${botCount} ───` : `─── Bot ${i + 1} / ${botCount} ───`}`));
1407
1435
  const defaults = existingBots[i] || {};
1408
1436
  const fields = [
1409
1437
  {
1410
1438
  key: 'name',
1411
- message: isVi ? `Tên Bot ${i + 1}:` : `Bot ${i + 1} name:`,
1439
+ message: isVi ? `Tên Bot ${i + 1}:` : `Bot ${i + 1} name:`,
1412
1440
  defaultValue: defaults.name || `Bot ${i + 1}`,
1413
1441
  required: true,
1414
1442
  },
@@ -1420,19 +1448,19 @@ async function collectBotSetupStepWithGroupBack(options) {
1420
1448
  },
1421
1449
  {
1422
1450
  key: 'desc',
1423
- message: isVi ? ` tả Bot ${i + 1} (VD: Trợ AI nhân):` : `Bot ${i + 1} description (e.g. Personal AI assistant):`,
1424
- defaultValue: defaults.desc || (isVi ? 'Trợ AI nhân' : 'Personal AI assistant'),
1451
+ message: isVi ? `Mô tả Bot ${i + 1} (VD: Trợ lý AI cá nhân):` : `Bot ${i + 1} description (e.g. Personal AI assistant):`,
1452
+ defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1425
1453
  required: true,
1426
1454
  },
1427
1455
  {
1428
1456
  key: 'persona',
1429
- 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):`,
1457
+ 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):`,
1430
1458
  defaultValue: defaults.persona || '',
1431
1459
  required: false,
1432
1460
  },
1433
1461
  {
1434
1462
  key: 'token',
1435
- message: isVi ? 'Bot Token (từ @BotFather):' : 'Bot Token (from @BotFather):',
1463
+ message: isVi ? 'Bot Token (từ @BotFather):' : 'Bot Token (from @BotFather):',
1436
1464
  defaultValue: defaults.token || '',
1437
1465
  required: true,
1438
1466
  },
@@ -1466,25 +1494,25 @@ async function collectBotSetupStepWithGroupBack(options) {
1466
1494
  const fields = [
1467
1495
  {
1468
1496
  key: 'name',
1469
- message: isVi ? 'Tên Bot:' : 'Bot Name:',
1497
+ message: isVi ? 'Tên Bot:' : 'Bot Name:',
1470
1498
  defaultValue: defaults.name || 'Chat Bot',
1471
1499
  required: true,
1472
1500
  },
1473
1501
  {
1474
1502
  key: 'desc',
1475
- message: isVi ? ' tả Bot:' : 'Bot Description:',
1476
- defaultValue: defaults.desc || (isVi ? 'Trợ AI nhân' : 'Personal AI assistant'),
1503
+ message: isVi ? 'Mô tả Bot:' : 'Bot Description:',
1504
+ defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1477
1505
  required: true,
1478
1506
  },
1479
1507
  {
1480
1508
  key: 'persona',
1481
- message: isVi ? 'Tính cách & quy tắc (VD: gọn gàng, thân thiện):' : 'Persona & rules (e.g. concise, friendly):',
1509
+ message: isVi ? 'Tính cách & quy tắc (VD: gọn gàng, thân thiện):' : 'Persona & rules (e.g. concise, friendly):',
1482
1510
  defaultValue: defaults.persona || '',
1483
1511
  required: false,
1484
1512
  },
1485
1513
  {
1486
1514
  key: 'token',
1487
- message: isVi ? `Nhập ${channel.name} Token:` : `Enter ${channel.name} Token:`,
1515
+ message: isVi ? `Nhập ${channel.name} Token:` : `Enter ${channel.name} Token:`,
1488
1516
  defaultValue: defaults.token || '',
1489
1517
  required: true,
1490
1518
  },
@@ -1531,7 +1559,7 @@ async function collectProviderStep({
1531
1559
  existingOllamaModel = 'gemma4:e2b',
1532
1560
  }) {
1533
1561
  const providerKey = await selectWithBack({
1534
- message: isVi ? 'Chọn AI Provider:' : 'Select AI Provider:',
1562
+ message: isVi ? 'Chọn AI Provider:' : 'Select AI Provider:',
1535
1563
  choices: Object.entries(PROVIDERS).map(([k, v]) => ({ name: `${v.icon} ${v.name}`, value: k })),
1536
1564
  defaultValue: existingProviderKey || undefined,
1537
1565
  allowBack: true,
@@ -1545,7 +1573,7 @@ async function collectProviderStep({
1545
1573
  let providerKeyVal = existingProviderKey === providerKey ? existingProviderKeyVal : '';
1546
1574
  if (!provider.isProxy && !provider.isLocal) {
1547
1575
  const keyValue = await inputWithBack({
1548
- message: isVi ? `Nhập ${provider.envKey}:` : `Enter ${provider.envKey}:`,
1576
+ message: isVi ? `Nhập ${provider.envKey}:` : `Enter ${provider.envKey}:`,
1549
1577
  defaultValue: providerKeyVal,
1550
1578
  required: true,
1551
1579
  allowBack: true,
@@ -1560,33 +1588,33 @@ async function collectProviderStep({
1560
1588
  let selectedOllamaModel = existingProviderKey === 'ollama' ? existingOllamaModel : 'gemma4:e2b';
1561
1589
  if (providerKey === 'ollama') {
1562
1590
  console.log(chalk.yellow(isVi
1563
- ? '\n💡 Gemma 4 (02/04/2026) chọn kích thước phù hợp với RAM máy bạn:'
1564
- : '\n💡 Gemma 4 (April 2, 2026) pick a size that fits your RAM:'));
1591
+ ? '\n💡 Gemma 4 (02/04/2026) — chọn kích thước phù hợp với RAM máy bạn:'
1592
+ : '\n💡 Gemma 4 (April 2, 2026) — pick a size that fits your RAM:'));
1565
1593
  const modelValue = await selectWithBack({
1566
- message: isVi ? 'Chọn model Ollama:' : 'Select Ollama model:',
1594
+ message: isVi ? 'Chọn model Ollama:' : 'Select Ollama model:',
1567
1595
  choices: [
1568
1596
  {
1569
1597
  name: isVi
1570
- ? '🟢 gemma4:e2b Nhẹ nhất (~4-6 GB RAM) Laptop / test nhanh Khuyên dùng'
1571
- : '🟢 gemma4:e2b Lightest (~4-6 GB RAM) Laptop / fastest test Recommended',
1598
+ ? '🟢 gemma4:e2b — Nhẹ nhất (~4-6 GB RAM) — Laptop / test nhanh ★ Khuyên dùng'
1599
+ : '🟢 gemma4:e2b — Lightest (~4-6 GB RAM) — Laptop / fastest test ★ Recommended',
1572
1600
  value: 'gemma4:e2b'
1573
1601
  },
1574
1602
  {
1575
1603
  name: isVi
1576
- ? '🟡 gemma4:e4b Cân bằng (~8-10 GB RAM) Dùng hằng ngày'
1577
- : '🟡 gemma4:e4b Balanced (~8-10 GB RAM) Daily use',
1604
+ ? '🟡 gemma4:e4b — Cân bằng (~8-10 GB RAM) — Dùng hằng ngày'
1605
+ : '🟡 gemma4:e4b — Balanced (~8-10 GB RAM) — Daily use',
1578
1606
  value: 'gemma4:e4b'
1579
1607
  },
1580
1608
  {
1581
1609
  name: isVi
1582
- ? '🟠 gemma4:26b Mạnh (~18-24 GB RAM/VRAM) Máy mạnh'
1583
- : '🟠 gemma4:26b Powerful (~18-24 GB RAM/VRAM) High-end machine',
1610
+ ? '🟠 gemma4:26b — Mạnh (~18-24 GB RAM/VRAM) — Máy mạnh'
1611
+ : '🟠 gemma4:26b — Powerful (~18-24 GB RAM/VRAM) — High-end machine',
1584
1612
  value: 'gemma4:26b'
1585
1613
  },
1586
1614
  {
1587
1615
  name: isVi
1588
- ? '🔴 gemma4:31b Mạnh nhất (~24+ GB RAM/VRAM) GPU workstation'
1589
- : '🔴 gemma4:31b Most powerful (~24+ GB RAM/VRAM) GPU workstation',
1616
+ ? '🔴 gemma4:31b — Mạnh nhất (~24+ GB RAM/VRAM) — GPU workstation'
1617
+ : '🔴 gemma4:31b — Most powerful (~24+ GB RAM/VRAM) — GPU workstation',
1590
1618
  value: 'gemma4:31b'
1591
1619
  },
1592
1620
  ],
@@ -1622,7 +1650,7 @@ async function collectSkillsStep({
1622
1650
  existingSmtpPass = '',
1623
1651
  }) {
1624
1652
  const selectedSkills = await checkboxWithBack({
1625
- message: isVi ? 'Bật tính năng bổ sung (Space để chọn):' : 'Enable extra skills (Space to select):',
1653
+ message: isVi ? 'Bật tính năng bổ sung (Space để chọn):' : 'Enable extra skills (Space to select):',
1626
1654
  choices: getCliSkillChoices({ providerKey, isVi }).map((choice) => ({
1627
1655
  ...choice,
1628
1656
  checked: existingSelectedSkills.includes(choice.value),
@@ -1638,18 +1666,18 @@ async function collectSkillsStep({
1638
1666
  if (selectedSkills.includes('browser')) {
1639
1667
  const isLinux = process.platform === 'linux';
1640
1668
  const browserValue = await selectWithBack({
1641
- message: isVi ? 'Chế độ Browser Automation:' : 'Browser Automation mode:',
1669
+ message: isVi ? 'Chế độ Browser Automation:' : 'Browser Automation mode:',
1642
1670
  choices: [
1643
1671
  {
1644
1672
  name: isVi
1645
- ? '🖥️ Dùng Chrome trên máy tính (Windows/Mac Bypass Cloudflare tốt hơn)'
1646
- : '🖥️ Use Host Chrome (Windows/Mac Better Cloudflare bypass)',
1673
+ ? '🖥️ Dùng Chrome trên máy tính (Windows/Mac — Bypass Cloudflare tốt hơn)'
1674
+ : '🖥️ Use Host Chrome (Windows/Mac — Better Cloudflare bypass)',
1647
1675
  value: 'desktop'
1648
1676
  },
1649
1677
  {
1650
1678
  name: isVi
1651
- ? '🐳 Headless Chromium trong Docker (Ubuntu Server / VPS không cần GUI)'
1652
- : '🐳 Headless Chromium inside Docker (Ubuntu Server / VPS No GUI)',
1679
+ ? '🐳 Headless Chromium trong Docker (Ubuntu Server / VPS — không cần GUI)'
1680
+ : '🐳 Headless Chromium inside Docker (Ubuntu Server / VPS — No GUI)',
1653
1681
  value: 'server'
1654
1682
  }
1655
1683
  ],
@@ -1669,7 +1697,7 @@ async function collectSkillsStep({
1669
1697
  let ttsElevenKey = existingTtsElevenKey;
1670
1698
  if (selectedSkills.includes('tts')) {
1671
1699
  const openaiKey = await inputWithBack({
1672
- 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):',
1700
+ 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):',
1673
1701
  defaultValue: ttsOpenaiKey,
1674
1702
  allowBack: true,
1675
1703
  isVi,
@@ -1680,7 +1708,7 @@ async function collectSkillsStep({
1680
1708
  ttsOpenaiKey = openaiKey;
1681
1709
 
1682
1710
  const elevenKey = await inputWithBack({
1683
- message: isVi ? 'Nhập ELEVENLABS_API_KEY (hoặc bỏ trống):' : 'Enter ELEVENLABS_API_KEY (or leave empty):',
1711
+ message: isVi ? 'Nhập ELEVENLABS_API_KEY (hoặc bỏ trống):' : 'Enter ELEVENLABS_API_KEY (or leave empty):',
1684
1712
  defaultValue: ttsElevenKey,
1685
1713
  allowBack: true,
1686
1714
  isVi,
@@ -1767,7 +1795,7 @@ async function collectSkillsStep({
1767
1795
  }
1768
1796
 
1769
1797
 
1770
- // ─── Shared workspace file writer ─────────────────────────────────────────────
1798
+ // ─── Shared workspace file writer ─────────────────────────────────────────────
1771
1799
  // Used by both native and docker flows to write .md + browser files consistently.
1772
1800
  async function writeWorkspaceFiles({
1773
1801
  workspaceDir, // absolute path to the workspace dir
@@ -1787,6 +1815,7 @@ async function writeWorkspaceFiles({
1787
1815
  agentWorkspaceDir = 'workspace',
1788
1816
  isRelayBot = false,
1789
1817
  replyToDirectMessages = true,
1818
+ channelKey = 'telegram',
1790
1819
  }) {
1791
1820
  const skillListStr = SKILLS
1792
1821
  .filter((s) => selectedSkills.includes(s.value))
@@ -1796,7 +1825,7 @@ async function writeWorkspaceFiles({
1796
1825
  ? `- ${label}${s.slug ? ` (${s.slug})` : ' (native)'}`
1797
1826
  : `- **${label}**${s.slug ? ` (${s.slug})` : ' (native)'}`;
1798
1827
  })
1799
- .join('\n') || (isVi ? '- _(Chưa skill nào)_' : '- _(No skills installed)_');
1828
+ .join('\n') || (isVi ? '- _(Chưa có skill nào)_' : '- _(No skills installed)_');
1800
1829
 
1801
1830
  const workspacePath = `.openclaw/${agentWorkspaceDir}/`;
1802
1831
  const teamRosterFormatted = teamRoster
@@ -1831,6 +1860,7 @@ async function writeWorkspaceFiles({
1831
1860
  includeBrowserTool: isDesktop,
1832
1861
  teamRosterFormatted,
1833
1862
  hasScheduler: selectedSkills.includes('scheduler'),
1863
+ hasZaloMod: channelKey === 'zalo-personal',
1834
1864
  });
1835
1865
 
1836
1866
  await fs.ensureDir(workspaceDir);
@@ -1887,9 +1917,9 @@ async function main() {
1887
1917
  while (true) {
1888
1918
  if (setupStep === 'language') {
1889
1919
  lang = await select({
1890
- message: 'Select language / Chọn ngôn ngữ:',
1920
+ message: 'Select language / Chọn ngôn ngữ:',
1891
1921
  choices: [
1892
- { name: 'Tiếng Việt', value: 'vi' },
1922
+ { name: 'Tiếng Việt', value: 'vi' },
1893
1923
  { name: 'English', value: 'en' }
1894
1924
  ],
1895
1925
  default: lang
@@ -1901,12 +1931,12 @@ async function main() {
1901
1931
 
1902
1932
  if (setupStep === 'os') {
1903
1933
  const nextOsChoice = await selectWithBack({
1904
- message: isVi ? 'Bạn đang chạy trên hệ điều hành nào?' : 'What OS are you running on?',
1934
+ message: isVi ? 'Bạn đang chạy trên hệ điều hành nào?' : 'What OS are you running on?',
1905
1935
  choices: [
1906
- { name: isVi ? '🪟 Windows' : '🪟 Windows', value: 'windows' },
1907
- { name: isVi ? '🍎 macOS' : '🍎 macOS', value: 'macos' },
1908
- { name: isVi ? '🐧 Ubuntu Desktop' : '🐧 Ubuntu Desktop', value: 'ubuntu' },
1909
- { name: isVi ? '🖥️ VPS / Ubuntu Server' : '🖥️ VPS / Ubuntu Server', value: 'vps' },
1936
+ { name: isVi ? '🪟 Windows' : '🪟 Windows', value: 'windows' },
1937
+ { name: isVi ? '🍎 macOS' : '🍎 macOS', value: 'macos' },
1938
+ { name: isVi ? '🐧 Ubuntu Desktop' : '🐧 Ubuntu Desktop', value: 'ubuntu' },
1939
+ { name: isVi ? '🖥️ VPS / Ubuntu Server' : '🖥️ VPS / Ubuntu Server', value: 'vps' },
1910
1940
  ],
1911
1941
  defaultValue: osChoice,
1912
1942
  allowBack: true,
@@ -1924,10 +1954,10 @@ async function main() {
1924
1954
  if (setupStep === 'deploy') {
1925
1955
  const deployModeDefault = (osChoice === 'ubuntu' || osChoice === 'vps') ? 'native' : 'docker';
1926
1956
  const nextDeployMode = await selectWithBack({
1927
- message: isVi ? 'Chọn cách chạy bot:' : 'How do you want to run the bot?',
1957
+ message: isVi ? 'Chọn cách chạy bot:' : 'How do you want to run the bot?',
1928
1958
  choices: [
1929
- { 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' },
1930
- { 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' }
1959
+ { 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' },
1960
+ { 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' }
1931
1961
  ],
1932
1962
  defaultValue: deployMode || deployModeDefault,
1933
1963
  allowBack: true,
@@ -1939,24 +1969,24 @@ async function main() {
1939
1969
  }
1940
1970
  deployMode = nextDeployMode;
1941
1971
  if (deployMode === 'docker' && !isDockerInstalled()) {
1942
- 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...'));
1972
+ 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...'));
1943
1973
  try {
1944
1974
  const platform = process.platform;
1945
1975
  if (platform === 'win32') {
1946
1976
  execSync('winget install -e --id Docker.DockerDesktop --accept-source-agreements --accept-package-agreements', { stdio: 'inherit' });
1947
- 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.'));
1977
+ 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.'));
1948
1978
  process.exit(0);
1949
1979
  } else if (platform === 'darwin') {
1950
1980
  execSync('brew install --cask docker', { stdio: 'inherit' });
1951
- 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.'));
1981
+ 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.'));
1952
1982
  process.exit(0);
1953
1983
  } else {
1954
1984
  execSync('curl -fsSL https://get.docker.com | sh', { stdio: 'inherit', shell: true });
1955
1985
  try { execSync('apt-get install -y docker-compose-plugin', { stdio: 'ignore', shell: true }); } catch {}
1956
- console.log(chalk.green(isVi ? ' Docker Engine + Compose plugin đã cài xong.' : ' Docker Engine + Compose plugin installed.'));
1986
+ console.log(chalk.green(isVi ? '✅ Docker Engine + Compose plugin đã cài xong.' : '✅ Docker Engine + Compose plugin installed.'));
1957
1987
  }
1958
1988
  } catch {
1959
- 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/'));
1989
+ 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/'));
1960
1990
  process.exit(1);
1961
1991
  }
1962
1992
  }
@@ -1966,7 +1996,7 @@ async function main() {
1966
1996
 
1967
1997
  if (setupStep === 'channel') {
1968
1998
  const nextChannelKey = await selectWithBack({
1969
- message: isVi ? 'Chọn nền tảng bot:' : 'Select bot platform:',
1999
+ message: isVi ? 'Chọn nền tảng bot:' : 'Select bot platform:',
1970
2000
  choices: Object.entries(CHANNELS).map(([k, v]) => ({ name: v.icon + ' ' + v.name, value: k })),
1971
2001
  defaultValue: channelKey,
1972
2002
  allowBack: true,
@@ -1979,7 +2009,7 @@ async function main() {
1979
2009
  channelKey = nextChannelKey;
1980
2010
  channel = CHANNELS[channelKey];
1981
2011
  if (channelKey === 'zalo-bot') {
1982
- 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 HTTPS). Hãy dùng Zalo Personal nếu bạn chưa Webhook.' : 'NOTE: Zalo OA requires a Public Webhook (via VPS/ngrok with HTTPS). Use Zalo Personal if you do not have one.')));
2012
+ 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.')));
1983
2013
  }
1984
2014
  setupStep = 'botSetup';
1985
2015
  continue;
@@ -2000,8 +2030,8 @@ async function main() {
2000
2030
  }
2001
2031
 
2002
2032
  if (setupStep === 'userInfo') {
2003
- console.log(chalk.bold('\n' + (isVi ? '👤 Thông tin của bạn 👤' : '👤 About You 👤')));
2004
- 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 });
2033
+ console.log(chalk.bold('\n' + (isVi ? '👤 Thông tin của bạn 👤' : '👤 About You 👤')));
2034
+ 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 });
2005
2035
  if (nextUserInfo === CLI_BACK) {
2006
2036
  setupStep = 'botSetup';
2007
2037
  continue;
@@ -2044,7 +2074,7 @@ async function main() {
2044
2074
  }
2045
2075
 
2046
2076
  if (setupStep === 'projectDir') {
2047
- const nextProjectDir = await inputWithBack({ message: isVi ? 'Thư mục cài đặt project:' : 'Project install directory:', defaultValue: projectDir, allowBack: true, isVi });
2077
+ const nextProjectDir = await inputWithBack({ message: isVi ? 'Thư mục cài đặt project:' : 'Project install directory:', defaultValue: projectDir, allowBack: true, isVi });
2048
2078
  if (nextProjectDir === CLI_BACK) {
2049
2079
  setupStep = 'skills';
2050
2080
  continue;
@@ -2062,12 +2092,12 @@ async function main() {
2062
2092
  const modelsPrimary = providerKey === 'ollama' ? selectedOllamaModel : providerKey === '9router' ? 'smart-route' : provider.models?.[0]?.id || 'gpt-4o-mini';
2063
2093
  const hasBrowserDesktop = selectedSkills.includes('browser') && browserMode === 'desktop';
2064
2094
  const hasBrowserServer = selectedSkills.includes('browser') && browserMode === 'server';
2065
- console.log(chalk.cyan(`\n🚀 ${isVi ? 'Đang tạo thư mục file cấu hình...' : 'Generating directories and configurations...'}`));
2095
+ console.log(chalk.cyan(`\n🚀 ${isVi ? 'Đang tạo thư mục và file cấu hình...' : 'Generating directories and configurations...'}`));
2066
2096
 
2067
2097
  await fs.ensureDir(projectDir);
2068
2098
 
2069
2099
 
2070
- // ─── Helper: build .env content per bot ──────────────────────────────────
2100
+ // ─── Helper: build .env content per bot ──────────────────────────────────
2071
2101
 
2072
2102
  function buildEnvContentForBot(botIndex) {
2073
2103
  const tok = bots[botIndex]?.token || botToken;
@@ -2108,7 +2138,7 @@ async function main() {
2108
2138
  });
2109
2139
  }
2110
2140
 
2111
- // ─── Create directories and write .env files ─────────────────────────────
2141
+ // ─── Create directories and write .env files ─────────────────────────────
2112
2142
  if (isMultiBot) {
2113
2143
  await fs.ensureDir(path.join(projectDir, '.openclaw'));
2114
2144
  if (deployMode === 'docker') {
@@ -2127,16 +2157,19 @@ async function main() {
2127
2157
  }
2128
2158
 
2129
2159
 
2130
- // ── Docker artifacts: Dockerfile + docker-compose via shared buildDockerArtifacts() ──────
2131
- const skillSlugs = SKILLS
2132
- .filter(s => selectedSkills.includes(s.value) && s.slug)
2133
- .map(s => s.slug);
2134
- const skillInstallCmd = skillSlugs.length > 0
2135
- ? skillSlugs.map(s => `openclaw skills install ${s} 2>/dev/null || true`).join(' && ')
2136
- : '';
2160
+ // ── Docker artifacts: Dockerfile + docker-compose via shared buildDockerArtifacts() ──────
2161
+ const skillSlugs = SKILLS
2162
+ .filter(s => selectedSkills.includes(s.value) && s.slug)
2163
+ .map(s => s.slug);
2164
+ const skillInstallCmd = skillSlugs.length > 0
2165
+ ? skillSlugs.map(s => `ensure_skill ${s}`).join('\n')
2166
+ : '';
2137
2167
  const relayInstallCmd = (isMultiBot && channelKey === 'telegram')
2138
2168
  ? buildRelayPluginInstallCommand('openclaw')
2139
2169
  : '';
2170
+ const zaloModInstallCmd = hasZaloPersonal(channelKey)
2171
+ ? 'ensure_plugin zalo-mod openclaw-zalo-mod'
2172
+ : '';
2140
2173
  const socatBridge = hasBrowserDesktop ? 'socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 &' : '';
2141
2174
  const deviceApproveLoop = 'while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done >/dev/null 2>&1 &';
2142
2175
 
@@ -2151,11 +2184,12 @@ async function main() {
2151
2184
  hasBrowser: hasBrowserDesktop || hasBrowserServer,
2152
2185
  selectedModel: modelsPrimary,
2153
2186
  agentId,
2154
- runtimeCommandParts: [
2155
- skillInstallCmd ? skillInstallCmd + ' &&' : '',
2156
- relayInstallCmd ? relayInstallCmd + ' &&' : '',
2157
- socatBridge,
2158
- deviceApproveLoop,
2187
+ runtimeCommandParts: [
2188
+ skillInstallCmd,
2189
+ relayInstallCmd,
2190
+ zaloModInstallCmd,
2191
+ socatBridge,
2192
+ deviceApproveLoop,
2159
2193
  ].filter(Boolean),
2160
2194
  volumeMount: '../../.openclaw:/root/project/.openclaw',
2161
2195
  singleComposeName: `oc-${agentId}`,
@@ -2276,7 +2310,7 @@ async function main() {
2276
2310
  );
2277
2311
  // Generate ecosystem.config.js for PM2 native multi-bot
2278
2312
  if (deployMode === 'native') {
2279
- // Also write config to ~/.openclaw/ openclaw binary on Linux/Mac reads from home dir
2313
+ // Also write config to ~/.openclaw/ — openclaw binary on Linux/Mac reads from home dir
2280
2314
  const homeClawDir = path.join(os.homedir(), '.openclaw');
2281
2315
  await fs.ensureDir(homeClawDir);
2282
2316
  const homeConfig = JSON.parse(JSON.stringify(sharedConfig));
@@ -2307,7 +2341,7 @@ async function main() {
2307
2341
  ' }',
2308
2342
  ].join('\n');
2309
2343
  const ecosystemContent = [
2310
- '// PM2 ecosystem run: pm2 start ecosystem.config.js',
2344
+ '// PM2 ecosystem — run: pm2 start ecosystem.config.js',
2311
2345
  'module.exports = {',
2312
2346
  ' apps: [',
2313
2347
  pm2Apps,
@@ -2338,12 +2372,12 @@ async function main() {
2338
2372
  .map((peer) => ({ name: peer.name, agentId: peer.agentId }));
2339
2373
 
2340
2374
  // agentYaml & auth still needed, keep non-workspace writes here
2341
- // .yaml removed OpenClaw reads config exclusively from openclaw.json
2375
+ // .yaml removed — OpenClaw reads config exclusively from openclaw.json
2342
2376
  if (Object.keys(authProfilesJson).length > 0) {
2343
2377
  await fs.writeJson(path.join(rootClawDir, 'agents', meta.agentId, 'agent', 'auth-profiles.json'), authProfilesJson, { spaces: 2 });
2344
2378
  }
2345
2379
 
2346
- // ── Workspace files via shared helper ────────────────────────────────
2380
+ // ── Workspace files via shared helper ────────────────────────────────
2347
2381
  await writeWorkspaceFiles({
2348
2382
  workspaceDir,
2349
2383
  isVi, botName: meta.name, botDesc: meta.desc, persona: meta.persona,
@@ -2354,6 +2388,7 @@ async function main() {
2354
2388
  agentWorkspaceDir: meta.workspaceDir,
2355
2389
  isRelayBot: true,
2356
2390
  replyToDirectMessages: true,
2391
+ channelKey,
2357
2392
  });
2358
2393
  }
2359
2394
  } else {
@@ -2410,7 +2445,7 @@ async function main() {
2410
2445
 
2411
2446
  await fs.writeJson(path.join(loopBotDir, '.openclaw', 'openclaw.json'), botConfig, { spaces: 2 });
2412
2447
 
2413
- // ── Workspace files: use shared writeWorkspaceFiles() ──────────────────────
2448
+ // ── Workspace files: use shared writeWorkspaceFiles() ──────────────────────
2414
2449
  const dockerWorkspaceDir = path.join(loopBotDir, '.openclaw', loopWorkspaceDir);
2415
2450
  const dockerOwnAliases = [loopBotName, bots[bIndex]?.slashCmd || '', `bot ${bIndex + 1}`].filter(Boolean);
2416
2451
  const dockerOtherAgents = teamRoster
@@ -2435,23 +2470,24 @@ async function main() {
2435
2470
  agentWorkspaceDir: loopWorkspaceDir,
2436
2471
  isRelayBot: isMultiBot,
2437
2472
  replyToDirectMessages: true,
2473
+ channelKey,
2438
2474
  });
2439
2475
 
2440
2476
  if (isMultiBot) {
2441
2477
  // Append per-bot reply rules to AGENTS.md
2442
2478
  const otherBotNames = teamRoster.filter((p) => p.idx !== bIndex).map((p) => p.name);
2443
2479
  const extraAgentsMd = isVi
2444
- ? `\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 tool/action reaction native thì ưu tiên dùng để thả \`👍\` trước, trừ khi \`👍\` đã 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.`
2445
- : `\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.`;
2480
+ ? `\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.`
2481
+ : `\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.`;
2446
2482
  await fs.appendFile(path.join(dockerWorkspaceDir, 'AGENTS.md'), extraAgentsMd);
2447
2483
  }
2448
2484
  } // END FOR LOOP
2449
2485
  }
2450
2486
 
2451
- // ── Chrome Debug scripts via shared builder (same content as wizard ZIP)
2487
+ // ── Chrome Debug scripts — via shared builder (same content as wizard ZIP) ─
2452
2488
  await writeGeneratedArtifacts(projectDir, buildCliChromeDebugArtifacts());
2453
2489
 
2454
- // ── Uninstall scripts ───────────────────────────────────────────────────────
2490
+ // ── Uninstall scripts ───────────────────────────────────────────────────────
2455
2491
  await writeGeneratedArtifacts(projectDir, buildCliUninstallArtifacts({
2456
2492
  deployMode,
2457
2493
  osChoice: detectedOS,
@@ -2461,10 +2497,10 @@ async function main() {
2461
2497
  : botName,
2462
2498
  }));
2463
2499
 
2464
- // ── Upgrade scripts ─────────────────────────────────────────────────────────
2500
+ // ── Upgrade scripts ─────────────────────────────────────────────────────────
2465
2501
  await writeGeneratedArtifacts(projectDir, buildCliUpgradeArtifacts());
2466
2502
 
2467
- // ── start-bot.bat / start-bot.sh one-click restart scripts ─────────────
2503
+ // ── start-bot.bat / start-bot.sh — one-click restart scripts ─────────────
2468
2504
  // Generated for native deployments only (docker has docker compose up)
2469
2505
  if (deployMode !== 'docker') {
2470
2506
  await writeGeneratedArtifacts(projectDir, buildCliStartBotArtifacts({
@@ -2480,31 +2516,31 @@ async function main() {
2480
2516
  console.log(chalk.cyan(
2481
2517
  process.platform === 'win32'
2482
2518
  ? (isVi
2483
- ? `\n🚀 start-bot.bat / start-bot.sh đã tạo double-click để restart bot.`
2484
- : `\n🚀 start-bot.bat / start-bot.sh created double-click to restart the bot.`)
2519
+ ? `\n🚀 start-bot.bat / start-bot.sh đã tạo — double-click để restart bot.`
2520
+ : `\n🚀 start-bot.bat / start-bot.sh created — double-click to restart the bot.`)
2485
2521
  : (isVi
2486
- ? `\n🚀 start-bot.sh đã tạo chạy ./start-bot.sh để restart bot.`
2487
- : `\n🚀 start-bot.sh created run ./start-bot.sh to restart the bot.`)
2522
+ ? `\n🚀 start-bot.sh đã tạo — chạy ./start-bot.sh để restart bot.`
2523
+ : `\n🚀 start-bot.sh created — run ./start-bot.sh to restart the bot.`)
2488
2524
  ));
2489
2525
  }
2490
2526
 
2491
- console.log(chalk.green(`✅ ${isVi ? 'Tạo cấu hình thành công!' : 'Configs created successfully!'}`));
2527
+ console.log(chalk.green(`✅ ${isVi ? 'Tạo cấu hình thành công!' : 'Configs created successfully!'}`));
2492
2528
 
2493
2529
  installLatestOpenClaw({ isVi, osChoice });
2494
2530
 
2495
2531
  // 7. Auto Run
2496
2532
  const autoRun = deployMode === 'docker' ? await confirm({
2497
- message: isVi ? 'Bạn muốn tự động build Docker khởi động Bot luôn không?' : 'Do you want to run Docker compose and start the bot now?',
2533
+ 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?',
2498
2534
  default: true
2499
2535
  }) : false;
2500
2536
 
2501
2537
  if (deployMode === 'docker' && autoRun) {
2502
- console.log(chalk.yellow(`\n🐳 ${isVi ? 'Đang khởi động Docker ( thể mất vài phút)...' : 'Starting Docker (might take a few minutes)...'}`));
2538
+ console.log(chalk.yellow(`\n🐳 ${isVi ? 'Đang khởi động Docker (có thể mất vài phút)...' : 'Starting Docker (might take a few minutes)...'}`));
2503
2539
  const dockerPath = path.join(projectDir, 'docker', 'openclaw');
2504
2540
 
2505
2541
  // Auto-detect Docker Compose V2 (plugin) vs V1 (standalone docker-compose).
2506
2542
  // On Ubuntu 24.04 installed via `apt install docker.io`, the Compose V2 plugin
2507
- // is NOT included `docker compose` subcommand may not exist or may be broken.
2543
+ // is NOT included — `docker compose` subcommand may not exist or may be broken.
2508
2544
  // We test both and use whichever actually works.
2509
2545
  let composeCmd, composeArgs;
2510
2546
  const detectCompose = () => {
@@ -2523,8 +2559,8 @@ async function main() {
2523
2559
  const detected = detectCompose();
2524
2560
  if (!detected) {
2525
2561
  console.log(chalk.red(isVi
2526
- ? '\n Không tìm thấy Docker Compose!\n Cài bằng lệnh: sudo apt-get install docker-compose-plugin'
2527
- : '\n Docker Compose not found!\n Install: sudo apt-get install docker-compose-plugin'));
2562
+ ? '\n❌ Không tìm thấy Docker Compose!\n Cài bằng lệnh: sudo apt-get install docker-compose-plugin'
2563
+ : '\n❌ Docker Compose not found!\n Install: sudo apt-get install docker-compose-plugin'));
2528
2564
  process.exit(1);
2529
2565
  }
2530
2566
  composeCmd = detected.cmd;
@@ -2537,87 +2573,87 @@ async function main() {
2537
2573
 
2538
2574
  child.on('close', (code) => {
2539
2575
  if (code === 0) {
2540
- console.log(chalk.green(`\n🎉 ${isVi ? 'Setup hoàn tất! Bot đang chạy.' : 'Setup complete! Bot is running.'}`));
2576
+ console.log(chalk.green(`\n🎉 ${isVi ? 'Setup hoàn tất! Bot đang chạy.' : 'Setup complete! Bot is running.'}`));
2541
2577
 
2542
2578
  if (providerKey === '9router') {
2543
- console.log(chalk.yellow(`\n🔀 ${isVi
2579
+ console.log(chalk.yellow(`\n🔀 ${isVi
2544
2580
  ? '9Router Dashboard: http://localhost:20128/dashboard'
2545
2581
  : '9Router Dashboard: http://localhost:20128/dashboard'}`));
2546
2582
  console.log(chalk.gray(isVi
2547
- ? ' Mở dashboard đăng nhập OAuth để kết nối các Provider (iFlow, Gemini CLI, Claude Code...)'
2548
- : ' Open dashboard OAuth login to connect Providers (iFlow, Gemini CLI, Claude Code...)'));
2583
+ ? ' → Mở dashboard → đăng nhập OAuth để kết nối các Provider (iFlow, Gemini CLI, Claude Code...)'
2584
+ : ' → Open dashboard → OAuth login to connect Providers (iFlow, Gemini CLI, Claude Code...)'));
2549
2585
  console.log(chalk.gray(isVi
2550
- ? ' Sau khi kết nối provider, bot sẽ tự động hoạt động qua combo "smart-route"'
2551
- : ' After connecting providers, bot works automatically via "smart-route" combo'));
2586
+ ? ' → Sau khi kết nối provider, bot sẽ tự động hoạt động qua combo "smart-route"'
2587
+ : ' → After connecting providers, bot works automatically via "smart-route" combo'));
2552
2588
  }
2553
2589
 
2554
2590
  if (channelKey === 'telegram') {
2555
- console.log(chalk.cyan(`\n💬 ${isVi
2556
- ? 'Nhắn tin cho bot trên Telegram dùng được ngay!'
2591
+ console.log(chalk.cyan(`\n💬 ${isVi
2592
+ ? 'Nhắn tin cho bot trên Telegram là dùng được ngay!'
2557
2593
  : 'Just message your bot on Telegram to start chatting!'}`));
2558
2594
  if (isMultiBot) {
2559
- console.log(chalk.yellow(`\n${isVi ? '📋 Đọc hướng dẫn setup bot trong Group:' : '📋 Read setup guide in Group:'} ${TELEGRAM_SETUP_GUIDE_FILENAME}`));
2595
+ console.log(chalk.yellow(`\n${isVi ? '📋 Đọc hướng dẫn setup bot trong Group:' : '📋 Read setup guide in Group:'} ${TELEGRAM_SETUP_GUIDE_FILENAME}`));
2560
2596
  console.log(chalk.gray(isVi
2561
- ? ' Chạy scripts/telegram-post-install-check.mjs để lấy link thật, kiểm tra group/privacy, rồi mới add bot Disable privacy mode.'
2562
- : ' Run scripts/telegram-post-install-check.mjs to get the real links, verify group/privacy, then add the bots and disable privacy mode.'));
2597
+ ? ' → Chạy scripts/telegram-post-install-check.mjs để lấy link thật, kiểm tra group/privacy, rồi mới add bot và Disable privacy mode.'
2598
+ : ' → Run scripts/telegram-post-install-check.mjs to get the real links, verify group/privacy, then add the bots and disable privacy mode.'));
2563
2599
  }
2564
2600
  } else if (channelKey === 'zalo-personal') {
2565
2601
  printZaloPersonalLoginInfo({ isVi, deployMode: 'docker', projectDir });
2566
2602
  }
2567
2603
  } else {
2568
- console.log(chalk.red(`\n Docker exited with code ${code}`));
2604
+ console.log(chalk.red(`\n❌ Docker exited with code ${code}`));
2569
2605
  console.log(chalk.yellow(isVi
2570
- ? `\n💡 Nếu lỗi "unknown shorthand flag", chạy: sudo apt-get install docker-compose-plugin\n Rồi thử lại: cd ${dockerPath} && docker compose up -d --build`
2571
- : `\n💡 If "unknown shorthand flag" error, run: sudo apt-get install docker-compose-plugin\n Then retry: cd ${dockerPath} && docker compose up -d --build`));
2606
+ ? `\n💡 Nếu lỗi "unknown shorthand flag", chạy: sudo apt-get install docker-compose-plugin\n Rồi thử lại: cd ${dockerPath} && docker compose up -d --build`
2607
+ : `\n💡 If "unknown shorthand flag" error, run: sudo apt-get install docker-compose-plugin\n Then retry: cd ${dockerPath} && docker compose up -d --build`));
2572
2608
  }
2573
2609
  });
2574
2610
 
2575
2611
  }
2576
2612
  if (deployMode === 'docker') {
2577
2613
 
2578
- // ── Auto-install openclaw binary if not present ──────────────────────────
2614
+ // ── Auto-install openclaw binary if not present ──────────────────────────
2579
2615
  const isOpenClawInstalled = () => { try { execSync('openclaw --version', { stdio: 'ignore' }); return true; } catch { return false; } };
2580
2616
  if (!isOpenClawInstalled()) {
2581
2617
  console.log(chalk.cyan(isVi
2582
- ? `\n📦 Đang cài openclaw binary (${OPENCLAW_NPM_SPEC})...`
2583
- : `\n📦 Installing openclaw binary (${OPENCLAW_NPM_SPEC})...`));
2618
+ ? `\n📦 Đang cài openclaw binary (${OPENCLAW_NPM_SPEC})...`
2619
+ : `\n📦 Installing openclaw binary (${OPENCLAW_NPM_SPEC})...`));
2584
2620
  try {
2585
2621
  execSync(`npm install -g ${OPENCLAW_NPM_SPEC}`, { stdio: 'inherit' });
2586
- console.log(chalk.green(isVi ? ' openclaw đã cài xong!' : ' openclaw installed!'));
2622
+ console.log(chalk.green(isVi ? '✅ openclaw đã cài xong!' : '✅ openclaw installed!'));
2587
2623
  } catch {
2588
2624
  console.log(chalk.yellow(isVi
2589
- ? `⚠️ Không tự cài được. Chạy thủ công: sudo npm install -g ${OPENCLAW_NPM_SPEC}`
2590
- : `⚠️ Could not auto-install. Run manually: sudo npm install -g ${OPENCLAW_NPM_SPEC}`));
2625
+ ? `⚠️ Không tự cài được. Chạy thủ công: sudo npm install -g ${OPENCLAW_NPM_SPEC}`
2626
+ : `⚠️ Could not auto-install. Run manually: sudo npm install -g ${OPENCLAW_NPM_SPEC}`));
2591
2627
  }
2592
2628
  }
2593
2629
 
2594
2630
  if (isMultiBot && channelKey === 'telegram') {
2595
- console.log(chalk.yellow(`\n${isVi ? '📋 Xem hướng dẫn sau cài:' : '📋 Read post-install guide:'} ${path.join(projectDir, TELEGRAM_SETUP_GUIDE_FILENAME)}`));
2631
+ console.log(chalk.yellow(`\n${isVi ? '📋 Xem hướng dẫn sau cài:' : '📋 Read post-install guide:'} ${path.join(projectDir, TELEGRAM_SETUP_GUIDE_FILENAME)}`));
2596
2632
  }
2597
2633
  } else {
2598
2634
  if (!isOpenClawInstalled()) {
2599
2635
  console.log(chalk.cyan(isVi
2600
- ? `\n📦 Dang cai openclaw binary (${OPENCLAW_NPM_SPEC})...`
2601
- : `\n📦 Installing openclaw binary (${OPENCLAW_NPM_SPEC})...`));
2636
+ ? `\n📦 Dang cai openclaw binary (${OPENCLAW_NPM_SPEC})...`
2637
+ : `\n📦 Installing openclaw binary (${OPENCLAW_NPM_SPEC})...`));
2602
2638
  if (!installGlobalPackage(OPENCLAW_NPM_SPEC, { isVi, osChoice, displayName: 'openclaw' })) {
2603
2639
  process.exit(1);
2604
2640
  }
2605
- console.log(chalk.green(isVi ? ' openclaw da cai xong!' : ' openclaw installed!'));
2641
+ console.log(chalk.green(isVi ? '✅ openclaw da cai xong!' : '✅ openclaw installed!'));
2606
2642
  }
2607
2643
 
2608
2644
  if (providerKey === '9router') {
2609
2645
  if (shouldReuseInstalledGlobals() && is9RouterInstalled()) {
2610
2646
  console.log(chalk.green(isVi
2611
- ? '\n♻️ Dang dung lai 9Router da cai san de test nhanh.'
2612
- : '\n♻️ Reusing the installed 9Router for a faster test run.'));
2647
+ ? '\n♻️ Dang dung lai 9Router da cai san de test nhanh.'
2648
+ : '\n♻️ Reusing the installed 9Router for a faster test run.'));
2613
2649
  } else if (!is9RouterInstalled()) {
2614
2650
  console.log(chalk.cyan(isVi
2615
- ? '\n📦 Dang cai 9Router binary (npm install -g 9router)...'
2616
- : '\n📦 Installing 9Router binary (npm install -g 9router)...'));
2651
+ ? '\n📦 Dang cai 9Router binary (npm install -g 9router)...'
2652
+ : '\n📦 Installing 9Router binary (npm install -g 9router)...'));
2617
2653
  if (!installGlobalPackage('9router@latest', { isVi, osChoice, displayName: '9Router' })) {
2618
2654
  process.exit(1);
2619
2655
  }
2620
- console.log(chalk.green(isVi ? ' 9Router da cai xong!' : ' 9Router installed!'));
2656
+ console.log(chalk.green(isVi ? '✅ 9Router da cai xong!' : '✅ 9Router installed!'));
2621
2657
  }
2622
2658
  }
2623
2659
 
@@ -2643,7 +2679,7 @@ async function main() {
2643
2679
 
2644
2680
  if (osChoice === 'vps') {
2645
2681
  if (!isPm2Installed()) {
2646
- console.log(chalk.cyan(isVi ? '\n📦 Dang cai PM2...' : '\n📦 Installing PM2...'));
2682
+ console.log(chalk.cyan(isVi ? '\n📦 Dang cai PM2...' : '\n📦 Installing PM2...'));
2647
2683
  if (!installGlobalPackage('pm2@latest', { isVi, osChoice, displayName: 'PM2' })) {
2648
2684
  process.exit(1);
2649
2685
  }
@@ -2658,7 +2694,7 @@ async function main() {
2658
2694
  stdio: 'inherit',
2659
2695
  shell: true
2660
2696
  });
2661
- 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.'}`));
2697
+ 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.'}`));
2662
2698
  console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${getNativePm2AppName(true)}` : ` View logs: pm2 logs ${getNativePm2AppName(true)}`));
2663
2699
  printNativeDashboardAccessInfo({ isVi, providerKey, projectDir });
2664
2700
  if (channelKey === 'zalo-personal') {
@@ -2688,7 +2724,7 @@ async function main() {
2688
2724
  env: getProjectRuntimeEnv(projectDir)
2689
2725
  });
2690
2726
  runPm2Save({ projectDir, isVi });
2691
- console.log(chalk.green(`\n🎉 ${isVi ? 'Setup hoan tat! Bot native dang chay qua PM2.' : 'Setup complete! Native bot is running via PM2.'}`));
2727
+ console.log(chalk.green(`\n🎉 ${isVi ? 'Setup hoan tat! Bot native dang chay qua PM2.' : 'Setup complete! Native bot is running via PM2.'}`));
2692
2728
  console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${appName}` : ` View logs: pm2 logs ${appName}`));
2693
2729
  printNativeDashboardAccessInfo({ isVi, providerKey, projectDir });
2694
2730
  if (channelKey === 'zalo-personal') {
@@ -2714,8 +2750,8 @@ async function main() {
2714
2750
  : ' 9Router dashboard: http://localhost:20128/dashboard'));
2715
2751
  if (!routerHealth.ok) {
2716
2752
  console.log(chalk.yellow(isVi
2717
- ? ` ⚠️ 9Router da mo cong 20128 nhung admin API chua san sang. Kiem tra them: ${routerHealth.url}`
2718
- : ` ⚠️ 9Router opened port 20128 but the admin API is not ready yet. Check: ${routerHealth.url}`));
2753
+ ? ` ⚠️ 9Router da mo cong 20128 nhung admin API chua san sang. Kiem tra them: ${routerHealth.url}`
2754
+ : ` ⚠️ 9Router opened port 20128 but the admin API is not ready yet. Check: ${routerHealth.url}`));
2719
2755
  }
2720
2756
  }
2721
2757
  if (hasZaloPersonal(channelKey)) {
@@ -2747,9 +2783,9 @@ async function main() {
2747
2783
  return;
2748
2784
  }
2749
2785
 
2750
- console.log(chalk.cyan(`\n👉 ${isVi ? 'Native runtime da duoc cai san va khoi dong.' : 'Native runtime is installed and started.'}`));
2786
+ console.log(chalk.cyan(`\n👉 ${isVi ? 'Native runtime da duoc cai san va khoi dong.' : 'Native runtime is installed and started.'}`));
2751
2787
  if (isMultiBot && channelKey === 'telegram') {
2752
- console.log(chalk.yellow(`\n📋 ${isVi ? 'Xem huong dan sau cai:' : 'Read post-install guide:'} ${path.join(projectDir, TELEGRAM_SETUP_GUIDE_FILENAME)}`));
2788
+ console.log(chalk.yellow(`\n📋 ${isVi ? 'Xem huong dan sau cai:' : 'Read post-install guide:'} ${path.join(projectDir, TELEGRAM_SETUP_GUIDE_FILENAME)}`));
2753
2789
  }
2754
2790
  }
2755
2791
  }