create-openclaw-bot 5.1.5 → 5.1.7

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 CHANGED
@@ -1,6 +1,22 @@
1
1
  # Changelog (English)
2
2
 
3
3
 
4
+ ## [5.1.7] — 2026-04-07
5
+
6
+ ### 🌟 Fix Control UI CORS & Native 9Router Path Resolution
7
+
8
+ - **Fix Control UI CORS Rejections**: OpenClaw v2026.3.x strict CORS policies blocked remote dashboard access. The setup configuration and Docker patching scripts now automatically resolve all active IPv4 interfaces (`os.networkInterfaces()`) alongside localhost to pre-populate the `gateway.controlUi.allowedOrigins` array. This ensures the Web UI works flawlessly out-of-the-box on remote VPS instances.
9
+ - **Improved Native PM2 Path Resolution**: To prevent PM2 `$PATH` lookup failures with `nvm` on Linux, the script now bypasses the OS `9router` binary wrapper entirely. Instead, it computes the exact explicit path using `$(npm root -g)/9router/app/server.js` and executes it directly via the NodeJS interpreter.
10
+
11
+
12
+ ## [5.1.6] — 2026-04-07
13
+
14
+ ### 🐞 Fix PM2 SIGKILL on Native VPS Installs
15
+
16
+ - **Fix `PM2 SIGKILL` Error**: Removed the `-t` (interactive TTY) flag from all background `9router` launches. This terminal-dependent flag could cause PM2 to hang and aggressively SIGKILL the spawned process on headless VPS environments.
17
+ - **Robust PM2 Sync Helper**: Added a two-stage fallback for the 9Router smart-route sync script. If PM2 encounters `SIGKILL` or memory limits while spawning the sync helper, the setup gracefully falls back to a background `nohup node ... &` process instead of throwing a hard exception. If both fail, it logs a warning but allows the overall OpenClaw setup to finish successfully.
18
+
19
+
4
20
  ## [5.1.5] — 2026-04-06
5
21
 
6
22
  ### 🐞 Fix Native PM2 9Router Startup
package/CHANGELOG.vi.md CHANGED
@@ -1,6 +1,22 @@
1
1
  # Changelog (Tiếng Việt)
2
2
 
3
3
 
4
+ ## [5.1.7] — 2026-04-07
5
+
6
+ ### 🌟 Sửa lỗi CORS Control UI & Đường dẫn 9Router Native
7
+
8
+ - **Sửa lỗi dội ngược CORS khi vào Control UI**: OpenClaw v2026.3.x siết chặt policy CORS khiến việc truy cập dashboard từ IP ngoài bị block. Các script tạo config và vá Docker giờ đã tự động quét toàn bộ IPv4 hiện có của server (`os.networkInterfaces()`) để nhúng vào mảng `gateway.controlUi.allowedOrigins`. Đảm bảo người dùng VPS vào được thẳng Control UI mà không bị lỗi mạng.
9
+ - **Tối ưu đường dẫn PM2 Native**: Để tránh trường hợp tính năng PM2 không nhận diện đúng môi trường (lỗi `\$PATH` khi dùng `nvm`), bộ cài giờ bỏ qua file thực thi `9router` của HĐH. Thay vào đó, bộ cài tự tính toán đường dẫn tuyệt đối `\$(npm root -g)/9router/app/server.js` và truyền thẳng vào trình thông dịch Node, đảm bảo PM2 100% tìm thấy file khởi chạy 9Router.
10
+
11
+
12
+ ## [5.1.6] — 2026-04-07
13
+
14
+ ### 🐞 Khắc phục lỗi PM2 ngắt cài đặt (SIGKILL) trên VPS
15
+
16
+ - **Sửa lỗi `PM2 SIGKILL`**: Loại bỏ cờ `-t` (chế độ giao diện terminal) khỏi tất cả các lệnh gọi `9router` chạy ngầm. Trên các VPS không giao diện (headless), cờ này có thể khiến PM2 bị treo và ném ra lỗi SIGKILL làm chết toàn bộ quá trình cài đặt.
17
+ - **Tối ưu Sync Helper chạy ngầm**: Bổ sung cơ chế dự phòng 2 lớp cho script tự động đồng bộ (sync helper). Nếu PM2 bị giới hạn RAM hoặc quá tải gây lỗi SIGKILL, script sẽ không văng lỗi sập Setup nữa mà tự động fallback xuống chạy ẩn bằng `nohup node ... &`. Trong trường hợp xấu nhất, bộ cài chỉ báo cảnh báo vàng và rẽ nhánh cho phép tiến trình Setup tiếp tục tới bước cuối cùng thành công.
18
+
19
+
4
20
  ## [5.1.5] — 2026-04-06
5
21
 
6
22
  ### 🐞 Sửa lỗi PM2 khởi động 9Router trên Native
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.5-0EA5E9?style=for-the-badge" alt="Version 5.1.5" /></a>
6
+ <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.1.7-0EA5E9?style=for-the-badge" alt="Version 5.1.7" /></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.5
27
+ ## 🆕 What's new in v5.1.7
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.5 for me.
115
+ Read SETUP.md and set up OpenClaw v5.1.7 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.5-0EA5E9?style=for-the-badge" alt="Version 5.1.5" /></a>
6
+ <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.1.7-0EA5E9?style=for-the-badge" alt="Version 5.1.7" /></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.5
27
+ ## 🆕 Có gì mới trong v5.1.7
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.5 for me.
115
+ Read SETUP.md and set up OpenClaw v5.1.7 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
@@ -145,50 +145,49 @@ function spawnBackgroundProcess(command, args, options = {}) {
145
145
  });
146
146
  }
147
147
 
148
- function resolveNative9RouterDesktopLaunch() {
149
- if (process.platform === 'win32') {
150
- const npmRoot = (() => {
151
- try {
152
- return execSync('npm root -g', {
153
- stdio: ['ignore', 'pipe', 'ignore'],
154
- encoding: 'utf8',
155
- shell: true,
156
- env: process.env
157
- }).trim();
158
- } catch {
159
- return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'npm', 'node_modules');
160
- }
161
- })();
162
-
163
- return {
164
- command: process.execPath,
165
- args: [path.join(npmRoot, '9router', 'app', 'server.js')],
166
- env: {
167
- PORT: '20128',
168
- HOSTNAME: '0.0.0.0'
169
- }
170
- };
171
- }
172
-
173
- return {
174
- command: '9router',
175
- args: ['-n', '-t', '-l', '-H', '0.0.0.0', '-p', '20128', '--skip-update'],
176
- env: {}
177
- };
178
- }
179
-
180
- function getNative9RouterDataDir() {
181
- if (process.platform === 'win32') {
182
- return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), '9router');
183
- }
184
-
185
- return path.join(os.homedir(), '.9router');
186
- }
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
+ }
187
158
 
