create-openclaw-bot 5.6.8 → 5.6.11

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
@@ -23,14 +23,21 @@ function loadSharedModule(modulePath, globalName) {
23
23
  const {
24
24
  OPENCLAW_NPM_SPEC,
25
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,
26
30
  TELEGRAM_RELAY_PLUGIN_SPEC,
27
31
  TELEGRAM_SETUP_GUIDE_FILENAME,
28
32
  buildRelayPluginInstallCommand,
29
33
  buildTelegramPostInstallChecklist,
34
+ get9RouterBaseUrl,
35
+ build9RouterProviderConfig,
30
36
  } = loadSharedModule('./setup/shared/common-gen.js', '__openclawCommon');
31
37
 
32
38
  const {
33
39
  buildDockerArtifacts,
40
+ build9RouterPatchScript,
34
41
  } = loadSharedModule('./setup/shared/docker-gen.js', '__openclawDockerGen');
35
42
 
36
43
  const {
@@ -193,48 +200,31 @@ function resolveNative9RouterDesktopLaunch() {
193
200
  };
194
201
  }
195
202
 
196
- function build9RouterSmartRouteSyncScript(dbPath) {
197
- const safeDbPath = JSON.stringify(dbPath);
198
- return `function bootstrap() {
199
- const fs = require('fs');
200
- const path = require('path');
201
- const dbPath = ${safeDbPath};
202
- const ROUTER='http://localhost:20128';
203
- const MODEL_PRIORITY = {
204
- codex: ['cx/gpt-5.4', 'cx/gpt-5.3-codex', 'cx/gpt-5.3-codex-high', 'cx/gpt-5.2-codex', 'cx/gpt-5.2', 'cx/gpt-5.1-codex-max', 'cx/gpt-5.1-codex', 'cx/gpt-5.1', 'cx/gpt-5-codex'],
205
- 'claude-code': ['cc/claude-opus-4-6', 'cc/claude-sonnet-4-6', 'cc/claude-opus-4-5-20251101', 'cc/claude-sonnet-4-5-20250929', 'cc/claude-haiku-4-5-20251001'],
206
- github: ['gh/gpt-5.4', 'gh/gpt-5.3-codex', 'gh/gpt-5.2-codex', 'gh/gpt-5.2', 'gh/gpt-5.1-codex-max', 'gh/gpt-5.1-codex', 'gh/gpt-5.1', 'gh/gpt-5', 'gh/gpt-4.1', 'gh/gpt-4o', 'gh/claude-opus-4.6', 'gh/claude-sonnet-4.6', 'gh/claude-sonnet-4.5', 'gh/claude-opus-4.5', 'gh/claude-haiku-4.5', 'gh/gemini-3-pro-preview', 'gh/gemini-3-flash-preview', 'gh/gemini-2.5-pro'],
207
- cursor: ['cu/default', 'cu/claude-4.6-opus-max', 'cu/claude-4.5-opus-high-thinking', 'cu/claude-4.5-sonnet-thinking', 'cu/claude-4.5-sonnet', 'cu/gpt-5.3-codex', 'cu/gpt-5.2-codex', 'cu/gemini-3-flash-preview'],
208
- kilo: ['kc/anthropic/claude-sonnet-4-20250514', 'kc/anthropic/claude-opus-4-20250514', 'kc/google/gemini-2.5-pro', 'kc/google/gemini-2.5-flash', 'kc/openai/gpt-4.1', 'kc/deepseek/deepseek-chat'],
209
- cline: ['cl/anthropic/claude-sonnet-4.6', 'cl/anthropic/claude-opus-4.6', 'cl/openai/gpt-5.3-codex', 'cl/openai/gpt-5.4', 'cl/google/gemini-3.1-pro-preview'],
210
- 'gemini-cli': ['gc/gemini-3-flash-preview', 'gc/gemini-3-pro-preview'],
211
- iflow: ['if/qwen3-coder-plus', 'if/kimi-k2', 'if/kimi-k2-thinking', 'if/glm-4.7', 'if/deepseek-r1', 'if/deepseek-v3.2', 'if/deepseek-v3', 'if/qwen3-max', 'if/qwen3-235b', 'if/iflow-rome-30ba3b'],
212
- qwen: ['qw/qwen3-coder-plus', 'qw/qwen3-coder-flash', 'qw/vision-model', 'qw/coder-model'],
213
- kiro: ['kr/claude-sonnet-4.5', 'kr/claude-haiku-4.5', 'kr/deepseek-3.2', 'kr/deepseek-3.1', 'kr/qwen3-coder-next'],
214
- ollama: ['ollama/gemma4:e2b', 'ollama/gemma4:e4b', 'ollama/gemma4:26b', 'ollama/gemma4:31b', 'ollama/qwen3.5', 'ollama/kimi-k2.5', 'ollama/glm-5', 'ollama/glm-4.7-flash', 'ollama/minimax-m2.5', 'ollama/gpt-oss:120b'],
215
- 'kimi-coding': ['kmc/kimi-k2.5', 'kmc/kimi-k2.5-thinking', 'kmc/kimi-latest'],
216
- glm: ['glm/glm-5.1', 'glm/glm-5', 'glm/glm-4.7'],
217
- 'glm-cn': ['glm/glm-5.1', 'glm/glm-5', 'glm/glm-4.7'],
218
- minimax: ['minimax/MiniMax-M2.7', 'minimax/MiniMax-M2.5', 'minimax/MiniMax-M2.1'],
219
- kimi: ['kimi/kimi-k2.5', 'kimi/kimi-k2.5-thinking', 'kimi/kimi-latest'],
220
- deepseek: ['deepseek/deepseek-chat', 'deepseek/deepseek-reasoner'],
221
- xai: ['xai/grok-4', 'xai/grok-4-fast-reasoning', 'xai/grok-code-fast-1'],
222
- mistral: ['mistral/mistral-large-latest', 'mistral/codestral-latest'],
223
- groq: ['groq/llama-3.3-70b-versatile', 'groq/openai/gpt-oss-120b'],
224
- cerebras: ['cerebras/gpt-oss-120b'],
225
- alicode: ['alicode/qwen3.5-plus', 'alicode/qwen3-coder-plus'],
226
- openai: ['openai/gpt-4o', 'openai/gpt-4.1'],
227
- anthropic: ['anthropic/claude-sonnet-4', 'anthropic/claude-haiku-3.5'],
228
- gemini: ['gemini/gemini-2.5-flash', 'gemini/gemini-2.5-pro'],
229
- };
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};
230
215
  const sync = async () => {
231
216
  try {
232
217
  const response = await fetch(ROUTER + '/api/providers');
233
218
  if (!response.ok) return;
234
219
  const payload = await response.json();
235
- const a = (payload.connections || [])
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
236
226
  .filter((item) => item && item.provider && item.isActive !== false && !item.disabled)
237
- .map((item) => item.provider);
227
+ .map((item) => item.provider))];
238
228
  let db = {};
239
229
  try {
240
230
  db = JSON.parse(fs.readFileSync(dbPath, 'utf8'));
@@ -253,6 +243,7 @@ function build9RouterSmartRouteSyncScript(dbPath) {
253
243
  removeSmartRoute();
254
244
  return;
255
245
  }
246
+ a.sort((x, y) => (PREF.indexOf(x) === -1 ? 99 : PREF.indexOf(x)) - (PREF.indexOf(y) === -1 ? 99 : PREF.indexOf(y)));
256
247
  const m = a.flatMap((provider) => MODEL_PRIORITY[provider] || []);
257
248
  if (!m.length) {
258
249
  removeSmartRoute();
@@ -460,6 +451,60 @@ async function writeNative9RouterSyncScript(projectDir) {
460
451
  return syncScriptPath;
461
452
  }
462
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
+
463
508
  function getGatewayAllowedOrigins(port) {
464
509
  const normalizedPort = Number(port) || 18791;
465
510
  const origins = new Set([
@@ -809,7 +854,7 @@ function detectProjectBotName(projectDir) {
809
854
  return path.basename(projectDir);
810
855
  }
811
856
 
812
- function detectProjectUses9Router(projectDir) {
857
+ function detectProjectUses9Router(projectDir) {
813
858
  try {
814
859
  const configPath = path.join(projectDir, '.openclaw', 'openclaw.json');
815
860
  if (fs.existsSync(configPath)) {
@@ -821,25 +866,25 @@ function detectProjectUses9Router(projectDir) {
821
866
  } catch {
822
867
  // fallback below
823
868
  }
824
- return fs.existsSync(path.join(projectDir, '.9router'));
825
- }
826
-
827
- function detectProjectIsMultiBot(projectDir) {
828
- try {
829
- const configPath = path.join(projectDir, '.openclaw', 'openclaw.json');
830
- if (fs.existsSync(configPath)) {
831
- const config = fs.readJsonSync(configPath);
832
- return (config?.agents?.list?.length || 0) > 1;
833
- }
834
- } catch {
835
- // fallback below
836
- }
837
- return false;
838
- }
839
-
840
- function getNativePm2AppName(isMultiBot = false) {
841
- return isMultiBot ? 'openclaw-multibot' : 'openclaw';
842
- }
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
+ }
843
888
 
844
889
  async function runUpgradeCommand() {
845
890
  const projectDir = findProjectDir();
@@ -849,115 +894,133 @@ async function runUpgradeCommand() {
849
894
  process.exit(1);
850
895
  }
851
896
 
852
- const deployMode = detectProjectDeployMode(projectDir);
853
- const osChoice = getDetectedOsChoice();
854
- const botName = detectProjectBotName(projectDir);
855
- const is9Router = detectProjectUses9Router(projectDir);
856
- 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);
857
902
 
858
903
  console.log(chalk.cyan('\nRefreshing generated OpenClaw project artifacts...'));
859
904
  console.log(chalk.gray(` Project: ${projectDir}`));
860
905
  console.log(chalk.gray(` Mode: ${deployMode}`));
861
906
 
862
907
  await writeGeneratedArtifacts(projectDir, buildCliChromeDebugArtifacts());
863
- await writeGeneratedArtifacts(projectDir, buildCliUninstallArtifacts({
864
- deployMode,
865
- osChoice,
866
- projectDir,
867
- botName: (deployMode !== 'docker' && osChoice === 'vps')
868
- ? getNativePm2AppName(isMultiBot)
869
- : botName,
870
- }));
908
+ await writeGeneratedArtifacts(projectDir, buildCliUninstallArtifacts({
909
+ deployMode,
910
+ osChoice,
911
+ projectDir,
912
+ botName: (deployMode !== 'docker' && osChoice === 'vps')
913
+ ? getNativePm2AppName(isMultiBot)
914
+ : botName,
915
+ }));
871
916
  await writeGeneratedArtifacts(projectDir, buildCliUpgradeArtifacts());
872
917
 
873
- if (deployMode !== 'docker') {
874
- await writeGeneratedArtifacts(projectDir, buildCliStartBotArtifacts({
875
- projectDir,
876
- openclawHome: path.join(projectDir, '.openclaw'),
877
- is9Router,
878
- osChoice,
879
- isMultiBot,
880
- appName: getNativePm2AppName(isMultiBot),
881
- isVi: false,
882
- }));
883
- }
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
+ }
884
947
 
885
948
  console.log(chalk.green('\nUpgrade artifacts refreshed successfully.'));
886
949
  if (deployMode === 'docker') {
887
950
  console.log(chalk.white(` Next: cd ${path.join(projectDir, 'docker', 'openclaw')} && docker compose up -d --build`));
888
951
  } else {
889
- console.log(chalk.white(` Next: run ${process.platform === 'win32' ? '.\\start-bot.bat' : './start-bot.sh'} from ${projectDir}`));
890
- }
891
- }
892
-
893
- function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
894
- const routerAppName = `${appName}-9router`;
895
- const routerLaunch = resolveNative9RouterDesktopLaunch();
896
- const normalizedProjectDir = projectDir.replace(/\\/g, '/');
897
- const normalizedSyncScriptPath = syncScriptPath ? syncScriptPath.replace(/\\/g, '/') : '';
898
- try {
899
- execSync(`pm2 delete ${routerAppName}`, {
900
- cwd: projectDir,
901
- stdio: 'ignore',
902
- shell: true
903
- });
904
- } catch {
905
- // ignore missing app
906
- }
907
- execFileSync('pm2', [
908
- 'start',
909
- routerLaunch.command,
910
- '--name',
911
- routerAppName,
912
- '--cwd',
913
- normalizedProjectDir,
914
- '--interpreter',
915
- 'none',
916
- '--',
917
- ...routerLaunch.args
918
- ], {
919
- cwd: projectDir,
920
- stdio: 'inherit',
921
- env: { ...process.env, ...routerLaunch.env }
922
- });
923
- if (syncScriptPath) {
924
- const syncAppName = `${appName}-9router-sync`;
925
- try {
926
- execSync(`pm2 delete ${syncAppName}`, {
927
- cwd: projectDir,
928
- stdio: 'ignore',
929
- shell: true
930
- });
931
- } catch {
932
- // ignore missing app
933
- }
934
- try {
935
- execFileSync('pm2', [
936
- 'start',
937
- normalizedSyncScriptPath,
938
- '--name',
939
- syncAppName,
940
- '--cwd',
941
- normalizedProjectDir,
942
- '--interpreter',
943
- process.execPath,
944
- '--no-autorestart',
945
- ], {
946
- cwd: projectDir,
947
- stdio: 'inherit',
948
- env: process.env
949
- });
950
- } catch (syncErr) {
951
- console.log(chalk.yellow(isVi
952
- ? `\n⚠️ Khong the tu dong khoi dong sync script qua PM2.`
953
- : `\n⚠️ Could not auto-start 9router sync script via PM2.`));
954
- }
955
- }
956
- runPm2Save({ projectDir, isVi });
957
- console.log(chalk.green(`\n✅ ${isVi ? '9Router da duoc khoi dong qua PM2.' : '9Router is running via PM2.'}`));
958
- console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${routerAppName}` : ` View logs: pm2 logs ${routerAppName}`));
959
- }
960
-
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
+
961
1024
  async function ensureProjectRuntimeDirs(projectDir, isVi) {
962
1025
  await fs.ensureDir(path.join(projectDir, '.openclaw'));
963
1026
  await fs.ensureDir(getProject9RouterDataDir(projectDir));
@@ -1007,9 +1070,9 @@ function providerSupportsMemoryEmbeddings(providerKey) {
1007
1070
  return !!PROVIDERS[providerKey]?.supportsEmbeddings;
1008
1071
  }
1009
1072
 
1010
- function getCliSkillChoices({ providerKey, isVi }) {
1011
- const memoryRecommended = providerSupportsMemoryEmbeddings(providerKey);
1012
- return SKILLS
1073
+ function getCliSkillChoices({ providerKey, isVi }) {
1074
+ const memoryRecommended = providerSupportsMemoryEmbeddings(providerKey);
1075
+ return SKILLS
1013
1076
  .filter((skill) => skill.value !== 'memory' || providerSupportsMemoryEmbeddings(providerKey) || skill.id === 'memory')
1014
1077
  .map((skill) => {
1015
1078
  const value = skill.value || skill.id;
@@ -1024,677 +1087,677 @@ function getCliSkillChoices({ providerKey, isVi }) {
1024
1087
  value,
1025
1088
  checked: value === 'browser' || value === 'scheduler' || (value === 'memory' && memoryRecommended),
1026
1089
  };
1027
- });
1028
- }
1029
-
1030
- const CLI_BACK = '__openclaw_cli_back__';
1031
-
1032
- function getBackChoice(isVi) {
1033
- return {
1034
- name: isVi ? '← Quay lại' : '← Back',
1035
- value: CLI_BACK,
1036
- };
1037
- }
1038
-
1039
- function withBackHint(message, isVi) {
1040
- return `${message} ${isVi ? '(gõ "back" để quay lại)' : '(type "back" to go back)'}`;
1041
- }
1042
-
1043
- async function selectWithBack({ message, choices, defaultValue, allowBack = false, isVi = true }) {
1044
- const finalChoices = allowBack ? [...choices, getBackChoice(isVi)] : choices;
1045
- return select({
1046
- message,
1047
- choices: finalChoices,
1048
- ...(defaultValue ? { default: defaultValue } : {}),
1049
- });
1050
- }
1051
-
1052
- async function inputWithBack({ message, defaultValue = '', required = false, allowBack = false, isVi = true }) {
1053
- const value = await input({
1054
- message: allowBack ? withBackHint(message, isVi) : message,
1055
- default: defaultValue,
1056
- required,
1057
- });
1058
- if (allowBack && String(value || '').trim().toLowerCase() === 'back') {
1059
- return CLI_BACK;
1060
- }
1061
- return value;
1062
- }
1063
-
1064
- async function checkboxWithBack({ message, choices, isVi = true, allowBack = false }) {
1065
- const finalChoices = allowBack ? [...choices, getBackChoice(isVi)] : choices;
1066
- const value = await checkbox({
1067
- message,
1068
- choices: finalChoices,
1069
- });
1070
- return allowBack && value.includes(CLI_BACK) ? CLI_BACK : value;
1071
- }
1072
-
1073
- async function collectBotSetupStep({
1074
- isVi,
1075
- channelKey,
1076
- channel,
1077
- existingBots = [],
1078
- existingBotCount = 1,
1079
- existingGroupId = '',
1080
- }) {
1081
- let botCount = channelKey === 'telegram' ? existingBotCount : 1;
1082
- let groupId = existingGroupId;
1083
- const bots = [];
1084
-
1085
- if (channelKey === 'telegram') {
1086
- const botCountValue = await selectWithBack({
1087
- message: isVi ? 'Bạn muốn cài bao nhiêu Telegram bot?' : 'How many Telegram bots do you want to deploy?',
1088
- choices: [
1089
- { name: '1 bot (single)', value: '1' },
1090
- { name: '2 bots (Department Room)', value: '2' },
1091
- { name: '3 bots', value: '3' },
1092
- { name: '4 bots', value: '4' },
1093
- { name: '5 bots', value: '5' },
1094
- ],
1095
- defaultValue: String(existingBotCount || 1),
1096
- allowBack: true,
1097
- isVi,
1098
- });
1099
- if (botCountValue === CLI_BACK) {
1100
- return { back: true };
1101
- }
1102
- botCount = parseInt(botCountValue, 10);
1103
-
1104
- if (botCount > 1) {
1105
- const groupOption = await selectWithBack({
1106
- message: isVi ? 'Bạn có sẵn Telegram Group chưa?' : 'Do you already have a Telegram Group?',
1107
- choices: [
1108
- { 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' },
1109
- { name: isVi ? '🔗 Đã có group — nhập Group ID ngay' : '🔗 Already have a group — enter Group ID now', value: 'existing' }
1110
- ],
1111
- defaultValue: groupId ? 'existing' : 'create',
1112
- allowBack: true,
1113
- isVi,
1114
- });
1115
- if (groupOption === CLI_BACK) {
1116
- return { back: true };
1117
- }
1118
-
1119
- if (groupOption === 'existing') {
1120
- console.log(chalk.dim(isVi
1121
- ? '\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'
1122
- : '\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'));
1123
- const nextGroupId = await inputWithBack({
1124
- message: isVi ? 'Telegram Group ID (VD: -1001234567890):' : 'Telegram Group ID (e.g. -1001234567890):',
1125
- defaultValue: groupId,
1126
- allowBack: true,
1127
- isVi,
1128
- });
1129
- if (nextGroupId === CLI_BACK) {
1130
- return { back: true };
1131
- }
1132
- groupId = nextGroupId;
1133
- } else {
1134
- groupId = '';
1135
- }
1136
- } else {
1137
- groupId = '';
1138
- }
1139
-
1140
- for (let i = 0; i < botCount; i++) {
1141
- console.log(chalk.bold(`\n${isVi ? `─── Bot ${i + 1} / ${botCount} ───` : `─── Bot ${i + 1} / ${botCount} ───`}`));
1142
- const defaults = existingBots[i] || {};
1143
- const fields = [
1144
- {
1145
- key: 'name',
1146
- message: isVi ? `Tên Bot ${i + 1}:` : `Bot ${i + 1} name:`,
1147
- defaultValue: defaults.name || `Bot ${i + 1}`,
1148
- required: true,
1149
- },
1150
- {
1151
- key: 'slashCmd',
1152
- message: isVi ? `Slash command (VD: /bot${i + 1}):` : `Slash command (e.g. /bot${i + 1}):`,
1153
- defaultValue: defaults.slashCmd || `/bot${i + 1}`,
1154
- required: true,
1155
- },
1156
- {
1157
- key: 'desc',
1158
- message: isVi ? `Mô tả Bot ${i + 1} (VD: Trợ lý AI cá nhân):` : `Bot ${i + 1} description (e.g. Personal AI assistant):`,
1159
- defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1160
- required: true,
1161
- },
1162
- {
1163
- key: 'persona',
1164
- 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):`,
1165
- defaultValue: defaults.persona || '',
1166
- required: false,
1167
- },
1168
- {
1169
- key: 'token',
1170
- message: isVi ? 'Bot Token (từ @BotFather):' : 'Bot Token (from @BotFather):',
1171
- defaultValue: defaults.token || '',
1172
- required: true,
1173
- },
1174
- ];
1175
-
1176
- const draft = { ...defaults };
1177
- let fieldIndex = 0;
1178
- while (fieldIndex < fields.length) {
1179
- const field = fields[fieldIndex];
1180
- const value = await inputWithBack({
1181
- message: field.message,
1182
- defaultValue: draft[field.key] || field.defaultValue,
1183
- required: field.required,
1184
- allowBack: true,
1185
- isVi,
1186
- });
1187
- if (value === CLI_BACK) {
1188
- if (fieldIndex > 0) {
1189
- fieldIndex--;
1190
- continue;
1191
- }
1192
- return { back: true };
1193
- }
1194
- draft[field.key] = value;
1195
- fieldIndex++;
1196
- }
1197
- bots.push(draft);
1198
- }
1199
- } else if (channelKey !== 'zalo-personal') {
1200
- const defaults = existingBots[0] || {};
1201
- const fields = [
1202
- {
1203
- key: 'name',
1204
- message: isVi ? 'Tên Bot:' : 'Bot Name:',
1205
- defaultValue: defaults.name || 'Chat Bot',
1206
- required: true,
1207
- },
1208
- {
1209
- key: 'desc',
1210
- message: isVi ? 'Mô tả Bot:' : 'Bot Description:',
1211
- defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1212
- required: true,
1213
- },
1214
- {
1215
- key: 'persona',
1216
- message: isVi ? 'Tính cách & quy tắc (VD: gọn gàng, thân thiện):' : 'Persona & rules (e.g. concise, friendly):',
1217
- defaultValue: defaults.persona || '',
1218
- required: false,
1219
- },
1220
- {
1221
- key: 'token',
1222
- message: isVi ? `Nhập ${channel.name} Token:` : `Enter ${channel.name} Token:`,
1223
- defaultValue: defaults.token || '',
1224
- required: true,
1225
- },
1226
- ];
1227
- const draft = { ...defaults, slashCmd: '' };
1228
- let fieldIndex = 0;
1229
- while (fieldIndex < fields.length) {
1230
- const field = fields[fieldIndex];
1231
- const value = await inputWithBack({
1232
- message: field.message,
1233
- defaultValue: draft[field.key] || field.defaultValue,
1234
- required: field.required,
1235
- allowBack: true,
1236
- isVi,
1237
- });
1238
- if (value === CLI_BACK) {
1239
- if (fieldIndex > 0) {
1240
- fieldIndex--;
1241
- continue;
1242
- }
1243
- return { back: true };
1244
- }
1245
- draft[field.key] = value;
1246
- fieldIndex++;
1247
- }
1248
- bots.push(draft);
1249
- } else {
1250
- bots.push({ name: 'Bot', slashCmd: '', desc: '', persona: '', token: '' });
1251
- }
1252
-
1253
- return {
1254
- back: false,
1255
- botCount,
1256
- groupId,
1257
- bots,
1258
- botToken: bots[0]?.token || '',
1259
- };
1260
- }
1261
-
1262
- async function collectBotSetupStepWithGroupBack(options) {
1263
- const {
1264
- isVi,
1265
- channelKey,
1266
- channel,
1267
- existingBots = [],
1268
- existingBotCount = 1,
1269
- existingGroupId = '',
1270
- } = options;
1271
-
1272
- let botCount = channelKey === 'telegram' ? existingBotCount : 1;
1273
- let groupId = existingGroupId;
1274
- const bots = [];
1275
-
1276
- if (channelKey === 'telegram') {
1277
- const botCountValue = await selectWithBack({
1278
- message: isVi ? 'Bạn muốn cài bao nhiêu Telegram bot?' : 'How many Telegram bots do you want to deploy?',
1279
- choices: [
1280
- { name: '1 bot (single)', value: '1' },
1281
- { name: '2 bots (Department Room)', value: '2' },
1282
- { name: '3 bots', value: '3' },
1283
- { name: '4 bots', value: '4' },
1284
- { name: '5 bots', value: '5' },
1285
- ],
1286
- defaultValue: String(existingBotCount || 1),
1287
- allowBack: true,
1288
- isVi,
1289
- });
1290
- if (botCountValue === CLI_BACK) {
1291
- return { back: true };
1292
- }
1293
- botCount = parseInt(botCountValue, 10);
1294
-
1295
- if (botCount > 1) {
1296
- while (true) {
1297
- const groupOption = await selectWithBack({
1298
- message: isVi ? 'Bạn có sẵn Telegram Group chưa?' : 'Do you already have a Telegram Group?',
1299
- choices: [
1300
- { 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' },
1301
- { name: isVi ? '🔗 Đã có group — nhập Group ID ngay' : '🔗 Already have a group — enter Group ID now', value: 'existing' }
1302
- ],
1303
- defaultValue: groupId ? 'existing' : 'create',
1304
- allowBack: true,
1305
- isVi,
1306
- });
1307
- if (groupOption === CLI_BACK) {
1308
- return { back: true };
1309
- }
1310
-
1311
- if (groupOption === 'existing') {
1312
- console.log(chalk.dim(isVi
1313
- ? '\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'
1314
- : '\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'));
1315
- const nextGroupId = await inputWithBack({
1316
- message: isVi ? 'Telegram Group ID (VD: -1001234567890):' : 'Telegram Group ID (e.g. -1001234567890):',
1317
- defaultValue: groupId,
1318
- allowBack: true,
1319
- isVi,
1320
- });
1321
- if (nextGroupId === CLI_BACK) {
1322
- continue;
1323
- }
1324
- groupId = nextGroupId;
1325
- break;
1326
- }
1327
-
1328
- groupId = '';
1329
- break;
1330
- }
1331
- } else {
1332
- groupId = '';
1333
- }
1334
-
1335
- for (let i = 0; i < botCount; i++) {
1336
- console.log(chalk.bold(`\n${isVi ? `─── Bot ${i + 1} / ${botCount} ───` : `─── Bot ${i + 1} / ${botCount} ───`}`));
1337
- const defaults = existingBots[i] || {};
1338
- const fields = [
1339
- {
1340
- key: 'name',
1341
- message: isVi ? `Tên Bot ${i + 1}:` : `Bot ${i + 1} name:`,
1342
- defaultValue: defaults.name || `Bot ${i + 1}`,
1343
- required: true,
1344
- },
1345
- {
1346
- key: 'slashCmd',
1347
- message: isVi ? `Slash command (VD: /bot${i + 1}):` : `Slash command (e.g. /bot${i + 1}):`,
1348
- defaultValue: defaults.slashCmd || `/bot${i + 1}`,
1349
- required: true,
1350
- },
1351
- {
1352
- key: 'desc',
1353
- message: isVi ? `Mô tả Bot ${i + 1} (VD: Trợ lý AI cá nhân):` : `Bot ${i + 1} description (e.g. Personal AI assistant):`,
1354
- defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1355
- required: true,
1356
- },
1357
- {
1358
- key: 'persona',
1359
- 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):`,
1360
- defaultValue: defaults.persona || '',
1361
- required: false,
1362
- },
1363
- {
1364
- key: 'token',
1365
- message: isVi ? 'Bot Token (từ @BotFather):' : 'Bot Token (from @BotFather):',
1366
- defaultValue: defaults.token || '',
1367
- required: true,
1368
- },
1369
- ];
1370
-
1371
- const draft = { ...defaults };
1372
- let fieldIndex = 0;
1373
- while (fieldIndex < fields.length) {
1374
- const field = fields[fieldIndex];
1375
- const value = await inputWithBack({
1376
- message: field.message,
1377
- defaultValue: draft[field.key] || field.defaultValue,
1378
- required: field.required,
1379
- allowBack: true,
1380
- isVi,
1381
- });
1382
- if (value === CLI_BACK) {
1383
- if (fieldIndex > 0) {
1384
- fieldIndex--;
1385
- continue;
1386
- }
1387
- return { back: true };
1388
- }
1389
- draft[field.key] = value;
1390
- fieldIndex++;
1391
- }
1392
- bots.push(draft);
1393
- }
1394
- } else if (channelKey !== 'zalo-personal') {
1395
- const defaults = existingBots[0] || {};
1396
- const fields = [
1397
- {
1398
- key: 'name',
1399
- message: isVi ? 'Tên Bot:' : 'Bot Name:',
1400
- defaultValue: defaults.name || 'Chat Bot',
1401
- required: true,
1402
- },
1403
- {
1404
- key: 'desc',
1405
- message: isVi ? 'Mô tả Bot:' : 'Bot Description:',
1406
- defaultValue: defaults.desc || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'),
1407
- required: true,
1408
- },
1409
- {
1410
- key: 'persona',
1411
- message: isVi ? 'Tính cách & quy tắc (VD: gọn gàng, thân thiện):' : 'Persona & rules (e.g. concise, friendly):',
1412
- defaultValue: defaults.persona || '',
1413
- required: false,
1414
- },
1415
- {
1416
- key: 'token',
1417
- message: isVi ? `Nhập ${channel.name} Token:` : `Enter ${channel.name} Token:`,
1418
- defaultValue: defaults.token || '',
1419
- required: true,
1420
- },
1421
- ];
1422
- const draft = { ...defaults, slashCmd: '' };
1423
- let fieldIndex = 0;
1424
- while (fieldIndex < fields.length) {
1425
- const field = fields[fieldIndex];
1426
- const value = await inputWithBack({
1427
- message: field.message,
1428
- defaultValue: draft[field.key] || field.defaultValue,
1429
- required: field.required,
1430
- allowBack: true,
1431
- isVi,
1432
- });
1433
- if (value === CLI_BACK) {
1434
- if (fieldIndex > 0) {
1435
- fieldIndex--;
1436
- continue;
1437
- }
1438
- return { back: true };
1439
- }
1440
- draft[field.key] = value;
1441
- fieldIndex++;
1442
- }
1443
- bots.push(draft);
1444
- } else {
1445
- bots.push({ name: 'Bot', slashCmd: '', desc: '', persona: '', token: '' });
1446
- }
1447
-
1448
- return {
1449
- back: false,
1450
- botCount,
1451
- groupId,
1452
- bots,
1453
- botToken: bots[0]?.token || '',
1454
- };
1455
- }
1456
-
1457
- async function collectProviderStep({
1458
- isVi,
1459
- existingProviderKey = '',
1460
- existingProviderKeyVal = '',
1461
- existingOllamaModel = 'gemma4:e2b',
1462
- }) {
1463
- const providerKey = await selectWithBack({
1464
- message: isVi ? 'Chọn AI Provider:' : 'Select AI Provider:',
1465
- choices: Object.entries(PROVIDERS).map(([k, v]) => ({ name: `${v.icon} ${v.name}`, value: k })),
1466
- defaultValue: existingProviderKey || undefined,
1467
- allowBack: true,
1468
- isVi,
1469
- });
1470
- if (providerKey === CLI_BACK) {
1471
- return { back: true };
1472
- }
1473
- const provider = PROVIDERS[providerKey];
1474
-
1475
- let providerKeyVal = existingProviderKey === providerKey ? existingProviderKeyVal : '';
1476
- if (!provider.isProxy && !provider.isLocal) {
1477
- const keyValue = await inputWithBack({
1478
- message: isVi ? `Nhập ${provider.envKey}:` : `Enter ${provider.envKey}:`,
1479
- defaultValue: providerKeyVal,
1480
- required: true,
1481
- allowBack: true,
1482
- isVi,
1483
- });
1484
- if (keyValue === CLI_BACK) {
1485
- return { back: true };
1486
- }
1487
- providerKeyVal = keyValue;
1488
- }
1489
-
1490
- let selectedOllamaModel = existingProviderKey === 'ollama' ? existingOllamaModel : 'gemma4:e2b';
1491
- if (providerKey === 'ollama') {
1492
- console.log(chalk.yellow(isVi
1493
- ? '\n💡 Gemma 4 (02/04/2026) — chọn kích thước phù hợp với RAM máy bạn:'
1494
- : '\n💡 Gemma 4 (April 2, 2026) — pick a size that fits your RAM:'));
1495
- const modelValue = await selectWithBack({
1496
- message: isVi ? 'Chọn model Ollama:' : 'Select Ollama model:',
1497
- choices: [
1498
- {
1499
- name: isVi
1500
- ? '🟢 gemma4:e2b — Nhẹ nhất (~4-6 GB RAM) — Laptop / test nhanh ★ Khuyên dùng'
1501
- : '🟢 gemma4:e2b — Lightest (~4-6 GB RAM) — Laptop / fastest test ★ Recommended',
1502
- value: 'gemma4:e2b'
1503
- },
1504
- {
1505
- name: isVi
1506
- ? '🟡 gemma4:e4b — Cân bằng (~8-10 GB RAM) — Dùng hằng ngày'
1507
- : '🟡 gemma4:e4b — Balanced (~8-10 GB RAM) — Daily use',
1508
- value: 'gemma4:e4b'
1509
- },
1510
- {
1511
- name: isVi
1512
- ? '🟠 gemma4:26b — Mạnh (~18-24 GB RAM/VRAM) — Máy mạnh'
1513
- : '🟠 gemma4:26b — Powerful (~18-24 GB RAM/VRAM) — High-end machine',
1514
- value: 'gemma4:26b'
1515
- },
1516
- {
1517
- name: isVi
1518
- ? '🔴 gemma4:31b — Mạnh nhất (~24+ GB RAM/VRAM) — GPU workstation'
1519
- : '🔴 gemma4:31b — Most powerful (~24+ GB RAM/VRAM) — GPU workstation',
1520
- value: 'gemma4:31b'
1521
- },
1522
- ],
1523
- defaultValue: selectedOllamaModel,
1524
- allowBack: true,
1525
- isVi,
1526
- });
1527
- if (modelValue === CLI_BACK) {
1528
- return { back: true };
1529
- }
1530
- selectedOllamaModel = modelValue;
1531
- }
1532
-
1533
- return {
1534
- back: false,
1535
- providerKey,
1536
- provider,
1537
- providerKeyVal,
1538
- selectedOllamaModel,
1539
- };
1540
- }
1541
-
1542
- async function collectSkillsStep({
1543
- isVi,
1544
- providerKey,
1545
- existingSelectedSkills = [],
1546
- existingBrowserMode = 'server',
1547
- existingTtsOpenaiKey = '',
1548
- existingTtsElevenKey = '',
1549
- existingSmtpHost = 'smtp.gmail.com',
1550
- existingSmtpPort = '587',
1551
- existingSmtpUser = '',
1552
- existingSmtpPass = '',
1553
- }) {
1554
- const selectedSkills = await checkboxWithBack({
1555
- message: isVi ? 'Bật tính năng bổ sung (Space để chọn):' : 'Enable extra skills (Space to select):',
1556
- choices: getCliSkillChoices({ providerKey, isVi }).map((choice) => ({
1557
- ...choice,
1558
- checked: existingSelectedSkills.includes(choice.value),
1559
- })),
1560
- allowBack: true,
1561
- isVi,
1562
- });
1563
- if (selectedSkills === CLI_BACK) {
1564
- return { back: true };
1565
- }
1566
-
1567
- let browserMode = existingBrowserMode;
1568
- if (selectedSkills.includes('browser')) {
1569
- const isLinux = process.platform === 'linux';
1570
- const browserValue = await selectWithBack({
1571
- message: isVi ? 'Chế độ Browser Automation:' : 'Browser Automation mode:',
1572
- choices: [
1573
- {
1574
- name: isVi
1575
- ? '🖥️ Dùng Chrome trên máy tính (Windows/Mac — Bypass Cloudflare tốt hơn)'
1576
- : '🖥️ Use Host Chrome (Windows/Mac — Better Cloudflare bypass)',
1577
- value: 'desktop'
1578
- },
1579
- {
1580
- name: isVi
1581
- ? '🐳 Headless Chromium trong Docker (Ubuntu Server / VPS — không cần GUI)'
1582
- : '🐳 Headless Chromium inside Docker (Ubuntu Server / VPS — No GUI)',
1583
- value: 'server'
1584
- }
1585
- ],
1586
- defaultValue: browserMode || (isLinux ? 'server' : 'desktop'),
1587
- allowBack: true,
1588
- isVi,
1589
- });
1590
- if (browserValue === CLI_BACK) {
1591
- return { back: true };
1592
- }
1593
- browserMode = browserValue;
1594
- } else {
1595
- browserMode = 'server';
1596
- }
1597
-
1598
- let ttsOpenaiKey = existingTtsOpenaiKey;
1599
- let ttsElevenKey = existingTtsElevenKey;
1600
- if (selectedSkills.includes('tts')) {
1601
- const openaiKey = await inputWithBack({
1602
- 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):',
1603
- defaultValue: ttsOpenaiKey,
1604
- allowBack: true,
1605
- isVi,
1606
- });
1607
- if (openaiKey === CLI_BACK) {
1608
- return { back: true };
1609
- }
1610
- ttsOpenaiKey = openaiKey;
1611
-
1612
- const elevenKey = await inputWithBack({
1613
- message: isVi ? 'Nhập ELEVENLABS_API_KEY (hoặc bỏ trống):' : 'Enter ELEVENLABS_API_KEY (or leave empty):',
1614
- defaultValue: ttsElevenKey,
1615
- allowBack: true,
1616
- isVi,
1617
- });
1618
- if (elevenKey === CLI_BACK) {
1619
- return { back: true };
1620
- }
1621
- ttsElevenKey = elevenKey;
1622
- } else {
1623
- ttsOpenaiKey = '';
1624
- ttsElevenKey = '';
1625
- }
1626
-
1627
- let smtpHost = existingSmtpHost;
1628
- let smtpPort = existingSmtpPort;
1629
- let smtpUser = existingSmtpUser;
1630
- let smtpPass = existingSmtpPass;
1631
- if (selectedSkills.includes('email')) {
1632
- const smtpHostValue = await inputWithBack({
1633
- message: isVi ? 'SMTP Host (VD: smtp.gmail.com):' : 'SMTP Host (e.g. smtp.gmail.com):',
1634
- defaultValue: smtpHost,
1635
- required: true,
1636
- allowBack: true,
1637
- isVi,
1638
- });
1639
- if (smtpHostValue === CLI_BACK) {
1640
- return { back: true };
1641
- }
1642
- smtpHost = smtpHostValue;
1643
-
1644
- const smtpPortValue = await inputWithBack({
1645
- message: 'SMTP Port:',
1646
- defaultValue: smtpPort,
1647
- required: true,
1648
- allowBack: true,
1649
- isVi,
1650
- });
1651
- if (smtpPortValue === CLI_BACK) {
1652
- return { back: true };
1653
- }
1654
- smtpPort = smtpPortValue;
1655
-
1656
- const smtpUserValue = await inputWithBack({
1657
- message: isVi ? 'SMTP Email:' : 'SMTP Email:',
1658
- defaultValue: smtpUser,
1659
- required: true,
1660
- allowBack: true,
1661
- isVi,
1662
- });
1663
- if (smtpUserValue === CLI_BACK) {
1664
- return { back: true };
1665
- }
1666
- smtpUser = smtpUserValue;
1667
-
1668
- const smtpPassValue = await inputWithBack({
1669
- message: isVi ? 'SMTP App Password:' : 'SMTP App Password:',
1670
- defaultValue: smtpPass,
1671
- required: true,
1672
- allowBack: true,
1673
- isVi,
1674
- });
1675
- if (smtpPassValue === CLI_BACK) {
1676
- return { back: true };
1677
- }
1678
- smtpPass = smtpPassValue;
1679
- } else {
1680
- smtpHost = 'smtp.gmail.com';
1681
- smtpPort = '587';
1682
- smtpUser = '';
1683
- smtpPass = '';
1684
- }
1685
-
1686
- return {
1687
- back: false,
1688
- selectedSkills,
1689
- browserMode,
1690
- ttsOpenaiKey,
1691
- ttsElevenKey,
1692
- smtpHost,
1693
- smtpPort,
1694
- smtpUser,
1695
- smtpPass,
1696
- };
1697
- }
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
+ }
1698
1761
 
