create-openclaw-bot 5.0.8 → 5.1.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/CHANGELOG.md +1 -1
- package/CHANGELOG.vi.md +1 -1
- package/README.md +3 -3
- package/README.vi.md +3 -3
- package/cli.js +113 -10
- package/docs/install-native.md +1 -1
- package/docs/install-native.vi.md +1 -1
- package/package.json +1 -1
- package/setup.js +116 -111
- package/tests/smoke-cli-logic.mjs +55 -1
- package/tmp/live-enable-zalouser.cjs +20 -0
- package/tmp/live-enable-zalouser.js +20 -0
- package/tmp/live-patch-9router.cjs +40 -0
- package/tmp/live-patch-9router.js +40 -0
package/CHANGELOG.md
CHANGED
package/CHANGELOG.vi.md
CHANGED
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# 🦞 OpenClaw Setup
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.0
|
|
6
|
+
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.1.0-0EA5E9?style=for-the-badge" alt="Version 5.1.0" /></a>
|
|
7
7
|
<a href="https://github.com/tuanminhhole/openclaw-setup?tab=MIT-1-ov-file"><img src="https://img.shields.io/badge/LICENSE-MIT-success?style=for-the-badge" alt="MIT License" /></a>
|
|
8
8
|
<a href="https://www.npmjs.com/package/create-openclaw-bot"><img src="https://img.shields.io/npm/v/create-openclaw-bot?style=for-the-badge&label=CLI&color=2563EB&logo=npm&logoColor=white" alt="NPM Version" /></a>
|
|
9
9
|
<a href="https://github.com/tuanminhhole/openclaw-setup/stargazers"><img src="https://img.shields.io/github/stars/tuanminhhole/openclaw-setup?style=for-the-badge&color=eab308&logo=github&logoColor=white" alt="GitHub Stars" /></a>
|
|
@@ -24,7 +24,7 @@ An interactive **CLI tool** and **Setup Wizard** to deploy your own free AI Bot
|
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
-
## 🆕 What's new in v5.0
|
|
27
|
+
## 🆕 What's new in v5.1.0
|
|
28
28
|
|
|
29
29
|
- 💻 **OS-First Setup** — Step 1 is now choosing your OS (Windows, macOS, Ubuntu, VPS). All scripts, configs, and instructions are generated to match.
|
|
30
30
|
- 🧠 **Gemma 4 — 4 sizes** — `gemma4:e2b` (~4 GB), `gemma4:e4b` (~8 GB), `gemma4:26b` (~18 GB), `gemma4:31b` (~24 GB). Auto-pulled on first launch.
|
|
@@ -111,7 +111,7 @@ Run in your terminal → follow the interactive prompts → startup script is ge
|
|
|
111
111
|
2. Open this repo as your workspace
|
|
112
112
|
3. Paste into chat:
|
|
113
113
|
```
|
|
114
|
-
Read SETUP.md and set up OpenClaw v5.0
|
|
114
|
+
Read SETUP.md and set up OpenClaw v5.1.0 for me.
|
|
115
115
|
My bot token is X. Use 9Router (no API key).
|
|
116
116
|
My project folder: <YOUR_PATH>
|
|
117
117
|
```
|
package/README.vi.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# 🦞 OpenClaw Setup
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.0
|
|
6
|
+
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.1.0-0EA5E9?style=for-the-badge" alt="Version 5.1.0" /></a>
|
|
7
7
|
<a href="https://github.com/tuanminhhole/openclaw-setup?tab=MIT-1-ov-file"><img src="https://img.shields.io/badge/LICENSE-MIT-success?style=for-the-badge" alt="MIT License" /></a>
|
|
8
8
|
<a href="https://www.npmjs.com/package/create-openclaw-bot"><img src="https://img.shields.io/npm/v/create-openclaw-bot?style=for-the-badge&label=CLI&color=2563EB&logo=npm&logoColor=white" alt="NPM Version" /></a>
|
|
9
9
|
<a href="https://github.com/tuanminhhole/openclaw-setup/stargazers"><img src="https://img.shields.io/github/stars/tuanminhhole/openclaw-setup?style=for-the-badge&color=eab308&logo=github&logoColor=white" alt="GitHub Stars" /></a>
|
|
@@ -24,7 +24,7 @@ Công cụ **CLI tương tác** và **Setup Wizard** để tự triển khai Bot
|
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
-
## 🆕 Có gì mới trong v5.0
|
|
27
|
+
## 🆕 Có gì mới trong v5.1.0
|
|
28
28
|
|
|
29
29
|
- 💻 **OS-First Setup** — Bước đầu tiên bây giờ là chọn hệ điều hành của bạn (Windows, macOS, Ubuntu, VPS). Toàn bộ script, cấu hình và hướng dẫn được tạo ra phù hợp với lựa chọn đó.
|
|
30
30
|
- 🧠 **Gemma 4 — 4 kích thước** — `gemma4:e2b` (~4 GB), `gemma4:e4b` (~8 GB), `gemma4:26b` (~18 GB), `gemma4:31b` (~24 GB). Tự pull về khi bot khởi động lần đầu.
|
|
@@ -111,7 +111,7 @@ Chạy lệnh trên trong Terminal → làm theo các prompt tương tác → sc
|
|
|
111
111
|
2. Mở repo này làm workspace
|
|
112
112
|
3. Paste vào chat:
|
|
113
113
|
```
|
|
114
|
-
Read SETUP.md and set up OpenClaw v5.0
|
|
114
|
+
Read SETUP.md and set up OpenClaw v5.1.0 for me.
|
|
115
115
|
My bot token is X. Use 9Router (no API key).
|
|
116
116
|
My project folder: <THƯ_MỤC_CỦA_BẠN>
|
|
117
117
|
```
|
package/cli.js
CHANGED
|
@@ -160,6 +160,23 @@ function installGlobalPackage(pkg, { isVi, osChoice, displayName }) {
|
|
|
160
160
|
return false;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
function build9RouterSmartRouteSyncScript(dbPath) {
|
|
164
|
+
const safeDbPath = JSON.stringify(dbPath);
|
|
165
|
+
return `const fs=require('fs');
|
|
166
|
+
const INTERVAL=30000;
|
|
167
|
+
const p=${safeDbPath};
|
|
168
|
+
const PM={codex:['cx/gpt-5.4','cx/gpt-5.3-codex','cx/gpt-5.3-codex-high','cx/gpt-5.2-codex','cx/gpt-5.2','cx/gpt-5.1-codex-max','cx/gpt-5.1-codex','cx/gpt-5.1','cx/gpt-5-codex'],claude-code:['cc/claude-opus-4-6','cc/claude-sonnet-4-6','cc/claude-opus-4-5-20251101','cc/claude-sonnet-4-5-20250929','cc/claude-haiku-4-5-20251001'],github:['gh/gpt-5.4','gh/gpt-5.3-codex','gh/gpt-5.2-codex','gh/gpt-5.2','gh/gpt-5.1-codex-max','gh/gpt-5.1-codex','gh/gpt-5.1','gh/gpt-5','gh/gpt-4.1','gh/gpt-4o','gh/claude-opus-4.6','gh/claude-sonnet-4.6','gh/claude-sonnet-4.5','gh/claude-opus-4.5','gh/claude-haiku-4.5','gh/gemini-3-pro-preview','gh/gemini-3-flash-preview','gh/gemini-2.5-pro'],cursor:['cu/default','cu/claude-4.6-opus-max','cu/claude-4.5-opus-high-thinking','cu/claude-4.5-sonnet-thinking','cu/claude-4.5-sonnet','cu/gpt-5.3-codex','cu/gpt-5.2-codex','cu/gemini-3-flash-preview'],kilo:['kc/anthropic/claude-sonnet-4-20250514','kc/anthropic/claude-opus-4-20250514','kc/google/gemini-2.5-pro','kc/google/gemini-2.5-flash','kc/openai/gpt-4.1','kc/deepseek/deepseek-chat'],cline:['cl/anthropic/claude-sonnet-4.6','cl/anthropic/claude-opus-4.6','cl/openai/gpt-5.3-codex','cl/openai/gpt-5.4','cl/google/gemini-3.1-pro-preview'],'gemini-cli':['gc/gemini-3-flash-preview','gc/gemini-3-pro-preview'],iflow:['if/qwen3-coder-plus','if/kimi-k2','if/kimi-k2-thinking','if/glm-4.7','if/deepseek-r1','if/deepseek-v3.2','if/deepseek-v3','if/qwen3-max','if/qwen3-235b','if/iflow-rome-30ba3b'],qwen:['qw/qwen3-coder-plus','qw/qwen3-coder-flash','qw/vision-model','qw/coder-model'],kiro:['kr/claude-sonnet-4.5','kr/claude-haiku-4.5','kr/deepseek-3.2','kr/deepseek-3.1','kr/qwen3-coder-next'],ollama:['ollama/gemma4:e2b','ollama/gemma4:e4b','ollama/gemma4:26b','ollama/gemma4:31b','ollama/qwen3.5','ollama/kimi-k2.5','ollama/glm-5','ollama/glm-4.7-flash','ollama/minimax-m2.5','ollama/gpt-oss:120b'],'kimi-coding':['kmc/kimi-k2.5','kmc/kimi-k2.5-thinking','kmc/kimi-latest'],glm:['glm/glm-5.1','glm/glm-5','glm/glm-4.7'],'glm-cn':['glm/glm-5.1','glm/glm-5','glm/glm-4.7'],minimax:['minimax/MiniMax-M2.7','minimax/MiniMax-M2.5','minimax/MiniMax-M2.1'],kimi:['kimi/kimi-k2.5','kimi/kimi-k2.5-thinking','kimi/kimi-latest'],deepseek:['deepseek/deepseek-chat','deepseek/deepseek-reasoner'],xai:['xai/grok-4','xai/grok-4-fast-reasoning','xai/grok-code-fast-1'],mistral:['mistral/mistral-large-latest','mistral/codestral-latest'],groq:['groq/llama-3.3-70b-versatile','groq/openai/gpt-oss-120b'],cerebras:['cerebras/gpt-oss-120b'],alicode:['alicode/qwen3.5-plus','alicode/qwen3-coder-plus'],openai:['openai/gpt-4o','openai/gpt-4.1'],anthropic:['anthropic/claude-sonnet-4','anthropic/claude-haiku-3.5'],gemini:['gemini/gemini-2.5-flash','gemini/gemini-2.5-pro']};
|
|
169
|
+
console.log('[sync-combo] 9Router sync loop started...');
|
|
170
|
+
const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catch{}if(!db.combos)db.combos=[];const removeSmartRoute=()=>{const next=db.combos.filter(x=>x.id!=='smart-route');if(next.length!==db.combos.length){db.combos=next;fs.writeFileSync(p,JSON.stringify(db,null,2));console.log('[sync-combo] Removed smart-route (no active providers)');}};const a=(db.providerConnections||[]).filter(c=>c&&c.provider&&c.isActive!==false&&!c.disabled).map(c=>c.provider);if(!a.length){removeSmartRoute();return;}const PREF=['openai','anthropic','claude-code','codex','cursor','github','cline','kimi','minimax','deepseek','glm','alicode','xai','mistral','kilo','kiro','iflow','qwen','gemini-cli','ollama'];a.sort((x,y)=>(PREF.indexOf(x)===-1?99:PREF.indexOf(x))-(PREF.indexOf(y)===-1?99:PREF.indexOf(y)));const m=a.flatMap(provider=>PM[provider]||[]);if(!m.length){removeSmartRoute();return;}const c={id:'smart-route',name:'smart-route',alias:'smart-route',models:m};const i=db.combos.findIndex(x=>x.id==='smart-route');if(i>=0){if(JSON.stringify(db.combos[i].models)!==JSON.stringify(c.models)){db.combos[i]=c;fs.writeFileSync(p,JSON.stringify(db,null,2));console.log('[sync-combo] Updated smart-route: '+c.models.length+' models');}}else{db.combos.push(c);fs.writeFileSync(p,JSON.stringify(db,null,2));console.log('[sync-combo] Created smart-route: '+c.models.length+' models');}}catch{}};sync();setInterval(sync,INTERVAL);`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function writeNative9RouterSyncScript(projectDir) {
|
|
174
|
+
const syncScriptPath = path.join(projectDir, '.openclaw', '9router-smart-route-sync.js');
|
|
175
|
+
await fs.ensureDir(path.dirname(syncScriptPath));
|
|
176
|
+
await fs.writeFile(syncScriptPath, build9RouterSmartRouteSyncScript(path.join(os.homedir(), '.9router', 'db.json')));
|
|
177
|
+
return syncScriptPath;
|
|
178
|
+
}
|
|
179
|
+
|
|
163
180
|
function extractFirstHttpUrl(text) {
|
|
164
181
|
const match = String(text || '').match(/https?:\/\/[^\s"'`]+/);
|
|
165
182
|
return match ? match[0] : null;
|
|
@@ -209,10 +226,44 @@ function printNativeDashboardAccessInfo({ isVi, providerKey, projectDir, gateway
|
|
|
209
226
|
}
|
|
210
227
|
}
|
|
211
228
|
|
|
212
|
-
function
|
|
229
|
+
function printZaloPersonalLoginInfo({ isVi, deployMode, projectDir }) {
|
|
230
|
+
const nativeCmd = 'openclaw channels login --channel zalouser --verbose';
|
|
231
|
+
const dockerCmd = 'docker compose exec -it ai-bot openclaw channels login --channel zalouser --verbose';
|
|
232
|
+
const cmd = deployMode === 'native' ? nativeCmd : dockerCmd;
|
|
233
|
+
const qrPath = '/tmp/openclaw/openclaw-zalouser-qr-default.png';
|
|
234
|
+
const copyCmd = deployMode === 'native'
|
|
235
|
+
? `cp ${qrPath} ./zalo-login-qr.png`
|
|
236
|
+
: `docker compose cp ai-bot:${qrPath} ./zalo-login-qr.png`;
|
|
237
|
+
|
|
238
|
+
console.log(chalk.yellow(`\n📱 ${isVi ? 'Đăng nhập Zalo Personal (1 lần):' : 'Zalo Personal login (one time):'}`));
|
|
239
|
+
console.log(chalk.white(` cd ${projectDir}${deployMode === 'native' ? '' : '/docker/openclaw'} && ${cmd}`));
|
|
240
|
+
console.log(chalk.gray(isVi
|
|
241
|
+
? ` → OpenClaw sẽ tạo file QR tại: ${qrPath}`
|
|
242
|
+
: ` → OpenClaw will generate a QR image at: ${qrPath}`));
|
|
243
|
+
console.log(chalk.gray(isVi
|
|
244
|
+
? ` → Nếu cần copy QR ra ngoài, dùng: ${copyCmd}`
|
|
245
|
+
: ` → If needed, copy the QR out with: ${copyCmd}`));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function runPm2Save({ projectDir, isVi }) {
|
|
249
|
+
try {
|
|
250
|
+
execSync('pm2 save', {
|
|
251
|
+
cwd: projectDir,
|
|
252
|
+
stdio: 'inherit',
|
|
253
|
+
shell: true,
|
|
254
|
+
env: process.env
|
|
255
|
+
});
|
|
256
|
+
} catch {
|
|
257
|
+
console.log(chalk.yellow(isVi
|
|
258
|
+
? '⚠️ PM2 save khong hoan tat. Bot van co the dang chay, nhung hay thu chay lai `pm2 save` sau.'
|
|
259
|
+
: '⚠️ PM2 save did not complete. The app may still be running, but try `pm2 save` again afterwards.'));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
|
|
213
264
|
const routerAppName = `${appName}-9router`;
|
|
214
265
|
execSync(
|
|
215
|
-
`pm2 start "9router -n -t -l -H 0.0.0.0 -p 20128 --skip-update" --name "${routerAppName}" --cwd "${projectDir.replace(/\\/g, '/')}"
|
|
266
|
+
`pm2 start "9router -n -t -l -H 0.0.0.0 -p 20128 --skip-update" --name "${routerAppName}" --cwd "${projectDir.replace(/\\/g, '/')}"`,
|
|
216
267
|
{
|
|
217
268
|
cwd: projectDir,
|
|
218
269
|
stdio: 'inherit',
|
|
@@ -220,6 +271,19 @@ function startNative9RouterPm2({ isVi, projectDir, appName }) {
|
|
|
220
271
|
env: process.env
|
|
221
272
|
}
|
|
222
273
|
);
|
|
274
|
+
if (syncScriptPath) {
|
|
275
|
+
const syncAppName = `${appName}-9router-sync`;
|
|
276
|
+
execSync(
|
|
277
|
+
`pm2 start "${syncScriptPath.replace(/\\/g, '/')}" --name "${syncAppName}" --cwd "${projectDir.replace(/\\/g, '/')}"`,
|
|
278
|
+
{
|
|
279
|
+
cwd: projectDir,
|
|
280
|
+
stdio: 'inherit',
|
|
281
|
+
shell: true,
|
|
282
|
+
env: process.env
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
runPm2Save({ projectDir, isVi });
|
|
223
287
|
console.log(chalk.green(`\n✅ ${isVi ? '9Router da duoc khoi dong qua PM2.' : '9Router is running via PM2.'}`));
|
|
224
288
|
console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${routerAppName}` : ` View logs: pm2 logs ${routerAppName}`));
|
|
225
289
|
}
|
|
@@ -877,17 +941,31 @@ const sync = async () => {
|
|
|
877
941
|
try {
|
|
878
942
|
let db = {};
|
|
879
943
|
try { db = JSON.parse(fs.readFileSync(p, 'utf8')); } catch(e){}
|
|
944
|
+
if (!db.combos) db.combos = [];
|
|
945
|
+
const removeSmartRoute = () => {
|
|
946
|
+
const next = db.combos.filter(x => x.id !== 'smart-route');
|
|
947
|
+
if (next.length !== db.combos.length) {
|
|
948
|
+
db.combos = next;
|
|
949
|
+
fs.writeFileSync(p, JSON.stringify(db, null, 2));
|
|
950
|
+
console.log('[sync-combo] Removed smart-route (no active providers)');
|
|
951
|
+
}
|
|
952
|
+
};
|
|
880
953
|
const a = (db.providerConnections || [])
|
|
881
954
|
.filter(c => c && c.provider && c.isActive !== false && !c.disabled)
|
|
882
955
|
.map(c => c.provider);
|
|
883
|
-
if (!a.length)
|
|
956
|
+
if (!a.length) {
|
|
957
|
+
removeSmartRoute();
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
884
960
|
|
|
885
961
|
const PREF = ['openai','anthropic','claude-code','codex','cursor','github','cline','kimi','minimax','deepseek','glm','alicode','xai','mistral','kilo','kiro','iflow','qwen','gemini-cli','ollama'];
|
|
886
962
|
a.sort((x, y) => (PREF.indexOf(x) === -1 ? 99 : PREF.indexOf(x)) - (PREF.indexOf(y) === -1 ? 99 : PREF.indexOf(y)));
|
|
887
963
|
|
|
888
964
|
const m = a.flatMap(p => PM[p] || []);
|
|
889
|
-
if (!m.length)
|
|
890
|
-
|
|
965
|
+
if (!m.length) {
|
|
966
|
+
removeSmartRoute();
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
891
969
|
|
|
892
970
|
const c = { id: 'smart-route', name: 'smart-route', alias: 'smart-route', models: m };
|
|
893
971
|
const i = db.combos.findIndex(x => x.id === 'smart-route');
|
|
@@ -1580,7 +1658,11 @@ ${hasBrowserDesktop ? ` extra_hosts:
|
|
|
1580
1658
|
}
|
|
1581
1659
|
botConfig.channels['telegram'] = telegramConfig;
|
|
1582
1660
|
} else if (channelKey === 'zalo-personal') {
|
|
1583
|
-
botConfig.channels['
|
|
1661
|
+
botConfig.channels['zalouser'] = {
|
|
1662
|
+
enabled: true,
|
|
1663
|
+
dmPolicy: 'pairing',
|
|
1664
|
+
autoReply: true
|
|
1665
|
+
};
|
|
1584
1666
|
} else if (channelKey === 'zalo-bot') {
|
|
1585
1667
|
botConfig.channels['zalo'] = { enabled: true, provider: 'official_account' };
|
|
1586
1668
|
}
|
|
@@ -1817,8 +1899,7 @@ fi
|
|
|
1817
1899
|
: ' → Run scripts/telegram-post-install-check.mjs to get the real links, verify group/privacy, then add the bots and disable privacy mode.'));
|
|
1818
1900
|
}
|
|
1819
1901
|
} else if (channelKey === 'zalo-personal') {
|
|
1820
|
-
|
|
1821
|
-
console.log(`cd ${projectDir} && docker compose exec -it openclaw bun run core:onboard`);
|
|
1902
|
+
printZaloPersonalLoginInfo({ isVi, deployMode: 'docker', projectDir });
|
|
1822
1903
|
}
|
|
1823
1904
|
} else {
|
|
1824
1905
|
console.log(chalk.red(`\n\u274c Docker exited with code ${code}`));
|
|
@@ -1906,6 +1987,11 @@ fi
|
|
|
1906
1987
|
console.log(chalk.green(isVi ? '✅ 9Router da cai xong!' : '✅ 9Router installed!'));
|
|
1907
1988
|
}
|
|
1908
1989
|
|
|
1990
|
+
let native9RouterSyncScriptPath = null;
|
|
1991
|
+
if (providerKey === '9router') {
|
|
1992
|
+
native9RouterSyncScriptPath = await writeNative9RouterSyncScript(projectDir);
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1909
1995
|
await syncLocalConfigToHome(projectDir, isVi);
|
|
1910
1996
|
|
|
1911
1997
|
if (isMultiBot && channelKey === 'telegram') {
|
|
@@ -1922,7 +2008,7 @@ fi
|
|
|
1922
2008
|
|
|
1923
2009
|
if (isMultiBot && channelKey === 'telegram') {
|
|
1924
2010
|
if (providerKey === '9router') {
|
|
1925
|
-
startNative9RouterPm2({ isVi, projectDir, appName: botName || 'openclaw-multibot' });
|
|
2011
|
+
startNative9RouterPm2({ isVi, projectDir, appName: botName || 'openclaw-multibot', syncScriptPath: native9RouterSyncScriptPath });
|
|
1926
2012
|
}
|
|
1927
2013
|
execSync('pm2 start ecosystem.config.js && pm2 save', {
|
|
1928
2014
|
cwd: projectDir,
|
|
@@ -1932,10 +2018,13 @@ fi
|
|
|
1932
2018
|
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.'}`));
|
|
1933
2019
|
console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${botName || 'openclaw-multibot'}` : ` View logs: pm2 logs ${botName || 'openclaw-multibot'}`));
|
|
1934
2020
|
printNativeDashboardAccessInfo({ isVi, providerKey, projectDir });
|
|
2021
|
+
if (channelKey === 'zalo-personal') {
|
|
2022
|
+
printZaloPersonalLoginInfo({ isVi, deployMode: 'native', projectDir });
|
|
2023
|
+
}
|
|
1935
2024
|
} else {
|
|
1936
2025
|
const appName = botName || 'openclaw';
|
|
1937
2026
|
if (providerKey === '9router') {
|
|
1938
|
-
startNative9RouterPm2({ isVi, projectDir, appName });
|
|
2027
|
+
startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath: native9RouterSyncScriptPath });
|
|
1939
2028
|
}
|
|
1940
2029
|
execSync(`pm2 start "openclaw gateway run" --name "${appName}" --cwd "${projectDir.replace(/\\/g, '/')}" && pm2 save`, {
|
|
1941
2030
|
cwd: projectDir,
|
|
@@ -1945,6 +2034,9 @@ fi
|
|
|
1945
2034
|
console.log(chalk.green(`\n🎉 ${isVi ? 'Setup hoan tat! Bot native dang chay qua PM2.' : 'Setup complete! Native bot is running via PM2.'}`));
|
|
1946
2035
|
console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${appName}` : ` View logs: pm2 logs ${appName}`));
|
|
1947
2036
|
printNativeDashboardAccessInfo({ isVi, providerKey, projectDir });
|
|
2037
|
+
if (channelKey === 'zalo-personal') {
|
|
2038
|
+
printZaloPersonalLoginInfo({ isVi, deployMode: 'native', projectDir });
|
|
2039
|
+
}
|
|
1948
2040
|
}
|
|
1949
2041
|
} else {
|
|
1950
2042
|
if (providerKey === '9router') {
|
|
@@ -1955,10 +2047,21 @@ fi
|
|
|
1955
2047
|
stdio: 'ignore',
|
|
1956
2048
|
shell: process.platform === 'win32'
|
|
1957
2049
|
}).unref();
|
|
2050
|
+
if (native9RouterSyncScriptPath) {
|
|
2051
|
+
spawn('node', [native9RouterSyncScriptPath], {
|
|
2052
|
+
cwd: projectDir,
|
|
2053
|
+
detached: true,
|
|
2054
|
+
stdio: 'ignore',
|
|
2055
|
+
shell: process.platform === 'win32'
|
|
2056
|
+
}).unref();
|
|
2057
|
+
}
|
|
1958
2058
|
console.log(chalk.gray(isVi
|
|
1959
2059
|
? ' 9Router dashboard: http://localhost:20128/dashboard'
|
|
1960
2060
|
: ' 9Router dashboard: http://localhost:20128/dashboard'));
|
|
1961
2061
|
}
|
|
2062
|
+
if (channelKey === 'zalo-personal') {
|
|
2063
|
+
printZaloPersonalLoginInfo({ isVi, deployMode: 'native', projectDir });
|
|
2064
|
+
}
|
|
1962
2065
|
console.log(chalk.yellow(`\n${isVi ? 'Khoi dong native bot (foreground)...' : 'Starting native bot (foreground)...'}`));
|
|
1963
2066
|
const child = spawn('openclaw', ['gateway', 'run'], {
|
|
1964
2067
|
cwd: projectDir,
|
package/docs/install-native.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Native installation is designed for users who cannot or prefer not to use Docker. This includes deployments on Shared Hosting (cPanel), low-tier VPS environments, or Windows desktops for direct access.
|
|
4
4
|
|
|
5
|
-
OpenClaw v5.0
|
|
5
|
+
OpenClaw v5.1.0+ natively supports deployment script generation for Windows, Linux, VPS, and Hosting environments.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Chế độ Native được thiết kế dành cho những ai không thể hoặc không muốn cài Docker. Chế độ này thường tối ưu cho Shared Hosting (cPanel), các gói VPS cấu hình rất thấp, hoặc cài trực tiếp trên máy Window để chạy cá nhân.
|
|
4
4
|
|
|
5
|
-
OpenClaw v5.0
|
|
5
|
+
OpenClaw v5.1.0+ tự động sinh sẵn các script cài đặt dành riêng cho Windows, Linux, VPS và Hosting.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
package/package.json
CHANGED
package/setup.js
CHANGED
|
@@ -271,16 +271,16 @@
|
|
|
271
271
|
},
|
|
272
272
|
pluginInstall: '',
|
|
273
273
|
},
|
|
274
|
-
'zalo-personal': {
|
|
275
|
-
name: 'Zalo Personal',
|
|
276
|
-
envKeys: [],
|
|
277
|
-
envExtra: '',
|
|
278
|
-
credSteps: [
|
|
279
|
-
{ textVi: '⚠️ Zalo Personal dùng <strong>unofficial API (zca-js)</strong> — chỉ nên dùng tài khoản phụ', textEn: '⚠️ Zalo Personal uses <strong>unofficial API (zca-js)</strong> — use an alternate account' },
|
|
280
|
-
{ textVi: 'Sau khi
|
|
281
|
-
],
|
|
282
|
-
channelConfig: {
|
|
283
|
-
zalouser: {
|
|
274
|
+
'zalo-personal': {
|
|
275
|
+
name: 'Zalo Personal',
|
|
276
|
+
envKeys: [],
|
|
277
|
+
envExtra: '',
|
|
278
|
+
credSteps: [
|
|
279
|
+
{ textVi: '⚠️ Zalo Personal dùng <strong>unofficial API (zca-js)</strong> — chỉ nên dùng tài khoản phụ', textEn: '⚠️ Zalo Personal uses <strong>unofficial API (zca-js)</strong> — use an alternate account' },
|
|
280
|
+
{ textVi: 'Sau khi runtime chạy, dùng <code>openclaw channels login --channel zalouser --verbose</code> để tạo <strong>mã QR đăng nhập Zalo</strong>. Không cần onboard lại.', textEn: 'After the runtime is up, use <code>openclaw channels login --channel zalouser --verbose</code> to generate the <strong>Zalo login QR</strong>. No full onboard needed.' },
|
|
281
|
+
],
|
|
282
|
+
channelConfig: {
|
|
283
|
+
zalouser: {
|
|
284
284
|
enabled: true,
|
|
285
285
|
accounts: {
|
|
286
286
|
default: {
|
|
@@ -1520,7 +1520,7 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1520
1520
|
// Generate native script if native mode
|
|
1521
1521
|
if (isNativeMode) generateNativeScript();
|
|
1522
1522
|
|
|
1523
|
-
// Show/hide Zalo Personal
|
|
1523
|
+
// Show/hide Zalo Personal login notice
|
|
1524
1524
|
const zaloNotice = document.getElementById('zalo-onboard-notice');
|
|
1525
1525
|
const isZaloPersonal = state.channel === 'zalo-personal';
|
|
1526
1526
|
if (zaloNotice) {
|
|
@@ -1802,7 +1802,7 @@ model:
|
|
|
1802
1802
|
const browserPrefix = hasBrowser
|
|
1803
1803
|
? 'socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 & '
|
|
1804
1804
|
: '';
|
|
1805
|
-
// Patch config on every startup to
|
|
1805
|
+
// Patch config on every startup to keep gateway settings stable
|
|
1806
1806
|
const patchCmd = `node -e \\"const fs=require('fs'),p='/root/.openclaw/openclaw.json';if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));c.tools=Object.assign({},c.tools,{profile:'full',exec:{host:'gateway',security:'full',ask:'off'}});c.gateway=Object.assign({},c.gateway,{port:18791,bind:'custom',customBindHost:'0.0.0.0'});fs.writeFileSync(p,JSON.stringify(c,null,2));}\\" && `;
|
|
1807
1807
|
// Auto-approve device pairing after gateway starts (required since v2026.3.x)
|
|
1808
1808
|
const autoApproveCmd = '(while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done) & ';
|
|
@@ -1841,17 +1841,31 @@ const sync = async () => {
|
|
|
1841
1841
|
try {
|
|
1842
1842
|
let db = {};
|
|
1843
1843
|
try { db = JSON.parse(fs.readFileSync(p, 'utf8')); } catch(e){}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
.
|
|
1847
|
-
|
|
1844
|
+
if (!db.combos) db.combos = [];
|
|
1845
|
+
const removeSmartRoute = () => {
|
|
1846
|
+
const next = db.combos.filter(x => x.id !== 'smart-route');
|
|
1847
|
+
if (next.length !== db.combos.length) {
|
|
1848
|
+
db.combos = next;
|
|
1849
|
+
fs.writeFileSync(p, JSON.stringify(db, null, 2));
|
|
1850
|
+
console.log('[sync-combo] Removed smart-route (no active providers)');
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
const a = (db.providerConnections || [])
|
|
1854
|
+
.filter(c => c && c.provider && c.isActive !== false && !c.disabled)
|
|
1855
|
+
.map(c => c.provider);
|
|
1856
|
+
if (!a.length) {
|
|
1857
|
+
removeSmartRoute();
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1848
1860
|
|
|
1849
1861
|
const PREF = ['openai','anthropic','claude-code','codex','cursor','github','cline','kimi','minimax','deepseek','glm','alicode','xai','mistral','kilo','kiro','iflow','qwen','gemini-cli','ollama'];
|
|
1850
1862
|
a.sort((x, y) => (PREF.indexOf(x) === -1 ? 99 : PREF.indexOf(x)) - (PREF.indexOf(y) === -1 ? 99 : PREF.indexOf(y)));
|
|
1851
1863
|
|
|
1852
|
-
const m = a.flatMap(p => PM[p] || []);
|
|
1853
|
-
if (!m.length)
|
|
1854
|
-
|
|
1864
|
+
const m = a.flatMap(p => PM[p] || []);
|
|
1865
|
+
if (!m.length) {
|
|
1866
|
+
removeSmartRoute();
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1855
1869
|
|
|
1856
1870
|
const c = { id: 'smart-route', name: 'smart-route', alias: 'smart-route', models: m };
|
|
1857
1871
|
const i = db.combos.findIndex(x => x.id === 'smart-route');
|
|
@@ -2832,8 +2846,17 @@ I am **${botName}**. When asked my name, I answer: _"I'm ${botName}"_.`;
|
|
|
2832
2846
|
const p = PLUGINS.find((x) => x.id === pid);
|
|
2833
2847
|
if (p) allPlugins.push(p.package);
|
|
2834
2848
|
});
|
|
2835
|
-
if (isMultiBot && state.channel === 'telegram') allPlugins.push(relayPluginSpec);
|
|
2836
|
-
const pluginCmd = allPlugins.length > 0 ? ('npm exec openclaw plugins install ' + allPlugins.join(' ')) : '';
|
|
2849
|
+
if (isMultiBot && state.channel === 'telegram') allPlugins.push(relayPluginSpec);
|
|
2850
|
+
const pluginCmd = allPlugins.length > 0 ? ('npm exec openclaw plugins install ' + allPlugins.join(' ')) : '';
|
|
2851
|
+
|
|
2852
|
+
function native9RouterSyncScriptContent() {
|
|
2853
|
+
return `const fs=require('fs');
|
|
2854
|
+
const path=require('path');
|
|
2855
|
+
const INTERVAL=30000;
|
|
2856
|
+
const p=path.join(process.env.HOME||process.env.USERPROFILE||'.','.9router','db.json');
|
|
2857
|
+
const PM={codex:['cx/gpt-5.4','cx/gpt-5.3-codex','cx/gpt-5.3-codex-high','cx/gpt-5.2-codex','cx/gpt-5.2','cx/gpt-5.1-codex-max','cx/gpt-5.1-codex','cx/gpt-5.1','cx/gpt-5-codex'],claude-code:['cc/claude-opus-4-6','cc/claude-sonnet-4-6','cc/claude-opus-4-5-20251101','cc/claude-sonnet-4-5-20250929','cc/claude-haiku-4-5-20251001'],github:['gh/gpt-5.4','gh/gpt-5.3-codex','gh/gpt-5.2-codex','gh/gpt-5.2','gh/gpt-5.1-codex-max','gh/gpt-5.1-codex','gh/gpt-5.1','gh/gpt-5','gh/gpt-4.1','gh/gpt-4o','gh/claude-opus-4.6','gh/claude-sonnet-4.6','gh/claude-sonnet-4.5','gh/claude-opus-4.5','gh/claude-haiku-4.5','gh/gemini-3-pro-preview','gh/gemini-3-flash-preview','gh/gemini-2.5-pro'],cursor:['cu/default','cu/claude-4.6-opus-max','cu/claude-4.5-opus-high-thinking','cu/claude-4.5-sonnet-thinking','cu/claude-4.5-sonnet','cu/gpt-5.3-codex','cu/gpt-5.2-codex','cu/gemini-3-flash-preview'],kilo:['kc/anthropic/claude-sonnet-4-20250514','kc/anthropic/claude-opus-4-20250514','kc/google/gemini-2.5-pro','kc/google/gemini-2.5-flash','kc/openai/gpt-4.1','kc/deepseek/deepseek-chat'],cline:['cl/anthropic/claude-sonnet-4.6','cl/anthropic/claude-opus-4.6','cl/openai/gpt-5.3-codex','cl/openai/gpt-5.4','cl/google/gemini-3.1-pro-preview'],'gemini-cli':['gc/gemini-3-flash-preview','gc/gemini-3-pro-preview'],iflow:['if/qwen3-coder-plus','if/kimi-k2','if/kimi-k2-thinking','if/glm-4.7','if/deepseek-r1','if/deepseek-v3.2','if/deepseek-v3','if/qwen3-max','if/qwen3-235b','if/iflow-rome-30ba3b'],qwen:['qw/qwen3-coder-plus','qw/qwen3-coder-flash','qw/vision-model','qw/coder-model'],kiro:['kr/claude-sonnet-4.5','kr/claude-haiku-4.5','kr/deepseek-3.2','kr/deepseek-3.1','kr/qwen3-coder-next'],ollama:['ollama/gemma4:e2b','ollama/gemma4:e4b','ollama/gemma4:26b','ollama/gemma4:31b','ollama/qwen3.5','ollama/kimi-k2.5','ollama/glm-5','ollama/glm-4.7-flash','ollama/minimax-m2.5','ollama/gpt-oss:120b'],'kimi-coding':['kmc/kimi-k2.5','kmc/kimi-k2.5-thinking','kmc/kimi-latest'],glm:['glm/glm-5.1','glm/glm-5','glm/glm-4.7'],'glm-cn':['glm/glm-5.1','glm/glm-5','glm/glm-4.7'],minimax:['minimax/MiniMax-M2.7','minimax/MiniMax-M2.5','minimax/MiniMax-M2.1'],kimi:['kimi/kimi-k2.5','kimi/kimi-k2.5-thinking','kimi/kimi-latest'],deepseek:['deepseek/deepseek-chat','deepseek/deepseek-reasoner'],xai:['xai/grok-4','xai/grok-4-fast-reasoning','xai/grok-code-fast-1'],mistral:['mistral/mistral-large-latest','mistral/codestral-latest'],groq:['groq/llama-3.3-70b-versatile','groq/openai/gpt-oss-120b'],cerebras:['cerebras/gpt-oss-120b'],alicode:['alicode/qwen3.5-plus','alicode/qwen3-coder-plus'],openai:['openai/gpt-4o','openai/gpt-4.1'],anthropic:['anthropic/claude-sonnet-4','anthropic/claude-haiku-3.5'],gemini:['gemini/gemini-2.5-flash','gemini/gemini-2.5-pro']};
|
|
2858
|
+
const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catch{}if(!db.combos)db.combos=[];const removeSmartRoute=()=>{const next=db.combos.filter(x=>x.id!=='smart-route');if(next.length!==db.combos.length){db.combos=next;fs.writeFileSync(p,JSON.stringify(db,null,2));}};const a=(db.providerConnections||[]).filter(c=>c&&c.provider&&c.isActive!==false&&!c.disabled).map(c=>c.provider);if(!a.length){removeSmartRoute();return;}const PREF=['openai','anthropic','claude-code','codex','cursor','github','cline','kimi','minimax','deepseek','glm','alicode','xai','mistral','kilo','kiro','iflow','qwen','gemini-cli','ollama'];a.sort((x,y)=>(PREF.indexOf(x)===-1?99:PREF.indexOf(x))-(PREF.indexOf(y)===-1?99:PREF.indexOf(y)));const m=a.flatMap(provider=>PM[provider]||[]);if(!m.length){removeSmartRoute();return;}const c={id:'smart-route',name:'smart-route',alias:'smart-route',models:m};const i=db.combos.findIndex(x=>x.id==='smart-route');if(i>=0){if(JSON.stringify(db.combos[i].models)!==JSON.stringify(c.models)){db.combos[i]=c;fs.writeFileSync(p,JSON.stringify(db,null,2));}}else{db.combos.push(c);fs.writeFileSync(p,JSON.stringify(db,null,2));}}catch{}};sync();setInterval(sync,INTERVAL);`;
|
|
2859
|
+
}
|
|
2837
2860
|
|
|
2838
2861
|
// ─── Shared initializer (provider install) ───────────────────────────────
|
|
2839
2862
|
function providerLines(arr, shell) {
|
|
@@ -2841,10 +2864,12 @@ I am **${botName}**. When asked my name, I answer: _"I'm ${botName}"_.`;
|
|
|
2841
2864
|
if (shell === 'bat') {
|
|
2842
2865
|
arr.push('npm install -g 9router');
|
|
2843
2866
|
arr.push('start "9Router" cmd /k "9router -n -t -l -H 0.0.0.0 -p 20128 --skip-update"');
|
|
2867
|
+
arr.push('start "9Router Smart Route Sync" cmd /k "node .\\.openclaw\\9router-smart-route-sync.js"');
|
|
2844
2868
|
arr.push('timeout /t 5 /nobreak >nul');
|
|
2845
2869
|
} else {
|
|
2846
2870
|
arr.push('npm install -g 9router');
|
|
2847
2871
|
arr.push('nohup 9router -n -t -l -H 0.0.0.0 -p 20128 --skip-update >/tmp/9router.log 2>&1 &');
|
|
2872
|
+
arr.push('nohup node ./.openclaw/9router-smart-route-sync.js >/tmp/9router-sync.log 2>&1 &');
|
|
2848
2873
|
arr.push('sleep 3');
|
|
2849
2874
|
}
|
|
2850
2875
|
} else if (isOllama) {
|
|
@@ -3015,13 +3040,14 @@ I am **${botName}**. When asked my name, I answer: _"I'm ${botName}"_.`;
|
|
|
3015
3040
|
}
|
|
3016
3041
|
|
|
3017
3042
|
function sharedNativeFileMap() {
|
|
3018
|
-
const files = {
|
|
3019
|
-
'.env': sharedNativeEnvContent(),
|
|
3020
|
-
'.openclaw/openclaw.json': sharedNativeConfigContent(),
|
|
3021
|
-
'.openclaw/exec-approvals.json': sharedNativeExecApprovalsContent(),
|
|
3022
|
-
'.openclaw/auth-profiles.json': sharedNativeAuthProfilesContent(),
|
|
3023
|
-
'TELEGRAM-POST-INSTALL.md': buildTelegramPostInstallChecklist(),
|
|
3024
|
-
};
|
|
3043
|
+
const files = {
|
|
3044
|
+
'.env': sharedNativeEnvContent(),
|
|
3045
|
+
'.openclaw/openclaw.json': sharedNativeConfigContent(),
|
|
3046
|
+
'.openclaw/exec-approvals.json': sharedNativeExecApprovalsContent(),
|
|
3047
|
+
'.openclaw/auth-profiles.json': sharedNativeAuthProfilesContent(),
|
|
3048
|
+
'TELEGRAM-POST-INSTALL.md': buildTelegramPostInstallChecklist(),
|
|
3049
|
+
};
|
|
3050
|
+
if (is9Router) files['.openclaw/9router-smart-route-sync.js'] = native9RouterSyncScriptContent();
|
|
3025
3051
|
const teamMd = isVi
|
|
3026
3052
|
? `# Doi Bot\n\n${multiBotAgentMetas.map((meta) => `## ${meta.name}\n- Vai tro: ${meta.desc}\n- Agent ID: \`${meta.agentId}\`\n- Telegram accountId: \`${meta.accountId}\`\n- Slash command: ${meta.slashCmd || '_(chua co)_'}\n- Tinh cach: ${meta.persona || '_(khong ghi ro)_'}`).join('\n\n')}\n\n## Quy uoc phoi hop\n- Tat ca bot trong doi biet ro vai tro cua nhau.\n- Neu user bao ban hoi mot bot khac, hay dung agent-to-agent noi bo thay vi doi Telegram chuyen tin cua bot.\n- Bot mo loi chi noi 1 cau ngan, sau do chuyen turn noi bo cho bot dich.\n- Bot dich phai tra loi cong khai bang chinh Telegram account cua minh trong cung chat/thread hien tai.\n- Neu can fallback, chi bot mo loi moi duoc phep tom tat thay.`
|
|
3027
3053
|
: `# Bot Team\n\n${multiBotAgentMetas.map((meta) => `## ${meta.name}\n- Role: ${meta.desc}\n- Agent ID: \`${meta.agentId}\`\n- Telegram accountId: \`${meta.accountId}\`\n- Slash command: ${meta.slashCmd || '_(not set)_'}\n- Persona: ${meta.persona || '_(not specified)_'}`).join('\n\n')}\n\n## Coordination Rules\n- Every bot knows the full roster.\n- If the user asks you to consult another bot, use internal agent-to-agent handoff instead of waiting for Telegram bot-to-bot delivery.\n- The caller bot only sends one short opener, then hands off internally.\n- The target bot must publish the real answer with its own Telegram account in the same chat/thread.\n- If a fallback is needed, only the caller bot may summarize on behalf of the target.`;
|
|
@@ -3369,10 +3395,11 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3369
3395
|
const base = '.';
|
|
3370
3396
|
const files = {};
|
|
3371
3397
|
files[`${base}/.env`] = botEnvContent(botIndex);
|
|
3372
|
-
files[`${base}/.openclaw/openclaw.json`] = botConfigContent(botIndex);
|
|
3373
|
-
files[`${base}/.openclaw/exec-approvals.json`] = botExecApprovalsContent(botIndex);
|
|
3374
|
-
files[`${base}/.openclaw/auth-profiles.json`] = botAuthProfilesContent(botIndex);
|
|
3375
|
-
files[`${base}/.openclaw/
|
|
3398
|
+
files[`${base}/.openclaw/openclaw.json`] = botConfigContent(botIndex);
|
|
3399
|
+
files[`${base}/.openclaw/exec-approvals.json`] = botExecApprovalsContent(botIndex);
|
|
3400
|
+
files[`${base}/.openclaw/auth-profiles.json`] = botAuthProfilesContent(botIndex);
|
|
3401
|
+
if (is9Router) files[`${base}/.openclaw/9router-smart-route-sync.js`] = native9RouterSyncScriptContent();
|
|
3402
|
+
files[`${base}/.openclaw/agents/${agentId}.yaml`] = botAgentYamlContent(botIndex);
|
|
3376
3403
|
files[`${base}/.openclaw/agents/${agentId}/agent/auth-profiles.json`] = botAuthProfilesContent(botIndex);
|
|
3377
3404
|
Object.entries(botWorkspaceFiles(botIndex)).forEach(([name, content]) => {
|
|
3378
3405
|
files[`${base}/.openclaw/workspace/${name}`] = content;
|
|
@@ -3497,21 +3524,29 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3497
3524
|
providerLines(vps, 'sh');
|
|
3498
3525
|
if (pluginCmd) vps.push(pluginCmd);
|
|
3499
3526
|
|
|
3500
|
-
if (isMultiBot) {
|
|
3501
|
-
vps.push('echo "--- Creating shared multi-agent runtime ---"');
|
|
3502
|
-
appendShWriteCommands(vps, sharedNativeFileMap());
|
|
3503
|
-
vps.push('echo "--- Starting shared gateway via PM2 ---"');
|
|
3504
|
-
|
|
3505
|
-
|
|
3527
|
+
if (isMultiBot) {
|
|
3528
|
+
vps.push('echo "--- Creating shared multi-agent runtime ---"');
|
|
3529
|
+
appendShWriteCommands(vps, sharedNativeFileMap());
|
|
3530
|
+
vps.push('echo "--- Starting shared gateway via PM2 ---"');
|
|
3531
|
+
if (is9Router) {
|
|
3532
|
+
vps.push('pm2 start --name openclaw-multibot-9router -- sh -c "9router -n -t -l -H 0.0.0.0 -p 20128 --skip-update"');
|
|
3533
|
+
vps.push('pm2 start --name openclaw-multibot-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
|
|
3534
|
+
}
|
|
3535
|
+
vps.push('pm2 start --name openclaw-multibot -- sh -c "openclaw gateway run"');
|
|
3536
|
+
vps.push('pm2 save && pm2 startup');
|
|
3506
3537
|
vps.push(`echo ""`);
|
|
3507
3538
|
vps.push(`echo "=== ✅ Shared multi-bot gateway running via PM2 ==="`);
|
|
3508
3539
|
vps.push(`echo "Commands:"`);
|
|
3509
3540
|
vps.push(`echo " pm2 status # Status gateway"`);
|
|
3510
3541
|
vps.push(`echo " pm2 logs openclaw-multibot"`);
|
|
3511
|
-
} else {
|
|
3512
|
-
appendShWriteCommands(vps, botFiles(0));
|
|
3513
|
-
|
|
3514
|
-
|
|
3542
|
+
} else {
|
|
3543
|
+
appendShWriteCommands(vps, botFiles(0));
|
|
3544
|
+
if (is9Router) {
|
|
3545
|
+
vps.push('pm2 start --name openclaw-9router -- sh -c "9router -n -t -l -H 0.0.0.0 -p 20128 --skip-update"');
|
|
3546
|
+
vps.push('pm2 start --name openclaw-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
|
|
3547
|
+
}
|
|
3548
|
+
vps.push('pm2 start --name openclaw -- sh -c "openclaw gateway run"');
|
|
3549
|
+
vps.push('pm2 save && pm2 startup');
|
|
3515
3550
|
vps.push('echo "Bot dang chay! Xem log: pm2 logs openclaw"');
|
|
3516
3551
|
}
|
|
3517
3552
|
scriptContent = vps.filter(Boolean).join('\n');
|
|
@@ -3683,9 +3718,11 @@ Write-Host " 🎉 ${isVi ? 'Setup hoàn tất!' : 'Setup complete!'}" -Foregrou
|
|
|
3683
3718
|
if (is9Router) {
|
|
3684
3719
|
ps += `Write-Host " ${isVi ? 'Mở http://localhost:30128/dashboard để login OAuth' : 'Open http://localhost:30128/dashboard to login OAuth'}" -ForegroundColor White\n`;
|
|
3685
3720
|
}
|
|
3686
|
-
if (state.channel === 'zalo-personal') {
|
|
3687
|
-
ps += `Write-Host " ${isVi ? 'Chạy: docker exec -it
|
|
3688
|
-
|
|
3721
|
+
if (state.channel === 'zalo-personal') {
|
|
3722
|
+
ps += `Write-Host " ${isVi ? 'Chạy: docker compose exec -it ai-bot openclaw channels login --channel zalouser --verbose' : 'Run: docker compose exec -it ai-bot openclaw channels login --channel zalouser --verbose'}" -ForegroundColor White\n`;
|
|
3723
|
+
ps += `Write-Host " ${isVi ? 'QR sẽ nằm tại /tmp/openclaw/openclaw-zalouser-qr-default.png' : 'QR will be written to /tmp/openclaw/openclaw-zalouser-qr-default.png'}" -ForegroundColor DarkGray\n`;
|
|
3724
|
+
ps += `Write-Host " ${isVi ? 'Copy QR ra ngoài: docker compose cp ai-bot:/tmp/openclaw/openclaw-zalouser-qr-default.png ./zalo-login-qr.png' : 'Copy the QR out: docker compose cp ai-bot:/tmp/openclaw/openclaw-zalouser-qr-default.png ./zalo-login-qr.png'}" -ForegroundColor DarkGray\n`;
|
|
3725
|
+
}
|
|
3689
3726
|
|
|
3690
3727
|
ps += `Write-Host ""
|
|
3691
3728
|
} catch {
|
|
@@ -3858,72 +3895,40 @@ echo ""
|
|
|
3858
3895
|
}
|
|
3859
3896
|
|
|
3860
3897
|
|
|
3861
|
-
// ========== Zalo Personal
|
|
3862
|
-
|
|
3863
|
-
function generateZaloOnboardGuide() {
|
|
3864
|
-
const lang = document.getElementById('cfg-language')?.value || 'vi';
|
|
3865
|
-
setOutput('out-zalo-onboard-cmd', `docker exec -it
|
|
3866
|
-
|
|
3867
|
-
if (lang === 'vi') {
|
|
3868
|
-
setOutput('out-zalo-onboard-guide', `┌─────────────────────────────────────────────────────┐
|
|
3869
|
-
│
|
|
3870
|
-
|
|
3871
|
-
│
|
|
3872
|
-
|
|
3873
|
-
│
|
|
3874
|
-
│
|
|
3875
|
-
│
|
|
3876
|
-
│
|
|
3877
|
-
│
|
|
3878
|
-
│
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
│
|
|
3883
|
-
|
|
3884
|
-
│
|
|
3885
|
-
│
|
|
3886
|
-
│
|
|
3887
|
-
│
|
|
3888
|
-
|
|
3889
|
-
│
|
|
3890
|
-
│
|
|
3891
|
-
│
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
└─────────────────────────────────────────────────────┘`);
|
|
3896
|
-
} else {
|
|
3897
|
-
setOutput('out-zalo-onboard-guide', `┌─────────────────────────────────────────────────────┐
|
|
3898
|
-
│ OpenClaw will prompt you — choose as follows: │
|
|
3899
|
-
├──────────────────────┬──────────────────────────────┤
|
|
3900
|
-
│ Prompt │ Choice │
|
|
3901
|
-
├──────────────────────┼──────────────────────────────┤
|
|
3902
|
-
│ Security warning │ ✅ Yes │
|
|
3903
|
-
│ Setup mode │ ✅ QuickStart │
|
|
3904
|
-
│ Config handling │ ✅ Use existing values │
|
|
3905
|
-
│ Model/auth provider │ Choose any (e.g. Google) │
|
|
3906
|
-
│ API key │ Enter key (or press Enter │
|
|
3907
|
-
│ │ if already in .env) │
|
|
3908
|
-
│ Select channel │ ✅ Zalo (Personal Account) │
|
|
3909
|
-
│ Login via QR? │ ✅ Yes │
|
|
3910
|
-
│ ─── QR LOGIN ─── │ 📱 Open QR file → Scan Zalo │
|
|
3911
|
-
│ Did you scan QR? │ ✅ Yes │
|
|
3912
|
-
│ DM policy │ ✅ Pairing (recommended) │
|
|
3913
|
-
│ Configure groups? │ ✅ No │
|
|
3914
|
-
│ Configure skills? │ ✅ No │
|
|
3915
|
-
│ Enable hooks? │ ✅ Enter (default) │
|
|
3916
|
-
│ Hatch your bot? │ ✅ Do this later │
|
|
3917
|
-
├──────────────────────┴──────────────────────────────┤
|
|
3918
|
-
│ 💡 QR Login Step: │
|
|
3919
|
-
│ When prompted, OpenClaw saves the QR code to │
|
|
3920
|
-
│ /tmp inside the container. │
|
|
3921
|
-
│ Run: docker cp openclaw-bot:/tmp/qr.png . │
|
|
3922
|
-
│ Open image → scan with Zalo mobile app → │
|
|
3923
|
-
│ confirm login → go back & select Yes. │
|
|
3924
|
-
└─────────────────────────────────────────────────────┘`);
|
|
3925
|
-
}
|
|
3926
|
-
}
|
|
3898
|
+
// ========== Zalo Personal Login Guide (post-setup) ==========
|
|
3899
|
+
|
|
3900
|
+
function generateZaloOnboardGuide() {
|
|
3901
|
+
const lang = document.getElementById('cfg-language')?.value || 'vi';
|
|
3902
|
+
setOutput('out-zalo-onboard-cmd', `docker compose exec -it ai-bot openclaw channels login --channel zalouser --verbose`);
|
|
3903
|
+
|
|
3904
|
+
if (lang === 'vi') {
|
|
3905
|
+
setOutput('out-zalo-onboard-guide', `┌─────────────────────────────────────────────────────┐
|
|
3906
|
+
│ Chạy lệnh bên trái để OpenClaw tạo QR đăng nhập. │
|
|
3907
|
+
├─────────────────────────────────────────────────────┤
|
|
3908
|
+
│ 1. Đảm bảo container/gateway đã chạy xong. │
|
|
3909
|
+
│ 2. Chạy lệnh login để tạo QR cho zalouser. │
|
|
3910
|
+
│ 3. OpenClaw sẽ in ra đường dẫn file QR trong /tmp. │
|
|
3911
|
+
│ 4. Copy file QR ra ngoài nếu cần: │
|
|
3912
|
+
│ docker compose cp ai-bot:/tmp/openclaw/ │
|
|
3913
|
+
│ openclaw-zalouser-qr-default.png . │
|
|
3914
|
+
│ 5. Mở ảnh QR → quét bằng app Zalo → xác nhận. │
|
|
3915
|
+
│ 6. Sau khi login xong, restart bot nếu cần. │
|
|
3916
|
+
└─────────────────────────────────────────────────────┘`);
|
|
3917
|
+
} else {
|
|
3918
|
+
setOutput('out-zalo-onboard-guide', `┌─────────────────────────────────────────────────────┐
|
|
3919
|
+
│ Run the command on the left to generate a Zalo QR. │
|
|
3920
|
+
├─────────────────────────────────────────────────────┤
|
|
3921
|
+
│ 1. Make sure the container/gateway is already up. │
|
|
3922
|
+
│ 2. Run the login command for zalouser. │
|
|
3923
|
+
│ 3. OpenClaw prints the QR image path under /tmp. │
|
|
3924
|
+
│ 4. Copy the QR out if needed: │
|
|
3925
|
+
│ docker compose cp ai-bot:/tmp/openclaw/ │
|
|
3926
|
+
│ openclaw-zalouser-qr-default.png . │
|
|
3927
|
+
│ 5. Open the image → scan with Zalo mobile app. │
|
|
3928
|
+
│ 6. Restart the bot afterwards if needed. │
|
|
3929
|
+
└─────────────────────────────────────────────────────┘`);
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3927
3932
|
|
|
3928
3933
|
function setOutput(id, text) {
|
|
3929
3934
|
const el = document.getElementById(id);
|
|
@@ -56,6 +56,18 @@ checks.push(() => expectMatch(
|
|
|
56
56
|
'Native 9Router flow must auto-install 9Router'
|
|
57
57
|
));
|
|
58
58
|
|
|
59
|
+
checks.push(() => expectMatch(
|
|
60
|
+
cli,
|
|
61
|
+
/async function writeNative9RouterSyncScript\(projectDir\) \{[\s\S]*9router-smart-route-sync\.js[\s\S]*providerConnections[\s\S]*smart-route/s,
|
|
62
|
+
'Native 9Router flow must write a smart-route sync script based on ~/.9router/db.json'
|
|
63
|
+
));
|
|
64
|
+
|
|
65
|
+
checks.push(() => expectMatch(
|
|
66
|
+
cli,
|
|
67
|
+
/Removed smart-route \(no active providers\)[\s\S]*if \(!a\.length\) \{[\s\S]*removeSmartRoute\(\)[\s\S]*if \(!m\.length\) \{[\s\S]*removeSmartRoute\(\)/s,
|
|
68
|
+
'9Router sync logic in CLI must remove stale smart-route combos when providers are disabled'
|
|
69
|
+
));
|
|
70
|
+
|
|
59
71
|
checks.push(() => expectMatch(
|
|
60
72
|
cli,
|
|
61
73
|
/function ensureUserWritableGlobalNpm\(\{ isVi, osChoice \}\) \{[\s\S]*process\.env\.npm_config_prefix = npmInfo\.prefixDir[\s\S]*npm config set prefix "\$\{npmInfo\.prefixDir\.replace/s,
|
|
@@ -80,6 +92,12 @@ checks.push(() => expectMatch(
|
|
|
80
92
|
'Native PM2 flow must expose dashboard access info and the tokenized dashboard command'
|
|
81
93
|
));
|
|
82
94
|
|
|
95
|
+
checks.push(() => expectMatch(
|
|
96
|
+
cli,
|
|
97
|
+
/function printZaloPersonalLoginInfo\(\{ isVi, deployMode, projectDir \}\) \{[\s\S]*docker compose exec -it ai-bot openclaw channels login --channel zalouser --verbose[\s\S]*const copyCmd = deployMode === 'native'[\s\S]*docker compose cp ai-bot:\$\{qrPath\} \.\/zalo-login-qr\.png/s,
|
|
98
|
+
'CLI must print the dedicated Docker/native Zalo Personal login commands and QR copy path instead of onboarding'
|
|
99
|
+
));
|
|
100
|
+
|
|
83
101
|
checks.push(() => expectMatch(
|
|
84
102
|
cli,
|
|
85
103
|
/baseUrl: deployMode === 'native' \? 'http:\/\/localhost:20128\/v1' : 'http:\/\/9router:20128\/v1'/,
|
|
@@ -88,10 +106,22 @@ checks.push(() => expectMatch(
|
|
|
88
106
|
|
|
89
107
|
checks.push(() => expectMatch(
|
|
90
108
|
cli,
|
|
91
|
-
/
|
|
109
|
+
/channelKey === 'zalo-personal'\) \{\s*botConfig\.channels\['zalouser'\] = \{\s*enabled: true,\s*dmPolicy: 'pairing',\s*autoReply: true/s,
|
|
110
|
+
'CLI must configure Zalo Personal under channels.zalouser'
|
|
111
|
+
));
|
|
112
|
+
|
|
113
|
+
checks.push(() => expectMatch(
|
|
114
|
+
cli,
|
|
115
|
+
/function startNative9RouterPm2\(\{ isVi, projectDir, appName, syncScriptPath \}\) \{[\s\S]*9router -n -t -l -H 0\.0\.0\.0 -p 20128 --skip-update[\s\S]*9router-sync[\s\S]*runPm2Save\(\{ projectDir, isVi \}\)/s,
|
|
92
116
|
'VPS native 9Router flow must start a standalone 9Router dashboard on port 20128 via PM2'
|
|
93
117
|
));
|
|
94
118
|
|
|
119
|
+
checks.push(() => expectMatch(
|
|
120
|
+
cli,
|
|
121
|
+
/function runPm2Save\(\{ projectDir, isVi \}\) \{[\s\S]*execSync\('pm2 save'[\s\S]*PM2 save did not complete/s,
|
|
122
|
+
'Native PM2 save should be handled as a separate recoverable step'
|
|
123
|
+
));
|
|
124
|
+
|
|
95
125
|
checks.push(() => expectMatch(
|
|
96
126
|
cli,
|
|
97
127
|
/const child = spawn\('openclaw', \['gateway', 'run'\], \{/,
|
|
@@ -153,6 +183,24 @@ checks.push(() => expectMatch(
|
|
|
153
183
|
'Native script generation must install and start a standalone 9Router dashboard on port 20128'
|
|
154
184
|
));
|
|
155
185
|
|
|
186
|
+
checks.push(() => expectMatch(
|
|
187
|
+
setup,
|
|
188
|
+
/function native9RouterSyncScriptContent\(\) \{[\s\S]*providerConnections[\s\S]*smart-route/s,
|
|
189
|
+
'Native script generation must embed a 9Router smart-route sync script'
|
|
190
|
+
));
|
|
191
|
+
|
|
192
|
+
checks.push(() => expectMatch(
|
|
193
|
+
setup,
|
|
194
|
+
/Removed smart-route \(no active providers\)[\s\S]*if \(!a\.length\) \{[\s\S]*removeSmartRoute\(\)[\s\S]*if \(!m\.length\) \{[\s\S]*removeSmartRoute\(\)/s,
|
|
195
|
+
'9Router sync logic in setup.js must remove stale smart-route combos when providers are disabled'
|
|
196
|
+
));
|
|
197
|
+
|
|
198
|
+
checks.push(() => expectMatch(
|
|
199
|
+
setup,
|
|
200
|
+
/\.openclaw\/9router-smart-route-sync\.js[\s\S]*pm2 start --name openclaw-9router-sync/s,
|
|
201
|
+
'VPS native script generation must write and run the 9Router smart-route sync loop'
|
|
202
|
+
));
|
|
203
|
+
|
|
156
204
|
checks.push(() => expectMatch(
|
|
157
205
|
cli,
|
|
158
206
|
/readdirSync\(dir\)\.find\(n=>\/\^gateway-cli-.*\\\\\.js\$\/\.test\(n\)\)[\s\S]*skipping timeout patch/,
|
|
@@ -195,6 +243,12 @@ checks.push(() => expectMatch(
|
|
|
195
243
|
'Auto-steps summary must mention OpenClaw CLI installation'
|
|
196
244
|
));
|
|
197
245
|
|
|
246
|
+
checks.push(() => expectMatch(
|
|
247
|
+
setup,
|
|
248
|
+
/docker compose exec -it ai-bot openclaw channels login --channel zalouser --verbose[\s\S]*docker compose cp ai-bot:\/tmp\/openclaw\/openclaw-zalouser-qr-default\.png \.\/zalo-login-qr\.png/s,
|
|
249
|
+
'Wizard copy must use the dedicated zalouser login command and Docker QR copy path'
|
|
250
|
+
));
|
|
251
|
+
|
|
198
252
|
for (const check of checks) {
|
|
199
253
|
check();
|
|
200
254
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
const configPath = process.argv[2];
|
|
4
|
+
|
|
5
|
+
if (!configPath) {
|
|
6
|
+
console.error('Usage: node live-enable-zalouser.cjs <configPath>');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
11
|
+
config.channels = config.channels || {};
|
|
12
|
+
config.channels.zalouser = {
|
|
13
|
+
...(config.channels.zalouser || {}),
|
|
14
|
+
enabled: true,
|
|
15
|
+
dmPolicy: 'pairing',
|
|
16
|
+
autoReply: true
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
20
|
+
console.log(`Enabled channels.zalouser in ${configPath}`);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
const configPath = process.argv[2];
|
|
4
|
+
|
|
5
|
+
if (!configPath) {
|
|
6
|
+
console.error('Usage: node live-enable-zalouser.js <configPath>');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
11
|
+
config.channels = config.channels || {};
|
|
12
|
+
config.channels.zalouser = {
|
|
13
|
+
...(config.channels.zalouser || {}),
|
|
14
|
+
enabled: true,
|
|
15
|
+
dmPolicy: 'pairing',
|
|
16
|
+
autoReply: true
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
20
|
+
console.log(`Enabled channels.zalouser in ${configPath}`);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
const dbPath = process.argv[2];
|
|
4
|
+
|
|
5
|
+
if (!dbPath) {
|
|
6
|
+
console.error('Usage: node live-patch-9router.cjs <dbPath>');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const models = [
|
|
11
|
+
'cx/gpt-5.4',
|
|
12
|
+
'cx/gpt-5.3-codex',
|
|
13
|
+
'cx/gpt-5.3-codex-high',
|
|
14
|
+
'cx/gpt-5.2-codex',
|
|
15
|
+
'cx/gpt-5.2',
|
|
16
|
+
'cx/gpt-5.1-codex-max',
|
|
17
|
+
'cx/gpt-5.1-codex',
|
|
18
|
+
'cx/gpt-5.1',
|
|
19
|
+
'cx/gpt-5-codex'
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const db = JSON.parse(fs.readFileSync(dbPath, 'utf8'));
|
|
23
|
+
db.combos = db.combos || [];
|
|
24
|
+
|
|
25
|
+
const combo = {
|
|
26
|
+
id: 'smart-route',
|
|
27
|
+
name: 'smart-route',
|
|
28
|
+
alias: 'smart-route',
|
|
29
|
+
models
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const index = db.combos.findIndex((entry) => entry && entry.id === 'smart-route');
|
|
33
|
+
if (index >= 0) {
|
|
34
|
+
db.combos[index] = combo;
|
|
35
|
+
} else {
|
|
36
|
+
db.combos.push(combo);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fs.writeFileSync(dbPath, JSON.stringify(db, null, 2));
|
|
40
|
+
console.log(`Patched smart-route in ${dbPath}`);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
const dbPath = process.argv[2];
|
|
4
|
+
|
|
5
|
+
if (!dbPath) {
|
|
6
|
+
console.error('Usage: node live-patch-9router.js <dbPath>');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const models = [
|
|
11
|
+
'cx/gpt-5.4',
|
|
12
|
+
'cx/gpt-5.3-codex',
|
|
13
|
+
'cx/gpt-5.3-codex-high',
|
|
14
|
+
'cx/gpt-5.2-codex',
|
|
15
|
+
'cx/gpt-5.2',
|
|
16
|
+
'cx/gpt-5.1-codex-max',
|
|
17
|
+
'cx/gpt-5.1-codex',
|
|
18
|
+
'cx/gpt-5.1',
|
|
19
|
+
'cx/gpt-5-codex'
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const db = JSON.parse(fs.readFileSync(dbPath, 'utf8'));
|
|
23
|
+
db.combos = db.combos || [];
|
|
24
|
+
|
|
25
|
+
const combo = {
|
|
26
|
+
id: 'smart-route',
|
|
27
|
+
name: 'smart-route',
|
|
28
|
+
alias: 'smart-route',
|
|
29
|
+
models
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const index = db.combos.findIndex((entry) => entry && entry.id === 'smart-route');
|
|
33
|
+
if (index >= 0) {
|
|
34
|
+
db.combos[index] = combo;
|
|
35
|
+
} else {
|
|
36
|
+
db.combos.push(combo);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fs.writeFileSync(dbPath, JSON.stringify(db, null, 2));
|
|
40
|
+
console.log(`Patched smart-route in ${dbPath}`);
|