188
- async function waitFor9RouterApiReady({ port = 20128, timeoutMs = 15000 } = {}) {
189
- const deadline = Date.now() + timeoutMs;
190
- const candidates = [
191
- `http://127.0.0.1:${port}/api/settings/require-login`,
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
+ async function waitFor9RouterApiReady({ port = 20128, timeoutMs = 15000 } = {}) {
188
+ const deadline = Date.now() + timeoutMs;
189
+ const candidates = [
190
+ `http://127.0.0.1:${port}/api/settings/require-login`,
192
191
  `http://127.0.0.1:${port}/api/version`
193
192
  ];
194
193
 
@@ -380,6 +379,22 @@ function resolveCommandOnPath(command) {
380
379
  }
381
380
  }
382
381
 
382
+ function getGlobalNpmRoot() {
383
+ try {
384
+ return execSync('npm root -g', {
385
+ stdio: ['ignore', 'pipe', 'ignore'],
386
+ encoding: 'utf8',
387
+ shell: true,
388
+ env: process.env
389
+ }).trim();
390
+ } catch {
391
+ if (process.platform === 'win32') {
392
+ return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'npm', 'node_modules');
393
+ }
394
+ return path.join(os.homedir(), '.local', 'lib', 'node_modules');
395
+ }
396
+ }
397
+
383
398
  function indentBlock(text, spaces) {
384
399
  const prefix = ' '.repeat(spaces);
385
400
  return String(text)
@@ -393,7 +408,7 @@ function build9RouterComposeEntrypointScript(syncScriptBase64) {
393
408
  'npm install -g 9router',
394
409
  `node -e "require('fs').writeFileSync('/tmp/sync.js',Buffer.from('${syncScriptBase64}','base64').toString())"`,
395
410
  'node /tmp/sync.js > /tmp/sync.log 2>&1 &',
396
- 'exec 9router -n -t -l -H 0.0.0.0 -p 20128 --skip-update'
411
+ 'exec 9router -n -l -H 0.0.0.0 -p 20128 --skip-update'
397
412
  ].join('\n');
398
413
  }
399
414
 
@@ -625,10 +640,10 @@ function runPm2Save({ projectDir, isVi }) {
625
640
 
626
641
  function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
627
642
  const routerAppName = `${appName}-9router`;
628
- const routerCommand = resolveCommandOnPath('9router');
643
+ const routerLaunch = resolveNative9RouterDesktopLaunch();
629
644
  execFileSync('pm2', [
630
645
  'start',
631
- routerCommand,
646
+ routerLaunch.command,
632
647
  '--name',
633
648
  routerAppName,
634
649
  '--cwd',
@@ -636,35 +651,47 @@ function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
636
651
  '--interpreter',
637
652
  'none',
638
653
  '--',
639
- '-n',
640
- '-t',
641
- '-l',
642
- '-H',
643
- '0.0.0.0',
644
- '-p',
645
- '20128',
646
- '--skip-update'
654
+ ...routerLaunch.args
647
655
  ], {
648
656
  cwd: projectDir,
649
657
  stdio: 'inherit',
650
- env: process.env
658
+ env: { ...process.env, ...routerLaunch.env }
651
659
  });
652
660
  if (syncScriptPath) {
653
661
  const syncAppName = `${appName}-9router-sync`;
654
- execFileSync('pm2', [
655
- 'start',
656
- syncScriptPath.replace(/\\/g, '/'),
657
- '--name',
658
- syncAppName,
659
- '--cwd',
660
- projectDir.replace(/\\/g, '/'),
661
- '--interpreter',
662
- process.execPath
663
- ], {
664
- cwd: projectDir,
665
- stdio: 'inherit',
666
- env: process.env
667
- });
662
+ const normalizedSyncScriptPath = syncScriptPath.replace(/\\/g, '/');
663
+ try {
664
+ execFileSync('pm2', [
665
+ 'start',
666
+ normalizedSyncScriptPath,
667
+ '--name',
668
+ syncAppName,
669
+ '--cwd',
670
+ projectDir.replace(/\\/g, '/'),
671
+ '--interpreter',
672
+ process.execPath
673
+ ], {
674
+ cwd: projectDir,
675
+ stdio: 'inherit',
676
+ env: process.env
677
+ });
678
+ } catch {
679
+ try {
680
+ execSync(`nohup "${process.execPath}" "${normalizedSyncScriptPath}" >/tmp/${syncAppName}.log 2>&1 &`, {
681
+ cwd: projectDir,
682
+ stdio: 'ignore',
683
+ shell: true,
684
+ env: process.env
685
+ });
686
+ console.log(chalk.yellow(isVi
687
+ ? `⚠️ PM2 khong khoi dong duoc sync helper. Da fallback sang background node: /tmp/${syncAppName}.log`
688
+ : `⚠️ PM2 could not start the sync helper. Fell back to a background node process: /tmp/${syncAppName}.log`));
689
+ } catch {
690
+ console.log(chalk.yellow(isVi
691
+ ? `⚠️ Khong the khoi dong 9Router sync helper. 9Router van chay, nhung smart-route co the can dong bo thu cong sau.`
692
+ : `⚠️ Could not start the 9Router sync helper. 9Router is still running, but smart-route may need manual syncing later.`));
693
+ }
694
+ }
668
695
  }
669
696
  runPm2Save({ projectDir, isVi });
670
697
  console.log(chalk.green(`\n✅ ${isVi ? '9Router da duoc khoi dong qua PM2.' : '9Router is running via PM2.'}`));
@@ -1255,7 +1282,7 @@ async function main() {
1255
1282
  }
1256
1283
 
1257
1284
 
1258
- const patchScript = `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));}`;
1285
+ 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));}`;
1259
1286
  const b64Patch = Buffer.from(patchScript).toString('base64');
1260
1287
 
1261
1288
  // Browser Playwright (both desktop & server modes need chromium)
@@ -1714,13 +1741,16 @@ ${hasBrowserDesktop ? ` extra_hosts:
1714
1741
  allow: agentMetas.map((meta) => meta.agentId),
1715
1742
  },
1716
1743
  },
1717
- gateway: {
1718
- port: 18791,
1719
- mode: 'local',
1720
- bind: 'custom',
1721
- customBindHost: '0.0.0.0',
1722
- auth: { mode: 'token', token: 'cli-dummy-token-xyz123' },
1723
- },
1744
+ gateway: {
1745
+ port: 18791,
1746
+ mode: 'local',
1747
+ bind: 'custom',
1748
+ customBindHost: '0.0.0.0',
1749
+ controlUi: {
1750
+ allowedOrigins: getGatewayAllowedOrigins(18791),
1751
+ },
1752
+ auth: { mode: 'token', token: 'cli-dummy-token-xyz123' },
1753
+ },
1724
1754
  };
