create-openclaw-bot 5.1.2 → 5.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/CHANGELOG.vi.md +19 -0
- package/README.md +3 -3
- package/README.vi.md +3 -3
- package/cli.js +53 -40
- package/package.json +1 -1
- package/setup.js +36 -28
- package/tests/smoke-cli-logic.mjs +35 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# Changelog (English)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [5.1.4] — 2026-04-06
|
|
5
|
+
|
|
6
|
+
### 🐞 Fix CLI Startup BOM Error & Improve Docker Timeout Patch
|
|
7
|
+
|
|
8
|
+
- **Fix CLI BOM**: Removed the unexpected byte order mark (BOM) `\uFEFF` at the beginning of `cli.js` which could cause the shebang `#!/usr/bin/env node` to fail resolving or cause SyntaxErrors in certain environments
|
|
9
|
+
- **Improve Docker Timeout Patching**: The backend timeout override injection (`300s`) during Docker build now defensively scans all `.js` files in the `openclaw/dist` directory rather than trying to fuzzy-find a specific `gateway-cli-*` hash. This ensures the patch succeeds across different OpenClaw backend builds without noisy console warnings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## [5.1.3] — 2026-04-06
|
|
13
|
+
|
|
14
|
+
### 🐜 Fix Docker Compose Variable Interpolation Leak
|
|
15
|
+
|
|
16
|
+
The previous base64 fix introduced a regression where the template literal `${Buffer.from(...)}` was mistakenly escaped in the composition script, causing the literal string to leak into `docker-compose.yml` instead of the actual base64 computed value.
|
|
17
|
+
|
|
18
|
+
- **Fix**: Precompute the base64 string completely in JavaScript (`const syncScriptBase64 = encodeBase64Utf8(syncScript)`) before injecting it into the compose template
|
|
19
|
+
- This guarantees the generated compose file receives the raw base64 string without any template interpolator conflicts
|
|
20
|
+
- Also cleans up testing logic validating these fixes
|
|
21
|
+
|
|
22
|
+
|
|
4
23
|
## [5.1.2] — 2026-04-06
|
|
5
24
|
|
|
6
25
|
### 🐛 Fix Shell Injection: Sync Script Now Uses Base64 Encoding
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# Changelog (Tiếng Việt)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [5.1.4] — 2026-04-06
|
|
5
|
+
|
|
6
|
+
### 🐞 Sửa lỗi BOM khởi động CLI & Tối ưu luồng vá Timeout trên Docker
|
|
7
|
+
|
|
8
|
+
- **Sửa file CLI (BOM)**: Xóa tự động chèn BOM (`\uFEFF`) ở đầu file `cli.js`. Ký tự thừa này vốn làm hỏng shebang `#!/usr/bin/env node` và gây `SyntaxError: Unexpected token` trong nhiều môi trường khi chạy npx
|
|
9
|
+
- **Cải thiện Docker Timeout Patch**: Quá trình can thiệp timeout (`300s`) trong lúc build Docker giờ chuyển sang scan quét toàn bộ các file `.js` trong thư mục `openclaw/dist` thay vì cố tìm file trùng hash `gateway-cli-*`. Giúp bản vá luôn áp dụng thành công trên các phiên bản backend khác biệt mà không in ra warning rác trên console
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## [5.1.3] — 2026-04-06
|
|
13
|
+
|
|
14
|
+
### 🐜 Lỗi lọt biến nội suy vào giao diện Docker Compose
|
|
15
|
+
|
|
16
|
+
Bản vá lỗi base64 trước đó đã gây ra lỗi mới (regression) do dùng ngoặc `${Buffer.from(...)}` bên trong chuỗi string sinh ra docker-compose. Điều này làm lọt nguyên đoạn text nội suy vào `docker-compose.yml` thay vì sinh ra chuỗi base64 thật.
|
|
17
|
+
|
|
18
|
+
- **Fix**: Thực hiện tạo mã base64 hoàn chỉnh qua JavaScript (`const syncScriptBase64 = encodeBase64Utf8(syncScript)`) ngay từ ban đầu trước khi ghép chuỗi vào file compose
|
|
19
|
+
- Đảm bảo file compose tạo thành nhận chính xác mã base64 thuần túy mà không bị lọt biến môi trường
|
|
20
|
+
- Dọn dẹp lại script test tương ứng
|
|
21
|
+
|
|
22
|
+
|
|
4
23
|
## [5.1.2] — 2026-04-06
|
|
5
24
|
|
|
6
25
|
### 🐛 Fix Shell Injection: Sync Script Dùng Base64
|
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.1.
|
|
6
|
+
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.1.4-0EA5E9?style=for-the-badge" alt="Version 5.1.4" /></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.1.
|
|
27
|
+
## 🆕 What's new in v5.1.4
|
|
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.
|
|
@@ -112,7 +112,7 @@ Run in your terminal → follow the interactive prompts → startup script is ge
|
|
|
112
112
|
2. Open this repo as your workspace
|
|
113
113
|
3. Paste into chat:
|
|
114
114
|
```
|
|
115
|
-
Read SETUP.md and set up OpenClaw v5.1.
|
|
115
|
+
Read SETUP.md and set up OpenClaw v5.1.4 for me.
|
|
116
116
|
My bot token is X. Use 9Router (no API key).
|
|
117
117
|
My project folder: <YOUR_PATH>
|
|
118
118
|
```
|
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.1.
|
|
6
|
+
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.1.4-0EA5E9?style=for-the-badge" alt="Version 5.1.4" /></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.1.
|
|
27
|
+
## 🆕 Có gì mới trong v5.1.4
|
|
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.
|
|
@@ -112,7 +112,7 @@ Chạy lệnh trên trong Terminal → làm theo các prompt tương tác → sc
|
|
|
112
112
|
2. Mở repo này làm workspace
|
|
113
113
|
3. Paste vào chat:
|
|
114
114
|
```
|
|
115
|
-
Read SETUP.md and set up OpenClaw v5.1.
|
|
115
|
+
Read SETUP.md and set up OpenClaw v5.1.4 for me.
|
|
116
116
|
My bot token is X. Use 9Router (no API key).
|
|
117
117
|
My project folder: <THƯ_MỤC_CỦA_BẠN>
|
|
118
118
|
```
|
package/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { input, select, checkbox, confirm } from '@inquirer/prompts';
|
|
4
4
|
import fs from 'fs-extra';
|
|
@@ -312,9 +312,9 @@ function installLatestOpenClaw({ isVi, osChoice }) {
|
|
|
312
312
|
: '✅ openclaw is now on the latest version!'));
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
function build9RouterSmartRouteSyncScript(dbPath) {
|
|
316
|
-
const safeDbPath = JSON.stringify(dbPath);
|
|
317
|
-
return `const fs=require('fs');
|
|
315
|
+
function build9RouterSmartRouteSyncScript(dbPath) {
|
|
316
|
+
const safeDbPath = JSON.stringify(dbPath);
|
|
317
|
+
return `const fs=require('fs');
|
|
318
318
|
const INTERVAL=30000;
|
|
319
319
|
const p=${safeDbPath};
|
|
320
320
|
const ROUTER='http://localhost:20128';
|
|
@@ -359,15 +359,32 @@ const sync = async () => {
|
|
|
359
359
|
}
|
|
360
360
|
} catch(e) { console.log('[sync-combo] Error:', e.message); }
|
|
361
361
|
};
|
|
362
|
-
setTimeout(sync, 5000);
|
|
363
|
-
setInterval(sync, INTERVAL);`;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
362
|
+
setTimeout(sync, 5000);
|
|
363
|
+
setInterval(sync, INTERVAL);`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function indentBlock(text, spaces) {
|
|
367
|
+
const prefix = ' '.repeat(spaces);
|
|
368
|
+
return String(text)
|
|
369
|
+
.split('\n')
|
|
370
|
+
.map((line) => `${prefix}${line}`)
|
|
371
|
+
.join('\n');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function build9RouterComposeEntrypointScript(syncScriptBase64) {
|
|
375
|
+
return [
|
|
376
|
+
'npm install -g 9router',
|
|
377
|
+
`node -e "require('fs').writeFileSync('/tmp/sync.js',Buffer.from('${syncScriptBase64}','base64').toString())"`,
|
|
378
|
+
'node /tmp/sync.js > /tmp/sync.log 2>&1 &',
|
|
379
|
+
'exec 9router -n -t -l -H 0.0.0.0 -p 20128 --skip-update'
|
|
380
|
+
].join('\n');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function writeNative9RouterSyncScript(projectDir) {
|
|
384
|
+
const syncScriptPath = path.join(projectDir, '.openclaw', '9router-smart-route-sync.js');
|
|
385
|
+
await fs.ensureDir(path.dirname(syncScriptPath));
|
|
386
|
+
await fs.writeFile(syncScriptPath, build9RouterSmartRouteSyncScript(path.join(getNative9RouterDataDir(), 'db.json')));
|
|
387
|
+
return syncScriptPath;
|
|
371
388
|
}
|
|
372
389
|
|
|
373
390
|
function extractFirstHttpUrl(text) {
|
|
@@ -1244,7 +1261,7 @@ async function main() {
|
|
|
1244
1261
|
'# Fix chat.send dropping resolved agent timeout into reply pipeline.',
|
|
1245
1262
|
'# Without this, Telegram/WebChat paths fall back to an internal 300s default even when',
|
|
1246
1263
|
'# agents.defaults.timeoutSeconds is higher in config.',
|
|
1247
|
-
`RUN node -e "const fs=require('fs');const path=require('path');const dir='/usr/local/lib/node_modules/openclaw/dist';const
|
|
1264
|
+
`RUN node -e "const fs=require('fs');const path=require('path');const dir='/usr/local/lib/node_modules/openclaw/dist';const from='\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const to='\\t\\t\\t\\t\\ttimeoutOverrideSeconds: Math.max(1, Math.ceil(timeoutMs / 1e3)),\\n\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const files=fs.readdirSync(dir).filter(n=>/\\.js$/.test(n));let patched=0;for(const file of files){const p=path.join(dir,file);let s='';try{s=fs.readFileSync(p,'utf8');}catch{continue;}if(s.includes(to)||!s.includes(from))continue;s=s.replace(from,to);fs.writeFileSync(p,s);patched++;}if(!patched){process.exit(0);}"`,
|
|
1248
1265
|
'',
|
|
1249
1266
|
'WORKDIR /root/.openclaw',
|
|
1250
1267
|
'',
|
|
@@ -1260,10 +1277,12 @@ async function main() {
|
|
|
1260
1277
|
const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-$/, '') || 'chat';
|
|
1261
1278
|
|
|
1262
1279
|
// ─── Dynamic Smart Route Sync Script ────────────────────────────────────────
|
|
1263
|
-
// This script runs inside the 9Router container as a background loop.
|
|
1264
|
-
// It reads the persisted 9Router DB directly so smart-route still works
|
|
1265
|
-
// even when newer dashboard APIs require auth or change response shape.
|
|
1266
|
-
const syncComboScript = build9RouterSmartRouteSyncScript('/root/.9router/db.json');
|
|
1280
|
+
// This script runs inside the 9Router container as a background loop.
|
|
1281
|
+
// It reads the persisted 9Router DB directly so smart-route still works
|
|
1282
|
+
// even when newer dashboard APIs require auth or change response shape.
|
|
1283
|
+
const syncComboScript = build9RouterSmartRouteSyncScript('/root/.9router/db.json');
|
|
1284
|
+
const syncComboScriptBase64 = Buffer.from(syncComboScript).toString('base64');
|
|
1285
|
+
const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(syncComboScriptBase64);
|
|
1267
1286
|
|
|
1268
1287
|
// ─── Resolve primary model ───────────────────────────────────────────────────
|
|
1269
1288
|
let modelsPrimary;
|
|
@@ -1307,17 +1326,14 @@ ${dependsOn}${extraHosts} ports:
|
|
|
1307
1326
|
image: node:22-slim
|
|
1308
1327
|
container_name: 9router-multibot
|
|
1309
1328
|
restart: always
|
|
1310
|
-
entrypoint:
|
|
1311
|
-
- /bin/sh
|
|
1312
|
-
- -c
|
|
1313
|
-
- |
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
environment:
|
|
1319
|
-
- PORT=20128
|
|
1320
|
-
- HOSTNAME=0.0.0.0
|
|
1329
|
+
entrypoint:
|
|
1330
|
+
- /bin/sh
|
|
1331
|
+
- -c
|
|
1332
|
+
- |
|
|
1333
|
+
${indentBlock(docker9RouterEntrypointScript, 8)}
|
|
1334
|
+
environment:
|
|
1335
|
+
- PORT=20128
|
|
1336
|
+
- HOSTNAME=0.0.0.0
|
|
1321
1337
|
- CI=true
|
|
1322
1338
|
volumes:
|
|
1323
1339
|
- 9router-data:/root/.9router
|
|
@@ -1402,17 +1418,14 @@ ${hasBrowserDesktop ? ` extra_hosts:\n - "host.docker.internal:host-gate
|
|
|
1402
1418
|
image: node:22-slim
|
|
1403
1419
|
container_name: 9router-${agentId}
|
|
1404
1420
|
restart: always
|
|
1405
|
-
entrypoint:
|
|
1406
|
-
- /bin/sh
|
|
1407
|
-
- -c
|
|
1408
|
-
- |
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
environment:
|
|
1414
|
-
- PORT=20128
|
|
1415
|
-
- HOSTNAME=0.0.0.0
|
|
1421
|
+
entrypoint:
|
|
1422
|
+
- /bin/sh
|
|
1423
|
+
- -c
|
|
1424
|
+
- |
|
|
1425
|
+
${indentBlock(docker9RouterEntrypointScript, 8)}
|
|
1426
|
+
environment:
|
|
1427
|
+
- PORT=20128
|
|
1428
|
+
- HOSTNAME=0.0.0.0
|
|
1416
1429
|
- CI=true
|
|
1417
1430
|
volumes:
|
|
1418
1431
|
- 9router-data:/root/.9router
|
package/package.json
CHANGED
package/setup.js
CHANGED
|
@@ -1758,7 +1758,19 @@ model:
|
|
|
1758
1758
|
// 3. Dockerfile
|
|
1759
1759
|
const allPlugins = [];
|
|
1760
1760
|
if (ch.pluginInstall) allPlugins.push(ch.pluginInstall);
|
|
1761
|
-
|
|
1761
|
+
const encodeBase64Utf8 = (value) => btoa(String.fromCharCode(...new TextEncoder().encode(String(value))));
|
|
1762
|
+
const indentBlock = (text, spaces) => {
|
|
1763
|
+
const prefix = ' '.repeat(spaces);
|
|
1764
|
+
return String(text).split('\n').map((line) => `${prefix}${line}`).join('\n');
|
|
1765
|
+
};
|
|
1766
|
+
const build9RouterComposeEntrypointScript = (syncScriptBase64) => [
|
|
1767
|
+
'npm install -g 9router',
|
|
1768
|
+
`node -e "require('fs').writeFileSync('/tmp/sync.js',Buffer.from('${syncScriptBase64}','base64').toString())"`,
|
|
1769
|
+
'node /tmp/sync.js > /tmp/sync.log 2>&1 &',
|
|
1770
|
+
'exec 9router -n -t -l -H 0.0.0.0 -p 20128 --skip-update'
|
|
1771
|
+
].join('\n');
|
|
1772
|
+
|
|
1773
|
+
state.config.plugins.forEach((pid) => {
|
|
1762
1774
|
const plug = PLUGINS.find((p) => p.id === pid);
|
|
1763
1775
|
if (plug) allPlugins.push(plug.package);
|
|
1764
1776
|
});
|
|
@@ -1815,7 +1827,7 @@ RUN apt-get update && apt-get install -y git curl${browserAptExtra} && rm -rf /v
|
|
|
1815
1827
|
|
|
1816
1828
|
ARG CACHEBUST=${Date.now()}
|
|
1817
1829
|
RUN npm install -g openclaw@latest${skillLines}${browserInstallLines}
|
|
1818
|
-
RUN node -e "const fs=require('fs');const path=require('path');const dir='/usr/local/lib/node_modules/openclaw/dist';const
|
|
1830
|
+
RUN node -e "const fs=require('fs');const path=require('path');const dir='/usr/local/lib/node_modules/openclaw/dist';const from='\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const to='\\t\\t\\t\\t\\ttimeoutOverrideSeconds: Math.max(1, Math.ceil(timeoutMs / 1e3)),\\n\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const files=fs.readdirSync(dir).filter(n=>/\\.js$/.test(n));let patched=0;for(const file of files){const p=path.join(dir,file);let s='';try{s=fs.readFileSync(p,'utf8');}catch{continue;}if(s.includes(to)||!s.includes(from))continue;s=s.replace(from,to);fs.writeFileSync(p,s);patched++;}if(!patched){process.exit(0);}"
|
|
1819
1831
|
WORKDIR /root/.openclaw
|
|
1820
1832
|
|
|
1821
1833
|
EXPOSE 18791
|
|
@@ -1834,7 +1846,7 @@ ${finalCmd}`;
|
|
|
1834
1846
|
// Background loop inside 9Router container every 30s.
|
|
1835
1847
|
// Read providerConnections directly from db.json so smart-route survives
|
|
1836
1848
|
// dashboard auth/response changes in newer 9Router builds.
|
|
1837
|
-
const syncScript = `const fs=require('fs');const INTERVAL=30000;const p='/root/.9router/db.json';
|
|
1849
|
+
const syncScript = `const fs=require('fs');const INTERVAL=30000;const p='/root/.9router/db.json';
|
|
1838
1850
|
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']};
|
|
1839
1851
|
console.log('[sync-combo] 9Router sync loop started...');
|
|
1840
1852
|
const sync = async () => {
|
|
@@ -1884,9 +1896,11 @@ const sync = async () => {
|
|
|
1884
1896
|
console.log('[sync-combo] Created smart-route: ' + c.models.length + ' models');
|
|
1885
1897
|
}
|
|
1886
1898
|
} catch (e) { }
|
|
1887
|
-
};
|
|
1888
|
-
setTimeout(sync, 5000);
|
|
1889
|
-
setInterval(sync, INTERVAL);`;
|
|
1899
|
+
};
|
|
1900
|
+
setTimeout(sync, 5000);
|
|
1901
|
+
setInterval(sync, INTERVAL);`;
|
|
1902
|
+
const syncScriptBase64 = encodeBase64Utf8(syncScript);
|
|
1903
|
+
const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(syncScriptBase64);
|
|
1890
1904
|
|
|
1891
1905
|
let compose;
|
|
1892
1906
|
if (isMultiBotWizard) {
|
|
@@ -1915,17 +1929,14 @@ ${dependsOn}${extraHosts} volumes:
|
|
|
1915
1929
|
image: node:22-slim
|
|
1916
1930
|
container_name: 9router-multibot
|
|
1917
1931
|
restart: always
|
|
1918
|
-
entrypoint:
|
|
1919
|
-
- /bin/sh
|
|
1920
|
-
- -c
|
|
1921
|
-
- |
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
environment:
|
|
1927
|
-
- PORT=20128
|
|
1928
|
-
- HOSTNAME=0.0.0.0
|
|
1932
|
+
entrypoint:
|
|
1933
|
+
- /bin/sh
|
|
1934
|
+
- -c
|
|
1935
|
+
- |
|
|
1936
|
+
${indentBlock(docker9RouterEntrypointScript, 8)}
|
|
1937
|
+
environment:
|
|
1938
|
+
- PORT=20128
|
|
1939
|
+
- HOSTNAME=0.0.0.0
|
|
1929
1940
|
- CI=true
|
|
1930
1941
|
volumes:
|
|
1931
1942
|
- 9router-data:/root/.9router
|
|
@@ -2011,17 +2022,14 @@ ${extraHostsBlock}
|
|
|
2011
2022
|
image: node:22-slim
|
|
2012
2023
|
container_name: 9router
|
|
2013
2024
|
restart: always
|
|
2014
|
-
entrypoint:
|
|
2015
|
-
- /bin/sh
|
|
2016
|
-
- -c
|
|
2017
|
-
- |
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
environment:
|
|
2023
|
-
- PORT=20128
|
|
2024
|
-
- HOSTNAME=0.0.0.0
|
|
2025
|
+
entrypoint:
|
|
2026
|
+
- /bin/sh
|
|
2027
|
+
- -c
|
|
2028
|
+
- |
|
|
2029
|
+
${indentBlock(docker9RouterEntrypointScript, 8)}
|
|
2030
|
+
environment:
|
|
2031
|
+
- PORT=20128
|
|
2032
|
+
- HOSTNAME=0.0.0.0
|
|
2025
2033
|
- CI=true
|
|
2026
2034
|
volumes:
|
|
2027
2035
|
- 9router-data:/root/.9router
|
|
@@ -5,6 +5,7 @@ const root = process.cwd();
|
|
|
5
5
|
const cli = fs.readFileSync(path.join(root, 'cli.js'), 'utf8');
|
|
6
6
|
const setup = fs.readFileSync(path.join(root, 'setup.js'), 'utf8');
|
|
7
7
|
const indexHtml = fs.readFileSync(path.join(root, 'index.html'), 'utf8');
|
|
8
|
+
const cliBytes = fs.readFileSync(path.join(root, 'cli.js'));
|
|
8
9
|
|
|
9
10
|
function expect(condition, message) {
|
|
10
11
|
if (!condition) {
|
|
@@ -32,6 +33,11 @@ checks.push(() => expectMatch(
|
|
|
32
33
|
'Ubuntu/VPS must default to native deploy mode'
|
|
33
34
|
));
|
|
34
35
|
|
|
36
|
+
checks.push(() => expect(
|
|
37
|
+
!(cliBytes[0] === 0xef && cliBytes[1] === 0xbb && cliBytes[2] === 0xbf),
|
|
38
|
+
'CLI entrypoint must not include a UTF-8 BOM before the shebang'
|
|
39
|
+
));
|
|
40
|
+
|
|
35
41
|
checks.push(() => expectMatch(
|
|
36
42
|
cli,
|
|
37
43
|
/if \(deployMode === 'docker' && !isDockerInstalled\(\)\)/,
|
|
@@ -86,9 +92,12 @@ checks.push(() => expectMatch(
|
|
|
86
92
|
'CLI fast-test mode must be able to reuse an existing 9Router install'
|
|
87
93
|
));
|
|
88
94
|
|
|
89
|
-
checks.push(() =>
|
|
90
|
-
cli
|
|
91
|
-
|
|
95
|
+
checks.push(() => expect(
|
|
96
|
+
cli.includes('function build9RouterSmartRouteSyncScript(dbPath) {')
|
|
97
|
+
&& cli.includes('const safeDbPath = JSON.stringify(dbPath);')
|
|
98
|
+
&& cli.includes("const ROUTER='http://localhost:20128';")
|
|
99
|
+
&& cli.includes("fetch(ROUTER + '/api/providers')")
|
|
100
|
+
&& cli.includes("build9RouterSmartRouteSyncScript(path.join(getNative9RouterDataDir(), 'db.json'))"),
|
|
92
101
|
'Native 9Router flow must write a smart-route sync script based on the platform-specific 9Router data directory'
|
|
93
102
|
));
|
|
94
103
|
|
|
@@ -100,8 +109,9 @@ checks.push(() => expectMatch(
|
|
|
100
109
|
|
|
101
110
|
checks.push(() => expect(
|
|
102
111
|
cli.includes("Removed smart-route (no active providers)")
|
|
103
|
-
&& cli.includes("if(!a.length){
|
|
104
|
-
&& cli.includes("if(!m.length){
|
|
112
|
+
&& cli.includes("if (!a.length) {")
|
|
113
|
+
&& cli.includes("if (!m.length) {")
|
|
114
|
+
&& cli.includes("removeSmartRoute();"),
|
|
105
115
|
'9Router sync logic in CLI must remove stale smart-route combos when providers are disabled'
|
|
106
116
|
));
|
|
107
117
|
|
|
@@ -282,20 +292,21 @@ checks.push(() => expectMatch(
|
|
|
282
292
|
|
|
283
293
|
checks.push(() => expectMatch(
|
|
284
294
|
setup,
|
|
285
|
-
/function providerLines\(arr, shell\) \{[\s\S]*npm install -g 9router[\s\S]*
|
|
295
|
+
/function providerLines\(arr, shell\) \{[\s\S]*npm install -g 9router[\s\S]*start "9Router" cmd \/k "9router -n -t -l -H 0\.0\.0\.0 -p 20128 --skip-update"[\s\S]*9router-smart-route-sync\.js/s,
|
|
286
296
|
'Native script generation must install and start a standalone 9Router dashboard on port 20128'
|
|
287
297
|
));
|
|
288
298
|
|
|
289
299
|
checks.push(() => expectMatch(
|
|
290
300
|
setup,
|
|
291
|
-
/function native9RouterSyncScriptContent\(\) \{[\s\S]*process\.
|
|
292
|
-
'Native script generation must embed a 9Router smart-route sync script
|
|
301
|
+
/function native9RouterSyncScriptContent\(\) \{[\s\S]*path\.join\(process\.env\.HOME\|\|process\.env\.USERPROFILE\|\|'\.'\,\'\.9router\'\,\'db\.json\'\)[\s\S]*providerConnections[\s\S]*smart-route/s,
|
|
302
|
+
'Native script generation must embed a 9Router smart-route sync script'
|
|
293
303
|
));
|
|
294
304
|
|
|
295
305
|
checks.push(() => expect(
|
|
296
306
|
setup.includes("Removed smart-route (no active providers)")
|
|
297
|
-
&& setup.includes("if(!a.length){
|
|
298
|
-
&& setup.includes("if(!m.length){
|
|
307
|
+
&& setup.includes("if (!a.length) {")
|
|
308
|
+
&& setup.includes("if (!m.length) {")
|
|
309
|
+
&& setup.includes("removeSmartRoute();"),
|
|
299
310
|
'9Router sync logic in setup.js must remove stale smart-route combos when providers are disabled'
|
|
300
311
|
));
|
|
301
312
|
|
|
@@ -307,14 +318,24 @@ checks.push(() => expectMatch(
|
|
|
307
318
|
|
|
308
319
|
checks.push(() => expectMatch(
|
|
309
320
|
cli,
|
|
310
|
-
/readdirSync\(dir\)\.
|
|
311
|
-
'Dockerfile patching in CLI must
|
|
321
|
+
/const files=fs\.readdirSync\(dir\)\.filter\(n=>\/\\\\\.js\$\/\.test\(n\)\)[\s\S]*let patched=0[\s\S]*if\(!patched\)\{process\.exit\(0\);\}/,
|
|
322
|
+
'Dockerfile patching in CLI must scan all OpenClaw dist JS files and silently skip when no timeout patch anchor exists'
|
|
323
|
+
));
|
|
324
|
+
|
|
325
|
+
checks.push(() => expect(
|
|
326
|
+
!cli.includes("Buffer.from('\\${Buffer.from(syncComboScript).toString('base64')}','base64')"),
|
|
327
|
+
'CLI must precompute the 9Router sync script base64 instead of leaking Docker Compose interpolation markers'
|
|
312
328
|
));
|
|
313
329
|
|
|
314
330
|
checks.push(() => expectMatch(
|
|
315
331
|
setup,
|
|
316
|
-
/readdirSync\(dir\)\.
|
|
317
|
-
'Dockerfile patching in setup.js must
|
|
332
|
+
/const files=fs\.readdirSync\(dir\)\.filter\(n=>\/\\\\\.js\$\/\.test\(n\)\)[\s\S]*let patched=0[\s\S]*if\(!patched\)\{process\.exit\(0\);\}/,
|
|
333
|
+
'Dockerfile patching in setup.js must scan all OpenClaw dist JS files and silently skip when no timeout patch anchor exists'
|
|
334
|
+
));
|
|
335
|
+
|
|
336
|
+
checks.push(() => expect(
|
|
337
|
+
!setup.includes("Buffer.from('\\${Buffer.from(syncScript).toString('base64')}','base64')"),
|
|
338
|
+
'Wizard compose generation must precompute the 9Router sync script base64 instead of leaking Docker Compose interpolation markers'
|
|
318
339
|
));
|
|
319
340
|
|
|
320
341
|
checks.push(() => expectMatch(
|