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