1725
1755
  sharedConfig.plugins = {
1726
1756
  entries: {
@@ -1947,10 +1977,11 @@ ${hasBrowserDesktop ? ` extra_hosts:
1947
1977
  commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
1948
1978
  channels: {},
1949
1979
  tools: { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } },
1950
- gateway: {
1951
- port: 18791 + (isMultiBot ? bIndex : 0), mode: 'local', bind: 'custom', customBindHost: '0.0.0.0',
1952
- auth: { mode: 'token', token: 'cli-dummy-token-xyz123' }
1953
- }
1980
+ gateway: {
1981
+ port: 18791 + (isMultiBot ? bIndex : 0), mode: 'local', bind: 'custom', customBindHost: '0.0.0.0',
1982
+ controlUi: { allowedOrigins: getGatewayAllowedOrigins(18791 + (isMultiBot ? bIndex : 0)) },
1983
+ auth: { mode: 'token', token: 'cli-dummy-token-xyz123' }
1984
+ }
1954
1985
  };
1955
1986
 
1956
1987
  if (hasBrowserDesktop) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-openclaw-bot",
3
- "version": "5.1.5",
3
+ "version": "5.1.7",
4
4
  "description": "Interactive CLI installer for OpenClaw Bot",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/setup.js CHANGED
@@ -24,7 +24,7 @@
24
24
  };
25
25
 
26
26
  // ========== State ==========
27
- const state = {
27
+ const state = {
28
28
  currentStep: 1,
29
29
  totalSteps: 5,
30
30
  channel: null,
@@ -47,7 +47,21 @@
47
47
  apiKey: '',
48
48
  projectPath: '',
49
49
  },
50
- };
50
+ };
51
+
52
+ function getGatewayAllowedOrigins(port) {
53
+ const normalizedPort = Number(port) || 18791;
54
+ const origins = new Set([
55
+ `http://localhost:${normalizedPort}`,
56
+ `http://127.0.0.1:${normalizedPort}`,
57
+ `http://0.0.0.0:${normalizedPort}`,
58
+ ]);
59
+ const currentHost = (window.location && window.location.hostname) ? window.location.hostname.trim() : '';
60
+ if (currentHost) {
61
+ origins.add(`http://${currentHost}:${normalizedPort}`);
62
+ }
63
+ return Array.from(origins);
64
+ }
51
65
 
52
66
 
53
67
  // ========== AI Providers & Models ==========
@@ -1577,12 +1591,15 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
1577
1591
  commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
1578
1592
  channels: ch.channelConfig,
1579
1593
  tools: { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } },
1580
- gateway: {
1581
- port: 18791,
1582
- mode: 'local',
1583
- bind: '0.0.0.0',
1584
- auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
1585
- },
1594
+ gateway: {
1595
+ port: 18791,
1596
+ mode: 'local',
1597
+ bind: '0.0.0.0',
1598
+ controlUi: {
1599
+ allowedOrigins: getGatewayAllowedOrigins(18791),
1600
+ },
1601
+ auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
1602
+ },
1586
1603
  };
1587
1604
 
1588
1605
  // 9Router: add proxy endpoint config under models.providers
@@ -1767,7 +1784,7 @@ model:
1767
1784
  'npm install -g 9router',
1768
1785
  `node -e "require('fs').writeFileSync('/tmp/sync.js',Buffer.from('${syncScriptBase64}','base64').toString())"`,
1769
1786
  'node /tmp/sync.js > /tmp/sync.log 2>&1 &',
1770
- 'exec 9router -n -t -l -H 0.0.0.0 -p 20128 --skip-update'
1787
+ 'exec 9router -n -l -H 0.0.0.0 -p 20128 --skip-update'
1771
1788
  ].join('\n');
1772
1789
 
1773
1790
  state.config.plugins.forEach((pid) => {
@@ -1815,7 +1832,7 @@ model:
1815
1832
  ? 'socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 & '
1816
1833
  : '';
1817
1834
  // Patch config on every startup to keep gateway settings stable
1818
- 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));}\\" && `;
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));}\\" && `;
1819
1836
  // Auto-approve device pairing after gateway starts (required since v2026.3.x)
1820
1837
  const autoApproveCmd = '(while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done) & ';
1821
1838
  const finalCmd = `CMD sh -c "${pluginInstallCmd}${patchCmd}${browserPrefix}${autoApproveCmd}${gatewayCmd}"`;
@@ -2868,17 +2885,17 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
2868
2885
  // ─── Shared initializer (provider install) ───────────────────────────────
2869
2886
  function providerLines(arr, shell) {
2870
2887
  if (is9Router) {
2871
- if (shell === 'bat') {
2872
- arr.push('npm install -g 9router');
2873
- arr.push('start "9Router" cmd /k "9router -n -t -l -H 0.0.0.0 -p 20128 --skip-update"');
2874
- arr.push('start "9Router Smart Route Sync" cmd /k "node .\\.openclaw\\9router-smart-route-sync.js"');
2875
- arr.push('timeout /t 5 /nobreak >nul');
2876
- } else {
2877
- arr.push('npm install -g 9router');
2878
- arr.push('nohup 9router -n -t -l -H 0.0.0.0 -p 20128 --skip-update >/tmp/9router.log 2>&1 &');
2879
- arr.push('nohup node ./.openclaw/9router-smart-route-sync.js >/tmp/9router-sync.log 2>&1 &');
2880
- arr.push('sleep 3');
2881
- }
2888
+ if (shell === 'bat') {
2889
+ arr.push('npm install -g 9router');
2890
+ arr.push('start "9Router" cmd /k "9router -n -l -H 0.0.0.0 -p 20128 --skip-update"');
2891
+ arr.push('start "9Router Smart Route Sync" cmd /k "node .\\.openclaw\\9router-smart-route-sync.js"');
2892
+ arr.push('timeout /t 5 /nobreak >nul');
2893
+ } else {
2894
+ arr.push('npm install -g 9router');
2895
+ arr.push('nohup env PORT=20128 HOSTNAME=0.0.0.0 node "$(npm root -g)/9router/app/server.js" >/tmp/9router.log 2>&1 &');
2896
+ arr.push('nohup node ./.openclaw/9router-smart-route-sync.js >/tmp/9router-sync.log 2>&1 &');
2897
+ arr.push('sleep 3');
2898
+ }
2882
2899
  } else if (isOllama) {
2883
2900
  if (shell === 'bat') {
2884
2901
  arr.push('where ollama >nul 2>&1 || (powershell -Command "Invoke-WebRequest -Uri https://ollama.com/download/OllamaSetup.exe -OutFile OllamaSetup.exe" && OllamaSetup.exe && del OllamaSetup.exe)');
@@ -3036,12 +3053,15 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
3036
3053
  'telegram-multibot-relay': { enabled: true },
3037
3054
  },
3038
3055
  },
3039
- gateway: {
3040
- port: 18791,
3041
- mode: 'local',
3042
- bind: '0.0.0.0',
3043
- auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
3044
- },
3056
+ gateway: {
3057
+ port: 18791,
3058
+ mode: 'local',
3059
+ bind: '0.0.0.0',
3060
+ controlUi: {
3061
+ allowedOrigins: getGatewayAllowedOrigins(18791),
3062
+ },
3063
+ auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
3064
+ },
3045
3065
  };
3046
3066
  return JSON.stringify(cfg, null, 2);
3047
3067
  }
@@ -3149,12 +3169,15 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
3149
3169
  },
3150
3170
  commands: { native: 'auto', nativeSkills: 'auto', restart: true },
3151
3171
  channels: channelConfig,
3152
- gateway: {
3153
- port: basePort,
3154
- mode: 'local',
3155
- bind: '0.0.0.0',
3156
- auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
3157
- },
3172
+ gateway: {
3173
+ port: basePort,
3174
+ mode: 'local',
3175
+ bind: '0.0.0.0',
3176
+ controlUi: {
3177
+ allowedOrigins: getGatewayAllowedOrigins(basePort),
3178
+ },
3179
+ auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
3180
+ },
3158
3181
 
3159
3182
  };
3160
3183
  return JSON.stringify(cfg, null, 2);
@@ -3533,12 +3556,12 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3533
3556
 
