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/README.md +18 -19
- package/README.vi.md +18 -19
- package/dist/cli.js +1231 -1161
- package/dist/setup/data/channels.js +11 -1
- package/dist/setup/shared/common-gen.js +86 -0
- package/dist/setup/shared/docker-gen.js +73 -30
- package/dist/setup/shared/install-gen.js +23 -3
- package/dist/setup.js +217 -121
- package/package.json +1 -1
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
|
-
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
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
|
|
896
|
-
const
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
'
|
|
916
|
-
'
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
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);
|