create-openclaw-bot 5.1.8 → 5.1.10
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 +15 -0
- package/CHANGELOG.vi.md +15 -0
- package/README.md +3 -3
- package/README.vi.md +3 -3
- package/cli.js +321 -289
- package/package.json +1 -1
- package/setup.js +1 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
# Changelog (English)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [5.1.10] — 2026-04-07
|
|
5
|
+
|
|
6
|
+
### 🌟 Native UI Auto-Approve Bypasser
|
|
7
|
+
|
|
8
|
+
- **Native PM2 Auto-Approve Loop**: The strict `pairing required` security feature mandates that all users manually execute an approval command in their terminal for new web dashboard authentications. While Docker deployments already included an automated bypass, the Native setup did not. This release introduces a dedicated `auto-approve` PM2 background daemon that infinitely polls and accepts new device keys, delivering a frictionless, zero-touch login experience identical to Docker deployments.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## [5.1.9] — 2026-04-07
|
|
12
|
+
|
|
13
|
+
### 🌟 Strict Schema Fix & WebCrypto UX Improvement
|
|
14
|
+
|
|
15
|
+
- **Revert Unrecognized Config Key**: OpenClaw v2026.x.x enforces strict Zod schema validation. The previously injected `requireDeviceIdentity` flag caused an immediate startup crash (`Config invalid`). This version surgically removes the offending flag, ensuring the gateway boots successfully.
|
|
16
|
+
- **Dynamic SSH Tunnel Helper**: Since WebCrypto strictly demands a secure context (HTTPS/localhost), accessing the dashboard via raw VPS IP triggers a `1008` error natively. The CLI now dynamically generates and prints the exact `ssh -L 18791:localhost:18791 ...` Port Forwarding command right in the terminal, guaranteeing a flawless, secure login experience for remote server operators without needing SSL.
|
|
17
|
+
|
|
18
|
+
|
|
4
19
|
## [5.1.8] — 2026-04-07
|
|
5
20
|
|
|
6
21
|
### 🌟 Dashboard VPS Connectivity & Token Login Fix
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
# Changelog (Tiếng Việt)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [5.1.10] — 2026-04-07
|
|
5
|
+
|
|
6
|
+
### 🌟 Tự động Auto-Approve Thiết Bị cho Native VPS
|
|
7
|
+
|
|
8
|
+
- **Bỏ Nhập Lệnh Thủ Công Trải Nghiệm PM2**: Cảnh báo `pairing required` (chờ duyệt thiết bị ghép nối E2E) trên giao diện Web buộc người dùng phải gõ lệnh đồng ý dưới Terminal. Ở luồng Docker, tính năng này đã được vô hiệu hóa bằng một đoạn script chạy ngầm tự gật đầu. Nhưng ở Native thì chưa! Phiên bản này chính thức nhúng thêm 1 tiến trình PM2 `auto-approve` siêu nhẹ chạy kẹp với các lệnh chính, giúp tự động gật đầu phê duyệt kết nối web mỗi 5 giây. Đảm bảo trải nghiệm "Click là vào" trên Native VPS mượt mà y hệt Docker!
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## [5.1.9] — 2026-04-07
|
|
12
|
+
|
|
13
|
+
### 🌟 Trả lại Schema Chuẩn & Cải thiện UX WebCrypto
|
|
14
|
+
|
|
15
|
+
- **Sửa lỗi sập Gateway do sai lầm Config**: OpenClaw bản mới nhất dùng Zod để khóa chặt Schema cấu hình. Cờ `requireDeviceIdentity` chêm vào bản 5.1.8 đã bị Backend từ chối thẳng thừng (`Unrecognized key`), dẫn đến server không thể khởi động vòng lặp. Bản 5.1.9 đã gỡ sạch cờ này, trả lại môi trường sạch để PM2 hoạt động 100%.
|
|
16
|
+
- **Trợ lý SSH Tunnel Tự Động**: Bù lại sự khắt khe của WebCrypto khi dùng VPS/IP ngoài, Console giờ đây sẽ tự động in sẵn thần chú lệnh bẻ khóa `ssh -L ...` y hệt IP và Username thật của bạn. Bạn chỉ cần copy-paste để thông luồng một cách ngầu lòi, bảo mật tuyệt đối mà không cần mua Tên miền HTTPS.
|
|
17
|
+
|
|
18
|
+
|
|
4
19
|
## [5.1.8] — 2026-04-07
|
|
5
20
|
|
|
6
21
|
### 🌟 Sửa lỗi Đăng nhập Token (1008) & Cải tiến IP hiển thị trên VPS
|
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.10-0EA5E9?style=for-the-badge" alt="Version 5.1.10" /></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.10
|
|
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.10 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.10-0EA5E9?style=for-the-badge" alt="Version 5.1.10" /></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.10
|
|
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.10 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
|
@@ -5,7 +5,7 @@ import fs from 'fs-extra';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import os from 'os';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
|
-
import { spawn, execSync, execFileSync } from 'child_process';
|
|
8
|
+
import { spawn, execSync, execFileSync } from 'child_process';
|
|
9
9
|
const TELEGRAM_RELAY_PLUGIN_ID = 'openclaw-telegram-multibot-relay';
|
|
10
10
|
// Use plain npm package name — clawhub: protocol not supported in all OpenClaw versions
|
|
11
11
|
const TELEGRAM_RELAY_PLUGIN_SPEC = TELEGRAM_RELAY_PLUGIN_ID;
|
|
@@ -95,7 +95,7 @@ function quotePowerShellSingle(value) {
|
|
|
95
95
|
return `'${String(value).replace(/'/g, "''")}'`;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
function resolveWindowsCommand(command) {
|
|
98
|
+
function resolveWindowsCommand(command) {
|
|
99
99
|
try {
|
|
100
100
|
const output = execSync(`where.exe ${command}`, {
|
|
101
101
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
@@ -145,88 +145,88 @@ function spawnBackgroundProcess(command, args, options = {}) {
|
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
function resolveNative9RouterDesktopLaunch() {
|
|
149
|
-
return {
|
|
150
|
-
command: process.execPath,
|
|
151
|
-
args: [path.join(getGlobalNpmRoot(), '9router', 'app', 'server.js')],
|
|
152
|
-
env: {
|
|
153
|
-
PORT: '20128',
|
|
154
|
-
HOSTNAME: '0.0.0.0'
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function getNative9RouterDataDir() {
|
|
160
|
-
if (process.platform === 'win32') {
|
|
161
|
-
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), '9router');
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return path.join(os.homedir(), '.9router');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function getGatewayAllowedOrigins(port) {
|
|
168
|
-
const normalizedPort = Number(port) || 18791;
|
|
169
|
-
const origins = new Set([
|
|
170
|
-
`http://localhost:${normalizedPort}`,
|
|
171
|
-
`http://127.0.0.1:${normalizedPort}`,
|
|
172
|
-
`http://0.0.0.0:${normalizedPort}`
|
|
173
|
-
]);
|
|
174
|
-
|
|
175
|
-
for (const entries of Object.values(os.networkInterfaces() || {})) {
|
|
176
|
-
for (const entry of entries || []) {
|
|
177
|
-
if (!entry || entry.internal || entry.family !== 'IPv4' || !entry.address) {
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
origins.add(`http://${entry.address}:${normalizedPort}`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return Array.from(origins);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function getReachableDashboardHosts(port) {
|
|
188
|
-
const normalizedPort = Number(port) || 18791;
|
|
189
|
-
const hosts = [];
|
|
190
|
-
const pushHost = (host) => {
|
|
191
|
-
if (!host) return;
|
|
192
|
-
const url = `http://${host}:${normalizedPort}`;
|
|
193
|
-
if (!hosts.includes(url)) {
|
|
194
|
-
hosts.push(url);
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
pushHost('127.0.0.1');
|
|
199
|
-
pushHost('localhost');
|
|
200
|
-
|
|
201
|
-
for (const entries of Object.values(os.networkInterfaces() || {})) {
|
|
202
|
-
for (const entry of entries || []) {
|
|
203
|
-
if (!entry || entry.internal || entry.family !== 'IPv4' || !entry.address) {
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
pushHost(entry.address);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return hosts;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function rewriteDashboardUrlHost(urlText, fallbackPort, targetBaseUrl) {
|
|
214
|
-
try {
|
|
215
|
-
const target = new URL(targetBaseUrl);
|
|
216
|
-
const parsed = new URL(urlText);
|
|
217
|
-
parsed.protocol = target.protocol;
|
|
218
|
-
parsed.hostname = target.hostname;
|
|
219
|
-
parsed.port = target.port || String(fallbackPort || '');
|
|
220
|
-
return parsed.toString();
|
|
221
|
-
} catch {
|
|
222
|
-
return `${targetBaseUrl}${String(urlText || '').startsWith('/') ? '' : '/'}${String(urlText || '')}`;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async function waitFor9RouterApiReady({ port = 20128, timeoutMs = 15000 } = {}) {
|
|
227
|
-
const deadline = Date.now() + timeoutMs;
|
|
228
|
-
const candidates = [
|
|
229
|
-
`http://127.0.0.1:${port}/api/settings/require-login`,
|
|
148
|
+
function resolveNative9RouterDesktopLaunch() {
|
|
149
|
+
return {
|
|
150
|
+
command: process.execPath,
|
|
151
|
+
args: [path.join(getGlobalNpmRoot(), '9router', 'app', 'server.js')],
|
|
152
|
+
env: {
|
|
153
|
+
PORT: '20128',
|
|
154
|
+
HOSTNAME: '0.0.0.0'
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getNative9RouterDataDir() {
|
|
160
|
+
if (process.platform === 'win32') {
|
|
161
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), '9router');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return path.join(os.homedir(), '.9router');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getGatewayAllowedOrigins(port) {
|
|
168
|
+
const normalizedPort = Number(port) || 18791;
|
|
169
|
+
const origins = new Set([
|
|
170
|
+
`http://localhost:${normalizedPort}`,
|
|
171
|
+
`http://127.0.0.1:${normalizedPort}`,
|
|
172
|
+
`http://0.0.0.0:${normalizedPort}`
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
for (const entries of Object.values(os.networkInterfaces() || {})) {
|
|
176
|
+
for (const entry of entries || []) {
|
|
177
|
+
if (!entry || entry.internal || entry.family !== 'IPv4' || !entry.address) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
origins.add(`http://${entry.address}:${normalizedPort}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return Array.from(origins);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getReachableDashboardHosts(port) {
|
|
188
|
+
const normalizedPort = Number(port) || 18791;
|
|
189
|
+
const hosts = [];
|
|
190
|
+
const pushHost = (host) => {
|
|
191
|
+
if (!host) return;
|
|
192
|
+
const url = `http://${host}:${normalizedPort}`;
|
|
193
|
+
if (!hosts.includes(url)) {
|
|
194
|
+
hosts.push(url);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
pushHost('127.0.0.1');
|
|
199
|
+
pushHost('localhost');
|
|
200
|
+
|
|
201
|
+
for (const entries of Object.values(os.networkInterfaces() || {})) {
|
|
202
|
+
for (const entry of entries || []) {
|
|
203
|
+
if (!entry || entry.internal || entry.family !== 'IPv4' || !entry.address) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
pushHost(entry.address);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return hosts;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function rewriteDashboardUrlHost(urlText, fallbackPort, targetBaseUrl) {
|
|
214
|
+
try {
|
|
215
|
+
const target = new URL(targetBaseUrl);
|
|
216
|
+
const parsed = new URL(urlText);
|
|
217
|
+
parsed.protocol = target.protocol;
|
|
218
|
+
parsed.hostname = target.hostname;
|
|
219
|
+
parsed.port = target.port || String(fallbackPort || '');
|
|
220
|
+
return parsed.toString();
|
|
221
|
+
} catch {
|
|
222
|
+
return `${targetBaseUrl}${String(urlText || '').startsWith('/') ? '' : '/'}${String(urlText || '')}`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function waitFor9RouterApiReady({ port = 20128, timeoutMs = 15000 } = {}) {
|
|
227
|
+
const deadline = Date.now() + timeoutMs;
|
|
228
|
+
const candidates = [
|
|
229
|
+
`http://127.0.0.1:${port}/api/settings/require-login`,
|
|
230
230
|
`http://127.0.0.1:${port}/api/version`
|
|
231
231
|
];
|
|
232
232
|
|
|
@@ -350,9 +350,9 @@ function installLatestOpenClaw({ isVi, osChoice }) {
|
|
|
350
350
|
: '✅ openclaw is now on the latest version!'));
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
function build9RouterSmartRouteSyncScript(dbPath) {
|
|
354
|
-
const safeDbPath = JSON.stringify(dbPath);
|
|
355
|
-
return `const fs=require('fs');
|
|
353
|
+
function build9RouterSmartRouteSyncScript(dbPath) {
|
|
354
|
+
const safeDbPath = JSON.stringify(dbPath);
|
|
355
|
+
return `const fs=require('fs');
|
|
356
356
|
const INTERVAL=30000;
|
|
357
357
|
const p=${safeDbPath};
|
|
358
358
|
const ROUTER='http://localhost:20128';
|
|
@@ -397,65 +397,65 @@ const sync = async () => {
|
|
|
397
397
|
}
|
|
398
398
|
} catch(e) { console.log('[sync-combo] Error:', e.message); }
|
|
399
399
|
};
|
|
400
|
-
setTimeout(sync, 5000);
|
|
401
|
-
setInterval(sync, INTERVAL);`;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function resolveCommandOnPath(command) {
|
|
405
|
-
if (process.platform === 'win32') {
|
|
406
|
-
return resolveWindowsCommand(command);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
try {
|
|
410
|
-
return execSync(`command -v ${command}`, {
|
|
411
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
412
|
-
encoding: 'utf8',
|
|
413
|
-
shell: true,
|
|
414
|
-
env: process.env
|
|
415
|
-
}).trim() || command;
|
|
416
|
-
} catch {
|
|
417
|
-
return command;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function getGlobalNpmRoot() {
|
|
422
|
-
try {
|
|
423
|
-
return execSync('npm root -g', {
|
|
424
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
425
|
-
encoding: 'utf8',
|
|
426
|
-
shell: true,
|
|
427
|
-
env: process.env
|
|
428
|
-
}).trim();
|
|
429
|
-
} catch {
|
|
430
|
-
if (process.platform === 'win32') {
|
|
431
|
-
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'npm', 'node_modules');
|
|
432
|
-
}
|
|
433
|
-
return path.join(os.homedir(), '.local', 'lib', 'node_modules');
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function indentBlock(text, spaces) {
|
|
438
|
-
const prefix = ' '.repeat(spaces);
|
|
439
|
-
return String(text)
|
|
440
|
-
.split('\n')
|
|
441
|
-
.map((line) => `${prefix}${line}`)
|
|
442
|
-
.join('\n');
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function build9RouterComposeEntrypointScript(syncScriptBase64) {
|
|
446
|
-
return [
|
|
447
|
-
'npm install -g 9router',
|
|
448
|
-
`node -e "require('fs').writeFileSync('/tmp/sync.js',Buffer.from('${syncScriptBase64}','base64').toString())"`,
|
|
449
|
-
'node /tmp/sync.js > /tmp/sync.log 2>&1 &',
|
|
450
|
-
'exec 9router -n -l -H 0.0.0.0 -p 20128 --skip-update'
|
|
451
|
-
].join('\n');
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
async function writeNative9RouterSyncScript(projectDir) {
|
|
455
|
-
const syncScriptPath = path.join(projectDir, '.openclaw', '9router-smart-route-sync.js');
|
|
456
|
-
await fs.ensureDir(path.dirname(syncScriptPath));
|
|
457
|
-
await fs.writeFile(syncScriptPath, build9RouterSmartRouteSyncScript(path.join(getNative9RouterDataDir(), 'db.json')));
|
|
458
|
-
return syncScriptPath;
|
|
400
|
+
setTimeout(sync, 5000);
|
|
401
|
+
setInterval(sync, INTERVAL);`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function resolveCommandOnPath(command) {
|
|
405
|
+
if (process.platform === 'win32') {
|
|
406
|
+
return resolveWindowsCommand(command);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
return execSync(`command -v ${command}`, {
|
|
411
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
412
|
+
encoding: 'utf8',
|
|
413
|
+
shell: true,
|
|
414
|
+
env: process.env
|
|
415
|
+
}).trim() || command;
|
|
416
|
+
} catch {
|
|
417
|
+
return command;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function getGlobalNpmRoot() {
|
|
422
|
+
try {
|
|
423
|
+
return execSync('npm root -g', {
|
|
424
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
425
|
+
encoding: 'utf8',
|
|
426
|
+
shell: true,
|
|
427
|
+
env: process.env
|
|
428
|
+
}).trim();
|
|
429
|
+
} catch {
|
|
430
|
+
if (process.platform === 'win32') {
|
|
431
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'npm', 'node_modules');
|
|
432
|
+
}
|
|
433
|
+
return path.join(os.homedir(), '.local', 'lib', 'node_modules');
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function indentBlock(text, spaces) {
|
|
438
|
+
const prefix = ' '.repeat(spaces);
|
|
439
|
+
return String(text)
|
|
440
|
+
.split('\n')
|
|
441
|
+
.map((line) => `${prefix}${line}`)
|
|
442
|
+
.join('\n');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function build9RouterComposeEntrypointScript(syncScriptBase64) {
|
|
446
|
+
return [
|
|
447
|
+
'npm install -g 9router',
|
|
448
|
+
`node -e "require('fs').writeFileSync('/tmp/sync.js',Buffer.from('${syncScriptBase64}','base64').toString())"`,
|
|
449
|
+
'node /tmp/sync.js > /tmp/sync.log 2>&1 &',
|
|
450
|
+
'exec 9router -n -l -H 0.0.0.0 -p 20128 --skip-update'
|
|
451
|
+
].join('\n');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function writeNative9RouterSyncScript(projectDir) {
|
|
455
|
+
const syncScriptPath = path.join(projectDir, '.openclaw', '9router-smart-route-sync.js');
|
|
456
|
+
await fs.ensureDir(path.dirname(syncScriptPath));
|
|
457
|
+
await fs.writeFile(syncScriptPath, build9RouterSmartRouteSyncScript(path.join(getNative9RouterDataDir(), 'db.json')));
|
|
458
|
+
return syncScriptPath;
|
|
459
459
|
}
|
|
460
460
|
|
|
461
461
|
function extractFirstHttpUrl(text) {
|
|
@@ -480,45 +480,68 @@ function getTokenizedDashboardUrl(projectDir) {
|
|
|
480
480
|
}
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
-
function printNativeDashboardAccessInfo({ isVi, providerKey, projectDir, gatewayPort = 18791 }) {
|
|
484
|
-
const gatewayUrls = getReachableDashboardHosts(gatewayPort);
|
|
485
|
-
const dashboardUrl = gatewayUrls[0] || `http://127.0.0.1:${gatewayPort}`;
|
|
486
|
-
const tokenizedUrl = getTokenizedDashboardUrl(projectDir);
|
|
487
|
-
|
|
488
|
-
console.log(chalk.yellow(`\n🧭 ${isVi ? 'Dashboard OpenClaw:' : 'OpenClaw Dashboard:'} ${dashboardUrl}`));
|
|
489
|
-
if (gatewayUrls.length > 1) {
|
|
490
|
-
console.log(chalk.gray(isVi
|
|
491
|
-
? ` → Link khac co the mo duoc: ${gatewayUrls.slice(1).join(' , ')}`
|
|
492
|
-
: ` → Other reachable URLs: ${gatewayUrls.slice(1).join(' , ')}`));
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
if (tokenizedUrl) {
|
|
496
|
-
const tokenizedUrls = gatewayUrls.map((baseUrl) => rewriteDashboardUrlHost(tokenizedUrl, gatewayPort, baseUrl));
|
|
497
|
-
console.log(chalk.green(isVi
|
|
498
|
-
? ` → Mở link đã kèm token: ${tokenizedUrls[0]}`
|
|
499
|
-
: ` → Open the tokenized link directly: ${tokenizedUrls[0]}`));
|
|
500
|
-
if (tokenizedUrls.length > 1) {
|
|
501
|
-
console.log(chalk.gray(isVi
|
|
502
|
-
? ` → Ban mo tu may khac/WSL thi thu: ${tokenizedUrls.slice(1).join(' , ')}`
|
|
503
|
-
: ` → If you are opening from another machine/WSL, try: ${tokenizedUrls.slice(1).join(' , ')}`));
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
483
|
+
function printNativeDashboardAccessInfo({ isVi, providerKey, projectDir, gatewayPort = 18791 }) {
|
|
484
|
+
const gatewayUrls = getReachableDashboardHosts(gatewayPort);
|
|
485
|
+
const dashboardUrl = gatewayUrls[0] || `http://127.0.0.1:${gatewayPort}`;
|
|
486
|
+
const tokenizedUrl = getTokenizedDashboardUrl(projectDir);
|
|
487
|
+
|
|
488
|
+
console.log(chalk.yellow(`\n🧭 ${isVi ? 'Dashboard OpenClaw:' : 'OpenClaw Dashboard:'} ${dashboardUrl}`));
|
|
489
|
+
if (gatewayUrls.length > 1) {
|
|
490
|
+
console.log(chalk.gray(isVi
|
|
491
|
+
? ` → Link khac co the mo duoc: ${gatewayUrls.slice(1).join(' , ')}`
|
|
492
|
+
: ` → Other reachable URLs: ${gatewayUrls.slice(1).join(' , ')}`));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (tokenizedUrl) {
|
|
496
|
+
const tokenizedUrls = gatewayUrls.map((baseUrl) => rewriteDashboardUrlHost(tokenizedUrl, gatewayPort, baseUrl));
|
|
497
|
+
console.log(chalk.green(isVi
|
|
498
|
+
? ` → Mở link đã kèm token: ${tokenizedUrls[0]}`
|
|
499
|
+
: ` → Open the tokenized link directly: ${tokenizedUrls[0]}`));
|
|
500
|
+
if (tokenizedUrls.length > 1) {
|
|
501
|
+
console.log(chalk.gray(isVi
|
|
502
|
+
? ` → Ban mo tu may khac/WSL thi thu: ${tokenizedUrls.slice(1).join(' , ')}`
|
|
503
|
+
: ` → If you are opening from another machine/WSL, try: ${tokenizedUrls.slice(1).join(' , ')}`));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const externalHosts = gatewayUrls.filter(u => !u.includes('localhost') && !u.includes('127.0.0.1')).map(u => {
|
|
507
|
+
try { return new URL(u).hostname; } catch { return u; }
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
if (externalHosts.length > 0) {
|
|
511
|
+
const mainIp = externalHosts[0];
|
|
512
|
+
const username = process.env.USER || 'user';
|
|
513
|
+
console.log(chalk.cyan(`\n🔐 ${isVi ? 'Bảo mật WebCrypto (Sửa Lỗi 1008)' : 'WebCrypto Security (Fix Error 1008)'}`));
|
|
514
|
+
console.log(chalk.gray(isVi
|
|
515
|
+
? ` Nếu dùng các link IP ngoài (như ${mainIp}) bị lỗi từ chối kết nối WebCrypto (mã 1008):`
|
|
516
|
+
: ` If non-localhost IPs (like ${mainIp}) block WebCrypto connections with Error 1008:`));
|
|
517
|
+
console.log(chalk.white(isVi
|
|
518
|
+
? ` 1. Trên WSL/Máy nội bộ: Bạn CHỈ CẦN mở link http://127.0.0.1:${gatewayPort} là vào được.`
|
|
519
|
+
: ` 1. On WSL/Local Network: Use the http://127.0.0.1:${gatewayPort} link directly.`));
|
|
520
|
+
console.log(chalk.white(isVi
|
|
521
|
+
? ` 2. Trên VPS Xa: Mở tab Terminal khác ở MÁY CỦA BẠN (Windows/Mac) chạy lệnh SSH Tunnel sau:`
|
|
522
|
+
: ` 2. On Remote VPS: Run this SSH Tunnel command on YOUR LOCAL COMPUTER (Windows/Mac):`));
|
|
523
|
+
console.log(chalk.bgBlack.white(` ssh -L ${gatewayPort}:localhost:${gatewayPort} ${username}@${mainIp} `));
|
|
524
|
+
console.log(chalk.gray(isVi
|
|
525
|
+
? ` Rồi quay lại trình duyệt mở link http://127.0.0.1:${gatewayPort}/#token=... là xong!`
|
|
526
|
+
: ` Then open the http://127.0.0.1:${gatewayPort}/#token=... link in your browser!`));
|
|
527
|
+
}
|
|
528
|
+
} else {
|
|
529
|
+
console.log(chalk.gray(isVi
|
|
530
|
+
? ' → Nếu dashboard đòi Gateway Token, chạy: openclaw dashboard'
|
|
531
|
+
: ' → If the dashboard asks for a Gateway Token, run: openclaw dashboard'));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (providerKey === '9router') {
|
|
535
|
+
const routerUrls = getReachableDashboardHosts(20128).map((baseUrl) => `${baseUrl}/dashboard`);
|
|
536
|
+
console.log(chalk.yellow(`\n🔀 ${isVi ? '9Router Dashboard:' : '9Router Dashboard:'} ${routerUrls[0] || 'http://127.0.0.1:20128/dashboard'}`));
|
|
537
|
+
if (routerUrls.length > 1) {
|
|
538
|
+
console.log(chalk.gray(isVi
|
|
539
|
+
? ` → Link khac co the mo duoc: ${routerUrls.slice(1).join(' , ')}`
|
|
540
|
+
: ` → Other reachable URLs: ${routerUrls.slice(1).join(' , ')}`));
|
|
541
|
+
}
|
|
542
|
+
console.log(chalk.gray(isVi
|
|
543
|
+
? ' → Mở dashboard 9Router → đăng nhập OAuth → kết nối provider miễn phí'
|
|
544
|
+
: ' → Open the 9Router dashboard → complete OAuth login → connect a free provider'));
|
|
522
545
|
console.log(chalk.gray(isVi
|
|
523
546
|
? ' → Sau khi login 9Router xong, bot sẽ tự dùng model smart-route qua http://localhost:20128/v1'
|
|
524
547
|
: ' → Once 9Router is logged in, the bot will use smart-route through http://localhost:20128/v1'));
|
|
@@ -695,62 +718,62 @@ function runPm2Save({ projectDir, isVi }) {
|
|
|
695
718
|
}
|
|
696
719
|
}
|
|
697
720
|
|
|
698
|
-
function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
|
|
699
|
-
const routerAppName = `${appName}-9router`;
|
|
700
|
-
const routerLaunch = resolveNative9RouterDesktopLaunch();
|
|
701
|
-
execFileSync('pm2', [
|
|
702
|
-
'start',
|
|
703
|
-
routerLaunch.command,
|
|
704
|
-
'--name',
|
|
705
|
-
routerAppName,
|
|
706
|
-
'--cwd',
|
|
707
|
-
projectDir.replace(/\\/g, '/'),
|
|
708
|
-
'--interpreter',
|
|
709
|
-
'none',
|
|
710
|
-
'--',
|
|
711
|
-
...routerLaunch.args
|
|
712
|
-
], {
|
|
713
|
-
cwd: projectDir,
|
|
714
|
-
stdio: 'inherit',
|
|
715
|
-
env: { ...process.env, ...routerLaunch.env }
|
|
716
|
-
});
|
|
717
|
-
if (syncScriptPath) {
|
|
718
|
-
const syncAppName = `${appName}-9router-sync`;
|
|
719
|
-
const normalizedSyncScriptPath = syncScriptPath.replace(/\\/g, '/');
|
|
720
|
-
try {
|
|
721
|
-
execFileSync('pm2', [
|
|
722
|
-
'start',
|
|
723
|
-
normalizedSyncScriptPath,
|
|
724
|
-
'--name',
|
|
725
|
-
syncAppName,
|
|
726
|
-
'--cwd',
|
|
727
|
-
projectDir.replace(/\\/g, '/'),
|
|
728
|
-
'--interpreter',
|
|
729
|
-
process.execPath
|
|
730
|
-
], {
|
|
731
|
-
cwd: projectDir,
|
|
732
|
-
stdio: 'inherit',
|
|
733
|
-
env: process.env
|
|
734
|
-
});
|
|
735
|
-
} catch {
|
|
736
|
-
try {
|
|
737
|
-
execSync(`nohup "${process.execPath}" "${normalizedSyncScriptPath}" >/tmp/${syncAppName}.log 2>&1 &`, {
|
|
738
|
-
cwd: projectDir,
|
|
739
|
-
stdio: 'ignore',
|
|
740
|
-
shell: true,
|
|
741
|
-
env: process.env
|
|
742
|
-
});
|
|
743
|
-
console.log(chalk.yellow(isVi
|
|
744
|
-
? `⚠️ PM2 khong khoi dong duoc sync helper. Da fallback sang background node: /tmp/${syncAppName}.log`
|
|
745
|
-
: `⚠️ PM2 could not start the sync helper. Fell back to a background node process: /tmp/${syncAppName}.log`));
|
|
746
|
-
} catch {
|
|
747
|
-
console.log(chalk.yellow(isVi
|
|
748
|
-
? `⚠️ Khong the khoi dong 9Router sync helper. 9Router van chay, nhung smart-route co the can dong bo thu cong sau.`
|
|
749
|
-
: `⚠️ Could not start the 9Router sync helper. 9Router is still running, but smart-route may need manual syncing later.`));
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
runPm2Save({ projectDir, isVi });
|
|
721
|
+
function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
|
|
722
|
+
const routerAppName = `${appName}-9router`;
|
|
723
|
+
const routerLaunch = resolveNative9RouterDesktopLaunch();
|
|
724
|
+
execFileSync('pm2', [
|
|
725
|
+
'start',
|
|
726
|
+
routerLaunch.command,
|
|
727
|
+
'--name',
|
|
728
|
+
routerAppName,
|
|
729
|
+
'--cwd',
|
|
730
|
+
projectDir.replace(/\\/g, '/'),
|
|
731
|
+
'--interpreter',
|
|
732
|
+
'none',
|
|
733
|
+
'--',
|
|
734
|
+
...routerLaunch.args
|
|
735
|
+
], {
|
|
736
|
+
cwd: projectDir,
|
|
737
|
+
stdio: 'inherit',
|
|
738
|
+
env: { ...process.env, ...routerLaunch.env }
|
|
739
|
+
});
|
|
740
|
+
if (syncScriptPath) {
|
|
741
|
+
const syncAppName = `${appName}-9router-sync`;
|
|
742
|
+
const normalizedSyncScriptPath = syncScriptPath.replace(/\\/g, '/');
|
|
743
|
+
try {
|
|
744
|
+
execFileSync('pm2', [
|
|
745
|
+
'start',
|
|
746
|
+
normalizedSyncScriptPath,
|
|
747
|
+
'--name',
|
|
748
|
+
syncAppName,
|
|
749
|
+
'--cwd',
|
|
750
|
+
projectDir.replace(/\\/g, '/'),
|
|
751
|
+
'--interpreter',
|
|
752
|
+
process.execPath
|
|
753
|
+
], {
|
|
754
|
+
cwd: projectDir,
|
|
755
|
+
stdio: 'inherit',
|
|
756
|
+
env: process.env
|
|
757
|
+
});
|
|
758
|
+
} catch {
|
|
759
|
+
try {
|
|
760
|
+
execSync(`nohup "${process.execPath}" "${normalizedSyncScriptPath}" >/tmp/${syncAppName}.log 2>&1 &`, {
|
|
761
|
+
cwd: projectDir,
|
|
762
|
+
stdio: 'ignore',
|
|
763
|
+
shell: true,
|
|
764
|
+
env: process.env
|
|
765
|
+
});
|
|
766
|
+
console.log(chalk.yellow(isVi
|
|
767
|
+
? `⚠️ PM2 khong khoi dong duoc sync helper. Da fallback sang background node: /tmp/${syncAppName}.log`
|
|
768
|
+
: `⚠️ PM2 could not start the sync helper. Fell back to a background node process: /tmp/${syncAppName}.log`));
|
|
769
|
+
} catch {
|
|
770
|
+
console.log(chalk.yellow(isVi
|
|
771
|
+
? `⚠️ Khong the khoi dong 9Router sync helper. 9Router van chay, nhung smart-route co the can dong bo thu cong sau.`
|
|
772
|
+
: `⚠️ Could not start the 9Router sync helper. 9Router is still running, but smart-route may need manual syncing later.`));
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
runPm2Save({ projectDir, isVi });
|
|
754
777
|
console.log(chalk.green(`\n✅ ${isVi ? '9Router da duoc khoi dong qua PM2.' : '9Router is running via PM2.'}`));
|
|
755
778
|
console.log(chalk.gray(isVi ? ` Xem log: pm2 logs ${routerAppName}` : ` View logs: pm2 logs ${routerAppName}`));
|
|
756
779
|
}
|
|
@@ -1339,7 +1362,7 @@ async function main() {
|
|
|
1339
1362
|
}
|
|
1340
1363
|
|
|
1341
1364
|
|
|
1342
|
-
const patchScript = `const fs=require('fs'),os=require('os'),p='/root/.openclaw/openclaw.json';if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const a=new Set(['http://localhost:18791','http://127.0.0.1:18791','http://0.0.0.0:18791']);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add(\`http://\${entry.address}:18791\`);}}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',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a)
|
|
1365
|
+
const patchScript = `const fs=require('fs'),os=require('os'),p='/root/.openclaw/openclaw.json';if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const a=new Set(['http://localhost:18791','http://127.0.0.1:18791','http://0.0.0.0:18791']);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add(\`http://\${entry.address}:18791\`);}}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',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a)})});fs.writeFileSync(p,JSON.stringify(c,null,2));}`;
|
|
1343
1366
|
const b64Patch = Buffer.from(patchScript).toString('base64');
|
|
1344
1367
|
|
|
1345
1368
|
// Browser Playwright (both desktop & server modes need chromium)
|
|
@@ -1382,7 +1405,7 @@ async function main() {
|
|
|
1382
1405
|
'# Fix chat.send dropping resolved agent timeout into reply pipeline.',
|
|
1383
1406
|
'# Without this, Telegram/WebChat paths fall back to an internal 300s default even when',
|
|
1384
1407
|
'# agents.defaults.timeoutSeconds is higher in config.',
|
|
1385
|
-
`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);}"`,
|
|
1408
|
+
`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);}"`,
|
|
1386
1409
|
'',
|
|
1387
1410
|
'WORKDIR /root/.openclaw',
|
|
1388
1411
|
'',
|
|
@@ -1398,12 +1421,12 @@ async function main() {
|
|
|
1398
1421
|
const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-$/, '') || 'chat';
|
|
1399
1422
|
|
|
1400
1423
|
// ─── Dynamic Smart Route Sync Script ────────────────────────────────────────
|
|
1401
|
-
// This script runs inside the 9Router container as a background loop.
|
|
1402
|
-
// It reads the persisted 9Router DB directly so smart-route still works
|
|
1403
|
-
// even when newer dashboard APIs require auth or change response shape.
|
|
1404
|
-
const syncComboScript = build9RouterSmartRouteSyncScript('/root/.9router/db.json');
|
|
1405
|
-
const syncComboScriptBase64 = Buffer.from(syncComboScript).toString('base64');
|
|
1406
|
-
const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(syncComboScriptBase64);
|
|
1424
|
+
// This script runs inside the 9Router container as a background loop.
|
|
1425
|
+
// It reads the persisted 9Router DB directly so smart-route still works
|
|
1426
|
+
// even when newer dashboard APIs require auth or change response shape.
|
|
1427
|
+
const syncComboScript = build9RouterSmartRouteSyncScript('/root/.9router/db.json');
|
|
1428
|
+
const syncComboScriptBase64 = Buffer.from(syncComboScript).toString('base64');
|
|
1429
|
+
const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(syncComboScriptBase64);
|
|
1407
1430
|
|
|
1408
1431
|
// ─── Resolve primary model ───────────────────────────────────────────────────
|
|
1409
1432
|
let modelsPrimary;
|
|
@@ -1447,14 +1470,14 @@ ${dependsOn}${extraHosts} ports:
|
|
|
1447
1470
|
image: node:22-slim
|
|
1448
1471
|
container_name: 9router-multibot
|
|
1449
1472
|
restart: always
|
|
1450
|
-
entrypoint:
|
|
1451
|
-
- /bin/sh
|
|
1452
|
-
- -c
|
|
1453
|
-
- |
|
|
1454
|
-
${indentBlock(docker9RouterEntrypointScript, 8)}
|
|
1455
|
-
environment:
|
|
1456
|
-
- PORT=20128
|
|
1457
|
-
- HOSTNAME=0.0.0.0
|
|
1473
|
+
entrypoint:
|
|
1474
|
+
- /bin/sh
|
|
1475
|
+
- -c
|
|
1476
|
+
- |
|
|
1477
|
+
${indentBlock(docker9RouterEntrypointScript, 8)}
|
|
1478
|
+
environment:
|
|
1479
|
+
- PORT=20128
|
|
1480
|
+
- HOSTNAME=0.0.0.0
|
|
1458
1481
|
- CI=true
|
|
1459
1482
|
volumes:
|
|
1460
1483
|
- 9router-data:/root/.9router
|
|
@@ -1539,14 +1562,14 @@ ${hasBrowserDesktop ? ` extra_hosts:\n - "host.docker.internal:host-gate
|
|
|
1539
1562
|
image: node:22-slim
|
|
1540
1563
|
container_name: 9router-${agentId}
|
|
1541
1564
|
restart: always
|
|
1542
|
-
entrypoint:
|
|
1543
|
-
- /bin/sh
|
|
1544
|
-
- -c
|
|
1545
|
-
- |
|
|
1546
|
-
${indentBlock(docker9RouterEntrypointScript, 8)}
|
|
1547
|
-
environment:
|
|
1548
|
-
- PORT=20128
|
|
1549
|
-
- HOSTNAME=0.0.0.0
|
|
1565
|
+
entrypoint:
|
|
1566
|
+
- /bin/sh
|
|
1567
|
+
- -c
|
|
1568
|
+
- |
|
|
1569
|
+
${indentBlock(docker9RouterEntrypointScript, 8)}
|
|
1570
|
+
environment:
|
|
1571
|
+
- PORT=20128
|
|
1572
|
+
- HOSTNAME=0.0.0.0
|
|
1550
1573
|
- CI=true
|
|
1551
1574
|
volumes:
|
|
1552
1575
|
- 9router-data:/root/.9router
|
|
@@ -1798,17 +1821,16 @@ ${hasBrowserDesktop ? ` extra_hosts:
|
|
|
1798
1821
|
allow: agentMetas.map((meta) => meta.agentId),
|
|
1799
1822
|
},
|
|
1800
1823
|
},
|
|
1801
|
-
gateway: {
|
|
1802
|
-
port: 18791,
|
|
1803
|
-
mode: 'local',
|
|
1804
|
-
bind: 'custom',
|
|
1805
|
-
customBindHost: '0.0.0.0',
|
|
1806
|
-
controlUi: {
|
|
1807
|
-
allowedOrigins: getGatewayAllowedOrigins(18791),
|
|
1808
|
-
|
|
1809
|
-
},
|
|
1810
|
-
|
|
1811
|
-
},
|
|
1824
|
+
gateway: {
|
|
1825
|
+
port: 18791,
|
|
1826
|
+
mode: 'local',
|
|
1827
|
+
bind: 'custom',
|
|
1828
|
+
customBindHost: '0.0.0.0',
|
|
1829
|
+
controlUi: {
|
|
1830
|
+
allowedOrigins: getGatewayAllowedOrigins(18791),
|
|
1831
|
+
},
|
|
1832
|
+
auth: { mode: 'token', token: 'cli-dummy-token-xyz123' },
|
|
1833
|
+
},
|
|
1812
1834
|
};
|
|
1813
1835
|
sharedConfig.plugins = {
|
|
1814
1836
|
entries: {
|
|
@@ -1847,6 +1869,16 @@ ${hasBrowserDesktop ? ` extra_hosts:
|
|
|
1847
1869
|
` autorestart: true,`,
|
|
1848
1870
|
` watch: false,`,
|
|
1849
1871
|
` env: { NODE_ENV: 'production' }`,
|
|
1872
|
+
' },',
|
|
1873
|
+
' {',
|
|
1874
|
+
` name: '${botName || 'openclaw-multibot'}-auto-approve',`,
|
|
1875
|
+
` script: 'sh',`,
|
|
1876
|
+
` args: '-c "while true; do npx --yes openclaw devices approve --latest 2>/dev/null || true; sleep 5; done"',`,
|
|
1877
|
+
` cwd: '${projectDir.replace(/\\/g, '/')}',`,
|
|
1878
|
+
` interpreter: 'none',`,
|
|
1879
|
+
` autorestart: true,`,
|
|
1880
|
+
` watch: false,`,
|
|
1881
|
+
` env: { NODE_ENV: 'production' }`,
|
|
1850
1882
|
' }',
|
|
1851
1883
|
].join('\n');
|
|
1852
1884
|
const ecosystemContent = [
|
|
@@ -2035,11 +2067,11 @@ ${hasBrowserDesktop ? ` extra_hosts:
|
|
|
2035
2067
|
commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
|
|
2036
2068
|
channels: {},
|
|
2037
2069
|
tools: { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } },
|
|
2038
|
-
gateway: {
|
|
2039
|
-
port: 18791 + (isMultiBot ? bIndex : 0), mode: 'local', bind: 'custom', customBindHost: '0.0.0.0',
|
|
2040
|
-
controlUi: { allowedOrigins: getGatewayAllowedOrigins(18791 + (isMultiBot ? bIndex : 0))
|
|
2041
|
-
auth: { mode: 'token', token: 'cli-dummy-token-xyz123' }
|
|
2042
|
-
}
|
|
2070
|
+
gateway: {
|
|
2071
|
+
port: 18791 + (isMultiBot ? bIndex : 0), mode: 'local', bind: 'custom', customBindHost: '0.0.0.0',
|
|
2072
|
+
controlUi: { allowedOrigins: getGatewayAllowedOrigins(18791 + (isMultiBot ? bIndex : 0)) },
|
|
2073
|
+
auth: { mode: 'token', token: 'cli-dummy-token-xyz123' }
|
|
2074
|
+
}
|
|
2043
2075
|
};
|
|
2044
2076
|
|
|
2045
2077
|
if (hasBrowserDesktop) {
|
|
@@ -2291,14 +2323,14 @@ fi
|
|
|
2291
2323
|
if (code === 0) {
|
|
2292
2324
|
console.log(chalk.green(`\n🎉 ${isVi ? 'Setup hoàn tất! Bot đang chạy.' : 'Setup complete! Bot is running.'}`));
|
|
2293
2325
|
|
|
2294
|
-
if (providerKey === '9router') {
|
|
2295
|
-
const routerDashboardUrl = `${getReachableDashboardHosts(20128)[0] || 'http://127.0.0.1:20128'}/dashboard`;
|
|
2296
|
-
console.log(chalk.yellow(`\n🔀 ${isVi
|
|
2297
|
-
? `9Router Dashboard: ${routerDashboardUrl}`
|
|
2298
|
-
: `9Router Dashboard: ${routerDashboardUrl}`}`));
|
|
2299
|
-
console.log(chalk.gray(isVi
|
|
2300
|
-
? ' → Mở dashboard → đăng nhập OAuth để kết nối các Provider (iFlow, Gemini CLI, Claude Code...)'
|
|
2301
|
-
: ' → Open dashboard → OAuth login to connect Providers (iFlow, Gemini CLI, Claude Code...)'));
|
|
2326
|
+
if (providerKey === '9router') {
|
|
2327
|
+
const routerDashboardUrl = `${getReachableDashboardHosts(20128)[0] || 'http://127.0.0.1:20128'}/dashboard`;
|
|
2328
|
+
console.log(chalk.yellow(`\n🔀 ${isVi
|
|
2329
|
+
? `9Router Dashboard: ${routerDashboardUrl}`
|
|
2330
|
+
: `9Router Dashboard: ${routerDashboardUrl}`}`));
|
|
2331
|
+
console.log(chalk.gray(isVi
|
|
2332
|
+
? ' → Mở dashboard → đăng nhập OAuth để kết nối các Provider (iFlow, Gemini CLI, Claude Code...)'
|
|
2333
|
+
: ' → Open dashboard → OAuth login to connect Providers (iFlow, Gemini CLI, Claude Code...)'));
|
|
2302
2334
|
console.log(chalk.gray(isVi
|
|
2303
2335
|
? ' → Sau khi kết nối provider, bot sẽ tự động hoạt động qua combo "smart-route"'
|
|
2304
2336
|
: ' → After connecting providers, bot works automatically via "smart-route" combo'));
|
package/package.json
CHANGED
package/setup.js
CHANGED
|
@@ -1597,7 +1597,6 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1597
1597
|
bind: '0.0.0.0',
|
|
1598
1598
|
controlUi: {
|
|
1599
1599
|
allowedOrigins: getGatewayAllowedOrigins(18791),
|
|
1600
|
-
requireDeviceIdentity: false,
|
|
1601
1600
|
},
|
|
1602
1601
|
auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
|
|
1603
1602
|
},
|
|
@@ -1833,7 +1832,7 @@ model:
|
|
|
1833
1832
|
? 'socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 & '
|
|
1834
1833
|
: '';
|
|
1835
1834
|
// Patch config on every startup to keep gateway settings stable
|
|
1836
|
-
const patchCmd = `node -e \\"const fs=require('fs'),os=require('os'),p='/root/.openclaw/openclaw.json';if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const a=new Set(['http://localhost:18791','http://127.0.0.1:18791','http://0.0.0.0:18791']);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add(\\\`http://\\\${entry.address}:18791\\\`);}}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',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a)
|
|
1835
|
+
const patchCmd = `node -e \\"const fs=require('fs'),os=require('os'),p='/root/.openclaw/openclaw.json';if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const a=new Set(['http://localhost:18791','http://127.0.0.1:18791','http://0.0.0.0:18791']);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add(\\\`http://\\\${entry.address}:18791\\\`);}}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',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a)})});fs.writeFileSync(p,JSON.stringify(c,null,2));}\\" && `;
|
|
1837
1836
|
// Auto-approve device pairing after gateway starts (required since v2026.3.x)
|
|
1838
1837
|
const autoApproveCmd = '(while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done) & ';
|
|
1839
1838
|
const finalCmd = `CMD sh -c "${pluginInstallCmd}${patchCmd}${browserPrefix}${autoApproveCmd}${gatewayCmd}"`;
|
|
@@ -3060,7 +3059,6 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
|
|
|
3060
3059
|
bind: '0.0.0.0',
|
|
3061
3060
|
controlUi: {
|
|
3062
3061
|
allowedOrigins: getGatewayAllowedOrigins(18791),
|
|
3063
|
-
requireDeviceIdentity: false,
|
|
3064
3062
|
},
|
|
3065
3063
|
auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
|
|
3066
3064
|
},
|
|
@@ -3177,7 +3175,6 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
|
|
|
3177
3175
|
bind: '0.0.0.0',
|
|
3178
3176
|
controlUi: {
|
|
3179
3177
|
allowedOrigins: getGatewayAllowedOrigins(basePort),
|
|
3180
|
-
requireDeviceIdentity: false,
|
|
3181
3178
|
},
|
|
3182
3179
|
auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
|
|
3183
3180
|
},
|