1699
1762
 
1700
1763
  // ─── Shared workspace file writer ─────────────────────────────────────────────
@@ -1770,228 +1833,228 @@ async function writeWorkspaceFiles({
1770
1833
  }
1771
1834
 
1772
1835
 
1773
- async function main() {
1774
- const cliSubcommand = getCliSubcommand();
1775
- if (cliSubcommand === 'upgrade') {
1776
- await runUpgradeCommand();
1777
- return;
1778
- }
1779
-
1780
- console.log(chalk.red('\n=================================='));
1781
- console.log(chalk.redBright(LOGO));
1782
- console.log(chalk.greenBright(' OpenClaw Auto Setup CLI '));
1783
- console.log(chalk.red('==================================\n'));
1784
-
1785
- let lang = 'vi';
1786
- let isVi = true;
1787
- const detectedPlatform = process.platform;
1788
- const detectedOS = detectedPlatform === 'win32' ? 'windows'
1789
- : detectedPlatform === 'darwin' ? 'macos'
1790
- : 'linux';
1791
- let osChoice = detectedOS === 'linux' ? 'vps' : detectedOS;
1792
- let deployMode = 'docker';
1793
- let channelKey = 'telegram';
1794
- let channel = CHANNELS[channelKey];
1795
- let botToken = '';
1796
- let botCount = 1;
1797
- let bots = [];
1798
- let groupId = '';
1799
- let userInfo = '';
1800
- let providerKey = '9router';
1801
- let provider = PROVIDERS[providerKey];
1802
- let providerKeyVal = '';
1803
- let selectedOllamaModel = 'gemma4:e2b';
1804
- let selectedSkills = [];
1805
- let tavilyKey = '';
1806
- let browserMode = 'server';
1807
- let ttsOpenaiKey = '';
1808
- let ttsElevenKey = '';
1809
- let smtpHost = 'smtp.gmail.com', smtpPort = '587', smtpUser = '', smtpPass = '';
1810
- let defaultDir = process.cwd();
1811
- if (!defaultDir.endsWith('openclaw-setup') && !defaultDir.endsWith('openclaw')) {
1812
- defaultDir = path.join(defaultDir, 'openclaw-setup');
1813
- }
1814
- let projectDir = defaultDir;
1815
-
1816
- let setupStep = 'language';
1817
- while (true) {
1818
- if (setupStep === 'language') {
1819
- lang = await select({
1820
- message: 'Select language / Chọn ngôn ngữ:',
1821
- choices: [
1822
- { name: 'Tiếng Việt', value: 'vi' },
1823
- { name: 'English', value: 'en' }
1824
- ],
1825
- default: lang
1826
- });
1827
- isVi = lang === 'vi';
1828
- setupStep = 'os';
1829
- continue;
1830
- }
1831
-
1832
- if (setupStep === 'os') {
1833
- const nextOsChoice = await selectWithBack({
1834
- message: isVi ? 'Bạn đang chạy trên hệ điều hành nào?' : 'What OS are you running on?',
1835
- choices: [
1836
- { name: isVi ? '🪟 Windows' : '🪟 Windows', value: 'windows' },
1837
- { name: isVi ? '🍎 macOS' : '🍎 macOS', value: 'macos' },
1838
- { name: isVi ? '🐧 Ubuntu Desktop' : '🐧 Ubuntu Desktop', value: 'ubuntu' },
1839
- { name: isVi ? '🖥️ VPS / Ubuntu Server' : '🖥️ VPS / Ubuntu Server', value: 'vps' },
1840
- ],
1841
- defaultValue: osChoice,
1842
- allowBack: true,
1843
- isVi,
1844
- });
1845
- if (nextOsChoice === CLI_BACK) {
1846
- setupStep = 'language';
1847
- continue;
1848
- }
1849
- osChoice = nextOsChoice;
1850
- setupStep = 'deploy';
1851
- continue;
1852
- }
1853
-
1854
- if (setupStep === 'deploy') {
1855
- const deployModeDefault = (osChoice === 'ubuntu' || osChoice === 'vps') ? 'native' : 'docker';
1856
- const nextDeployMode = await selectWithBack({
1857
- message: isVi ? 'Chọn cách chạy bot:' : 'How do you want to run the bot?',
1858
- choices: [
1859
- { 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' },
1860
- { 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' }
1861
- ],
1862
- defaultValue: deployMode || deployModeDefault,
1863
- allowBack: true,
1864
- isVi,
1865
- });
1866
- if (nextDeployMode === CLI_BACK) {
1867
- setupStep = 'os';
1868
- continue;
1869
- }
1870
- deployMode = nextDeployMode;
1871
- if (deployMode === 'docker' && !isDockerInstalled()) {
1872
- 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...'));
1873
- try {
1874
- const platform = process.platform;
1875
- if (platform === 'win32') {
1876
- execSync('winget install -e --id Docker.DockerDesktop --accept-source-agreements --accept-package-agreements', { stdio: 'inherit' });
1877
- 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.'));
1878
- process.exit(0);
1879
- } else if (platform === 'darwin') {
1880
- execSync('brew install --cask docker', { stdio: 'inherit' });
1881
- 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.'));
1882
- process.exit(0);
1883
- } else {
1884
- execSync('curl -fsSL https://get.docker.com | sh', { stdio: 'inherit', shell: true });
1885
- try { execSync('apt-get install -y docker-compose-plugin', { stdio: 'ignore', shell: true }); } catch {}
1886
- console.log(chalk.green(isVi ? '✅ Docker Engine + Compose plugin đã cài xong.' : '✅ Docker Engine + Compose plugin installed.'));
1887
- }
1888
- } catch {
1889
- 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/'));
1890
- process.exit(1);
1891
- }
1892
- }
1893
- setupStep = 'channel';
1894
- continue;
1895
- }
1896
-
1897
- if (setupStep === 'channel') {
1898
- const nextChannelKey = await selectWithBack({
1899
- message: isVi ? 'Chọn nền tảng bot:' : 'Select bot platform:',
1900
- choices: Object.entries(CHANNELS).map(([k, v]) => ({ name: v.icon + ' ' + v.name, value: k })),
1901
- defaultValue: channelKey,
1902
- allowBack: true,
1903
- isVi,
1904
- });
1905
- if (nextChannelKey === CLI_BACK) {
1906
- setupStep = 'deploy';
1907
- continue;
1908
- }
1909
- channelKey = nextChannelKey;
1910
- channel = CHANNELS[channelKey];
1911
- if (channelKey === 'zalo-bot') {
1912
- 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.')));
1913
- }
1914
- setupStep = 'botSetup';
1915
- continue;
1916
- }
1917
-
1918
- if (setupStep === 'botSetup') {
1919
- const botSetup = await collectBotSetupStepWithGroupBack({ isVi, channelKey, channel, existingBots: bots, existingBotCount: botCount, existingGroupId: groupId });
1920
- if (botSetup.back) {
1921
- setupStep = 'channel';
1922
- continue;
1923
- }
1924
- botCount = botSetup.botCount;
1925
- groupId = botSetup.groupId;
1926
- bots = botSetup.bots;
1927
- botToken = botSetup.botToken;
1928
- setupStep = 'userInfo';
1929
- continue;
1930
- }
1931
-
1932
- if (setupStep === 'userInfo') {
1933
- console.log(chalk.bold('\n' + (isVi ? '👤 Thông tin của bạn 👤' : '👤 About You 👤')));
1934
- 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 });
1935
- if (nextUserInfo === CLI_BACK) {
1936
- setupStep = 'botSetup';
1937
- continue;
1938
- }
1939
- userInfo = nextUserInfo;
1940
- setupStep = 'provider';
1941
- continue;
1942
- }
1943
-
1944
- if (setupStep === 'provider') {
1945
- const providerSetup = await collectProviderStep({ isVi, existingProviderKey: providerKey, existingProviderKeyVal: providerKeyVal, existingOllamaModel: selectedOllamaModel });
1946
- if (providerSetup.back) {
1947
- setupStep = 'userInfo';
1948
- continue;
1949
- }
1950
- providerKey = providerSetup.providerKey;
1951
- provider = providerSetup.provider;
1952
- providerKeyVal = providerSetup.providerKeyVal;
1953
- selectedOllamaModel = providerSetup.selectedOllamaModel;
1954
- setupStep = 'skills';
1955
- continue;
1956
- }
1957
-
1958
- if (setupStep === 'skills') {
1959
- const skillSetup = await collectSkillsStep({ isVi, providerKey, existingSelectedSkills: selectedSkills, existingBrowserMode: browserMode, existingTtsOpenaiKey: ttsOpenaiKey, existingTtsElevenKey: ttsElevenKey, existingSmtpHost: smtpHost, existingSmtpPort: smtpPort, existingSmtpUser: smtpUser, existingSmtpPass: smtpPass });
1960
- if (skillSetup.back) {
1961
- setupStep = 'provider';
1962
- continue;
1963
- }
1964
- selectedSkills = skillSetup.selectedSkills;
1965
- browserMode = skillSetup.browserMode;
1966
- ttsOpenaiKey = skillSetup.ttsOpenaiKey;
1967
- ttsElevenKey = skillSetup.ttsElevenKey;
1968
- smtpHost = skillSetup.smtpHost;
1969
- smtpPort = skillSetup.smtpPort;
1970
- smtpUser = skillSetup.smtpUser;
1971
- smtpPass = skillSetup.smtpPass;
1972
- setupStep = 'projectDir';
1973
- continue;
1974
- }
1975
-
1976
- if (setupStep === 'projectDir') {
1977
- const nextProjectDir = await inputWithBack({ message: isVi ? 'Thư mục cài đặt project:' : 'Project install directory:', defaultValue: projectDir, allowBack: true, isVi });
1978
- if (nextProjectDir === CLI_BACK) {
1979
- setupStep = 'skills';
1980
- continue;
1981
- }
1982
- projectDir = nextProjectDir;
1983
- break;
1984
- }
1985
- }
1986
-
1987
- const isMultiBot = botCount > 1 && channelKey === 'telegram';
1988
- const botName = bots[0].name;
1989
- const botDesc = bots[0].desc;
1990
- const botPersona = bots[0].persona;
1991
- const agentId = String(botName || 'chat').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'chat';
1992
- const modelsPrimary = providerKey === 'ollama' ? selectedOllamaModel : providerKey === '9router' ? 'smart-route' : provider.models?.[0]?.id || 'gpt-4o-mini';
1993
- const hasBrowserDesktop = selectedSkills.includes('browser') && browserMode === 'desktop';
1994
- 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';
1995
2058
  console.log(chalk.cyan(`\n🚀 ${isVi ? 'Đang tạo thư mục và file cấu hình...' : 'Generating directories and configurations...'}`));
1996
2059
 
1997
2060
  await fs.ensureDir(projectDir);
@@ -2093,7 +2156,7 @@ async function main() {
2093
2156
  socatBridge,
2094
2157
  deviceApproveLoop,
2095
2158
  ].filter(Boolean),
2096
- volumeMount: '../../.openclaw:/root/project/.openclaw',
2159
+ volumeMount: '../../.openclaw:/root/project/.openclaw',
2097
2160
  singleComposeName: `oc-${agentId}`,
2098
2161
  multiComposeName: 'oc-multibot',
2099
2162
  singleAppContainerName: `openclaw-${agentId}`,
@@ -2152,13 +2215,13 @@ async function main() {
2152
2215
  } else if (providerKey === '9router') {
2153
2216
  authProfilesJson = {
2154
2217
  version: 1,
2155
- profiles: {
2156
- '9router-proxy': {
2157
- provider: '9router',
2158
- type: 'api_key',
2159
- key: 'sk-no-key',
2218
+ profiles: {
2219
+ '9router-proxy': {
2220
+ provider: '9router',
2221
+ type: 'api_key',
2222
+ key: NINE_ROUTER_PROXY_API_KEY,
2223
+ },
2160
2224
  },
2161
- },
2162
2225
  order: { '9router': ['9router-proxy'] },
2163
2226
  };
2164
2227
  }
@@ -2185,9 +2248,9 @@ async function main() {
2185
2248
  workspaceDir: `workspace-${agentSlug}`,
2186
2249
  };
2187
2250
  });
2188
- const telegramAccounts = Object.fromEntries(agentMetas.map((meta) => [meta.accountId, {
2189
- botToken: meta.token,
2190
- }]));
2251
+ const telegramAccounts = Object.fromEntries(agentMetas.map((meta) => [meta.accountId, {
2252
+ botToken: meta.token,
2253
+ }]));
2191
2254
  const telegramChannelConfig = {
2192
2255
  enabled: true,
2193
2256
  defaultAccount: 'default',
@@ -2234,14 +2297,7 @@ async function main() {
2234
2297
  models: {
2235
2298
  mode: 'merge',
2236
2299
  providers: {
2237
- '9router': {
2238
- baseUrl: deployMode === 'native' ? 'http://localhost:20128/v1' : 'http://9router:20128/v1',
2239
- apiKey: 'sk-no-key',
2240
- api: 'openai-completions',
2241
- models: [
2242
- { id: 'smart-route', name: 'Smart Proxy (Auto Route)', contextWindow: 200000, maxTokens: 8192 },
2243
- ],
2244
- },
2300
+ '9router': build9RouterProviderConfig(get9RouterBaseUrl(deployMode)),
2245
2301
  },
2246
2302
  },
2247
2303
  } : provider.isLocal ? {
@@ -2323,7 +2379,7 @@ async function main() {
2323
2379
  const safeRootClawDir = rootClawDir.replace(/\\/g, '/');
2324
2380
  const pm2Apps = [
2325
2381
  ' {',
2326
- ` name: 'openclaw-multibot',`,
2382
+ ` name: 'openclaw-multibot',`,
2327
2383
  ` script: 'openclaw',`,
2328
2384
  ` args: 'gateway run',`,
2329
2385
  ` cwd: '${projectDir.replace(/\\/g, '/')}',`,
@@ -2400,6 +2456,7 @@ async function main() {
2400
2456
  const loopBotName = isMultiBot ? (bots[bIndex]?.name || `Bot ${bIndex+1}`) : botName;
2401
2457
  const loopBotDesc = isMultiBot ? (bots[bIndex]?.desc || '') : botDesc;
2402
2458
  const loopBotPersona = isMultiBot ? (bots[bIndex]?.persona || '') : botPersona;
2459
+ const loopBotToken = isMultiBot ? (bots[bIndex]?.token || '') : botToken;
2403
2460
  const teamRoster = bots.slice(0, numBotsToConfigure).map((peer, idx) => ({
2404
2461
  idx,
2405
2462
  name: peer?.name || `Bot ${idx + 1}`,
@@ -2440,14 +2497,7 @@ async function main() {
2440
2497
  models: {
2441
2498
  mode: 'merge',
2442
2499
  providers: {
2443
- '9router': {
2444
- baseUrl: deployMode === 'native' ? 'http://localhost:20128/v1' : 'http://9router:20128/v1',
2445
- apiKey: 'sk-no-key',
2446
- api: 'openai-completions',
2447
- models: [
2448
- { id: 'smart-route', name: 'Smart Proxy (Auto Route)', contextWindow: 200000, maxTokens: 8192 }
2449
- ]
2450
- }
2500
+ '9router': build9RouterProviderConfig(get9RouterBaseUrl(deployMode))
2451
2501
  }
2452
2502
  }
2453
2503
  } : provider.isLocal ? {
@@ -2495,44 +2545,55 @@ async function main() {
2495
2545
  botConfig.skills = { entries: skillEntries };
2496
2546
  }
2497
2547
 
2498
- if (channelKey === 'telegram') {
2499
- const telegramConfig = {
2500
- enabled: true,
2501
- dmPolicy: 'open',
2502
- allowFrom: ['*'],
2503
- defaultAccount: 'default',
2504
- replyToMode: 'first',
2505
- reactionLevel: 'minimal',
2506
- actions: {
2507
- sendMessage: true,
2508
- reactions: true,
2509
- },
2510
- accounts: {
2511
- default: {
2512
- botToken: loopBotToken || '<your_bot_token>',
2513
- },
2514
- },
2515
- };
2516
- if (isMultiBot) {
2517
- telegramConfig.groupPolicy = groupId ? 'allowlist' : 'open';
2518
- telegramConfig.groupAllowFrom = ['*'];
2519
- telegramConfig.groups = {
2520
- [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 }
2521
2571
  };
2522
2572
  }
2523
2573
  botConfig.channels['telegram'] = telegramConfig;
2524
2574
  } else if (hasZaloPersonal(channelKey)) {
2525
2575
  botConfig.channels['zalouser'] = {
2526
2576
  enabled: true,
2577
+ defaultAccount: 'default',
2527
2578
  dmPolicy: 'open',
2528
- allowFrom: ['*']
2579
+ allowFrom: ['*'],
2580
+ groupPolicy: 'allowlist',
2581
+ groupAllowFrom: ['*'],
2582
+ historyLimit: 50,
2583
+ groups: {
2584
+ '*': {
2585
+ enabled: true,
2586
+ requireMention: false,
2587
+ },
2588
+ },
2589
+ autoReply: true,
2529
2590
  };
2530
2591
  } else if (channelKey === 'zalo-bot') {
2531
2592
  botConfig.channels['zalo'] = { enabled: true, provider: 'official_account' };
2532
2593
  }
2533
2594
 
2534
2595
  await fs.writeJson(path.join(loopBotDir, '.openclaw', 'openclaw.json'), botConfig, { spaces: 2 });
2535
-
2596
+
2536
2597
  // ── Workspace files: use shared writeWorkspaceFiles() ──────────────────────
2537
2598
  const dockerWorkspaceDir = path.join(loopBotDir, '.openclaw', loopWorkspaceDir);
2538
2599
  const dockerOwnAliases = [loopBotName, bots[bIndex]?.slashCmd || '', `bot ${bIndex + 1}`].filter(Boolean);
@@ -2564,8 +2625,8 @@ async function main() {
2564
2625
  // Append per-bot reply rules to AGENTS.md
2565
2626
  const otherBotNames = teamRoster.filter((p) => p.idx !== bIndex).map((p) => p.name);
2566
2627
  const extraAgentsMd = isVi
2567
- ? `\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.`
2568
- : `\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.`;
2569
2630
  await fs.appendFile(path.join(dockerWorkspaceDir, 'AGENTS.md'), extraAgentsMd);
2570
2631
  }
2571
2632
  } // END FOR LOOP
@@ -2575,14 +2636,14 @@ async function main() {
2575
2636
  await writeGeneratedArtifacts(projectDir, buildCliChromeDebugArtifacts());
2576
2637
 
2577
2638
  // ── Uninstall scripts ───────────────────────────────────────────────────────
2578
- await writeGeneratedArtifacts(projectDir, buildCliUninstallArtifacts({
2579
- deployMode,
2580
- osChoice: detectedOS,
2581
- projectDir,
2582
- botName: (deployMode !== 'docker' && detectedOS === 'vps')
2583
- ? getNativePm2AppName(isMultiBot)
2584
- : botName,
2585
- }));
2639
+ await writeGeneratedArtifacts(projectDir, buildCliUninstallArtifacts({
2640
+ deployMode,
2641
+ osChoice: detectedOS,
2642
+ projectDir,
2643
+ botName: (deployMode !== 'docker' && detectedOS === 'vps')
2644
+ ? getNativePm2AppName(isMultiBot)
2645
+ : botName,
2646
+ }));
2586
2647
 
2587
2648
  // ── Upgrade scripts ─────────────────────────────────────────────────────────
2588
2649
  await writeGeneratedArtifacts(projectDir, buildCliUpgradeArtifacts());
@@ -2590,33 +2651,33 @@ async function main() {
2590
2651
  // ── start-bot.bat / start-bot.sh — one-click restart scripts ─────────────
2591
2652
  // Generated for native deployments only (docker has docker compose up)
2592
2653
  if (deployMode !== 'docker') {
2593
- await writeGeneratedArtifacts(projectDir, buildCliStartBotArtifacts({
2594
- projectDir,
2595
- openclawHome: path.join(projectDir, '.openclaw'),
2596
- is9Router: providerKey === '9router',
2597
- osChoice,
2598
- isMultiBot,
2599
- appName: getNativePm2AppName(isMultiBot),
2600
- isVi,
2601
- }));
2602
-
2603
- console.log(chalk.cyan(
2604
- process.platform === 'win32'
2605
- ? (isVi
2606
- ? `\n🚀 start-bot.bat / start-bot.sh đã tạo — double-click để restart bot.`
2607
- : `\n🚀 start-bot.bat / start-bot.sh created — double-click to restart the bot.`)
2608
- : (isVi
2609
- ? `\n🚀 start-bot.sh đã tạo — chạy ./start-bot.sh để restart bot.`
2610
- : `\n🚀 start-bot.sh created — run ./start-bot.sh to restart the bot.`)
2611
- ));
2612
- }
2613
-
2614
- console.log(chalk.green(`✅ ${isVi ? 'Tạo cấu hình thành công!' : 'Configs created successfully!'}`));
2615
-
2616
- installLatestOpenClaw({ isVi, osChoice });
2617
-
2618
- // 7. Auto Run
2619
- 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({
2620
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?',
2621
2682
  default: true
2622
2683
  }) : false;
@@ -2696,7 +2757,7 @@ async function main() {
2696
2757
  });
2697
2758
 
2698
2759
  }
2699
- if (deployMode === 'docker') {
2760
+ if (deployMode === 'docker') {
2700
2761
 
2701
2762
  // ── Auto-install openclaw binary if not present ──────────────────────────
2702
2763
  const isOpenClawInstalled = () => { try { execSync('openclaw --version', { stdio: 'ignore' }); return true; } catch { return false; } };
@@ -2746,7 +2807,16 @@ async function main() {
2746
2807
 
2747
2808
  let native9RouterSyncScriptPath = null;
2748
2809
  if (providerKey === '9router') {
2810
+ await writeNative9RouterPatchScript(projectDir);
2749
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
+ }
2750
2820
  }
2751
2821
 
2752
2822
  await ensureProjectRuntimeDirs(projectDir, isVi);
@@ -2763,26 +2833,26 @@ async function main() {
2763
2833
  }
2764
2834
  }
2765
2835
 
2766
- if (isMultiBot && channelKey === 'telegram') {
2767
- if (providerKey === '9router') {
2768
- startNative9RouterPm2({ isVi, projectDir, appName: getNativePm2AppName(true), syncScriptPath: native9RouterSyncScriptPath });
2769
- }
2770
- execSync('pm2 start ecosystem.config.js && pm2 save', {
2771
- cwd: projectDir,
2772
- stdio: 'inherit',
2773
- shell: true
2774
- });
2775
- 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.'}`));
2776
- console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${getNativePm2AppName(true)}` : ` View logs: pm2 logs ${getNativePm2AppName(true)}`));
2777
- printNativeDashboardAccessInfo({ isVi, providerKey, projectDir });
2778
- if (channelKey === 'zalo-personal') {
2779
- printZaloPersonalLoginInfo({ isVi, deployMode: 'native', projectDir });
2780
- }
2781
- } else {
2782
- const appName = getNativePm2AppName(false);
2783
- if (providerKey === '9router') {
2784
- startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath: native9RouterSyncScriptPath });
2785
- }
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
+ }
2786
2856
  if (channelKey === 'zalo-personal') {
2787
2857
  await runNativeZaloPersonalLoginFlow({ isVi, projectDir });
2788
2858
  }
@@ -2866,9 +2936,9 @@ async function main() {
2866
2936
  console.log(chalk.yellow(`\n📋 ${isVi ? 'Xem huong dan sau cai:' : 'Read post-install guide:'} ${path.join(projectDir, TELEGRAM_SETUP_GUIDE_FILENAME)}`));
2867
2937
  }
2868
2938
  }
2869
- }
2870
-
2871
-
2939
+ }
2940
+
2941
+
2872
2942
 
2873
2943
  main().catch(err => {
2874
2944
  console.error(chalk.red('Error:'), err);