3534
3557
  if (isMultiBot) {
3535
3558
  vps.push('echo "--- Creating shared multi-agent runtime ---"');
3536
- appendShWriteCommands(vps, sharedNativeFileMap());
3537
- vps.push('echo "--- Starting shared gateway via PM2 ---"');
3538
- if (is9Router) {
3539
- vps.push('pm2 start --name openclaw-multibot-9router -- sh -c "9router -n -t -l -H 0.0.0.0 -p 20128 --skip-update"');
3540
- vps.push('pm2 start --name openclaw-multibot-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
3541
- }
3559
+ appendShWriteCommands(vps, sharedNativeFileMap());
3560
+ vps.push('echo "--- Starting shared gateway via PM2 ---"');
3561
+ if (is9Router) {
3562
+ vps.push('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$(npm root -g)/9router/app/server.js" --name openclaw-multibot-9router --interpreter "$(command -v node)"');
3563
+ vps.push('pm2 start --name openclaw-multibot-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
3564
+ }
3542
3565
  vps.push('pm2 start --name openclaw-multibot -- sh -c "openclaw gateway run"');
3543
3566
  vps.push('pm2 save && pm2 startup');
3544
3567
  vps.push(`echo ""`);
@@ -3546,12 +3569,12 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3546
3569
  vps.push(`echo "Commands:"`);
3547
3570
  vps.push(`echo " pm2 status # Status gateway"`);
3548
3571
  vps.push(`echo " pm2 logs openclaw-multibot"`);
3549
- } else {
3550
- appendShWriteCommands(vps, botFiles(0));
3551
- if (is9Router) {
3552
- vps.push('pm2 start --name openclaw-9router -- sh -c "9router -n -t -l -H 0.0.0.0 -p 20128 --skip-update"');
3553
- vps.push('pm2 start --name openclaw-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
3554
- }
3572
+ } else {
3573
+ appendShWriteCommands(vps, botFiles(0));
3574
+ if (is9Router) {
3575
+ vps.push('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$(npm root -g)/9router/app/server.js" --name openclaw-9router --interpreter "$(command -v node)"');
3576
+ vps.push('pm2 start --name openclaw-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
3577
+ }
3555
3578
  vps.push('pm2 start --name openclaw -- sh -c "openclaw gateway run"');
3556
3579
  vps.push('pm2 save && pm2 startup');
3557
3580
  vps.push('echo "Bot dang chay! Xem log: pm2 logs openclaw"');
@@ -107,6 +107,15 @@ checks.push(() => expectMatch(
107
107
  'CLI must resolve the correct native 9Router data directory on both Windows and Unix'
108
108
  ));
109
109
 
110
+ checks.push(() => expect(
111
+ cli.includes('function getGatewayAllowedOrigins(port) {')
112
+ && cli.includes('Object.values(os.networkInterfaces() || {})')
113
+ && cli.includes('`http://localhost:${normalizedPort}`')
114
+ && cli.includes('`http://127.0.0.1:${normalizedPort}`')
115
+ && cli.includes('`http://0.0.0.0:${normalizedPort}`'),
116
+ 'CLI must derive control UI allowed origins from localhost plus non-internal IPv4 interfaces'
117
+ ));
118
+
110
119
  checks.push(() => expect(
111
120
  cli.includes("Removed smart-route (no active providers)")
112
121
  && cli.includes("if (!a.length) {")
@@ -187,6 +196,18 @@ checks.push(() => expectMatch(
187
196
  'Native 9Router config must target localhost instead of the Docker hostname'
188
197
  ));
189
198
 
199
+ checks.push(() => expectMatch(
200
+ cli,
201
+ /controlUi:\s*\{\s*allowedOrigins: getGatewayAllowedOrigins\(18791\)/s,
202
+ 'Native shared gateway config must seed control UI allowed origins'
203
+ ));
204
+
205
+ checks.push(() => expectMatch(
206
+ cli,
207
+ /controlUi:\s*\{\s*allowedOrigins: getGatewayAllowedOrigins\(18791 \+ \(isMultiBot \? bIndex : 0\)\)/s,
208
+ 'Native per-bot gateway config must seed control UI allowed origins for each port'
209
+ ));
210
+
190
211
  checks.push(() => expectMatch(
191
212
  cli,
192
213
  /channelKey === 'zalo-personal'\) \{\s*botConfig\.channels\['zalouser'\] = \{\s*enabled: true,\s*dmPolicy: 'pairing',\s*autoReply: true/s,
@@ -195,7 +216,7 @@ checks.push(() => expectMatch(
195
216
 
196
217
  checks.push(() => expectMatch(
197
218
  cli,
198
- /function startNative9RouterPm2\(\{ isVi, projectDir, appName, syncScriptPath \}\) \{[\s\S]*resolveCommandOnPath\('9router'\)[\s\S]*execFileSync\('pm2'[\s\S]*--interpreter'?,?[\s\S]*none[\s\S]*--skip-update[\s\S]*syncScriptPath\.replace\(\/\\\\\/g, '\/'\)[\s\S]*process\.execPath[\s\S]*runPm2Save\(\{ projectDir, isVi \}\)/s,
219
+ /function startNative9RouterPm2\(\{ isVi, projectDir, appName, syncScriptPath \}\) \{[\s\S]*resolveNative9RouterDesktopLaunch\(\)[\s\S]*execFileSync\('pm2'[\s\S]*routerLaunch\.command[\s\S]*--interpreter'?,?[\s\S]*none[\s\S]*routerLaunch\.args[\s\S]*routerLaunch\.env[\s\S]*nohup "\$\{process\.execPath\}" "\$\{normalizedSyncScriptPath\}" >\/tmp\/\$\{syncAppName\}\.log 2>&1 &[\s\S]*runPm2Save\(\{ projectDir, isVi \}\)/s,
199
220
  'VPS native 9Router flow must start a standalone 9Router dashboard on port 20128 via PM2'
200
221
  ));
201
222
 
@@ -207,8 +228,8 @@ checks.push(() => expectMatch(
207
228
 
208
229
  checks.push(() => expectMatch(
209
230
  cli,
210
- /function resolveNative9RouterDesktopLaunch\(\) \{[\s\S]*process\.platform === 'win32'[\s\S]*npm root -g[\s\S]*9router', 'app', 'server\.js'[\s\S]*PORT: '20128'[\s\S]*HOSTNAME: '0\.0\.0\.0'[\s\S]*command: '9router'[\s\S]*\['-n', '-t', '-l', '-H', '0\.0\.0\.0', '-p', '20128', '--skip-update'\]/s,
211
- 'Native desktop 9Router launch must bypass the interactive CLI menu on Windows while preserving the standard CLI launch elsewhere'
231
+ /function resolveNative9RouterDesktopLaunch\(\) \{[\s\S]*command: process\.execPath[\s\S]*getGlobalNpmRoot\(\), '9router', 'app', 'server\.js'[\s\S]*PORT: '20128'[\s\S]*HOSTNAME: '0\.0\.0\.0'/s,
232
+ 'Native desktop 9Router launch must bypass the interactive CLI menu by running the 9Router server entry directly'
212
233
  ));
213
234
 
214
235
  checks.push(() => expectMatch(
@@ -292,7 +313,7 @@ checks.push(() => expectMatch(
292
313
 
293
314
  checks.push(() => expectMatch(
294
315
  setup,
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,
316
+ /function providerLines\(arr, shell\) \{[\s\S]*npm install -g 9router[\s\S]*start "9Router" cmd \/k "9router -n -l -H 0\.0\.0\.0 -p 20128 --skip-update"[\s\S]*nohup env PORT=20128 HOSTNAME=0\.0\.0\.0 node "\$\(npm root -g\)\/9router\/app\/server\.js"[\s\S]*9router-smart-route-sync\.js/s,
296
317
  'Native script generation must install and start a standalone 9Router dashboard on port 20128'
297
318
  ));
298
319
 
@@ -340,13 +361,13 @@ checks.push(() => expect(
340
361
 
341
362
  checks.push(() => expectMatch(
342
363
  setup,
343
- /else if \(state\.nativeOs === 'vps'\) \{[\s\S]*pm2 start --name openclaw-multibot -- sh -c "openclaw gateway run"[\s\S]*pm2 logs openclaw-multibot/s,
364
+ /else if \(state\.nativeOs === 'vps'\) \{[\s\S]*PORT=20128 HOSTNAME=0\.0\.0\.0 pm2 start "\$\(npm root -g\)\/9router\/app\/server\.js" --name openclaw-multibot-9router --interpreter "\$\(command -v node\)"[\s\S]*pm2 start --name openclaw-multibot -- sh -c "openclaw gateway run"[\s\S]*pm2 logs openclaw-multibot/s,
344
365
  'VPS multi-bot native script must start the shared gateway via PM2'
345
366
  ));
346
367
 
347
368
  checks.push(() => expectMatch(
348
369
  setup,
349
- /else if \(state\.nativeOs === 'vps'\) \{[\s\S]*pm2 start --name openclaw -- sh -c "openclaw gateway run"[\s\S]*pm2 logs openclaw/s,
370
+ /else if \(state\.nativeOs === 'vps'\) \{[\s\S]*PORT=20128 HOSTNAME=0\.0\.0\.0 pm2 start "\$\(npm root -g\)\/9router\/app\/server\.js" --name openclaw-9router --interpreter "\$\(command -v node\)"[\s\S]*pm2 start --name openclaw -- sh -c "openclaw gateway run"[\s\S]*pm2 logs openclaw/s,
350
371
  'VPS single-bot native script must start one bot via PM2'
351
372
  ));
352
373
 
@@ -374,6 +395,32 @@ checks.push(() => expectMatch(
374
395
  'Wizard copy must mention native auto-login and still show the dedicated Docker QR login command'
375
396
  ));
376
397
 
398
+ checks.push(() => expect(
399
+ setup.includes('function getGatewayAllowedOrigins(port) {')
400
+ && setup.includes('window.location')
401
+ && setup.includes('`http://localhost:${normalizedPort}`')
402
+ && setup.includes('`http://127.0.0.1:${normalizedPort}`'),
403
+ 'Web wizard must expose a helper that seeds likely control UI origins'
404
+ ));
405
+
406
+ checks.push(() => expectMatch(
407
+ setup,
408
+ /controlUi:\s*\{\s*allowedOrigins: getGatewayAllowedOrigins\(18791\)/s,
409
+ 'Web wizard single-bot gateway config must seed control UI allowed origins'
410
+ ));
411
+
412
+ checks.push(() => expectMatch(
413
+ setup,
414
+ /controlUi:\s*\{\s*allowedOrigins: getGatewayAllowedOrigins\(basePort\)/s,
415
+ 'Web wizard per-bot gateway config must seed control UI allowed origins'
416
+ ));
417
+
418
+ checks.push(() => expectMatch(
419
+ setup,
420
+ /const patchCmd = `node -e \\\\"const fs=require\('fs'\),os=require\('os'\),p='\/root\/\.openclaw\/openclaw\.json';if\(fs\.existsSync\(p\)\)\{[\s\S]*allowedOrigins:Array\.from\(a\)/s,
421
+ 'Web wizard Docker patch command must add interface-based control UI allowed origins'
422
+ ));
423
+
377
424
  for (const check of checks) {
378
425
  check();
379
426
  }
package/tmp_diff.patch CHANGED
@@ -1,114 +1,363 @@
1
1
  diff --git a/cli.js b/cli.js
2
- index e0bbb01..4d1b591 100644
2
+ index 4b32aaf..4323e60 100644
3
3
  --- a/cli.js
4
4
  +++ b/cli.js
5
- @@ -5,7 +5,7 @@ import fs from 'fs-extra';
6
- import path from 'path';
7
- import os from 'os';
8
- import chalk from 'chalk';
9
- -import { spawn, execSync } from 'child_process';
10
- +import { spawn, execSync, execFileSync } from 'child_process';
11
- const TELEGRAM_RELAY_PLUGIN_ID = 'openclaw-telegram-multibot-relay';
12
- // Use plain npm package name ΓÇö clawhub: protocol not supported in all OpenClaw versions
13
- const TELEGRAM_RELAY_PLUGIN_SPEC = TELEGRAM_RELAY_PLUGIN_ID;
14
- @@ -363,6 +363,23 @@ setTimeout(sync, 5000);
15
- setInterval(sync, INTERVAL);`;
5
+ @@ -146,34 +146,13 @@ function spawnBackgroundProcess(command, args, options = {}) {
16
6
  }
17
7
 
18
- +function resolveCommandOnPath(command) {
19
- + if (process.platform === 'win32') {
20
- + return resolveWindowsCommand(command);
8
+ function resolveNative9RouterDesktopLaunch() {
9
+ - if (process.platform === 'win32') {
10
+ - const npmRoot = (() => {
11
+ - try {
12
+ - return execSync('npm root -g', {
13
+ - stdio: ['ignore', 'pipe', 'ignore'],
14
+ - encoding: 'utf8',
15
+ - shell: true,
16
+ - env: process.env
17
+ - }).trim();
18
+ - } catch {
19
+ - return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'npm', 'node_modules');
20
+ - }
21
+ - })();
22
+ -
23
+ - return {
24
+ - command: process.execPath,
25
+ - args: [path.join(npmRoot, '9router', 'app', 'server.js')],
26
+ - env: {
27
+ - PORT: '20128',
28
+ - HOSTNAME: '0.0.0.0'
29
+ - }
30
+ - };
31
+ - }
32
+ -
33
+ return {
34
+ - command: '9router',
35
+ - args: ['-n', '-l', '-H', '0.0.0.0', '-p', '20128', '--skip-update'],
36
+ - env: {}
37
+ + command: process.execPath,
38
+ + args: [path.join(getGlobalNpmRoot(), '9router', 'app', 'server.js')],
39
+ + env: {
40
+ + PORT: '20128',
41
+ + HOSTNAME: '0.0.0.0'
42
+ + }
43
+ };
44
+ }
45
+
46
+ @@ -185,6 +164,26 @@ function getNative9RouterDataDir() {
47
+ return path.join(os.homedir(), '.9router');
48
+ }
49
+
50
+ +function getGatewayAllowedOrigins(port) {
51
+ + const normalizedPort = Number(port) || 18791;
52
+ + const origins = new Set([
53
+ + `http://localhost:${normalizedPort}`,
54
+ + `http://127.0.0.1:${normalizedPort}`,
55
+ + `http://0.0.0.0:${normalizedPort}`
56
+ + ]);
57
+ +
58
+ + for (const entries of Object.values(os.networkInterfaces() || {})) {
59
+ + for (const entry of entries || []) {
60
+ + if (!entry || entry.internal || entry.family !== 'IPv4' || !entry.address) {
61
+ + continue;
62
+ + }
63
+ + origins.add(`http://${entry.address}:${normalizedPort}`);
64
+ + }
21
65
  + }
22
66
  +
67
+ + return Array.from(origins);
68
+ +}
69
+ +
70
+ async function waitFor9RouterApiReady({ port = 20128, timeoutMs = 15000 } = {}) {
71
+ const deadline = Date.now() + timeoutMs;
72
+ const candidates = [
73
+ @@ -380,6 +379,22 @@ function resolveCommandOnPath(command) {
74
+ }
75
+ }
76
+
77
+ +function getGlobalNpmRoot() {
23
78
  + try {
24
- + return execSync(`command -v ${command}`, {
79
+ + return execSync('npm root -g', {
25
80
  + stdio: ['ignore', 'pipe', 'ignore'],
26
81
  + encoding: 'utf8',
27
82
  + shell: true,
28
83
  + env: process.env
29
- + }).trim() || command;
84
+ + }).trim();
30
85
  + } catch {
31
- + return command;
86
+ + if (process.platform === 'win32') {
87
+ + return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'npm', 'node_modules');
88
+ + }
89
+ + return path.join(os.homedir(), '.local', 'lib', 'node_modules');
32
90
  + }
33
91
  +}
34
92
  +
35
93
  function indentBlock(text, spaces) {
36
94
  const prefix = ' '.repeat(spaces);
37
95
  return String(text)
38
- @@ -608,26 +625,46 @@ function runPm2Save({ projectDir, isVi }) {
96
+ @@ -625,10 +640,10 @@ function runPm2Save({ projectDir, isVi }) {
39
97
 
40
98
  function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
41
99
  const routerAppName = `${appName}-9router`;
42
- - execSync(
43
- - `pm2 start "9router -n -t -l -H 0.0.0.0 -p 20128 --skip-update" --name "${routerAppName}" --cwd "${projectDir.replace(/\\/g, '/')}"`,
44
- - {
45
- + const routerCommand = resolveCommandOnPath('9router');
46
- + execFileSync('pm2', [
47
- + 'start',
48
- + routerCommand,
49
- + '--name',
50
- + routerAppName,
51
- + '--cwd',
52
- + projectDir.replace(/\\/g, '/'),
53
- + '--interpreter',
54
- + 'none',
55
- + '--',
56
- + '-n',
57
- + '-t',
58
- + '-l',
59
- + '-H',
60
- + '0.0.0.0',
61
- + '-p',
62
- + '20128',
63
- + '--skip-update'
64
- + ], {
65
- + cwd: projectDir,
66
- + stdio: 'inherit',
67
- + env: process.env
68
- + });
69
- + if (syncScriptPath) {
70
- + const syncAppName = `${appName}-9router-sync`;
71
- + execFileSync('pm2', [
72
- + 'start',
73
- + syncScriptPath.replace(/\\/g, '/'),
74
- + '--name',
75
- + syncAppName,
76
- + '--cwd',
77
- + projectDir.replace(/\\/g, '/'),
78
- + '--interpreter',
79
- + process.execPath
80
- + ], {
81
- cwd: projectDir,
82
- stdio: 'inherit',
83
- - shell: true,
84
- env: process.env
85
- - }
86
- - );
87
- - if (syncScriptPath) {
88
- - const syncAppName = `${appName}-9router-sync`;
89
- - execSync(
90
- - `pm2 start "node ${syncScriptPath.replace(/\\/g, '/')}" --name "${syncAppName}" --cwd "${projectDir.replace(/\\/g, '/')}"`,
91
- - {
92
- - cwd: projectDir,
93
- - stdio: 'inherit',
94
- - shell: true,
95
- - env: process.env
96
- - }
97
- - );
98
- + });
100
+ - const routerCommand = resolveCommandOnPath('9router');
101
+ + const routerLaunch = resolveNative9RouterDesktopLaunch();
102
+ execFileSync('pm2', [
103
+ 'start',
104
+ - routerCommand,
105
+ + routerLaunch.command,
106
+ '--name',
107
+ routerAppName,
108
+ '--cwd',
109
+ @@ -636,17 +651,11 @@ function startNative9RouterPm2({ isVi, projectDir, appName, syncScriptPath }) {
110
+ '--interpreter',
111
+ 'none',
112
+ '--',
113
+ - '-n',
114
+ - '-l',
115
+ - '-H',
116
+ - '0.0.0.0',
117
+ - '-p',
118
+ - '20128',
119
+ - '--skip-update'
120
+ + ...routerLaunch.args
121
+ ], {
122
+ cwd: projectDir,
123
+ stdio: 'inherit',
124
+ - env: process.env
125
+ + env: { ...process.env, ...routerLaunch.env }
126
+ });
127
+ if (syncScriptPath) {
128
+ const syncAppName = `${appName}-9router-sync`;
129
+ @@ -1273,7 +1282,7 @@ async function main() {
99
130
  }
100
- runPm2Save({ projectDir, isVi });
101
- console.log(chalk.green(`\n✅ ${isVi ? '9Router da duoc khoi dong qua PM2.' : '9Router is running via PM2.'}`));
131
+
132
+
133
+ - const patchScript = `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));}`;
134
+ + 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));}`;
135
+ const b64Patch = Buffer.from(patchScript).toString('base64');
136
+
137
+ // Browser Playwright (both desktop & server modes need chromium)
138
+ @@ -1737,6 +1746,9 @@ ${hasBrowserDesktop ? ` extra_hosts:
139
+ mode: 'local',
140
+ bind: 'custom',
141
+ customBindHost: '0.0.0.0',
142
+ + controlUi: {
143
+ + allowedOrigins: getGatewayAllowedOrigins(18791),
144
+ + },
145
+ auth: { mode: 'token', token: 'cli-dummy-token-xyz123' },
146
+ },
147
+ };
148
+ @@ -1967,6 +1979,7 @@ ${hasBrowserDesktop ? ` extra_hosts:
149
+ tools: { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } },
150
+ gateway: {
151
+ port: 18791 + (isMultiBot ? bIndex : 0), mode: 'local', bind: 'custom', customBindHost: '0.0.0.0',
152
+ + controlUi: { allowedOrigins: getGatewayAllowedOrigins(18791 + (isMultiBot ? bIndex : 0)) },
153
+ auth: { mode: 'token', token: 'cli-dummy-token-xyz123' }
154
+ }
155
+ };
156
+ diff --git a/setup.js b/setup.js
157
+ index 885a34a..2a33996 100644
158
+ --- a/setup.js
159
+ +++ b/setup.js
160
+ @@ -49,6 +49,20 @@
161
+ },
162
+ };
163
+
164
+ + function getGatewayAllowedOrigins(port) {
165
+ + const normalizedPort = Number(port) || 18791;
166
+ + const origins = new Set([
167
+ + `http://localhost:${normalizedPort}`,
168
+ + `http://127.0.0.1:${normalizedPort}`,
169
+ + `http://0.0.0.0:${normalizedPort}`,
170
+ + ]);
171
+ + const currentHost = (window.location && window.location.hostname) ? window.location.hostname.trim() : '';
172
+ + if (currentHost) {
173
+ + origins.add(`http://${currentHost}:${normalizedPort}`);
174
+ + }
175
+ + return Array.from(origins);
176
+ + }
177
+ +
178
+
179
+ // ========== AI Providers & Models ==========
180
+ const PROVIDERS = {
181
+ @@ -1581,6 +1595,9 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
182
+ port: 18791,
183
+ mode: 'local',
184
+ bind: '0.0.0.0',
185
+ + controlUi: {
186
+ + allowedOrigins: getGatewayAllowedOrigins(18791),
187
+ + },
188
+ auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
189
+ },
190
+ };
191
+ @@ -1815,7 +1832,7 @@ model:
192
+ ? 'socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 & '
193
+ : '';
194
+ // Patch config on every startup to keep gateway settings stable
195
+ - 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));}\\" && `;
196
+ + 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));}\\" && `;
197
+ // Auto-approve device pairing after gateway starts (required since v2026.3.x)
198
+ const autoApproveCmd = '(while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done) & ';
199
+ const finalCmd = `CMD sh -c "${pluginInstallCmd}${patchCmd}${browserPrefix}${autoApproveCmd}${gatewayCmd}"`;
200
+ @@ -2875,7 +2892,7 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
201
+ arr.push('timeout /t 5 /nobreak >nul');
202
+ } else {
203
+ arr.push('npm install -g 9router');
204
+ - arr.push('nohup 9router -n -l -H 0.0.0.0 -p 20128 --skip-update >/tmp/9router.log 2>&1 &');
205
+ + arr.push('nohup env PORT=20128 HOSTNAME=0.0.0.0 node "$(npm root -g)/9router/app/server.js" >/tmp/9router.log 2>&1 &');
206
+ arr.push('nohup node ./.openclaw/9router-smart-route-sync.js >/tmp/9router-sync.log 2>&1 &');
207
+ arr.push('sleep 3');
208
+ }
209
+ @@ -3040,6 +3057,9 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
210
+ port: 18791,
211
+ mode: 'local',
212
+ bind: '0.0.0.0',
213
+ + controlUi: {
214
+ + allowedOrigins: getGatewayAllowedOrigins(18791),
215
+ + },
216
+ auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
217
+ },
218
+ };
219
+ @@ -3153,6 +3173,9 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
220
+ port: basePort,
221
+ mode: 'local',
222
+ bind: '0.0.0.0',
223
+ + controlUi: {
224
+ + allowedOrigins: getGatewayAllowedOrigins(basePort),
225
+ + },
226
+ auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
227
+ },
228
+
229
+ @@ -3536,7 +3559,7 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
230
+ appendShWriteCommands(vps, sharedNativeFileMap());
231
+ vps.push('echo "--- Starting shared gateway via PM2 ---"');
232
+ if (is9Router) {
233
+ - vps.push('pm2 start --name openclaw-multibot-9router -- sh -c "9router -n -l -H 0.0.0.0 -p 20128 --skip-update"');
234
+ + vps.push('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$(npm root -g)/9router/app/server.js" --name openclaw-multibot-9router --interpreter "$(command -v node)"');
235
+ vps.push('pm2 start --name openclaw-multibot-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
236
+ }
237
+ vps.push('pm2 start --name openclaw-multibot -- sh -c "openclaw gateway run"');
238
+ @@ -3549,7 +3572,7 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
239
+ } else {
240
+ appendShWriteCommands(vps, botFiles(0));
241
+ if (is9Router) {
242
+ - vps.push('pm2 start --name openclaw-9router -- sh -c "9router -n -l -H 0.0.0.0 -p 20128 --skip-update"');
243
+ + vps.push('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$(npm root -g)/9router/app/server.js" --name openclaw-9router --interpreter "$(command -v node)"');
244
+ vps.push('pm2 start --name openclaw-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
245
+ }
246
+ vps.push('pm2 start --name openclaw -- sh -c "openclaw gateway run"');
102
247
  diff --git a/tests/smoke-cli-logic.mjs b/tests/smoke-cli-logic.mjs
103
- index 3ab996c..7fd80f8 100644
248
+ index 23c1982..0827726 100644
104
249
  --- a/tests/smoke-cli-logic.mjs
105
250
  +++ b/tests/smoke-cli-logic.mjs
106
- @@ -195,7 +195,7 @@ checks.push(() => expectMatch(
251
+ @@ -107,6 +107,15 @@ checks.push(() => expectMatch(
252
+ 'CLI must resolve the correct native 9Router data directory on both Windows and Unix'
253
+ ));
254
+
255
+ +checks.push(() => expect(
256
+ + cli.includes('function getGatewayAllowedOrigins(port) {')
257
+ + && cli.includes('Object.values(os.networkInterfaces() || {})')
258
+ + && cli.includes('`http://localhost:${normalizedPort}`')
259
+ + && cli.includes('`http://127.0.0.1:${normalizedPort}`')
260
+ + && cli.includes('`http://0.0.0.0:${normalizedPort}`'),
261
+ + 'CLI must derive control UI allowed origins from localhost plus non-internal IPv4 interfaces'
262
+ +));
263
+ +
264
+ checks.push(() => expect(
265
+ cli.includes("Removed smart-route (no active providers)")
266
+ && cli.includes("if (!a.length) {")
267
+ @@ -187,6 +196,18 @@ checks.push(() => expectMatch(
268
+ 'Native 9Router config must target localhost instead of the Docker hostname'
269
+ ));
270
+
271
+ +checks.push(() => expectMatch(
272
+ + cli,
273
+ + /controlUi:\s*\{\s*allowedOrigins: getGatewayAllowedOrigins\(18791\)/s,
274
+ + 'Native shared gateway config must seed control UI allowed origins'
275
+ +));
276
+ +
277
+ +checks.push(() => expectMatch(
278
+ + cli,
279
+ + /controlUi:\s*\{\s*allowedOrigins: getGatewayAllowedOrigins\(18791 \+ \(isMultiBot \? bIndex : 0\)\)/s,
280
+ + 'Native per-bot gateway config must seed control UI allowed origins for each port'
281
+ +));
282
+ +
283
+ checks.push(() => expectMatch(
284
+ cli,
285
+ /channelKey === 'zalo-personal'\) \{\s*botConfig\.channels\['zalouser'\] = \{\s*enabled: true,\s*dmPolicy: 'pairing',\s*autoReply: true/s,
286
+ @@ -195,7 +216,7 @@ checks.push(() => expectMatch(
107
287
 
108
288
  checks.push(() => expectMatch(
109
289
  cli,
110
- - /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,
111
- + /function startNative9RouterPm2\(\{ isVi, projectDir, appName, syncScriptPath \}\) \{[\s\S]*resolveCommandOnPath\('9router'\)[\s\S]*execFileSync\('pm2'[\s\S]*--interpreter'?,?[\s\S]*none[\s\S]*--skip-update[\s\S]*syncScriptPath\.replace\(\/\\\\\/g, '\/'\)[\s\S]*process\.execPath[\s\S]*runPm2Save\(\{ projectDir, isVi \}\)/s,
290
+ - /function startNative9RouterPm2\(\{ isVi, projectDir, appName, syncScriptPath \}\) \{[\s\S]*resolveCommandOnPath\('9router'\)[\s\S]*execFileSync\('pm2'[\s\S]*--interpreter'?,?[\s\S]*none[\s\S]*'-n'[\s\S]*'-l'[\s\S]*'--skip-update'[\s\S]*nohup "\$\{process\.execPath\}" "\$\{normalizedSyncScriptPath\}" >\/tmp\/\$\{syncAppName\}\.log 2>&1 &[\s\S]*runPm2Save\(\{ projectDir, isVi \}\)/s,
291
+ + /function startNative9RouterPm2\(\{ isVi, projectDir, appName, syncScriptPath \}\) \{[\s\S]*resolveNative9RouterDesktopLaunch\(\)[\s\S]*execFileSync\('pm2'[\s\S]*routerLaunch\.command[\s\S]*--interpreter'?,?[\s\S]*none[\s\S]*routerLaunch\.args[\s\S]*routerLaunch\.env[\s\S]*nohup "\$\{process\.execPath\}" "\$\{normalizedSyncScriptPath\}" >\/tmp\/\$\{syncAppName\}\.log 2>&1 &[\s\S]*runPm2Save\(\{ projectDir, isVi \}\)/s,
112
292
  'VPS native 9Router flow must start a standalone 9Router dashboard on port 20128 via PM2'
113
293
  ));
114
294
 
295
+ @@ -207,8 +228,8 @@ checks.push(() => expectMatch(
296
+
297
+ checks.push(() => expectMatch(
298
+ cli,
299
+ - /function resolveNative9RouterDesktopLaunch\(\) \{[\s\S]*process\.platform === 'win32'[\s\S]*npm root -g[\s\S]*9router', 'app', 'server\.js'[\s\S]*PORT: '20128'[\s\S]*HOSTNAME: '0\.0\.0\.0'[\s\S]*command: '9router'[\s\S]*\['-n', '-l', '-H', '0\.0\.0\.0', '-p', '20128', '--skip-update'\]/s,
300
+ - 'Native desktop 9Router launch must bypass the interactive CLI menu on Windows while preserving the standard CLI launch elsewhere'
301
+ + /function resolveNative9RouterDesktopLaunch\(\) \{[\s\S]*command: process\.execPath[\s\S]*getGlobalNpmRoot\(\), '9router', 'app', 'server\.js'[\s\S]*PORT: '20128'[\s\S]*HOSTNAME: '0\.0\.0\.0'/s,
302
+ + 'Native desktop 9Router launch must bypass the interactive CLI menu by running the 9Router server entry directly'
303
+ ));
304
+
305
+ checks.push(() => expectMatch(
306
+ @@ -292,7 +313,7 @@ checks.push(() => expectMatch(
307
+
308
+ checks.push(() => expectMatch(
309
+ setup,
310
+ - /function providerLines\(arr, shell\) \{[\s\S]*npm install -g 9router[\s\S]*start "9Router" cmd \/k "9router -n -l -H 0\.0\.0\.0 -p 20128 --skip-update"[\s\S]*nohup 9router -n -l -H 0\.0\.0\.0 -p 20128 --skip-update[\s\S]*9router-smart-route-sync\.js/s,
311
+ + /function providerLines\(arr, shell\) \{[\s\S]*npm install -g 9router[\s\S]*start "9Router" cmd \/k "9router -n -l -H 0\.0\.0\.0 -p 20128 --skip-update"[\s\S]*nohup env PORT=20128 HOSTNAME=0\.0\.0\.0 node "\$\(npm root -g\)\/9router\/app\/server\.js"[\s\S]*9router-smart-route-sync\.js/s,
312
+ 'Native script generation must install and start a standalone 9Router dashboard on port 20128'
313
+ ));
314
+
315
+ @@ -340,13 +361,13 @@ checks.push(() => expect(
316
+
317
+ checks.push(() => expectMatch(
318
+ setup,
319
+ - /else if \(state\.nativeOs === 'vps'\) \{[\s\S]*pm2 start --name openclaw-multibot -- sh -c "openclaw gateway run"[\s\S]*pm2 logs openclaw-multibot/s,
320
+ + /else if \(state\.nativeOs === 'vps'\) \{[\s\S]*PORT=20128 HOSTNAME=0\.0\.0\.0 pm2 start "\$\(npm root -g\)\/9router\/app\/server\.js" --name openclaw-multibot-9router --interpreter "\$\(command -v node\)"[\s\S]*pm2 start --name openclaw-multibot -- sh -c "openclaw gateway run"[\s\S]*pm2 logs openclaw-multibot/s,
321
+ 'VPS multi-bot native script must start the shared gateway via PM2'
322
+ ));
323
+
324
+ checks.push(() => expectMatch(
325
+ setup,
326
+ - /else if \(state\.nativeOs === 'vps'\) \{[\s\S]*pm2 start --name openclaw -- sh -c "openclaw gateway run"[\s\S]*pm2 logs openclaw/s,
327
+ + /else if \(state\.nativeOs === 'vps'\) \{[\s\S]*PORT=20128 HOSTNAME=0\.0\.0\.0 pm2 start "\$\(npm root -g\)\/9router\/app\/server\.js" --name openclaw-9router --interpreter "\$\(command -v node\)"[\s\S]*pm2 start --name openclaw -- sh -c "openclaw gateway run"[\s\S]*pm2 logs openclaw/s,
328
+ 'VPS single-bot native script must start one bot via PM2'
329
+ ));
330
+
331
+ @@ -374,6 +395,32 @@ checks.push(() => expectMatch(
332
+ 'Wizard copy must mention native auto-login and still show the dedicated Docker QR login command'
333
+ ));
334
+
335
+ +checks.push(() => expect(
336
+ + setup.includes('function getGatewayAllowedOrigins(port) {')
337
+ + && setup.includes('window.location')
338
+ + && setup.includes('`http://localhost:${normalizedPort}`')
339
+ + && setup.includes('`http://127.0.0.1:${normalizedPort}`'),
340
+ + 'Web wizard must expose a helper that seeds likely control UI origins'
341
+ +));
342
+ +
343
+ +checks.push(() => expectMatch(
344
+ + setup,
345
+ + /controlUi:\s*\{\s*allowedOrigins: getGatewayAllowedOrigins\(18791\)/s,
346
+ + 'Web wizard single-bot gateway config must seed control UI allowed origins'
347
+ +));
348
+ +
349
+ +checks.push(() => expectMatch(
350
+ + setup,
351
+ + /controlUi:\s*\{\s*allowedOrigins: getGatewayAllowedOrigins\(basePort\)/s,
352
+ + 'Web wizard per-bot gateway config must seed control UI allowed origins'
353
+ +));
354
+ +
355
+ +checks.push(() => expectMatch(
356
+ + setup,
357
+ + /const patchCmd = `node -e \\\\"const fs=require\('fs'\),os=require\('os'\),p='\/root\/\.openclaw\/openclaw\.json';if\(fs\.existsSync\(p\)\)\{[\s\S]*allowedOrigins:Array\.from\(a\)/s,
358
+ + 'Web wizard Docker patch command must add interface-based control UI allowed origins'
359
+ +));
360
+ +
361
+ for (const check of checks) {
362
+ check();
363
+ }